Scout Packs is a B2B lead lookup API for AI agents. One call costs $0.10 in USDC on Base: the agent gets a 402 challenge, sends the payment on-chain, and posts the transaction hash back to get the data. No accounts, no API keys. It's live at scout-packs-production.up.railway.app.
A few days after launch, I checked Agent402 — one of the indexes where agents discover x402 services — and my paid routes were flagged ineligible. My server was up. Every 402 I curled came back perfectly formed. The bug was one string in the payment challenge: I advertised the token's EIP-712 name as USDC. The contract calls itself USD Coin.
This is the story of that string, and why a field that looks like metadata is actually load-bearing.
What the challenge carries
When an agent calls my endpoint without paying, it gets a 402 Payment Required with a JSON body describing the terms:
{
"x402Version": 1,
"error": "payment_required",
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"maxAmountRequired": "100000",
"resource": "https://scout-packs-production.up.railway.app/lookup?query=acme",
"description": "Lead enrichment lookup — verified business contact for one company (company, location, category, verified email, source URL + provenance). Tiger Operations.",
"mimeType": "application/json",
"payTo": "0xAF7B70D8487EE6193701597E67f56A5902d23913",
"maxTimeoutSeconds": 300,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": { "name": "USD Coin", "version": "2" }
}]
}
maxAmountRequired is in atomic units — USDC has 6 decimals, so 100000 is $0.10. asset is the USDC contract on Base. The field that bit me is extra. It looks like a label. It isn't.
Why the name matters
EIP-712 signatures are bound to a domain, and the domain includes the token contract's name and version:
const domain = {
name: "USD Coin", // must match the contract's name()
version: "2", // must match the contract's version()
chainId: 8453,
verifyingContract: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
};
Marketplaces and wallets that validate x402 challenges check this domain against the real contract. My challenge said USDC. The contract on Base mainnet says USD Coin. Domain mismatch — and Agent402's eligibility check failed my routes. Nothing on my side errored. The 402 was well-formed. curl showed exactly what I expected. The failure lived in someone else's validation, which is why it took me a day to find.
The fix, and the check I added
The fix was changing one string. The more useful change was refusing to boot if my config ever disagrees with the chain again:
import { createPublicClient, http, parseAbi } from "viem";
import { base } from "viem/chains";
const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const client = createPublicClient({ chain: base, transport: http() });
const abi = parseAbi([
"function name() view returns (string)",
"function version() view returns (string)",
]);
export async function assertDomainMatches(extra: { name: string; version: string }) {
const [name, version] = await Promise.all([
client.readContract({ address: USDC, abi, functionName: "name" }),
client.readContract({ address: USDC, abi, functionName: "version" }),
]);
if (name !== extra.name || version !== extra.version) {
throw new Error(
`EIP-712 domain mismatch: contract=${name}/${version}, config=${extra.name}/${extra.version}`
);
}
}
Read the domain from the chain. Never trust a string you typed once.
Lessons
-
extrais part of the cryptography. Treatnameandversionlike a chain ID, not a label. -
curltests your JSON, not your listing. My challenge was perfectly shaped and still failed the marketplace's eligibility check. After every deploy, check how the index sees you, not just what your endpoint returns. - A passing probe isn't a working payment. Shape validation and domain validation are different checks. You need both.
- Silence is a symptom. Lots of 402s and zero paid completions means the problem is probably in your challenge, not in demand.
Scout Packs: https://scout-packs-production.up.railway.app
Top comments (0)