Compliance as Code: On-Chain Screening, KYC/AML Programs, and the Travel Rule

What a contract-level sanctions gate can and cannot do — and where it fits inside a real compliance program

SeriesFinance Professional Series
TrackCompliance, Disclosure & Governance
LevelAdvanced
AudienceCompliance officers, BSA/AML program leads, fintech risk and product managers, general counsel
Tagssanctions, KYC-AML, travel-rule, screening, controls
Reading time~9 minutes

Informational, not advice. This briefing is educational. It is not legal, regulatory, or compliance advice, and it does not describe a regulated money-services program. Sanctions and AML obligations vary by jurisdiction and are evolving; consult qualified counsel for your own facts.

The control that lived in the wrong place

Imagine reviewing a peer-to-peer platform’s compliance posture. On paper it screens users: when a customer connects, the interface checks their wallet address against the sanctions list and refuses to proceed if there’s a hit. The Terms of Service describe this as a control. The screen even renders a tidy “blocked” state. As a compliance reviewer, you ask the question that decides whether this is a control or a decoration: what happens if a sanctioned party never loads your interface?

On a public blockchain, the settlement logic is directly reachable. Anyone with a script can call the contracts without ever touching the website. A screen-only check is a suggestion, not an enforcement point. And US sanctions exposure is effectively strict liability — intent and interface design don’t rescue you if prohibited property was accepted. So the compliance-relevant question is never “does the app screen?” It is “where does enforcement actually bind, and can it be routed around?”

This is the lens for understanding programmatic compliance: pushing a specific, narrow control — sanctions and blocklist screening — down to the layer where value actually moves, so it cannot be bypassed. It is powerful and genuinely useful. It is also frequently oversold. The honest version distinguishes carefully between what address screening does and what a full KYC/AML program does, because they are not substitutes.

The traditional model

In a regulated financial institution, sanctions screening is one component of a broader Bank Secrecy Act / anti-money-laundering (BSA/AML) program. That program rests on several pillars: a written program with board-level ownership; Customer Identification and Customer Due Diligence (CIP/CDD) — you verify who the customer is; ongoing transaction monitoring; sanctions screening of parties and transactions against lists such as OFAC’s Specially Designated Nationals; suspicious-activity reporting; recordkeeping; and independent testing. Screening is the gate; identity verification, monitoring, and reporting are the rest of the house.

Layered on top for value transfers is the FATF travel rule (FATF Recommendation 16). It requires that originator and beneficiary information — names, account identifiers, and often address or identifier details — travel with a qualifying transfer between the institutions on each end. It exists so that the sending and receiving institutions can each screen the counterparty and reconstruct a payment’s parties after the fact. Crucially, the travel rule is an obligation on regulated intermediaries (banks, and, in the crypto context, “virtual asset service providers”). It presumes there is an intermediary who takes custody and holds identity data to pass along.

What changes on-chain

A contract-level screening control collapses the gate into the settlement path itself. Instead of a website consulting a list and asking the system to stop, the settlement logic consults a shared screening component on every entry point where money moves in — and simply refuses to complete a transaction from a listed address. Because the check lives with the money, skipping the interface skips nothing.

Three properties are worth a compliance reader’s attention.

Fail-closed by design. A well-built on-chain gate treats an unavailable or malformed answer from its screening source the same as a hit: it blocks. If the sanctions data source can’t give a clean, well-formed “yes” or “no,” the safe posture is to deny rather than wave everyone through. This is the on-chain analogue of a control that defaults to “hold” when the screening system is down — the opposite of the fail-open behavior that quietly creates exposure.

A built-in, immutable audit trail. The screening source combines a public on-chain sanctions oracle (a service that answers, for any address, whether it appears on the US Treasury list) with a discretionary operator blocklist for addresses tied to illicit finance beyond the official set. Every blocklist change — who made it, which address, in which direction, and a human-readable reason — is written permanently to the chain. There is no separate compliance database to reconcile or lose; the event log is the record. For anyone who has assembled evidence for an exam or a lookback, an immutable, timestamped change history is a real operational advantage.

Gate entry, never exit. A deliberate and defensible line: entry paths (putting value in, creating or accepting a position) are screened; exit paths (claiming a refund or a payout already owed) are not. If an address is listed after its stake is already in escrow, screening the exit would trap those funds — converting a screening control into a de facto asset freeze, a heavier and legally distinct act that would make the contract a custodian of blocked property. Refusing new business is not the same as freezing existing property, and conflating the two is a common design error.

Where it genuinely differs — address screening is not identity KYC

Here is the distinction a compliance professional must hold firmly, because marketing language routinely blurs it.

Address screening answers “is this address on a list?” It does not answer “who is this person?” A contract gate checks a wallet against sanctions and blocklists. It performs no identity verification, no beneficial-ownership analysis, no CDD, no sanctions screening of a name behind the address, and no ongoing behavioral monitoring. A newly created wallet with no listing history passes trivially — it is clean as an address while being completely unknown as a person. Address screening is necessary but nowhere near sufficient for a program that must actually know its customer.

This also reframes the travel rule honestly. The travel rule is about intermediaries exchanging identity data about the parties to a transfer. A non-custodial, peer-to-peer settlement layer where users hold their own keys and the platform never takes custody sits differently from a custodial VASP in most current frameworks — but “differently” is not “exempt,” classification is jurisdiction-specific and unsettled, and regulators are actively extending travel-rule expectations into crypto. The correct posture is not to claim the rule doesn’t apply; it is to recognize that a screening gate does not satisfy the travel rule, because the gate transmits no originator/beneficiary identity information at all. These are different controls addressing different obligations.

Risk & controls

  • Oracle/data-source risk. The on-chain sanctions oracle is a centralized, permissioned feed taken as ground truth. Mitigations are structural, not trustless: the gate only reads from it, the source can be replaced if compromised or retired, and the operator blocklist keeps working even if the oracle is unset. No decentralized sanctions feed exists; pretending otherwise helps no one.
  • List-coverage risk. Sanctions lists change; on-chain oracles update on their own cadence; addresses are added faster than any list captures. Screening reduces exposure to known listed addresses. It does nothing about a sanctioned party operating through a fresh, unlisted wallet — the residual risk that only real identity diligence addresses.
  • Availability vs. exposure. Fail-closed means a screening-source outage can halt entry across the platform until an operator restores or reconfigures it. That downtime is the accepted cost: a recoverable outage beats a strict-liability violation.
  • Program completeness. The largest risk is treating the gate as the whole program. Address screening is one layer among many a serious operation still needs — jurisdictional geo-controls, versioned legal terms with recorded consent, and, where the activity and jurisdiction require it, actual identity verification and monitoring. Defense in depth, not defense in one place.

How FairWins approaches this

FairWins treats sanctions screening as a shared, fail-closed building block enforced inside the contracts rather than in the interface. A single screening component — the public sanctions oracle plus an operator-maintained, fully audit-logged blocklist — is consulted at every entry point where value moves: creating and accepting wagers (both sides are re-screened at acceptance), buying or extending memberships, joining pools, and issuing tokens. The interface still checks first for fast feedback, but the enforcement layer is the one no one can route around. Exit paths are intentionally never gated, so a mid-position listing can’t strand funds. FairWins is explicit that this is address screening, not identity KYC, and that participants remain fully subject to applicable law — the gate is a genuine control, not a complete compliance program.

Informational, not advice. Nothing here is a determination that any particular activity is or isn’t subject to the travel rule, VASP registration, or any AML obligation. Those are facts-and-jurisdiction questions for qualified counsel.

Related deep-dive

For the engineering details, see Sanctions Screening as a Shared Building Block.

Further reading

  • FATF Recommendations and the travel rule (Recommendation 16): https://www.fatf-gafi.org/en/publications/Fatfrecommendations/Fatf-recommendations.html
  • FATF guidance on virtual assets and VASPs: https://www.fatf-gafi.org/en/publications/Fatfrecommendations/Guidance-rba-virtual-assets-2021.html
  • US Treasury OFAC sanctions programs and the SDN List: https://ofac.treasury.gov/sanctions-list-service
  • FFIEC BSA/AML Examination Manual (program pillars, CDD, sanctions): https://bsaaml.ffiec.gov/manual
  • Investopedia, “Know Your Client (KYC)”: https://www.investopedia.com/terms/k/knowyourclient.asp

How we added Bitcoin to an EVM-native app (without a seed phrase)

FairWins was born EVM. Every address was twenty bytes of 0x…, every network
was a numeric chainId, every balance read went through an RPC provider, and
every signature came from a passkey-backed ERC-4337 smart account. Then we
decided members should be able to hold, receive, and send actual Bitcoin —
first-class, self-custodied, inside the same account.

Bitcoin is not “another chain” to an EVM app. It’s a different account model
(UTXOs, not balances), a different address universe (four generations of
formats), a different fee market (sat/vB, not gas), and no smart contracts at
all. This post is about the five design decisions that made it fit — and the
guardrails that keep it honest.

1. The wallet nobody has to back up

The hardest question was key management. FairWins deliberately killed the
seed phrase: accounts are WebAuthn passkeys driving P-256 smart accounts.
Bitcoin needs secp256k1 keys. Where do they come from, if not a new mnemonic
we’d force members to write down?

The answer was already in our stack. Our passkey accounts maintain a 32-byte
master seed — created once per account, wrapped under a key derived from
the WebAuthn PRF extension, recoverable on any device with one passkey
ceremony, and shareable across a member’s registered devices. It already
powers our end-to-end encryption. It is, functionally, a seed phrase that
nobody ever sees.

Bitcoin becomes one more consumer of that seed:

masterSeed (32B, PRF-recoverable)
  → HKDF-SHA256(info = "fairwins-btc-seed-v1", 64 bytes)
  → BIP32 root
  → m/84'/0'/0'  native segwit (bc1q…, default)
  → m/86'/0'/0'  taproot      (bc1p…, opt-in)

The HKDF info string gives us domain separation — the Bitcoin tree can never
collide with the encryption keys or any future consumer of the seed. The
paths are bog-standard BIP84/BIP86, which means the wallet is legible to the
wider Bitcoin ecosystem, not a proprietary scheme. Those constants are
frozen in a normative contract in-repo and marked wallet-breaking: funds
live at the derived addresses, so changing them ever requires a versioned
migration, not a refactor.

Recovery falls out for free. Sign in on a new device → one PRF ceremony
unwraps the master seed → standard gap-limit-20 discovery walks the
derivation chain against chain data and rebuilds every address and balance.
There is no Bitcoin backup step because there is no Bitcoin-specific secret.

Everything stays client-side. Private keys and account xpubs never leave the
browser; our backend sees bare addresses (batched, max 50 per call) and
signed raw transactions. That’s the whole interface.

2. A network that isn’t a chainId

Our network registry was a map keyed by numeric EVM chainIds, and dozens of
consumers assume those keys mean “something wagmi can switch to” and
“somewhere contracts are deployed.” The tempting hack — give Bitcoin a fake
number and stuff it in — would have leaked those assumptions everywhere:
contract-address lookups, subgraph routing, wallet switch prompts for a chain
no wallet can switch to.

Instead, Bitcoin lives in a parallel, string-keyed registry: 'bitcoin'
and 'bitcoin-testnet' (testnet4), with their own capability descriptors,
explorer links, and testnet/mainnet pairing. A single type guard —
isBitcoinNetworkId() — sits at every boundary the two worlds share, and a
dedicated test suite pins that no Bitcoin id ever reaches EVM-typed code.

That suite earned its keep before launch: our capability resolver had a
default-network fallback that quietly reported Polygon’s features as
available for unknown ids — meaning Bitcoin would have claimed swap and DAO
support it doesn’t have. The guard-rail test caught it; the fix shipped with
the feature.

Capabilities are self-disclosed and blunt: Bitcoin does portfolio, send,
receive, and Stamps display. It does not do wagers, pools, memberships,
swaps, or gasless anything, and every surface that lists networks says so
rather than hiding the row.

3. A stateless data plane you can turn off

The app needs UTXOs, balances, fee estimates, transaction status, and a
broadcast path. Browsers calling public block explorers directly would leak
members’ full address sets to third parties, with no rate control and CORS
fragility.

We already had a pattern for this: our relay gateway fronts external services
(prediction markets, NFT data) through small self-contained modules with a
fixed pipeline — killswitch → validation → per-IP and global quotas → TTL
cache → normalize to DTOs. Bitcoin became one more module, speaking the
Esplora REST dialect. mempool.space is the default upstream; because Esplora
is the de-facto standard, swapping to blockstream.info or a self-hosted
electrs is a config change, not a code change.

Two details we’d underline for anyone building similar infrastructure:

  • Broadcasts are never retried. A timed-out broadcast may still have
    propagated; a retry loop can double-submit. Reads retry, writes don’t.
  • The whole module is optional. With BTC_ENABLED=false the routes
    return an explicit “disabled” status and every Bitcoin surface in the app
    hides or degrades with an honest message. Ops can also kill it instantly
    mid-incident — and because keys are client-side, members’ funds remain
    theirs and fully recoverable in any standard wallet regardless of what our
    infrastructure is doing.

4. Transactions built like a contract audit

Signing happens entirely in the client with @scure/btc-signer and
@scure/bip32 — audited, zero-WASM libraries from the same family as the
crypto primitives we already ship, so the whole signing path stays auditable
JavaScript in our bundle. We can spend our own P2WPKH and P2TR (key-path)
coins and pay out to every standard destination type: P2PKH, P2SH, bech32 v0,
bech32m v1.

The interesting engineering is in coin selection and fees, where we applied
the same paranoia we’d give a smart contract:

  • Fail-safe classification. Every UTXO is classified before selection:
    spendable, pending (unconfirmed), protected (a Bitcoin Stamp travels
    with it), or unverified (Stamps recognition unavailable). Only
    positively-verified spendable coins may fund a send. This is how Stamps
    protection works: spending a Stamps-bearing UTXO destroys or transfers the
    collectible, so recognition failure defaults to over-protection. The
    degraded state is disclosed in the UI along with exactly how much value is
    protected — total ≠ spendable is always explained, never mysterious.
  • A fee ceiling, enforced twice. Fee quotes (fast/normal/slow, sat/vB)
    expire after 60 seconds. The fee a member confirms becomes a hard ceiling:
    the planner checks it, and the signer independently refuses to produce a
    signature for any transaction whose real fee exceeds it. If the mempool
    moves, the member re-confirms against a fresh quote. The UI can’t promise
    one fee and pay another even if it has a bug — the signing layer won’t let
    it.
  • No dust, no strays. Sub-dust change folds into the fee (reported
    honestly as fee, not vanished); MAX computes everything-spendable-minus-fee
    to the satoshi; selection tests assert exact conservation — inputs equal
    amount + fee + change on every path.
  • Concurrency locks. Coins referenced by an in-flight send are excluded
    from selection until the transaction confirms or is abandoned, so two
    quick sends can’t double-commit the same UTXO. Everything signals RBF.

Address rotation rounds out the receive side: every receive request issues
the next derivation index, cursors never decrease (even against stale local
caches), and old addresses are monitored forever. The local address ledger is
just a cache — chain-driven discovery is the source of truth, which is what
makes recovery trustworthy.

5. Tests that pin the protocol, not the mock

A wallet is the wrong place for “seems to work.” The test strategy leaned on
the fact that Bitcoin’s derivation standards publish reference vectors:

  • Derivation is tested against the official BIP84 and BIP86 vectors, plus
    pinned FairWins fixture vectors that freeze our HKDF domain separation —
    if anyone ever changes a constant, byte-exact assertions fail loudly.
  • Address parsing runs an accept/reject matrix: checksum mutations,
    mainnet/testnet cross-use, EVM addresses pasted by muscle memory, mixed-case
    bech32, unknown witness versions — each with its own member-facing reason,
    because “invalid address” is a uniquely useless error in a four-format
    universe.
  • Coin selection is tested as properties (protected coins never selected,
    MAX leaves zero remainder, no dust outputs exist) rather than examples.
  • The gateway module’s route tests cover the unhappy paths that matter
    operationally: quota breaches, upstream outages, degraded Stamps data,
    cache behavior, and boot-time config validation that fails loudly.

Roughly 250 tests landed with the feature, alongside a security review that
covered key hygiene, fund-loss vectors, and the gateway surface. The review’s
two real findings were both honesty bugs — a capability leak and a gasless
badge that could have appeared on the Bitcoin row — and both were caught by
tests written to enforce disclosure rules, which says something about where
the real risks live in a wallet UI.

What we deliberately didn’t build

No Lightning (on-chain only, for now). No BTC wagers — FairWins escrow is
smart-contract-based and Bitcoin has no contracts; we’d rather not offer a
trust-us version. No server-side anything for keys: no custody, no xpub
sync, no address registry. And no gasless Bitcoin — sponsoring UserOps on an
EVM chain is one thing; misrepresenting who pays a Bitcoin miner is another.
The confirm screen says, in plain words, that you pay the network fee.

The pattern that made all of this tractable: derive from what you already trust, isolate what doesn’t fit, and disclose everything the system can’t do. The passkey seed gave us self-custody without new backup burdens; the
parallel registry kept a UTXO chain from contaminating EVM assumptions; and
the honesty rules — stale-not-zero balances, pending-not-final deposits,
fee ceilings, capability self-disclosure — did more for member safety than
any single cryptographic choice.

Bitcoin is FairWins’ first non-EVM network. The seams we cut for it are the
ones the next one will slide through.