> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truthfactor.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scoring and banding

> How the score rolls up from the signals, the published table, band overrides, and worked examples.

`score` is an explainable roll-up over the returned signals: it is **reproducible** from the
`signals` shown plus the published table below. Phase 1 is a transparent rule engine with
**equal weight for every signal**, so the base score is the mean of the resolved contributions.

## The published table

The table is versioned data, published alongside the scorer. A score is reproducible from the
`signals` shown plus this table; calibrating the numbers is a data change and a version bump,
not a silent re-scoring.

| Element | Value |
| - | - |
| Table version | `1.1.0` |
| Contribution, `trusted: true` | `50 + confidence/2` (50–100) |
| Contribution, `trusted: false` | `50 − confidence/2` (0–50) |
| Contribution, `trusted: null` | excluded from the denominator |
| Per-signal weight (Phase 1) | `1` (equal) |
| Base score | mean of the resolved signals' contributions |
| Rounding | `half_up` — a tie rounds to the larger integer (`96.5` → `97`) |
| Band: `clear` | score ≥ 75 |
| Band: `caution` | 45 ≤ score \< 75 |
| Band: `high_risk` | score \< 45 |

> [!NOTE]
>
> The response does **not** carry the table version today — `data[].{score, band, reason,
>     signals, input, correlation_id}` is the whole scored item — so recompute against the version
> published here.

## Band overrides and `reason`

A dominant check can override the band and set the `reason`. When several apply, the first one
here sets the `reason`:

1. any `*_name_match` = `false` → `reason: name_mismatch`, band forced to `high_risk`;
2. any `*_profile_match` = `false` → `reason: profile_contradiction`, band forced to `high_risk`;
3. `phone_connectivity` or `premium_phone_connectivity` = `false` → `reason: phone_disconnected` (band from the score);
4. `phone_type` = `false` or `phone_ported` = `false` → `reason: line_risk` (band from the score).

The precedence settles the **`reason` only**. The band is the **most severe band named by any
override that fired**, and only where that band is more severe than the score-derived one. A
false `*_name_match` therefore lands `high_risk` even when another row publishes the `reason`.

If no override fires, `reason` is the **lowest-scoring resolved signal**, named by its signal key.

## Recomputing a score

Take the `signals` map, turn each resolved verdict into a contribution with the table, average
them, round with the `half_up` rule, then apply the overrides.

> [!WARNING]
>
> `half_up` is applied to the **exact** decimal weighted mean. Recompute in decimal, not binary
> floating point: once a calibrated table carries a fractional weight, ordinary `double`
> arithmetic can land an exact `x.5` just under the tie and round the wrong way. A decimal or
> rational library reproduces the published integer; `double` may not.

## Worked example — a consistent identity

The requested email resolves to no one, so `email_name_match` is `null` and drops out of the
denominator.

```json Result (GET /v1/signals/rq_7a1d) theme={null}
{
  "status": "complete",
  "data": [
    {
      "correlation_id": "signup-3391",
      "status": "scored",
      "score": 94,
      "band": "clear",
      "reason": "phone_profile_match",
      "signals": {
        "phone_name_match": { "trusted": true, "confidence": 90 },
        "phone_connectivity": { "trusted": true, "confidence": 98 },
        "phone_profile_match": { "trusted": true, "confidence": 76 },
        "email_name_match": { "trusted": null, "confidence": null }
      }
    }
  ]
}
```

**Recompute:** contributions `50 + 90/2 = 95`, `50 + 98/2 = 99`, `50 + 76/2 = 88`;
`email_name_match` is `null` and excluded. Mean `(95 + 99 + 88) / 3 = 94` → **`score: 94`**,
band `clear` (≥ 75). No override fired, so `reason` is the lowest-scoring resolved signal,
`phone_profile_match` (88).

## Worked example — a name mismatch

The phone is live but belongs to a different name and profile, so `name_mismatch` fires and
forces `high_risk`.

```json Delivery theme={null}
{
  "status": "complete",
  "data": [
    {
      "correlation_id": "order-5567",
      "status": "scored",
      "score": 36,
      "band": "high_risk",
      "reason": "name_mismatch",
      "signals": {
        "phone_name_match": { "trusted": false, "confidence": 100 },
        "phone_connectivity": { "trusted": true, "confidence": 94 },
        "phone_profile_match": { "trusted": false, "confidence": 78 }
      }
    }
  ]
}
```

**Recompute:** contributions `50 − 100/2 = 0`, `50 + 94/2 = 97`, `50 − 78/2 = 11`. Mean
`(0 + 97 + 11) / 3 = 36`. `phone_name_match` is `false`, so `name_mismatch` fires and forces
`high_risk` regardless of the score → **`score: 36`, band `high_risk`, reason `name_mismatch`**.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.