Escalations
What happens when a check does not cleanly pass.
An escalation is a warrant handed to a person, with the funds still held and the deadline still running.
{
"id": "esc_01J8Z3M4YQ7K2V9X",
"warrant_id": "wrt_01J8Z3M4YQ7K2V9X",
"amount": "8400000",
"asset": "USDC",
"payee": "api.exampledata.io",
"reason_codes": ["data_stale"],
"score": 6667,
"opened_at": "2026-08-15T12:00:00.000Z",
"resolved_at": null,
"resolution": null
}Escalating is a decision, not a failure to decide
A policy resolves to escalate when the score lands in a
band it will not settle automatically, or when a reason code appears in
alwaysEscalate. Both are the policy working, not the policy giving up.
The money stays held throughout. Nothing about an open escalation is ambiguous
from the ledger's point of view: the funds are in held, they are not in
payee, and they are not back in available.
The four resolutions
resolution | What happens |
|---|---|
released | The payee is paid in full. |
partially_released | A share is paid, the remainder returns to available. |
refunded | Nothing is paid; the whole amount returns to available. |
expired | The deadline passed with nobody deciding. |
The API accepts release, partial_release and refund. expired is not
something a person can choose — it is what the sweep records when the clock runs
out.
released_bps is required on a partial release and must be strictly between 0
and 10000. Zero is a refund and 10000 is a release, so neither needs a third
name.
Nothing waits forever
POST /internal/jobs/expire sweeps overdue warrants. An escalation nobody
resolves does not sit indefinitely: the warrant goes held → expired → refunded
and the money is back in available before the sweep returns.
This is the invariant the whole product rests on. Every path out of an escalation ends with the funds somewhere definite, and no path leaves them held.
Above the threshold, a person signs
An organisation can set passkey_threshold. A settlement at or above it does
not complete on the first request: the API answers with a WebAuthn challenge,
and the decision only lands when the browser returns an assertion.
{
"passkey_required": true,
"challenge": "…",
"scope": "resolve"
}Resubmit with assertion carrying challenge, credential_id and signature.
An API key can never do this. Keys carry no role and cannot resolve an escalation at all — if they could, the threshold would be decorative, since anyone holding a key could route around it.
The challenge is consumed on use, so a captured assertion cannot be replayed, and a passkey whose signature counter fails to advance is rejected as a probable clone.
What a notification carries
An escalation notifies everyone who can act on it — operators and above, since a viewer cannot resolve one and paging them is noise. The notification carries four facts, and they are chosen so the decision can be made from a lock screen: amount, payee, which clause failed, and how long is left.
Approving still requires opening the console and using a passkey. A Telegram message or a push notification can carry the escalation; neither can release funds.
Amounts
Integer strings of the asset's smallest unit, matching ^[0-9]+$.
"8400000" 8.40 USDC
8400000 rejected — a JSON number
"8.40" rejected — not the smallest unitscore is basis points, not a fraction: 6667 means two clauses of three
passed. It shares the integer discipline of amounts for the same reason — a
threshold comparison against a float is a threshold you cannot reason about.