DEV Community

tigerops-win
tigerops-win

Posted on

The one-string bug that made a marketplace mark my x402 API unpayable

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" }
  }]
}
Enter fullscreen mode Exit fullscreen mode

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",
};
Enter fullscreen mode Exit fullscreen mode

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}`
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Read the domain from the chain. Never trust a string you typed once.

Lessons

  • extra is part of the cryptography. Treat name and version like a chain ID, not a label.
  • curl tests 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)