x402 exact en EVM: escribir las reglas de verify
La spec de x402 dice que un facilitator debe verificar que un pago EVM "cumple" los requisitos. Tres SDKs leen esa palabra igual, y un pull request abierto quiere dejar de depender de esa suerte. Reescribe el paso de verify como cinco reglas MUST y, al hacerlo, deja a la vista algo en lo que los SDKs no coinciden: qué hacer con un wrapper ERC-6492 que no trae datos de despliegue.
El PR #3925 lo abrió DrVelvetFog el 2026-10-24. Cambia un solo archivo, specs/schemes/exact/scheme_exact_evm.md, y ningún código. Resuelve el lado de spec del issue #2725, abierto el 2026-06-28 y todavía abierto, titulado "EVM exact scheme verification is non-normative and under-specified relative to the SVM scheme's Facilitator Verification Rules (MUST)". En el head que leímos, d0dcbf4, el PR tiene 13 comentarios, ninguna aprobación formal (decisión de review: requerida), y el texto se revisó dos veces desde el primer commit (8d08156, 277bffe, d0dcbf4) porque los reviewers seguían ejecutando el código.
Este post cubre cuatro cosas: qué dice hoy la spec, cuáles son las cinco reglas, qué reprodujimos nosotros en Base mainnet y en los tres SDKs, y qué debería sacar de esto un gateway que vende inferencia por llamada. Todo lo que sigue se verificó el 2026-10-27 contra x402 main en edf6d9e, salvo que se indique otra cosa.
La redacción que dos lectores toman de dos maneras
En main, la lista de verificación EIP-3009 de la spec EVM exact termina el paso 3 con: "Verify the authorization parameters (Amount, Validity Window) meet the PaymentRequirements" (línea 79). La lista de Permit2 dice "Verify the permit2Authorization.amount covers the payment" (línea 189).
"Meet" puede significar igual. "Covers" puede significar al menos. La spec núcleo ya eligió: su tabla de errores define invalid_exact_evm_payload_authorization_value_mismatch como "Payment amount does not exactly match the required amount" (línea 600 de x402-specification-v2.md). El facilitator de TypeScript también eligió: la línea 285 de eip3009.ts rechaza cuando BigInt(authorization.value) !== BigInt(requirements.amount). El PR cita la misma igualdad en Go y Python.
Las implementaciones coinciden y la prosa no. Eso es inofensivo hasta que alguien escribe un cuarto verifier a partir de la prosa. La descripción del PR cuenta que eso pasó: dos verifiers independientes pasaron los diez vectores de conformidad publicados mientras discrepaban con los SDKs de referencia en sobrepago y en el alcance del nonce, porque los vectores no pueden probar lo que la spec no dice.
Para un agente que paga por llamada esto es una cuestión de dinero. Un verifier que acepta un sobrepago acepta a un comprador que firmó más que la cotización. Uno que acepta un monto menor libera el recurso por menos de lo que cuesta. Las dos cosas están a una palabra de distancia en la spec.
Las cinco reglas, como las redacta hoy el PR
La sección nueva es "Facilitator Verification Rules (MUST)", calcada de la que ya tiene el scheme SVM. Enmarca las reglas con una división que usa el propio token: el SignatureChecker de USDC elige la verificación según si el payer tiene código, no según la forma de la firma. En d0dcbf4 las reglas dicen, en resumen:
- Canonicidad de la firma. Si
fromno tiene código y la firma es una ECDSA simple de 65 bytes, se rechaza cuandos > secp256k1n/2(la regla low-s de EIP-2). - Vínculo con el firmante. Si
fromno tiene código, la firma debe recuperar afromdesde el digest EIP-712. Sifromtiene código (una cuenta contrato, o una EOA con delegación EIP-7702),isValidSignature(digest, signature)de ERC-1271 debe devolver el magic value. - Ventana de validez. Se rechaza cuando
now <= validAfteronow >= validBefore. Son las comparaciones estrictas del propio token. Un verifier puede exigir quevalidBeforesupere la hora de verificación por un margen; los SDKs de referencia usan al menos 6 segundos. - Valor exacto. Se rechaza cuando
valueno es igual aamount. El sobrepago es un mismatch, no un margen. - Replay. Se rechaza cuando
authorizationState(from, nonce)es true en el token, o el nonce ya está en el pending set del propio verifier. Los nonces se acotan por autorizador: los mismos 32 bytes desde otrofromno son un replay.
El PR es franco sobre qué reglas son del token y cuáles del protocolo. Las reglas 1 a 3 y la mitad on-chain de la regla 5 reflejan lo que transferWithAuthorization hace cumplir. La regla 4 no: el token ejecuta el value que firmó el payer, sobrepago incluido. La distinción importa para la sección siguiente, porque decide qué reglas puede hacer cumplir el token por ti y cuáles solo tu verifier.
Qué hace USDC con una firma high-s
Una firma ECDSA (r, s, v) tiene una gemela: (r, n - s, v invertido) recupera a la misma dirección. La regla low-s conserva una de las dos. Que un token la haga cumplir es una propiedad de su wrapper de ecrecover. Un verifier que no revisa low-s y llama a ecrecover directo aceptará con gusto una firma que el token después rechaza.
El hilo del issue ya lo había medido. Lo reprodujimos de forma independiente. En Base mainnet, en el bloque 53.151.234, con una clave descartable, un nonce aleatorio y el balance de USDC del payer fijado con un state override de eth_call (slot keccak256(abi.encode(from, 9))), llamamos dos veces a transferWithAuthorization en 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 con la misma autorización:
firma low-s -> eth_call OK
gemela high-s -> execution reverted: ECRecover: invalid signature 's' value
Nada más cambió entre las dos llamadas. La autorización es idéntica, el firmante es idéntico y la gemela high-s es criptográficamente una firma válida de ella. Un verifier que se hubiera detenido en "esto recupera a from" habría liberado el recurso y después habría fallado al settlear.
La redacción del PR es cuidadosa en esto, tras el review. No dice que un pago high-s "nunca puede settlear", porque un settler que normaliza s sí puede ejecutar la gemela low-s. Dice que aceptarlo "predice un settlement que revierte si la firma se envía sin cambios". Eso es lo que sostiene la medición, y nada más.
El estado de los SDKs se movió mientras el PR estaba abierto. Al inicio del hilo solo Go aplicaba low-s de forma explícita. TypeScript (#2728) y Python (#3912) mergearon sus checks el 2026-10-26 a las 16:48 y 16:49 UTC. En main los tres rechazan hoy una firma high-s simple. La descripción del PR todavía describía el estado viejo cuando la leímos; un reviewer lo señaló el 2026-10-27.
Dónde los tres SDKs todavía discrepan
Las reglas 1 y 2 tuvieron que acotarse por código del payer a causa de las smart wallets. Eso trae a ERC-6492, que permite firmar a una wallet aún no desplegada: la firma se envuelve como abi.encode(factory, factoryCalldata, innerSignature) seguida de un sufijo mágico de 32 bytes. Cubrimos el camino feliz en nuestra auditoría de ERC-6492. Aquí la pregunta es el camino infeliz: un wrapper que tiene el sufijo pero no puede desplegar nada.
La regla en la que el PR quedó en d0dcbf4 es estricta. Si el payer no tiene código y la firma termina con el sufijo ERC-6492, el verifier solo puede aceptarla por el camino de despliegue (factory en allowlist y una simulación que despliega y transfiere juntos). No debe quitar el wrapper y correr ecrecover sobre la firma interna, "whether or not the wrapper carries deployment data". La justificación es la del propio ERC-6492: la verificación del sufijo va antes de ecrecover.
Para ver qué tan lejos están los SDKs de eso, armamos siete firmas a partir de una misma firma interna fija de 65 bytes: una low-s simple, una high-s simple y cinco wrappers (factory cero o no cero, calldata vacío o no, y uno con firma interna high-s). Las pasamos por el parser de cada SDK en edf6d9e. TypeScript usa la expresión de classifyErc6492Payer de shared/verifySignature.ts sobre parseErc6492Signature de viem. Go usa ParseERC6492Signature y HasEIP6492Deployment. Python usa parse_erc6492_signature y has_deployment_info. Cada columna es la respuesta de ese SDK a "este wrapper trae datos de despliegue":
forma TS Go Python
low-s simple false false false
high-s simple false false false
factory cero + calldata vacío false false false
factory cero + calldata false false false
factory + calldata vacío true false false
factory + calldata true true true
factory + cd vacío + high-s true false false
Una fila difiere. Para un factory no cero con calldata vacío, TypeScript dice "trae datos de despliegue" y Go y Python dicen que no. La causa se ve en el código. Go y Python evalúan factory != zero && len(calldata) > 0. TypeScript evalúa la veracidad de erc6492Data.data, y viem devuelve el string no vacío "0x" para un calldata vacío, que es truthy.
Un reviewer, robertolocatelli81-dev, corrió los caminos completos de verify y settle contra Base mainnet con el broadcast detenido, y reporta las consecuencias. Nosotros no corrimos el facilitator completo, así que estos son resultados suyos, no nuestros. Con un factory en allowlist y calldata vacío, el verify de TypeScript devuelve válido: la llamada al factory no despliega nada, su resultado no se revisa y el transfer pasa con la firma interna porque el payer sigue sin código. Go y Python, ante los mismos wrappers incompletos, quitan el wrapper y corren ecrecover, que es el fallback que el PR ahora prohíbe. Cuando una firma interna high-s va dentro de un wrapper incompleto, esos dos la rechazan solo en la simulación del transfer, y el settle con la configuración por defecto no corre simulación, así que la envía.
Lo que sí podemos confirmar por nuestra cuenta es la mitad on-chain. Enviamos los dos wrappers incompletos directo a USDC. Ambos revierten con ECRecover: invalid signature length. El token nunca acepta una firma envuelta tal cual. Cualquier aceptación la decide el verifier, y el camino de settle tiene que quitar el wrapper bien.
La resolución del hilo hasta ahora: la regla del wrapper es estricta, el fallback de Go y Python pasa a ser no conforme con el texto nuevo, y el mismo reviewer propuso un arreglo de una línea para TypeScript (agregar un chequeo del tamaño del calldata). El texto aún no define "incompleto". El maintainer de Kanon pidió esa definición, y dijo que la spec debería nombrar el código de rechazo para que los vectores lo sigan. El candidato obvio es invalid_exact_evm_payload_undeployed_smart_wallet, que el facilitator de TypeScript ya define pero que no aparece en ninguna parte de la spec.
Lo que sigue suelto
Quedan cuatro cabos sueltos en d0dcbf4, todos pequeños y todos encontrables leyendo.
Primero, la sección aún dice que las reglas 1 a 4 son "stateless (pure signed-input to verdict)". Eso dejó de ser cierto cuando la regla 2 puede llamar a una cuenta contrato cuya respuesta cambia con el tiempo. Un reviewer propuso una reescritura el 2026-10-26; no estaba en el texto que leímos.
Segundo, el PR deja fuera un mismatch que notó: los tres SDKs devuelven permit2_amount_mismatch y la lista de errores de la spec núcleo no lo contiene. El autor lo difirió a un follow-up para mantener el PR en un solo archivo.
Tercero, el camino ERC-7710 no se toca; el PR lo dice de forma explícita.
Cuarto, los vectores de conformidad viven fuera del repo, en Kanon. Su maintainer dijo que seguirá la redacción de la spec cuando aterrice. Es la dirección de dependencia correcta, pero significa que el texto tiene que mergearse antes de que los vectores puedan probarlo.
Qué significa para LLM4Agents
Vendemos inferencia por llamada, liquidada con x402. El paso de verify es el momento en que decidimos gastar tiempo de GPU sobre una promesa. Todo lo que verify acepta y settle después rechaza es inferencia que servimos gratis. Nuestro trabajo previo sobre el two-phase gap y sobre la finalidad del settlement trataba del tiempo entre verify y settle. Este PR trata de la corrección del propio verify.
Se siguen tres consecuencias. Una: si dependemos de un facilitator alojado para verify y settle, como en la interfaz de facilitator que mapeamos antes, las reglas 1 a 5 son problema del facilitator, y deberíamos poder ver qué implementación y versión corre. Dos: si algún día verificamos en local para bajar latencia, el PR dice sin rodeos que en un camino de verificación local estas reglas "son toda la garantía de settlement". Cada línea sería nuestra. Tres: la regla 4 es nuestra en todos los casos. El token no revisará el precio por nosotros, y un sobrepago aceptado por error es una disputa de facturación que hay que explicar a un agente sin humano a quien preguntar.
El hallazgo de ERC-6492 importa por una razón más estrecha. Las wallets de agentes son cada vez más smart accounts, y las counterfactual son comunes en agentes recién creados. Es justo la población de payers que envía firmas envueltas. Un gateway que la atiende debería saber cuál de los tres comportamientos tiene su facilitator.
Cómo mantenerse en la frontera
Pasos concretos, en orden.
Uno. Convertir las cinco reglas en un test guiado por tabla que podamos apuntar a cualquier facilitator que usemos: construir una autorización por regla, mutar un campo y afirmar que verify la rechaza. Los casos son baratos de generar: value más uno, value menos uno, now == validAfter, validBefore dentro del margen de 6 segundos, un nonce repetido del mismo payer, el mismo nonce de otro payer y una gemela high-s.
Dos. Agregar al mismo test el conjunto de siete formas de wrapper ERC-6492 y registrar la respuesta por facilitator. Ya tenemos el generador; son 20 líneas. El resultado esperado con el texto nuevo es que todo wrapper incompleto se rechace.
Tres. Registrar, por facilitator, la versión de SDK que corre y la fecha, para atar un resultado a un build. El check high-s se agregó a TypeScript y Python el 2026-10-26; un facilitator con código más viejo se comporta distinto hoy.
Cuatro. Preguntar a cualquier facilitator alojado que usemos si su settle corre una simulación por defecto. Los resultados del reviewer muestran que el settle sin simulación por defecto en dos SDKs enviará una firma interna high-s que la simulación de verify habría rechazado. Donde el settle no simula, verify es la única compuerta.
Cinco. Agregar un pre-check duro propio sobre el monto, independiente del facilitator: comparar el value firmado con el precio que cotizamos antes de llamar a verify. Cuesta una comparación de enteros y hace que la regla 4 sea nuestra en vez de prestada. Se combina con los controles de precio esperado del lado comprador que cubrimos en la auditoría de spend controls.
Seis. Vigilar tres cosas en #3925: si "wrapper incompleto" recibe una definición, si se corrige la frase sobre stateless y qué código de error adopta la spec para el rechazo del wrapper. Cuando el texto se mergee, volver a correr los pasos uno y dos contra cada facilitator de nuestra lista y guardar el diff.
Paga por llamada, en stablecoins, sobre una API compatible con OpenAI
Un 402, una firma, más de 345 modelos. Sin suscripción, sin mínimo.
Registra tu agente