RialtoRialto
Design Decisions

ADR 0001: On-chain cap for upto

Why the upto spending cap is enforced by a Soroban contract.

Status: accepted internally · 2026-08-08 · upstream spec contribution in preparation

Context

The x402 upto scheme lets a client authorize a maximum spend while the actual amount is decided at settlement from real usage. The generic spec (scheme_upto.md) makes five properties mandatory: single-use authorization, time bounds (validAfter and deadline), recipient binding, max-amount enforcement, and phase-dependent amount semantics. No Stellar profile of upto exists yet.

On Stellar, a Soroban authorization entry signs an exact invocation with exact arguments - but upto's defining feature is an amount unknown at signature time. Something on-chain must hold the signed maximum and accept any actual ≤ max.

Decision

Ship a minimal, stateless Soroban settlement contract with a single entrypoint. The client signs one authorization binding (recipient, token, max amount, validity window, salt); the actual amount is an unsigned argument supplied at settlement; the contract enforces actual ≤ max and pays out atomically in one transaction. Settlement is facilitator-agnostic: the generic spec mandates recipient binding, not submitter binding, and leaving the submitter open is what the federation and self-facilitation paths require - a leaked authorization can at worst settle the signed maximum to the signed recipient.

  • Single use comes free from Soroban's protocol-level auth-entry nonce - consumed on-chain when used, no contract storage needed (and therefore no rent/TTL management).
  • Expiry is a single client-chosen, signed expirationLedger (ledger sequence) checked by the contract - the same signed value feeds the token approval's expiry, so no timestamp-to-ledger conversion exists to mismatch the pre-signed arguments.
  • validAfter has no native Soroban primitive, so it is a signed clock-time argument the contract checks against the ledger timestamp - a real "not before" bound under the client's signature rather than off-chain policy.
  • Zero usage ⇒ no transaction; the authorization simply expires.

Alternatives considered

  • SEP-41 allowances only (approve/transfer_from), no contract - rejected. The approval names only the spender, so the client never binds the recipient; and one allowance supports multiple draws, so single settlement cannot be guaranteed. Two of the spec's five MUSTs fail. (Note the accepted design does use an allowance - but granted only to the contract, spent and optionally zeroed inside one atomic settlement, which is a different thing from handing a standing allowance to a facilitator.)
  • Escrow (deposit-then-settle, SVM style) - rejected. Two transactions and locked capital where Stellar's auth entries allow single-transaction atomicity, plus contract state (rent/TTL) and drift toward payment-channel territory covered elsewhere.

Both existing profiles reached the same conclusion by different roads: EVM enforces the cap with the Permit2 proxy contract, SVM (draft) with an escrow program. The public design discussions in stellar/x402-stellar#71 and #72 converge on on-chain enforcement as well.

Consequences

  • A small Rust contract (~one entrypoint) enters audit scope before mainnet.
  • The signed-argument design makes smart-account spending policies straightforward: a __check_auth policy can inspect exactly what the client signs (token, max, recipient), so per-request caps (this contract) compose with per-agent budgets (account policies).
  • We author scheme_upto_stellar.md and contribute it upstream, coordinating with the authors already active in #71/#72. The spec lives in this repo: docs/scheme_upto_stellar.md (both review fixes applied - client-signed expirationLedger as the single expiry, required salt).

On this page