Publish a policy
A policy decides about money, so getting one live is deliberately a sequence of proofs: it parses, it is internally consistent, it was measured against your own history, a second person agreed, and the publisher proved who they were.
Click any step to read what happens there and what can refuse it.
The rules language
A definition is JSON:
{
"policy_type": "ROUTING",
"label": "review disputed orders",
"rules": [
{
"id": "open-dispute-on-order-review",
"description": "An order with an open dispute is reviewed before more money is taken for it.",
"when": { "feature": "dispute.openCountOnOrder", "op": "gte", "value": 1 },
"then": { "decision": "REVIEW", "reasons": ["OPEN_DISPUTE_ON_ORDER"], "actions": [] },
"weight": 300
}
],
"defaults": { "decision": "ALLOW", "reasons": ["POLICY_DEFAULT"] }
}
- Rules evaluate in order. The first whose
whenmatches supplies the decision, reasons and actions. No match →defaults. whenis a predicate{feature, op, value}or a combinator:{all: […]},{any: […]},{not: …}.- Operators:
eq,neq,gt,gte,lt,lte(numbers only),in,notIn,exists. A missing feature makes every predicate false exceptexists: false. - Decisions:
ALLOW,STEP_UP,HOLD,REROUTE,REVIEW,DENY. - Reasons must come from the registry. Actions (TRUST only) include
REQUIRE_SECOND_APPROVER,HOLD_PAYOUT,REQUIRE_ADMIN_APPROVAL,REQUIRE_STEP_UP,NOTIFY_OLD_CHANNEL,NOTIFY_NEW_CHANNEL,DELAY_PAYOUT_24H,DELAY_PAYOUT_72H. - Weight (0 to 1000) contributes to a score summed over every matched rule. The score never changes the decision; it is a severity accumulator shown on the record.
Features you can reference
| Feature | Type | Applies to | Meaning |
|---|---|---|---|
intent.amountMinor, order.amountMinor | number | intents | Amount in minor units |
order.category, order.channel | string | intents | What and where; null if you did not supply them |
intent.methodFamily | string | intents | CARD or PAYNOW |
intent.attemptCount | number | intents | Attempts so far |
dispute.openCountOnOrder | number | intents | Unresolved disputes on the order |
payout.amountMinor, payout.currency | number, string | payouts | The instruction |
payout.amountVsBaselineRatio | number | payouts | Amount ÷ the seller's average successful payout over 90 days; null with no baseline |
seller.verificationLevel, seller.status, seller.ageDays, seller.openCaseCount | string, string, number, number | all | The seller |
beneficiary.isNew, beneficiary.ageDays, beneficiary.changeCount30d | boolean, number, number | changes, payouts | Never paid successfully; days since first seen; change requests in 30 days |
beneficiary.nameMatchLevel | string | changes, payouts | EXACT, PARTIAL (provider-verified only), LOCAL_NORMALIZED, NONE, UNCHECKED |
beneficiaryChange.coolingOffSatisfied, beneficiaryChange.hoursSinceRequested | boolean, number | changes, payouts | The pending change's clock |
fulfillment.bestEvidenceTier, fulfillment.hasReversal | number, boolean | intents, payouts | Strongest qualifying evidence tier in scope (2 = signed platform event, 3 = buyer confirmation); whether any was reversed |
session.recentCredentialChange | boolean | changes | The requester changed credentials in the last 24 hours |
case.openCountOnSubject | number | all | Open cases on this subject |
connection.provider, connection.healthState | string | intents, payouts | For analysis of the executed route, not for choosing it |
Per-candidate routing eligibility (capability, currency, amount range, circuit state) is the routing engine's job, not a policy's. A ROUTING policy decides whether and how strictly to route, not which connection wins.
What the templates do
trust_baseline_protective is the recommended first TRUST policy. Its rules, in order:
- A payout or change while a beneficiary change is still cooling off →
HOLD(COOLING_OFF_ACTIVE). - A payout at 3× the seller's baseline or above SGD 10,000 without at least tier-2 fulfillment evidence →
HOLDwithREQUIRE_SECOND_APPROVER(PAYOUT_AMOUNT_ABOVE_THRESHOLD,FULFILLMENT_EVIDENCE_TIER_TOO_LOW). - First payout to a new beneficiary →
STEP_UP(NEW_BENEFICIARY). - A request from an account that changed credentials recently →
STEP_UP(ATO_SESSION_RULE).
trust_permissive_shadow fires the same signals but always ALLOWs, so you can watch what would have held on live test traffic before you publish the protective one. Publish the shadow first, run traffic, read the reason codes on your payouts, then publish the protective policy.
Rolling back
On a retired version's page: Request rollback opens an approval bound to both ends of the move (from the current head, to this version). A second person approves. Roll back executes with a fresh second factor. If anything publishes in between, the approval is stale and refused. Definitions never change; only the head pointer moves.