<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OFAC Alert</title>
    <description>The latest articles on DEV Community by OFAC Alert (@ofac_alert).</description>
    <link>https://dev.to/ofac_alert</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3964824%2F2e516174-fe20-4e37-b37b-e7d046e5158c.png</url>
      <title>DEV Community: OFAC Alert</title>
      <link>https://dev.to/ofac_alert</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ofac_alert"/>
    <language>en</language>
    <item>
      <title>DIY OFAC SDN monitoring for crypto addresses — and where it silently breaks</title>
      <dc:creator>OFAC Alert</dc:creator>
      <pubDate>Tue, 02 Jun 2026 14:33:32 +0000</pubDate>
      <link>https://dev.to/ofac_alert/diy-ofac-sdn-monitoring-for-crypto-addresses-and-where-it-silently-breaks-20lm</link>
      <guid>https://dev.to/ofac_alert/diy-ofac-sdn-monitoring-for-crypto-addresses-and-where-it-silently-breaks-20lm</guid>
      <description>&lt;p&gt;If your product touches crypto and you have any AML/sanctions obligation, sooner or later someone asks: &lt;em&gt;"How do we know if an address we interact with lands on the OFAC SDN list?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The reassuring part: the data is &lt;strong&gt;free&lt;/strong&gt;. 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: &lt;em&gt;I'll just poll it myself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 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
&lt;/span&gt;
&lt;span class="n"&gt;sdn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_sdn_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_crypto_addresses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# {"XBT": {...}, "ETH": {...}, ...}
&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_snapshot&lt;/span&gt;
&lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_snapshot&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;my_watched&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a watched address changed on the SDN list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship it on a cron, done? Not quite. Here's where reality leaks in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it silently breaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The diff is harder than &lt;code&gt;==&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Addresses don't compare cleanly across chains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum&lt;/strong&gt; addresses appear in mixed case (EIP-55 checksum) in some sources and lowercase in others. &lt;code&gt;0xAbC…&lt;/code&gt; and &lt;code&gt;0xabc…&lt;/code&gt; are the same address; a naive set diff sees two. Normalize to a canonical form &lt;em&gt;per chain&lt;/em&gt; before diffing, or you'll fire false alerts and miss real ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin&lt;/strong&gt; is the opposite — case &lt;strong&gt;is&lt;/strong&gt; significant, and you've got legacy, P2SH, and bech32 formats for what may be related holdings.&lt;/li&gt;
&lt;li&gt;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 &lt;em&gt;entry&lt;/em&gt; instead of the &lt;em&gt;normalized address&lt;/em&gt;, a reshuffle looks like a churn of adds/removes that aren't real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Delivery is the actual hard part
&lt;/h3&gt;

&lt;p&gt;Detecting the change is maybe 30% of the work. &lt;em&gt;Reliably telling someone&lt;/em&gt; is the other 70%:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A webhook that fails once and isn't retried is a &lt;strong&gt;silent miss&lt;/strong&gt;. Your endpoint was redeploying for 90 seconds; the one alert that mattered fell on the floor.&lt;/li&gt;
&lt;li&gt;No &lt;strong&gt;delivery log&lt;/strong&gt; means you can't answer "were we notified?" — which is exactly the question an examiner or your own incident review will ask.&lt;/li&gt;
&lt;li&gt;Unsigned webhooks mean the receiver can't trust the payload. You want &lt;strong&gt;HMAC-SHA256&lt;/strong&gt; signatures so the other side can verify it's really you.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The watcher dies and nobody watches the watcher
&lt;/h3&gt;

&lt;p&gt;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 &lt;em&gt;looks&lt;/em&gt; fine because no news looks identical to good news. You need a &lt;strong&gt;dead-man's switch&lt;/strong&gt;: something that alarms when the pipeline &lt;em&gt;stops&lt;/em&gt; producing, not just when it finds a change.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Freshness vs. politeness
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "done right" actually requires
&lt;/h2&gt;

&lt;p&gt;If you build it yourself, get these four things right or don't bother:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Or don't build it
&lt;/h2&gt;

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

&lt;p&gt;To be clear about scope: it is &lt;strong&gt;not&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>cryptocurrency</category>
      <category>compliance</category>
      <category>webdev</category>
      <category>api</category>
    </item>
  </channel>
</rss>
