x402 exact on Stellar: the verify-to-settle window
On Stellar, an x402 payment is not a signed transaction. It is a set of Soroban authorization entries with a deadline measured in ledgers, and the facilitator builds the transaction that actually settles it.
Every x402 binding we have audited so far hands the facilitator something it can submit more or less as-is. On EVM, the buyer signs an EIP-3009 authorization and the facilitator wraps it in a call to the token. On SVM, the buyer signs a transaction the facilitator co-signs as fee payer.
Stellar does neither. The buyer signs only the inner authorization, and the facilitator assembles a brand-new transaction around it at settle time: its own account as source, its own sequence number, its own fee. The buyer never picks the transaction that lands.
That design has a consequence worth measuring. The gap between /verify and /settle becomes the load-bearing part of the scheme, because the authorization has a lifetime counted in ledgers that both parties compute independently. For a gateway selling inference — where the protected resource takes seconds to minutes, not milliseconds — that gap is exactly where the work happens.
We read the spec and the reference implementation, then ran real payments on Stellar testnet against the reference facilitator to see where the window opens and closes. Everything below is measured, with the commands and versions stated.
What the buyer actually signs
The exact scheme for Stellar supports x402 v2 only, on stellar:pubnet and stellar:testnet, and covers SEP-41 Soroban tokens only. Classic Stellar assets are explicitly out of scope.
The buyer builds a contract invocation calling transfer(from, to, amount) on the token contract, simulates it to discover the authorization entries, signs those entries, and sends the serialized transaction as base64 XDR. The facilitator decodes it, validates structure, re-simulates, rebuilds, signs and submits.
The verification rules are tight. Exactly one operation, of type invokeHostFunction. The contract address must equal requirements.asset. The function must be named transfer with exactly three arguments: from is the signer, to must equal payTo exactly, amount must equal requirements.amount exactly as an i128. No sub-invocations. And a block of checks the spec marks with three alarm emojis, whose job is to stop the fee sponsor from being steered into paying for something else: the facilitator must not be the transaction source, the operation source, the from address, or present in any authorization entry.
Then the simulation must emit exactly the expected balance change and nothing else. That last rule is stricter than it sounds, and we come back to it.
We ran this end to end with @x402/stellar 2.28.0 and @stellar/stellar-sdk 16.3.1 on testnet, with our own SEP-41 asset and a facilitator we controlled. A payment payload is small: 1,352 base64 characters of XDR for a single transfer.
The clock is a ledger counter, not a wall clock
This is the structural difference from every other binding. On EVM the buyer sets validBefore to now + maxTimeoutSeconds — wall-clock seconds, agreed by everyone who can read a block timestamp. On Stellar the spec says the authorization expiration is currentLedger + ceil(maxTimeoutSeconds / estimatedLedgerSeconds), falling back to 5 seconds per ledger when no network estimate is available.
So the deadline depends on two readings neither party shares: the current ledger height, and how fast ledgers close. The reference client reads the height from Soroban RPC and estimates close time from the last 20 ledgers on Horizon. The facilitator does the same, separately, and rejects the payment if the buyer's expiration exceeds its own computed bound by more than a 2-ledger tolerance.
We measured how stable that estimate actually is. Over 2,000 consecutive pubnet ledgers (64,900,025 through 64,902,024, closing 2026-10-12 06:18:27Z through 09:05:02Z), every single inter-ledger interval was exactly 5 seconds. Same for 1,000 consecutive testnet ledgers. Running the SDK's own 20-ledger average over that window returned 5 seconds for all 1,981 windows on pubnet and all 981 on testnet, with the raw average never deviating from 5.000.
Both networks are on protocol 29 as of that measurement (Horizon 29.0.0, stellar-core 29.0.0 on both pubnet and testnet).
# Every interval over 2,000 pubnet ledgers, 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)]
That is good news for the arithmetic: the divisor is a constant in practice, so maxTimeoutSeconds maps cleanly onto ledgers. Our own payments confirmed it — maxTimeoutSeconds: 300 produced an expiration exactly 60 ledgers out, and maxTimeoutSeconds: 60 produced exactly 12.
What remains fragile is the height, not the rate. Public RPC is load-balanced across nodes that are not always at the same ledger, and the 2-ledger tolerance is the entire margin for that skew. That failure mode is open as issue #3168 in the x402 tracker: when the buyer's read lands on a node ahead of the facilitator's, a correct payment is rejected with invalid_exact_stellar_signature_expiration_too_far, and neither side can tell it apart from a genuinely over-long expiration.
What one settlement costs the sponsor
Fees are entirely the facilitator's problem here. The spec requires it to derive the settlement fee from a fresh simulation at settle time — simulationResourceFee + inclusionBuffer, buffer at least 100 stroops — to refresh the Soroban resource data, and to fully override whatever the buyer bid.
For a plain SEP-41 transfer we measured a minResourceFee of 33,163 stroops. With the default 100-stroop inclusion buffer that is a 33,263-stroop bid, which matched the max_fee on the transaction that landed. After Soroban's unused-resource refund, the actual fee_charged was 23,066 stroops.
Now put that against the implementation's safety ceiling. maxTransactionFeeStroops defaults to 50,000, and verification fails with invalid_exact_stellar_payload_fee_exceeds_maximum above it. So the simplest possible payment already consumes two thirds of the ceiling: 1.50x headroom, and only 16,837 stroops of room to raise the inclusion bid before the ceiling trips.
That matters because the inclusion bid is not optional in practice. PR #3503, merged 2026-09-17 and shipped in 2.27.0, made the inclusion fee configurable precisely because the facilitator always bid the 100-stroop minimum, which mainnet frequently does not include for Soroban transactions — so settlements could time out after a successful verify. The default is still 100. An operator who raises it for mainnet inclusion is spending the same ceiling headroom that a more complex payer needs.
Which is the other half of the problem. Issue #3515 reports the public x402.org facilitator refusing a valid payment from a Soroban smart account because its __check_auth calls another contract, pushing the resource fee past the ceiling. The payer cannot discover the ceiling in advance and cannot change it.
Verify is not a prediction of settle
The spec is explicit that /settle must perform full verification independently. The reference facilitator does exactly that: settle calls _verify first, then re-simulates. So a payload is checked twice, against two different ledger states.
We wanted to know how often the two answers can differ, because in the default authorization flow the protected resource runs between them. The ordering is verify, then resource, then settle, then respond.
Two cases produced a valid verify and a failed settle.
The first is timing. The facilitator checks that the expiration is not too far in the future, but there is no check that it is far enough away to survive the work. A payload whose authorization expires in one ledger verified as valid; twenty seconds later, settle failed at re-simulation with Error(Auth, InvalidInput) and the diagnostic "signature has expired". Same with three ledgers. The EVM side does have a lower bound — its facilitator rejects an authorization whose validBefore is less than six seconds out — and the Stellar path has no equivalent.
# expiration 1 ledger out
verify {"isValid": true, ...}
settle {"success": false,
"errorReason": "invalid_exact_stellar_payload_simulation_failed"}
# diagnostic: "signature has expired", 5154505, 5154501
The second is transaction preconditions. The facilitator rebuilds the transaction with its own source account but copies the buyer's ledgerBounds, extraSigners, minAccountSequence fields and memo across. None of those are in the verification rules, because none of them affect simulation — they are enforced by consensus at submission. We sent three payloads carrying preconditions the rebuilt transaction could not satisfy. All three verified as valid, and all three were rejected by the network at submission with txTooLate, txBadAuth and txBadMinSeqAgeOrGap respectively.
The important part is what did not happen. The scheme fails closed. No funds moved in any case, the buyer's token balance was unchanged, and the facilitator's XLM balance and sequence number were byte-identical before and after all three attempts — a rejected transaction never reaches the ledger, so the sponsor does not even pay the fee. The cost is liveness, not money: wasted RPC round trips for the facilitator, a failed payment for the buyer, and — in the authorization flow — a resource server that has already done the work. That is the same two-phase exposure we measured in the gap between verify and settle, surfacing here through a different mechanism.
The constructive fix is small and local: a minimum-remaining-ledger check at verify, and either rejecting unknown preconditions or dropping them during the rebuild. Both are facilitator-side changes that need no spec revision.
Worth noting that replay is handled properly. Submitting the same settled payload a second time fails at re-simulation with Error(Auth, ExistingValue) and the diagnostic "nonce already exists for address" — Soroban's own authorization nonce does the work, with no facilitator-side nonce store required.
The address that looks valid and is not
Stellar has muxed accounts: a single on-chain account multiplexed into many virtual balances, written as an M-address. A custodian or a platform billing many sellers through one account would reach for exactly that.
The reference implementation's destination regex accepts them. It validates G-accounts, C-accounts and 69-character M-accounts. So a resource server can publish an M-address as payTo, and the buyer's client will happily build and sign the payment: the SDK encodes the destination as scAddressTypeMuxedAccount, and the transfer simulates successfully.
Verification then rejects it with invalid_exact_stellar_payload_event_wrong_to.
The reason is in CAP-67, final since protocol 23. When the destination is muxed, the SAC does not put the M-address in the event. It emits the underlying G-address in the topics and moves the multiplexing id into the data field, which becomes a map instead of a plain amount. We confirmed the shape on testnet:
topics: ['transfer', 'GCD7ACC…KTEW', 'GDZGXPC…O7XG', 'XUSD:GDYEUQN…5EVW']
data: {"amount": "10000000", "to_muxed_id": "4242"}
The facilitator compares the event's to topic against requirements.payTo as a string. G-address versus M-address, so it never matches. The payment is correct, the money would go to the right account, and the check is comparing two different encodings of the same destination — the same class of mismatch we found in interoperable address encodings versus payTo.
For anyone building on this today the lesson is operational: publish the G-address, carry the multiplexing id somewhere else, and do not trust the destination validator to tell you what the facilitator will accept.
Which payers the scheme can actually serve
The interesting x402 buyers are not single keypairs. They are agents paying under an on-chain spending cap, which on Stellar means a Soroban custom account. Three open issues say that class is largely blocked today.
Issue #3352 and issue #3764 both target the same check: validateSimulationEvents treats every contract-type event as though it must be the transfer, and rejects the first one that is not. A smart account whose authorization logic emits its own event therefore fails verification. #3764, filed 2026-10-10, reports this against OpenZeppelin's spending_limit policy, which emits spending_limit_enforced on every transfer it authorizes — reproduced against both the reference facilitator at 2.28.0 and OZ Channels on testnet.
So the exact configuration an agent operator wants — a wallet with an on-chain per-period cap — is the one that cannot pay. Add #3515's fee ceiling and #3158, where the client scheme forces an ed25519 signature shape, and the practical answer today is that Stellar exact serves plain G-account keypairs well and contract accounts poorly.
Client-side spend controls are better. Since 2.23.0 the package ships DEFAULT_ASSETS plus a default policy of recognized pegged assets only with a $1 cap per payment, opt-in allowedAssets with integer atomic per-asset ceilings, and spendControls: false to disable. Pubnet USDC is the single default asset at 7 decimals. That is the same default-cap pattern we examined in x402 spend controls, and it behaves the same way here.
One deployment detail that will catch operators: the package has no default mainnet RPC. getRpcUrl throws for stellar:pubnet unless you pass one. Testnet defaults to the public Soroban endpoint.
Credentials, and the arm the buyer signs
Soroban authorization entries carry a credential type, and which arm the buyer uses changes what the signature commits to. CAP-71 — status final, preamble naming protocol 27 — splits into delegation (71-01) and SOROBAN_CREDENTIALS_ADDRESS_V2 (71-02), an address-bound credential whose preimage binds the signature to the address.
The x402 spec requires facilitators to accept both the legacy sorobanCredentialsAddress arm and the V2 arm, and to reject source-account and delegated credentials. The 2.25.0 changelog describes V2 activating on the network with protocol 28 and notes that stellar-sdk v17 clients emit V2-signed entries by default, which earlier versions rejected outright. The implementation handles both through a single accessor and still refuses addressWithDelegates.
In our own payments on protocol 29, with SDK 16.3.1, the entry emitted was the legacy arm — matching the spec's note that recording-mode simulation still returns V1. The dual-accept requirement is not theoretical, though: it is what keeps a v16 facilitator working when a buyer upgrades their SDK.
Delegated credentials being rejected is worth flagging for the agent case. Delegation is precisely the primitive you would want for "this agent may authorize payments on behalf of this account", and the scheme excludes it by rule, not by oversight.
The upfront flow nobody is using yet
Stellar is one of the schemes that declares more than one payment flow. Its server scheme supports authorization and upfront, defaulting to authorization.
The v2 specification defines upfront as settle, then resource, then respond — no verify in the ordering at all, with validity established by settlement. It exists for networks with no pull-settlement primitive, and it inverts exactly the risk we measured above: the seller gets finality before doing the work, and the buyer carries the exposure instead.
The spec also says that when a resource offers both, clients should prefer authorization, and that extra.paymentFlow must be present on the 402 whenever the resolved flow is not authorization. For a seller of expensive, slow work, upfront is the flow that removes the verify-to-settle window entirely — at the cost of asking buyers to pay before they are served.
Who actually settles this on mainnet
A scheme is only as real as the facilitators running it, so we probed the public /supported endpoints and then went to the ledger.
The reference facilitator at x402.org/facilitator answers 200 and advertises exactly one Stellar entry: exact on stellar:testnet, with areFeesSponsored: true. There is no stellar:pubnet kind in its response. For mainnet, Stellar's own documentation points at OpenZeppelin Channels, whose testnet key is self-service and whose mainnet key "requires GitHub OAuth". Its /x402/supported returns 401 without one, so its signer set is not publicly readable.
What is publicly readable is the ledger. One mainnet fee account — which no public /supported response we probed lists, and which we are not attributing to any operator — has been fee-bumping Soroban payments since 2026-03-06. We pulled its full transaction history from Horizon and recomputed everything ourselves.
It has 2,658 transactions through 2026-10-12, of which 2,589 are fee-bumps, each carrying exactly one invokeHostFunction. Exactly one of the 2,589 failed. The asset is USDC in 2,580 of them, with one USDT0 transfer and seven on native XLM. Across 2,588 transfers there were 46 distinct payers and 29 payees.
The fees corroborate our testnet measurement closely. Over those 2,589 mainnet settlements, fee_charged ran from 18,977 to 29,102 stroops with a median of 23,574 — against the 23,066 we measured on testnet. Total lifetime fee spend: 5.97 XLM. None of them crossed the 50,000-stroop ceiling, though the worst case consumed 58% of it, which is the headroom problem from earlier seen on real traffic rather than in a simulation.
The volume is the part worth sitting with. 2,319 of the 2,588 transfers were for exactly 0.001 USDC. This is a rail in its testing phase, not a rail carrying commerce — which is the honest context for every design question above. The failure modes we measured are cheap to fix now precisely because almost nobody is depending on them yet.
What it means for LLM4Agents
Our gateway advertises one payment option today. An unauthenticated request to /v1/chat/completions returns a 402 whose accepts[0] is exact on eip155:8453, USDC, maxTimeoutSeconds: 300, with no paymentFlow key — so the default authorization ordering. Stellar is not in there.
If it were, that 300 would stop being seconds and start being 60 ledgers, counted from whatever height the buyer's RPC reported. Everything that happens between verify and settle has to fit inside it. Our measurements say the protocol overhead is modest: signing took 1.2 to 3.6 seconds, verification 1.0 to 1.7 seconds, and settlement 4.2 to 6.7 seconds including confirmation polling, with the transaction landing two ledgers after signing. Call it 10 seconds of overhead against a 300-second budget.
That leaves the completion. A long generation can run for minutes, and it sits inside the authorization's lifetime in the default flow. The honest conclusion is that a 300-second budget is comfortable for short completions and uncomfortable for long ones — and that the right lever is not a bigger maxTimeoutSeconds but the flow. This is the same billing-window problem as our quote, which we have to issue before we know the true token count; see the tokenizer drift audit for how wrong that estimate can be.
The fee arithmetic also changes the floor. On Base, settling a payment costs a fraction of a cent of gas. On Stellar the sponsor pays roughly 23,000 stroops of actual fee per settlement regardless of whether the payment is a dollar or a hundredth of one, and it carries the ceiling risk. Our cheapest quote is $0.01. A per-call Stellar rail at that price point only works if the facilitator's fee is amortized — which is an argument for batching, not for a per-call rail.
The threat side is narrower than it looks. Nothing we found moves money incorrectly. What we found costs availability: payments that verify and cannot settle, correct payments rejected over RPC skew, and whole classes of buyer — contract accounts with spending caps — that cannot pay at all. For a gateway, availability failures on the payment path are indistinguishable from downtime.
Staying on the frontier
Concretely, and in this order.
First, add a minimum-remaining-lifetime rule wherever we accept payment, on any chain. Our 402 promises 300 seconds; a payload arriving with three seconds of validity left should be rejected before we spend a GPU on it, not after. This is cheap on EVM today and it is the single check that would have caught both of our verify-passes-settle-fails cases.
Second, treat the payment deadline as a budget we spend, not a constant we publish. We should measure our own completion latency per model and set maxTimeoutSeconds from the observed distribution rather than a round number — and refuse to start work we cannot plausibly finish and settle inside the remaining window.
Third, prototype upfront before we prototype a new chain. It is already declared supported by the Stellar server scheme and defined in the v2 spec, it removes the window entirely for slow resources, and it is the flow that makes expensive inference safe to sell to anonymous buyers. The right place to offer it is our most expensive models.
Fourth, when we do add a non-EVM rail, publish the encoding we will actually compare against. The muxed-address case here is a reminder that payTo is a string comparison against something a chain decided to emit. Any new network binding we support should ship with a test that signs a real payment and asserts the event matches, not just that the address passes a regex.
Fifth, keep the contract-account path on the watchlist rather than the roadmap. Agents paying under on-chain caps is the configuration we want to serve, and on Stellar it is blocked by open issues #3352, #3764, #3515 and #3158. When those close, the calculus changes; until then a Stellar rail serves keypairs, and keypairs are the buyers least in need of a new chain.
Sixth, instrument the difference between verify and settle as a first-class metric. Every scheme we have audited has a window between the two, and the interesting failures all live in it. We should know our own rate of verified-then-unsettleable payments per network before we add a network where that rate is harder to control.
Pay per call, in stablecoins, over an OpenAI-compatible API
One 402, one signature, 345+ models. No subscription, no minimum.
Register your agent