Una firma, cincuenta invocaciones: auditoría del two-phase gap de x402
x402 verifica un pago antes de que corra el recurso y lo liquida después. Tres grupos de investigación pasaron 2026 documentando qué cabe dentro de esa ventana. Reprodujimos sus ataques contra el stack publicado hace cuatro días.
El diseño es deliberado. La confirmación on-chain tarda segundos; una petición HTTP no debería. Así que x402 parte el procesamiento del pago en dos: un /verify de solo lectura antes de que se ejecute el handler, y un /settle que compromete estado después de que retorna. El protocolo llama a esto payment flow authorization, y es el default de todo scheme en toda red.
Entre esas dos llamadas la cadena no registró nada. La autorización es válida, no consumida y —porque x402 la transporta como artefacto bearer en un header— perfectamente replayable. Ese intervalo es lo que la literatura llama hoy el two-phase gap.
Este post hace tres cosas: resume qué midieron realmente los ataques publicados, los reproduce contra @x402/express 2.24.0, y audita el monorepo en HEAD para separar lo que los parches de agosto cerraron de lo que solo movieron de lugar.
Tres papers, una ventana
El tratamiento más sistemático es Free-Riding the Agentic Web: A Systematic Security Analysis of x402 Payments (Ling, Huang, Du, Chen, Zhou, Wu y Wang; City University of Hong Kong, Zhejiang University y CUHK), publicado el 22 de junio de 2026 y presentado en ATC '26. Destila cinco invariantes de seguridad, asigna cada hallazgo a una de tres capas —protocolo, SDK, deployment— y nombra cinco clases de fallo.
Dos importan acá. F1, sustitución de recursos: la autorización firmada se compromete con un payee y un monto, pero no con el recurso que se está comprando, así que una firma emitida para un endpoint puede re-adjuntarse a cualquier hermano del mismo precio. Los autores reportan que funciona en 100 de 100 rondas controladas. Su censo de exposición —una paginación completa de 24.875 recursos del registry de CDP, 915 merchants sobre 868 hosts tras de-duplicar— encontró 331 de esos 868 hosts (38%) exponiendo clusters de hermanos al mismo precio, mediana 3, máximo 178, y 91 hosts con diez o más recursos mutuamente sustituibles.
F2, la carrera de doble liquidación: peticiones concurrentes que llevan la misma autorización pasan todas la verificación antes de que alguna llegue a la cadena. Contra el facilitator oficial de CDP en Base mainnet, usando su propio merchant para que todo el impacto económico quedara interno, cincuenta rondas de veinte peticiones concurrentes produjeron entrega duplicada del servicio en el 6% de las rondas: dos respuestas HTTP 200 distintas, con payloads distintos, contra una sola liquidación confirmada.
La segunda fuente, Five Attacks on x402 Agentic Payment Protocol (Li, Wang y Wang; Ohio State University, CSIRO y University of Manchester, 12 de mayo de 2026), llega al mismo lugar por otro camino. Su hallazgo de replay reporta 248 grants a nivel HTTP a partir de una sola liquidación on-chain contra un endpoint vivo. Su hallazgo de confusión de caché es todavía más limpio: ruteado por nginx, el 100,0% de las peticiones no pagadas sobre 1.000 pruebas recibió contenido pagado desde caché, y cayó a 0,0% cuando la respuesta llevaba Cache-Control: no-store.
El tercero es un estudio de caja negra: Exploiting the Two-Phase Gap in the x402 Protocol for Autonomous AI Payments (Hwang y Choi, Sungkyunkwan University, poster de NDSS 2026). Tomaron siete servicios x402 públicos, obtuvieron de cada uno una prueba de pago legítima y la replayaron en ráfagas sincronizadas de diez. Cinco servicios lo suprimieron por completo: un HTTP 200 sobre diez. Dos no: una API de datos on-chain devolvió cuatro y luego cinco respuestas exitosas en dos ráfagas, y un sitio de referencia de x402 devolvió entre dos y diez a lo largo de cuatro ráfagas. Su Tabla I trae la línea que define el problema entero: el número de transacciones registradas on-chain fue 1 en todas las ráfagas.
Qué dice la especificación al respecto
Leímos la especificación v2 en HEAD. La sección 10.1, "Replay Attack Prevention", lista cuatro protecciones: el nonce de 32 bytes de EIP-3009, la prevención de reuso de nonce a nivel contrato, las ventanas de validez explícitas y la verificación de firma sobre la autorización.
Todas son controles de capa cadena. Ninguna exige que el resource server reclame un pago antes de otorgar servicio. Eso no es un descuido de redacción: es una descripción fiel de lo que el protocolo garantiza realmente, y es exactamente el hueco en el que aterrizan los tres papers.
La sección 6.1 es más nueva y más interesante. Ahora formaliza los payment flows como una tabla explícita:
// specs/x402-specification-v2.md, sección 6.1
authorization verify → resource → settle → respond // default
upfront settle → resource → respond
escrow settle → resource → settle → respond
con una invariante declarada: "al menos un chequeo —un verify o un settle antes del recurso— MUST correr antes de que el recurso se ejecute".
upfront y escrow son los dos ordenamientos que cierran el gap por construcción. escrow en particular es el patrón reserve-then-commit que los autores de Free-Riding proponen como two-phase locking pesimista. Ambos están en la spec. La pregunta es dónde están realmente disponibles.
Reproducción: una ráfaga de veinte sobre el stack actual
Armamos un harness contra los paquetes publicados el 27 de agosto de 2026: @x402/core, @x402/express y @x402/evm, todos 2.24.0. Un facilitator local implementa /supported, un /verify de solo lectura y un /settle que modela la cadena: 400 ms de latencia de confirmación y un nonce que se puede consumir exactamente una vez. Un resource server expone dos rutas al mismo precio detrás del paymentMiddleware de fábrica. Cada handler incrementa un contador, que hace de cómputo pagado.
El cliente firma exactamente una autorización para GET /weather usando el scheme exact real de @x402/evm, y después dispara N peticiones concurrentes con el header PAYMENT-SIGNATURE idéntico.
// N = 20, una autorización firmada, replayada concurrentemente
{
"byStatus": { "200": 1, "402": 19 },
"grants": 1,
"handlerInvocations": 20,
"facilitator": { "verify": 20, "settle": 20,
"settleOk": 1, "settleFail": 19 }
}
Lo corrimos con N = 5, 20 y 50. El resultado tiene la misma forma todas las veces: grants = 1, invocaciones del handler = N. Veinte verifies, veinte settles, un nonce consumido, un HTTP 200.
El grant duplicado está cerrado. Es un fix real y conviene decirlo claro. La razón se ve en el middleware: @x402/express intercepta writeHead, write, end y flushHeaders, bufferea cada llamada, y las replaya al socket recién después de que la liquidación retorna éxito. Nada llega al cliente hasta que el pago se comprometió. Verificamos la misma propiedad en los adapters de Hono, Fastify y Next, en el middleware de net/http de Go —que envuelve el writer en un responseCapture sobre un bytes.Buffer— y en el middleware de FastAPI y Flask de Python. Todos bufferean, y todos condicionan la liquidación a un status de respuesta por debajo de 400.
Esto es precisamente la defensa de "deferred delivery" que el paper de Free-Riding propone en su sección 5.3, shippeada por default en tres stacks de lenguajes. También cierra la variante de interrupción de stream de ese paper, donde un cliente consume el output y después mata la conexión TCP antes de que dispare el callback de liquidación. No se puede interrumpir un stream que nunca arrancó.
El ataque de caché también está cerrado. El challenge 402 lleva Cache-Control: no-store —lo confirmamos sobre el cable— y toda respuesta pagada pasa por withPrivateCacheControl, que agrega private para que las cachés compartidas no la almacenen. Es exactamente la mitigación que el paper de Five Attacks midió llevando la fuga de nginx de 100,0% a cero.
En qué convirtió el fix al ataque
Ahora la otra mitad de la medición. Las invocaciones del handler son iguales a N en todas las ráfagas. El merchant ejecutó el trabajo pagado veinte veces, y cincuenta veces, por un solo pago.
Nada en el camino de la petición reclama la autorización antes de que corra el handler. Cada petición concurrente llama a /verify por su cuenta, recibe una respuesta limpia, ejecuta el handler, bufferea el output, llama a /settle, y recién ahí descubre que otra petición consumió el nonce primero. Diecinueve de veinte respuestas se descartan después de que el cómputo que las produjo ya se gastó.
Para un endpoint de JSON estático eso es un error de redondeo. Para la carga de trabajo que x402 existe para monetizar —inferencia, retrieval, tool calls— es el costo entero. El stack v2 no eliminó la carrera de doble liquidación. Movió el daño de output robado a cómputo robado, que es exactamente el modo de fallo que el paper de Free-Riding archiva bajo F4, denial of settlement, donde midieron ratios de fuga de 86,95% y 100% contra un deployment de inferencia vivo.
Dos reportes de producción de esta clase siguen abiertos en el repositorio: el issue #1062, abierto el 31 de enero de 2026, sobre un timeout de facilitator más corto que el tiempo de confirmación de Base, y el issue #1805, abierto el 25 de marzo de 2026, que reporta una prueba de liquidación reusada en cinco peticiones concurrentes.
La sustitución de recursos sigue funcionando, y ahora es estructural
Segunda reproducción. Firmamos una autorización contra GET /weather y la replayamos, sin modificar, contra GET /premium, al mismo precio. Una petición, sin concurrencia, sin manipular headers.
{ "target": "/weather", "replayedAgainst": "/premium",
"byStatus": { "200": 1 }, "grants": 1 }
HTTP 200, el body premium, liquidación exitosa. La razón está en las definiciones de tipos, y es más filosa en v2 de lo que era en v1.
El mensaje EIP-3009 que firma el cliente lo fija el contrato del token: {from, to, value, validAfter, validBefore, nonce}. No hay campo para un recurso, y el merchant no puede agregarlo: el typehash vive en el ERC-20 desplegado. Así que cualquier binding tiene que ocurrir en la capa SDK, sobre datos que la firma no cubre.
En v1, PaymentRequirements llevaba resource: z.string().url(), y el matcher eligió ignorarlo. Ese código sigue en el repositorio en HEAD:
// typescript/packages/legacy/x402/src/shared/middleware.ts
return paymentRequirements.find(
value => value.scheme === payment.scheme && value.network === payment.network,
);
En v2 el campo desapareció. PaymentRequirements es ahora {scheme, network, asset, amount, payTo, maxTimeoutSeconds, extra}: el recurso se mudó a un ResourceInfo aparte, requerido en el challenge PaymentRequired y opcional en el payload de pago. El matcher de v2 es genuinamente más estricto que el de v1: hace deep-equal de todos los campos core del requirement que el cliente devuelve y chequea extra como subconjunto. Pero el recurso no está entre los campos que compara, porque el objeto ya no tiene uno. Grepeamos los caminos de server y HTTP buscando alguna comparación del recurso del payload entrante contra la ruta que se está sirviendo. No hay ninguna.
Así que v2 hizo el predicado completo sobre un conjunto de campos que excluye el único que identifica qué se está comprando. Dos rutas que comparten red, asset, precio y payee son, a nivel protocolo, la misma compra. Esto es lo que el paper llama autorización flotante, y su censo del registry sugiere que no es un caso de borde: un tercio de los hosts relevados expone recursos hermanos a precios idénticos.
Qué shippeó agosto realmente
Dos merges del 25 de agosto de 2026 son directamente relevantes, y ambos son más angostos de lo que parecen al principio.
PendingSettlementStore es recovery, no idempotencia
El PR #3214 agrega una interfaz PendingSettlementStore en TypeScript, Go y Python, indexada por un identificador determinista derivado del payload del pago. El nombre sugiere deduplicación. La implementación es otra cosa.
El store se escribe solo cuando un intento de settle transmite una transacción y después falla en confirmar. En un reintento, el mecanismo reconcilia contra el hash ya transmitido en vez de firmar uno segundo. Las entradas se borran al confirmar, y el default en memoria las expira a los cinco minutos. En el camino feliz —verify limpio, settle confirma— nunca se crea una entrada. No puede deduplicar un replay porque no guarda registro de un pago que funcionó. Es recovery de caídas y timeouts del facilitator, correctamente acotado y documentado como tal, sentado una capa debajo del problema.
La lógica de reintento asociada en el resource server está capada en exactamente un intento sin backoff, que es la decisión correcta: la capa de mecanismo es dueña de cualquier espera.
El flow upfront aterrizó donde menos hacía falta
El PR #3240 agrega el payment flow upfront —liquidar antes de que corra el handler— al scheme exact. Enumeramos las declaraciones en HEAD. Trece filas de asset transfer method sobre doce mecanismos ahora anuncian ["authorization", "upfront"]: EVM, SVM, Aptos, Stellar, XRPL, NEAR, Hedera, TVM, AVM, Keeta y Concordium.
Todas son exact: precio fijo, conocido antes de que corra el handler, sin cómputo dinámico. El scheme donde liquidar-antes-de-entregar ya era alcanzable buffereando.
El scheme upto en EVM —pricing variable, el que los papers midieron sobre Arbitrum, el que factura por tokens cuya cantidad se desconoce al momento de la petición— declara { permit2: { supported: ["authorization"] } }. Solo authorization. Lo mismo batch-settlement. El único lugar donde existe un compromiso pre-handler para upto es la variante channel de SVM, que declara ["escrow"]: un settle de depósito antes del handler, un settle de claim o cancel después. Ese es el diseño reserve-commit que pide la literatura, y shippea en exactamente una red para exactamente un scheme.
Una línea más de la sección 6.1 merece atención. Cuando un recurso ofrece a la vez authorization y un flow pre-handler para la misma petición, la spec dice que los clientes "SHOULD prefer authorization". Del lado del comprador eso es racional: no pagues antes de tener la mercadería. También significa que el flow que protege al merchant es el que la especificación le dice a todo agente conforme que se saltee. Los servers que quieran upfront van a tener que ofrecerlo solo, lo que convierte una postura de seguridad en una negociación que el cliente puede rechazar.
El marcador honesto
Contra el catálogo de ataques publicado, sobre el stack que shippea hoy: los grants duplicados bajo replay concurrente están cerrados por el buffering incondicional de la respuesta. La fuga de contenido pagado por cachés compartidas está cerrada por no-store en el challenge y private en la respuesta. La interrupción de stream antes de liquidar está cerrada por la misma razón que el buffering cierra todo lo demás.
Sigue abierto: una autorización todavía compra N ejecuciones del handler pagado, porque nada reclama el pago antes de que arranque el trabajo. La sustitución de recursos tiene éxito en la primera petición contra hermanos del mismo precio, y los cambios de tipos de v2 la volvieron estructural en vez de incidental. El camino de allowance de upto en EVM no tiene nonce por deducción ni compromiso pre-handler. Y la propia sección de seguridad de la especificación sigue describiendo la protección contra replay como algo que hace la cadena.
Nada de esto vuelve inutilizable a x402. Vuelve a la capa de deployment portante de maneras que el protocolo no anuncia, que es la misma conclusión a la que llegamos auditando el cap de gasto por defecto: el formato de cable está especificado con rigor, y el envelope operativo alrededor queda en manos de quien despliega.
Qué significa para LLM4Agents
Somos un merchant sobre este rail, y la carga que vendemos es de la cara. Una llamada al gateway es inferencia: segundos de GPU gastados antes de que un solo byte llegue al cliente. La distinción entre "grant duplicado" y "trabajo duplicado" que parece académica para un endpoint de clima es, para nosotros, la diferencia entre una respuesta descartada y un minuto de H100 pagado que no recuperamos.
Eso reencuadra el resultado de la ráfaga. En nuestro stack, un atacante con una autorización válida no puede conseguir cincuenta respuestas. Puede conseguir que computemos cincuenta respuestas. El ingreso está protegido; el costo no. Cualquier planeamiento de capacidad que asuma que peticiones servidas equivale a pagos cobrados está equivocado por la concurrencia que elija un adversario.
La sustitución de recursos pega distinto. Nuestras rutas se cotizan por modelo y por token, así que los hermanos del mismo precio existen por construcción: dos modelos a la misma tarifa en la misma red hacia el mismo payee son indistinguibles para el protocolo. Una autorización emitida para una ruta barata es una autorización válida para cualquier ruta a ese precio. El control de acceso sobre la selección de modelo no puede vivir en la capa de pago, porque la capa de pago no sabe qué modelo se pidió.
Las partes ya arregladas también importan. El buffering de la respuesta significa que un cliente no puede consumir nuestro output y después soltar la conexión antes de liquidar —una preocupación real en generaciones largas, y algo que de otro modo habríamos tenido que resolver nosotros—. La directiva de caché private significa que nuestras respuestas pagadas no se van a servir desde un edge de CDN a alguien que nunca pagó, que para un endpoint compatible con OpenAI detrás de cualquier proxy no es una exposición teórica.
El hueco de upto es el que condiciona nuestro roadmap directamente. La facturación medida para inferencia contada por tokens es exactamente para lo que existe upto, y en EVM shippea sin nonce por deducción y sin compromiso pre-handler. El scheme que encaja con nuestro modelo de facturación es el scheme con la fuga medida más ancha.
Cómo mantenerse en la frontera
Pasos concretos, en el orden en que creemos que hay que darlos.
Primero, reclamar el pago antes de que corra el handler. Un insert atómico sobre la tupla de nonce de autorización y ruta, en el store que ya está delante del gateway, tomado antes de despachar cualquier cómputo. Los perdedores reciben un 402 de inmediato. Este es el único cambio que convierte el resultado de cincuenta invocaciones en uno de una invocación, cuesta un round trip a Redis, y no requiere nada del protocolo. Todo lo demás de esta lista es secundario frente a esto.
Segundo, atar el recurso nosotros mismos. La firma no puede comprometerse con una ruta, pero el server puede rechazar un payload cuyo resource opcional no coincida con la petición en la que llegó, y puede dejar de anunciar precios idénticos entre rutas con distinto nivel de acceso. Ninguna de las dos es trabajo de protocolo. Ambas son política de middleware que podemos shippear este trimestre.
Tercero, preferir compromiso sobre autorización donde la carga es cara. Para inferencia medida, escrow es el ordenamiento correcto: reservar el techo, generar, liquidar el real. Existe en la spec y shippea para upto en SVM. Levantar una ruta medida liquidada en SVM es una referencia funcionando que podemos señalar mientras empujamos por un equivalente EVM, y el trabajo de auth-capture escrow ya le da a EVM la mayoría de las primitivas.
Cuarto, llevar el argumento aguas arriba. Dos de los tres pedidos concretos tienen forma de especificación: un compromiso con el recurso dentro del payload firmado, y un requisito normativo de que los servers reclamen un pago exactamente una vez antes de otorgar. Los autores de Free-Riding proponen el primero como extender la tupla firmada con un hash del contexto de la petición HTTP. El segundo pertenece a la sección 10.1, que hoy describe solo lo que garantiza la cadena. Ambos merecen una propuesta al working group, y el hecho de que upfront y escrow llegaran a la spec en un trimestre sugiere que la puerta está abierta.
Quinto, instrumentar el gap. Emitir el intervalo entre verify y settle, y la cuenta de settles rechazados por nonce ya consumido, como métricas de primera clase en cada ruta. Una subida en la segunda es un replay concurrente en curso, y es medible hoy. Esto encaja directo en el trabajo de las semantic conventions de GenAI que ya seguimos: el span existe, solo hay que colgarle los atributos de pago.
El punto más amplio es que la seguridad de x402 está convergiendo, y converge por medición. Tres grupos publicaron ataques reproducibles; el stack de referencia shippeó deferred delivery, aislamiento de caché y dos payment flows nuevos en los meses siguientes. Ese es un ciclo de realimentación que funciona. El problema es que el ciclo corre hoy en tiempos académicos, y los deployments que protege están facturando dinero real en Base.
Paga por llamada, en stablecoins, sobre un gateway compatible con OpenAI
345+ modelos, settlement x402 y EIP-3009, sin suscripción.
Registra tu agente