Run the cheap filters first and the expensive one last. Free counts and query filters cut a market of thousands to a few dozen people. Claude then reads career history for only those few.
TL;DR
- Four layers: free count, structured filters, keyword filters, then a Claude judge on career history.
- Time: about 25 minutes. Records: about 35 (25 from search, 10 from enrich). The counts cost nothing.
- Everything fits inside the free plan: 1,000 records for life, no card.
Prerequisites
- A LeadOcean API key from app.leadocean.io, stored as
$LEADOCEAN_API_KEY. - Claude Code, or Claude with a remote MCP connector. The MCP server is at
https://api.leadocean.io/mcpand signs in with OAuth, so no key is pasted. curlandjqfor the REST steps.- A role brief. This guide uses a firmware engineer in the US at a company with 51 to 500 employees.
Step 1: Size the pool with a free count
Layer one is a count. It spends no records, so run it before every search.
Connect LeadOcean to Claude Code.
claude mcp add --transport http leadocean https://api.leadocean.io/mcpThen ask Claude to count, one filter at a time.
Use leadocean_count_leads. Count title "Firmware Engineer" in country US.
Then add employeeRange 51-200 and 201-500. Then add keywords "embedded C" and "RTOS".
Report total, totalAll and distinctCompanies for each. Do not search.Over REST, the same sizing call is a search with count=true as a query parameter and limit=1. The parameter goes in the URL, not the JSON body.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":["Firmware Engineer"],"country":["US"],"limit":1}' | jq '.meta'meta.total caps at 100,000. If a count is near that, add filters before you do anything else.
Step 2: Add structured filters
Layer two is the filters that do not need judgment: title, country, company size. Titles are job title keywords. Wrap a value in brackets for an exact match.
Check enum values before you guess. They are exact strings, and a wrong one returns nothing.
Call leadocean_list_enum_values with name employee_range, then name job_level.The company size values are 51-200 and 201-500, and seniority values are lowercase (senior, director). Add excludeTitle for the noise you already know about:
{
"title": ["Firmware Engineer", "Embedded Software Engineer"],
"excludeTitle": ["Intern", "Test", "Manager"],
"country": ["US"],
"employeeRange": ["51-200", "201-500"],
"excludeCompanyName": ["Acme"]
}Use excludeCompanyName for your own employer so you never source from yourself.
Step 3: Add keyword filters on the profile text
Layer three checks what the person wrote about their own work. keywords matches the profile headline, summary, skills and job descriptions. Each value is a phrase whose words must all appear, and several values OR together.
This is the layer that separates a firmware engineer from someone who mentions firmware once. Use excludeKeywords to drop the false friends.
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": ["Firmware Engineer"],
"country": ["US"],
"employeeRange": ["51-200", "201-500"],
"keywords": ["embedded C", "RTOS"],
"excludeKeywords": ["quality assurance"],
"limit": 25,
"fields": [
"profile_data.profile_full_name",
"contact_data.contact_current_experiences",
"meta.source_ids"
]
}' > candidates.json
jq '.meta' candidates.json
jq -r '.data[].meta.source_ids.person_id' candidates.json > ids.txtThis spends 25 records. The body is flat JSON, every filter is an array, and limit runs from 1 to 100. Page with meta.nextCursor only if the first 25 are good. One search pages to 10,000 rows.
Step 4: Judge career history with Claude
Layer four is the only one that needs a model. It runs on 10 people, not 10,000.
Person enrich returns career history since October 2026: past roles, education, certifications and awards. Enrich takes exactly one of linkedin_url, email, phone or person_id. Search first, then enrich by person_id.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"person_id":"PERSON_ID_FROM_SEARCH"}' > candidate_01.jsonRepeat for the top 10 and save each file. Then give Claude Code this prompt:
Read candidate_*.json. Role: Senior Firmware Engineer, US, startup to mid-size.
For each candidate return JSON: name, verdict (strong | maybe | no), and three fields:
- years_in_firmware: sum only roles whose title or description is firmware work
- recency: is the most recent role still firmware? yes | no
- evidence: one quote of up to 15 words from a past role that supports the verdict
Rules: ignore the headline. Judge only data.resume_data.experiences and data.contact_data.contact_current_experiences.
If a field is missing, say "not held". Never guess.
Sort strong first. Output one table, then the JSON.Two rules keep the judge honest. Make it quote the evidence, and make it say "not held" instead of guessing. Past employers carry no company_id yet, so match them by name.
What you get
On 21 October 2026, the free leadocean_count_leads tool returned these counts for the same role, layer by layer. Counts use the tool's default of mailable addresses (verified, catch-all valid or catch-all).
| Layer | Filters added | Mailable | Everyone matched |
|---|---|---|---|
| 1 | title "Firmware Engineer", country US | 5,508 | 10,998 |
| 2 | employeeRange 51-200, 201-500 | 524 | 1,735 |
| 3 | keywords "embedded C", "RTOS" | 289 | 911 |
The 289 span 214 distinct companies. Layer four then reads 10 of them.
Enrich response shape from the API reference and the enrichment docs, with placeholder values.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe" },
"contact_data": {
"contact_current_experiences": [
{ "job_title": "Senior Firmware Engineer", "company_name": "Acme", "company_domain": "acme.com" }
]
},
"resume_data": {
"experiences": [
{ "job_title": "Embedded Software Engineer", "company_name": "Example Co" /* plus job_seniority, start and end dates */ }
],
"educations": [],
"certifications": []
}
}
}The docs name the experience fields (company_name, job_title, job_seniority, start and end dates) but not every key, so print one record with jq before you write code against the dates.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Check $LEADOCEAN_API_KEY is exported in the shell that runs curl. |
| 429 | Above the plan's rate limit: 5 requests a second on Free, 200 on Agency | Wait the Retry-After seconds. Enrich candidates one at a time. |
| 402 | The 1,000 free records are spent | Upgrade to Solo at $97 a month, or narrow the layers so fewer records are pulled. |
| 200 with no rows | A bad enum value, such as Senior where the value is senior | Run leadocean_list_enum_values and copy the exact string. |
| 404 on enrich | Nobody matched the person_id | Free, nothing is billed. Re-run the search and take a fresh ID. |
| 400 or 429 on an export | 400: over 50,000 rows in one export. 429: past 500,000 rows a day | Split the filters into batches under the ceiling. Bulk CSV export is not on Free. |
FAQ
How many records does this cost?
About 35 for a test run: 25 from the search page and 10 from enrich. Counts, filter lists and a 404 enrich cost nothing. The free plan has 1,000 records for life.
Can I run this without Claude Code?
Yes. Steps 1 to 3 are plain curl. For step 4, paste the saved JSON into any Claude chat that has the prompt, or connect the LeadOcean MCP server as a remote connector. Claude's MCP docs show how (September 2026).
Why not let Claude judge everyone?
Cost and noise. A model reading thousands of profiles spends time and tokens on people a keyword filter would have dropped. The Crustdata guide (September 2026) argues the same order: cheap filters first, the model last.
Does this give me emails and phones?
Search returns has_email and has_phone flags, not addresses. Enrich with reveal_email: true returns the addresses. Phones come from GET /v2/people/phone, and each call is one record. Read email_status before you send anything, because bounced emails are not refunded.
Where do I go from here?
Build the sourcing brief with build an ICP for B2B. Then wire the shortlist into outreach with Claude Code email sequences, or see the wider recruiting workflows with Claude. More guides are in the blog, and plans are on pricing.
Filter candidates in layers, and pay only for the survivors
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →