DEV Community

OFAC Alert
OFAC Alert

Posted on

DIY OFAC SDN monitoring for crypto addresses — and where it silently breaks

If your product touches crypto and you have any AML/sanctions obligation, sooner or later someone asks: "How do we know if an address we interact with lands on the OFAC SDN list?"

The reassuring part: the data is free. The U.S. Treasury publishes the Specially Designated Nationals (SDN) list, including the crypto addresses tied to sanctioned entities, as public downloads. Chainalysis even gives away a free sanctions screening API and an on-chain oracle. So the instinct is: I'll just poll it myself.

You can. It's also a deceptively deep little pipeline, and the ways it breaks are quiet — which is the dangerous kind. Here's the honest map of building it yourself.

The naive version

# 1. download the SDN data (XML/CSV from treasury.gov)
# 2. extract the crypto addresses (the "Digital Currency Address" fields)
# 3. compare against the set you saw last time
# 4. if a watched address newly appears (or disappears), alert someone

sdn = fetch_sdn_list()
current = extract_crypto_addresses(sdn)      # {"XBT": {...}, "ETH": {...}, ...}
added   = current - last_snapshot
removed = last_snapshot - current
if my_watched & (added | removed):
    notify("a watched address changed on the SDN list")
save(current)
Enter fullscreen mode Exit fullscreen mode

Ship it on a cron, done? Not quite. Here's where reality leaks in.

Where it silently breaks

1. The diff is harder than ==

Addresses don't compare cleanly across chains:

  • Ethereum addresses appear in mixed case (EIP-55 checksum) in some sources and lowercase in others. 0xAbC… and 0xabc… are the same address; a naive set diff sees two. Normalize to a canonical form per chain before diffing, or you'll fire false alerts and miss real ones.
  • Bitcoin is the opposite — case is significant, and you've got legacy, P2SH, and bech32 formats for what may be related holdings.
  • OFAC re-lists and restructures entries. An address can move between SDN entries, or an entity can be re-added under a new listing. If you key your snapshot on the entry instead of the normalized address, a reshuffle looks like a churn of adds/removes that aren't real.

2. Delivery is the actual hard part

Detecting the change is maybe 30% of the work. Reliably telling someone is the other 70%:

  • A webhook that fails once and isn't retried is a silent miss. Your endpoint was redeploying for 90 seconds; the one alert that mattered fell on the floor.
  • No delivery log means you can't answer "were we notified?" — which is exactly the question an examiner or your own incident review will ask.
  • Unsigned webhooks mean the receiver can't trust the payload. You want HMAC-SHA256 signatures so the other side can verify it's really you.
  • The moment you add email and Telegram as channels, each has its own failure modes (bounces, rate limits, bot token expiry) and you're now running three delivery systems.

3. The watcher dies and nobody watches the watcher

This is the one that actually bites people. Cron jobs fail silently. Treasury tweaks the XML schema and your parser throws — but only in the logs nobody reads. The poller has been dead for three weeks and everything looks fine because no news looks identical to good news. You need a dead-man's switch: something that alarms when the pipeline stops producing, not just when it finds a change.

4. Freshness vs. politeness

How often do you poll? Too rare and you're stale when it counts; too aggressive and you're hammering a government endpoint. You'll want conditional requests (ETag / If-Modified-Since), sane backoff, and a defensible "we re-check every N" story you can put in front of an auditor.

What "done right" actually requires

If you build it yourself, get these four things right or don't bother:

  1. Idempotent diffing on normalized, per-chain canonical addresses — not raw string equality, not entry-keyed snapshots.
  2. Signed webhooks + retries with backoff, plus email/Telegram fan-out that degrades gracefully.
  3. A delivery-status history you can point at to prove every detected change was actually dispatched.
  4. A dead-man's switch on the pipeline itself, because silence is the failure you won't notice.

None of this is exotic. It's just boring, and easy to get 80%-right in a way that fails exactly when it matters. That gap — between "it runs" and "I'd stake an audit on it" — is the whole job.

Or don't build it

I got tired of watching every crypto team rebuild this same plumbing, so I packaged the boring layer as OFAC Alert: hourly-refreshed SDN data, normalized cross-chain diffing, HMAC-signed webhooks with retries, delivery history, batch screening, and a REST API (live docs). If the piece you actually want is "tell me the moment a watched address changes," that's exactly what its OFAC SDN change alerts do. The free tier monitors one address with no signup gate, so you can see the shape of it.

To be clear about scope: it is not a Chainalysis/TRM/Elliptic replacement — no risk scoring, no clustering, no enterprise contract. It's the monitoring-and-delivery layer for the free sanctions data, built once so it's reliable and not your problem.

But honestly — whether you use it or roll your own — get those four things right. The data being free is the easy part. Staking your compliance posture on a cron job is the part that keeps people up at night.

Top comments (0)