Skip to content

Support 'upto' scheme #71

Description

@jeesunikim

Summary

x402's upto scheme 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/stellar currently supports exact which sends the exact amount via SAC's transfer(from, to, amount).

Proposed mechanism

Stellar has the equivalent primitive natively in the SEP-41 token interface:

fn approve(env: Env, from: Address, spender: Address, amount: i128, live_until_ledger: u32);
fn transfer_from(env: Env, spender: Address, from: Address, to: Address, amount: i128);

approve(max) → transfer_from(actual). live_until_ledger gives native authorization expiry. EVM's Permit2 encodes deadlines manually.

scheme_upto.md requires each network binding to define four things; proposed Stellar answers:

Requirement Proposed Stellar mechanism
Replay protection (Permit2-nonce equivalent) Soroban auth entry nonce
Time-bound (validAfter / deadline) live_until_ledger + auth entry signatureExpirationLedger
Recipient binding to bound in the authorized invocation args
Settlement ≤ authorized max Enforced by allowance semantics; belt-and-braces check in a settle proxy

Open design questions

  1. Who submits approve, and does it cost an extra transaction? This is the crux. If approve needs its own submitted tx, upto costs 2 on-chain ops vs exact'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 does transfer_from with an enforced ceiling, (c) batch both into one transaction with multiple auth entries.
  2. Does fee sponsorship still hold? exact on Stellar advertises areFeesSponsored: true. Need to confirm the allowance flow preserves zero-XLM-for-payer.
  3. Allowance hygiene. Does a partial transfer_from leave a dangling allowance? live_until_ledger bounds it, but zeroing on settle may be cleaner.
  4. Proxy or no proxy? Raw SAC approve/transfer_from may suffice, avoiding a deployed contract entirely — a meaningful simplification over EVM if it holds.

Acceptance criteria (this repo)

  • examples/facilitator verifies an upto payload and settles actualAmount ≤ authorizedMax
  • /supported advertises {scheme: "upto", network: "stellar:testnet"}
  • Settle response populates amount with the actual settled value
  • Rejects actualAmount > authorizedMax with a spec-conformant error
  • Testnet demo: authorize $0.10, settle $0.007, verify on-chain

Written with Claude

Activity

  1. tolgayayci commented on Aug 3, 2026

    @tolgayayci

    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_from is genuinely simpler than EVM's two contracts, and if it held it would be the better design. I worked it through against scheme_upto.md's MUSTs, and two of them break with a bare allowance:

    • Recipient binding. Your table maps this to "to bound in the authorized invocation args", but the invocation the client authorizes is approve(from, spender, max, live_until_ledger), and it has no to. The recipient is chosen later, by the spender, at transfer_from time. So the client never binds to, and a compromised or dishonest facilitator can transfer_from to any address. This is the MUST I couldn't close without a contract.
    • Single-use. The Soroban auth-entry nonce protects the approve from replay, but not the number of draws. Once the allowance exists, transfer_from is authorized by the spender (the facilitator), not the client, so the facilitator can draw it across several calls up to max. That is multi-settlement, which upto lists as out of scope. The nonce guards the approve, 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 to and 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 because live_until_ledger and auth-entry nonces do the time-bound and replay work Permit2 hand-rolls.

    Q1, who submits approve and 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 does approve then transfer_from internally; 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 as exact on the hot path. I ruled out (a), two sequenced txs, for exactly the cost reason you gave, and (c) collapses into (b) here since the approve rides 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_ledger plus 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 a settle() 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 at live_until_ledger regardless. Zeroing would cost a second signed approve on 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), /supported advertises {scheme: "upto", network: "stellar:testnet"}, the settle response carries the actual amount, 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 call settle(USDC, payer, seller, 1000000, ..., 70000) shows the ceiling (max_amount 1000000, i.e. $0.10) and the settled amount (actual_amount 70000, i.e. $0.007) side by side, with the recipient bound as the to argument.

    A couple of things I'd genuinely love your thoughts on, whenever you have a moment

    1. validAfter. The spec wants both validAfter and deadline. I have deadline via live_until_ledger and the auth entry's signatureExpirationLedger, but Soroban has no native "not valid before", so I treat validAfter as off-chain verify-time policy. Do you think deadline-only is okay for Stellar, or would a real start-time be worth having?
    2. 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 upto I know of, so a second pair of eyes from you would mean a lot.
  2. Nikolife2016 commented on Aug 4, 2026

    @Nikolife2016

    Some usage numbers, in case they help with prioritisation — upto is 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 advertised accepts[] entries:

    scheme offers
    exact 28,230
    exact_cosmos_authz 126
    upto 124
    batch-settlement 50
    others (onchain, aggr_deferred, agent-pay, …) 67

    upto is 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:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp
    

    Two things that bear on your issue:

    Stellar is not a small constituency here. 564 resources in that index advertise a Stellar
    network — none with upto. So the gap you describe is between an established chain presence and a
    scheme that demonstrably carries real traffic elsewhere.

    Your table says upto is EVM-only, and there is one counterexample in the wild. A resource
    advertises upto on Solana mainnet. Either a Solana binding exists that the specs/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: l30DaysTotalCalls is the index's own settlement counter. I
    checked it against Base chain data on six resources whose payTo serves 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 upto the amount field 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.

  3. bomanaps commented on Aug 8, 2026

    @bomanaps

    We're preparing a Stellar Upto spec 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 (approve has no to) and can't enforce single settlement (one allowance allows multiple transfer_from draws). Both are MUSTs in scheme_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_ledger as a signed argument and check it in settle(). 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.

  4. Iam0TI commented on Aug 12, 2026

    @Iam0TI

    @bomanaps and I have been working through the Stellar upto design and have opened a PR with the proposed Stellar binding:

    x402-foundation/x402#3134

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions