On this page

Fill your CRM with who is actually visiting

The job: your CRM has the accounts your reps created. Signal has the accounts that are reading your site. The overlap is smaller than anyone expects, and the gap is your pipeline.

Who this is for: anyone running HubSpot, Salesforce or a spreadsheet that wishes it were one.


Do the comparison before you build anything

signal_domains = {a["company_domain"] for a in get_all("/accounts")}
crm_domains = {c["domain"] for c in crm.list_accounts()}

print("in both:", len(signal_domains & crm_domains))
print("visiting, not in CRM:", len(signal_domains - crm_domains))
print("in CRM, not visiting:", len(crm_domains - signal_domains))

Run that first. The three numbers tell you which problem you actually have, and they are usually not the one you assumed. A large "visiting, not in CRM" is demand you are not working. A large "in CRM, not visiting" is a pipeline review waiting to happen.

Creating only what is worth creating

Do not sync everything. Most identified visitors are not prospects, and a CRM full of them is worse than one missing them:

def worth_creating(a):
    return (
        a["company_domain"] != "personal"
        and a["classification"] == "lead"
        and a["intent_score"] >= 50
        and a["visitor_count"] >= 2      # more than one human looked
    )

visitor_count versus visit_count matters here: one person visiting fifteen times is a researcher; three people visiting twice each is an account.

Writing back, idempotently

for a in filter(worth_creating, accounts):
    crm.upsert_account(
        domain=a["company_domain"],
        name=a["company_name"] or a["company_domain"],
        properties={
            "signal_intent": a["intent_score"],
            "signal_people": a["visitor_count"],
            "signal_first_seen": a["first_seen_at"],
            "signal_last_seen": a["last_seen_at"],
        },
    )

Key on company_domain. It is the natural key on both sides and the only field that will not drift — company names change spelling constantly.

Letting an LLM do the triage

When "visiting, not in CRM" is a few hundred rows, a model is better than a threshold at separating real prospects from noise:

Here are companies visiting our site that are not in our CRM:
{accounts_json}

We sell {one sentence about your product} to {your ICP}.

Split them into: create now, watch, and ignore. For each in "create now", one
sentence citing the specific numbers that justify it. Put anything you cannot
tell apart into "watch" rather than guessing — an over-full CRM costs us more
than a missed account.

What will go wrong

Duplicates. Your CRM probably has acme.com, www.acme.com and Acme Corp as three records already. Normalise before you compare, or you will create a fourth.

Subsidiaries. acme.co.uk and acme.com are one customer to a human and two domains here.

total is capped. If you are near your plan's resolution cap, get_all returns what you may resolve, not everything that exists. The response says so; do not treat the count as the population.


Next: score and route inbound leads · export and keep in sync

This page is maintained next to the service it describes and rendered here. Read it as markdown if you are pointing an agent at it.