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 Lead Generation Agent (With a B2B Data API)

A 60-line agent that sizes a list for free, searches people, enriches the best fits and stops when you tell it to. Real queries, real output shape.

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

An AI lead generation agent is a language model in a loop with two or three data tools. You give it an ICP in plain English. It turns that into filters, checks the size for free, pulls a short list and enriches the best rows.

Most guides on this topic list vendors. This one builds the thing. The agent is about 60 lines of Python, and the data layer is LeadOcean's REST API.

TL;DR

  • The agent has three tools: a free count, a people search and a person enrich. It decides the filters, you decide the budget.
  • It takes about 30 minutes to build and spends about 8 records of the 1,000 on the free plan.
  • Counts cost nothing, so the agent can try ten filter sets before it spends a single record.

Prerequisites

  • A LeadOcean API key in $LEADOCEAN_API_KEY. The Free plan has 1,000 records for life, 5 requests a second, no card.
  • An Anthropic API key in $ANTHROPIC_API_KEY, or any model API that supports tool calling. See the tool use docs (checked September 2026).
  • Python 3.10+ with requests and anthropic installed.
  • Optional: skip the code and connect a chat client to the hosted MCP server at https://api.leadocean.io/mcp. It exposes 13 tools and signs in with OAuth 2.1, so no key is pasted.

Step 1: Size the list for free

Start with the count, not the search. Sizing is free, so the agent should do it first and often.

The REST way is a search with count=true as a query parameter and limit=1. The data array comes back empty and meta.total holds the number, capped at 100,000.

bash
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "hqCountry": ["US"],
    "employeeRange": ["51-200"],
    "technologies": ["HubSpot"]
  }'

Filters are arrays. Enum values are exact strings, so VP works and vice president returns nothing. GET /v1/filters lists every filter.

Step 2: Give the model three tools

The model never calls LeadOcean itself. It emits a tool call, your code runs the request and returns the JSON. Keep the tool descriptions blunt about cost.

python
import os, requests

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

TOOLS = [
  {"name": "count_people",
   "description": "Free. Count people matching filters. Call this first and often.",
   "input_schema": {"type": "object", "properties": {"filters": {"type": "object"}}, "required": ["filters"]}},
  {"name": "search_people",
   "description": "Costs 1 record per person returned. Max limit 10 in this agent.",
   "input_schema": {"type": "object", "properties": {"filters": {"type": "object"}, "limit": {"type": "integer"}}, "required": ["filters", "limit"]}},
  {"name": "enrich_person",
   "description": "Costs 1 record. Needs the meta.source_ids.person_id of a search row.",
   "input_schema": {"type": "object", "properties": {"person_id": {"type": "string"}}, "required": ["person_id"]}},
]

def run_tool(name, args):
    if name == "count_people":
        r = requests.post(f"{BASE}/v1/people/search?count=true&limit=1", headers=H, json=args["filters"])
    elif name == "search_people":
        body = {**args["filters"], "limit": min(args["limit"], 10)}
        r = requests.post(f"{BASE}/v1/people/search", headers=H, json=body)
    else:
        r = requests.post(f"{BASE}/v1/people/enrich", headers=H, json={"person_id": args["person_id"]})
    return r.text if r.ok else f"HTTP {r.status_code}: {r.text}"

The cap of 10 rows lives in your code, not in the prompt. A prompt is a request. A min() is a limit.

Step 3: Write the loop and a budget

The loop sends the conversation, runs any tool calls and feeds results back until the model stops asking. Add a hard record budget so a confused model cannot burn your allowance.

python
import anthropic
client = anthropic.Anthropic()

SYSTEM = """You are a lead research agent. Rules:
1. Call count_people before search_people. Aim for under 10,000 matches.
2. Enum values are exact: jobLevel C-Team, VP, Director, Manager, Staff, Other.
3. Search at most 10 people, then enrich at most 3 that best fit the ICP.
4. Finish with a short table: name, title, company, why they fit."""

def run(icp, budget=10):
    msgs, spent = [{"role": "user", "content": icp}], 0
    while True:
        resp = client.messages.create(model="claude-sonnet-5-5", max_tokens=2000,
                                      system=SYSTEM, tools=TOOLS, messages=msgs)
        msgs.append({"role": "assistant", "content": resp.content})
        calls = [b for b in resp.content if b.type == "tool_use"]
        if not calls:
            return "".join(b.text for b in resp.content if b.type == "text")
        results = []
        for c in calls:
            cost = {"search_people": c.input.get("limit", 0), "enrich_person": 1}.get(c.name, 0)
            if spent + cost > budget:
                out = "Budget reached. Stop and summarise."
            else:
                spent += cost
                out = run_tool(c.name, c.input)
            results.append({"type": "tool_result", "tool_use_id": c.id, "content": out})
        msgs.append({"role": "user", "content": results})

print(run("VPs of sales at US companies with 51-200 staff that run HubSpot."))

claude-sonnet-5-5 is a current model id (Anthropic models, October 2026). Swap it for whichever tool-calling model you use. Nothing else in the loop changes.

Step 4: Keep the agent honest about email

Search rows carry contact_data.email_status. The agent should read it, not assume. Add one rule to the system prompt. Tell it to recommend only people whose email_status is verified, catch_all_valid or catch_all.

Bounced emails are not refunded, so the filter belongs in the prompt and in your sending code. The MCP count tool applies the same default, which counts verified, catch_all_valid and catch_all unless told otherwise.

Step 5: Move the same agent to MCP

If you want a chat client to do this instead of your script, point it at https://api.leadocean.io/mcp. It exposes the same capabilities as tools: leadocean_count_leads, leadocean_search_leads, leadocean_get_lead and ten more.

The free tools include leadocean_count_leads, leadocean_count_companies, leadocean_list_filters and leadocean_list_enum_values. Tell the client in its instructions to call the free ones first. Setup steps per client are on the MCP page.

Step 6: Narrow before you spend

The count in Step 1 is the agent's steering wheel. A good run looks like this: the model counts a broad filter, sees too many people, adds a filter and counts again. Only then does it search.

Here is the shape you want in the logs (counts shown only where measured). Each line is one tool call, and only the last two cost records.

text
count_people  {jobLevel: VP, jobFunction: Sales & Business Development}          -> too wide, narrow it
count_people  {... hqCountry: US, employeeRange: 51-200}                          -> still wide, narrow it
count_people  {... technologies: HubSpot}                                         -> total 5,855
search_people {... limit: 10}                                                     -> 10 records
enrich_person {person_id: per_0000}                                               -> 1 record

If the first count is capped at 100,000, the agent knows the list is too wide to be useful. One search also pages to 10,000 rows, so narrowing beats walking the index. Put that rule in the system prompt too.

What to leave out of version one

Three things make an agent worse, not better.

  • Open-ended loops. A cap on tool calls (say 12) stops a model that keeps re-counting.
  • Free-form filters. Give the model the filter names from GET /v1/filters. It will invent seniorityLevel if you do not.
  • Auto-send. An agent that writes and sends in one pass sends its mistakes. Review the table first.

Once the table looks right, export the full list. POST /v1/exports takes up to 50,000 rows a request and 500,000 a day, and bulk CSV export is not on Free.

What you get

The count in Step 1, run through the free leadocean_count_leads tool on 2026-10-21:

json
{
  "total": 5855,
  "totalIsExact": true,
  "totalAll": 18203,
  "distinctCompanies": 3284,
  "credits": 0
}

total is the mailable audience (verified, catch_all_valid or catch_all email). totalAll is the same filters with no email condition. The count cost 0 records.

A person comes back from search or enrich in this shape (response shape from the API reference, placeholder values).

json
{
  "profile_data": { "profile_full_name": "Jane Doe" },
  "contact_data": {
    "contact_current_experiences": [{ "job_title": "VP of Sales", "company_name": "Acme", "company_domain": "acme.com" }],
    "has_email": true,
    "email_status": "verified"
  },
  "meta": { "source_ids": { "person_id": "123456", "person_group_id": "123456" } }
}

The agent passes meta.source_ids.person_id to enrich_person.

Enrich also returns career history since October 2026: past roles, education, certifications and awards, each when held. Your agent can use it for a one-line opener.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported in the shell that runs the script
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour the Retry-After header and sleep in run_tool
402Out of recordsFree is 1,000 records for life. Move to Solo at $97 a month or Agency at $497 a month
total is 0An enum value is not an exact stringCall GET /v1/enums or leadocean_list_enum_values and copy the value
404 on enrichNobody matched that person_idNothing was charged. Skip the row and move on
Export refusedOver 50,000 rows a request or 500,000 a daySplit by country or seniority and run several exports

FAQ

Is an AI lead generation agent just a chatbot?

No. A chatbot answers. An agent calls tools in a loop and acts on the results. Here it counts, searches and enriches without you writing the filters.

How many records does the agent spend?

In this build, at most 10 for search plus 3 for enrich, set by the budget in run(). Counts are free. A search page of 25 is 25 records, so keep limit small while testing.

Should the agent send the emails?

Keep that out of the first version. Have it return a table and let a person or a separate sequencer send. LeadOcean has no sequencer or CRM, so sending happens in your own tool.

Can I use this at volume?

Yes, on a paid plan. Paid plans run on a fair-use pace, not credits, so nothing needs topping up. Pick a plan on the pricing page. More recipes are on the blog. For list sizing and funnel context, see outbound lead generation, predictive lead generation and the lead generation funnel.

Give your lead generation agent real data. Start free.

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

Get your free API key →