x402 exact en Stellar: la ventana entre verify y settle
En Stellar un pago x402 no es una transacción firmada. Es un conjunto de authorization entries de Soroban con un plazo medido en ledgers, y el facilitator arma la transacción que realmente lo settlea.
Todos los bindings de x402 que auditamos hasta ahora le entregan al facilitator algo que puede enviar más o menos tal cual. En EVM, el buyer firma una authorization EIP-3009 y el facilitator la envuelve en una llamada al token. En SVM, el buyer firma una transacción que el facilitator co-firma como fee payer.
Stellar no hace ninguna de las dos. El buyer firma solo la authorization interna, y el facilitator ensambla una transacción nueva a su alrededor al momento de settlear: su propia cuenta como source, su propio sequence number, su propio fee. El buyer nunca elige la transacción que entra.
Ese diseño tiene una consecuencia que vale medir. El hueco entre /verify y /settle pasa a ser la parte que carga el peso del scheme, porque la authorization tiene una vida contada en ledgers que ambas partes calculan por separado. Para un gateway que vende inferencia — donde el recurso protegido tarda segundos o minutos, no milisegundos — ese hueco es exactamente donde ocurre el trabajo.
Leímos la spec y la implementación de referencia, y después corrimos pagos reales en Stellar testnet contra el facilitator de referencia para ver dónde se abre y se cierra la ventana. Todo lo que sigue está medido, con los comandos y las versiones indicadas.
Qué firma realmente el buyer
El exact scheme para Stellar soporta solo x402 v2, en stellar:pubnet y stellar:testnet, y cubre únicamente tokens Soroban SEP-41. Los assets clásicos de Stellar quedan explícitamente fuera.
El buyer arma una invocación de contrato que llama transfer(from, to, amount) en el contrato del token, la simula para descubrir las authorization entries, firma esas entries y envía la transacción serializada como XDR base64. El facilitator la decodifica, valida la estructura, re-simula, reconstruye, firma y envía.
Las reglas de verificación son estrictas. Exactamente una operación, de tipo invokeHostFunction. La dirección del contrato debe ser igual a requirements.asset. La función debe llamarse transfer con exactamente tres argumentos: from es el firmante, to debe ser igual a payTo exactamente, amount debe ser igual a requirements.amount exactamente como i128. Sin sub-invocations. Y un bloque de chequeos que la spec marca con tres emojis de alarma, cuyo trabajo es evitar que el sponsor del fee termine pagando otra cosa: el facilitator no puede ser el source de la transacción, ni el source de la operación, ni la dirección from, ni aparecer en ninguna authorization entry.
Después, la simulación debe emitir exactamente el cambio de balance esperado y nada más. Esa última regla es más estricta de lo que suena, y volvemos a ella.
Corrimos esto de punta a punta con @x402/stellar 2.28.0 y @stellar/stellar-sdk 16.3.1 en testnet, con nuestro propio asset SEP-41 y un facilitator que controlábamos. Un payload de pago es chico: 1,352 caracteres base64 de XDR para un solo transfer.
El reloj es un contador de ledgers, no un reloj de pared
Esta es la diferencia estructural con cualquier otro binding. En EVM el buyer pone validBefore en now + maxTimeoutSeconds — segundos de reloj, consensuados por cualquiera que pueda leer un timestamp de bloque. En Stellar la spec dice que la expiración de la authorization es currentLedger + ceil(maxTimeoutSeconds / estimatedLedgerSeconds), con fallback a 5 segundos por ledger cuando no hay estimación de red disponible.
Así que el plazo depende de dos lecturas que ninguna de las partes comparte: la altura del ledger actual, y qué tan rápido cierran los ledgers. El client de referencia lee la altura de Soroban RPC y estima el tiempo de cierre con los últimos 20 ledgers de Horizon. El facilitator hace lo mismo, por separado, y rechaza el pago si la expiración del buyer supera su propio límite calculado por más de una tolerancia de 2 ledgers.
Medimos qué tan estable es esa estimación. Sobre 2,000 ledgers consecutivos de pubnet (64,900,025 a 64,902,024, cerrando entre 2026-10-12 06:18:27Z y 09:05:02Z), cada intervalo entre ledgers fue de exactamente 5 segundos. Lo mismo con 1,000 ledgers consecutivos de testnet. Corriendo el propio promedio de 20 ledgers del SDK sobre esa ventana se obtuvo 5 segundos en las 1,981 ventanas de pubnet y en las 981 de testnet, sin que el promedio crudo se desviara nunca de 5.000.
Ambas redes están en protocolo 29 al momento de esa medición (Horizon 29.0.0, stellar-core 29.0.0 tanto en pubnet como en testnet).
# Cada intervalo sobre 2,000 ledgers de pubnet, 2026-10-12
interval mean 5.000 median 5.0 min 5 max 5
interval hist [(5, 1999)]
SDK estimate (ceil of 20-ledger avg) hist [(5, 1981)]
Eso es buena noticia para la aritmética: en la práctica el divisor es una constante, así que maxTimeoutSeconds se mapea limpio a ledgers. Nuestros propios pagos lo confirmaron — maxTimeoutSeconds: 300 produjo una expiración exactamente 60 ledgers más adelante, y maxTimeoutSeconds: 60 produjo exactamente 12.
Lo que sigue siendo frágil es la altura, no la tasa. El RPC público está balanceado entre nodos que no siempre están en el mismo ledger, y la tolerancia de 2 ledgers es todo el margen para ese desfase. Ese modo de falla está abierto como issue #3168 en el tracker de x402: cuando la lectura del buyer cae en un nodo adelantado respecto del facilitator, un pago correcto se rechaza con invalid_exact_stellar_signature_expiration_too_far, y ninguna de las partes puede distinguirlo de una expiración genuinamente demasiado larga.
Cuánto cuesta un settlement al sponsor
Acá los fees son enteramente problema del facilitator. La spec le exige derivar el fee de settlement de una simulación fresca al momento de settlear — simulationResourceFee + inclusionBuffer, buffer de al menos 100 stroops — refrescar la Soroban resource data, y sobreescribir por completo lo que haya ofertado el buyer.
Para un transfer SEP-41 simple medimos un minResourceFee de 33,163 stroops. Con el inclusion buffer por defecto de 100 stroops eso da una oferta de 33,263 stroops, que coincidió con el max_fee de la transacción que entró. Después del reembolso de recursos no usados de Soroban, el fee_charged real fue 23,066 stroops.
Ahora pongamos eso contra el techo de seguridad de la implementación. maxTransactionFeeStroops tiene default 50,000, y la verificación falla con invalid_exact_stellar_payload_fee_exceeds_maximum por encima. Así que el pago más simple posible ya consume dos tercios del techo: 1.50x de margen, y solo 16,837 stroops de espacio para subir la oferta de inclusión antes de que el techo se dispare.
Eso importa porque la oferta de inclusión no es opcional en la práctica. El PR #3503, mergeado el 2026-09-17 y publicado en 2.27.0, hizo configurable el inclusion fee precisamente porque el facilitator siempre ofertaba el mínimo de 100 stroops, que mainnet con frecuencia no incluye para transacciones Soroban — así que los settlements podían timeoutear después de un verify exitoso. El default sigue siendo 100. Un operador que lo sube para lograr inclusión en mainnet está gastando el mismo margen de techo que necesita un payer más complejo.
Que es la otra mitad del problema. El issue #3515 reporta que el facilitator público de x402.org rechazó un pago válido desde una smart account de Soroban porque su __check_auth llama a otro contrato, empujando el resource fee por encima del techo. El payer no puede descubrir el techo de antemano ni cambiarlo.
Verify no es una predicción de settle
La spec es explícita en que /settle debe hacer la verificación completa de forma independiente. El facilitator de referencia hace exactamente eso: settle llama primero a _verify y después re-simula. Así que un payload se chequea dos veces, contra dos estados de ledger distintos.
Queríamos saber con qué frecuencia las dos respuestas pueden diferir, porque en el flow authorization por defecto el recurso protegido corre entre ambas. El orden es verify, después recurso, después settle, después responder.
Dos casos produjeron un verify válido y un settle fallido.
El primero es timing. El facilitator chequea que la expiración no esté demasiado lejos en el futuro, pero no hay ningún chequeo de que esté lo bastante lejos para sobrevivir al trabajo. Un payload cuya authorization expiraba en un ledger verificó como válido; veinte segundos después, settle falló en la re-simulación con Error(Auth, InvalidInput) y el diagnóstico "signature has expired". Igual con tres ledgers. El lado EVM sí tiene un límite inferior — su facilitator rechaza una authorization cuyo validBefore esté a menos de seis segundos — y la ruta de Stellar no tiene equivalente.
# expiración a 1 ledger
verify {"isValid": true, ...}
settle {"success": false,
"errorReason": "invalid_exact_stellar_payload_simulation_failed"}
# diagnóstico: "signature has expired", 5154505, 5154501
El segundo son las preconditions de la transacción. El facilitator reconstruye la transacción con su propia cuenta como source pero copia los campos ledgerBounds, extraSigners, minAccountSequence y el memo del buyer. Ninguno está en las reglas de verificación, porque ninguno afecta la simulación — los hace cumplir el consenso al momento del envío. Enviamos tres payloads con preconditions que la transacción reconstruida no podía satisfacer. Los tres verificaron como válidos, y los tres fueron rechazados por la red al enviarse con txTooLate, txBadAuth y txBadMinSeqAgeOrGap respectivamente.
La parte importante es lo que no pasó. El scheme falla cerrado. En ningún caso se movieron fondos, el balance de tokens del buyer quedó intacto, y el balance de XLM y el sequence number del facilitator fueron idénticos antes y después de los tres intentos — una transacción rechazada nunca llega al ledger, así que el sponsor ni siquiera paga el fee. El costo es de liveness, no de dinero: round trips de RPC desperdiciados para el facilitator, un pago fallido para el buyer y — en el flow authorization — un resource server que ya hizo el trabajo. Es la misma exposición de dos fases que medimos en el hueco entre verify y settle, apareciendo acá por otro mecanismo.
El arreglo constructivo es chico y local: un chequeo de vida mínima restante en verify, y o rechazar preconditions desconocidas o descartarlas durante la reconstrucción. Ambos son cambios del lado del facilitator que no necesitan revisión de la spec.
Vale notar que el replay está bien manejado. Enviar el mismo payload ya settleado una segunda vez falla en la re-simulación con Error(Auth, ExistingValue) y el diagnóstico "nonce already exists for address" — el propio nonce de authorization de Soroban hace el trabajo, sin necesidad de un store de nonces del lado del facilitator.
La dirección que parece válida y no lo es
Stellar tiene muxed accounts: una sola cuenta on-chain multiplexada en muchos balances virtuales, escrita como una dirección M. Un custodio o una plataforma que factura para muchos sellers a través de una cuenta usaría exactamente eso.
El regex de destino de la implementación de referencia las acepta. Valida cuentas G, cuentas C y cuentas M de 69 caracteres. Así que un resource server puede publicar una dirección M como payTo, y el client del buyer va a armar y firmar el pago sin chistar: el SDK codifica el destino como scAddressTypeMuxedAccount, y el transfer simula correctamente.
La verificación después lo rechaza con invalid_exact_stellar_payload_event_wrong_to.
La razón está en el CAP-67, final desde el protocolo 23. Cuando el destino es muxed, el SAC no pone la dirección M en el evento. Emite la dirección G subyacente en los topics y mueve el id de multiplexación al campo de data, que pasa a ser un map en vez de un monto simple. Confirmamos la forma en testnet:
topics: ['transfer', 'GCD7ACC…KTEW', 'GDZGXPC…O7XG', 'XUSD:GDYEUQN…5EVW']
data: {"amount": "10000000", "to_muxed_id": "4242"}
El facilitator compara el topic to del evento contra requirements.payTo como string. Dirección G contra dirección M, así que nunca coincide. El pago es correcto, el dinero iría a la cuenta correcta, y el chequeo está comparando dos codificaciones distintas del mismo destino — la misma clase de desajuste que encontramos en las codificaciones de direcciones interoperables contra payTo.
Para quien construya sobre esto hoy la lección es operativa: publica la dirección G, lleva el id de multiplexación en otro lado, y no confíes en el validador de destino para saber qué va a aceptar el facilitator.
A qué payers puede servir el scheme hoy
Los buyers interesantes de x402 no son keypairs sueltas. Son agentes pagando bajo un tope on-chain, lo que en Stellar significa una custom account de Soroban. Tres issues abiertos dicen que esa clase está en buena medida bloqueada hoy.
El issue #3352 y el issue #3764 apuntan al mismo chequeo: validateSimulationEvents trata todo evento de tipo contract como si tuviera que ser el transfer, y rechaza el primero que no lo sea. Una smart account cuya lógica de autorización emite su propio evento falla la verificación. El #3764, abierto el 2026-10-10, lo reporta contra la policy spending_limit de OpenZeppelin, que emite spending_limit_enforced en cada transfer que autoriza — reproducido tanto contra el facilitator de referencia en 2.28.0 como contra OZ Channels en testnet.
Así que la configuración exacta que quiere un operador de agentes — una wallet con un tope por período on-chain — es la que no puede pagar. Súmale el techo de fees del #3515 y el #3158, donde el client scheme fuerza una forma de firma ed25519, y la respuesta práctica hoy es que Stellar exact sirve bien a keypairs de cuentas G y mal a contract accounts.
Los spend controls del lado client están mejor. Desde 2.23.0 el paquete trae DEFAULT_ASSETS más una política por defecto de solo assets pegged reconocidos con un tope de $1 por pago, allowedAssets opt-in con techos atómicos enteros por asset, y spendControls: false para desactivar. El USDC de pubnet es el único asset por defecto, con 7 decimales. Es el mismo patrón de tope por defecto que examinamos en x402 spend controls, y acá se comporta igual.
Un detalle de deployment que va a agarrar a operadores: el paquete no tiene RPC de mainnet por defecto. getRpcUrl lanza error para stellar:pubnet salvo que pases uno. Testnet usa el endpoint público de Soroban.
Credentials, y el arm que firma el buyer
Las authorization entries de Soroban llevan un tipo de credential, y cuál usa el buyer cambia a qué se compromete la firma. El CAP-71 — status final, con preámbulo que nombra el protocolo 27 — se divide en delegation (71-01) y SOROBAN_CREDENTIALS_ADDRESS_V2 (71-02), un credential address-bound cuyo preimage ata la firma a la dirección.
La spec de x402 exige que los facilitators acepten tanto el arm legacy sorobanCredentialsAddress como el V2, y que rechacen credentials de source-account y delegados. El changelog de 2.25.0 describe el V2 activándose en la red con el protocolo 28 y nota que los clients de stellar-sdk v17 emiten entries firmadas con V2 por defecto, que versiones anteriores rechazaban de plano. La implementación maneja ambos a través de un único accessor y sigue rechazando addressWithDelegates.
En nuestros propios pagos sobre protocolo 29, con el SDK 16.3.1, la entry emitida fue el arm legacy — coincidiendo con la nota de la spec de que la simulación en modo recording sigue devolviendo V1. El requisito de aceptar ambos no es teórico, igual: es lo que mantiene funcionando a un facilitator v16 cuando un buyer actualiza su SDK.
Que los credentials delegados estén rechazados vale señalarlo para el caso de agentes. La delegación es justamente la primitiva que uno querría para "este agente puede autorizar pagos en nombre de esta cuenta", y el scheme la excluye por regla, no por descuido.
El flow upfront que todavía nadie usa
Stellar es uno de los schemes que declara más de un payment flow. Su server scheme soporta authorization y upfront, con default authorization.
La especificación v2 define upfront como settle, después recurso, después responder — sin verify en el ordenamiento, con la validez establecida por el settlement. Existe para redes sin primitiva de pull-settlement, e invierte exactamente el riesgo que medimos arriba: el seller obtiene finalidad antes de hacer el trabajo, y el buyer carga la exposición.
La spec también dice que cuando un recurso ofrece ambos, los clients deberían preferir authorization, y que extra.paymentFlow debe estar presente en el 402 cuando el flow resuelto no sea authorization. Para quien vende trabajo caro y lento, upfront es el flow que elimina la ventana entre verify y settle por completo — al costo de pedirle al buyer que pague antes de ser servido.
Quién settlea esto realmente en mainnet
Un scheme es tan real como los facilitators que lo corren, así que probamos los endpoints públicos /supported y después fuimos al ledger.
El facilitator de referencia en x402.org/facilitator responde 200 y publica exactamente una entrada de Stellar: exact en stellar:testnet, con areFeesSponsored: true. No hay ningún kind stellar:pubnet en su respuesta. Para mainnet, la propia documentación de Stellar apunta a OpenZeppelin Channels, cuya key de testnet es self-service y cuya key de mainnet "requires GitHub OAuth". Su /x402/supported devuelve 401 sin una, así que su conjunto de signers no es legible públicamente.
Lo que sí es legible es el ledger. Una cuenta de fees en mainnet — que ningún /supported público de los que probamos lista, y que no atribuimos a ningún operador — viene haciendo fee-bump de pagos Soroban desde el 2026-03-06. Bajamos su historial completo de transacciones de Horizon y recalculamos todo por nuestra cuenta.
Tiene 2,658 transacciones hasta el 2026-10-12, de las cuales 2,589 son fee-bumps, cada una con exactamente un invokeHostFunction. Exactamente una de las 2,589 falló. El asset es USDC en 2,580 de ellas, con un transfer de USDT0 y siete sobre XLM nativo. A lo largo de 2,588 transfers hubo 46 payers distintos y 29 payees.
Los fees corroboran de cerca nuestra medición en testnet. Sobre esos 2,589 settlements de mainnet, fee_charged fue de 18,977 a 29,102 stroops con una mediana de 23,574 — contra los 23,066 que medimos en testnet. Gasto total de fees en toda su vida: 5.97 XLM. Ninguno cruzó el techo de 50,000 stroops, aunque el peor caso consumió el 58% de él, que es el problema de margen de antes visto sobre tráfico real y no en una simulación.
El volumen es la parte que vale detenerse a mirar. 2,319 de los 2,588 transfers fueron por exactamente 0.001 USDC. Este es un rail en fase de pruebas, no un rail moviendo comercio — que es el contexto honesto para todas las preguntas de diseño de arriba. Los modos de falla que medimos son baratos de arreglar ahora precisamente porque casi nadie depende todavía de ellos.
Qué significa para LLM4Agents
Nuestro gateway publica hoy una sola opción de pago. Un request sin autenticar a /v1/chat/completions devuelve un 402 cuyo accepts[0] es exact en eip155:8453, USDC, maxTimeoutSeconds: 300, sin clave paymentFlow — o sea el ordenamiento authorization por defecto. Stellar no está ahí.
Si estuviera, ese 300 dejaría de ser segundos y pasaría a ser 60 ledgers, contados desde la altura que haya reportado el RPC del buyer. Todo lo que pase entre verify y settle tiene que caber ahí. Nuestras mediciones dicen que el overhead del protocolo es modesto: firmar tomó entre 1.2 y 3.6 segundos, verificar entre 1.0 y 1.7, y settlear entre 4.2 y 6.7 incluyendo el polling de confirmación, con la transacción entrando dos ledgers después de la firma. Digamos 10 segundos de overhead contra un presupuesto de 300.
Queda la completion. Una generación larga puede correr durante minutos, y en el flow por defecto se ubica dentro de la vida de la authorization. La conclusión honesta es que un presupuesto de 300 segundos es cómodo para completions cortas e incómodo para largas — y que la palanca correcta no es un maxTimeoutSeconds más grande sino el flow. Es el mismo problema de ventana de facturación que tiene nuestro quote, que debemos emitir antes de conocer el conteo real de tokens; ver la auditoría de tokenizer drift para cuán equivocada puede estar esa estimación.
La aritmética de fees también cambia el piso. En Base, settlear un pago cuesta una fracción de centavo de gas. En Stellar el sponsor paga alrededor de 23,000 stroops de fee real por settlement sin importar si el pago es de un dólar o de un centésimo, y carga el riesgo del techo. Nuestro quote más barato es $0.01. Un rail de Stellar por llamada a ese precio solo funciona si el fee del facilitator se amortiza — que es un argumento a favor de batching, no de un rail por llamada.
El lado de la amenaza es más angosto de lo que parece. Nada de lo que encontramos mueve dinero incorrectamente. Lo que encontramos cuesta disponibilidad: pagos que verifican y no pueden settlear, pagos correctos rechazados por desfase de RPC, y clases enteras de buyer — contract accounts con topes — que no pueden pagar en absoluto. Para un gateway, las fallas de disponibilidad en la ruta de pago son indistinguibles de estar caído.
Cómo mantenerse en la frontera
Concretamente, y en este orden.
Primero, agregar una regla de vida mínima restante donde sea que aceptemos pagos, en cualquier chain. Nuestro 402 promete 300 segundos; un payload que llega con tres segundos de validez debería rechazarse antes de gastar una GPU, no después. Esto es barato en EVM hoy y es el único chequeo que habría atrapado los dos casos de verify-pasa-settle-falla.
Segundo, tratar el plazo de pago como un presupuesto que gastamos, no como una constante que publicamos. Deberíamos medir nuestra propia latencia de completion por modelo y fijar maxTimeoutSeconds desde la distribución observada en vez de un número redondo — y negarnos a empezar trabajo que no podamos plausiblemente terminar y settlear dentro de la ventana restante.
Tercero, prototipar upfront antes de prototipar una chain nueva. Ya está declarado como soportado por el server scheme de Stellar y definido en la spec v2, elimina la ventana por completo para recursos lentos, y es el flow que hace seguro vender inferencia cara a buyers anónimos. El lugar correcto para ofrecerlo son nuestros modelos más caros.
Cuarto, cuando agreguemos un rail no-EVM, publicar la codificación contra la que realmente vamos a comparar. El caso de la dirección muxed es un recordatorio de que payTo es una comparación de strings contra algo que una chain decidió emitir. Cualquier binding de red nuevo que soportemos debería venir con un test que firme un pago real y verifique que el evento coincide, no solo que la dirección pasa un regex.
Quinto, mantener la ruta de contract accounts en la watchlist y no en el roadmap. Agentes pagando bajo topes on-chain es la configuración que queremos servir, y en Stellar está bloqueada por los issues abiertos #3352, #3764, #3515 y #3158. Cuando cierren, el cálculo cambia; hasta entonces un rail de Stellar sirve keypairs, y las keypairs son los buyers que menos necesitan una chain nueva.
Sexto, instrumentar la diferencia entre verify y settle como métrica de primera clase. Todo scheme que auditamos tiene una ventana entre ambos, y las fallas interesantes viven todas ahí. Deberíamos conocer nuestra propia tasa de pagos verificados-y-no-settleables por red antes de agregar una red donde esa tasa es más difícil de controlar.
Paga por llamada, en stablecoins, sobre una API compatible con OpenAI
Un 402, una firma, 345+ modelos. Sin suscripción, sin mínimo.
Registra tu agente