# Risk scores and reasons

Every check returns the same `risk` block:

```json
"risk": {
  "score": 64,
  "level": "medium",
  "verdict": "review",
  "reasons": [
    { "code": "domain_brand_new", "impact": 55, "message": "acme-trial.io was registered 3 days ago." },
    { "code": "role_account", "impact": 15, "message": "info@ is usually a shared team inbox, not a person." }
  ]
}
```

## Score, level and verdict

| Score | Level | Verdict | Suggested action |
|---|---|---|---|
| 0–29 | `low` | `admit` | Let them in. |
| 30–69 | `medium` | `review` | Let them in with friction: email or phone verification, or hold rewards until verified. |
| 70–100 | `high` | `refuse` | Block, or ask for different details. |

**Impact** is how strongly one reason, on its own, points to a fake or unusable signup. Reasons combine like independent odds: `score = 100 × (1 − Π(1 − impact/100))`.

Codes are stable and safe to branch on. New codes may be added over time, so treat unknown codes by their `impact`.

## Email reasons

| Code | Impact | Meaning |
|---|---|---|
| `invalid_syntax` | 100 | Not a valid email address. |
| `domain_not_found` | 95 | The domain doesn't exist. |
| `domain_refuses_mail` | 95 | The domain publishes a null MX: it accepts no email. |
| `mailbox_not_found` | 95 | The mail server says this mailbox doesn't exist (mailbox verification only). |
| `disposable_domain` | 90 | A throwaway inbox provider. |
| `no_mail_server` | 90 | The domain has no mail server. |
| `mx_shared_with_disposable` | 75 | The domain's mail server also serves known throwaway domains. Catches new throwaway brands. |
| `domain_brand_new` | 55 | Registered less than 7 days ago. |
| `domain_new` | 35 | Registered less than 30 days ago. |
| `possible_typo` | 30 | Looks like a misspelling of a popular provider, such as gmial.com. |
| `catch_all_domain` | 20 | The server accepts mail for any address, so the mailbox can't be confirmed. |
| `role_account` | 15 | A shared inbox, like info@ or support@. |
| `random_local_part` | 12 | The part before the @ looks machine-generated. |
| `dns_unavailable` | 10 | DNS couldn't be reached, so mail servers are unknown. |
| `no_email_authentication` | 8 | The domain publishes neither SPF nor DMARC. |
| `plus_addressed` | 5 | The address uses a +tag. |

## IP reasons

| Code | Impact | Meaning |
|---|---|---|
| `invalid_ip` | 100 | Not a valid IPv4 or IPv6 address. |
| `tor_exit_node` | 85 | A current Tor exit node. |
| `vpn_network` | 60 | A hosting network mainly used by commercial VPNs. |
| `datacenter_ip` | 45–55 | A cloud or hosting provider, not a home or mobile network. |
| `non_public_ip` | 40 | A private or reserved address: you're probably sending your own proxy's IP. |
| `privacy_relay` | 20 | Traffic relayed through a CDN or privacy service. |

## Phone reasons

| Code | Impact | Meaning |
|---|---|---|
| `unparseable_number` | 100 | Not a phone number, or missing its country code. |
| `invalid_number` | 85–95 | Wrong length, or an unassigned range. |
| `premium_rate_number` | 80 | Premium-rate line. |
| `toll_free_number` | 60 | Toll-free business line. |
| `pager_number`, `voicemail_number` | 60 | Can't receive SMS codes. |
| `voip_number` | 50 | VoIP range. |
| `shared_cost_number` | 40 | Business line. |
| `personal_number`, `uan_number` | 30 | Forwarding or switchboard number. |
| `country_mismatch` | 15 | The number's country differs from the one you passed. |

## Domain reasons

The domain check uses the same codes as the email check, with lower impact for "doesn't receive mail" (a website domain may legitimately not use email): `domain_refuses_mail` 60, `no_mail_server` 50.

## Signup cross-checks

| Code | Impact | Meaning |
|---|---|---|
| `automation_pattern` | 40 | Throwaway email from a datacenter or Tor network. |
| `phone_ip_country_mismatch` | 15 | Phone country differs from IP country. |
