Presupuestos de conversación de x402 en Solana: una clave prepagada con un escrow detrás
Una app de chat no puede pedirle al usuario un popup de wallet por cada mensaje. Un pull request abierto de x402 propone una salida: aprobar un presupuesto con tope y luego conversar. Leímos la spec de 856 líneas, reprodujimos su vector de hash y revisamos qué garantizan realmente la credencial, el tope y el refund.
La propuesta es el PR #4068 de x402-foundation/x402. Lo abrió notorious-d-e-v el 2026-10-30, es solo spec, y su commit head al leerlo era 2d05e55. No tenía reviews. Agrega un archivo nuevo, scheme_batch_settlement_svm_conversation_budget.md, y edita el binding padre de batch-settlement para Solana (899 líneas agregadas y 12 eliminadas entre los dos archivos).
Cubrimos la mecánica de los canales en el deep dive de batch-settlement y la pregunta de la custodia en la auditoría de custodia de vouchers de ayer. Este post trata otra capa: la autorización que un usuario en el navegador da una sola vez, y qué puede y qué no puede acotar.
Qué propone el PR
El flujo tiene cinco piezas.
- Un canal, un depósito. El canal es nuevo y se fondea exactamente hasta el máximo aprobado. La spec dice que un canal existente más grande "no puede tratarse como un presupuesto más pequeño igualmente protegido".
- Una credencial privada. El navegador genera 32 bytes aleatorios, escritos como 64 caracteres hex en minúscula. Solo su SHA-256 es público. Viaja en el header
PAYMENT-SIGNATUREde cada request posterior. - Una apertura nativa que compromete los términos. La transacción de apertura firmada por el payer lleva un Memo con el ID del presupuesto, el tope, la expiración y un digest de los términos completos. Los términos incluyen el origen de la audiencia, network, payer, operator, receiver, sponsor, asset, salt, open slot, withdrawal delay y el hash de la credencial.
- Vouchers acumulados firmados por el servidor. El merchant reserva un techo antes del handler, lo ejecuta y luego confirma el cargo real y el siguiente voucher acumulado. La admisión es una decisión del ledger del merchant, no una llamada a la cadena.
- Recuperación con el refund existente. Una credencial perdida se resuelve con la wallet del payer firmando la intención de cierre de cargo cero que ya existe. En v1 no hay endpoint de recuperación nuevo.
El modo se negocia con un campo nuevo. Un facilitator lista "conversation-budget-v1" en extra.authorizationModes. El 402 del merchant fija extra.authorizationMode, más paymentFlow: "escrow" y voucherSigner: "server". Un cliente que no conoce el modo debe saltarse ese accept, nunca leerlo como legacy. El merchant también anuncia un segundo accept convencional en la misma ruta, usado solo para refunds.
El primer request es un 402 ordinario y un reintento firmado, con un payload budget-deposit. Los siguientes envían budget-authorization con el ID del presupuesto, un requestId fresco y la credencial. El facilitator nunca ve la credencial: el merchant reenvía solo una variante pública sanitizada.
Reproducir el vector
La spec termina con un fixture sin pagos: claves públicas sintéticas, una credencial de 64 ceros ASCII y los hashes esperados. Un formato que compromete el hash de un secreto y un digest de términos vive o muere por la serialización exacta al byte, así que lo recalculamos desde cero en Python.
# credential = "0" * 64
sha256(credential) = 60e05bd1b195af2f94112fa7197a5c88289058840ce7c6df9693756bc6250f55
sha256(terms bytes) = 15ad92f2f521b188bdf4414aa4658b033f832c71bcce0333b9b729339ca50234
termsHash (b64url) = Fa2S8vUhsYi99EFKpGWLAz-DLHG8zgMzubcpM5ylAjQ # 43 chars
Memo length = 145 bytes
Memo at max fields = 164 bytes # cap 2^64-1, expiry 2^53-1
Los cuatro valores coinciden con el Apéndice, incluido el peor caso de 164 bytes, que queda bien por debajo del límite de 256 bytes del Memo. Los términos son 18 líneas fijas unidas con LF y sin salto final, así que una wallet que normalice finales de línea o recorte espacios rompe el digest. La spec lo sabe: rechaza hex en mayúscula, CRLF y cambios de caja, y su matriz de aceptación lista "MetaMask, Phantom, Solflare" como una fila de compatibilidad sin verificar. Nosotros no pudimos probar ninguna wallet.
El diseño del compromiso es cuidadoso. Como el hash de la credencial está dentro de los términos firmados, un merchant no puede cambiarlo por otro después de que el usuario aprueba. Como el origen de la audiencia también está en los términos, una credencial aprobada para un merchant no valida contra otro.
Lo que el PR no cambia en el SDK
Clonamos el repositorio y buscamos authorizationMode, conversation-budget y budget-deposit. Aparecen solo en los dos archivos de spec. El código de batch-settlement para Solana en typescript/packages/mechanisms/svm/src/batch-settlement ya tiene una opción voucherSigner y un campo maxIdleSecs, así que la base firmada por el servidor existe. El modo de presupuesto no.
Las notas del propio PR dicen lo mismo: no se afirma que las versiones actuales del SDK lo soporten. Un detalle importa para el chat. El helper de refund actual en client/refund.ts sondea la ruta y envía el refund firmado con method: "GET". Un endpoint de chat es una ruta POST. La spec lo resuelve exigiendo que el merchant despache los mensajes de control de refund en la URL del recurso "independientemente del handler pagado y sus restricciones de método". Es una obligación del lado servidor que el helper todavía no ejercita.
Un PR relacionado, #3905, agrega pruebas de batch legibles opt-in para signers de navegador. Es independiente y sigue abierto. La spec de presupuestos se apoya en él de forma indirecta: una wallet que no puede firmar la prueba binaria de refund actual no tiene recuperación HTTP cooperativa.
Hallazgo 1: el tope es una promesa, el depósito es la cota
La palabra "presupuesto" sugiere un límite que el sistema hace cumplir. La spec aclara que no es así: "Expiry y Stop son controles de admisión del merchant; el operator conserva su autoridad onchain existente sobre el depósito". El firmante de vouchers es el servidor, y el servidor puede reclamar onchain hasta el depósito completo.
La garantía real es entonces el tamaño del escrow. Un merchant deshonesto puede llevarse todo el monto. No puede llevarse más. Es una cota estrictamente mejor que un allowance abierto, y es la razón de la regla un-canal-un-depósito. También significa que el usuario bloquea el tope completo desde el inicio. Un presupuesto de 5 USDC son 5 USDC fuera de la wallet con el primer mensaje, no 5 USDC de permiso.
Compáralo con el modelo de allowance de nuestra auditoría de Spend Permissions, donde la wallet conserva los fondos y un contrato onchain hace cumplir un límite por período. El presupuesto de conversación cede esa aplicación a cambio de un solo popup. El intercambio es honesto pero conviene nombrarlo: es un saldo prepagado con refund, no una política de gasto.
Hallazgo 2: una credencial robada vacía, pero no dirige
La credencial es un secreto bearer. Quien la tenga puede gastar el remanente. La spec lo dice directamente y acota el daño con expiración, Stop y contabilidad honesta. Dos decisiones de diseño dejan el modo de falla más estrecho de lo que parece.
- La credencial no puede detener ni reactivar. Stop es la intención de refund autenticada por el payer. Un ladrón con solo la credencial no puede cerrar el grant, ni reabrir uno que el payer detuvo.
- El refund no necesita la credencial. La wallet del payer firma un
BatchAuthorizationnuevo conauthorizedAmount: "0". El merchant debe fijar de forma durable la admisión enclosingantes de revisar reservas pendientes, de modo que un request concurrente queda reservado antes del stop o rechazado después. Un sondeo sin autenticar no puede detener un presupuesto.
Esa regla de orden es la parte de la spec con más probabilidad de implementarse mal. "Verificar, revisar si está ocupado y luego detener" deja una ventana en la que un ladrón sigue admitiendo trabajo mientras el refund del dueño se rechaza por ocupado. La matriz de aceptación tiene una fila: "Refund mientras una credencial robada envía trabajo concurrente: detener de forma durable antes de revisar busy".
La spec lista dónde la credencial nunca debe aparecer: URLs, datos onchain, requests al facilitator, logs, analytics, entradas de modelo o de tools, y redirects ajenos. Para un producto con LLM lo último no es abstracto. Un header que lleva un secreto gastable queda junto a prompts y salidas de tools en muchos pipelines de tracing.
Hallazgo 3: reservar el techo deja parte del presupuesto varada
La admisión es una sola desigualdad:
spent + reserved + next request ceiling <= approved maximum
El techo es accepted.amount, el máximo cotizado por request. El costo real puede ser menor y la parte no usada se libera. Pero el último request se admite solo mientras el remanente sea al menos un techo. Lo que quede por debajo no se puede gastar. No se pierde, porque el refund lo devuelve, pero gastarlo hasta cero requiere una firma de wallet.
El peor caso de monto varado es justo menos de un techo. Sobre un presupuesto de 5 USDC:
| Techo cotizado | Máx. varado | Parte de 5 USDC |
|---|---|---|
| 0,01 USDC | menos de 0,01 | 0,2 % |
| 0,10 USDC | menos de 0,10 | 2 % |
| 0,50 USDC | menos de 0,50 | 10 % |
| 1,00 USDC | menos de 1,00 | 20 % |
Es aritmética, no una medición. Importa para facturar LLMs porque el techo suele derivarse de un tope de tokens, y un request de contexto largo o de razonamiento puede tener un techo varias veces mayor que su costo típico. La spec también dice que el cliente debe obtener requisitos nuevos si el precio cambió, y que las ofertas ecoadas no pueden redefinir el grant.
Sondeamos el 402 gratuito de nuestro propio gateway el 2026-11-01 con cuatro slugs de modelo y max_tokens de 256 a 200.000. Cada respuesta fue una cotización exact de 10.000 unidades atómicas de USDC (0,01 USDC) en Base. Ese valor plano parece un piso, así que esos sondeos no pueden mostrar cómo escalaría un techo real. Lo que sí muestran es el segundo campo: el 402 anuncia un maxTimeoutSeconds de 300.
Hallazgo 4: una operación en vuelo significa un canal por llamada en paralelo
Los clientes v1 "DEBEN mantener como máximo una operación pagada lógica en vuelo por canal, incluido el primer recurso fondeado". Es razonable para un chat humano, donde los turnos son secuenciales. Un agente es distinto. Un planner que lanza ocho tool calls en paralelo tiene dos opciones con esta spec: serializarlas o tener ocho canales.
Ocho canales con tope de 5 USDC cada uno son 40 USDC en escrow y ocho transacciones de apertura, con ocho credenciales que guardar. Nada en la propuesta lo prohíbe, y la spec indica que los presupuestos multi-merchant y los top-ups quedan fuera de v1. Pero la economía muestra para quién es este modo: una persona escribiendo, no un agente abriendo abanico. El título de la spec dice "conversation", y ese alcance es exacto.
Hallazgo 5: el deadline decide quién asume el costo de una respuesta tardía
Cada reserva tiene un deadline: el momento de admisión registrado por el merchant más el maxTimeoutSeconds cotizado. Los reintentos no pueden extenderlo. En el deadline el merchant debe confirmar un éxito establecido de forma durable o cancelar la operación sin cargo. Tras la cancelación, "todo worker o resultado tardío" queda impedido de forma permanente de confirmar un cargo o un voucher. La spec agrega que terminar en el deadline "no crea el derecho a cobrar después", y que los costos del proveedor "no bastan por sí solos para autorizar el cobro de un request de recurso fallido".
El 402 de ejemplo de la spec usa 60 segundos. Un merchant que cotiza 60 segundos y ejecuta una llamada de razonamiento lenta asume el costo de toda respuesta que llega en el segundo 61. El remedio es fijar el timeout según la latencia de cola real del handler y dejar margen para confirmar antes del deadline anunciado, como recomienda la spec. Nuestra cotización actual anuncia 300.
La misma sección protege al comprador de una forma que vale la pena copiar. El paso del tiempo nunca libera una reserva por sí solo, y una operación cancelada devuelve una razón fija invalid_batch_settlement_svm_budget_operation_canceled en vez de un bucle de pendiente. Reintentar con el mismo requestId devuelve el resultado retenido y nunca re-ejecuta el trabajo. Un recibo de pago guardado no puede recrear una respuesta de IA, por eso la spec exige también idempotencia a nivel de aplicación.
El refund, y dónde deja de ser cooperativo
El refund cooperativo corre por los settle_and_seal y el distribute sellado existentes. El merchant inyecta su voucher final de operator guardado y la autorización de cierre del receiver authorizer, así que nunca confía en un voucher aportado por el payer. Una vez aceptada la prueba del payer, el merchant sigue el cierre sin otro prompt, incluso si la prueba expira.
Si el merchant desaparece, el respaldo es el request_close onchain del propio payer, luego el período de gracia y los pasos de seal, withdraw y distribute. La spec es directa: eso "no promete un refund inmediato ni trustless" y "puede permitir claims finales del operator hasta el depósito durante la gracia". El canal de ejemplo usa un withdrawal delay de 900 segundos. Durante esos 15 minutos el operator todavía puede reclamar.
La regla de reapertura es un buen default. Tras un refund confirmado, la continuación por defecto es el remanente bajo la expiración original: con 1,75 USDC gastados de 5, como máximo 3,25 USDC. Un tope mayor o una expiración posterior requiere una aprobación nueva y explícita. Abrir un canal nuevo antes de confirmar el refund es "una decisión de fondeo explícita aparte, no recuperación automática".
Qué significa para LLM4Agents
Nuestro gateway cotiza un pago exact por llamada. Un presupuesto de conversación es la forma opuesta: pagar una vez y luego llamar muchas veces con una credencial. Los agentes ya conocen esa forma, porque es una API key prepagada. Lo que agrega el PR es una versión no custodial, donde el saldo está en un canal que el payer puede cerrar con una firma de wallet y que el merchant no puede subir.
El encaje es parcial. La spec apunta a navegadores y humanos: un popup de wallet, una pestaña, turnos secuenciales. Los agentes autónomos chocan de inmediato con la regla de una operación en vuelo y con el remanente varado. El modo es más relevante para un cliente de cara a humanos de un producto de agentes, como una UI de chat delante de nuestro gateway, que para el salto agente-gateway en sí.
Vale nombrar tres riesgos. Una credencial en un header es una clase nueva de secreto en nuestros caminos de logging y tracing. Una reserva basada en techo expone qué tan laxamente derivamos los techos por llamada. Y la regla del deadline hace que el timeout que cotizamos sea un término financiero, no solo de transporte.
Cómo mantenerse en la frontera
- No hacer nada en código todavía. El PR es un borrador sin reviews, sin SDK y sin prueba de wallets. Seguirlo, y seguir #3905 por el formato de prueba del que depende.
- Auditar nuestros techos por llamada. Si alguna vez cotizamos un techo, derivarlo del tope real de tokens del modelo y publicar cómo. Un piso plano es seguro para
exact, pero sería inútil como reserva. - Fijar
maxTimeoutSecondsdesde la latencia de cola medida por ruta, y decidir de antemano quién asume el costo de un resultado que llega después del deadline. - Agregar la credencial a la lista de redacción ya. Todo header o campo que pueda contener un secreto bearer gastable debe limpiarse de logs, traces y entradas de modelo, sea cual sea el scheme que termine usándolo.
- Prototipar contra un fork si el PR gana reviewers. Armar el harness que implica la matriz de aceptación: reservas concurrentes, un refund compitiendo con una credencial robada y un reintento tras una respuesta perdida. Esas tres filas deciden si una implementación es segura.
- Combinarlo con topes del lado comprador. Un presupuesto de conversación acota a un merchant. Los topes por defecto de la auditoría de spend controls del SDK siguen acotando al agente entre merchants.
Paga por llamada, sin popups de wallet
LLM4Agents le da a los agentes autónomos un gateway compatible con OpenAI que cobra en stablecoins sobre x402.
Registra tu agente