By Lauras, founder of Verdix. Disclosure: Verdix is the address-risk API that the code below calls. I'm not a neutral reviewer of it.
If your agent can pay over x402 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.
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.
Three ways an agent ends up paying the wrong address
1. Prompt injection
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.
The best-known public demo is Freysa, 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 approveTransfer handled incoming payments, then announced a fake deposit. The agent sent its whole balance, 13.19 ETH (about $47,000).
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.
2. Address poisoning
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.
On 3 May 2024, a holder sent 1,155 WBTC, about $68M, to a lookalike address that matched the first and last six characters of the intended recipient. The funds were later returned after negotiation, which is rare.
It isn't a one-off. A USENIX Security 2025 paper, Blockchain Address Poisoning, measured two years on Ethereum and BSC: 270M attack attempts targeting 17M victims, and 6,633 incidents with at least $83.8M in losses.
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.
3. Sanctioned addresses
OFAC's SDN list includes crypto addresses. For example, the 14 April 2022 update added an Ethereum address for the Lazarus Group: 0x098B716B8Aaf21512996dC57EB0615e2383E2f96. The 0xB10C/ofac-sanctioned-digital-currency-addresses project extracts these addresses from Treasury's XML every night.
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.
Why the check goes before the signature
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.
So the check has to run in the signing path:
- outside the model, so a prompt can't talk it out of running;
-
on the final recipient, the
payToortothat will actually be signed, not the name the model was given; - failing closed, so an unanswered check blocks the payment instead of waving it through.
Doing it with verdix-guard
verdix-guard (MIT, source) hooks into two places:
-
x402 payments: it registers an
onBeforePaymentCreationhook on the official@x402client, so thepayTois screened before the payment payload is created. -
Any transfer: it wraps a viem account and screens native transfers, ERC-20
transfer/transferFrom/approve, and EIP-3009 / EIP-2612 signatures before signing.
npm install verdix-guard @x402/fetch @x402/evm viem
For x402:
import { ExactEvmScheme } from '@x402/evm';
import { wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { privateKeyToAccount } from 'viem/accounts';
import { createVerdixGuard, guardX402Client } from 'verdix-guard';
const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`);
const client = new x402Client().register('eip155:8453', new ExactEvmScheme(account));
guardX402Client(client, createVerdixGuard({ account }));
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment('https://seller.example/paid-endpoint');
For direct transfers with viem:
import { createVerdixGuard, guardAccount, VerdixGuardBlockedError } from 'verdix-guard';
const guard = createVerdixGuard({ account });
const guardedAccount = guardAccount(account, guard); // use it in your walletClient
try {
await guardedAccount.signTransaction(tx);
} catch (error) {
if (error instanceof VerdixGuardBlockedError) {
console.log(error.decision.reason, error.decision.verdict);
}
}
A blocked x402 request throws an error containing verdix-guard: <reason>. A guarded account throws VerdixGuardBlockedError. In both cases nothing is signed.
There is no API key. Each check is itself an x402 payment: $0.01 in USDC on Base at the default lite tier, paid from a wallet you choose. Answers are cached for 24 hours per address, and spending on checks is capped at dailyBudgetUsd (default $1). USDC payments on Base below minAmountUsd (default $0.10) aren't checked. Amounts the guard can't price in USD, such as ETH or other tokens, always are.
What it decides by default:
| Answer | Default |
|---|---|
danger |
block, always |
caution |
block (`onCaution: 'allow' \ |
{% raw %}no_known_risk / safe
|
allow |
| no usable answer | block (failMode: 'allow' to change) |
A real run
To check the published package end to end, I installed verdix-guard@0.1.0 from npm into an empty directory. I wrapped a test wallet with guardAccount, using tier: 'lite', minAmountUsd: 0 and dailyBudgetUsd: 0.01, and asked it to sign a 0.20 USDC transfer to 0x32fb1bedd95bf78ca2c6943ae5aeaeaafc0d97c1. That's an exploit contract on Base, listed in DeFiHackLabs as CloberDEX (2024-12).
- Verdix answered
dangerwith the reasondeployer_flagged. - The guard threw
VerdixGuardBlockedError, and no signature was produced. - 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.
- The only money that moved was the check itself: 0.01 USDC, tx
0xd3c4a29f…eb54a0(Base block 52220766).
One practical note if you reproduce this: a viem wallet client may call its transport (for example eth_fillTransaction) before signing. To prove that nothing left the machine, sign with the guarded account directly, not through the wallet client.
Ethereum: lite and a free lookup, from a daily scan
Until this week Verdix only screened Base. It now also answers for Ethereum mainnet, in two places, and it's narrower there than on Base. Here is exactly what it does.
What is checked on Ethereum: 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 not_checked. There is no quick, standard or deep tier for Ethereum, so there's no Ethereum answer that says safe.
How fresh it is: the Ethereum poisoning data is a daily scan, not live. 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:
curl -s 'https://api.verdixapi.com/check/api?address=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&chain=ethereum'
{"flagged": false, "first_seen": null, "kind": null, "data_through": "2026-10-30", "coverage": "daily, not live"}
A lookalike planted after data_through 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": /check/api returns a 503, and a paid lite check returns a 503 caution that isn't charged.
The two ways to use it:
-
Free lookup,
GET /check/api?address=…&chain=ethereum, or the page at api.verdixapi.com/check?chain=ethereum. It answers only whether our poisoning scan flagged the address.flagged: falseis 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). -
Lite,
POST /risk/address/litewith{"address": "0x…", "chain": "ethereum"}: $0.01, paid over x402 in USDC on Base, like every Verdix check. It adds the sanctions and scam-list checks. Its clean answer isno_known_risk, neversafe.
Ethereum in the packages
The client packages take the same chain option. Base stays the default and behaves as before.
-
verdix-guard 0.2.0: Ethereum screening is opt-in:
createVerdixGuard({ account, ethereum: 'lite' }). Payments on Ethereum mainnet (eip155:1) then get a lite check, whatevertieryou chose for Base. Each decision carriesdataThrough. Without the option, Ethereum payments are handled like any other non-Base network, as before. -
verdix-mcp 0.4.0:
check_address_risk_litetakeschain, and a new free tool,check_address_poisoning, wraps/check/apifor Base and Ethereum. -
verdix-ai-sdk 0.4.0:
checkAddressRiskLitetakeschain;checkAddressPoisoning()is the free lookup as a tool. -
langchain-verdix 0.3.0:
VerdixLiteAddressRiskTooltakeschain;VerdixPoisoningToolis the free lookup.
In all four, an Ethereum answer that doesn't say how fresh it is (data_through) counts as incomplete, and an answer about a different chain than the one asked for is rejected.
The limits, honestly
-
safeisn't a guarantee. 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. -
litenever sayssafe. 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 isno_known_risk, which says less thansafe. On Base, if you needsafe, usetier: 'quick'($0.02). -
Ethereum is daily and narrower. No live poisoning data, no contract checks, no full tier. Treat an Ethereum
no_known_riskas "nothing on these lists throughdata_through", not more. -
Failing closed has a cost. With the default
failMode: 'block', your agent stops paying while Verdix or one of its data sources is unavailable.failMode: 'allow'keeps it running, but those payments go through unchecked. An answer that already saysdangeris blocked in both modes. -
Other networks aren't screened. Payments on networks other than Base (and Ethereum, if you turn it on) pass through unchecked by default (
otherNetworks: 'allow'). -
Not everything is decoded. Raw
sign/signMessage, EIP-7702 authorizations and contract calls that aren't a known transfer or approval pass through unchanged. - State is in memory. The cache and the daily budget reset when your process restarts.
- It doesn't fix prompt injection. 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.
Using MCP, Vercel AI SDK or LangChain?
The same check is also available as an agent tool. Every package below has a lite tool ($0.01) whose clean answer is no_known_risk, never safe:
-
MCP:
verdix-mcp(npm, GitHub), run withnpx -y verdix-mcp. A one-file.mcpbbundle for Claude Desktop is attached to the latest GitHub release. -
Vercel AI SDK:
verdix-ai-sdk(npm, GitHub). It also hasverdixNeedsApproval, aneedsApprovalfor your own transfer tool that asks the user only when Verdix finds a risk (Base, quick tier). -
LangChain:
langchain-verdix(PyPI, GitHub).
Takeaway
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.
Lauras, Verdix. Questions or corrections: lauras@verdixapi.com.
Top comments (0)