EOAs con código EIP-7702 en x402: quién valida la firma
La wallet de tu agente firma un pago igual que siempre. Pero si esa wallet fue actualizada con EIP-7702, otra función decide si la firma es válida — y el contrato que decide no lo elegiste tú.
La mecánica cabe en una frase. EIP-7702, un EIP core en estado Final firmado por Vitalik Buterin, Sam Wilson, Ansgar Dietrichs y lightclient (creado el 7 de mayo de 2024), agrega el tipo de transacción 0x04 con una tupla de autorización [chain_id, address, nonce, y_parity, r, s]. Cuando se ejecuta, en palabras de la spec: "Set the code of authority to be 0xef0100 || address. This is a delegation indicator." Veintitrés bytes: tres de prefijo y veinte de dirección del delegate.
Esos veintitrés bytes son todo el tema de esta auditoría. Cada camino de pago en stablecoin que usa un agente — EIP-3009 transferWithAuthorization, Permit2, depósitos de batch-settlement — hace la misma pregunta antes de mover dinero: ¿esta dirección tiene código? Antes de la delegación la respuesta era no, y el token corría ecrecover. Después de la delegación la respuesta es sí, y el token llama a isValidSignature en el contrato que apuntó el proveedor de la wallet.
Como siempre en esta serie, todo lo que sigue es de fuente primaria. Leímos las specs y los contratos, clonamos el monorepo de x402 en HEAD 8468e3a (26 de agosto de 2026), escaneamos 24 horas de Base mainnet y probamos el verificador real de USDC contra cada implementación de delegate que encontramos. Todas las mediciones son propias, del 27 de agosto de 2026.
El router dentro del token
Empecemos por el token, porque el token tiene la última palabra. El repo stablecoin-evm de Circle incluye un util/SignatureChecker.sol cuya decisión es una sola rama: if (!isContract(signer)) recupera la clave con ECRecover.recover, y en caso contrario llama a isValidSignature de ERC-1271 y compara el resultado con IERC1271.isValidSignature.selector. USDC v2.2 documenta la consecuencia en el natspec de transferWithAuthorization, receiveWithAuthorization y permit: la firma puede venir de una wallet EOA o de una wallet contrato.
Esa rama lee extcodesize, y una delegation designation de 7702 es código. Así que un EOA delegado toma el camino de contrato — no porque alguien lo haya decidido, sino porque hay 23 bytes en la dirección.
Antes de tocar nada confirmamos el dominio contra el que íbamos a firmar. El USDC de Base (0x8335…2913) devuelve DOMAIN_SEPARATOR() = 0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f, idéntico al valor que computamos localmente con name USD Coin, version 2, chainId 8453 y la dirección del token. Mismo dominio, mismo digest, cero ambigüedad en lo que sigue.
Qué hizo x402 al respecto
El monorepo de x402 se tomó esto en serio en junio. El PR #2658, "feat: improve & document wallet compatibility" de Carson Roscoe, se abrió el 17 de junio de 2026 y se mergeó el 25 de junio de 2026: 115 archivos, +12.115/−3.018, 23 commits. Salió en @x402/evm 2.17.0 con una entrada de changelog que promete que "payments verify and settle consistently across plain EOAs, deployed smart accounts (ERC-4337 / ERC-7579), counterfactual ERC-6492 wallets, and ERC-7702-delegated EOAs".
El núcleo es un archivo, typescript/packages/mechanisms/evm/src/shared/verifySignature.ts, que reimplementa la rama onchain en TypeScript y se niega a ser más permisivo que ella:
// if signer.code.length == 0:
// ecrecover(digest, sig) == signer
// else:
// IERC1271(signer).isValidSignature(digest, sig) == 0x1626ba7e
export function verifyHashSignatureWithCode(signer, address, code, digest, signature) {
if (!code || code === "0x") {
return verifyECDSA(address, digest, signature);
}
return verifyERC1271(signer, address, digest, signature);
}
El comentario que lo acompaña nombra la regresión que motivó el trabajo: la primitiva estricta "deliberately does NOT fall back to ECDSA when EIP-1271 returns failure. That fallback (which viem's publicClient.verifyTypedData performs) makes pre-verify accept signatures that on-chain rejects — most visibly for ERC-7702 delegated EOAs whose delegate's isValidSignature does not accept raw owner ECDSA."
El SDK de Go arrastra la misma historia en verify_universal.go: "The old 65-byte EOA fast-path (that skipped GetCode) was removed because it caused pre-verify to accept signatures that on-chain verifiers routed to isValidSignature and rejected." Python lo replica como verify_typed_data_strict. Tres SDKs, una regla: el verify del facilitator nunca debe decir sí a algo que settle va a rechazar.
El PR también agregó docs/advanced-concepts/wallet-compatibility.mdx, 155 líneas que definen cinco tipos de wallet. A es un EOA plano. B es una smart account desplegada. C es una wallet counterfactual ERC-6492. D y E son ambas EOAs con delegación 7702, separadas por lo que acepta el delegate: D es un "permissive delegate" que toma ECDSA cruda del dueño, E es un "strict delegate" que "requires a wrapped or prefixed format". La matriz de soporte marca D como soportada en todos los schemes y E como no soportada en todos, con una nota al pie admirablemente directa: "x402 signs with signTypedData, which produces a raw 65-byte ECDSA signature that a strict delegate rejects, so the payment fails on-chain even though signing succeeded. This is the verifier behaving correctly, not an x402 limitation."
Ahí está toda la tensión del diseño. El cliente de x402 (src/exact/client/eip3009.ts) llama a signer.signTypedData y entrega 65 bytes. Que esos 65 bytes sean dinero depende de un contrato elegido por quien actualizó la wallet.
isValidSignature(digest, rawECDSASig) sobre una dirección 7702: 0x1626ba7e significa permissive (Tipo D), 0xffffffff o un revert significa strict (Tipo E). Nadie había corrido esa prueba sobre la población de producción. Así que la corrimos.
Censo: quién paga realmente con EIP-3009 en Base
Escaneamos los bloques 50.472.851 a 50.516.051 de Base — 43.200 bloques, unas 24 horas — buscando AuthorizationUsed(address,bytes32) en USDC. Es el evento que emite cada pago EIP-3009, la primitiva de settlement que describimos en nuestro deep dive de EIP-3009.
La ventana contiene 74.029 autorizaciones en 73.991 transacciones distintas, firmadas por 6.687 direcciones distintas. Después llamamos eth_getCode sobre cada una de esas 6.687 direcciones. El reparto:
- 6.125 direcciones (91,6%) sin código — EOAs planos, Tipo A.
- 497 direcciones (7,4%) con exactamente 23 bytes que empiezan en
0xef0100— smart EOAs 7702, Tipo D o E. - 65 direcciones (1,0%) con bytecode de contrato normal — smart accounts desplegadas, Tipo B.
Ponderado por pagos en vez de por direcciones: 69.267 autorizaciones (93,6%) vinieron de EOAs planos, 4.330 (5,9%) de cuentas 7702 y 432 (0,6%) de contratos desplegados. Una de cada diecisiete autorizaciones de stablecoin en Base ya la firma una cuenta cuyas reglas de firma las fija un contrato delegate.
Esas 497 cuentas apuntan a 18 implementaciones distintas de delegate. Ordenadas por número de cuentas, con nombres tomados de fuentes verificadas en Base:
0x7702cb55…7176c EIP7702Proxy 243 cuentas 708 auths
0x63c0c19a…ae32b EIP7702StatelessDeleGator 117 cuentas 1.596 auths
0x955d8413…22c6f TKGasDelegate 48 cuentas 76 auths
0x000066a0…30084 TKGasDelegate 32 cuentas 49 auths
0x612373d7…951d3 CaliburEntry 13 cuentas 51 auths
0x00000000…8f00 CaliburEntry 7 cuentas 29 auths
0xd6cedde8…75b28 Kernel 7 cuentas 33 auths
0xe40ccb2d…6fa4 SmartWalletEntry 6 cuentas 15 auths
0xcc0c946e…6f17b TokenPocketSimple7702 5 cuentas 50 auths
0xd2e28229…530fb Biz 5 cuentas 5 auths
0x5a7fc113…6f6d AmbireAccount7702 3 cuentas 4 auths
0xe6cae83b…555b Simple7702Account 2 cuentas 1.698 auths
… más seis con una cuenta cada una (3 sin verificar)
La primera entrada, EIP7702Proxy, lleva @author Coinbase (https://github.com/base/eip-7702-proxy) en su código verificado: 243 cuentas, el 48,9% de la población. La segunda es el EIP7702StatelessDeleGator de MetaMask, cuyo path de código src/EIP7702/EIP7702StatelessDeleGator.sol lo ubica dentro del Delegation Framework que auditamos en la auditoría del rail ERC-7710. El CaliburEntry de Uniswap aparece en tres direcciones distintas, el Kernel de ZeroDev en una, Ambire en dos y la referencia de eth-infinitism Simple7702Account en una.
Dos proveedores de wallets de consumo concentran dos tercios de las cuentas. Las sorpresas están en la cola larga.
La prueba: 18 delegates, una firma cruda
Saber a qué delegate apunta una cuenta no dice nada sobre si aceptará un pago. Así que corrimos la prueba que prescriben los docs de x402 — no contra mocks de testnet, sino contra el contrato real de USDC en Base con el código real de cada delegate.
El método: tomar una clave privada propia, construir un mensaje TransferWithAuthorization genuino para el USDC de Base, firmarlo con signTypedData exactamente como hace el cliente de x402 (65 bytes) y luego correr eth_call con un state override que fija el código de nuestra dirección en 0xef0100 || delegate. Los RPC públicos de Base resuelven las delegation designations durante la ejecución, así que la llamada corre el bytecode real del delegate en el contexto de nuestra cuenta. Llamamos a transferWithAuthorization sobre el propio USDC, sin saldo, para que los dos desenlaces sean distinguibles: un revert con FiatTokenV2: invalid signature significa firma rechazada, y un revert con ERC20: transfer amount exceeds balance significa que pasó la validación y solo falló por falta de fondos.
Trece de los dieciocho delegates dejaron pasar la firma y revirtieron por saldo. Cinco la rechazaron con FiatTokenV2: invalid signature: Kernel, Biz, SemiModularAccount7702 y dos implementaciones sin verificar. Para los trece que aceptaron, una llamada directa a isValidSignature(digest, sig) devolvió el magic value 0x1626ba7e — Tipo D de manual.
También corrimos la prueba de detección de ERC-7739, el hash centinela 0x7739…7739 con firma vacía que una cuenta compatible responde con 0x77390001. Solo respondió el CaliburEntry de Uniswap, en sus tres direcciones — consistente con que ERC7739Utils.sol aparezca en sus fuentes verificadas. El proxy de Coinbase y SmartWalletEntry devolvieron 0xffffffff; el resto revirtió. ERC-7739 sigue en Draft (creado el 28 de mayo de 2024) y su defensive rehashing es exactamente el "wrapped format" contra el que advierte el Tipo E del doc de x402.
La corrección que forzaron las cuentas vivas
Una cuenta sintetizada tiene el código del delegate y storage vacío. Una real tiene storage: un owner registrado, un validator instalado, un nonce tracker inicializado. Esa diferencia no es cosmética, y nuestros propios datos la detectaron.
Tomamos las cuentas 7702 del censo, sacamos sus transacciones reales, decodificamos el calldata, recomputamos el digest EIP-712 desde los argumentos y llamamos isValidSignature(digestReal, firmaReal) sobre la cuenta viva. Los diez delegates que pudimos muestrear así devolvieron 0x1626ba7e. Entre ellos estaban Kernel y dos de las implementaciones que habían rechazado nuestra firma sintética — y en la ventana muestreada, cuentas Kernel liquidaron tres autorizaciones EIP-3009 por el overload legacy (v, r, s), que no es otra cosa que una firma de 65 bytes del dueño partida en tres campos.
Así que el hallazgo honesto es más filoso que el binario de los docs: Tipo D contra Tipo E no es una propiedad del contrato delegate. Es una propiedad del delegate más el storage de la cuenta en el momento del pago. El mismo bytecode de Kernel rechaza una firma cruda del dueño cuando no hay validator instalado y la acepta una vez que la cuenta está inicializada. Una wallet de agente puede ser Tipo E durante sus primeros minutos de vida y Tipo D después — y quien opere una flota clasificando wallets una sola vez, al provisionarlas, va a cachear la respuesta equivocada.
Las formas de firma en producción dicen lo mismo. Sobre 184 llamadas EIP-3009 directas desde cuentas 7702 en nuestra muestra, 176 llevaban una firma plana de 65 bytes, cinco llevaban 224 bytes (el formato envuelto que produce el stack de Coinbase), una llevaba 66 bytes (un prefijo de validator de un byte delante de una firma normal, el patrón ERC-7579) y dos llevaban 640 y 736 bytes. El camino permisivo domina porque los proveedores de wallets quieren que las firmas de sus usuarios funcionen en todas partes — pero las formas envueltas ya están en el cable.
Cómo se ve realmente este tráfico
Los montos son la parte que vale la pena mirar dos veces. Sobre esas mismas 184 autorizaciones: mediana 0,005 USDC, percentil 25 en 0,002, percentil 75 en 0,01, máximo 18. Ciento sesenta y siete de 184 estaban por debajo de diez centavos.
El pagador 7702 más activo de la ventana, 0x2b38a4bb…ab7d delegado a Simple7702Account, firmó 1.689 autorizaciones en 24 horas. Cada una de las muestreadas fue de exactamente 0,005 USDC al mismo destinatario, difundidas por nueve relayers distintos. El segundo, 0x41cdc787…f3a5 sobre el delegator de MetaMask, firmó 741 — todas de 0,002 USDC, un solo destinatario. Precio fijo, una contraparte, muchos relayers, un pago cada cincuenta segundos: eso no es un humano haciendo clic. Es una máquina en un medidor, usando precisamente la primitiva que estandariza x402, y ya corre hoy con un EOA actualizado.
Tres costuras que vale nombrar
Primera: los helpers de 7702 en x402 son decorativos. Los tres SDKs traen un detector — isERC7702Delegation en TypeScript, is_erc7702_delegation en Python, IsERC7702Delegation en Go — y cada uno lleva un docstring que dice que los helpers "are diagnostic only. The signature-verification path does not branch on 7702 detection". La versión de TypeScript se exporta desde el índice del paquete; no encontramos ninguna call site en todo el camino de pago. El SDK puede decirte que un pagador es un smart EOA y nunca usa el dato — así que no hay pre-flight en el cliente que avise a una wallet Tipo E antes de que desperdicie una firma.
Segunda: la matriz de wallets no corre tal cual. Los tests de integración en TypeScript, Python y Go definen un caso "Wallet D — ERC-7702 EOA delegated to PermissiveECDSADelegate", y ese nombre de contrato no aparece en ningún lado salvo en esos archivos de test: no existe tal contrato en el repo. El test se saltea salvo que un operador aporte una clave ya delegada en Base Sepolia. El cliente de ejemplo de 7702, por su parte, delega a Biconomy Nexus en 0x0000…3B03 con el comentario de que "matches the 7702 → 7579 stack used by Privy-based wallets like Bankr" — una cuenta ERC-7579, justamente la familia que los docs señalan como la candidata más probable a Tipo E.
Tercera: la estrictez cuesta un round-trip. Quitar el fast-path de 65 bytes implica que cada verify llama ahora a eth_getCode, incluso para el 91,6% de pagadores que no tienen código. Contra un RPC público de Base medimos esa llamada en 119 ms de mediana (114 ms mínimo, 223 ms máximo sobre 15 llamadas). En el camino caliente de un facilitator eso es un salto de red extra por pago, además del salto adicional cuando se dispara la rama ERC-1271. La corrección lo vale — la alternativa es un verify que dice sí y un settle que revierte — pero es una línea real de coste cuando una flota paga decenas de miles de veces por día.
Qué significa para LLM4Agents
Nuestro gateway recibe pagos en stablecoin desde wallets de agentes que no controlamos. Esta auditoría dice que el tipo de wallet es un input de primera clase para saber si un pago va a pasar, y que ese tipo puede cambiar por debajo nuestro sin ninguna acción de nuestra parte: un usuario abre una app de wallet, el proveedor manda una transacción tipo 0x04, y una cuenta que ayer era Tipo A hoy es Tipo D o E. Nada cambia en el payload del pago. Nada cambia en el código del agente. Cambia la rama del verificador.
Tres consecuencias concretas. Una: nuestra clasificación del pagador no se puede cachear en el registro. Dada la dependencia del storage que medimos, la única clasificación confiable es la que se toma contra el estado actual de la cuenta, y el único chequeo verdaderamente definitivo es la simulación de la transferencia. Dos: el modo de falla es silencioso desde el punto de vista del agente. La firma tiene éxito, el payload está bien formado, y el rechazo llega como un error con forma de invalid_signature desde un facilitator — el tipo de callejón sin salida que en un loop autónomo se convierte en tormenta de reintentos. Tres: el token importa tanto como la wallet. El doc de x402 advierte que los tokens ERC-3009 viejos usan solo ecrecover y nunca llaman a isValidSignature, lo que invierte todo el cuadro: en esos tokens la firma de una smart account falla y solo funcionan las claves de EOA crudas. Cualquier activo que aceptemos necesita su ruteo de firmas verificado, no supuesto.
Hay un lado positivo que conviene decir con la misma claridad. Una wallet 7702 conserva su clave y su dirección mientras gana código, lo que significa que puede sostener un allowance de Permit2, usar las extensiones de gas sponsoring que las smart accounts desplegadas no pueden, y seguir firmando transferWithAuthorization como un EOA — la posición de "lo mejor de ambos" que esbozamos cuando miramos account abstraction para wallets de agentes. Las 4.330 autorizaciones que medimos en un solo día son agentes y bots que ya viven en esa posición.
Cómo mantenerse en la frontera
Cinco movimientos, en orden.
Uno: clasificar en el momento del pago, no en el alta. Agregar una prueba de tipo de wallet al camino de pago — eth_getCode, luego el test de la designación de 23 bytes, luego la dirección del delegate — y adjuntar el resultado al registro del pago en vez de al registro de la cuenta. Cachear por segundos, no por la vida de la wallet.
Dos: hacer pre-flight del delegate antes del primer pago. Correr la prueba prescrita isValidSignature(digest, rawECDSASig) contra el estado vivo de la cuenta y mostrarle la respuesta al operador del agente en lenguaje claro: el delegate de esta wallet acepta las firmas que produce nuestro cliente, o no. Ambas respuestas son accionables antes de que se mueva dinero; ninguna lo es después de un revert en settle. Es exactamente el dato que los helpers de diagnóstico del SDK ya computan y luego descartan.
Tres: mantener un registro de delegates con comportamiento observado. Dieciocho implementaciones gobiernan toda la población en una cadena, y dos de ellas cubren dos tercios. Una tabla interna chica — dirección del delegate, nombre verificado, resultado de la detección ERC-7739, largos de firma aceptados observados — convierte una pregunta abierta de compatibilidad en un lookup con sonda de respaldo. La nuestra costó un día de llamadas RPC.
Cuatro: hacer el error legible para un caller autónomo. Cuando un pago falla porque el delegate rechazó el formato de firma, el agente debería recibir un motivo distinto y no reintentable, con un remedio sugerido (inicializar la cuenta, instalar un validator ECDSA por defecto o pagar desde otra wallet), no un string genérico de firma inválida. Los loops de reintento son la forma cara de descubrir que una wallet es Tipo E — y, como mostró nuestra auditoría de spend controls, los strings de error que el modelo puede leer son los que moldean su próximo movimiento.
Cinco: seguir la adopción de ERC-7739 como indicador adelantado. El Calibur de Uniswap ya responde la sonda de detección hoy. Si el defensive rehashing se extiende a los dos proveedores que concentran dos tercios de la población, el default permisivo desaparece y todo cliente que firme typed data cruda — incluido el de x402 — necesita un camino de firma envuelta. Ese es un cambio a nivel protocolo con mucho tiempo de anticipación, y la sonda del hash centinela da el aviso temprano por el precio de un eth_call.
Pagos que pasan al primer intento
LLM4Agents corre wallets de agentes financiadas con stablecoins sobre un gateway compatible con OpenAI — y audita la capa de firmas antes de que llegue a tu flota.
Registra tu agente