← Blog
September 20, 2026 · 14 min

One blob instead of two fields: ERC-7930 vs the x402 payTo

An x402 payment requirement names the chain in one string and the recipient in another. Nothing in the schema says they belong together. ERC-7930 makes that pairing a single self-describing blob — and we measured exactly how far the standard can carry x402 today.

Every autonomous payment an agent makes starts with the same question: where does the money go. In x402 the answer arrives as two independent JSON strings. The network field carries a CAIP-2 identifier. The payTo field carries an address. The protocol never asserts that the second is valid on the first.

That is not a hypothetical. It is the exact failure mode ERC-7930 was written to close, and its motivation section describes x402 without naming it: the ERC-55 address format "does not encode any information about the chain on which an interaction is intended to occur," which "has led each protocol to define its own ad hoc way of representing the combination of address and chain, typically using separate fields and protocol-specific conventions."

We spent this audit doing three things. Reproducing every ERC-7930 test vector against two independent implementations. Feeding x402's own network identifiers into those implementations to see what comes out. And counting how many of x402's advertised chains the standard can actually express.

What x402 encodes today

The x402 v2 specification defines each entry in the accepts array with seven fields. Three of them describe the destination:

{
  "scheme": "exact",
  "network": "eip155:84532",
  "amount": "10000",
  "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
  "maxTimeoutSeconds": 60
}

The v2 changelog dates this shape to 2025-12-09, when the protocol moved to CAIP-2 network identifiers. That move was the right direction — it replaced bare strings like "base-sepolia" with namespaced ones. But it stopped at the chain. The asset and payTo fields remained opaque strings whose format is inferred from the sibling network value, out of band, by whatever code happens to be reading the object.

A facilitator that reads payTo before reading network has no way to know which decoder to use. A facilitator that reads them in the other order has to trust that the pair is coherent. A gateway that logs the recipient for reconciliation stores a 42-character string that is ambiguous across every EVM chain in existence — a problem we touched on when we walked through the network bindings across fifteen chains.

The ERC-7930 envelope

ERC-7930 replaces the pair with one binary payload. Status is Review, created 2025-02-02, with an author list that includes Nick Johnson, Francisco Giordano, Sam Wilson and Vitalik Buterin. The layout is six fields:

┌─────────┬───────────┬──────────────────────┬────────────────┬───────────────┬─────────┐
│ Version │ ChainType │ ChainReferenceLength │ ChainReference │ AddressLength │ Address │
│ 2 bytes │  2 bytes  │        1 byte        │    variable    │    1 byte     │variable │
└─────────┴───────────┴──────────────────────┴────────────────┴───────────────┴─────────┘

Version 1 is 0x0001. ChainType is a two-byte key assigned to a CASA namespace. The two length-prefixed variable fields carry whatever the namespace's CAIP-350 profile says they should carry.

Two degenerate forms are legal and useful. An address with a zero-length ChainReference means "this address, on any chain of this type." A zero-length Address with a present ChainReference is a Chain Identifier — it names a chain and nothing else. Both zero is forbidden.

The versioning rules are stricter than they look. Future versions must be trivially convertible to v1, must set the most significant bit of the version field if they break v1 parsing rules, and may add fields but "MUST NOT alter or omit any data required to reconstruct the Version 1 Interoperable Address exactly, bit for bit." That last clause is what makes the format safe to store in a database column for a decade.

Reproducing the vectors

The spec ships four examples. We ran them through @wonderland/interop-addresses 0.8.1, published 2026-06-11, with no modifications:

import * as ia from '@wonderland/interop-addresses';

await ia.nameToBinary('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045@eip155:1');
// 0x00010000010114d8da6bf26964af9d7eed9e03e53415d37aa96045  (27 bytes)

await ia.nameToBinary('MJKqp326RZCHnAAbew9MDdui3iCKWco7fsK9sVuZTX2@solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d');
// 0x0001000220452969...8c7ef02005333498...fc8dfe0b5  (70 bytes)

Both match the ERC-7930 examples byte for byte. We then independently base58-decoded the Solana address with a twelve-line decoder to confirm the library was not fabricating agreement: MJKqp326RZCHnAAbew9MDdui3iCKWco7fsK9sVuZTX2 decodes to 05333498d5aea4ae009585c43f7b8c30df8e70187d4a713d134f977fc8dfe0b5, which is exactly the Address field of the spec vector.

An x402 recipient on Base encodes to 28 bytes:

0x0001 0000 02 2105 14 d8da6bf26964af9d7eed9e03e53415d37aa96045
  ^v1  ^evm ^2 ^8453 ^20 ^address

The chain reference is 0x2105 — 8453 as a minimal big-endian integer, per the eip155 profile's rule that leading zeroes are prohibited. Base's chain id costs two bytes. Ethereum's costs one.

The second implementation, on-chain

OpenZeppelin Contracts v5.7.0 ships contracts/utils/draft-InteroperableAddress.sol, a Solidity library with format and parse helpers, calldata variants, and non-reverting tryParse forms. We wrote a probe contract and ran it under forge 1.5.1:

function format(uint256 chainid, address addr) external pure returns (bytes memory) {
    return InteroperableAddress.formatEvmV1(chainid, addr);
}

function tryParse(bytes calldata self) external pure returns (bool, uint256, address) {
    return InteroperableAddress.tryParseEvmV1Calldata(self);
}

The library reproduces the spec vector for Ethereum mainnet exactly, and produces the same 28-byte Base blob the JavaScript SDK produced. The gas report, measured as the cost of the external call in the test harness:

format         3,990 gas
parse          3,164 gas
parseGeneric   2,905 gas
tryParse       2,114 gas

Those are small numbers. Parsing a chain-bound recipient inside a settlement contract costs less than a single cold storage read. The cost argument against binary interoperable addresses does not survive contact with a gas report.

More interesting is what the library refuses. We fed it the Solana vector through tryParseEvmV1Calldata and it returned false rather than a plausible-looking 20-byte address sliced out of a 32-byte Ed25519 key. The JavaScript SDK behaves the same way at the text layer:

ia.nameToBinary('0xd8dA6BF2...45@solana:5eykt4Us...2N9d')
// InvalidInteroperableAddress: Invalid Solana address: 0xd8dA6BF2...

ia.nameToBinary('MJKqp326RZ...TX2@eip155:8453')
// InvalidInteroperableAddress: Invalid Ethereum address: MJKqp326RZ...

In x402 JSON, both of those are well-formed documents. A server can advertise a Solana network with an EVM payTo and every JSON schema validator in the pipeline will pass it. The error surfaces later, at the facilitator, or never — if the facilitator resolves the chain from the payload and the address from the requirements without comparing them.

The binding is the product — the compactness of ERC-7930 is a side effect. The actual deliverable is that a chain/address mismatch stops being a runtime validation and becomes an encoding impossibility.

The Solana truncation trap

Here the audit turns up something x402 operators should act on.

CAIP-2 limits chain references to 32 characters. A Solana genesis blockhash is 44 base58 characters. So the CAIP-2 identifier for Solana mainnet truncates the text: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp. That truncated string is exactly what the x402 v2 spec and the x402 docs both list as the mainnet network identifier.

The CAIP-350 Solana profile deliberately does not truncate. It uses the full 44-character hash, "for consistency with eip155 and to avoid the complexity of slicing base58btc-encoded text." Its own footnote spells out why truncation is dangerous: cutting the text, rather than the bytes, yields something that "does not correspond to simply slicing the first 23 bytes from the genesis blockhash."

We verified that with our own decoder:

full  (44 chars) -> 32 bytes: 45296998a6f8e2a784db5d9f95e18fc23f70441a1039446801089879b08c7ef0
trunc (32 chars) -> 23 bytes: e15de390a1bfea7ad6ed13c9898b4881b8aef9e705b31b
trunc is a prefix of full?    false

Not a prefix. Not a subsequence. Twenty-three bytes with no relationship to the real chain identifier beyond both being derived from the same base58 text. The Solana devnet identifier x402 publishes decodes to 24 bytes with the same property.

Now feed x402's own string into an encoder. We asked the Wonderland SDK for the interoperable address of a Solana recipient using x402's truncated network id, and it produced a valid, parseable, 61-byte blob whose ChainReference is the 23-byte artifact:

from x402 network id:  0x0001 0002 17 e15de390a1bfea7ad6ed13c9898b4881b8aef9e705b31b 20 0533...
from full genesis:     0x0001 0002 20 45296998a6f8e2a784db5d9f95e18fc23f70441a1039...  20 0533...

Two different canonical-looking encodings of the same recipient on the same chain. Our forge test asserts they hash differently, and both parse cleanly. Any system that uses an interoperable address as a mapping key — a settlement ledger, a payee allowlist, a spend counter of the kind we described in the spend-controls audit — would treat them as two unrelated payees.

ERC-7930's Security Considerations anticipate this exactly: implementers requiring canonicity "should thoroughly review the CAIP-350 profile of a chain namespace for the possibility of a lack of canonicity." For Solana, canonicity depends on whether the producer had the full genesis hash or only the CAIP-2 string. x402 publishes only the CAIP-2 string. Converting back requires a lookup table, which both the profile and CAIP-350 concede is the expected client behavior — and which is the one thing a smart contract cannot do.

The chainType registry is the real bottleneck

ERC-7930 is extensible by design. CAIP-350 is a living registry, and each namespace must define exactly one two-byte key for itself. The question is how much of that registry exists.

We cloned the CASA namespaces repository at commit 463bae5, dated 2026-09-03. It contains 54 directories. Exactly four of them ship a caip350.md profile, plus the template:

eip155    0x0000   Draft   created 2025-04-23
bip122    0x0001   Draft   created 2026-01-29
solana    0x0002   Draft   created 2025-04-23
starknet  0x0003   Draft   created 2026-01-29

Four chain types. All still Draft. Now map that against what x402 advertises. The x402 documentation lists twelve network namespaces: eip155, solana, tvm, algorand, stellar, aptos, hedera, keeta, near, ccd, xrpl and cardano. We checked each one against the registry:

eip155, solana                                     CAIP-350 profile
tvm, algorand, stellar, aptos, hedera, ccd, xrpl   CAIP-2 only, no chainType
keeta, near, cardano                               no namespace directory at all

Two out of twelve. An x402 deployment that adopted ERC-7930 as its recipient encoding today could express its EVM and Solana rails and nothing else. Seven namespaces would need a profile written and merged. Three would need a CASA namespace created first.

And the gap is wider than chains. The x402 spec encourages non-blockchain rails to borrow the CAIP-2 shape — ach:us, sepa:eu. ERC-7930 has no mechanism for those. A two-byte chainType is a CASA namespace reference, and CASA namespaces describe chains. Any agent payment system that expects to settle over both stablecoin rails and bank rails cannot put both behind one encoding.

What adoption would actually cost in bytes

The compactness claim deserves a measurement rather than an assumption, because x402 is a JSON protocol and ERC-7930 is a binary one.

For an EVM recipient, the two x402 fields spell out to 76 characters. The equivalent blob is 28 bytes, which serializes as a 72-character JSON member. A small win.

For Solana, the arithmetic inverts. The two x402 fields cost 105 characters. The blob is 70 bytes, which as a hex string inside JSON costs 156 characters — half again as much as what it replaces. Hex-in-JSON is a 2x encoding penalty, and it wipes out the binary saving for any namespace with 32-byte references and addresses.

On-chain the picture is unambiguous. At 16 gas per non-zero calldata byte and 4 per zero byte, the 28-byte Base blob costs 412 gas of calldata and the 70-byte Solana blob costs 1,084. Passing the same information as two ABI-encoded strings costs multiples of that before a single byte is parsed.

The honest conclusion: ERC-7930 is a win at the contract boundary and roughly a wash at the HTTP boundary. Which means the argument for adopting it in x402 is correctness, not efficiency.

The human layer: ERC-7828

ERC-7828, status Review, created 2024-11-27, defines the text form that sits on top. Its grammar is short:

<interoperable-name> ::= <address> "@" <chain> [ "#" <checksum> ]
<checksum>           ::= [0-9A-F]{8}

Round-tripping our Base recipient through the SDK produces 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045@eip155:8453#17DE0709. The checksum covers the whole pair, so a transposed chain id invalidates it — something a bare ERC-55 address cannot detect, since ERC-55 checksums only the address.

The names resolve through ENS, which means an agent's payout destination can be treasury.myagent.eth@base rather than a hex string a human has to diff character by character. For an operator approving an agent's spend policy, that is the difference between reviewing a config and rubber-stamping one.

What it means for LLM4Agents

LLM4Agents is a gateway. Agents register, deposit stablecoins, and call models; the gateway holds a recipient address for settlement and an agent-side address for refunds and balance attribution. Every one of those addresses is stored today the way x402 stores them: a string, next to a separate chain identifier, bound only by convention.

Three concrete consequences.

// Consequence 1

Ledger keys are ambiguous across chains

An agent that funds a balance from Base and later from Polygon presents the same 20-byte address. If the ledger keys on the address alone, the two are one account — sometimes correct, sometimes a cross-chain attribution bug. If it keys on the pair, every query has to carry both fields and every join has to compare both. A 28-byte interoperable address is a single primary key with the chain already inside it, and it is fixed-width enough to index.

// Consequence 2

The Solana identifier we publish is not round-trippable

If the gateway advertises Solana rails using the CAIP-2 truncated genesis hash — which is what the x402 spec prescribes — then any downstream consumer that upgrades to ERC-7930 will derive a 23-byte chain reference that does not identify Solana mainnet. This is not a future problem. It is a property of the string we would be publishing on day one, and it interacts badly with everything we described in the confidential transfers audit, where the verification path already depends on getting Solana-side details exactly right.

// Consequence 3

Multi-rail expansion is gated on a registry we do not control

The moment the gateway wants a non-EVM, non-Solana settlement rail, ERC-7930 stops being available as the internal representation. Seven of the namespaces x402 advertises have no chainType. Committing to interoperable addresses as the only internal format would mean the roadmap for new rails runs through CASA pull requests, which is not a dependency an infrastructure provider should accept silently.

The synthesis: adopt ERC-7930 as an internal canonical form where it is defined, keep the x402 wire format unchanged, and treat the encoder as a validator rather than a replacement. Every time the gateway builds a payment requirement, encode (network, payTo) into an interoperable address. If the encoder throws, the pair was incoherent and the request should never reach a facilitator. That is a free consistency check on a class of error that is otherwise invisible until settlement — the same defensive posture we argued for in the facilitator verify/settle audit.

Staying on the frontier

Concrete steps, in the order they pay off.

First, add the encoder as a validation gate. Wrap every outbound PaymentRequirements and every inbound PaymentPayload in an ERC-7930 encode attempt for the eip155 and solana namespaces. Reject on failure. This is a dependency and a try/catch, it ships in an afternoon, and it catches chain/address mismatches before money moves. Log, do not reject, for namespaces without a profile.

Second, store the blob alongside the strings. Add a fixed-width binary column to the accounts and settlement tables holding the interoperable address, populated where a chainType exists. Index on it. Keep the legacy columns authoritative for now. When the registry catches up, the migration is a backfill rather than a redesign.

Third, resolve Solana to the full genesis hash internally. Never store the truncated CAIP-2 string as the source of truth. Keep a mapping from the truncated form to the full 44-character hash for the chains we support, resolve on ingest, and emit the truncated form only on the x402 wire where the spec requires it. This is the single highest-value fix in this audit because it is cheap and the failure it prevents is silent.

Fourth, propose an x402 extension rather than a breaking change. The v2 spec already has an extensions mechanism: a key-value map where each entry carries an info object and a schema, advertised by the server and echoed by the client, which may append but may not overwrite. An interoperable-recipient extension carrying the ERC-7930 hex next to the existing network and payTo would be additive, ignorable by clients that do not implement it, and would let a facilitator assert consistency without a protocol version bump. That is the path the offer-and-receipt and payment-identifier extensions already took.

Fifth, adopt ERC-7828 names in the operator surface, not the protocol. Display agent.eth@base#CHECKSUM in dashboards, policy files and approval flows. Humans approve agent payout destinations, and an eight-character checksum over the chain-plus-address pair is a meaningfully better review artifact than a hex string. Keep the wire format machine-readable; make the review surface human-readable.

Sixth, watch two specific things. Whether ERC-7930 moves from Review to Last Call, which would freeze the envelope and make the storage decision permanent. And whether any of the seven profile-less namespaces x402 advertises gets a CAIP-350 profile merged — the first non-EVM, non-Solana chainType to land tells us whether the registry is actually living or effectively finished at four.

The broader pattern is one we keep hitting in agent payments. The protocols are converging on correct primitives faster than the registries that make those primitives usable. ERC-7930 is a good format with a four-entry lookup table behind it. Build on the format; do not assume the table.

Pay per call, on rails you can audit

An OpenAI-compatible gateway with stablecoin settlement, per-agent balances, and payment requirements you can verify field by field.

Register an agent