TL;DR
- An agent needs three things from a data provider: a free way to size a list, a search it can filter exactly, and an enrich that returns one person by id.
- It takes about 25 minutes and spends about 11 records: 10 for the search, 1 for the enrich. Counts and filter lookups cost 0.
- "Real time" here means a live API call at run time, not a stale CSV. The dataset itself is refreshed monthly, and every record carries its own
fetched_atdate.
Prerequisites
- A LeadOcean account from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free runs at 5 requests a second.
- An API key exported as
$LEADOCEAN_API_KEY, pluscurl,jqand Python 3. - Optional, for the MCP route: Claude Code or any client that takes a remote MCP server with OAuth.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Size the audience before the agent spends anything
Make the agent count first. A count is free and tells it whether the filters are sane.
On REST, a count is a search with count=true as a query parameter and limit at 1. The body is flat JSON and every filter is an array.
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": ["CA"],
"limit": 1
}' | jq '.meta | {total, totalIsExact, countOnly}'meta.total is capped at 100,000. If the agent sees a number near that, it should narrow the filters, not page harder.
Step 2: Search ten people
Give the agent a search it can call with a small limit. Search rows are a thin projection with no contact points, so this call is cheap to reason about.
curl -s "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailStatus": ["verified", "catch_all_valid", "catch_all"],
"limit": 10
}' > search.json
jq '.meta | {count, credits, nextCursor, depthCapped}' search.jsonEach person returned counts one record, so this spends 10. Page with meta.nextCursor. One search stops at 10,000 rows, and then meta.depthCapped is true.
Step 3: Enrich one person by id
Search finds people. Enrich returns the full record for one of them. Pass the person_id from the search row, since there is no name-plus-domain lookup.
PID=$(jq -r '.data[0].meta.source_ids.person_id' search.json)
curl -s "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-d "{\"person_id\": \"$PID\", \"reveal_email\": true}" > person.json
jq '.data | keys' person.jsonThis costs 1 record, and reveal_email adds no extra charge. A person we hold nothing for is a 404 and costs nothing. Enrich also returns career history in resume_data: past roles, education and certifications.
Step 4: Wrap it as an agent tool
An agent calls tools, not curl. Here is one plain Python function with a hard cap, so a loop cannot burn records by accident. Hand it to any framework that takes function tools.
import os, requests
BASE = "https://api.leadocean.io"
HEAD = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def find_people(filters: dict, limit: int = 10) -> list[dict]:
"""Search people. Filters are arrays, e.g. {"jobLevel": ["VP"], "country": ["CA"]}."""
body = {**filters, "limit": min(limit, 25)} # cap: 25 records per call
r = requests.post(f"{BASE}/v1/people/search", json=body, headers=HEAD, timeout=30)
if r.status_code == 429:
raise RuntimeError("rate limited, wait Retry-After seconds")
r.raise_for_status()
return r.json()["data"]
def count_people(filters: dict) -> int:
"""Free. Always call this before find_people."""
r = requests.post(f"{BASE}/v1/people/search?count=true",
json={**filters, "limit": 1}, headers=HEAD, timeout=30)
r.raise_for_status()
return r.json()["meta"]["total"]Tell the agent in its system prompt to call count_people first and stop if the total is zero or above what the task needs.
Step 5: Or skip the code and use MCP
The hosted MCP server exposes 13 tools. The agent discovers them itself, and sign-in is OAuth, so no key is pasted. In Claude Code (Claude Code docs, October 2026):
claude mcp add --transport http leadocean https://api.leadocean.io/mcpThen give it a prompt that forces the free path first.
Use LeadOcean. Call leadocean_list_enum_values for jobLevel, then leadocean_count_leads
for VP, Sales & Business Development, country CA. Report the total and the email
statuses it counted. Do not pull anyone until I confirm.Counts, filter lookups, enum lookups, get_account, get_export and list_exports are free. Search, get, phone, company and export tools spend records. See the provider shortlist for agents for how the options compare, and social posts data if your agent also needs post activity. Recruiting agents should read real-time recruitment data.
What you get
The count is a real result. We ran the Step 1 filters through the free leadocean_count_leads tool on 2026-10-21 (trimmed).
{
"total": 5574,
"totalIsExact": true,
"totalAll": 18800,
"distinctCompanies": 3873,
"credits": 0,
"defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)",
"facets": {
"country": [{ "value": "CA", "count": 5574 }],
"seniority": [{ "value": "vp", "count": 5574 }]
}
}That is 5,574 mailable VPs across 3,873 companies. Without the email filter the same filters match 18,800 people. Your numbers will differ as the data changes.
A search row looks like this. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": [
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": { "has_email": true, "has_phone": false, "email_status": "verified" },
"meta": { "source_ids": { "person_id": "<digits>" } }
}
],
"meta": { "count": 1, "limit": 10, "credits": 1, "nextCursor": "<cursor or null>", "depthCapped": false }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing, wrong or revoked x-api-key | Re-copy the key from app.leadocean.io and export $LEADOCEAN_API_KEY again. |
| 429 | Above your plan's rate (5 requests a second on Free, 200 on Agency) | Wait the Retry-After seconds. Cap the agent's parallel calls. |
| 402 | The 1,000 free records are spent | Check GET /v1/account, lower limit, or move to a paid plan. |
Empty data on a valid query | An enum value outside the catalogue, such as jobLevel: "vp" | Call leadocean_list_enum_values or GET /v1/enums and pass the exact string, such as VP. |
depthCapped: true, no nextCursor | One search paged past 10,000 rows | Narrow the filters and run several smaller searches. |
| 404 on enrich | We hold nothing for that person | Nothing to fix. It cost 0 records. Try another person_id. |
FAQ
How real-time is the data?
The call is live: the agent gets the answer at run time. The dataset is refreshed monthly, and each record has a fetched_at date the agent can check before it trusts a title.
Can an agent run up a bill?
On Free, no. The key stops with a 402 after 1,000 records. Paid plans have a fair-use pace, not credits, so there is no per-record overage invoice. Still cap limit in your tool, as in Step 4.
Should I use MCP or REST?
Use MCP for an interactive agent in Claude Code or a chat client, where OAuth sign-in matters. Use REST for a scheduled or server-side agent where you want a fixed call shape and your own caps.
Does LeadOcean replace my CRM or sequencer?
No. It is a data API and an app with an Exports page. It has no rep workspace, sequencer, dialer or CRM. Your agent hands rows to the tools you already use.
Give your agent a data source that counts for free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →