Write your first policy
Three bands, one floor, and a reason code that overrides all of them.
A policy is bands read from the top score down, plus reason codes that win regardless — and it must have a band at minScore: 0 or it will be rejected.
{
"bands": [
{ "minScore": 1, "outcome": "release", "label": "clean pass" },
{ "minScore": 0.8, "outcome": "partial_release", "releasedBps": 5000 },
{ "minScore": 0, "outcome": "escalate", "label": "anything else" }
],
"alwaysRefund": ["source_not_found"]
}Start from the default
Every organisation without its own policy gets this, and it is the strictest sensible rule:
{
"bands": [
{ "minScore": 1, "outcome": "release", "label": "clean pass" },
{ "minScore": 0, "outcome": "escalate", "label": "anything else" }
]
}Only a clean pass pays. Everything else waits for a person. If you are unsure what you want, this is the right thing to be unsure with — it never pays for something nobody checked.
Add a middle band when the queue gets loud
The default sends every imperfect result to a human. Once you can see which imperfections are routine, give them an outcome:
{
"bands": [
{ "minScore": 1, "outcome": "release", "label": "clean pass" },
{ "minScore": 0.8, "outcome": "partial_release", "releasedBps": 5000, "label": "mostly there" },
{ "minScore": 0, "outcome": "escalate", "label": "anything else" }
]
}Bands sort by minScore descending and the first one the score reaches wins. A
score of 0.9 hits the middle band and pays half.
The floor band is mandatory
Leave out minScore: 0 and the policy is rejected at write time with
policy_no_floor_band. A low score would otherwise match nothing, and a policy
that can decline to decide would leave funds held indefinitely.
Reason codes beat the score
{
"alwaysRefund": ["source_not_found"],
"alwaysEscalate": ["source_unreachable"]
}alwaysRefund is checked first, then alwaysEscalate, then the bands.
The ordering matters. "The source did not exist" is not a quality judgement, and a band should not be allowed to pay 50% on it because two of four clauses happened to pass. A reason code says what went wrong; a score says how much went right.
source_unreachable is the usual candidate for alwaysEscalate: the check
could not be completed, so both paying and refusing would be deciding on
evidence that does not exist.
Two scales in one object
| Field | Scale |
|---|---|
minScore | Fraction, 0 to 1 |
releasedBps | Basis points, 0 to 10000 |
0.8 and 5000 in the same band are not a typo. minScore is compared against
the engine's score, which is a fraction; releasedBps is a share of an amount.
The field names are the only thing telling them apart.
releasedBps must be strictly between 0 and 10000 — zero is a refund and 10000
is a release, so neither needs a partial band.
Cap the size
{ "maxAmount": "50000000" }50.00 USDC, as an integer string of the smallest unit, ^[0-9]+$. A warrant
above it is refused at open.
"50000000" 50.00 USDC
50000000 rejected — a JSON number
"50.00" rejected — not the smallest unitVersions, not edits
A policy is never edited in place. New rules create a new version, and a
warrant pins the version it ran under, with the rules_hash recorded on the
trace. A settlement from March can be re-read against exactly the rules that
produced it.
Policies are written in the console — there is no POST /v1/policies. See
the API reference.