You get one score per account from your CRM and an outside dataset. The whole system is a CSV, three API calls and a short Python script. No scoring platform is needed.
TL;DR
- Score every account from three inputs: first-party engagement (your CRM), third-party fit (technology, size, funding) and buyer coverage (people you can reach).
- It takes about 90 minutes and spends about 100 LeadOcean records on a 100-account test. Sizing and buyer counts are free.
- The rule that matters: third-party signals alone can never push an account past 40 points. Only your own engagement gets it to the top.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY. The free plan has 1,000 records, one-off, no card (sign up). - A CRM export as
accounts.csvwith the columnsdomain,pricing_visits_30d,trial_active(0 or 1),days_since_replyandopted_out(0 or 1). - Python 3.9 or later and
pip install requests.
Step 1: Pick the signals and weights
Write the model down before you write code. Put it in a config so you can change a weight without touching logic.
First-party signals prove interest. Third-party signals prove fit and timing. Weight them 60 to 40, and cap the third-party half.
{
"first_party": {
"trial_active": 25,
"pricing_visits_30d": 20,
"days_since_reply": 15
},
"third_party": {
"tech_match": 15,
"funded_recently": 10,
"size_fit": 5,
"buyer_coverage": 10
},
"third_party_cap": 40
}These weights are starting values, not findings. Re-fit them against closed-won deals once you have 50 or more.
Step 2: Size your account list for free
Check how many companies match your fit before you spend anything. POST /v1/companies/count takes the company-search filters and costs no records.
curl -s -X POST https://api.leadocean.io/v1/companies/count \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"hqCountry":["US"],"employeeRange":["201-500","501-1000"],"technologies":["HubSpot"]}'The same call works as the MCP tool leadocean_count_companies. Use it to check each signal's hit rate. A signal that matches 95% of your accounts adds nothing, so drop it.
Step 3: Pull third-party flags for your CRM accounts
Look up each CRM domain once with POST /v1/companies/enrich and read the three flags from the record. One company returned is one record, and a domain we hold nothing for is a 404 that costs nothing. A 100-account test spends at most 100 records.
import csv, os, time, requests
BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}
def post(path, body, params=None):
r = requests.post(BASE + path, headers=H, json=body, params=params)
time.sleep(0.25) # stays under 5 requests a second on Free
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
accounts = list(csv.DictReader(open("accounts.csv")))
tech, funded, size = set(), set(), set()
for a in accounts:
res = post("/v1/companies/enrich", {"domain": a["domain"]})
if not res:
continue
c = res["data"]
cd = c.get("company_data") or {}
names = {(t.get("technology_product") or {}).get("name")
for t in c.get("company_detected_technologies") or []}
if "HubSpot" in names:
tech.add(a["domain"])
if ((cd.get("company_funding") or {}).get("last_round_year") or 0) >= 2025:
funded.add(a["domain"])
band = (cd.get("company_employees") or {}).get("number_of_employees_code")
if band in ("201-500", "501-1000"):
size.add(a["domain"])Why enrich and not a filtered company search: the domain filter in search and count is a match, not an exact lookup, so one domain can return several companies. On 2026-10-21 a free count with 25 domains matched 4,387 companies. Enrich returns the one company for that domain.
Funding is held for a small fraction of companies, so a missing round is the normal case, not a "no". Do not size a funding signal with the free people count, which does not support funding filters.
Step 4: Count buyers per account, free
Buyer coverage asks one question: can you reach a decision maker here? A people search with count=true and limit=1 returns meta.total and spends nothing.
def buyers(domain):
body = {"domain": [domain], "jobLevel": ["VP", "C-Team"],
"jobFunction": ["Sales & Business Development"],
"emailStatus": ["verified", "catch_all_valid"], "limit": 1}
meta = post("/v1/people/search", body, {"count": "true"})["meta"]
return meta.get("total") or 0count=true goes in the query string, not the JSON body, which rejects unknown fields. The people domain filter is a match too, so treat the total as approximate; Step 5 caps coverage at 3 buyers for that reason. Enum values are exact strings: VP for jobLevel, lowercase vp for seniority.
Step 5: Score, cap and write back
Normalize each signal to 0 to 1, multiply by its weight, then apply the two stacking rules. Opt-outs zero the account. Third-party points are capped.
import json
W = json.load(open("weights.json"))
fp, tp = W["first_party"], W["third_party"]
def score(a):
d, why = a["domain"], []
first = (fp["trial_active"] * int(a["trial_active"])
+ fp["pricing_visits_30d"] * min(int(a["pricing_visits_30d"]) / 5, 1)
+ fp["days_since_reply"] * max(0, 1 - int(a["days_since_reply"] or 90) / 90))
third = (tp["tech_match"] * (d in tech) + tp["funded_recently"] * (d in funded)
+ tp["size_fit"] * (d in size)
+ tp["buyer_coverage"] * min(buyers(d) / 3, 1))
if int(a["opted_out"]):
return 0, "opted_out"
third = min(third, W["third_party_cap"])
if int(a["trial_active"]): why.append("trial_active")
if int(a["pricing_visits_30d"]) >= 3: why.append("pricing_visits")
if d in tech: why.append("hubspot_detected")
if d in funded: why.append("funded_2025+")
return round(first + third), "; ".join(why)
results = sorted(((score(a), a["domain"]) for a in accounts), key=lambda x: -x[0][0])
with open("scored.csv", "w", newline="") as f:
w = csv.writer(f); w.writerow(["domain", "account_score", "reasons"])
for (s, why), domain in results:
w.writerow([domain, s, why])Import scored.csv into your CRM with its standard CSV import, mapping account_score to a custom number field. The reasons column tells a rep why an account is on top. Schedule the script weekly and re-import.
What you get
A ranked CSV, one row per account, with the reasons attached. This is the output format, shown on placeholder accounts:
domain,account_score,reasons
acme.com,84,trial_active; pricing_visits; hubspot_detected
example.org,57,hubspot_detected; funded_2025+
example.net,0,opted_outThe free sizing calls give you real numbers before you run anything. On 2026-10-21 I ran two of them.
leadocean_count_companies hqCountry US, employeeRange 201-500 + 501-1000,
technologies HubSpot
-> total 9,589 (exact), credits 0
leadocean_count_leads jobLevel VP, jobFunction Sales & Business Development,
hqCountry US, employeeRange 201-500 + 501-1000
-> 25,543 people with a verified, catch_all_valid or catch_all email
(77,826 without the email filter), across 10,738 companies, credits 0A search response has this shape (response shape from the API reference, placeholder values).
{
"success": true,
"data": [{ "company_data": { "company_domain": "acme.com" } }],
"meta": { "count": 1, "limit": 100, "nextCursor": null }
}Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-copy the key from app.leadocean.io and check $LEADOCEAN_API_KEY is exported |
| 429 | More than 5 requests a second on Free (200 on Agency) | Keep the 0.25 second sleep, or honor the Retry-After header |
| 402 | The 1,000 free records are spent | Check GET /v1/usage, then move to Solo or Agency |
Empty data with a 200 | A filter value is not an exact enum string | Fetch valid values from GET /v1/enums or leadocean_list_enum_values |
meta.depthCapped is true, no nextCursor | One search pages to 10,000 rows | Narrow the filters and run several smaller searches |
| Export refused | Limit is 50,000 rows a request and 500,000 a day | Split the export into several POST /v1/exports calls |
FAQ
Why cap the third-party half at 40 points?
Third-party data describes fit, not intent. A well-funded company on your target stack looks the same to every vendor selling to it. The cap forces your own engagement to separate the top accounts from the rest.
Do I need a scoring platform?
Not to start. A CSV and a script score a few thousand accounts and are easy to audit. Move to a platform when you need real-time scoring or many scoring models, not before.
Can I add a new-hire or job-change signal?
Yes. Person enrich returns career history, including past roles and start dates, so a new VP of Sales can raise an account's score. It spends one record per person. See build a buying committee for one account for the people side.
How does this compare to other approaches?
Other guides cover the architecture in prose, for example Crustdata's account scoring guide (checked September 2026). This one is the minimal runnable version. For a technographic scoring recipe, see build an account list and score by technographics. For tools that bundle intent signals, see five enrichment tools that combine data and timing.
More guides like this are in the LeadOcean blog.
Score your own accounts with one free API key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →