TL;DR
- A custom sales agent is three data tools (size, find, enrich), a rule list that caps spend, and a human who sends. You can build it in about 90 minutes.
- A test run spends about 20 of your 1,000 free LeadOcean records: 15 search rows and 5 enrichments. Sizing an audience costs nothing.
- Build instead of buy when your prospecting rules are the asset. Crustdata's internal sales tools page, September 2026, makes the same case for prospecting and pipeline agents. This page shows the data layer it skips.
Prerequisites
- A LeadOcean account from app.leadocean.io. Free is $0, no card, 1,000 records for life, 5 requests a second.
- Python 3.10+ with
requests, and any LLM that can call tools. - Your key in the shell. Never paste it into code.
export LEADOCEAN_API_KEY="paste-your-key-here"
pip install requestsWhich agent to build first
Build the prospecting agent first. It has one input (an ICP), one output (a short list) and the cheapest failure mode: a bad list, not a bad email. Crustdata's page, September 2026, names prospecting, pipeline prioritization and auto-updating prospect lists as agent types. Of those, only prospecting needs nothing but a people database.
Pipeline prioritization needs buying signals from your CRM and other sources. Keep that agent on those sources and feed it people from here.
Step 1: Write the rules first
Rules decide whether the agent is useful or expensive. Write them before any code and load them as the system prompt.
You are a prospecting assistant for one seller. You draft. A human sends.
1. Call size_audience before find_people. Report the total and what it counts.
2. Stop at 25 records per run. Report records spent at the end.
3. Use only enum values returned by the API. Never guess a job level or industry.
4. Use only fields present in the row. Never invent a company fact.
5. Never send, schedule or post anything. You have no tool for it.The default count covers mailable people, not everyone. Rule 1 makes the agent say so.
Step 2: Build the sizing tool
The first tool is a free count. It is a people search with count=true as a query parameter and limit at 1. It returns meta.total and no rows.
import os, requests
BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}
def size_audience(filters: dict) -> dict:
r = requests.post(f"{BASE}/v1/people/search?count=true",
headers=H, json={**filters, "limit": 1})
r.raise_for_status()
m = r.json()["meta"]
return {"total": m["total"], "exact": m["totalIsExact"],
"employers": m.get("distinctCompanies")}Totals are capped at 100,000 in meta.total. If the agent gets a capped number, tell it to narrow the filters.
Step 3: Build the find tool, with a spend cap
The second tool is a capped search. The body is flat JSON, every filter is an array of strings, limit runs 1 to 100, and you page with cursor. The cap lives in code, not only in the prompt.
SPENT = 0
CAP = 25
def find_people(filters: dict, limit: int = 10) -> dict:
global SPENT
limit = min(limit, CAP - SPENT)
if limit <= 0:
return {"error": "record cap reached"}
r = requests.post(f"{BASE}/v1/people/search", headers=H,
json={**filters, "limit": limit})
r.raise_for_status()
body = r.json()
SPENT += body["meta"]["count"]
return bodyA prompt can be talked around. A counter in the tool cannot. The same filters work in both tools, so the count you showed the seller is the list the agent pulls.
Example filters for the whole run:
{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["US"],
"industry": ["Software Development"],
"employeeRange": ["51-200"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"]
}Step 4: Build the enrich tool
The third tool deepens one person. Enrich takes exactly one of linkedin_url, email, phone or person_id. There is no name plus domain lookup, so search first and enrich by person_id. Since October 2026 the response carries career history in resume_data.
def enrich_person(person_id: str) -> dict:
global SPENT
if SPENT >= CAP:
return {"error": "record cap reached"}
r = requests.post(f"{BASE}/v1/people/enrich", headers=H,
json={"person_id": person_id})
if r.status_code == 404:
return {"found": False} # nobody matched, costs nothing
r.raise_for_status()
SPENT += 1
return r.json()A 404 costs nothing. Past roles, education and certifications come back when held, which gives the agent real material for a first line instead of a guess.
Step 5: Wire the tools to a loop
Any model that supports tool calling works. Give it the three functions, the rules from step 1, and the user's ICP. The loop is the same everywhere: call the model, run the tool it asks for, append the result, repeat until it answers.
TOOLS = {"size_audience": size_audience,
"find_people": find_people,
"enrich_person": enrich_person}
def run(model_call, icp: str):
messages = [{"role": "user", "content": icp}]
while True:
step = model_call(messages) # returns text or a tool request
if step["type"] == "text":
return step["text"], SPENT
result = TOOLS[step["name"]](**step["args"])
messages.append({"role": "tool", "name": step["name"], "content": result})model_call is whatever SDK you use. Check that provider's own tool-calling docs for the message format. The agent logic above does not change.
To skip the REST code, point a client at the hosted MCP server at https://api.leadocean.io/mcp. It exposes 13 tools with OAuth sign-in, no key pasted. Seven never spend records. The Claude Code sales agent guide shows that route.
Step 6: Check the count against the API
Do not trust a number you cannot reproduce. Run step 2's call by hand and compare it with what the agent reported.
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],
"country":["US"],"industry":["Software Development"],
"employeeRange":["51-200"],"limit":1}' | jq '.meta'If the totals differ, the agent changed a filter. Read its tool calls. GET /v1/account is free and shows what you have left.
Step 7: Add a review queue
An agent that drafts needs somewhere to put drafts. Write each run to a file the seller reads before anything is sent. Keep the count, the filters and the records spent at the top so the run is auditable.
import datetime, json
def save_brief(icp, count, rows, spent):
name = f"briefs/brief-{datetime.date.today()}.json"
os.makedirs("briefs", exist_ok=True)
with open(name, "w") as f:
json.dump({"icp": icp, "count": count, "rows": rows,
"records_spent": spent}, f, indent=2)
return nameReview the first five briefs by hand. Look for rows where the first line claims something the row does not show. Fix the rules, not the row, and rerun. Add a sequencer only after the briefs hold up.
What you get
The sizing tool answers first. This is the free MCP count tool run on 21 October 2026 with the filters above.
{
"total": 1649,
"totalIsExact": true,
"totalAll": 4141,
"distinctCompanies": 1125,
"credits": 0
}That is 1,649 mailable VPs of sales at 1,125 US software companies with 51-200 staff, for 0 records. Without an email filter the same filters match 4,141 people. Your numbers will differ as the data refreshes monthly.
A search row has a fixed shape. This is the response shape from the API reference, with placeholder values.
{
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": { "has_email": true, "email_status": "verified" },
"resume_data": { "<current role and company>": "<values>" },
"meta": { "source_ids": { "person_id": "<digits>" } }
}
],
"meta": { "count": 15, "limit": 15, "credits": 15, "total": "<total>", "nextCursor": "<cursor>" }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Copy the key from app.leadocean.io and re-export $LEADOCEAN_API_KEY. |
| 429 | Above the plan's rate limit: 5 a second on Free, 200 on Agency | Wait the Retry-After seconds. Run tool calls one at a time. |
| 402 | The free plan's 1,000 one-off records are spent | Check GET /v1/account. Move to Solo at $97 a month or Agency at $497. |
| Total of 0, or 400 | A filter value outside the list, such as jobLevel: "Vice President" | Pass an exact string. Use GET /v1/enums/job_level or leadocean_list_enum_values. |
meta.depthCapped is true, no nextCursor | One search pages to 10,000 rows | Narrow the filters and run several searches. |
| Export refused | Over 50,000 rows a request or 500,000 a day | Split the job. Bulk CSV export is not on Free. |
FAQ
Should I build a custom sales agent or buy one?
Build when your targeting rules and review step are the edge. Buy when you need a sequencer, inbox and CRM sync this quarter. LeadOcean is the data layer only: it has no sequencer, dialer, CRM or Chrome extension. For the bought options, read what an AI sales agent is, B2B sales agents and the best AI agent tools for GTM teams.
Why cap spend in code and not in the prompt?
Models follow a rule most of the time. A counter in the tool follows it every time. On Free the 1,000 records are one-off, so a loose agent can spend them in an afternoon.
How many records does a run cost?
About 20 here: 15 search rows and 5 enrichments. One record is one person you receive. Counts, an enrichment that finds nothing and a 404 cost nothing.
Can the agent send the emails?
Not as built. It has no send tool. Read email_status before anything goes out, because bounced emails are not refunded.
What does it cost at team scale?
Give your custom AI SDR 693M people to work from
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →