Risk and KYC are a standalone decision service that money services enforce, with a split fail posture
Context
Fraud/risk logic and KYC/verification are sensitive, fast-moving, and full of PII — different in cadence and data-handling from the transactional core. Embedding them in commerce-api couples fraud-rule changes to the money service's release cycle, mixes PII into the ledger's datastore, and (as we found) leaves in-core risk controls "designed but not operating." Separately, any risk check placed in the payment path introduces a new dependency: what should happen to a payment when the risk service is unreachable?
ADR 0001 already fixed where the merchant /verify KYC link is hosted; this ADR is the broader boundary decision — that risk/KYC is its own service — and the failure-mode stance that follows from putting it on the money path.
Decision
Run risk and KYC as a standalone decision service (commerce-risk) that owns the rules, the verification/KYC state, and the decision+audit store; the money services (commerce-api and the CDE) are enforcement points that call it and act on its verdict. When that service is unreachable, fail on the reversible side: buyer-facing gates fail open, money-out decisions fail safe.
- Decision point vs enforcement points.
commerce-riskis a separate deployable (its own repo, datastore and runtime) acting as the Policy Decision Point: it holds the risk rules and the decision audit.commerce-api(money/ledger) and thecommerce-payments-edgeCDE (card capture) are the enforcement points that request a verdict and enforce it. Service-to-service calls are authenticated with short-lived service JWTs (issuercommerce-api, audiencecommerce-risk), not shared static keys. - The live enforcement surface is two fraud gates. (1) A pre-sale card signal from the CDE before money moves — it sends the card's HMAC fingerprint (never the PAN) plus BIN6/last4/name/country and gets an
allow/blockverdict. (2) A checkout resolve in commerce-api at the payment hand-off that re-resolves the browser's coarse risk reference server-side (single-use, bound) into an authoritative decision. - Split fail posture. Both buyer-facing gates fail open (a risk-service outage returns "allow"), so a blip cannot block legitimate buyers — a fraudulent sale that slips through is chargeback-recoverable. The money-out side (payouts/limits) is designed to fail safe (hold), because money leaving the platform is not recoverable. The cost of an outage is deliberately placed on whichever side is reversible. (This is the same reasoning as the discount-quote limiter, ADR 0004.)
Consequences
- Positive: risk rules and KYC PII live in one isolated service with its own audit store, deployable and scalable on its own cadence and stack; the money services only consume a verdict.
- Positive: a risk outage degrades checkout to "no extra screening," not "checkout down," while never letting funds leave unscreened.
- Neutral / to revisit — partially realized. Today only the two fraud gates are wired. Risk-as-source-of- truth for payouts, limits and KYC state is designed but not yet enforced: merchant verification and limit state still live in commerce-api (verification requests, status, a local document→limit ladder), and the fail-safe payout decision path is documented but not wired. The boundary is set; the migration of those responsibilities into commerce-risk is outstanding.
- Neutral / to revisit: the risk service is a dependency on the charge path; the fail-open stance mitigates its availability impact for buyers, and the fail-open behaviour is env-gated (an environment fact, not a code fact).
Alternatives considered
- Risk/KYC as a module inside the commerce-api monolith: rejected — in-core controls were "designed but not operating," and embedding them couples fraud-rule changes to the money service, mixes PII into the ledger store, and prevents independent scaling/deploy; a separate decision service isolates all of that.
- A uniform fail posture (everything fails open, or everything fails closed): rejected — fail-closed everywhere breaks checkout on a risk blip (the ADR 0004 lesson), while fail-open everywhere would let funds leave the platform during an outage; splitting by reversibility gets both right.
- Trusting the browser-supplied risk reference at settlement: rejected — the coarse edge reference is re-resolved server-side into the authoritative decision; a client-passed verdict would be forgeable.