<?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: Nikolaos Dimitriadis</title>
    <description>The latest articles on DEV Community by Nikolaos Dimitriadis (@nikoble1926).</description>
    <link>https://dev.to/nikoble1926</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4169058%2F3e5cec25-63ad-471a-8bfb-3290d13e186b.jpg</url>
      <title>DEV Community: Nikolaos Dimitriadis</title>
      <link>https://dev.to/nikoble1926</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nikoble1926"/>
    <language>en</language>
    <item>
      <title>Check the payee before your AI agent pays it (x402, Node and Python)</title>
      <dc:creator>Nikolaos Dimitriadis</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:37:09 +0000</pubDate>
      <link>https://dev.to/nikoble1926/check-the-payee-before-your-ai-agent-pays-it-x402-node-and-python-671</link>
      <guid>https://dev.to/nikoble1926/check-the-payee-before-your-ai-agent-pays-it-x402-node-and-python-671</guid>
      <description>&lt;p&gt;An AI agent that pays over x402 reads a &lt;code&gt;payTo&lt;/code&gt; address from a 402 response and sends USDC to it. Some agent wallets already let the owner set a daily budget, a per transaction limit and approved domains. A check on the address itself is a separate question: is the wallet you are about to pay on a sanctions list?&lt;/p&gt;

&lt;p&gt;This post shows a small pattern for that, with working Node and Python examples. The full code is here: &lt;a href="https://github.com/Nikoble1926/sanctions-check-before-pay" rel="noopener noreferrer"&gt;https://github.com/Nikoble1926/sanctions-check-before-pay&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check does
&lt;/h2&gt;

&lt;p&gt;One paid call screens a wallet address against four lists: OFAC SDN, UN Consolidated, EU and UK. The response is signed and reports each list separately, matched or clear, with that list's version date. It costs 0.01 USDC per call, paid over x402 itself, on Base or Solana. No account, no API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://sanctions.nsgoods.org/screen-multi?address=&amp;lt;payTo&amp;gt;&amp;amp;chain=&amp;lt;optional hint&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a free signed sample of the exact response shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://sanctions.nsgoods.org/screen-multi/preview
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The one rule that matters: fail closed
&lt;/h2&gt;

&lt;p&gt;The response has a headline &lt;code&gt;verdict&lt;/code&gt; (&lt;code&gt;clean&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt; or &lt;code&gt;indeterminate_ofac_unavailable&lt;/code&gt;) and an &lt;code&gt;any_list_match&lt;/code&gt; flag that covers all four lists. The headline verdict on its own only speaks for OFAC. So the decision uses both, and it treats everything that is not an explicit clean answer as a no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;any_list_match&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;indeterminate_ofac_unavailable&lt;/code&gt;, a 400, a 5xx, a timeout or a body that is not JSON all mean: do not pay. We had this wrong in our first draft. The script said "pay" when the OFAC feed was unavailable, which is the worst possible failure for a check like this. A check that cannot run must never turn into a yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;Node 20 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;EVM_PRIVATE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x... node check-before-pay.mjs &amp;lt;payTo address&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;chain]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nv"&gt;EVM_PRIVATE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0x... python check_before_pay.py &amp;lt;payTo address&amp;gt; &lt;span class="o"&gt;[&lt;/span&gt;chain]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 0 means clean, go ahead. Any other exit code means do not pay, so you can drop it in front of the payment step of an agent or a script. Without &lt;code&gt;EVM_PRIVATE_KEY&lt;/code&gt; both scripts stop at the 402 and print the price, so you can try them without a wallet. Use a low balance hot wallet for the key, never your main one.&lt;/p&gt;

&lt;p&gt;Two things we only found by testing on a clean machine, in case they save you time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;With &lt;code&gt;@x402/axios&lt;/code&gt;, the payment happens when the first 402 comes back as an error. If you set &lt;code&gt;validateStatus: () =&amp;gt; true&lt;/code&gt; on the paying client, the 402 counts as a success and the client never pays. The example uses &lt;code&gt;validateStatus: s =&amp;gt; s !== 402&lt;/code&gt;, so 400 and 5xx still do not throw.&lt;/li&gt;
&lt;li&gt;The Python x402 EVM signer needs &lt;code&gt;web3&lt;/code&gt; installed (the &lt;code&gt;x402[evm]&lt;/code&gt; extra). Without it, the import fails before anything runs. The &lt;code&gt;requirements.txt&lt;/code&gt; in the repo pins it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Verify the answer yourself
&lt;/h2&gt;

&lt;p&gt;The response is signed, so you do not have to trust the transport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node verify-signature.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fetches the free preview and recovers the signer offline. The service signs the body without &lt;code&gt;signed_by&lt;/code&gt; and &lt;code&gt;signature&lt;/code&gt;, as compact JSON with sorted keys and non ASCII characters escaped, with an Ethereum personal_sign. Expected signer: &lt;code&gt;0x57fF0F084Cba33e6761503f90eEF0Da9F159350c&lt;/code&gt;. The one trap here: the escaping. JavaScript's &lt;code&gt;JSON.stringify&lt;/code&gt; leaves non ASCII characters as they are, so the verifier escapes them by hand to match.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;Not legal advice. Passing the rule means the address is not on these four lists at their stated version dates, nothing more. It says nothing about who controls the wallet or where its funds came from.&lt;/p&gt;

&lt;p&gt;Code, MIT licensed: &lt;a href="https://github.com/Nikoble1926/sanctions-check-before-pay" rel="noopener noreferrer"&gt;https://github.com/Nikoble1926/sanctions-check-before-pay&lt;/a&gt;&lt;br&gt;
More about the service: &lt;a href="https://x402.nsgoods.org" rel="noopener noreferrer"&gt;https://x402.nsgoods.org&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>javascript</category>
      <category>python</category>
    </item>
  </channel>
</rss>
