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

How-to guide

How to Give a LangChain Agent B2B Data (With LeadOcean)

Three tools, one record budget and honest error handling. The agent sizes an audience for free, searches 5 people and enriches 2.

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

TL;DR

  • A LangChain agent gets B2B data through three plain tools that call the LeadOcean REST API: count, search and enrich.
  • It takes about 20 minutes. The example run spends about 7 of your 1,000 free records, and a guard in the code stops it at 25.
  • Counting is free. A 402 or a long 429 goes back to the model as a message to stop, not an error to retry.

The full code is in this post, in one file: leadocean_agent.py.

Prerequisites

  • A LeadOcean account and API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free keys get both scopes, search and enrich.
  • The key in your shell as $LEADOCEAN_API_KEY.
  • Python 3.10 or newer, LangChain with create_agent, requests, and the provider package for your model.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
pip install langchain langchain-anthropic requests

Step 1: Write one call helper that knows the limits

Put every request through one function. It adds the x-api-key header, counts records from meta.credits, and turns failures into short dicts the model can read.

Two rules come from the LeadOcean limits docs. A 429 with Retry-After: 1 means you are over your plan's rate (5 requests a second on Free, 200 on Agency), so wait and retry. A 429 with Retry-After: 60 means a paid plan reached its fair-use pace, and the docs say not to retry into it.

python
"""leadocean_agent.py"""
import os, time, requests

BASE = "https://api.leadocean.io"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
BUDGET = 25          # stop the run after this many records
spent = 0

def call(method, path, body=None, params=None, cost=0):
    global spent
    if spent + cost > BUDGET:
        return {"error": "budget_reached", "spent": spent, "budget": BUDGET}
    for attempt in range(3):
        r = requests.request(method, BASE + path, headers=HEADERS,
                             json=body, params=params, timeout=30)
        wait = int(r.headers.get("Retry-After", "1"))
        if r.status_code == 429 and wait <= 2:
            time.sleep(wait)                  # over the per-second rate
            continue
        if r.status_code == 503:
            time.sleep(2 ** attempt)          # transient, never billed
            continue
        break
    try:
        data = r.json()
    except ValueError:
        return {"error": r.status_code, "message": "non-JSON response"}
    if r.status_code == 402:
        return {"error": "records_spent",
                "message": "Free records are used up. Stop and tell the user to upgrade."}
    if r.status_code == 429:
        return {"error": "paced",
                "message": f"Retry-After {wait}s. Stop and tell the user."}
    if not r.ok:
        return {"error": r.status_code,
                "message": data.get("error", {}).get("message")}
    spent += data.get("meta", {}).get("credits", 0)
    return data

Step 2: Define the three tools

Each tool is a function with type hints and a docstring. LangChain's @tool decorator builds the schema the model sees from them (LangChain tools docs, October 2026). Filter values are exact strings: jobLevel takes VP, and jobFunction takes Sales & Business Development.

python
from langchain.tools import tool

@tool
def count_people(job_level: str, job_function: str, country: str) -> dict:
    """Count people for a job level, job function and ISO country code.
    Free. Always call this before search_people."""
    body = {"jobLevel": [job_level], "jobFunction": [job_function],
            "country": [country], "limit": 1}
    res = call("POST", "/v1/people/search", body, params={"count": "true"})
    return res.get("meta", res)

@tool
def search_people(job_level: str, job_function: str, country: str, limit: int = 5) -> list:
    """Search people. Costs one record per person returned. Max 10 per call."""
    limit = max(1, min(limit, 10))
    body = {"jobLevel": [job_level], "jobFunction": [job_function],
            "country": [country], "limit": limit}
    res = call("POST", "/v1/people/search", body, cost=limit)
    if "error" in res:
        return res
    return [{"name": p["profile_data"].get("profile_full_name"),
             "headline": p["profile_data"].get("profile_headline"),
             "email_status": p["contact_data"].get("email_status"),
             "person_id": p["meta"]["source_ids"]["person_id"]}
            for p in res["data"]]

@tool
def enrich_person(person_id: str) -> dict:
    """Get emails and phones for one person_id from search_people. Costs 1 record.
    A person with nothing on file is a 404 and costs nothing."""
    res = call("POST", "/v1/people/enrich",
               {"person_id": person_id, "reveal_email": True}, cost=1)
    if "error" in res:
        return res
    return res["data"]["contact_data"]

The count call sends count=true as a query parameter with limit set to 1. It returns meta.total and no rows, so it spends nothing. Search rows hold email flags, not addresses, so the agent needs enrich_person for those.

Step 3: Build the agent

LangChain describes an agent as a model calling tools in a loop. create_agent takes the model, the tools and a system prompt, and the system prompt is where the cost rules live.

python
from langchain.agents import create_agent

agent = create_agent(
    model="claude-sonnet-5",
    tools=[count_people, search_people, enrich_person],
    system_prompt=(
        "You find B2B contacts with LeadOcean tools. "
        "Call count_people first and report the total. "
        "Search at most 5 people, then enrich at most 2 who look best. "
        "If a tool returns an error, stop and explain it. Never retry a 402 or a pace."
    ),
)

Step 4: Run it and print the cost

python
if __name__ == "__main__":
    result = agent.invoke({"messages": [{"role": "user", "content":
        "Find VP-level sales leaders in Canada. Size the audience, "
        "show 5, and get contact details for the 2 best."}]})
    print(result["messages"][-1].content)
    print(f"Records spent this run: {spent} of {BUDGET}")
bash
python leadocean_agent.py

Search costs 5 records and two enrichments cost 2, so a normal run lands at 7. On the Free plan, 1,000 records cover about 140 runs of this size. The BUDGET check inside call is the hard stop if the model loops.

GET /v1/account is free and shows what you have left. Paid plans have no per-record price. They have a fair-use pace instead, so a heavy day slows the key rather than billing you. See pricing.

Step 5: Or load the MCP tools instead

The hosted server at https://api.leadocean.io/mcp exposes 13 tools, and seven of them are free. LangChain's MCP docs show MCPAdapter turning a server URL into tools you pass to create_agent (LangChain MCP docs, October 2026).

bash
pip install "langchain[mcp]"
python
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter

async def main():
    async with MCPAdapter("https://api.leadocean.io/mcp") as adapter:
        tools = await adapter.list_tools()
        agent = create_agent("claude-sonnet-5", tools)
        return await agent.ainvoke({"messages": [{"role": "user",
            "content": "Count VP sales leaders in CA. Do not search."}]})

The langchain.mcp namespace needs langchain[mcp] 1.4.0 or newer and is in beta, so the API may change. The LeadOcean server uses OAuth 2.1 with no key to paste. LangChain's MCP docs list OAuth 2.1 under authentication, so set that up before you run this.

Pick MCP when the agent should choose from every tool. Pick REST, as above, when you want your own budget guard and a narrow tool list. MCP exposes tools that spend records, such as search and get_lead, with no cap of your own.

What you get

The free enum lookup, run on 21 October 2026 with leadocean_list_enum_values, confirms the exact job level strings the filters accept.

json
{"name":"job_level","total":6,"matched":6,"values":["C-Team","Director","Manager","Other","Staff","VP"],"truncated":false}

The free leadocean_count_leads tool, run on 21 October 2026 for jobLevel VP, jobFunction Sales & Business Development and country CA, returned 5,574 people with a verified, catch_all_valid or catch_all email. The same filters with no email condition matched 18,800 people. Your number will differ as the data refreshes.

A search row looks like this, with placeholder values (response shape from the API reference):

json
{
  "success": true,
  "data": [{
    "profile_data": {"profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme"},
    "contact_data": {"has_email": true, "email_status": "verified", "email_type": "work"},
    "meta": {"source_ids": {"person_id": "1234567890"}}
  }],
  "meta": {"count": 1, "credits": 1, "nextCursor": "opaque-string"}
}

Troubleshooting

ErrorCauseFix
401Missing, wrong or revoked keyCheck $LEADOCEAN_API_KEY is exported in the shell that runs Python.
403The key lacks the scope for that endpointUse a key with both search and enrich scopes.
429 with Retry-After: 1Faster than your plan's rate (5 a second on Free, 200 on Agency)Wait the header value and retry, as call does. Ask for full pages, not more calls.
429 with Retry-After: 60A paid plan reached its fair-use pace and is held to one request a minuteStop the run and read X-Quota-Reset. Do not loop.
402Free plan: the 1,000 records are spent and do not resetUpgrade to a paid plan.
400 or empty resultsA filter value is not an exact enum stringCall leadocean_list_enum_values or GET /v1/enums and copy the value.

A 503 with code: no_source is transient and never billed. The helper backs off and retries it.

FAQ

Should I use MCP or REST with LangChain?

Use REST when you want your own budget guard, retries and a short tool list. Use MCP when you want all 13 LeadOcean tools without writing wrappers. Both draw on the same records.

How many records does one run cost?

One record per person returned and one per enrichment. Counting and a 404 cost nothing. The example run spends about 7, and BUDGET caps it at 25.

Can I use a different model?

Yes. create_agent takes a model string, and the tools do not change. Install the matching provider package.

What does it cost after the free 1,000 records?

Solo is $97 a month for one person, and Agency is $497 a month for teams. Neither has a per-record price. See pricing.

Give your LangChain agent real B2B data

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

Get your free API key →