Blog, by , , 7 min read

One extra letter in a USDC address flagged every Solana API we list

A typo in our token table left every Solana USDC listing unpriced, flagged and marked "avoid" for four days. Why our tests missed it, and the fix.

7min read
On this page
  1. What the token table is for
  2. Spot the typo
  3. What one failed lookup did
  4. Why our tests missed it
  5. How we found it
  6. The fix
  7. If your code handles on-chain addresses
  8. Frequently asked questions

Short version: for the first four days ToolVet ran, from 11 to 15 October 2026, the Solana USDC address in our table of known tokens had one letter too many. Every x402 API that charges USDC on Solana showed "Price n/a", carried a false security finding, lost 4 trust points, dropped out of price filters, and got an "avoid" verdict from the preflight check agents run before they pay. None of it was the sellers' fault. A correctness audit found it, we fixed it on 15 October, and the changelog records it. This post covers what broke, why our tests missed it, and what catches it now.

What the token table is for

An x402 server answers an unpaid request with HTTP 402 and a payment challenge. The challenge gives the amount in the token's smallest unit, the token's address, the network and the address to pay. An amount of 2000 is $0.002 only if you know the token is USDC, which has 6 decimals.

So ToolVet keeps a small table: network, then token address, then symbol, decimals and whether the token is pegged to the dollar. When a challenge names a token that isn't in the table, we can't turn the amount into a price. We also flag it, because a challenge that asks for an unknown token could be asking for something worth nothing, or for a look-alike of a real stablecoin.

This is the table as it shipped on 11 October:

/** network -> lower-cased asset address/mint -> asset info */
const KNOWN_ASSETS: Record<string, Record<string, KnownAsset>> = {
  // Base mainnet
  "eip155:8453": {
    "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": { symbol: "USDC", decimals: 6, usdPegged: true },
  },
  // ...Ethereum, Polygon, Arbitrum, Avalanche...
  // Solana mainnet
  "solana:5eykt4usfv8p8njdtrepy1vzqkqzkvdp": {
    epjfwdd5aufqssqem2qn1xzybapc8g4wegggkzwytdt1v: { symbol: "USDC", decimals: 6, usdPegged: true },
  },
};

export function lookupAsset(network: string, asset: string): KnownAsset | null {
  return KNOWN_ASSETS[normalizeNetwork(network).toLowerCase()]?.[asset.toLowerCase()] ?? null;
}

Spot the typo

Circle publishes the Solana USDC mint as EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v. Lower-cased, it ends in …c8g4weggkzwytdt1v. Our key ended in …c8g4wegggkzwytdt1v: three gs where there should be two, in a 44-character string with no capitals left to break it up.

The lower-casing came from Ethereum. An EVM address can be written all in lower case or in the mixed case of an EIP-55 checksum, and both are the same address. Lower-casing both sides is the usual way to compare them, and we applied that rule to every network.

Solana addresses are base58, where G and g are different digits. Lower-casing them still works for a lookup, as long as both sides are lower-cased and the key is right, which is why it looked harmless. What it cost us was the ability to check the key against its source. You can't paste Circle's mint into a search of the file and find it. A reviewer comparing the two strings has to lower-case one in their head, character by character.

What one failed lookup did

lookupAsset returned null for every Solana USDC challenge. Everything downstream read null as "this seller asks for a token we don't know":

  • Price. With no decimals, the amount couldn't become a price. The listing page showed "Price n/a", and the API and MCP tools returned no price.
  • Filters and sorts. A listing with no price drops out of price filters and price sorts. An agent searching with a budget never saw these APIs.
  • Security finding. Every one got "Asks for an unrecognised token".
  • Trust score. A valid challenge earns 10 points with a known token and 6 with an unknown one, so each lost 4.
  • Preflight. The paid preflight check, which an agent calls with a URL before paying it, answered "avoid".

Nothing threw an error. Each listing's result, taken alone, looked like a reasonable judgment about that listing: an unpriced challenge for an unknown token is exactly what a dubious listing looks like. A bug in our reference data had turned into a verdict against the sellers.

Why our tests missed it

The original test suite had the right address in it. The Bazaar importer test used a catalog entry with two payment options: USDC on Base, and USDC on Solana with the correct mint. But the test's assertions checked the listing's price, which came from the Base option, and that the entry had two options. No test priced a Solana option, so the one place the correct mint met the mistyped key went unchecked.

The aggregate would have given it away at once: every Solana USDC listing had the same finding. But we were looking at listings one at a time, not counting findings per network.

How we found it

On 15 October we audited every number the site publishes: we read the code behind each one, reproduced each suspected error with a failing test, and had every finding re-checked independently. The audit came to 52 findings. This one was confirmed on a live listing page, and the changelog lists the ones that changed what people saw. The same table was also missing USDC on OP Mainnet: the network had a name but no token entry.

The fix

Two changes. Solana mints are now written exactly as Circle publishes them and compared exactly, and only EVM addresses are compared without case:

const USDC: KnownAsset = { symbol: "USDC", decimals: 6, usdPegged: true };

const KNOWN_ASSETS: Record<string, Record<string, KnownAsset>> = {
  "eip155:8453": { "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": USDC },
  // ...
  "eip155:10": { "0x0b2c639c533813f4aa9d7837caf62653d097ff85": USDC }, // OP Mainnet, newly added
  "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": { EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: USDC },
};

/** EVM addresses are compared without case (checksums); every other address, such as a Solana mint, exactly. */
export function addressKey(network: string, address: string): string {
  return isEvmNetwork(network) ? address.toLowerCase() : address;
}

And a test now checks the table against Circle's list, pasted in the form Circle publishes it:

// Circle's published USDC addresses, as written there.
const circle: Array<[string, string]> = [
  ["eip155:1", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"],
  ["eip155:8453", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
  // ...Polygon, Arbitrum, Avalanche, OP Mainnet...
  ["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"],
];

it("recognises USDC on every network Circle lists", () => {
  for (const [network, asset] of circle) {
    expect(lookupAsset(network, asset), `${network} ${asset}`).toEqual({ symbol: "USDC", decimals: 6, usdPegged: true });
    expect(atomicToUsd("1000", network, asset), network).toBe(0.001);
  }
});

it("ignores case in EVM addresses but not in Solana mints", () => {
  expect(lookupAsset(BASE, BASE_USDC.toLowerCase())).not.toBeNull();
  expect(lookupAsset(SOL, SOL_USDC.toLowerCase())).toBeNull();
  expect(lookupAsset(SOL, SOL_USDC.slice(0, -1))).toBeNull();
});

What matters is where the expected values come from: the source, pasted as published, never derived from our own table. A test that takes its expected value from the code under test can only confirm that the code agrees with itself.

Each check after the fix recomputes a listing's price, trust score and findings from the challenge its endpoint returns.

If your code handles on-chain addresses

  • Normalise per chain, not once for all of them. Lower-case EVM addresses for comparison, or validate the checksum. Compare Solana addresses exactly. If you add a chain, look up its rules rather than reusing the last chain's.
  • Keep constants in the form the source publishes. A reviewer should be able to search the file for the address they copied from the issuer's page and find it.
  • Test constants against the source, verbatim. Paste the published list into the test, so the code and the test can't share a typo.
  • Count your "unknown" verdicts. When one listing asks for an unknown token, suspect the listing. When every listing on a network does, suspect your table. A few lines after each scan would have caught this on day one:
// An unknown token on most listings of one network is our table, not the sellers.
for (const [network, { listings, unknownToken }] of findingsByNetwork) {
  if (listings >= 20 && unknownToken / listings > 0.5) {
    alert(`unknown-token finding on ${unknownToken} of ${listings} listings on ${network}`);
  }
}

We already apply the same reasoning to uptime: when listings on many sites fail their checks at once, ToolVet assumes the problem is on our side and doesn't announce the outages it confirms during that time. The methodology describes the rule. The token table had no such guard.

Frequently asked questions

What is the USDC address on Solana?

EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v on mainnet and 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU on devnet, according to Circle's list. Copy addresses from the issuer's page, not from a blog post, this one included.

Are Solana addresses case-sensitive?

Yes. Base58 uses upper- and lower-case letters as different digits, so changing the case of one letter gives a different address or an invalid one.

Are Ethereum addresses case-sensitive?

No. The mixed case in an address like 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 is an EIP-55 checksum, and the all-lower-case form is the same address. Compare them lower-cased, or check the checksum.

My x402 API charges USDC on Solana. Do I need to do anything?

No. Each check after 15 October recomputes your listing's price, trust score and findings from the challenge your endpoint returns. The free x402 checker shows what an agent sees when it calls your endpoint today.

Found a number of ours that looks wrong? Write to [email protected] with the listing's URL. Corrections go in the changelog.