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

How-to guide

How to Build a Lead Qualification Agent (LangChain and LeadOcean)

Three tools, one system prompt, and error handling for 402 and 429. Runs on the free plan.

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

You build it with three LangChain tools over the LeadOcean REST API: size a list, search, enrich. The agent decides how many people to pull and who to enrich. The full code is in this post.

TL;DR

  • Time: about 45 minutes, most of it reading the agent's output and tuning the prompt.
  • Cost: about 13 LeadOcean records per run. Sizing is free, a 10-row search is 10 records, three enrichments are 3.
  • The free plan is enough: 1,000 records for life, no card, 5 requests a second.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, exported as $LEADOCEAN_API_KEY.
  • Python 3.10 or newer.
  • A model key for the provider you pick (the sample uses an Anthropic model string).
  • Packages: pip install langchain requests, plus the integration package for your model provider.

Check the key first. GET /v1/account is free.

bash
curl -s https://api.leadocean.io/v1/account \
  -H "x-api-key: $LEADOCEAN_API_KEY"

Step 1: Write a client that handles 402 and 429

All three tools share one request helper. It retries a 429 once after the Retry-After header and turns a 402 into a plain message the agent can read.

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):
    for attempt in range(2):
        r = requests.request(method, BASE + path, headers=HEADERS,
                             params=params, json=body, timeout=30)
        if r.status_code == 429 and attempt == 0:
            time.sleep(float(r.headers.get("Retry-After", "1")))
            continue
        if r.status_code == 402:
            raise OutOfRecords("LeadOcean records are spent. Stop and tell the user.")
        if r.status_code == 404:
            return {"data": None, "note": "nobody matched"}
        r.raise_for_status()
        return r.json()
    raise RuntimeError("Rate limited twice. Slow down.")

A 404 on enrich costs nothing, so the helper returns it as an empty answer instead of an error.

Step 2: Define the tools

Use @tool from langchain.tools. The docstring is what the model reads, so say what each tool costs.

python
from langchain.tools import tool

@tool
def size_list(title: list[str], country: list[str], job_level: list[str]) -> dict:
    """Count how many people match. Free. Call this before search_people.
    job_level values are exact: C-Team, VP, Director, Manager, Staff, Other.
    country is ISO alpha-2, for example US."""
    out = call("POST", "/v1/people/search", params={"count": "true"},
               body={"title": title, "country": country,
                     "jobLevel": job_level, "limit": 1})
    return {"total": out["meta"].get("total"),
            "exact": out["meta"].get("totalIsExact")}

@tool
def search_people(title: list[str], country: list[str],
                  job_level: list[str], limit: int = 10) -> list:
    """Search people. Costs 1 record per person returned. Keep limit at 10 or less.
    Returns profile rows and a person_id for enrich_person."""
    out = call("POST", "/v1/people/search",
               body={"title": title, "country": country,
                     "jobLevel": job_level, "limit": min(limit, 25)})
    return out["data"]

@tool
def enrich_person(person_id: str) -> dict:
    """Enrich one person by person_id. Costs 1 record. Returns career history
    and the best email with its status. Call it only for strong matches."""
    return call("POST", "/v1/people/enrich",
                body={"person_id": person_id, "reveal_email": True})

Search takes a flat JSON body, filters are arrays, and limit runs from 1 to 100. The sample caps it at 25 in code so the model cannot spend a whole free plan on one call.

Step 3: Build the agent

create_agent takes a model string, a list of tools and a system prompt, per the LangChain agents docs (checked September 2026). The scoring rules live in the prompt.

python
from langchain.agents import create_agent

SYSTEM = """You qualify B2B leads for a seller of sales-data tooling.
ICP: VP or Director in sales or revenue operations at software companies.
Process:
1. Call size_list first. If total is above 5,000, narrow the filters.
2. Call search_people with limit 10.
3. Score each person 0 to 10 on title fit and seniority.
4. Call enrich_person for at most 3 people scoring 8 or higher.
5. Return a table: name, title, score, email, email_status, one reason.
Never enrich a person scoring below 8. If a tool says records are spent, stop."""

agent = create_agent(
    model="anthropic:claude-sonnet-5",
    tools=[size_list, search_people, enrich_person],
    system_prompt=SYSTEM,
)

Check the model string against your provider's page in the LangChain docs. The format is provider:model.

Step 4: Run it and catch the stop

An uncaught tool exception ends the run. Catch OutOfRecords around the call so the user sees a clean message. LangChain also offers wrap_tool_call middleware for retry and custom error messages (tools docs, checked September 2026).

python
def run(task: str):
    try:
        result = agent.invoke({"messages": [{"role": "user", "content": task}]})
        return result["messages"][-1].content
    except OutOfRecords as e:
        return f"Stopped: {e}"

print(run("Find VP Sales contacts in the US and qualify the best three."))

Step 5: Cap the cost per run

The prompt asks for restraint. Code enforces it. Count records in the tools and refuse past a budget.

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

def charge(n):
    SPENT["records"] += n
    if SPENT["records"] > BUDGET:
        raise OutOfRecords("Run budget reached.")

Call charge(len(out["data"])) inside search_people and charge(1) inside enrich_person. One record is one person you receive. A count, a 404 and an empty page cost nothing.

What you get

The sizing call is free. On 2026-10-21 we ran the free leadocean_count_leads tool with the same filters size_list sends: title VP Sales, country US, job level VP.

json
{
  "total": 10839,
  "totalIsExact": true,
  "totalAll": 36817,
  "distinctCompanies": 7845,
  "credits": 0
}

total counts people whose best email is verified, catch_all_valid or catch_all. totalAll is the same filters with no email condition. At 10,839 the prompt's rule (narrow above 5,000) kicks in, so the agent adds a filter such as employeeRange before it searches.

A search row carries the id you pass to enrich. Response shape from the API reference, placeholder values:

json
{
  "success": true,
  "data": [
    { "profile_data": { "profile_full_name": "Jane Doe", "...": "headline, location" },
      "meta": { "source_ids": { "person_id": "1234567890" } } }
  ],
  "meta": { "count": 1, "nextCursor": null }
}

Search rows carry email flags (has_email, email_status), not addresses. Enrich with reveal_email returns contact_data.contact_emails and email_status, and since October 2026 a resume_data block with past roles, education and certifications.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyRe-export $LEADOCEAN_API_KEY and call GET /v1/account
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honor Retry-After, as in Step 1, or upgrade
402Records are spentFree is 1,000 for life. Paid plans have a fair-use pace instead
Empty dataBad enum value, such as Vice President for jobLevelUse VP. Check values with GET /v1/enums
Search stops pagingOne search pages to 10,000 rows (meta.depthCapped)Narrow the filters and run smaller searches
404 on enrichNobody matchedFree. Skip the lead

Bulk CSV export is not on Free. If the agent should hand a list to a sequencer, use POST /v1/exports on a paid plan.

FAQ

Can the agent use the LeadOcean MCP server instead of REST?

Yes. LeadOcean runs a hosted MCP server with 13 tools at https://api.leadocean.io/mcp. LangChain's MCPAdapter loads a server's tools into an agent, and it is in beta (docs, checked October 2026). LeadOcean's MCP signs in with OAuth, so a headless agent is simpler on REST with a key.

How many records does one run cost?

About 13 in this build: 10 from search, 3 from enrich. The code budget in Step 5 stops a bad run at 25. Sizing, counts and 404s are free.

Which plan do I need?

Free is enough to build and test. A team running this daily wants Agency at $497 a month, with 200 requests a second and 5 keys. Solo at $97 a month covers one person.

Does the agent send emails?

No. LeadOcean is data only. It has no sequencer or CRM. Pass the qualified rows to your sequencer, and read email_status first, since bounced emails are not refunded.

Where are the sibling builds?

Same tools, other frameworks: Pydantic AI, Mastra and LlamaIndex. More recipes are in the blog hub.

Qualify your first 100 leads with LangChain and LeadOcean

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

Get your free API key →