<?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: verdixapi</title>
    <description>The latest articles on DEV Community by verdixapi (@verdixapi).</description>
    <link>https://dev.to/verdixapi</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%2F4169715%2F1af293f6-ac1b-4fd6-91f1-c890fd4a7bae.png</url>
      <title>DEV Community: verdixapi</title>
      <link>https://dev.to/verdixapi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/verdixapi"/>
    <language>en</language>
    <item>
      <title>Check the recipient before your agent signs: Base and Ethereum</title>
      <dc:creator>verdixapi</dc:creator>
      <pubDate>Sun, 01 Nov 2026 22:11:24 +0000</pubDate>
      <link>https://dev.to/verdixapi/check-the-recipient-before-your-agent-signs-base-and-ethereum-2j3a</link>
      <guid>https://dev.to/verdixapi/check-the-recipient-before-your-agent-signs-base-and-ethereum-2j3a</guid>
      <description>&lt;p&gt;&lt;em&gt;By Lauras, founder of &lt;a href="https://api.verdixapi.com" rel="noopener noreferrer"&gt;Verdix&lt;/a&gt;. Disclosure: Verdix is the address-risk API that the code below calls. I'm not a neutral reviewer of it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If your agent can pay over &lt;a href="https://x402.org" rel="noopener noreferrer"&gt;x402&lt;/a&gt; or sign USDC transfers, it can also pay the wrong address. Nobody has to break in for that to happen. Someone only has to influence which address ends up in the payment.&lt;/p&gt;

&lt;p&gt;This post covers three ways that happens, why the check belongs before the signature, and how to add one. It also covers what we added for Ethereum this week and, just as important, what that Ethereum check does not do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways an agent ends up paying the wrong address
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Prompt injection
&lt;/h3&gt;

&lt;p&gt;An LLM decides what to do from text, and some of that text comes from people you don't control: web pages, emails, tool results, other agents. Whoever writes that text can steer the agent.&lt;/p&gt;

&lt;p&gt;The best-known public demo is &lt;a href="https://the-decoder.com/hacker-wins-47000-by-tricking-ai-chatbot-with-smart-prompting/" rel="noopener noreferrer"&gt;Freysa&lt;/a&gt;, an agent released in November 2024 with one rule: never approve a transfer of its prize pool. After 481 failed attempts, message 482 got it to do exactly that. The attacker convinced it that &lt;code&gt;approveTransfer&lt;/code&gt; handled &lt;em&gt;incoming&lt;/em&gt; payments, then announced a fake deposit. The agent sent its whole balance, 13.19 ETH (about $47,000).&lt;/p&gt;

&lt;p&gt;The lesson isn't that this model was weak. A rule written in the prompt is enforced only by the model that can be talked out of it. If the payment path has no check outside the model, a good enough injection can get through.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Address poisoning
&lt;/h3&gt;

&lt;p&gt;Addresses are 40 hex characters, and people (and agents) copy them from transaction history. An attacker generates a lookalike address with the same first and last characters as one you've used before, then sends you a tiny or zero-value transfer from it, or a transfer of a fake token named like a real one, so it shows up in your history.&lt;/p&gt;

&lt;p&gt;On 3 May 2024, a holder &lt;a href="https://www.coindesk.com/business/2024/05/03/exploiter-steals-68m-worth-of-crypto-through-address-poisoning" rel="noopener noreferrer"&gt;sent 1,155 WBTC, about $68M, to a lookalike address&lt;/a&gt; that matched the first and last six characters of the intended recipient. The funds were later returned after negotiation, which is rare.&lt;/p&gt;

&lt;p&gt;It isn't a one-off. A USENIX Security 2025 paper, &lt;a href="https://arxiv.org/abs/2501.16681" rel="noopener noreferrer"&gt;&lt;em&gt;Blockchain Address Poisoning&lt;/em&gt;&lt;/a&gt;, measured two years on Ethereum and BSC: 270M attack attempts targeting 17M victims, and 6,633 incidents with at least $83.8M in losses.&lt;/p&gt;

&lt;p&gt;An agent that takes "the address we paid last time" from history or logs is exactly the user this attack was built for. It also doesn't get the feeling that something looks off.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sanctioned addresses
&lt;/h3&gt;

&lt;p&gt;OFAC's SDN list includes crypto addresses. For example, the &lt;a href="https://ofac.treasury.gov/recent-actions/20220414" rel="noopener noreferrer"&gt;14 April 2022 update&lt;/a&gt; added an Ethereum address for the Lazarus Group: &lt;code&gt;0x098B716B8Aaf21512996dC57EB0615e2383E2f96&lt;/code&gt;. The &lt;a href="https://github.com/0xB10C/ofac-sanctioned-digital-currency-addresses" rel="noopener noreferrer"&gt;0xB10C/ofac-sanctioned-digital-currency-addresses&lt;/a&gt; project extracts these addresses from Treasury's XML every night.&lt;/p&gt;

&lt;p&gt;Here nothing has to trick your agent. An address can be perfectly valid, the payment can go through, and you have still paid a party you may be legally barred from paying. Whether that applies to you is a question for your lawyer, but checking the list costs very little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the check goes before the signature
&lt;/h2&gt;

&lt;p&gt;An x402 payment is an EIP-3009 authorization that the facilitator settles on-chain. A USDC transfer is a transaction. Once either is signed and broadcast, you can't take it back. A post-payment alert only tells you what you lost.&lt;/p&gt;

&lt;p&gt;So the check has to run in the signing path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;outside the model&lt;/strong&gt;, so a prompt can't talk it out of running;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;on the final recipient&lt;/strong&gt;, the &lt;code&gt;payTo&lt;/code&gt; or &lt;code&gt;to&lt;/code&gt; that will actually be signed, not the name the model was given;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;failing closed&lt;/strong&gt;, so an unanswered check blocks the payment instead of waving it through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing it with verdix-guard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/verdix-guard" rel="noopener noreferrer"&gt;&lt;code&gt;verdix-guard&lt;/code&gt;&lt;/a&gt; (MIT, &lt;a href="https://github.com/verdixapi/verdix-guard" rel="noopener noreferrer"&gt;source&lt;/a&gt;) hooks into two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;x402 payments&lt;/strong&gt;: it registers an &lt;code&gt;onBeforePaymentCreation&lt;/code&gt; hook on the official &lt;code&gt;@x402&lt;/code&gt; client, so the &lt;code&gt;payTo&lt;/code&gt; is screened before the payment payload is created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any transfer&lt;/strong&gt;: it wraps a viem account and screens native transfers, ERC-20 &lt;code&gt;transfer&lt;/code&gt; / &lt;code&gt;transferFrom&lt;/code&gt; / &lt;code&gt;approve&lt;/code&gt;, and EIP-3009 / EIP-2612 signatures before signing.
&lt;/li&gt;
&lt;/ul&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;verdix-guard @x402/fetch @x402/evm viem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For x402:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ExactEvmScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/evm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wrapFetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x402Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKeyToAccount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viem/accounts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guardX402Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verdix-guard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;privateKeyToAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT_KEY&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;x402Client&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eip155:8453&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactEvmScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;guardX402Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchWithPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapFetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://seller.example/paid-endpoint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For direct transfers with viem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guardAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VerdixGuardBlockedError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verdix-guard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guardedAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;guardAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// use it in your walletClient&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;guardedAccount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;VerdixGuardBlockedError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A blocked x402 request throws an error containing &lt;code&gt;verdix-guard: &amp;lt;reason&amp;gt;&lt;/code&gt;. A guarded account throws &lt;code&gt;VerdixGuardBlockedError&lt;/code&gt;. In both cases nothing is signed.&lt;/p&gt;

&lt;p&gt;There is no API key. Each check is itself an x402 payment: $0.01 in USDC on Base at the default &lt;code&gt;lite&lt;/code&gt; tier, paid from a wallet you choose. Answers are cached for 24 hours per address, and spending on checks is capped at &lt;code&gt;dailyBudgetUsd&lt;/code&gt; (default $1). USDC payments on Base below &lt;code&gt;minAmountUsd&lt;/code&gt; (default $0.10) aren't checked. Amounts the guard can't price in USD, such as ETH or other tokens, always are.&lt;/p&gt;

&lt;p&gt;What it decides by default:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;danger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;caution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block (`onCaution: 'allow' \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;no_known_risk&lt;/code&gt; / &lt;code&gt;safe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no usable answer&lt;/td&gt;
&lt;td&gt;block (&lt;code&gt;failMode: 'allow'&lt;/code&gt; to change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;To check the published package end to end, I installed &lt;code&gt;verdix-guard@0.1.0&lt;/code&gt; from npm into an empty directory. I wrapped a test wallet with &lt;code&gt;guardAccount&lt;/code&gt;, using &lt;code&gt;tier: 'lite'&lt;/code&gt;, &lt;code&gt;minAmountUsd: 0&lt;/code&gt; and &lt;code&gt;dailyBudgetUsd: 0.01&lt;/code&gt;, and asked it to sign a 0.20 USDC transfer to &lt;code&gt;0x32fb1bedd95bf78ca2c6943ae5aeaeaafc0d97c1&lt;/code&gt;. That's an exploit contract on Base, listed in DeFiHackLabs as &lt;a href="https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-12/CloberDEX_exp.sol" rel="noopener noreferrer"&gt;CloberDEX (2024-12)&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verdix answered &lt;code&gt;danger&lt;/code&gt; with the reason &lt;code&gt;deployer_flagged&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The guard threw &lt;code&gt;VerdixGuardBlockedError&lt;/code&gt;, and no signature was produced.&lt;/li&gt;
&lt;li&gt;The send function was a local stub, called only after a successful signature. It was never called, and the test wallet's nonce on Base stayed at 0.&lt;/li&gt;
&lt;li&gt;The only money that moved was the check itself: 0.01 USDC, tx &lt;a href="https://basescan.org/tx/0xd3c4a29f0dbac9aab012deeeaf20aa535f788595c9995c2e54c30e1795eb54a0" rel="noopener noreferrer"&gt;&lt;code&gt;0xd3c4a29f…eb54a0&lt;/code&gt;&lt;/a&gt; (Base block 52220766).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One practical note if you reproduce this: a viem wallet client may call its transport (for example &lt;code&gt;eth_fillTransaction&lt;/code&gt;) &lt;em&gt;before&lt;/em&gt; signing. To prove that nothing left the machine, sign with the guarded account directly, not through the wallet client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethereum: lite and a free lookup, from a daily scan
&lt;/h2&gt;

&lt;p&gt;Until this week Verdix only screened Base. It now also answers for &lt;strong&gt;Ethereum mainnet&lt;/strong&gt;, in two places, and it's narrower there than on Base. Here is exactly what it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is checked on Ethereum:&lt;/strong&gt; OFAC sanctions, known scam and phishing addresses, burn addresses, and address-poisoning lookalikes from our own Ethereum scan. The phishing-token and flagged-deployer checks that lite runs on Base don't run on Ethereum; the answer lists them under &lt;code&gt;not_checked&lt;/code&gt;. There is no quick, standard or deep tier for Ethereum, so there's no Ethereum answer that says &lt;code&gt;safe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How fresh it is:&lt;/strong&gt; the Ethereum poisoning data is a &lt;strong&gt;daily scan, not live&lt;/strong&gt;. Each finished UTC day is scanned once, the next morning, for lookalikes (same first four and last four hex characters as a real counterparty) that received a zero-value or fake-token bait transfer. We have this data since 28 September 2026. Every Ethereum answer says which day it is complete through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s1"&gt;'https://api.verdixapi.com/check/api?address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amp;amp;chain=ethereum'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"flagged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"first_seen"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data_through"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"coverage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"daily, not live"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lookalike planted after &lt;code&gt;data_through&lt;/code&gt; isn't in the answer yet. If the data is more than two days old, the API refuses to answer rather than give a stale "not flagged": &lt;code&gt;/check/api&lt;/code&gt; returns a 503, and a paid lite check returns a 503 &lt;code&gt;caution&lt;/code&gt; that isn't charged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two ways to use it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free lookup&lt;/strong&gt;, &lt;code&gt;GET /check/api?address=…&amp;amp;chain=ethereum&lt;/code&gt;, or the page at &lt;a href="https://api.verdixapi.com/check?chain=ethereum" rel="noopener noreferrer"&gt;api.verdixapi.com/check?chain=ethereum&lt;/a&gt;. It answers only whether our poisoning scan flagged the address. &lt;code&gt;flagged: false&lt;/code&gt; is not a safety verdict: there are no sanctions or scam-list checks in it. It's rate-limited per IP (30 a minute, 500 a day).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lite&lt;/strong&gt;, &lt;code&gt;POST /risk/address/lite&lt;/code&gt; with &lt;code&gt;{"address": "0x…", "chain": "ethereum"}&lt;/code&gt;: $0.01, paid over x402 in USDC &lt;strong&gt;on Base&lt;/strong&gt;, like every Verdix check. It adds the sanctions and scam-list checks. Its clean answer is &lt;code&gt;no_known_risk&lt;/code&gt;, never &lt;code&gt;safe&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ethereum in the packages
&lt;/h3&gt;

&lt;p&gt;The client packages take the same &lt;code&gt;chain&lt;/code&gt; option. Base stays the default and behaves as before.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;verdix-guard 0.2.0:&lt;/strong&gt; Ethereum screening is &lt;strong&gt;opt-in&lt;/strong&gt;: &lt;code&gt;createVerdixGuard({ account, ethereum: 'lite' })&lt;/code&gt;. Payments on Ethereum mainnet (&lt;code&gt;eip155:1&lt;/code&gt;) then get a lite check, whatever &lt;code&gt;tier&lt;/code&gt; you chose for Base. Each decision carries &lt;code&gt;dataThrough&lt;/code&gt;. Without the option, Ethereum payments are handled like any other non-Base network, as before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;verdix-mcp 0.4.0:&lt;/strong&gt; &lt;code&gt;check_address_risk_lite&lt;/code&gt; takes &lt;code&gt;chain&lt;/code&gt;, and a new free tool, &lt;code&gt;check_address_poisoning&lt;/code&gt;, wraps &lt;code&gt;/check/api&lt;/code&gt; for Base and Ethereum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;verdix-ai-sdk 0.4.0:&lt;/strong&gt; &lt;code&gt;checkAddressRiskLite&lt;/code&gt; takes &lt;code&gt;chain&lt;/code&gt;; &lt;code&gt;checkAddressPoisoning()&lt;/code&gt; is the free lookup as a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;langchain-verdix 0.3.0:&lt;/strong&gt; &lt;code&gt;VerdixLiteAddressRiskTool&lt;/code&gt; takes &lt;code&gt;chain&lt;/code&gt;; &lt;code&gt;VerdixPoisoningTool&lt;/code&gt; is the free lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all four, an Ethereum answer that doesn't say how fresh it is (&lt;code&gt;data_through&lt;/code&gt;) counts as incomplete, and an answer about a different chain than the one asked for is rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;safe&lt;/code&gt; isn't a guarantee.&lt;/strong&gt; It means the checks that ran found nothing. A brand-new scam address that is on no list yet and has no on-chain history will pass. Keep confirming large or unusual payments with a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lite&lt;/code&gt; never says &lt;code&gt;safe&lt;/code&gt;.&lt;/strong&gt; It checks sanctions, scam and phishing lists, address-poisoning lookalikes and burn addresses (plus phishing tokens and flagged deployers on Base). It skips address age and some contract checks. A clean result is &lt;code&gt;no_known_risk&lt;/code&gt;, which says less than &lt;code&gt;safe&lt;/code&gt;. On Base, if you need &lt;code&gt;safe&lt;/code&gt;, use &lt;code&gt;tier: 'quick'&lt;/code&gt; ($0.02).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ethereum is daily and narrower.&lt;/strong&gt; No live poisoning data, no contract checks, no full tier. Treat an Ethereum &lt;code&gt;no_known_risk&lt;/code&gt; as "nothing on these lists through &lt;code&gt;data_through&lt;/code&gt;", not more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failing closed has a cost.&lt;/strong&gt; With the default &lt;code&gt;failMode: 'block'&lt;/code&gt;, your agent stops paying while Verdix or one of its data sources is unavailable. &lt;code&gt;failMode: 'allow'&lt;/code&gt; keeps it running, but those payments go through unchecked. An answer that already says &lt;code&gt;danger&lt;/code&gt; is blocked in both modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other networks aren't screened.&lt;/strong&gt; Payments on networks other than Base (and Ethereum, if you turn it on) pass through unchecked by default (&lt;code&gt;otherNetworks: 'allow'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not everything is decoded.&lt;/strong&gt; Raw &lt;code&gt;sign&lt;/code&gt; / &lt;code&gt;signMessage&lt;/code&gt;, EIP-7702 authorizations and contract calls that aren't a known transfer or approval pass through unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State is in memory.&lt;/strong&gt; The cache and the daily budget reset when your process restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't fix prompt injection.&lt;/strong&gt; It narrows what an injected agent can do with money. Keep your other controls too: allowlists for known payees, per-payment caps in the wallet, and a human approval step above a threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using MCP, Vercel AI SDK or LangChain?
&lt;/h2&gt;

&lt;p&gt;The same check is also available as an agent tool. Every package below has a lite tool ($0.01) whose clean answer is &lt;code&gt;no_known_risk&lt;/code&gt;, never &lt;code&gt;safe&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP:&lt;/strong&gt; &lt;code&gt;verdix-mcp&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/verdix-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/verdix-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;), run with &lt;code&gt;npx -y verdix-mcp&lt;/code&gt;. A one-file &lt;code&gt;.mcpb&lt;/code&gt; bundle for Claude Desktop is attached to the &lt;a href="https://github.com/verdixapi/verdix-mcp/releases/latest" rel="noopener noreferrer"&gt;latest GitHub release&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel AI SDK:&lt;/strong&gt; &lt;code&gt;verdix-ai-sdk&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/verdix-ai-sdk" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/verdix-ai-sdk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). It also has &lt;code&gt;verdixNeedsApproval&lt;/code&gt;, a &lt;code&gt;needsApproval&lt;/code&gt; for your own transfer tool that asks the user only when Verdix finds a risk (Base, quick tier).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangChain:&lt;/strong&gt; &lt;code&gt;langchain-verdix&lt;/code&gt; (&lt;a href="https://pypi.org/project/langchain-verdix/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/langchain-verdix" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If your agent can sign payments, put a check between the decision and the signature: one the model can't skip and that blocks when it can't get an answer. On Ethereum, know what your check covers and how old its data is; ours tells you in every answer. Whatever you use, test it with a payment to an address you know is bad, before an attacker does it for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lauras, Verdix. Questions or corrections: &lt;a href="mailto:lauras@verdixapi.com"&gt;lauras@verdixapi.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>security</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Stop your AI agent from paying a scammer</title>
      <dc:creator>verdixapi</dc:creator>
      <pubDate>Wed, 07 Oct 2026 19:39:45 +0000</pubDate>
      <link>https://dev.to/verdixapi/stop-your-ai-agent-from-paying-a-scammer-32o9</link>
      <guid>https://dev.to/verdixapi/stop-your-ai-agent-from-paying-a-scammer-32o9</guid>
      <description>&lt;p&gt;&lt;strong&gt;Update (Nov 2026):&lt;/strong&gt; Verdix now also checks Ethereum addresses. See &lt;a href="https://dev.to/verdixapi/check-the-recipient-before-your-agent-signs-base-and-ethereum-2j3a"&gt;Check the recipient before your agent signs: Base and Ethereum&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your agent can pay over &lt;a href="https://x402.org" rel="noopener noreferrer"&gt;x402&lt;/a&gt; or sign USDC transfers, it can also pay the wrong address. Nobody has to break in for that to happen. Someone only has to influence which address ends up in the payment.&lt;/p&gt;

&lt;p&gt;This post covers three ways that happens, why the check belongs before the signature, and how to add one with a small open-source package. It also covers the limits of that check.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://api.verdixapi.com" rel="noopener noreferrer"&gt;Verdix&lt;/a&gt;, the address-risk API that the package below calls.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways an agent ends up paying the wrong address
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Prompt injection
&lt;/h3&gt;

&lt;p&gt;An LLM decides what to do from text, and some of that text comes from people you don't control: web pages, emails, tool results, other agents. Whoever writes that text can steer the agent.&lt;/p&gt;

&lt;p&gt;The best-known public demo is &lt;a href="https://the-decoder.com/hacker-wins-47000-by-tricking-ai-chatbot-with-smart-prompting/" rel="noopener noreferrer"&gt;Freysa&lt;/a&gt;, an agent released in November 2024 with one rule: never approve a transfer of its prize pool. After 481 failed attempts, message 482 got it to do exactly that. The attacker convinced it that &lt;code&gt;approveTransfer&lt;/code&gt; handled &lt;em&gt;incoming&lt;/em&gt; payments, then announced a fake deposit. The agent sent its whole balance, 13.19 ETH (about $47,000).&lt;/p&gt;

&lt;p&gt;The lesson isn't that this model was weak. A rule written in the prompt is enforced only by the model that can be talked out of it. If the payment path has no check outside the model, a good enough injection can get through.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Address poisoning
&lt;/h3&gt;

&lt;p&gt;Addresses are 40 hex characters, and people (and agents) copy them from transaction history. An attacker generates a lookalike address with the same first and last characters as one you've used before, then sends you a tiny or zero-value transfer from it, so it shows up in your history.&lt;/p&gt;

&lt;p&gt;On 3 May 2024, a holder &lt;a href="https://www.coindesk.com/business/2024/05/03/exploiter-steals-68m-worth-of-crypto-through-address-poisoning" rel="noopener noreferrer"&gt;sent 1,155 WBTC, about $68M, to a lookalike address&lt;/a&gt; that matched the first and last six characters of the intended recipient. The funds were later returned after negotiation, which is rare.&lt;/p&gt;

&lt;p&gt;It isn't a one-off. A USENIX Security 2025 paper, &lt;a href="https://arxiv.org/abs/2501.16681" rel="noopener noreferrer"&gt;&lt;em&gt;Blockchain Address Poisoning&lt;/em&gt;&lt;/a&gt;, measured two years on Ethereum and BSC: 270M attack attempts targeting 17M victims, and 6,633 incidents with at least $83.8M in losses.&lt;/p&gt;

&lt;p&gt;An agent that takes "the address we paid last time" from history or logs is exactly the user this attack was built for. It also doesn't get the feeling that something looks off.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sanctioned addresses
&lt;/h3&gt;

&lt;p&gt;OFAC's SDN list includes crypto addresses. For example, the &lt;a href="https://ofac.treasury.gov/recent-actions/20220414" rel="noopener noreferrer"&gt;14 April 2022 update&lt;/a&gt; added an Ethereum address for the Lazarus Group: &lt;code&gt;0x098B716B8Aaf21512996dC57EB0615e2383E2f96&lt;/code&gt;. The &lt;a href="https://github.com/0xB10C/ofac-sanctioned-digital-currency-addresses" rel="noopener noreferrer"&gt;0xB10C/ofac-sanctioned-digital-currency-addresses&lt;/a&gt; project extracts these addresses from Treasury's XML every night.&lt;/p&gt;

&lt;p&gt;Here nothing has to trick your agent. An address can be perfectly valid, the payment can go through, and you have still paid a party you may be legally barred from paying. Whether that applies to you is a question for your lawyer, but checking the list costs very little.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the check goes before the signature
&lt;/h2&gt;

&lt;p&gt;An x402 payment is an EIP-3009 authorization that the facilitator settles on-chain. A USDC transfer is a transaction. Once either is signed and broadcast, you can't take it back. A post-payment alert only tells you what you lost.&lt;/p&gt;

&lt;p&gt;So the check has to run in the signing path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;outside the model&lt;/strong&gt;, so a prompt can't talk it out of running;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;on the final recipient&lt;/strong&gt;, the &lt;code&gt;payTo&lt;/code&gt; or &lt;code&gt;to&lt;/code&gt; that will actually be signed, not the name the model was given;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;failing closed&lt;/strong&gt;, so an unanswered check blocks the payment instead of waving it through.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Doing it with verdix-guard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/verdix-guard" rel="noopener noreferrer"&gt;&lt;code&gt;verdix-guard&lt;/code&gt;&lt;/a&gt; (MIT, &lt;a href="https://github.com/verdixapi/verdix-guard" rel="noopener noreferrer"&gt;source&lt;/a&gt;) hooks into two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;x402 payments&lt;/strong&gt;: it registers an &lt;code&gt;onBeforePaymentCreation&lt;/code&gt; hook on the official &lt;code&gt;@x402&lt;/code&gt; client, so the &lt;code&gt;payTo&lt;/code&gt; is screened before the payment payload is created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any transfer&lt;/strong&gt;: it wraps a viem account and screens native transfers, ERC-20 &lt;code&gt;transfer&lt;/code&gt; / &lt;code&gt;transferFrom&lt;/code&gt; / &lt;code&gt;approve&lt;/code&gt;, and EIP-3009 / EIP-2612 signatures before signing.
&lt;/li&gt;
&lt;/ul&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;verdix-guard @x402/fetch @x402/evm viem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For x402:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ExactEvmScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/evm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wrapFetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x402Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@x402/fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKeyToAccount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viem/accounts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guardX402Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verdix-guard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;privateKeyToAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AGENT_KEY&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="s2"&gt;`0x&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;x402Client&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eip155:8453&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactEvmScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;guardX402Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchWithPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapFetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://seller.example/paid-endpoint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For direct transfers with viem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guardAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VerdixGuardBlockedError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verdix-guard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createVerdixGuard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guardedAccount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;guardAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// use it in your walletClient&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;guardedAccount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signTransaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;VerdixGuardBlockedError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A blocked x402 request throws an error containing &lt;code&gt;verdix-guard: &amp;lt;reason&amp;gt;&lt;/code&gt;. A guarded account throws &lt;code&gt;VerdixGuardBlockedError&lt;/code&gt;. In both cases nothing is signed.&lt;/p&gt;

&lt;p&gt;There is no API key. Each check is itself an x402 payment: $0.01 in USDC on Base at the default &lt;code&gt;lite&lt;/code&gt; tier, paid from a wallet you choose. Answers are cached for 24 hours per address, and spending on checks is capped at &lt;code&gt;dailyBudgetUsd&lt;/code&gt; (default $1). USDC payments below &lt;code&gt;minAmountUsd&lt;/code&gt; (default $0.10) aren't checked. Amounts the guard can't price in USD, such as ETH or other tokens, always are.&lt;/p&gt;

&lt;p&gt;What it decides by default:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;danger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;caution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;block (change with &lt;code&gt;onCaution&lt;/code&gt;: &lt;code&gt;'allow'&lt;/code&gt; or &lt;code&gt;'ask'&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;no_known_risk&lt;/code&gt; / &lt;code&gt;safe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no usable answer&lt;/td&gt;
&lt;td&gt;block (&lt;code&gt;failMode: 'allow'&lt;/code&gt; to change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;To check the published package end to end, I installed &lt;code&gt;verdix-guard@0.1.0&lt;/code&gt; from npm into an empty directory. I wrapped a test wallet with &lt;code&gt;guardAccount&lt;/code&gt;, using &lt;code&gt;tier: 'lite'&lt;/code&gt;, &lt;code&gt;minAmountUsd: 0&lt;/code&gt; and &lt;code&gt;dailyBudgetUsd: 0.01&lt;/code&gt;, and asked it to sign a 0.20 USDC transfer to &lt;code&gt;0x32fb1bedd95bf78ca2c6943ae5aeaeaafc0d97c1&lt;/code&gt;. That's an exploit contract on Base, listed in DeFiHackLabs as &lt;a href="https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2024-12/CloberDEX_exp.sol" rel="noopener noreferrer"&gt;CloberDEX (2024-12)&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verdix answered &lt;code&gt;danger&lt;/code&gt; with the reason &lt;code&gt;deployer_flagged&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The guard threw &lt;code&gt;VerdixGuardBlockedError&lt;/code&gt;, and no signature was produced.&lt;/li&gt;
&lt;li&gt;The send function was a local stub, called only after a successful signature. It was never called, and the test wallet's nonce on Base stayed at 0.&lt;/li&gt;
&lt;li&gt;The only money that moved was the check itself: 0.01 USDC, tx &lt;a href="https://basescan.org/tx/0xd3c4a29f0dbac9aab012deeeaf20aa535f788595c9995c2e54c30e1795eb54a0" rel="noopener noreferrer"&gt;&lt;code&gt;0xd3c4a29f…eb54a0&lt;/code&gt;&lt;/a&gt; (Base block 52220766).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One practical note if you reproduce this: a viem wallet client may call its transport (for example &lt;code&gt;eth_fillTransaction&lt;/code&gt;) &lt;em&gt;before&lt;/em&gt; signing. To prove that nothing left the machine, sign with the guarded account directly, not through the wallet client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;safe&lt;/code&gt; isn't a guarantee.&lt;/strong&gt; It means the checks that ran found nothing. A brand-new scam address that is on no list yet and has no on-chain history will pass. Keep confirming large or unusual payments with a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lite&lt;/code&gt; never says &lt;code&gt;safe&lt;/code&gt;.&lt;/strong&gt; The default tier checks sanctions, scam and phishing lists, address-poisoning lookalikes, burn addresses, phishing tokens and flagged deployers. It skips address age and some contract checks. A clean result is &lt;code&gt;no_known_risk&lt;/code&gt;, which says less than &lt;code&gt;safe&lt;/code&gt;. If you need &lt;code&gt;safe&lt;/code&gt;, use &lt;code&gt;tier: 'quick'&lt;/code&gt; ($0.02).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failing closed has a cost.&lt;/strong&gt; With the default &lt;code&gt;failMode: 'block'&lt;/code&gt;, your agent stops paying while Verdix or one of its data sources is unavailable. &lt;code&gt;failMode: 'allow'&lt;/code&gt; keeps it running, but those payments go through unchecked. An answer that already says &lt;code&gt;danger&lt;/code&gt; is blocked in both modes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base only.&lt;/strong&gt; Payments on other networks pass through unchecked by default (&lt;code&gt;otherNetworks: 'allow'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not everything is decoded.&lt;/strong&gt; Raw &lt;code&gt;sign&lt;/code&gt; / &lt;code&gt;signMessage&lt;/code&gt;, EIP-7702 authorizations and contract calls that aren't a known transfer or approval pass through unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State is in memory.&lt;/strong&gt; The cache and the daily budget reset when your process restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't fix prompt injection.&lt;/strong&gt; It narrows what an injected agent can do with money. Keep your other controls too: allowlists for known payees, per-payment caps in the wallet, and a human approval step above a threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using MCP, Vercel AI SDK or LangChain?
&lt;/h2&gt;

&lt;p&gt;The same check is also available as an agent tool. Every package below has a lite tool ($0.01) whose clean answer is &lt;code&gt;no_known_risk&lt;/code&gt;, never &lt;code&gt;safe&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP:&lt;/strong&gt; &lt;code&gt;verdix-mcp&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/verdix-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/verdix-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;), run with &lt;code&gt;npx -y verdix-mcp&lt;/code&gt;. A one-file &lt;code&gt;.mcpb&lt;/code&gt; bundle for Claude Desktop is attached to the &lt;a href="https://github.com/verdixapi/verdix-mcp/releases/latest" rel="noopener noreferrer"&gt;latest GitHub release&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel AI SDK:&lt;/strong&gt; &lt;code&gt;verdix-ai-sdk&lt;/code&gt; (&lt;a href="https://www.npmjs.com/package/verdix-ai-sdk" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/verdix-ai-sdk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). It also has &lt;code&gt;verdixNeedsApproval&lt;/code&gt;, a &lt;code&gt;needsApproval&lt;/code&gt; for your own transfer tool that asks the user only when Verdix finds a risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LangChain:&lt;/strong&gt; &lt;code&gt;langchain-verdix&lt;/code&gt; (&lt;a href="https://pypi.org/project/langchain-verdix/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;, &lt;a href="https://github.com/verdixapi/langchain-verdix" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If your agent can sign payments, put a check between the decision and the signature: one the model can't skip and that blocks when it can't get an answer. The package above is one way to do that. Whatever you use, test it with a payment to an address you know is bad, before an attacker does it for you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>web3</category>
      <category>security</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
