← Blog
20 de septiembre, 2026 · 14 min

Un blob en vez de dos campos: ERC-7930 frente al payTo de x402

Un payment requirement de x402 nombra la cadena en un string y al receptor en otro. Nada en el schema dice que van juntos. ERC-7930 convierte ese par en un único blob autodescriptivo — y medimos hasta dónde puede llegar el estándar hoy con x402.

Cada pago autónomo que hace un agente empieza con la misma pregunta: a dónde va el dinero. En x402 la respuesta llega como dos strings JSON independientes. El campo network lleva un identificador CAIP-2. El campo payTo lleva una dirección. El protocolo nunca afirma que la segunda sea válida en la primera.

No es hipotético. Es exactamente el modo de falla que ERC-7930 fue escrito para cerrar, y su sección de motivación describe a x402 sin nombrarlo: el formato de direcciones ERC-55 "no codifica ninguna información sobre la cadena en la que se pretende que ocurra la interacción", lo que "ha llevado a cada protocolo a definir su propia forma ad hoc de representar la combinación de dirección y cadena, típicamente usando campos separados y convenciones específicas del protocolo".

Esta auditoría hizo tres cosas. Reproducir cada vector de prueba de ERC-7930 contra dos implementaciones independientes. Alimentar esas implementaciones con los propios identificadores de red de x402 para ver qué sale. Y contar cuántas de las cadenas que x402 anuncia puede expresar el estándar realmente.

Lo que x402 codifica hoy

La especificación x402 v2 define cada entrada del array accepts con siete campos. Tres describen el destino:

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

El changelog de v2 fecha esta forma el 2025-12-09, cuando el protocolo pasó a identificadores de red CAIP-2. Ese movimiento fue la dirección correcta: reemplazó strings desnudos como "base-sepolia" por otros con namespace. Pero se detuvo en la cadena. Los campos asset y payTo siguieron siendo strings opacos cuyo formato se infiere del valor hermano network, fuera de banda, por el código que casualmente esté leyendo el objeto.

Un facilitator que lee payTo antes que network no tiene forma de saber qué decodificador usar. Uno que los lee en el otro orden tiene que confiar en que el par es coherente. Un gateway que registra al receptor para conciliación guarda un string de 42 caracteres ambiguo en cada cadena EVM que existe — un problema que rozamos cuando recorrimos los network bindings en quince cadenas.

El sobre de ERC-7930

ERC-7930 reemplaza el par por un único payload binario. Estado Review, creado el 2025-02-02, con una lista de autores que incluye a Nick Johnson, Francisco Giordano, Sam Wilson y Vitalik Buterin. El layout tiene seis campos:

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

La versión 1 es 0x0001. ChainType es una clave de dos bytes asignada a un namespace de CASA. Los dos campos variables con prefijo de longitud llevan lo que diga el perfil CAIP-350 de ese namespace.

Dos formas degeneradas son legales y útiles. Una dirección con ChainReference de longitud cero significa "esta dirección, en cualquier cadena de este tipo". Un Address de longitud cero con ChainReference presente es un Chain Identifier: nombra una cadena y nada más. Ambos en cero está prohibido.

Las reglas de versionado son más estrictas de lo que parecen. Las versiones futuras deben ser trivialmente convertibles a v1, deben poner en 1 el bit más significativo del campo versión si rompen las reglas de parseo de v1, y pueden agregar campos pero "NO DEBEN alterar ni omitir ningún dato requerido para reconstruir la Interoperable Address versión 1 exactamente, bit por bit". Esa última cláusula es lo que vuelve seguro guardar el formato en una columna de base de datos por una década.

Reproducir los vectores

El spec trae cuatro ejemplos. Los corrimos con @wonderland/interop-addresses 0.8.1, publicado el 2026-06-11, sin modificaciones:

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)

Ambos coinciden byte a byte con los ejemplos de ERC-7930. Luego decodificamos la dirección Solana en base58 con un decodificador propio de doce líneas para confirmar que la librería no estaba fabricando la coincidencia: MJKqp326RZCHnAAbew9MDdui3iCKWco7fsK9sVuZTX2 decodifica a 05333498d5aea4ae009585c43f7b8c30df8e70187d4a713d134f977fc8dfe0b5, que es exactamente el campo Address del vector del spec.

Un receptor de x402 en Base se codifica en 28 bytes:

0x0001 0000 02 2105 14 d8da6bf26964af9d7eed9e03e53415d37aa96045
  ^v1  ^evm ^2 ^8453 ^20 ^dirección

La referencia de cadena es 0x2105 — 8453 como entero big-endian mínimo, por la regla del perfil eip155 que prohíbe ceros a la izquierda. El chain id de Base cuesta dos bytes. El de Ethereum, uno.

La segunda implementación, on-chain

OpenZeppelin Contracts v5.7.0 incluye contracts/utils/draft-InteroperableAddress.sol, una librería en Solidity con helpers de format y parse, variantes para calldata y formas tryParse que no revierten. Escribimos un contrato de prueba y lo corrimos con 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);
}

La librería reproduce exactamente el vector de Ethereum mainnet y produce el mismo blob de 28 bytes para Base que produjo el SDK de JavaScript. El reporte de gas, medido como el costo de la llamada externa en el harness de test:

format         3.990 gas
parse          3.164 gas
parseGeneric   2.905 gas
tryParse       2.114 gas

Son números chicos. Parsear un receptor atado a su cadena dentro de un contrato de settlement cuesta menos que una sola lectura fría de storage. El argumento de costo contra las direcciones interoperables binarias no sobrevive al contacto con un reporte de gas.

Más interesante es lo que la librería rechaza. Le dimos el vector de Solana a tryParseEvmV1Calldata y devolvió false en vez de una dirección de 20 bytes con pinta plausible recortada de una clave Ed25519 de 32 bytes. El SDK de JavaScript se comporta igual en la capa de texto:

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

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

En el JSON de x402, ambos son documentos bien formados. Un servidor puede anunciar una red Solana con un payTo EVM y todos los validadores de JSON schema del pipeline lo dejan pasar. El error aparece después, en el facilitator, o nunca — si el facilitator resuelve la cadena desde el payload y la dirección desde los requirements sin compararlos.

El vínculo es el producto — la compacidad de ERC-7930 es un efecto secundario. El entregable real es que un desajuste entre cadena y dirección deja de ser una validación en runtime y pasa a ser una imposibilidad de codificación.

La trampa del truncado en Solana

Acá la auditoría encuentra algo sobre lo que los operadores de x402 deberían actuar.

CAIP-2 limita las referencias de cadena a 32 caracteres. Un genesis blockhash de Solana tiene 44 caracteres base58. Entonces el identificador CAIP-2 de Solana mainnet trunca el texto: solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp. Ese string truncado es exactamente lo que el spec de x402 v2 y la documentación de x402 listan como identificador de mainnet.

El perfil CAIP-350 de Solana deliberadamente no trunca. Usa el hash completo de 44 caracteres, "por consistencia con eip155 y para evitar la complejidad de recortar texto codificado en base58btc". Su propia nota al pie explica por qué el truncado es peligroso: cortar el texto, en vez de los bytes, produce algo que "no corresponde a simplemente recortar los primeros 23 bytes del genesis blockhash".

Lo verificamos con nuestro propio decodificador:

completo (44 chars) -> 32 bytes: 45296998a6f8e2a784db5d9f95e18fc23f70441a1039446801089879b08c7ef0
truncado (32 chars) -> 23 bytes: e15de390a1bfea7ad6ed13c9898b4881b8aef9e705b31b
¿el truncado es prefijo del completo?  false

No es prefijo. No es subsecuencia. Veintitrés bytes sin relación con el identificador real de la cadena, más allá de derivar del mismo texto base58. El identificador de devnet que publica x402 decodifica a 24 bytes con la misma propiedad.

Ahora alimenta un encoder con el propio string de x402. Le pedimos al SDK de Wonderland la interoperable address de un receptor de Solana usando el network id truncado de x402, y produjo un blob válido, parseable, de 61 bytes cuyo ChainReference es el artefacto de 23 bytes:

desde el network id de x402: 0x0001 0002 17 e15de390a1bfea7ad6ed13c9898b4881b8aef9e705b31b 20 0533...
desde el genesis completo:   0x0001 0002 20 45296998a6f8e2a784db5d9f95e18fc23f70441a1039...  20 0533...

Dos codificaciones distintas, ambas con pinta de canónicas, del mismo receptor en la misma cadena. Nuestro test de forge afirma que hashean distinto, y ambas parsean limpio. Cualquier sistema que use una interoperable address como clave de un mapping — un ledger de settlement, un allowlist de payees, un contador de gasto como los que describimos en la auditoría de spend controls — las trataría como dos payees sin relación.

Las Security Considerations de ERC-7930 anticipan esto exactamente: quienes requieran canonicidad "deberían revisar a fondo el perfil CAIP-350 del namespace por la posibilidad de una falta de canonicidad". En Solana, la canonicidad depende de si el productor tenía el genesis hash completo o solo el string CAIP-2. x402 publica solo el string CAIP-2. Convertir de vuelta requiere una tabla de lookup, que tanto el perfil como CAIP-350 admiten como el comportamiento esperado del cliente — y que es justo lo único que un smart contract no puede hacer.

El registro de chainType es el verdadero cuello de botella

ERC-7930 es extensible por diseño. CAIP-350 es un registro vivo y cada namespace debe definir exactamente una clave de dos bytes para sí mismo. La pregunta es cuánto de ese registro existe.

Clonamos el repositorio de namespaces de CASA en el commit 463bae5, del 2026-09-03. Contiene 54 directorios. Exactamente cuatro traen un perfil caip350.md, más la plantilla:

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

Cuatro chain types. Todos todavía en Draft. Ahora mapea eso contra lo que anuncia x402. La documentación de x402 lista doce namespaces de red: eip155, solana, tvm, algorand, stellar, aptos, hedera, keeta, near, ccd, xrpl y cardano. Verificamos cada uno contra el registro:

eip155, solana                                     perfil CAIP-350
tvm, algorand, stellar, aptos, hedera, ccd, xrpl   solo CAIP-2, sin chainType
keeta, near, cardano                               sin directorio de namespace

Dos de doce. Un despliegue de x402 que adoptara ERC-7930 hoy como codificación de receptor podría expresar sus rails EVM y Solana, y nada más. Siete namespaces necesitarían un perfil escrito y mergeado. Tres necesitarían que primero se cree el namespace en CASA.

Y la brecha es más ancha que las cadenas. El spec de x402 alienta a los rails no-blockchain a tomar prestada la forma CAIP-2: ach:us, sepa:eu. ERC-7930 no tiene mecanismo para eso. Un chainType de dos bytes es una referencia a un namespace de CASA, y los namespaces de CASA describen cadenas. Cualquier sistema de pagos de agentes que espere liquidar sobre rails de stablecoin y rails bancarios no puede poner ambos detrás de una sola codificación.

Lo que costaría adoptarlo, en bytes

La afirmación de compacidad merece una medición en vez de un supuesto, porque x402 es un protocolo JSON y ERC-7930 es binario.

Para un receptor EVM, los dos campos de x402 ocupan 76 caracteres. El blob equivalente son 28 bytes, que serializan como un miembro JSON de 72 caracteres. Una ganancia chica.

Para Solana, la aritmética se invierte. Los dos campos de x402 cuestan 105 caracteres. El blob son 70 bytes, que como string hexadecimal dentro de JSON cuesta 156 caracteres: la mitad más de lo que reemplaza. Hex dentro de JSON es una penalización de 2x, y borra el ahorro binario en cualquier namespace con referencias y direcciones de 32 bytes.

On-chain el panorama es inequívoco. A 16 gas por byte de calldata distinto de cero y 4 por byte cero, el blob de 28 bytes de Base cuesta 412 gas de calldata y el de 70 bytes de Solana cuesta 1.084. Pasar la misma información como dos strings codificados en ABI cuesta múltiplos de eso antes de parsear un solo byte.

La conclusión honesta: ERC-7930 gana en la frontera del contrato y empata más o menos en la frontera HTTP. Lo que significa que el argumento para adoptarlo en x402 es corrección, no eficiencia.

La capa humana: ERC-7828

ERC-7828, estado Review, creado el 2024-11-27, define la forma de texto que va encima. Su gramática es corta:

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

Hacer el round-trip de nuestro receptor de Base por el SDK produce 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045@eip155:8453#17DE0709. El checksum cubre el par completo, así que un chain id transpuesto lo invalida — algo que una dirección ERC-55 pelada no puede detectar, porque ERC-55 solo hace checksum de la dirección.

Los nombres resuelven vía ENS, lo que significa que el destino de cobro de un agente puede ser treasury.myagent.eth@base en vez de un string hexadecimal que un humano tiene que comparar carácter por carácter. Para un operador que aprueba la política de gasto de un agente, esa es la diferencia entre revisar una configuración y firmarla a ciegas.

Qué significa para LLM4Agents

LLM4Agents es un gateway. Los agentes se registran, depositan stablecoins y llaman modelos; el gateway guarda una dirección receptora para settlement y una dirección del lado del agente para reembolsos y atribución de saldo. Cada una de esas direcciones se guarda hoy como las guarda x402: un string, al lado de un identificador de cadena separado, unidos solo por convención.

Tres consecuencias concretas.

// Consecuencia 1

Las claves del ledger son ambiguas entre cadenas

Un agente que carga saldo desde Base y luego desde Polygon presenta la misma dirección de 20 bytes. Si el ledger indexa solo por la dirección, las dos son una cuenta — a veces correcto, a veces un bug de atribución cross-chain. Si indexa por el par, cada query tiene que cargar ambos campos y cada join tiene que comparar ambos. Una interoperable address de 28 bytes es una única clave primaria con la cadena adentro, y es de ancho fijo suficiente como para indexarla.

// Consecuencia 2

El identificador de Solana que publicamos no hace round-trip

Si el gateway anuncia rails de Solana usando el genesis hash truncado de CAIP-2 — que es lo que prescribe el spec de x402 — entonces cualquier consumidor aguas abajo que migre a ERC-7930 derivará una referencia de cadena de 23 bytes que no identifica a Solana mainnet. No es un problema futuro. Es una propiedad del string que estaríamos publicando desde el día uno, y se lleva mal con todo lo que describimos en la auditoría de confidential transfers, donde la ruta de verificación ya depende de acertar los detalles del lado Solana con exactitud.

// Consecuencia 3

La expansión multi-rail depende de un registro que no controlamos

En el momento en que el gateway quiera un rail de settlement que no sea EVM ni Solana, ERC-7930 deja de estar disponible como representación interna. Siete de los namespaces que anuncia x402 no tienen chainType. Comprometerse con las interoperable addresses como único formato interno significaría que el roadmap de rails nuevos pasa por pull requests en CASA, y esa no es una dependencia que un proveedor de infraestructura deba aceptar en silencio.

La síntesis: adoptar ERC-7930 como forma canónica interna donde esté definido, dejar el formato de cable de x402 sin cambios, y tratar al encoder como validador en vez de reemplazo. Cada vez que el gateway construya un payment requirement, codificar (network, payTo) a una interoperable address. Si el encoder lanza error, el par era incoherente y la request nunca debería llegar a un facilitator. Es un chequeo de consistencia gratis sobre una clase de error que de otro modo es invisible hasta el settlement — la misma postura defensiva que argumentamos en la auditoría de verify/settle del facilitator.

Cómo mantenerse en la frontera

Pasos concretos, en el orden en que rinden.

Primero, agregar el encoder como compuerta de validación. Envolver cada PaymentRequirements saliente y cada PaymentPayload entrante en un intento de codificación ERC-7930 para los namespaces eip155 y solana. Rechazar si falla. Es una dependencia y un try/catch, se implementa en una tarde, y atrapa los desajustes de cadena y dirección antes de que se mueva dinero. Para namespaces sin perfil, registrar en logs, no rechazar.

Segundo, guardar el blob junto a los strings. Agregar una columna binaria de ancho fijo a las tablas de cuentas y settlement con la interoperable address, poblada donde exista chainType. Indexarla. Mantener las columnas legacy como autoritativas por ahora. Cuando el registro se ponga al día, la migración es un backfill y no un rediseño.

Tercero, resolver Solana al genesis hash completo internamente. Nunca guardar el string CAIP-2 truncado como fuente de verdad. Mantener un mapeo de la forma truncada al hash completo de 44 caracteres para las cadenas que soportamos, resolver al ingresar, y emitir la forma truncada solo en el cable de x402 donde el spec lo exige. Es el arreglo de mayor valor de esta auditoría porque es barato y la falla que previene es silenciosa.

Cuarto, proponer una extensión de x402 en vez de un cambio rompedor. El spec v2 ya tiene un mecanismo de extensiones: un mapa clave-valor donde cada entrada lleva un objeto info y un schema, anunciado por el servidor y repetido por el cliente, que puede agregar pero no sobrescribir. Una extensión interoperable-recipient que lleve el hex de ERC-7930 junto a los network y payTo existentes sería aditiva, ignorable por clientes que no la implementen, y permitiría a un facilitator afirmar consistencia sin subir versión de protocolo. Es el camino que ya tomaron las extensiones de offer-and-receipt y payment-identifier.

Quinto, adoptar nombres ERC-7828 en la superficie del operador, no en el protocolo. Mostrar agent.eth@base#CHECKSUM en dashboards, archivos de política y flujos de aprobación. Los humanos aprueban los destinos de cobro de los agentes, y un checksum de ocho caracteres sobre el par cadena-más-dirección es un artefacto de revisión sensiblemente mejor que un string hexadecimal. Que el formato de cable siga siendo legible por máquinas; que la superficie de revisión sea legible por humanos.

Sexto, vigilar dos cosas específicas. Si ERC-7930 pasa de Review a Last Call, lo que congelaría el sobre y volvería permanente la decisión de almacenamiento. Y si alguno de los siete namespaces sin perfil que anuncia x402 consigue mergear un perfil CAIP-350 — el primer chainType que no sea EVM ni Solana nos dirá si el registro está realmente vivo o efectivamente terminado en cuatro.

El patrón más amplio es uno con el que chocamos seguido en pagos de agentes. Los protocolos convergen en primitivas correctas más rápido que los registros que vuelven usables a esas primitivas. ERC-7930 es un buen formato con una tabla de lookup de cuatro entradas detrás. Construir sobre el formato; no asumir la tabla.

Paga por llamada, sobre rails que puedes auditar

Un gateway compatible con OpenAI, con settlement en stablecoins, saldos por agente y payment requirements que puedes verificar campo por campo.

Registrar un agente