Repository navigation
Support 'upto' scheme #71
Description
Activity
Really thorough issue, thank you @jeesunikim. It made this easy to answer concretely. I'm building a Stellar x402 facilitator and wanted both exact and upto. exact was the straightforward one; upto is the one worth talking about, and it's exactly this. I have it running end to end on testnet, settling real USDC, and wrote the fuller design up in #72.
I'll take your Q4 first, because it drives the rest, and because I started exactly where you did.
Q4, proxy or no proxy. I wanted the no-proxy answer too. Raw SEP-41
approve/transfer_fromis genuinely simpler than EVM's two contracts, and if it held it would be the better design. I worked it through againstscheme_upto.md's MUSTs, and two of them break with a bare allowance:- Recipient binding. Your table maps this to "
tobound in the authorized invocation args", but the invocation the client authorizes isapprove(from, spender, max, live_until_ledger), and it has noto. The recipient is chosen later, by the spender, attransfer_fromtime. So the client never bindsto, and a compromised or dishonest facilitator cantransfer_fromto any address. This is the MUST I couldn't close without a contract. - Single-use. The Soroban auth-entry nonce protects the
approvefrom replay, but not the number of draws. Once the allowance exists,transfer_fromis authorized by the spender (the facilitator), not the client, so the facilitator can draw it across several calls up tomax. That is multi-settlement, whichuptolists as out of scope. The nonce guards theapprove, not the allowance.
Your table half-anticipates this in the "belt-and-braces check in a settle proxy" line. I think that proxy is load-bearing rather than belt-and-braces: it's the only place
toand single-use can be bound to the client's signature. So I landed on a small contract after all, the same conclusion EVM reached, though the Stellar version is much smaller becauselive_until_ledgerand auth-entry nonces do the time-bound and replay work Permit2 hand-rolls.Q1, who submits
approveand does it cost an extra tx. This is your option (b): a settle-proxy the client authorizes once. The client signs a single authorization over the settle call, with the recipient, ceiling, expiry and nonce bound; the contract doesapprovethentransfer_frominternally; and it is one submitted transaction. So the two-ops-versus-one cost you flagged doesn't materialize, it's the same single settled tx asexacton the hot path. I ruled out (a), two sequenced txs, for exactly the cost reason you gave, and (c) collapses into (b) here since theapproverides as a sub-invocation under the one settle authorization.Q2, fee sponsorship. Holds, unchanged from
exact. The facilitator re-sources the transaction to itself and pays, and the payer needs zero XLM. Proven in real USDC with the fee charged to the facilitator.Q3, allowance hygiene. I rely on
live_until_ledgerplus the consumed settle nonce rather than zeroing the residual. The leftover (max - actual) is an allowance to the contract, and only the contract can draw it, only inside asettle()call, which needs a fresh client signature. The original settle nonce is already consumed, so the same authorization can't re-trigger it, and it lapses atlive_until_ledgerregardless. Zeroing would cost a second signedapproveon every request for no security gain. Glad to walk through that if you want to poke at it.That covers your acceptance criteria: verify/settle enforce
actual ≤ max(on-ledger and facilitator-side),/supportedadvertises{scheme: "upto", network: "stellar:testnet"}, the settle response carries the actualamount, and overspend is a coded rejection. Proof, partial settlements with over-cap and replay refused in the same run: one in real USDC, and one from an OpenZeppelin smart account.And your exact acceptance-criteria case, authorize $0.10 then settle $0.007 in real USDC: testnet payment (0.007 to the seller, fee sponsored by the facilitator). To read that one, the contract entry point is
settle(token, from, to, max_amount, expiration_ledger, nonce, actual_amount), so the on-chain callsettle(USDC, payer, seller, 1000000, ..., 70000)shows the ceiling (max_amount1000000, i.e. $0.10) and the settled amount (actual_amount70000, i.e. $0.007) side by side, with the recipient bound as thetoargument.A couple of things I'd genuinely love your thoughts on, whenever you have a moment
validAfter. The spec wants bothvalidAfteranddeadline. I havedeadlinevialive_until_ledgerand the auth entry'ssignatureExpirationLedger, but Soroban has no native "not valid before", so I treatvalidAfteras off-chain verify-time policy. Do you thinkdeadline-only is okay for Stellar, or would a real start-time be worth having?- The overall shape. If you'd approach any of it differently, or see something I've missed, I'd really like to hear it. This is the first Stellar
uptoI know of, so a second pair of eyes from you would mean a lot.
- Recipient binding. Your table maps this to "
Some usage numbers, in case they help with prioritisation —
uptois easy to treat as
theoretical, and it is not.From the public Bazaar discovery index (14,675 resources, pulled today), counting payment options
across all advertisedaccepts[]entries:scheme offers exact28,230 exact_cosmos_authz126 upto124 batch-settlement50 others ( onchain,aggr_deferred,agent-pay, …)67 uptois on 84 resources, and those resources settled 4,460 paid calls in the last 30
days — so it is not a scheme waiting for its first user. Where it runs:85 eip155:8453 (Base) 19 eip155:137 (Polygon) 19 eip155:42161 (Arbitrum) 1 solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpTwo things that bear on your issue:
Stellar is not a small constituency here. 564 resources in that index advertise a Stellar
network — none withupto. So the gap you describe is between an established chain presence and a
scheme that demonstrably carries real traffic elsewhere.Your table says
uptois EVM-only, and there is one counterexample in the wild. A resource
advertisesuptoon Solana mainnet. Either a Solana binding exists that thespecs/schemes/upto/
listing does not reflect, or that offer is non-conforming and no client can settle against it.
Worth resolving before treating "EVM only" as the baseline — it changes whether a Stellar binding
is the first non-EVM one or the second.One caveat on the call counts:
l30DaysTotalCallsis the index's own settlement counter. I
checked it against Base chain data on six resources whosepayToserves exactly one resource —
all 478 incoming transfers over 30 days were EIP-3009, and the counter read about 9% low, never
high. Good enough for orders of magnitude, not for exact revenue.Unrelated but adjacent, since it cost me a public correction: in
uptotheamountfield is the
authorised ceiling, not the price of a call. I read it as a price, compared it against other
services' per-call prices, and published that one of the largest receivers on the network was
charging an absurd amount. Whatever Stellar binding lands, that distinction is worth making
loud in the docs — it is the first thing an integrator gets wrong.We're preparing a Stellar
Uptospec draft and landed on the same answers, so sharing them here.Q4 (proxy or no proxy): a contract is required A bare allowance can't bind the recipient (
approvehas noto) and can't enforce single settlement (one allowance allows multipletransfer_fromdraws). Both are MUSTs inscheme_upto.md, and both EVM and SVM ended up with a contract for the same reason.Q1: no extra transaction the approve rides as a sub-invocation under the one signed settle call.
Q2: we verified the sponsored flow on testnet for exact with this repo's facilitator and the upstream e2e suite (hashes:
6f5d4709...4a6b,597d2965...e0033), and #72's settled transactions demonstrate it carries over to upto unchanged the facilitator re-sources and pays either way.One addition to the design: the spec requires a validAfter start-time Soroban has no native one, but the contract can bind
valid_after_ledgeras a signed argument and check it insettle(). One comparison, and both time bounds stay under the client's signature.We would rather converge on one Stellar Upto spec than write two happy to coordinate.
@bomanaps and I have been working through the Stellar upto design and have opened a PR with the proposed Stellar binding:
We’ve also implemented and tested the proposed flow on Stellar testnet, with the test results included in the PR:
x402-foundation/x402#3134 (comment)The approach uses a minimal settlement contract so the client can authorize the ceiling while the actual settlement amount remains variable, with the recipient, asset, validity bounds, and authorization bound by Soroban auth.
Settlement stays a single facilitator-sponsored transaction and does not require the payer to hold XLM.
To answer the open questions: approve -> transfer_from-> optional revoke happens atomically inside settle(), so there is no extra transaction. The facilitator submits and sponsors the transaction, preserving zero-XLM settlement for the payer. autoRevoke can clear any remaining allowance; otherwise it expires at expirationLedger. We use a minimal stateless settlement contract because raw SAC calls cannot authorize an unknown final amount while enforcing the signed ceiling.
Would appreciate feedback, especially around the authorization shape and settlement contract design.
Summary
x402's
uptoscheme lets a client authorize a maximum spend and the server settle only actual usage. It's the scheme for metered APIs where cost isn't known until after execution — LLM inference, search, per-byte transfer. Stellar cannot express it today.@x402/stellarcurrently supportsexactwhich sends the exact amount via SAC'stransfer(from, to, amount).Proposed mechanism
Stellar has the equivalent primitive natively in the SEP-41 token interface:
approve(max)→transfer_from(actual).live_until_ledgergives native authorization expiry. EVM's Permit2 encodes deadlines manually.scheme_upto.mdrequires each network binding to define four things; proposed Stellar answers:validAfter/deadline)live_until_ledger+ auth entrysignatureExpirationLedgertobound in the authorized invocation argsOpen design questions
approve, and does it cost an extra transaction? This is the crux. Ifapproveneeds its own submitted tx,uptocosts 2 on-chain ops vsexact's 1, which may erase the benefit for small payments. Options: (a) facilitator submits both in sequence, (b) a Soroban settle-proxy the client authorizes once that internally doestransfer_fromwith an enforced ceiling, (c) batch both into one transaction with multiple auth entries.exacton Stellar advertisesareFeesSponsored: true. Need to confirm the allowance flow preserves zero-XLM-for-payer.transfer_fromleave a dangling allowance?live_until_ledgerbounds it, but zeroing on settle may be cleaner.approve/transfer_frommay suffice, avoiding a deployed contract entirely — a meaningful simplification over EVM if it holds.Acceptance criteria (this repo)
examples/facilitatorverifies anuptopayload and settlesactualAmount ≤ authorizedMax/supportedadvertises{scheme: "upto", network: "stellar:testnet"}amountwith the actual settled valueactualAmount > authorizedMaxwith a spec-conformant errorWritten with Claude