Extensiones de x402: auditoría de la capa de plugins
La spec core de x402 que todos leen tiene 4,252 palabras. Las specs de extensiones que casi nadie lee suman 14,820 — tres veces y media más texto. Ahí es donde viven realmente la identidad, los recibos firmados, la idempotencia, la atribución y la UX de gas. Auditamos las nueve.
Esta es otra auditoría de fuente primaria de la serie. Clonamos x402-foundation/x402 el 2026-08-11 en el HEAD 1d150626 (commit del 2026-08-10) y leímos cada archivo bajo specs/extensions/, la sección del framework de extensiones de la spec v2, el historial git de cada documento y los tres árboles de SDK que los implementan. Todo lo que sigue está verificado contra ese clon. Entregas anteriores cubrieron el API del facilitator, los quince network bindings y la extensión de discovery Bazaar. Esta cubre la capa que los une.
El framework: un envelope, dos reglas de echo
La spec v2 define extensions como un mapa clave-valor en tres objetos: el challenge PaymentRequired, el PaymentPayload del cliente y el SettlementResponse. Cada valor tiene un envelope obligatorio: un objeto info con los datos de la extensión y un objeto schema — un JSON Schema Draft 2020-12 que describe cómo debe verse info. Ambos campos figuran como Required en la tabla del framework.
Dos reglas de comportamiento hacen el trabajo real. Los servidores anuncian extensiones en el challenge 402; los clientes las devuelven en eco dentro del payload de pago. Y el eco está acotado: el cliente debe incluir al menos el info que recibió, puede agregar campos adicionales, pero no puede borrar ni sobrescribir los existentes. Esa única frase es todo el modelo de confianza de la capa — los términos declarados por el servidor sobreviven intactos el viaje de ida y vuelta, y todo lo que agrega el cliente es visiblemente aditivo. Los facilitators declaran qué extensiones entienden en el array extensions de GET /supported, la misma superficie de discovery que mapeamos en el deep dive del facilitator.
El censo: nueve specs, dieciocho meses de acumulación
El directorio specs/extensions/ contiene nueve documentos. El historial git le da a cada uno una fecha de nacimiento:
eip2612GasSponsoringyerc20ApprovalGasSponsoring— 2026-01-08, agregados en el mismo PR (#769) que trajo el soporte de Permit2 al scheme exact EVM.bazaar— 2026-01-15 (#956), la capa de discovery que cubrimos en julio.sign-in-with-x— 2026-02-02 (#921), autenticación con wallet.payment-identifier— 2026-02-05 (#1053), claves de idempotencia.offer-receipt— 2026-03-11 (#935), ofertas y recibos firmados por el servidor.http-message-signatures— 2026-04-15, agregado dentro del PR #1145, el mismo merge que trajo el scheme batch-settlement y el network binding de Cloudflare.auth-hints— 2026-04-24 (#1902), discovery de autenticación por entrada.builder-code— 2026-05-04 (#2050), atribución on-chain vía ERC-8021.
Los conteos de palabras van de 454 (payment-identifier) a 4,515 (offer-receipt). Se agrupan en tres clusters funcionales: identidad, prueba y operaciones.
Cluster uno: quién está pagando
Tres extensiones manejan identidad, cada una en una capa distinta del stack.
sign-in-with-x trae la autenticación de wallet de CAIP-122 al flujo del 402. El servidor incrusta un challenge en la extensión — dominio, URI, un nonce de 32 caracteres hex, issuedAt, un expiry opcional de cinco minutos — más un array supportedChains que empareja chain IDs CAIP-2 con tipos de firma: eip191 para EVM (formato de mensaje EIP-4361, con hints eip1271 y eip6492 para wallets smart y counterfactual) y ed25519 para Solana (formato Sign-In With Solana). El cliente firma y envía la prueba en un header SIGN-IN-WITH-X como JSON en base64. El punto es económico, no cosmético: un servidor que reconoce una wallet que vuelve puede saltarse el pago por completo para una dirección que ya pagó. Pagar una vez, ser reconocido después. La spec incluye quince códigos de error legibles por máquina (invalid_siwx_domain_mismatch, invalid_siwx_nonce, …) y una advertencia inusualmente explícita: validar domain contra el origin público configurado, nunca contra el header Host que controla el que llama.
auth-hints resuelve un problema más estrecho: cuando solo algunas entradas de accepts[] requieren autenticación, el cliente no debería descubrirlo fallando. La extensión mapea acceptIndexes a métodos de autenticación — oauth2 con token endpoint, registro dinámico RFC 7591 opcional y un tokenType Bearer o DPoP (RFC 9449), o sign-in-with-x como puntero a la extensión hermana. Las credenciales viajan luego como headers HTTP ordinarios junto a PAYMENT-SIGNATURE; el facilitator nunca las ve. La identidad de autenticación y la dirección del pagador son explícitamente independientes. Es la misma costura entre auth y pago que trazamos en el deep dive de autorización de MCP, comprimida en una sola respuesta 402.
http-message-signatures es la más delgada de las nueve: un puntero a territorio RFC 9421. Una red anuncia un registrationUrl, sus signatureSchemes aceptados (ed25519 para el único despliegue con nombre) y tags de firma como web-bot-auth. El cliente hospeda sus llaves en /.well-known/http-message-signatures-directory y firma los requests. La spec nombra exactamente una red de ejemplo: el cloudflare:402 de Cloudflare. También esboza la dirección inversa — servidores firmando respuestas sobre @status más el header PAYMENT-REQUIRED o PAYMENT-RESPONSE, atadas al request con flags ;req — lo que le permitiría a un cliente probar los términos que le mostraron. Es Web Bot Auth formalmente acoplado al envelope de extensiones de x402.
Cluster dos: probar que ocurrió
offer-receipt es el peso pesado, y el documento estratégicamente más interesante del directorio. Define dos artefactos firmados. Una oferta firmada es el compromiso criptográfico del servidor con los términos de una entrada de accepts[] — URL del recurso, scheme, red, asset, payTo, monto, validUntil opcional. Un recibo firmado, devuelto solo en caso de éxito, declara que un pagador dado pagó por un recurso dado en un momento dado, con un hash de transacción opcional. Ambos vienen en dos formatos: EIP-712 (con un chainId: 1 deliberadamente hardcodeado, porque la firma es un artefacto off-chain y la red real es un campo del payload) o JWS en serialización compacta con un kid que es una DID URL.
La sección más filosa se agregó el 2026-07-23 en el PR #2811: autorización del firmante. Una firma válida prueba que una llave firmó el artefacto — no que la llave tuviera derecho alguno a hablar por el servicio. Sin ese chequeo, cualquiera puede acuñar un keypair y firmar "recibos" por cualquier resourceUrl de internet. La spec ahora lista cuatro mecanismos de autorización: firmar con la llave de payTo, un documento did:web en /.well-known/did.json, un registro DNS TXT en _controllers.<dominio>, o un registry externo, con la nota de que las fuentes mutables deben evaluarse al momento del issuedAt del recibo, no al de la verificación.
Por qué importa: cuando auditamos el registro de reputación de ERC-8004, la conclusión fue que el feedback sin anclaje de settlement es combustible Sybil, y que la prueba de pago era el primitivo faltante. offer-receipt es ese primitivo, especificado — evidencia portable y verificable offline de que una interacción comercial ocurrió. La salvedad es igual de explícita: la spec declara su propia forma de wire "not considered stable" y solo las reglas de comportamiento como normativas. Es un draft que sabe que es un draft.
Cluster tres: operaciones
payment-identifier son 454 palabras que a los operadores de gateways les importan más que a nadie: un id generado por el cliente (16–128 caracteres, se recomienda UUID con prefijo pay_) que servidores y facilitators pueden usar como clave de idempotencia. Mismo id y mismo payload: devolver la respuesta cacheada. Mismo id, payload distinto: 409 Conflict. La spec indica atar cada id a un fingerprint normalizado de la operación pagada — scheme, red, asset, monto, payTo, ruta — antes de honrar un cache hit. Lo que sigue sin contener es un solo MUST, SHOULD o ventana de retención; señalamos ese hueco cuando AWS citó una ventana de idempotencia de "15 minutos" que esta spec no define en ningún lado. Sin cambios en este clon.
builder-code le da atribución on-chain a los pagos x402. Implementa el Schema 2 de ERC-8021: un mapa CBOR anexado al calldata del settlement detrás de un marcador de 16 bytes, que lleva a (la app que expuso el endpoint), w (el facilitator que liquidó, agregado al momento del settlement) y s (códigos de servicio del camino del cliente — un middleware MCP puede listar varios). Cada parte tiene una reserva no solapada: cinco códigos de cliente, cinco de servidor, uno de facilitator, once en total. Los códigos siguen el patrón ^[a-z0-9_]{1,32}$ y se resuelven vía registries; Base opera el primer code registry de ERC-8021 y reparte códigos gratis. La atribución es la materia prima de la analítica y, eventualmente, del revenue share — que es presumiblemente por qué es la extensión más parchada del repo.
El par de gas sponsoring ataca el problema de arranque en frío del settlement basado en Permit2: una wallet nueva que solo tiene USDC no puede pagar el gas del approve(Permit2) único. eip2612GasSponsoring deja que el cliente firme un permit EIP-2612 que el facilitator envía y paga; erc20ApprovalGasSponsoring cubre tokens sin EIP-2612 — el facilitator fondea la wallet con gas, difunde la aprobación firmada del cliente y liquida, en un batch atómico para evitar que el gas fondeado sea front-runeado. Vimos ambos en vivo en la auditoría del facilitator x402-rs.
El mapa desigual: 9 specs, 7 en TypeScript, 4 en Python, 4 en Go
Las specs son promesas; los paquetes son hechos. El paquete TypeScript @x402/extensions (versión 2.21.0, bump del 2026-08-04 en el PR #3041) implementa siete de las nueve: bazaar, builder-code, los dos flujos de gas sponsoring, offer-receipt, payment-identifier y sign-in-with-x. Python implementa cuatro: bazaar, builder_code, payment_identifier, sign_in_with_x. Go implementa las mismas cuatro. Dos extensiones tienen cero implementación de SDK en todo el repo: auth-hints y http-message-signatures — la segunda vive en el edge de Cloudflare y no en ningún SDK de x402, y la primera es una spec esperando código.
La consecuencia práctica: un seller en Python o Go hoy no puede emitir recibos firmados ni patrocinar gas con los SDK oficiales, y ningún cliente en ninguna parte puede actuar sobre auth-hints sin implementarlo a mano. La paridad multi-lenguaje, que los schemes en general lograron, no llegó a la capa de extensiones.
Dónde están aterrizando los parches de seguridad
Leyendo el último mes de commits salta un patrón: el protocolo core está quieto; la capa de extensiones es donde ocurre el trabajo de seguridad. El 2026-07-15, el PR #2859 ató la validación de dominio de SIWX a un origin configurado en vez de a headers del request — cerrando un spoof de header Host. El 2026-07-23, el PR #2933 corrigió la verificación Ed25519 de SIWX en Solana para rechazar puntos de orden pequeño. El mismo día, el PR #2811 agregó la sección de autorización del firmante a offer-receipt. Builder-code necesitó tres PRs correctivos en dos semanas (#2912, #2994, #3027) para limitar, adjuntar siempre y mergear correctamente los códigos de servicio del cliente con los arrays del servidor en los tres SDK. Y el 2026-08-04, el PR #3039 parchó un server-side request forgery en Bazaar: un seller malicioso podía plantar URLs externas en $ref/$id dentro del JSON Schema que publica, y un facilitator que validara ese schema no confiable las iría a buscar. Los facilitators ahora deben rechazar cualquier referencia que no sea un fragmento.
Nada de esto es exótico. Son las clases de bug estándar de todo sistema de plugins — input en el que se confía porque llegó dentro de un envelope familiar. El envelope es nuevo; los bugs no.
Tres hallazgos
Como en cada auditoría de esta serie, reportamos lo que pudimos verificar contra el clon, con ubicaciones.
http-message-signatures.md contradice su propio schema. La sección Fields marca tags como "(required)", pero los dos bloques de JSON Schema del mismo documento listan solo registrationUrl y signatureSchemes en el array required. Un implementador que valide con el schema acepta lo que la prosa prohíbe.
info y schema en cada valor de extensión, pero los ejemplos de PaymentPayload de builder_code.md envían un {"builder-code": {"a": "my_app", "s": "my_client"}} plano sin ninguno de los dos campos del envelope, y los pasos del facilitator en la spec leen extensions["builder-code"].a directo. El SDK TypeScript publicado hace lo contrario: el cliente escribe { info: { s: [...] } } y el facilitator lee .info. El código y la spec no pueden tener razón a la vez; hoy la verdad del wire es la del SDK.
auth-hints referencia un scheme que no existe. Su ejemplo motivador y su prosa usan "scheme": "deferred", pero el scheme se mergeó como batch-settlement en el PR #1145 el 2026-04-15 — nueve días antes de que auth-hints se agregara el 2026-04-24. La spec nació desactualizada y sigue así en HEAD: specs/schemes/ contiene exact, upto, batch-settlement y auth-capture. Ningún deferred.
Dos observaciones menores. Los identificadores de extensión mezclan convenciones — siete claves en kebab-case contra dos en camelCase (eip2612GasSponsoring, erc20ApprovalGasSponsoring), con nombres de archivo en un tercer estilo. Y ambas specs de gas sponsoring incrustan comentarios // dentro de ejemplos JSON, lo que hace que cada uno de esos bloques sea JSON inválido para quien los copie. También merece una línea: ese directorio auth-capture es un cuarto scheme de pago — authorize, capture, void y refund sobre escrow, con cliente TypeScript desde el PR #2486 (2026-05-29) — que no cerró la trilogía de nadie. Merece su propia auditoría.
Qué significa para LLM4Agents
La capa de extensiones es donde x402 deja de ser un formato de wire de pagos y empieza a ser un stack de comercio, y casi cada pieza mapea a algo que nuestro gateway ya hace o debería exponer. payment-identifier es la idempotencia que nuestro pipeline de facturación reserve-then-settle necesita en el borde del protocolo — el binding natural es el id de reserva que ya acuñamos. offer-receipt es la versión portable del rastro de settlement que guardamos internamente: recibos firmados emitidos al momento del settle son exactamente el anclaje de prueba de pago que la auditoría de ERC-8004 concluyó que les falta a los sistemas de reputación, y un gateway que los emite convierte cada llamada de inferencia pagada en un evento de reputación. sign-in-with-x le da memoria a los compradores walk-up de x402: pagar una vez, ser reconocido, saltarse el segundo 402. Y builder-code es el riel de atribución que un marketplace de modelos necesita el día que el revenue share se vuelva real.
El lado del riesgo es igual de concreto. Nueve extensiones con tres niveles de cobertura de SDK son una matriz de interoperabilidad, y las matrices se pudren por los bordes — un cliente que asuma offer-receipt en todas partes se encontrará con sellers en Python que no pueden firmar uno. Y el hilo de parches muestra que las extensiones son superficie de ataque: schemas que se van a buscar, headers spoofeados, casos borde de curvas. Un gateway que normaliza el manejo de extensiones para sus agentes — validando envelopes, pinneando schemas, rechazando referencias externas — absorbe ese riesgo una vez en lugar de dejar que cada agente lo absorba por separado.
Cómo mantenerse en la frontera
Pasos concretos, en orden. Primero, implementar payment-identifier de punta a punta en el camino de facturación: aceptar ids del cliente, atarlos a fingerprints de reserva, devolver respuestas de settlement cacheadas en el retry, 409 ante mismatch — y documentar una ventana de retención, porque la spec no lo hará. Segundo, emitir recibos offer-receipt desde nuestro paso de settle: un recibo EIP-712 firmado con una llave dedicada, autorizada vía did:web en /.well-known/did.json, para que cualquier agente lleve prueba portable de que nos pagó. Tercero, aceptar sign-in-with-x en los endpoints walk-up para que los compradores que vuelven se salten el re-pago donde la política lo permita; los quince códigos de error lo hacen implementable sin adivinar. Cuarto, registrar builder codes y adjuntar atribución s en los settlements mediados por el gateway — gratis hoy vía el registry de Base, valioso el día que la analítica o los rebates dependan de eso. Quinto, subir los tres hallazgos como issues; el fix del SSRF muestra que este repo responde. Sexto, vigilar auth-capture: authorize-capture-void-refund sobre escrow es la forma que falta para facturación de inferencia reembolsable, y ya está en specs/schemes/ con cliente TypeScript.
Pagos con recibos, identidad e idempotencia incorporados
LLM4Agents mide 345+ modelos detrás de un gateway OpenAI-compatible, liquidado por llamada en stablecoins sobre x402.
Registra tu agente