Today we’ll walk through a real CRM agent built on the Dedalus SDK. This agent:
- Searches X for accounts related to a target profile
- Uses web search as a fallback when X profiles are missing or incomplete
- Streams results into a clean, CRM‑ready profile
- Uses DAuth so your X API key is never exposed to the MCP server or Dedalus
You can use this pattern to:
- Qualify second‑degree leads by interest, activity, and relationships
- Map social graphs around a target customer or account
GitHub repo: dedalus-labs/web-crm-agent
Prerequisites: Install and Set Up the Dedalus SDK
Install dependencies
pip install dedalus-labs dedalus-mcp python-dotenv
Before you get started, create a Dedalus API key using the dashboard and set it as an environment variable in your .env file, and add the base URL needed for DAuth.
DEDALUS_API_KEY=your-api-keyDEDALUS_AS_URL=https://as.dedaluslabs.ai
You’ll also need to obtain an external X API key from X and set your X bearer token in your .env file. Note that the free version of the X API is limited to 500 posts and 100 reads per month.
X_BEARER_TOKEN=your-x-bearer-token
Full Agent Code (Also in GitHub)
Once you’ve set up your Dedalus API key and environment variables, you can run the agent using this code - just replace the name and x_username variables with your target profile:
import asyncioimport osfrom dedalus_labs import AsyncDedalus, DedalusRunnerfrom dedalus_labs.utils.stream import stream_asyncfrom dedalus_mcp.auth import Connection, SecretKeys, SecretValuesfrom dotenv import load_dotenvload_dotenv()# The user we are researching# !! Remember to update this to a real account !!name = "Jane Doe"x_username = "@JaneDoe"# Connection: schema for X/Twitter API# Note: Some providers may also require an additional "auth_header_format" parameter in Connection(). X does not.x = Connection(name="x",secrets=SecretKeys(token="X_BEARER_TOKEN"),base_url="https://api.x.com",)# SecretValues: binds actual credentials to a Connection schema.# Encrypted client-side, decrypted in secure enclave at dispatch time.x_secrets = SecretValues(x, token=os.getenv("X_BEARER_TOKEN", ""))async def main():client = AsyncDedalus(timeout=900) # 15 minutes (unit = seconds)runner = DedalusRunner(client)response = runner.run(input=f"""You are an expert CRM Research Agent. Your objective is to identify high-value second-degree connections for {name} ({x_username}).1. Use x-api-mcp (e.g., x_get_followers, x_get_user_by_username) to go through all of {name}'s followers.2. Research each follower's own network (second-degree) and their professional background.3. If X info is sparse, use brave-search-mcp to find more details on the web. Explicitly mention it when you cannot access X.4. Find at least 15 high-value connections for {name} ({x_username}). These high-value connections should be users that are not already in {name}'s network.5. For EVERY high-value connection found, format the output in a bulleted summary:- Name: [Full name]- X/Social: [Handle/Link]- Bio: [Brief summary]- Key Interests: [Topic 1, Topic 2]- Connection Logic: [Why they are a good lead]""",model="anthropic/claude-opus-4-5",mcp_servers=["windsor/x-api-mcp", "tsion/brave-search-mcp"],credentials=[x_secrets],stream=True,)await stream_async(response)if __name__ == "__main__":asyncio.run(main())
Follow the rest of the tutorial to see how we built this agent, step-by-step.
Step 1: Making a Basic Agent with the Dedalus SDK
To create any basic agent on Dedalus, you need to define the following variables:
input: instructions and context for the agent. e.g., what problem the agent is trying to solve and howmodel: which of our AI models your agent will usemcp_servers: which MCP servers your agent will use
...response = runner.run(input=f"""You are an expert CRM Research Agent. Your objective is to identify high-value second-degree connections for {name} ({x_username}).1. Use x-api-mcp (e.g., x_get_followers, x_get_user_by_username) to go through all of {name}'s followers.2. Research each follower's own network (second-degree) and their professional background.3. If X info is sparse, use brave-search-mcp to find more details on the web. Explicitly mention it when you cannot access X.4. Find at least 15 high-value connections for {name} ({x_username}). These high-value connections should be users that are not already in {name}'s network.5. For EVERY high-value connection found, format the output in a bulleted summary:- Name: [Full name]- X/Social: [Handle/Link]- Bio: [Brief summary]- Key Interests: [Topic 1, Topic 2]- Connection Logic: [Why they are a good lead]""",model="anthropic/claude-opus-4-5",mcp_servers=["windsor/x-api-mcp", "tsion/brave-search-mcp"],credentials=[x_secrets],stream=True,)...
This CRM agent uses two MCP servers:
tsion/brave-search-mcpfor web searchwindsor/x-api-mcpfor X (Twitter) profile and tweet search
Using both gives the agent richer context about the accounts it finds on X and lets it fall back to web search if it cannot find good matches directly on X.
You can add as many MCP servers as you need by populating the mcp_servers list. Note: each server fills the agent's context window, so adding too many may cause unexpected behavior.
For context, these are the tools built into the X MCP that your agent can access:
@tool(description="Get an X user by their username",tags=["user", "read"],annotations=ToolAnnotations(readOnlyHint=True),)async def x_get_user_by_username(username: str) -> dict:ctx = get_context()resp = await ctx.dispatch("x", HttpRequest(method=HttpMethod.GET,path=f"/2/users/by/username/{username}?user.fields={DEFAULT_USER_FIELDS}"))if resp.success:return {"data": resp.response.body.get("data")}return {"error": resp.error.message if resp.error else "Request failed"}@tool(description="Search recent tweets (last 7 days)",tags=["search", "read"],annotations=ToolAnnotations(readOnlyHint=True),)async def x_search_recent(query: str, max_results: int = 10) -> dict:from urllib.parse import quotectx = get_context()max_results = max(10, min(100, max_results))resp = await ctx.dispatch("x", HttpRequest(method=HttpMethod.GET,path=f"/2/tweets/search/recent?query={quote(query)}&tweet.fields={DEFAULT_TWEET_FIELDS}&max_results={max_results}"))if resp.success:body = resp.response.body or {}return {"data": body.get("data"), "meta": body.get("meta")}return {"error": resp.error.message if resp.error else "Request failed"}@tool(description="Get a user's recent tweets",tags=["tweet", "read"],annotations=ToolAnnotations(readOnlyHint=True),)async def x_get_user_tweets(user_id: str, max_results: int = 10) -> dict:ctx = get_context()max_results = max(5, min(100, max_results))resp = await ctx.dispatch("x", HttpRequest(method=HttpMethod.GET,path=f"/2/users/{user_id}/tweets?tweet.fields={DEFAULT_TWEET_FIELDS}&max_results={max_results}"))if resp.success:body = resp.response.body or {}return {"data": body.get("data"), "meta": body.get("meta")}return {"error": resp.error.message if resp.error else "Request failed"}
Step 2: Prompt Engineering and Model Choice for CRM Agents
The secret to creating high quality agents and workflows is prompt engineering + selecting the right models and tools.
- Define the agent’s role (for example: “You are a CRM researcher that focuses on high-value connections.”)
- Specify success criteria (what makes a good lead or connection, research at least how many leads, etc.)
- Explain how and when to use X tools vs web search - note that many foundation models will not by default call tools unless you explicitly specify in the prompt
- Clearly outline the final output format (bulleted summary, relationship map, scoring, next steps)
With the right models and tools, you can virtually build anything! For example, you can adapt the same setup to:
- Identify users who repost your account the most
- Compare follower overlap (shared network) between different accounts
- Categorize your followers into different buckets/ICPs
- Search your followers and find their LinkedIn profiles
- Score inbound leads based on social proof and activity
For this demo, we use the model Opus 4.5, which is good at:
- Chaining multiple tool calls per run
- Reasoning over noisy social data
- Producing clean, CRM‑ready relationship graphs
Depending on your use case, another model or a combination of multiple models (sometimes from different providers) might be a better fit for your agent. See available model providers that Dedalus offers, including leading models from OpenAI, Google, xAI, and more.
Step 3: Using the DAuth Authentication Layer
To use DAuth to protect your X API key and secrets, you’ll need to populate the X/Twitter API connection schema and then use SecretValues to pull the environment variable from your .env file.
To figure out the correct Connection() structure for your MCP, you need to research what HTTP request format the downstream API requires.
# Connection: schema for X/Twitter API# Note: Some providers may also require an additional "auth_header_format" parameter in Connection(). X does not.x = Connection(name="x",secrets=SecretKeys(token="X_BEARER_TOKEN"),base_url="https://api.x.com",)# SecretValues: binds actual credentials to a Connection schema.# Encrypted client-side, decrypted in secure enclave at dispatch time.x_secrets = SecretValues(x, token=os.getenv("X_BEARER_TOKEN", ""))
Securely pass secrets to your agent via the credentials variable. Note that this variable is only needed for MCP servers that require auth.
...model="anthropic/claude-opus-4-5",mcp_servers=["tsion/brave-search-mcp", "windsor/x-api-mcp"],credentials=[x_secrets],...
This allows our SDK to pass your X API key to windsor/x-api-mcp without ever exposing the bearer token to the host MCP server or Dedalus.
Step 4: Streaming With the Dedalus SDK
The SDK also provides streaming support, so agents running in the command line can display their output as they work and reduce latency.
Normally, you need to write your own streaming function. Not with Dedalus. We enable streaming (for both sync and async cases) across all major model providers in one line code. Simply stream=True in the Runner.
Try Building Your Own CRM Agent With Dedalus
With the right models and tools, building the right agent only takes minutes. The CRM Agent in this demo allows you to easily conduct deep user research that would normally take hours.
This is just one example of what you can build with the Dedalus SDK and Marketplace. We encourage you to experiment with all the models and tools on Dedalus to build even more powerful agents - for every possible use case.
Relevant Links:
- Access the GitHub repo for this project
- Check out our docs for more tutorials
- Join our developer community on Discord