Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Build an AI SDR (With LangChain and LeadOcean)

A LangChain agent that sizes a list for free, searches prospects and enriches the best ones, in about 40 lines of Python.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

You build an AI SDR in LangChain by giving an agent three tools: count, search and enrich. LeadOcean serves all three over REST. The agent picks the list, pulls ten people and enriches the best five.

TL;DR

  • Time: 30 to 40 minutes to a working agent.
  • Records: about 15 per run. Ten from search, five from enrich. The count call is free.
  • Stack: Python, langchain, requests, and a LeadOcean key from the free plan.

Prerequisites

  • A LeadOcean account on the free plan: 1,000 records for life, no card, 5 requests a second. Create a key at app.leadocean.io and export it as $LEADOCEAN_API_KEY.
  • Python 3.10 or newer.
  • A model key for the LLM you pick, for example ANTHROPIC_API_KEY.
  • pip install langchain requests, plus the package for your model provider.

LeadOcean also runs a hosted MCP server at https://api.leadocean.io/mcp with 13 tools. LangChain can load MCP tools through its adapters. But the adapters repo is no longer maintained and MCP support moved into langchain.mcp (checked September 2026). This guide uses REST so the code does not depend on that move.

Step 1: Wrap the API in one small client

Every call goes to https://api.leadocean.io with the x-api-key header. Put the 402 and 429 handling in one place so each tool stays short.

python
import os, time, requests

BASE = "https://api.leadocean.io"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}

class OutOfRecords(Exception):
    pass

def call(method, path, params=None, body=None, retries=3):
    for attempt in range(retries):
        r = requests.request(method, BASE + path, headers=HEADERS,
                             params=params, json=body, timeout=30)
        if r.status_code == 429:
            time.sleep(float(r.headers.get("Retry-After", 1)))
            continue
        if r.status_code == 402:
            raise OutOfRecords("LeadOcean records are spent. Stop the run.")
        if r.status_code == 404:
            return None
        r.raise_for_status()
        return r.json()
    raise RuntimeError("Still rate limited after retries")

A 429 means you went above the plan's rate limit (5 requests a second on Free). The Retry-After header says how long to wait. A 402 means the records are gone, so retrying does nothing.

Step 2: Define the three tools

The count tool sends count=true as a query parameter with limit=1. That call is free and returns meta.total, capped at 100,000. Filters are arrays of strings, and enum values are exact.

python
from langchain.tools import tool

@tool
def count_prospects(job_level: list[str], job_function: list[str],
                    country: list[str]) -> dict:
    """Free. Count people matching filters. Use before searching.
    job_level e.g. ["VP"]; job_function e.g. ["Sales & Business Development"];
    country e.g. ["US"]."""
    body = {"jobLevel": job_level, "jobFunction": job_function,
            "country": country, "limit": 1}
    res = call("POST", "/v1/people/search", params={"count": "true"}, body=body)
    return res["meta"]

@tool
def search_prospects(job_level: list[str], job_function: list[str],
                     country: list[str], limit: int = 10) -> list:
    """Spends 1 record per person returned. Max 100 per page."""
    body = {"jobLevel": job_level, "jobFunction": job_function,
            "country": country, "limit": min(limit, 100)}
    return call("POST", "/v1/people/search", body=body)["data"]

@tool
def enrich_prospect(person_id: str) -> dict:
    """Spends 1 record. Returns profile, current role and the work email."""
    res = call("POST", "/v1/people/enrich",
               body={"person_id": person_id, "reveal_email": True})
    return res or {"result": "No match. Nothing was spent."}

Enrich takes exactly one of linkedin_url, email, phone or person_id. There is no name plus domain lookup, so the agent searches first and enriches by person_id, which each search row carries at meta.source_ids.person_id.

Step 3: Build the agent

LangChain's current agent constructor is create_agent, shown in the adapters README (checked September 2026). The system prompt is where the spending rules live.

python
from langchain.agents import create_agent

SYSTEM = """You are an SDR research agent.
1. Always call count_prospects first. If total is over 5,000, narrow the filters.
2. Search at most 10 people. Never page further.
3. Enrich at most 5 people, the best title fits only.
4. Stop and report if a tool says records are spent.
Return a table: name, title, company, work email."""

agent = create_agent("anthropic:claude-sonnet-4-5",
                     [count_prospects, search_prospects, enrich_prospect],
                     system_prompt=SYSTEM)

Swap the model string for any provider you have a key for. The hard caps in the prompt are a guide only. Step 5 adds a hard stop in code.

Step 4: Run it

python
import asyncio

async def main():
    out = await agent.ainvoke({"messages": [
        {"role": "user",
         "content": "Find US VPs of Sales. Enrich the top 5."}]})
    print(out["messages"][-1].content)

asyncio.run(main())

The agent counts, searches ten, picks five and enriches them. Expect four to eight model turns.

Step 5: Cap spend in code

A prompt is not a budget. Add a counter inside the client so a loop cannot burn the 1,000 free records.

python
SPENT = {"records": 0}
LIMIT = 25

def guard(n):
    SPENT["records"] += n
    if SPENT["records"] > LIMIT:
        raise OutOfRecords(f"Run cap of {LIMIT} records reached")

Call guard(len(data)) in search_prospects and guard(1) in enrich_prospect. One record is one person you receive. A page of 10 is 10 records, and an enrichment that finds nothing costs nothing.

Step 6: Read the agent's trace

Print every tool call while you test. You want to see count_prospects first, then one search, then at most five enrich calls. If the agent searches before it counts, tighten the system prompt. If it enriches more than five people, lower LIMIT.

python
for m in out["messages"]:
    for c in getattr(m, "tool_calls", []) or []:
        print(c["name"], c["args"])

What you get

The count call is free. On 2026-10-21 leadocean_count_leads returned this for VPs in Sales & Business Development in the US. It counts verified, catch_all_valid and catch_all emails by default:

json
{
  "total": 129494,
  "totalIsExact": true,
  "totalAll": 350517,
  "distinctCompanies": 58121
}

That is 129,494 people with a mailable address. Without an email filter the same filters match 350,517. Over REST, meta.total stops at 100,000, so the agent sees 100,000 for this list and should narrow it before it searches.

A search row and an enrich result then follow the documented shape. This is the response shape from the API reference, with placeholder values:

json
{
  "data": [{
    "profile_data": { "...": "Jane Doe, VP Sales, Acme" },
    "meta": { "source_ids": { "person_id": "per_000000" } }
  }],
  "meta": { "total": 1, "credits": 1, "nextCursor": "..." }
}

Enrich returns the current role in contact_current_experiences[], phones in contact_phones[].phone and, since October 2026, career history in resume_data.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported in the shell that runs Python
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Sleep for Retry-After and retry, as in Step 1
402Free plan: the 1,000 records are spent. They do not resetStop the run. Paid plans slow down at their pace instead of returning 402. See pricing
Empty dataA bad enum value, such as vp for jobLevelCall leadocean_list_enum_values and copy the exact string (VP)
404 on enrichNobody matched. It costs nothingReturn "no match" to the agent and move on
Export refusedA request over 50,000 rows, or past 500,000 a daySplit the export or run it tomorrow

FAQ

Can I use LeadOcean's MCP server instead of REST?

Yes. The server at https://api.leadocean.io/mcp exposes 13 tools and signs in with OAuth 2.1. Check LangChain's current MCP docs for how to load them. REST is simpler to cap and test.

How many records does one run cost?

About 15 in this guide: ten from search and five from enrich. The count call, list_filters and list_enum_values cost nothing. The free plan covers around 65 such runs.

Does this send emails?

No. LeadOcean is a data API. It has no sequencer, dialer or CRM. Pass the table the agent returns to the sender you already use.

Which plan do I need?

Free covers building and testing. Solo at $97 a month suits one person running this daily. Agency at $497 a month fits teams that need 200 requests a second. See pricing.

Where do the other frameworks stand?

The same three tools port to Pydantic AI, Mastra and LlamaIndex. Only the decorator changes.

Give your LangChain agent real prospect data

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →