← Guides

Check the recipient before your agent signs: Base and Ethereum

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:

Doing it with verdix-guard

verdix-guard (MIT, source) hooks into two places:

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:

AnswerDefault
dangerblock, always
cautionblock (onCaution: 'allow' | 'ask' to change)
no_known_risk / safeallow
no usable answerblock (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).

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:

Ethereum in the packages

The client packages take the same chain option. Base stays the default and behaves as before.

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

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:

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.