docs/superpowers/plans/2026-05-21-sanctions-screening.md.
The stance
Screening uses the public Chainalysis on-chain oracle —isSanctioned(address) — not the REST Sanctions API.
The reasoning is structural rather than technical: the oracle is permissionless. No API key, no signup, no terms that can be changed to paid or revoked. It is queried through the existing eth_call infrastructure, so it introduces no new vendor dependency in the payment path.
OFAC lists are chain-agnostic, so every address is screened against the canonical Ethereum-mainnet oracle at 0x40C5…8fb, with a Base override at 0x3A91…739B. Selector 0xdf592f7d.
The trade-off: the oracle is boolean-only, with no OFAC name or description. That is fine — a block-or-allow decision needs only the boolean.
Two layers
Merchant wallets — default on, platform-level
Screened insiwe-verify at sign-in for merchant-role wallets, plus a daily re-scan cron at 03:17 UTC. A sanctioned wallet gets 403 wallet_blocked with no JWT and no cookie. The verdict is cached on merchants.sanctions_status (clean | sanctioned | pending) with sanctions_checked_at.
On-chain
setRecipient cannot be blocked off-chain — the registry is ownerless and permissionless by design. The platform’s control point is therefore the sign-in gate, not the chain.Customer wallets — default off, per-merchant opt-in
The merchant enablessanctions_screening in Settings → Compliance. When enabled, create-payment-intent screens the payer wallet plumbed from the widget’s connected account. A sanctioned payer receives a deliberately vague payment_blocked error code, to avoid coaching evasion.
Both layers fail open
screenAddress returns {kind: 'unknown'} on RPC outage, and the payment proceeds.
This is deliberate. A screening outage should not halt legitimate commerce, and the merchant sign-in gate remains the platform-level enforcement point regardless.
TRON deviation
_shared/sanctions-tron.ts implements the forced alternative. Two layers, both keyless, identity-blind, and fail-open:
- Tether USDT
isBlackListed— an on-chain read via TronGridtriggerconstantcontract. This is Tether’s freeze set, not OFAC — roughly a superset, and it can lag. It doubles as a UX pre-check, since a blacklisted payer’stransferFromwould revert anyway. - TRM Labs’ free keyless Sanctions API — for OFAC parity. Rate limited to 1 request per second and 100 per day.
Pre-commit screening
Before a recipient, treasury, or partner address is baked into a pool’s immutable init code, all of them are screened, per VM. This is self-protection: a sanctioned or frozen address inside an immutable pool permanently locks funds for every party in that pool. The partner is screened here regardless of sign-in history, because a referrer auto-created from a code may never have signed in.The framing
Repeat consistently across docs, landing pages, and sales:Sanctions-clean. KYC-free. We screen the wallet, not the person.
A common misconception, rebutted
A DEX swap inside
RouterFee.payWithSwap does not clean the on-chain attribution. The merchant’s transaction history still shows the sanctioned-payer trigger, and Coinbase, Binance, and Kraken inbound-deposit screening will flag it on cash-out.Regulatory framing
Sanctions screening is a list lookup, not a regulated crypto-asset service under MiCA Article 3(1). Refusing service to a sanctioned wallet is the negation of a regulated service, not the provision of one. Full analysis in MiCA position anddocs/MICA_REGULATORY_POSITION.md §5b.
Code
_shared/sanctions.ts and its tests, _shared/sanctions-tron.ts, create-payment-intent, siwe-verify/handler.ts, cron-rescan-sanctions. Migrations 20260521010000 for the columns and 20260521020000 for the cron.