Skip to main content
Implemented 2026-05-21; stance locked 2026-05-08. Plan: docs/superpowers/plans/2026-05-21-sanctions-screening.md.

The stance

Screening uses the public Chainalysis on-chain oracleisSanctioned(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 in siwe-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 enables sanctions_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.
Payer verdicts are never persisted. The screening is identity-blind and must stay that way.

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

There is no Chainalysis on-chain oracle on TRON — it is EVM-only — so T… addresses cannot use the oracle path at all.
_shared/sanctions-tron.ts implements the forced alternative. Two layers, both keyless, identity-blind, and fail-open:
  1. Tether USDT isBlackListed — an on-chain read via TronGrid triggerconstantcontract. 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’s transferFrom would revert anyway.
  2. TRM Labs’ free keyless Sanctions API — for OFAC parity. Rate limited to 1 request per second and 100 per day.
This is a documented deviation from the EVM oracle-only stance, forced by the absence of an oracle. It gates TRON mainnet only; testnet and demo proceed without it.

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.
This rebuts the “the swap cleans the tokens” intuition, which comes up regularly.

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 and docs/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.