← Blog
18 de septiembre, 2026 · 10 min

Semana agentic: el paywall y el handler leían requests distintos

Un paywall solo funciona si inspecciona el mismo request que sirve el handler. Cuatro bugs mergeados esta semana dicen que, en cuatro adapters distintos de x402, no lo hacía.

Entre el 11 y el 18 de septiembre se mergearon 30 pull requests en x402-foundation/x402. El reparto de autoría es el habitual: PhilBot402 con 11, phdargen con 7, el bot de docs de mintlify con 4 — 22 del maintainer y su automatización — y 8 de cinco cuentas externas. Pero los ocho de fuera son donde vive el modo de fallo interesante.

El hilo de la semana no es una feature. Es un hueco entre lo que un sistema registra y lo que de verdad pasó: entre lo que lee el payment hook y lo que recibe el route handler, entre lo que dice una transacción de settlement y quién la inició, entre el texto de un skill y el server que lo escribió.

1. Cuatro adapters, una sola clase de bug

Los paquetes HTTP de x402 envuelven el request del framework en un adapter para que los payment hooks — onProtectedRequest, onPaymentRequired — puedan mirar el request antes de decidir si dan acceso, qué cobran o qué anuncian en el 402. El adapter es la única vista que el hook tiene del request. Esta semana entraron cuatro fixes separados, todos diciendo lo mismo: esa vista estaba mal.

El issue #3445, abierto el 10 de septiembre por sunruize93-cmyk, es el más claro. En @x402/next, llamar a await context.adapter.getBody() dentro de un hook consumía el body del POST. Si el hook luego daba acceso, el request.json() del route handler lanzaba TypeError: Body is unusable: Body has already been read. Una segunda lectura del adapter devolvía undefined. Inspeccionar el body para tomar una decisión de acceso destruía el body sobre el que estabas decidiendo. El PR #3451 (viviviviviid, mergeado el 15 de septiembre) lee desde un clone.

El PR #3454 es el de radio más amplio. En @x402/axios, cuando el transport no exponía una URL final, el fallback resolvía la URL del request con reglas WHATWG e ignoraba el join propio de Axios y sus params. Con baseURL: "http://localhost/v1", url: "/quote" y params: { city: "Seoul" }, el server veía /v1/quote?city=Seoul y el hook veía /quote. El fix hace pasar el fallback por el getUri(config) de la instancia que llama.

Dos más del mismo contribuidor cubren query parameters. PR #3455: para GET /quote?tag=a&tag=b, el c.req.queries('tag') de Hono devuelve ambos valores, pero el adapter devolvía solo 'a'. PR #3456: para ?tag=&tag=b, el adapter de Next testeaba la verdad del valor existente, así que un string vacío al principio contaba como ausente y getQueryParams() devolvía { tag: 'b' } mientras el handler conservaba ['', 'b'].

Por qué importa esta clase — ninguno es un bug de firma ni de settlement, y ninguno pierde dinero de forma directa. Son peores de manera más sutil: hacen divergir la decisión del paywall de la entrega del recurso. Un hook que tarifa /quote y un handler que sirve /v1/quote?city=Seoul no están aplicando la misma política. Cualquier regla con scope de ruta — pricing por path, tiering por parámetro, checks de acceso basados en el body — hereda la divergencia.

Dos detalles valen para quien mantiene su propio middleware. Primero, los cuatro vinieron de fuera del set de maintainers, encontrados reproduciendo contra paquetes publicados (@x402/[email protected], @x402/[email protected], @x402/[email protected]) y no contra main. Segundo, cada PR reporta el conteo de fallos previo al fix — 5 de 40 en Axios, 4 de 19 en Hono, 3 de 20 en Next — que es la disciplina que separa un test de regresión real de uno escrito después del fix. Escribimos sobre la superficie del middleware de seller en el x402 seller stack; esta es la parte más fácil de equivocar de forma sutil.

2. Llega Casper, y EIP-3009 se vuelve a clonar

El PR #2877 se mergeó el 15 de septiembre tras dos meses abierto: 67 archivos, 6.034 adiciones, 39 commits, de davidatwhiletrue. Agrega @x402/casper — implementación TypeScript del scheme exact de v2 para tokens CEP-18 de Casper usando CEP-3009 transfer_with_authorization.

Ese nombre es la noticia. CEP-3009 es el port de Casper del mismo primitivo que EIP-3009 define en EVM: una autorización firmada que produce el pagador y que envía otro. Las redes son casper:casper y casper:casper-test; el asset es un contract package hash de 32 bytes como 64 caracteres hex, sin prefijo 0x ni hash-; payTo es una address de 33 bytes como 66 hex con prefijo 00 (account hash) o 01 (package hash); y los payment requirements deben llevar extra.name y extra.version para el dominio EIP-712 de CEP-3009. El default asset de testnet es csprUSD.

El diseño del facilitator es lo que vale la pena copiar. verify() siempre hace preflight checks en vivo y, si hay una URL de RPC especulativo configurada para la red, corre speculative execution de Casper en su lugar — una simulación que cubre balance, reuso de nonce y fallos de entry-point, en vez de tres lecturas separadas de balance, authorization_state y soporte de transfer_with_authorization. El README apunta que el endpoint especulativo suele exponerse aparte, habitualmente en el puerto 7778.

Como con Cardano en el roundup de la semana pasada, el paquete está mergeado pero no publicado: @x402/casper devuelve 404 en el registry de npm al 18 de septiembre. El patrón ya es lo bastante consistente como para planificarlo — una chain está en el repo bastante antes de ser instalable. El mapa completo de lo que cada binding de chain tiene que aportar está en la auditoría de network bindings.

Dos cambios de chain más pequeños salieron el mismo día. El PR #3457 agrega Celo USDT (0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e) y USAT (0xD2ab3C9A02DBBAB236BfEC45D1d755DF4267F771) como default assets en eip155:42220, ambos de 6 decimales, con la versión del dominio EIP-712 fijada a "1" porque version() revierte en ambos contratos — verificado on-chain recomputando cada DOMAIN_SEPARATOR(), y distinguido de los adapters de fee-currency de 18 decimales de Celo que llevan los mismos nombres. La página de facilitators ahora apunta a x402.celo.org.

Y el PR #3503, mergeado el 17 de septiembre, arregla un modo de fallo que sirve de etiqueta de advertencia para cualquier binding de chain: el facilitator exact de Stellar siempre ofertaba BASE_FEE, 100 stroops, como inclusion fee, sin forma de cambiarlo. En pubnet, las transacciones Soroban con esa oferta a menudo no entran, así que settle hacía timeout mientras verify había pasado. El fix agrega una opción inclusionFeeStroops, contada contra maxTransactionFeeStroops en verify, con default 100 para que nada cambie en silencio. Una constante de fee correcta en testnet y equivocada en mainnet es un facilitator que reporta éxito en verify y nada después.

3. El timeout de MCP recibe un techo

La semana pasada el merge de Cardano llevó el cliente HTTP del facilitator de 30 a 90 segundos e hizo del maxTimeoutSeconds anunciado por el seller el temporizador de aborto del cliente MCP. Esta semana el mismo arco recibió su otra mitad.

El PR #3481 (phdargen, 15 de septiembre, 16 archivos) agrega maxRequestTimeoutSeconds, un techo propiedad del cliente sobre las esperas derivadas del accept, con default de 600 segundos, probe inicial del 402 en min(300s, cap) y clamping seguro para timers ante accepts absurdos. Un timeout por llamada sigue prevaleciendo sobre el valor derivado y no queda limitado por el techo.

El bug que arregla de paso es instructivo: en TypeScript, auto-pay pasaba las opciones del probe al retry pagado, así que las esperas pagadas quedaban clavadas en 300 segundos anunciara lo que anunciara el seller. El cliente tenía un mecanismo para honrar la ventana del seller y no lo usaba justo en el request que lo necesitaba. Go y Python recibieron helpers de timeout compartidos en el mismo PR, y los retries pagados de v1 en Go ahora toman deadline del accept como los de v2. Los ports de la derivación original a Go y Python entraron como #3442 y #3443.

Leídas juntas, las dos semanas dejan el diseño explícito: el seller propone una ventana, el buyer la acota. Un seller ya no puede retener al agente de un buyer un tiempo arbitrario anunciando un maxTimeoutSeconds grande, y un buyer ya no aborta un settlement que el seller le avisó que sería lento. Los releases siguieron el 15 de septiembre — @x402/core 2.26.0 en npm y x402 2.23.0 en PyPI. Descargas de @x402/core en los 30 días terminados el 17 de septiembre: 1.070.730.

4. La extensión Skills de MCP pasa a Final

El SEP-2640 se mergeó el 13 de septiembre, 54 commits después de abrirse el 23 de abril. Define io.modelcontextprotocol/skills: una convención para servir Agent Skills sobre MCP encima del primitivo Resources ya existente, ahora publicada como extensión oficial.

La mecánica es deliberadamente delgada. Cada archivo del directorio de un skill es un resource, por convención bajo skill://<skill-path>/<file-path>, donde el último segmento del path debe ser igual al name del frontmatter de su SKILL.md — así el nombre del skill se recupera desde la URI sin leer el archivo. Un server que declare la extensión implementa skills/list y skills/get; resources/directory/read es opcional y queda tras un setting de capability directoryRead: true. Una entry del listado es un manifiesto completo: frontmatter verbatim como JSON, más cada archivo con digest SHA-256 y size en bytes, o el string literal "dynamic" cuando no se pueden publicar digests.

La sección de seguridad es donde esto pasa de ser un problema de empaquetado a uno de infraestructura. El contenido de un skill es texto instruccional entregado a un modelo, así que el SEP obliga a los hosts a tratarlo como input no confiable, a etiquetarlo con el server de origen en el punto en que entra al contexto del modelo, y a nunca presentar un skill servido por MCP como indistinguible de uno local. El campo allowed-tools de Agent Skills debe ignorarse para skills de origen MCP salvo que el usuario haya aprobado explícitamente esa concesión — "un server remoto que rellena allowed-tools está pidiendo acceso elevado en el host, no declarando una propiedad de su propio entorno". La aprobación es content-bound: si una entry posterior anuncia un set resources distinto, la aprobación previa queda revocada. Y los digests son explícitamente "no una frontera de seguridad" — no están firmados y vienen del mismo server que el contenido.

El apéndice de features diferidas es la página más útil del documento. La distribución por archivo comprimido — tar y ZIP pre-empacados de un skill entero — se eliminó durante la revisión porque desempacar con seguridad un archivo remoto implica defenderse de decompression bombs, path traversal, symlinks que escapan del directorio, colisiones de normalización Unicode que sobrescriben SKILL.md, bits setuid y device nodes, y cada host tendría que acertar en todo. El costo es un round trip por archivo. El SEP asume ese costo a propósito. Nuestro threat model de agentes trata las instrucciones provistas por el server como superficie de inyección; esta es la primera extensión de MCP que escribe el mismo supuesto como requisito normativo.

5. TRM le pone número a quién paga de verdad

El 9 de septiembre TRM Labs publicó una medición del settlement de x402 que cubre todo lo liquidado por facilitators conocidos desde mayo de 2025: unos 52,7 millones de dólares en 198,9 millones de transacciones de settlement sobre Base, Solana y Polygon.

Dos hallazgos. El inequívoco: 52,47 de 52,68 millones, o 99,6 por ciento, se liquidaron en USDC. La economía de pagos de agentes en rails públicos es una economía de stablecoins, y la pregunta del asset está prácticamente cerrada.

El discutible: tras quitar autopagos, flujos concentrados en uno o dos pagadores y sellers con menos de diez compradores distintos — cerca de la mitad de todo el volumen liquidado — TRM acotó la porción agentic de los 25,62 millones restantes entre 0,6 y 7,5 por ciento, usando un test permisivo y uno estricto en vez de una cifra única. El screening se apoya en heurísticas de tamaño de pago, y TRM advierte que podría pasar por alto a un agente genuino comprando el mismo servicio a precio fijo de forma repetida.

La conclusión no es bajista sobre el rail. El argumento de TRM es que la plomería funciona y lo que falta es "registro preciso, reputación de contraparte que un agente pueda consultar por su cuenta, y monitoreo construido para volumen y no para valor". Está cerca de lo que encontramos midiendo los registros de ERC-8004 empíricamente: los registries existen y casi nadie está en ellos.

Qué significa para LLM4Agents

El cluster de adapters es el item con peso operativo directo. LLM4Agents es un gateway: el componente que decide si una llamada es de pago y a qué precio está separado del componente que la reenvía a un proveedor de modelos. Eso es estructuralmente el mismo split que hook y handler, y falla igual. Si el camino de metering normaliza un request distinto del camino de forwarding — un parámetro repetido descartado, un body consumido, una URL resuelta sin su base path — cobramos por una llamada y servimos otra. La mitigación no es un code review; es un test que afirme que ambos caminos ven inputs idénticos byte a byte, y una regla de parsear el request exactamente una vez y pasarlo hacia abajo como valor.

El techo de timeout de MCP cambia un default que conviene adoptar en vez de inventar. Un seller que anuncia maxTimeoutSeconds está haciendo una afirmación sobre su propia latencia de settlement, y un buyer que la honra sin cota le entregó a un desconocido el control de su presupuesto de concurrencia. Un cap de 600 segundos propiedad del cliente con un probe más corto es una forma sensata para cualquier agente que llame endpoints de pago que no escribió, y pertenece a los defaults del buyer, no a la configuración por integración.

La extensión Skills es una oportunidad con filo. Servir un skill al lado de las tools que describe es exactamente lo que debería hacer una superficie MCP de pago: un gateway puede entregar las instrucciones de uso de su propio routing, fallback y semántica de billing como artefacto versionado y direccionado por digest, en vez de un README que ningún modelo lee. El filo es que el mismo mecanismo permite a cualquier server de pago por el que ruteemos empujar instrucciones al agente de un cliente. Ambos lados son nuestros: publicar skills con digests estables, y tratar los skills que llegan de servers upstream como texto no confiable con origen visible.

El número de TRM es el incómodo, y es útil justamente por eso. Si entre 0,6 y 7,5 por ciento del comercio x402 filtrado es plausiblemente agentic, entonces el volumen de settlement es un mal proxy de demanda y uno peor de product-market fit. La medición que nos importa no son dólares movidos sino agentes distintos que pagaron más de una vez, un número que podemos calcular sobre nuestro propio tráfico y que nadie más puede calcular por nosotros.

Cómo mantenerse en la frontera

Cinco cosas, en orden.

Primero, escribir el test de divergencia. Un test por ruta protegida que emita un request real con un query parameter repetido, un parámetro vacío al principio, un base path y un body JSON, y que afirme que la capa de metering y la de forwarding observaron valores idénticos. Es un día de trabajo y cierra la clase entera de bug que arriba costó cuatro PRs.

Segundo, adoptar la forma del timeout de buyer. Derivar las esperas del maxTimeoutSeconds del seller, acotarlas con un techo propiedad del cliente en el rango de 600 segundos, hacer el probe en no más de 300, y hacer explícito el override por llamada. Después revisar el camino de código que efectivamente paga, no solo el que sondea — ahí estaba el bug de arriba.

Tercero, publicar un skill de LLM4Agents sobre MCP. Nuestro server MCP ya expone tools; una entry skill:// con digests completos de resources convierte "acá hay una tool" en "acá está cómo elegir modelo, manejar un 402 y reconciliar un settlement". Empezar por la forma con digest, no por "dynamic", para que los hosts puedan atar la aprobación al contenido.

Cuarto, endurecer el camino de skills entrantes. Si el gateway alguna vez consume servers MCP por cuenta de un cliente, aplicar las reglas del SEP ahora y no tras el primer incidente: origen visible para el modelo, allowed-tools ignorado salvo aprobación explícita, lecturas atadas al server de origen, nombres resueltos por origen.

Quinto, instrumentar la métrica que TRM dice que falta. Agentes pagadores distintos, tasa de repetición y pagos por agente por día — publicados para nuestro propio tráfico. En un mercado donde nadie puede distinguir on-chain un agente de un cron, un operador que pueda probar la diferencia off-chain tiene algo que ningún block explorer provee.

Pagá por llamada, en stablecoins, sobre una API compatible con OpenAI

Registrá un agente, fondealo y empezá a rutear. Sin crédito prepago, sin mínimo mensual.

Registrar un agente