upto is the x402 scheme for resources whose price is known only after they are served: tokens generated,
rows returned, seconds of compute. The buyer authorizes a cap. The seller serves the request, then settles the
amount it actually used, anything from zero up to that cap. The buyer can never be charged more than the cap,
by the seller, the facilitator or anyone who sees the payment.
A Rail402 service serves upto next to exact on stellar:testnet by default (TESTNET_UPTO_ENABLED), with
the canonical settlement contract. On pubnet it is off until the operator enables it and names a contract (see
configuration). Buyers pay upto resources with the buyer SDK,
the MCP server or the @rail402.dev/upto client; sellers offer them with the
seller helpers.
How it works on Stellar
Anexact payment is a signed token transfer of a fixed amount. That cannot work for upto: the amount is
not known when the buyer signs. Stellar’s Soroban authorization framework lets a signer authorize a contract
call with exactly the arguments it will receive, so upto moves the choice of amount into a small settlement
contract that enforces the cap.
1
The seller asks for payment
The resource answers
402 with an upto option: the cap as amount, the token as asset, payTo, and
extra.facilitatorAddress, the facilitator’s settlement authority, copied from its /supported.2
The buyer signs one authorization entry
The buyer signs terms naming the recipient, token, cap, settlement authority and an expiration ledger: one
Soroban authorization entry for
settle(terms) on the settlement contract, with one nested call,
token.approve(payer, settlement contract, cap, expiration). The buyer needs no XLM and submits nothing.3
The facilitator verifies the cap
/verify checks every rule that needs no network access, then simulates the settlement for the full cap:
the signature, a smart account’s own checks, the buyer’s balance and trustline all run as they would on
chain.4
The seller serves and names the amount
The seller serves the request and sets the amount it used, with
setSettlementOverrides of the x402
resource server or the seller helpers’ setUptoCharge.5
The facilitator settles the amount
/settle checks the payment again for that amount and submits settle(payer, terms, amount). The
contract requires the buyer’s signed terms and the settlement authority’s authorization of the amount,
checks 0 ≤ amount ≤ cap, and pulls exactly amount from the buyer to payTo. The facilitator’s sponsor
pays the fee.What goes over the wire
GET /supported lists an upto kind per network that serves it, and the settlement authority among the
network’s signers:
amount is the cap at /verify and the amount to
settle at /settle; the payer’s accepted block always holds the cap. payTo is a G… or C… address: muxed
M… addresses are refused, because the contract pulls funds with transfer_from, which carries no muxed id.
signerAuthorizations is optional and only for smart-account (C…) payers whose own authorization check
requires entries from other signers (for example a delegated signer): at most four signed entries, each
authorizing only the account’s __check_auth. Rail402 places them in the settlement’s authorization list
next to the payer’s entry. For an OpenZeppelin smart account whose context rule’s signer is
Signer::Delegated(G…), the account’s __check_auth calls require_auth_for_args((auth_digest,)) on that
address, so the delegate signs an entry for __check_auth(auth_digest) of the account; such a payment verifies
and settles, and the same payment without the delegate’s entry, or with one signed by another key, is refused
with invalid_upto_stellar_payload_smart_account_rejected (upto-payers integration test).
Why upto does not send { transaction } like exact. For exact, the buyer can build the whole
transaction: the transfer, its amount and its authorization are all known, so the payload is the signed
transaction and Rail402 accepts it verbatim. For upto, the buyer cannot: the amount is decided after the
resource is served, and the settlement operation’s source and the settlement authority’s authorization
belong to the facilitator. Any transaction the buyer built would have to be rebuilt, so it would be dead
weight and one more thing to validate. The buyer signs what it can sign, one authorization entry, and the
facilitator builds and submits the invocation around it.
The settle response always carries the amount settled, "0" included:
The settlement authority
The amount is not part of the buyer’s signature, so something else must decide who may choose it. The terms name a settlement authority,extra.facilitatorAddress, and the contract requires that address to authorize
settle(payer, terms, amount). Only that authority can settle, and only for an amount between zero and the
cap: a copy of the payload submitted by anyone else fails the network’s authorization check.
That binding stops anyone but the configured facilitator from settling. It does not decide which caller of
that facilitator’s /settle names the amount: the authority settles the amount of the first /settle it
accepts for an authorization. Anyone who holds the payload (the buyer, the seller, or whoever sees it on the
way) can ask that facilitator to settle any amount from zero to the cap before the seller does. The first
request wins; the seller’s later request for a different amount is refused with
settle_upto_stellar_idempotency_conflict, whose reason names the amount that holds the authorization. Funds
still go only to payTo and never above the cap, but a buyer who settles zero first leaves the seller unpaid,
and anyone who settles the cap first charges the buyer the full cap. What limits it:
- The payload travels only from buyer to seller, over TLS. Keep it out of logs and third-party proxies.
- Sellers settle before they release the resource. The stock Express middleware buffers the response and withholds it when settlement fails.
- Operators can require API keys for
/verifyand/settle(REQUIRE_API_KEY), so only known sellers can settle. The public testnet service is keyless. - Conflicts carry a stable code and are counted in the settlement metrics.
upto settlement, so its authorization is the transaction’s own signature and
needs no separate authorization entry. The transaction’s source is a channel account and the sponsor pays
through a fee bump, exactly as for exact.
The service creates the authority at startup when it is missing; replicas starting together take turns. If
it cannot yet (for example, an unfunded sponsor), upto payments are refused with
settle_upto_stellar_authority_unavailable until a later poll creates it, and exact keeps working. /health
warns if the authority holds XLM it could spend or any accepted token. When the sponsor rotates, the authority
rotates with it; see rotating the sponsor key.
What the authority can and cannot do: it chooses the amount, from zero to the cap, or declines to settle. It
cannot exceed the cap, change the recipient or token, reuse an authorization, settle after the expiration, or
move funds of its own or anyone else’s.
Payment is not guaranteed after verification
upto is post-paid: the seller serves, then settles. /verify proves that the signature, the balance and the
trustline allow the full cap at that moment; nothing is locked. Until the seller’s own /settle succeeds, the
buyer can move its funds, use its authorization in another transaction, or settle the payload itself for
zero (see above), and the seller’s settlement then fails for work already done. The loss is one request, at
most its cap. To keep it small:
- keep
maxTimeoutSecondsshort and settle as soon as the resource is served; - withhold buffered output until settlement succeeds; a streaming seller cannot withhold what it already
sent, so stream only to payers it trusts or charge a fixed
exactprice for the first part; - stop serving payers whose settlements fail.
Zero, partial and maximum amounts
On testnet a zero settlement is submitted on chain by default, so the authorization is consumed on the
network and the response has a transaction hash. With
UPTO_ZERO_SETTLEMENT=offchain, the pubnet default,
Rail402 instead records the authorization as used in its settlement ledger and answers transaction: ""
without a transaction; that is safe only because no one but its own settlement authority can settle the
authorization, and only while that record lasts until the authorization expires. The service therefore
refuses to start with offchain and STORE=memory, and an in-process facilitator needs its settlement
ledger set explicitly: an in-memory ledger forgets the record on restart and is not seen by other processes.
The residual allowance of a partial settlement (at most cap − amount) cannot be spent by anyone: only the
settlement contract is the spender, and its only way to spend requires a fresh signature from the payer and
first overwrites the allowance.
The Bazaar lists an upto resource at its cap, never at an amount settled. A zero settlement never catalogs
a resource, on chain (bazaar_settlement_zero) or off (bazaar_settlement_offchain): a listing needs a
settlement that moved value to its payTo, and a zero needs neither a balance nor a trustline.
Duplicates and idempotency
One authorization settles at most once. Rail402’s settlement ledger keys it by network, payer and nonce, the same key asexact, because a nonce is per address.
- The same request again (same authorization, same amount) gets the recorded response, marked as a repeat in
EXTENSION-RESPONSES, and moves funds once. - The same authorization for a different amount is refused with
settle_upto_stellar_idempotency_conflict; the reason names the amount and transaction of the settlement that holds it. - With Postgres, this holds across replicas: an authorization settled concurrently through two replicas lands one transaction.
- Independently, the network refuses a second use of the authorization, whoever submits it.
exact: the signed envelope is recorded before it is broadcast, only the
recorded bytes are ever resubmitted, and a settlement not final within SETTLE_CONFIRM_TIMEOUT_MS answers
settlement_pending with its hash and is finished by the reconciler.
A settlement is finished as failed or expired only on proof that it did not land: after its ledger or time
bound has passed (or the network refused the recorded bytes for good), its inner transaction is looked up too,
in case someone wrapped it in their own fee bump, and the payer’s authorization nonce is read. A consumed nonce
finishes the settlement only once the transaction that consumed it is found on chain; otherwise the record
stays settlement_pending for an operator to resolve.
Expiration
The buyer’s entry expires atlatest ledger + ceil(maxTimeoutSeconds / ledger seconds), and the terms carry the
same ledger. Rail402 accepts an authorization while it has at least EXPIRATION_MARGIN_LEDGERS ledgers left
and expires no later than two ledgers past the window maxTimeoutSeconds allows. The settlement transaction’s
ledger bounds end one ledger after the expiration, so a transaction held past it is refused by the network
before inclusion and costs the sponsor nothing.
maxTimeoutSeconds must cover verification, the resource’s own work and settlement: give it room when a
request can take long.
Smart accounts
A smart-account (C…) payer’s __check_auth sees both calls it authorizes: settle(terms) on the settlement
contract and approve(payer, settlement contract, cap, expiration) on the token. It sees them even when the
amount settled is zero, so a spending policy reserves the full cap of every payment.
OpenZeppelin’s stock spending-limit policy budgets only transfer and refuses an approve context, so it
refuses upto payments. Rail402’s repository has a policy for OpenZeppelin smart accounts that budgets both
transfer and approve against one rolling limit (contracts/testing/approve-spending-limit, a test contract
not deployed on a public network). Configure the account’s rules so the constrained key cannot get around it:
Never put the agent key on a rule without the policy, the
Default rule included: the signer chooses which
rule each authorized call is checked under, so a policy-free rule would let the agent key skip the budget. The
tests prove that payments within the budget settle, that a payment whose cap exceeds what is left of the
budget is refused (invalid_upto_stellar_payload_smart_account_rejected) with nothing moved, that an amount
over the x402 cap is refused, and that the agent key cannot use the Default rule
(contracts/testing/composition, apps/rail402/test/upto-payers.integration.test.ts).
Fees
The sponsor pays everyupto settlement fee, under its own ceiling, UPTO_MAX_TX_FEE_STROOPS (5,000,000
stroops on testnet, 500,000 on pubnet). A settlement runs the contract, so it costs more than an exact
transfer: a partial settlement from a G… payer was observed to cost 134,335 stroops on a private network,
and a smart account’s first payment under a spending-limit policy 2,339,612 on testnet, because the policy
creates its record of the account. Fee ceilings lists the observed costs.
An on-chain zero settlement moves no tokens but still costs a fee, and needs no token balance or trustline,
so anyone who can sign an authorization can make the sponsor pay for settlements that move nothing. Rate
limits, the sponsor guard (SPONSOR_RESERVE_XLM, MAX_SPONSOR_SPEND_XLM_PER_HOUR) and
UPTO_ZERO_SETTLEMENT=offchain bound that. Pubnet settles zeros off-chain by default and does not start
without PUBNET_MAX_SPONSOR_SPEND_XLM_PER_HOUR; the public testnet service settles them on chain, so every
zero there has a public transaction.
Limits
Codes
Every refusal has a stable code and a non-empty reason. Theupto codes start with invalid_upto_stellar_ or
settle_upto_stellar_; the x402 standard codes (insufficient_funds, settlement_pending, …) are shared with
exact. Errors and verification rules lists every check in order with its
code and the tests that prove it, and error codes lists every code with its HTTP
status and whether a retry can succeed.