<!-- Generated from https://zscreen.zeruai.org/docs/reading-a-verdict by scripts/agent-docs.mjs. Do not edit. -->

# Reading a verdict

The number and the word always agree, and the body tells you which of them was decided by a lookup and which by a model. This page is how to read that.

## The three words

Nothing connects this address to a sanctioned or illicit source, and the model found nothing in its behaviour worth a person's time. It is the only verdict that means what it sounds like.

A person should look. Either the model put this wallet high enough to be worth an hour, or the trail reached somewhere the API refuses to clear on its own, such as a custodian's wallet holding thousands of customers at once.

Exposure to a sanctioned or illicit source, either directly or through an address that has one. When this comes from a list or a contact rule it is a database match, not a prediction, and `ofac_hard_match` tells you which.

## What sets the score

Exactly one thing sets `risk_score`, and `decided_by.rule` names it. The checks run in this order and stop at the first one that fires.

1. **Lists and contacts.** On the OFAC list, on our candidate list, or having sent to or received from any listed address or any known mixer, at any amount. Score 100, verdict FLAGGED, `source` is `rule`. No model runs.
2. **The model.** Nothing above fired, so the score is the model's own 0 to 100 number and the verdict is a pure function of it. Below the review line it is CLEAR, at or above it REVIEW, at or above the flag line FLAGGED.
3. **The floors.** The model said CLEAR but the address is a contract, or the trail ends in exchange custody, or a check could not run. The score is raised to the review line and the verdict becomes REVIEW.

A wallet that shows most of the named behaviours also gets a floor under its score, so a reader is never shown CLEAR next to a list of ticked behaviours. The floor moves the number the verdict is read from; it does not overrule the verdict separately.

The lines are configuration

The review line is 85 and the flag line is 99.1 by default. Both are deployment settings, so derive the verdict from the `verdict` field rather than recomputing it from the score at your end.

## The two kinds of reason

`why` looks like one list and is really two. Every entry carries a `kind`, and the difference between them is the difference between a cause and an observation.

```
"decided_by": {
  "rule": "mixer_contact",
  "source": "rule",
  "reason": "This wallet received funds from a known mixer."
},
"why": [
  {
    "kind": "decided",
    "reason": "This wallet received funds from a known mixer.",
    "pushed": null
  },
  {
    "kind": "model",
    "reason": "Funds were passed along in a chain of one-to-one transfers.",
    "pushed": "up"
  },
  {
    "kind": "model",
    "reason": "A long history with no flagged counterparty anywhere in it.",
    "pushed": "down"
  }
]
```

`kind: "decided"` is the first entry, always. It repeats `decided_by.reason` and it is the only line that explains the score. Its `pushed` is null, because it did not move the number: it set it.

`kind: "model"` is everything the model noticed about the wallet, each with `pushed` saying which way it leaned. In the example above a mixer contact set the verdict, and the two model lines are context: one raising, one lowering. Neither of them caused the FLAGGED.

Do not render them as one list

Flattening the two kinds into a single bulleted list of reasons puts a lowering observation directly under a sanctions match, as though they were weighed against each other. Show the decided line on its own, above the rest.

## Addresses that are never cleared

Three cases come back REVIEW no matter what the model thought, and two of them carry a null score.

- **Contracts.** A contract is not a wallet with a history, so scoring it as one would be a category error.
- **Exchange custody.** The trail arrived at a wallet a custodian holds for many customers at once. Beyond that point one customer is not separable from the rest, and the boundary is named in the caveats.
- **A check that could not run.** Missing input is missing information, and the API will not spend it as evidence in either direction.

## What is not a verdict

Several responses look like answers and are not. Each one carries a null verdict, or no verdict field at all, and a sentence saying so.

- `202`, the wallet has never been screened and is being fetched now.
- `402`, your allowance is spent, so no screening was performed.
- `503`, the store is briefly unreachable and the request never ran.
- A job in state `failed`, which is a failure to gather data.

The one mistake that matters

An address that has not been assessed is not a low-risk address. If your integration defaults to allowing anything that did not come back FLAGGED, an outage becomes an approval. Branch on the presence of a verdict first, and on its value second.

## Building on this

- Read `verdict`. If it is absent or null, you do not have an answer.
- Show `summary` to a person and `decided_by.reason` beside it. They agree by construction.
- Keep `screening_id`. It is how a decision made today is explained six months from now.
- Treat `caveats` as limits on the answer, never as evidence against the wallet.

The full field list is on [The response](https://zscreen.zeruai.org/docs/responses).
