# 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

```python
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:

```python
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

```python
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](./score-and-route-inbound-leads.md) ·
[export and keep in sync](./export-and-keep-in-sync.md)
