# Score and route inbound leads

**The job:** a form fill arrives. Before it reaches a rep, decide whether it is
worth a call, and who should get it — using what that person did on your site
before they filled the form.

**Who this is for:** anyone whose reps complain that inbound is mostly junk.

---

## The idea

A form tells you who someone claims to be. Signal tells you what they did for
the three weeks before they filled it in. The second is the better predictor
and you already have it.

## Enriching a form fill

```python
def enrich(email: str) -> dict | None:
    r = requests.get(f"{BASE}/visitors",
                     params={"q": email, "page_size": 1}, headers=H)
    r.raise_for_status()
    hits = r.json()["visitors"]
    return hits[0] if hits else None
```

A miss is common and informative: someone who filled a form on their first
visit has no history, which is itself a weaker signal than someone who read
you for a month first.

## A score you can defend

```python
def score(form, visitor):
    if visitor is None:
        return 20, "no prior visits"
    pts, why = visitor["intent_score"] // 2, []
    if visitor["visit_count"] >= 5:
        pts += 20; why.append(f"{visitor['visit_count']} visits")
    if visitor["classification"] == "customer":
        pts += 30; why.append("existing customer")
    if visitor["classification"] in ("competitor", "excluded"):
        return 0, visitor["classification"]
    if visitor["company_domain"] == "personal":
        pts -= 20; why.append("consumer mailbox")
    return min(pts, 100), ", ".join(why) or "no strong signal"
```

Returning the *reason* alongside the number is the part people skip. A rep who
can see "existing customer, 9 visits" trusts a 90 in a way they never trust a
bare 90.

## Routing

```python
points, reason = score(form, enrich(form["email"]))
if points >= 70:      assign(form, team="enterprise", sla_minutes=15)
elif points >= 40:    assign(form, team="smb", sla_minutes=240)
else:                 nurture(form, reason=reason)
```

## Letting a model handle the edges

Thresholds handle the middle well and the edges badly. For anything that lands
in a band you have flagged as uncertain, ask:

```
A form was submitted:
{form_json}

Here is what this person did on our site beforehand:
{visitor_json}
{journey_json}

Decide: enterprise, smb, or nurture. Give one sentence of reasoning that cites
specific behaviour. If the visitor record is empty, say so and route on the
form alone — do not infer behaviour that is not there.
```

## What will go wrong

**The email does not match.** People fill forms with a personal address and
browse from a work one, or the reverse. You will miss history you have. Match
on company domain as a fallback, and accept the miss rate.

**Competitors fill forms.** They do, constantly, and they read your pricing
page more carefully than your buyers. `classification: competitor` is the cheap
fix; it needs someone to have classified them once.

**Scores drift.** The weights above are guesses on day one. Store the score and
the reason with the lead, then look at what actually closed in ninety days and
change them. A scoring model nobody revisits is astrology with arithmetic.

---

Next: [build a daily call list](./build-a-daily-call-list.md) ·
[alert your team when a target account appears](./alert-your-team-when-a-target-account-appears.md)
