← Blog
24 de agosto, 2026 · 13 min

El default de $1: auditoría de los spend controls de x402

Desde la versión 2.23.0, todo cliente x402 construido sobre los SDKs oficiales se niega a pagar más de un dólar por request salvo que alguien suba el límite explícitamente. Leímos el código del filtro en los tres lenguajes, ejecutamos once escenarios contra el paquete publicado en npm y mapeamos exactamente dónde protege el nuevo guardarraíl a un agente — y dónde, silenciosamente, no.

En el roundup de la semana pasada cubrimos el anuncio: los spend controls aterrizaron en el SDK de TypeScript el 13 de agosto (PR #3124, 146 archivos, +3.795/−1.317, autor phdargen), seguidos por Python (#3154) y Go (#3156) el 18 de agosto, mergeados con once minutos de diferencia. Este post es la auditoría a nivel de código. Clonamos x402-foundation/x402 en HEAD 6557149b (24 de agosto de 2026) e instalamos @x402/core 2.23.0 y @x402/evm 2.23.0 desde npm — las versiones que recibe cualquier agente que se construya hoy. Lo que está en juego no es menor: @x402/core registró 889.128 descargas en npm en los treinta días que terminaron el 23 de agosto.

Qué se publicó, y dónde vive

El mecanismo es un filtro dentro de selectPaymentRequirements, la rutina del cliente que elige cuál de las ofertas de pago del servidor firmar. El pipeline de x402Client tiene ahora seis pasos: filtrar por schemes registrados, descartar valores de paymentFlow no reconocidos, aplicar spend controls, aplicar las políticas del usuario, preferir ofertas de flujo authorization y correr el selector. Dos detalles de posición importan.

Primero, los spend controls corren antes que las políticas definidas por el usuario. Una policy no puede ver una oferta que el filtro de gasto ya descartó. Segundo, el filtro es deliberadamente permisivo con ofertas mixtas: el comentario en el código dice "Keeps any accept that fits so a mixed offer can still pay the affordable option". Si un servidor ofrece el mismo recurso a $5 y a $0,50, el cliente no lanza error — firma en silencio la opción de $0,50. Lo confirmamos en nuestro drive: con ambos accepts, el SDK produjo una autorización EIP-3009 por exactamente 500000 unidades atómicas de USDC.

El default es una constante con nombre, idéntica en las tres bases de código: DEFAULT_MAX_AMOUNT_PER_PAYMENT = "$1" en TypeScript (x402Client.ts), Python (client_base.py) y Go (types.go). El Newx402Client de Go construye con spendControlsEnabled: true; TypeScript y Python inicializan el objeto de controles en {}, que resuelve al mismo comportamiento. Nadie hace opt-in. Todos están adentro.

Eso fue polémico en el review. En el PR #3124, el reviewer CarsonRoscoe señaló que "upgrading silently caps every existing client at $1/payment and default-asset-only" — un cambio de comportamiento montado sobre un bump de versión menor. El autor confirmó que era "the intended behaviour", y mitigó agregando spendControls explícitos a los ejemplos básicos. La decisión es defendible: un default que falla cerrado es la polaridad correcta para compradores autónomos. Pero los operadores deben tener claro que 2.22.x a 2.23.0 es una ruptura semántica para cualquier cliente que pagaba más de un dólar. El modo de falla es una excepción lanzada desde createPaymentPayload con un string de error nuevo, no un retry del 402.

Qué cuenta como stablecoin reconocida

El cap no aplica a todos los assets. Aplica solo a los que el SDK puede identificar como peggeados al dólar, vía un nuevo lookup inverso por mecanismo, findDefaultAsset(asset, network). Cada paquete de mecanismo incluye ahora una tabla DEFAULT_ASSETS indexada por red CAIP-2. En TypeScript, las once familias de cadenas la traen: EVM, SVM, Aptos, Algorand, Concordium, Hedera, Keeta, NEAR, Stellar, TVM y XRPL. La tabla EVM lista 24 redes; Solana mainnet lista cinco entradas (USDC, USDT, USDG, PYUSD, CASH — las últimas tres bajo Token-2022); XRPL trae RLUSD con el issuer fijado en el client scheme antes de firmar; Concordium trae USDR. Python cubre EVM, SVM y TVM; Go cubre EVM y SVM — en línea con la cobertura de mecanismos de cada SDK, con el set EVM de 24 redes idéntico en los tres.

La tabla es infraestructura viva, no documentación. El commit en HEAD cuando clonamos — mergeado la misma mañana de esta auditoría — era el #3227, que agrega USDC de Sei mainnet (eip155:1329) y testnet a los tres SDKs. Entrar a DEFAULT_ASSETS es ahora la diferencia entre "los agentes te pagan por defecto" y "los agentes te rechazan por defecto". Espera que ese archivo se convierta en uno de los más políticamente interesantes del repositorio.

La inversión de confianza — el cap de $1 aplica solo a los assets que findDefaultAsset reconoce. Los tokens que el SDK no conoce se rechazan de plano por defecto, pero una vez admitidos vía allowedAssets sin cap por entrada, quedan sin tope. Cuanto mejor entiende el SDK un asset, más corta la correa; cuanto más exótico el asset, menos protección ofrece el tope numérico.

Ejercitando el filtro: once casos

Leer código te dice qué debería pasar. Preferimos verlo pasar. La firma EIP-3009 es completamente offline — el SDK firma una autorización de typed data sin tocar ninguna cadena — así que una llave descartable ejercita todo el camino contra el paquete real publicado. Once escenarios contra @x402/core 2.23.0, requirements de Base mainnet, scheme exact:

// @x402/core 2.23.0 + @x402/evm 2.23.0 desde npm, 24-ago-2026
A  default / USDC $1.00           → ACEPTADO (firmado, 1000000 atómico)
B  default / USDC $1.000001       → RECHAZADO (spendControls.maxAmountPerPayment $1)
C  default / DAI $0.01            → RECHAZADO (solo default assets permitidos)
D  allowlist DAI, sin cap / 1M DAI → ACEPTADO — sin tope
E  allowedAssets: true / 1M DAI   → ACEPTADO — sin tope
F  allowedAssets: true / USDC $5  → RECHAZADO (el cap de $1 sigue atando defaults)
G  cap por asset "$2"             → error de config: debe ser entero atómico
H  spendControls: false / $250    → ACEPTADO
I  cap $0.05 / USDC $0.10         → RECHAZADO
J  oferta mixta $5 + $0.50        → ACEPTADO — firma la opción de $0.50
K  símbolo "USDC", cap 2000000 / $1.50 → ACEPTADO (cap por asset supera el de $1)

Cada caso coincidió con el código. El borde es inclusivo: exactamente $1,00 firma, una millonésima de dólar más rechaza. El caso K muestra la válvula de escape bien hecha — una entrada de allowedAssets puede referenciar un default asset por símbolo y subir su cap con un monto atómico entero, por token, sin tocar el límite global. El caso G muestra al SDK rechazando un cap por asset en formato dólar como error de configuración en lugar de adivinar decimales, lo cual es correcto: los caps por asset se denominan en unidades atómicas precisamente porque el SDK puede no conocer los decimales del token.

Los casos D, E y F juntos son el hallazgo. Con allowedAssets: true — el one-liner al que va a recurrir cada integrador frustrado — un pago de un millón de DAI pasa sin fricción mientras un pago de cinco dólares en USDC sigue bloqueado. La asimetría tiene fundamento (el SDK no puede convertir un token desconocido a USD, así que no puede caparlo), pero el resultado operativo es que el guardarraíl más ruidoso cuida los assets más seguros. Un agente cuyo operador "arregló" un rechazo con allowedAssets: true está a una prompt injection de firmar una autorización de valor arbitrario en un token arbitrario, mientras su gasto en USDC sigue educadamente cappeado en $1.

La letra chica del algoritmo

Cuatro detalles de la implementación merecen la atención de cualquiera que construya contra ella.

La versión 1 está cubierta. El filtro lee amount en requirements v2 y maxAmountRequired en v1, y los tres SDKs aplican spend controls en ambos caminos de selección. Los flujos legacy de facilitator no evitan el cap.

Los montos decimales tienen un camino paralelo. La mayoría de los montos x402 son strings enteros en unidades atómicas, pero algunos ledgers cotizan decimales — una oferta de RLUSD en XRPL puede traer "0.01". El filtro detecta la forma no entera y la compara contra el cap en dólares a paridad uno a uno, escalando ambos lados a dieciocho decimales. Eso funciona solo porque DEFAULT_ASSETS impone un invariante de peg al dólar — la documentación del repositorio es explícita en que agregar una entrada en EUR o JPY requeriría caps por moneda que todavía no existen. En el camino del cap por asset, la misma forma decimal simplemente se descarta, porque un cap atómico entero no puede compararse con un monto decimal de ledger sin metadata de decimales; el changelog lo dice sin vueltas: "a non-integer 402 amount on that path is dropped".

Los schemes custom son culpables hasta integrarse. findDefaultAsset es un método opcional en la interfaz del scheme client. Un mecanismo comunitario anterior a 2.23.0 que nunca lo implementó no devuelve default assets, lo que significa que todas sus ofertas son ahora "non-default" — rechazadas bajo la configuración por defecto hasta que los usuarios permitan sus tokens en la allowlist o el mecanismo publique una tabla. El mismo release también endureció la dirección inversa: un override de settlement en formato dólar ahora lanza error cuando getAssetDecimals no conoce el asset, en lugar de asumir seis decimales y despreciar en silencio un token de dieciocho decimales por doce órdenes de magnitud.

Los errores enseñan el arreglo. Cada string de rechazo enumera sus propias válvulas de escape — "Raise maxAmountPerPayment, set it to false to disable, set allowedAssets[].maxAmountPerPayment for a per-asset atomic cap, or set spendControls: false to disable all spend controls". Amigable para el desarrollador, y digno de una segunda lectura: esos strings aparecen en trazas de excepción que ven los agentes, lo que significa que un LLM operando su propio loop de pagos lee un menú de formas de quitarse su propio guardarraíl. Que el agente pueda actuar sobre ese menú depende enteramente de quién controla la configuración del cliente — que es exactamente donde los operadores deben trazar la línea.

Lo que los spend controls no son

Tres fronteras definen esta feature más que su código.

No son protocolo. La especificación x402 v2 menciona el concepto entero una sola vez, en la sección 12.1, como "budget management and spending controls (implementation-specific)". Nada cambió en el wire: sin headers, sin campos, sin participación del facilitator. Un servidor no puede detectar si el comprador corre spend controls, y un facilitator no puede aplicarlos. Esto es una propiedad de tres SDKs particulares, no de x402. El cuarto SDK lo demuestra: x402-rs, la implementación en Rust que auditamos este mes, no tiene equivalente — su cliente x402-reqwest trae un selector FirstMatch y cero chequeos de monto en HEAD e75adda. Un agente en Rust paga lo que exija la primera oferta que matchee.

No son un presupuesto. El cap es por pago. No hay contador acumulado, ni límite de sesión, ni techo diario en ninguna parte de la implementación. Un agente cappeado en $1 por pago puede hacer diez mil pagos de $1. Para trabajo medido, el scheme upto acota un flujo completo con un máximo escrowed on-chain — un presupuesto aplicado por la cadena donde los spend controls son uno local al proceso. Los dos se componen; ninguno reemplaza al otro.

No son enforcement. El chequeo corre en el proceso del propio comprador, de su lado de la frontera de confianza. Protege a un operador honesto de servidores con precios abusivos, configs con errores de dedo y agentes manipulados. No protege a nadie de un cliente modificado. Ese es el alcance correcto — la llave del wallet del comprador es la autoridad real, y cualquier cosa que corra quien tiene la llave puede firmar cualquier cosa — pero significa que "x402 ahora tiene spend controls" debe leerse como "los buyer stacks oficiales ahora tienen cinturón de seguridad", no como una garantía de la capa de settlement. Los presupuestos de capa wallet que emergen alrededor del protocolo — los límites a nivel infraestructura de AWS AgentCore, las allowances de los Virtual Wallets de Cloudflare — están del otro lado de esa frontera, y siguen siendo necesarios.

Fricción en el mundo real

Once días después del merge de TypeScript, el ecosistema ya enseña a aflojar el cinturón. La guía de agent wallets de Venice AI lo dice sin rodeos: "The SDK ships with max_amount_per_payment set to one dollar, and the Venice minimum top-up is five, so an unmodified client rejects every rail on offer". Su arreglo documentado — SpendControls(max_amount_per_payment="$5", allowed_assets=True) — sube el cap y abre el filtro de assets en una sola línea, heredando el comportamiento del caso E de arriba. Este es el patrón a vigilar: defaults así de estrictos generan arreglos de copy-paste, y el arreglo más fácil es el más ancho.

Dentro del propio monorepo las excepciones son instructivas. El paquete de paywall de navegador construye su cliente con spendControls: false — razonable, porque un humano hace clic en aprobar, y el humano es el spend control. El cliente MCP reenvía spendControls vía from_config, así que los agentes que llaman tools reciben los mismos defaults que los agentes HTTP. Y un drift de documentación que vale reportar: el README de @x402/evm referencia una clase llamada ExactEvmClient catorce veces; el paquete exporta ExactEvmScheme y esa clase no existe. El README publicado en npm es byte a byte idéntico al del repo. Cada ejemplo de código en la puerta de entrada del propio paquete falla al importar.

Qué significa para LLM4Agents

LLM4Agents opera el buyer stack para flotas de agentes que pagan por inferencia sobre un gateway OpenAI-compatible, así que este cambio cae directo en nuestro threat model — en su mayoría como validación, en parte como trabajo.

Validación, porque el ordenamiento de defensa en profundidad que describimos en nuestros billing internals asumía que la capa SDK era el eslabón más débil: la contabilidad de reservas del lado de la plataforma, no los chequeos del lado del cliente, es lo que realmente acota el gasto de un agente en nuestro gateway. Eso sigue siendo cierto — los spend controls no agregan un presupuesto acumulado, así que el ledger de la plataforma sigue siendo el único lugar donde existe un techo a nivel de flota. Trabajo, porque los defaults se propagan: cualquier agente que use los SDKs oficiales contra nuestros endpoints con precios x402 va a rechazar respuestas por encima de $1 salvo configuración explícita. Los endpoints con precios de llamadas frontier de contexto largo pueden cruzar esa línea. O mantenemos los precios por request bajo el cap default, o documentamos la configuración exacta de allowedAssets con cap atómico (nunca allowedAssets: true), o partimos las llamadas caras al scheme upto, donde el máximo escrowed vuelve irrelevante el cap por pago.

La tabla DEFAULT_ASSETS también es ahora una dependencia. Liquidamos en USDC sobre redes reconocidas, así que hoy estamos dentro de la allowlist — pero la tabla cambia semanalmente (Sei aterrizó la mañana de esta auditoría), Go y Python todavía van detrás de la cobertura de familias de TypeScript, y cualquier asset de settlement futuro que agreguemos debe evaluarse primero contra ella. Un token fuera de la tabla es un token que el agente default no puede pagar.

Cómo mantenerse en la frontera

Pasos concretos, en orden. Primero, pinnear y probar: nuestras integraciones de SDK pasan a versiones clase 2.23.0 detrás de nuestra suite de conformance, con spendControls explícitos en cada ejemplo que publiquemos — el default silencioso nunca debería sostener carga en nuestros docs. Segundo, auditar precios del catálogo: enumerar cada ruta con precio x402 que exponemos y marcar cualquiera cuyo peor caso supere $1, y entonces reajustar precio, documentar el opt-in angosto o migrar a upto. Tercero, construir el presupuesto que el SDK no tiene: un techo de gasto acumulado por agente aplicado del lado de la plataforma, expuesto en el dashboard junto al cap por pago que el cliente ya aplica, para que los operadores vean ambos números y entiendan que son cosas distintas. Cuarto, publicar una guía de hardening que nombre el antipatrón: allowedAssets: true y spendControls: false son herramientas de debugging, no configuración. Quinto, vigilar la tabla: suscribirse a los cambios de DEFAULT_ASSETS en CI como ya vigilamos el drift de /supported en los facilitators, porque una entrada nueva ahí cambia lo que todo agente con configuración default en internet va a pagar — y una eliminada cambia lo que va a rechazar.

La lectura profunda es estratégica. La capa de pagos sigue agregando frenos — cuatro de los cinco cambios que cubrimos la semana pasada eran restricciones — y cada freno se convierte en un default que moldea el comportamiento de los agentes a escala de población. Una constante de una línea en tres SDKs decide ahora que la unidad de gasto casual de la economía autónoma es un dólar. Quien opera agentes profesionalmente necesita saber exactamente dónde ata esa constante, dónde no, y qué construir en el hueco. Ese hueco — presupuestos acumulados, caps conscientes del riesgo del asset, políticas a nivel de flota — es donde las plataformas se ganan el lugar.

Corre agentes que pagan dentro de límites que tú defines

LLM4Agents le da a cada agente una identidad fondeada, billing por uso en stablecoins y techos de gasto del lado de la plataforma que el SDK no puede proveer.

Registra tu agente