← Blog
18 de octubre, 2026 · 16 min

El precio va en un header: ¿puede un navegador pagar en x402?

Le pedimos el precio a 2.233 sellers x402 como lo haría una página web. 568 dejarían que un cliente de navegador correcto lea el precio y envíe el pago. El cliente que hoy publica el SDK de x402 pasa en 120. La diferencia sale de un header que el cliente agrega a su propio reintento.

x402 v2 pone todo el protocolo en headers HTTP. El precio viaja en PAYMENT-REQUIRED, el pago en PAYMENT-SIGNATURE y el resultado del settlement en PAYMENT-RESPONSE. La spec del transporte lo dice sin rodeos: "All x402 protocol information is communicated through headers."

Casi siempre es un diseño limpio. Un agente del lado del servidor que llama a una API paga desde Node o Python nunca lo nota. Un navegador sí. El navegador decide qué headers puede leer una página y cuáles puede enviar. La mayoría de los clientes HTTP también limita el tamaño de un header. Y HTTP tiene métodos, como HEAD y OPTIONS, que un middleware de pago tiene que manejar a propósito.

Entre el 7 y el 15 de octubre, un grupo de issues y pull requests en el repositorio de x402 mapeó esos bordes. Este post los lee, reproduce cada problema contra el SDK publicado y mide qué tan seguido aparecen en el Bazaar.

Qué leímos y qué corrimos, todo el 2026-10-18. El repositorio de x402 en el commit faa1c69: los issues #3722, #3803, #3808, #3810 y #3657, el fix mergeado #3796 y los PRs abiertos #3478, #3738, #3741, #3774, #3794, #3805, #3809 y #3813. Los paquetes @x402/* en 2.28.0, el release actual en npm.

Corrimos cuatro sondas. La primera es un harness local: Express 4.22.3 con @x402/express 2.28.0 detrás de cuatro configuraciones de CORS, un facilitator simulado y una página en un segundo origen manejada por Chrome 144 headless. La segunda es una búsqueda del valor de PAYMENT-REQUIRED más grande que aceptan el fetch de Node 22, aiohttp, requests, httpx y Chrome. La tercera es un censo del Bazaar de CDP: 42.058 recursos en 2.447 hosts, con tres requests sin pagar por host (el request anunciado, un preflight CORS y un HEAD), más una segunda pasada de preflight. La cuarta corre las mismas sondas contra nuestro propio gateway.

La versión corta. Ningún middleware de servidor x402 v2 en TypeScript, Python o Go pone un header CORS, y la spec nunca menciona navegadores. De los 875 sellers del Bazaar cuyo 402 pasa el chequeo de origen del navegador, 240 le esconden el precio a la página. El @x402/fetch publicado agrega Access-Control-Expose-Headers a su reintento pago. Ese es un header de respuesta, así que enviarlo en un request fuerza un preflight que 448 de 568 sellers, que de otro modo se podrían pagar, rechazan. 126 sellers responden al preflight mismo con un 402. En el SDK publicado, un HEAD sin pagar ejecuta un handler GET pago. El fix está mergeado pero no salió. Y 188 sellers mandan un header de precio que aiohttp no puede leer. Nuestro propio gateway pasa el preflight y después también le esconde el precio a la página.

Tres headers, dos direcciones

El estándar Fetch (actualizado por última vez el 6 de octubre de 2026) fija las reglas. Por defecto, una página de otro origen solo puede leer siete headers de respuesta: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified y Pragma. Cualquier otro header queda oculto, salvo que la respuesta lo nombre en Access-Control-Expose-Headers.

Así que PAYMENT-REQUIRED y PAYMENT-RESPONSE son invisibles para una página por defecto. La respuesta igual llega. fetch() resuelve con status 402. headers.get("PAYMENT-REQUIRED") devuelve null.

El lado del request tiene su propia lista. PAYMENT-SIGNATURE no está en ella, y tampoco Content-Type: application/json. Solo los MIME types de formulario y texto plano están en la safelist. Antes de que una página pueda enviar cualquiera de los dos a otro origen, el navegador manda un preflight OPTIONS. El preflight tiene que volver con un ok status, que el estándar define como 200 a 299. Además tiene que nombrar cada header pedido en Access-Control-Allow-Headers.

Las dos listas aceptan *, pero solo en requests sin credenciales. En un request con credenciales, como cookies, * significa un header que se llama literalmente *. Y Authorization nunca queda cubierto por *.

La spec del transporte HTTP de x402 tiene 192 líneas. En faa1c69, una búsqueda sin distinguir mayúsculas de cors, cross-origin, browser o Access-Control no encuentra nada. El issue #3808 lo señala. El PR #3809 propone una subsección "Browser clients" que cubre las dos direcciones en un solo párrafo. El argumento es que una expose list faltante y una allow list faltante se ven igual desde el cliente. Las dos aparecen como un error de parseo sobre un challenge aparentemente vacío.

Cuatro configuraciones en un navegador real

Empezamos por el lado del servidor, que es donde arranca el issue #3722. En los paquetes 2.28.0, @x402/express y @x402/core no contienen el string Access-Control en ninguna parte de su código compilado. El middleware escribe PAYMENT-REQUIRED en el 402 y PAYMENT-RESPONSE en la respuesta liquidada. No escribe ningún header CORS. El issue reporta lo mismo para Hono, Next, Fastify, FastAPI, Flask, Gin, Echo y net/http.

Después armamos cuatro servidores Express en 127.0.0.1, cada uno con la misma ruta paga GET /paid-get a $0.01 en Base Sepolia. Solo cambiaba su capa CORS. Una página en http://localhost:4100 llamó a cada uno dos veces: una con fetch() simple para leer el precio y otra con @x402/fetch 2.28.0 y una clave descartable, contra un facilitator simulado. No se hizo ningún pago real.

// Chrome 144, página en http://localhost:4100, @x402/express 2.28.0
configuración                 fetch simple          @x402/fetch 2.28.0
1 solo x402                   Failed to fetch       Failed to fetch
2 cors() por defecto          402, precio = null    "Failed to parse payment
                                                     requirements"
3 CORS mínimo (#3808)         402, precio legible   Failed to fetch
4 config. 3 + allow           402, precio legible   200, pagado,
  Access-Control-Expose-Headers                     PAYMENT-RESPONSE legible

La primera fila no tiene CORS. El navegador bloquea la respuesta, así que la página ve un error de red.

La segunda fila es el paquete cors sin opciones. Sus defaults mandan Access-Control-Allow-Origin: * y no exponen nada, como dice su README: "If not specified, no custom headers are exposed." La página recibe un 402 que puede ver pero no puede cotizar. Los únicos headers visibles eran cache-control, content-length y content-type. @x402/fetch reporta entonces "Invalid payment required response", que no apunta ni cerca de CORS.

La tercera fila es la configuración mínima que recomienda #3808. Expone los dos headers de pago y permite Content-Type y PAYMENT-SIGNATURE. El precio ya es legible. Pero el reintento pago igual falla, y esta vez la causa es el cliente.

El cliente agrega un header que el servidor debe permitir

El build 2.28.0 de @x402/fetch pone esto en el reintento que lleva el pago, tanto en el primer reintento como en el camino de recuperación:

clonedRequest.headers.set(
  "Access-Control-Expose-Headers",
  "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
);

Access-Control-Expose-Headers es un header de respuesta. En un request no le dice nada al servidor. Pero no está en la safelist de request, así que el navegador tiene que pedir permiso para enviarlo. Nuestro servidor registró el preflight tal como lo mandó Chrome:

OPTIONS /paid-get
Access-Control-Request-Headers: access-control-expose-headers,payment-signature

El servidor mínimo permite payment-signature pero no el otro nombre, así que Chrome se niega a enviar el request pago. Agrega Access-Control-Expose-Headers a la allow list (cuarta fila) y el mismo cliente paga, recibe un 200 y puede leer PAYMENT-RESPONSE.

Este bug no es nuevo. El PR #724 propuso quitar la misma línea de los clientes v1 en diciembre de 2025. Se cerró sin mergear cuando los paquetes legacy quedaron congelados. El PR #3478 la quita de los wrappers v2 de Fetch y Axios. Está abierto desde el 15 de septiembre. El PR #3717 hace lo mismo para el cliente Python, donde el header es inerte porque no hay navegador de por medio.

Del lado del servidor, el PR #3774 (TypeScript) y el PR #3794 (Python y Go) hacen que toda respuesta que lleve un header de pago también exponga los dos headers de pago. Los dos combinan con cualquier expose list ya presente en lugar de reemplazarla, y ninguno toca Access-Control-Allow-Origin. #3794 además documenta una trampa de Flask: el middleware de pago reemplaza app.wsgi_app, así que el 402 sale antes de que corra flask-cors y no lleva ningún header CORS.

El preflight que recibe un 402

Una ruta se puede declarar con método ("GET /paid") o sin él ("/paid"). Una ruta sin método compila al verbo *. El matcher de 2.28.0 es una línea:

(route) => route.regex.test(candidate) && (route.verb === "*" || route.verb === upperMethod)

* también matchea OPTIONS, así que el middleware de pago responde el preflight del navegador con un 402. Un 402 no es un ok status, así que el preflight falla y el request pago nunca se envía. En nuestro harness sin capa CORS, OPTIONS /paid-any devolvió 402. Con un middleware CORS registrado primero devolvió 204, porque la capa CORS cierra el preflight antes de que x402 lo vea. La ruta declarada como GET dejó pasar OPTIONS hacia la app.

El PR #3738 acota el wildcard implícito en los tres cores para que deje de matchear OPTIONS. Una ruta declarada explícitamente como "OPTIONS /path" sigue exigiendo pago. Los paquetes legacy v1 conservan la regla vieja, porque están congelados.

HEAD: primero gratis, después pagado por nada

El RFC 9110 dice que HEAD "is identical to GET except that the server MUST NOT send content in the response." Express manda HEAD al handler de GET y descarta el body.

En 2.28.0, un request HEAD no matchea una ruta declarada como "GET /paid". El middleware lo deja pasar y el handler pago se ejecuta. En nuestro harness, un HEAD /paid-get sin pagar devolvió 200 OK, y el contador de llamadas del handler pasó de 0 a 1. El body se descartó, como exige HTTP. Pero el trabajo detrás se hizo, y cualquier header que ponga el handler igual sale. Si el handler llama a un upstream pago, como un modelo, el seller paga esa llamada y el que llama no paga nada.

El PR #3796 lo corrige en TypeScript, Python, Go y el paquete legacy. Lo abrió y lo mergeó el mismo maintainer el 13 de octubre, con 27 minutos de diferencia. Su changeset dice: "Require payment for HEAD requests when the matching route is configured for GET." Ninguno de los tres SDKs publicó un release desde el 29 de septiembre. Hoy las últimas versiones son @x402/core 2.28.0, x402 2.25.0 en PyPI y Go v2.28.0, así que toda instalación nueva de los paquetes publicados sigue teniendo el bypass.

El fix abre un segundo problema. El issue #3810 nota que, una vez que HEAD matchea una ruta paga, nada impide que un HEAD que lleva un pago haga settlement. Al comprador se le cobra, y la respuesta no tiene body por definición. Esto puede pasar en la práctica: según el issue #3657, el cliente Python de referencia reintenta el request preparado con el pago adjunto. Un cliente que descubrió el precio con HEAD después paga con HEAD. El PR #3813 mantiene el challenge en HEAD pero rechaza un HEAD pago antes de la verificación, con un error que nombra GET. El PR #3741 va un paso más allá. Agrega un method opcional a ResourceInfo, dentro del challenge que todo cliente ya parsea, para que el seller pueda decir qué método es el que paga.

¿Qué tan grande puede ser un precio?

Nada en la spec acota el tamaño de PAYMENT-REQUIRED. Lleva cada entrada de accepts y cada extensión, incluidos los ejemplos de input y output del Bazaar y sus JSON schemas. Los clientes sí tienen límites. Buscamos el valor más grande que acepta cada uno, en una respuesta con pocos headers adicionales y chicos:

cliente                valor máximo de PAYMENT-REQUIRED   primer fallo
Node 22.22.2 fetch     16.170 bytes                       UND_ERR_HEADERS_OVERFLOW
aiohttp 3.11.13         8.190 bytes                       400 "Got more than 8190 bytes"
requests 2.34.2        65.516 bytes                       ConnectionError
httpx 0.28.1          148.188 bytes                       RemoteProtocolError
Chrome 144            261.866 bytes                       TypeError: Failed to fetch

La cifra de Node es para todo el bloque de headers. Su default documentado es 16 KiB, subido desde 8 KiB en la v13.13.0. Los otros headers de nuestra respuesta ocuparon el resto. El límite de aiohttp es por header: max_field_size=8190 por defecto. En otras palabras, el navegador no es la restricción ajustada. Los runtimes habituales de agentes del lado del servidor sí lo son.

El issue #3803 encontró sellers en vivo por encima de los dos límites el 14 de octubre. En un caso, la extensión Bazaar pesaba unos 16 KB y accepts 0,25 KB. El PR #3805 hace que el servidor TypeScript avise una vez por ruta por encima de 8.190 bytes, y otra vez por encima de 16 KB. El PR además agrega el presupuesto de 8.190 bytes al texto de la spec. El header en sí no cambia.

El Bazaar visto desde un navegador

Paginamos el catálogo completo de discovery de CDP el 18 de octubre: 42.058 recursos en 2.447 hosts. Por host tomamos su recurso más llamado sin path con plantilla, lo que dejó 2.373 hosts. Cada uno recibió tres requests sin pagar desde Origin: https://llm4agents.com. Primero el método anunciado con el input de ejemplo del Bazaar. Después el preflight que dispararía @x402/fetch 2.28.0 y, para recursos GET, un HEAD. Una segunda pasada mandó el preflight que mandaría un cliente correcto, sin el header extra. Contamos * como válido en cada lista, porque un fetch() por defecto a otro origen no manda credenciales.

2.285 hosts respondieron 402, y 2.233 de ellos traían un header v2 PAYMENT-REQUIRED. Esos 2.233 son la base de todo lo que sigue.

// Censo, 2026-10-18, 2.233 sellers v2

De visible a pagable

Sin CORS: 1.353 (60,6%). Una página no puede ver la respuesta.

Origen permitido: 875 (39,2%), de los cuales 819 usan * y 56 reflejan el origen. Otros 5 tienen fijo un origen distinto.

Precio legible: 635 (28,4%). Los otros 240 que permiten el origen esconden PAYMENT-REQUIRED: 213 no mandan ninguna expose list, y 13 exponen el v1 X-PAYMENT-RESPONSE pero no el header v2.

Pagables por un cliente de navegador correcto: 568 (25,4%). Entre los legibles, 39 permiten el preflight pero no PAYMENT-SIGNATURE; 32 de ellos permiten en cambio el X-PAYMENT de v1.

Pagables por @x402/fetch 2.28.0: 120 (5,4%).

El salto de 568 a 120 es el bug del cliente. 448 sellers que un cliente correcto puede pagar igual rechazarían el wrapper publicado. La allow list más común entre ellos, en 144 hosts, es la de manual: Content-Type, PAYMENT-SIGNATURE y X-PAYMENT, en algún orden y capitalización. Seis de los 448 ni siquiera responden el preflight con un ok status cuando se pide el header extra.

Los otros bordes también aparecen. 126 sellers responden el preflight con 402. En los 1.118 sellers que anuncian GET, un HEAD sin pagar recibió un 402 en 584 y un 405 en 220. Recibió un 2xx en 242 (21,6%), lo que significa que ahí el gate de pago no cubre HEAD. Desde afuera no podemos saber si el handler pago se ejecutó. Tamaños de header: la mediana del valor PAYMENT-REQUIRED es 3.256 bytes, el percentil 99 es 11.996 y el más grande 22.492. 188 sellers (8,4%) superan los 8.190 bytes de aiohttp. Ocho de ocho muestreados fallaron en vivo con aiohttp. Dos sellers superan los 16 KiB de Node, y los dos fallaron en vivo con el fetch de Node.

Estos números son una foto, un recurso por host, desde un solo origen. El escaneo del issue #3722 usó otras herramientas y encontró la misma forma: de 1.359 endpoints con un 402 legible, 549 mandaban Access-Control-Allow-Origin y 192 de esos no exponían el precio. No pudimos probar la respuesta paga sin pagar. Las cifras asumen que un seller manda los mismos headers CORS en su 200 que en su 402.

CORS no es un control de gasto

Una parte de #3808 importa más allá de la lista de bugs. Un seller puede elegir no servir a orígenes de navegador, y es una elección válida. Pero CORS gobierna si una página puede leer una respuesta. No gobierna si alguien gasta. Un pago exact es una autorización EIP-3009 firmada con la clave del pagador. Una página que puede leer un challenge igual no puede firmar. Decide quien tenga la clave.

Rechazar orígenes de navegador protege al seller de recibir un pago que no esperaba. No protege al pagador, porque el mismo cliente que firma automáticamente le va a pagar a cualquier seller que sí permita el origen. El control real vive en la política del firmante. Es la capa que auditamos en el default de $1, y sigue siendo el lugar correcto para los límites cuando los agentes corren dentro de páginas, como con WebMCP.

Qué significa para LLM4Agents

Corrimos las mismas sondas contra nuestro gateway. El preflight a POST /v1/chat/completions devuelve 204 con Access-Control-Allow-Origin: *. Refleja los headers que pida el navegador, con Vary: Access-Control-Request-Headers. Eso significa que hasta el header extra del cliente 2.28.0 pasa. Nuestro challenge es chico: el valor PAYMENT-REQUIRED pesa 552 bytes, muy por debajo de cualquier límite de la tabla.

Después el 402 vuelve con Access-Control-Allow-Origin: * y sin Access-Control-Expose-Headers. Una página de otro origen ve el 402 pero no puede leer nuestro precio. Nada en nuestro código pone una expose list, así que después de un pago la página tampoco podría leer PAYMENT-RESPONSE. Nuestra app Hono registra cors() con sus defaults, y el default documentado de Hono para exposeHeaders es una lista vacía. En los términos del censo, somos uno de los 240.

El impacto es acotado, y es una brecha de compatibilidad, no de pagos. Los agentes en Node, Python o Go nunca ven CORS, y el camino Bearer no se ve afectado. Nuestras rutas pagas son POST, así que ninguno de los dos problemas de HEAD las alcanza. Lo que bloquea es un cliente al que el pago por llamada en stablecoins le queda muy bien: un agente que corre dentro de una pestaña del navegador o de una extensión y paga desde la página con su propia wallet. Es el cliente del que hablamos en el buyer stack. Más o menos uno de cada cuatro sellers del Bazaar se puede pagar desde un cliente así cuando está bien hecho, y más o menos uno de cada veinte con el cliente que publica el SDK.

El censo también dice algo sobre el seller stack que describimos en cómo una ruta se convierte en endpoint pago. Una prueba con curl contra ese pipeline se ve perfecta. Ninguna de estas fallas aparece en curl. Solo aparecen en un navegador real, en un cliente HTTP estricto o con un método que nadie planeó.

Cómo mantenerse en la frontera

El orden va de lo más barato y seguro a lo más estratégico.

  1. Exponer los headers de pago. Cambiar nuestra llamada a cors() para exponer PAYMENT-REQUIRED, PAYMENT-RESPONSE y X-Request-Id en toda respuesta. Es una opción en un middleware. Seguir reflejando los headers pedidos hasta que salga #3478, porque una allow list explícita dejaría afuera al cliente actual.
  2. Probar desde un navegador, no desde curl. Sumar a CI el harness de este post: una página en un segundo origen, Chrome headless, y tanto una lectura simple como un pago completo con @x402/fetch contra un facilitator simulado. Habría detectado cada una de las tres primeras configuraciones de arriba.
  3. Fijar un presupuesto de header. Hacer fallar el build si nuestro valor PAYMENT-REQUIRED pasa de 8.190 bytes. Hoy estamos en 552. Metadata del Bazaar, ofertas firmadas o redes extra son lo que lo haría crecer.
  4. Ser explícitos con los métodos. Mantener las rutas pagas declaradas con método y solo POST. Rechazar un pago en HEAD u OPTIONS con un error claro. Adoptar resource.method de #3741 si se mergea.
  5. Actualizar cuando salga el release, y verificarlo. El próximo release de @x402/* debería traer #3796. Antes de confiar en un changelog, desempaquetar el paquete publicado y buscar el fix con grep. Después volver a correr este censo para ver si los sellers acompañan.
  6. Publicar un ejemplo de navegador con tope de gasto. Una página corta que le pague a nuestro gateway desde una wallet de navegador, con un límite por llamada y por sesión en el firmante. CORS deja que la página vea el precio. Solo el firmante decide si lo paga.

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