Vellar SDK
ExplorerGitHubOpen Vellar Wallet

x402 Payments

upto — Metered Payments

A second payment scheme alongside exact: the buyer authorizes a spending ceiling with one signature, and the facilitator settles for the actual metered amount — usage-based pricing without a signature per unit consumed.

Status: experimental. This is a Vellar-specific extension of x402 v2, not yet part of the finalized spec. The wire shape may change before upstream settles on one — see x402-foundation/x402 #3134, which proposes standardizing an upto scheme for Stellar and is open, alongside a competing proposal. Don't build against this expecting the wire format to be stable yet.

Why this exists

exact needs the price known and signed before the resource is served — fine for a flat-rate API call, wrong for anything metered: tokens generated, seconds of compute, rows returned. upto lets a seller charge for what was actually used, capped by what the buyer agreed to risk.

How it's enforced

The buyer signs one authorization: (token, from, to, max_amount, expiration, nonce) — notably not the actual amount. At settlement, the facilitator supplies the metered actual_amount, and a Soroban contract checks actual <= max on-ledger before it moves anything. The facilitator cannot settle more than the ceiling the buyer signed, because the chain refuses the transaction if it tries — this isn't a promise the facilitator makes, it's a bound the contract enforces regardless of what the facilitator does.

Using it

GET /supported now advertises both schemes for stellar:testnet:

{
  "kinds": [
    { "scheme": "exact", "network": "stellar:testnet", "extra": { "areFeesSponsored": true } },
    {
      "scheme": "upto",
      "network": "stellar:testnet",
      "extra": {
        "uptoContract": "CDHPA64M73TUTEM4MMHIWIXINBQXH7JJXFGZMGH22VJWFJFROMR6QV2S",
        "areFeesSponsored": true
      }
    }
  ]
}

/verify and /settle accept scheme: "upto" payloads the same way they accept exact ones. Two things worth knowing if you're integrating by hand: the settlement hook is refused (no arbitrary post-settle callback), and /verify simulates at the ceiling, not the actual, since the actual isn't decided yet.

The wire shape

payload.payload.transaction is a base64-encoded, unsigned Soroban transaction envelope that invokes the deployed contract's settle function — "unsigned" at the envelope level; what actually carries the buyer's authorization is the Soroban auth entries attached to that one operation, which are signed. The facilitator never relays this envelope as-is: it rebuilds the transaction from its own sponsor account (so the buyer holds no XLM and pays no fee) and swaps in the metered actual_amount before submitting.

This is the actual request body POST /verify and POST /settle both take — matching what examples/upto-buyer.mjs constructs. paymentPayload.accepted is the same object as the top-level paymentRequirements (omitted a second time below for brevity):

{
  "x402Version": 2,
  "paymentPayload": {
    "x402Version": 2,
    "accepted": { "...": "same object as paymentRequirements below" },
    "payload": { "transaction": "AAAAAgAAAAC1I3O2CN...(base64, unsigned envelope)" }
  },
  "paymentRequirements": {
    "scheme": "upto",
    "network": "stellar:testnet",
    "asset": "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
    "amount": "1000000",
    "payTo": "GDEST...",
    "maxTimeoutSeconds": 120,
    "extra": { "actualAmount": "400000" }
  }
}

The metered actual lives in requirements.extra.actualAmount (a string, atomic units) — set by the seller/facilitator at settlement time, not something the buyer ever signs. It's the one field that's easy to miss: the buyer's signed authorization deliberately excludes it (that's the whole point — the signature covers the ceiling, not the eventual charge), and if you omit it entirely the facilitator settles for the full ceiling rather than a metered amount.

The settle response's amount is the actual settled amount, not the ceiling — distinct from paymentRequirements.amount, which stays the ceiling throughout:

FieldMeaningExample above
paymentRequirements.amountThe ceiling the buyer authorized (max_amount)"1000000"
SettleResponse.amountThe actual amount settled (actual_amount, ≤ ceiling, enforced on-ledger)"400000"

A settle response looks like { "success": true, "transaction": "<hash>", "payer": "G...", "amount": "400000" } — reconcile against paymentRequirements.amount, not the other way around, if you need to confirm how much of the ceiling was actually used.

Contract argument order

For anyone building the invocation by hand, the deployed contract's settle takes exactly eight arguments, in this order — get this wrong and it fails before any signature is even checked:

(token, from, to, max_amount, expiration_ledger, nonce, actual_amount, hook)

hook must be None/void. The facilitator refuses anything else (invalid_upto_stellar_hook_not_supported) — it's a deliberate settlement hook, not a defended one, because nothing legitimate needs it and refusing is cheaper than sandboxing an arbitrary post-settle callback aimed at the sponsor.

Not yet in vellar-sdk. wallet.x402.fetch() speaks exact only right now. To pay with upto today, build the authorization by hand — the same way the smart-account buyer example does for exact, because the official x402 client doesn't support this scheme either.

End-to-end client: examples/upto-buyer.mjs in the facilitator repo signs the ceiling authorization and drives /verify + /settle directly.

cd examples
FACILITATOR_URL=http://localhost:4100 UPTO_CONTRACT=C... \
PAYER_SECRET=S... PAYTO=G... ASSET=C... SIM_SOURCE_ACCOUNT=G... \
MAX=1000000 ACTUAL=400000 node upto-buyer.mjs

SIM_SOURCE_ACCOUNT must be a funded account that is not the payer — same reason as the exact smart-account flow: simulating from the payer yields source-account credentials the scheme rejects.

Deployed contract

Published so it can be verified against its source without trusting this page — every value below is independently checkable.

Contract ID (testnet)CDHPA64M73TUTEM4MMHIWIXINBQXH7JJXFGZMGH22VJWFJFROMR6QV2S
Wasm hash (on-chain)c276b905981eab91704ce9b9046ebb4867b164dd7e4ba0e0ecda841527d398a9
Sourcecontracts/upto-stellar/ in the facilitator repo — vendored verbatim (Apache-2.0) from rail402's contracts/upto-stellar/ at commit ff504b85ac065369dc985759afe4164a4541d861, reviewed line-by-line before vendoring
Deployed2026-08-21, from the facilitator repo's own sponsor account

The on-chain wasm hash is the sha256 of the wasm, so anyone can rebuild and compare:

cd contracts/upto-stellar
stellar contract build
shasum -a 256 target/wasm32v1-none/release/x402_upto_stellar.wasm
# → c276b905981eab91704ce9b9046ebb4867b164dd7e4ba0e0ecda841527d398a9

Full deployment record, including the fetch-and-compare steps against the live contract and the first on-chain settlement's transaction hash: docs/upto-deployment.md in the facilitator repo.

Watch it settle

explorer.vellar.xyz is a public transaction explorer — a separately built and operated service, not this facilitator describing itself — that classifies real settlements straight off the Stellar ledger. It shows upto settlements correctly today, verified rather than assumed: three settlements against the hosted facilitator each appear on the feed with scheme: upto, settled by: vellar, and the metered actual amount displayed, not the signed ceiling — be728773… (0.0555 USDC of a 0.15 ceiling), f558307e… (0.0312 of 0.08), and 12f0fa5c… (0.0417 of 0.12); full tx hashes and the Horizon-confirmed record in the facilitator repo's docs/upto-deployment.md. Per the explorer's own attribution breakdown at time of writing, 6 of 4,799 payments indexed across the visible testnet ecosystem carry a known facilitator, and all six are ours — a small, testnet-scale number stated as one, not a market-share claim.

One thing worth knowing rather than discovering: the first settlement attempted through this path didn't show up at all when this was verified — not lag, a genuine gap in the explorer's classifier, which only recognized exact-scheme's direct-transfer shape and never looked at an upto settlement's contract invocation. Fixed in that repo (it now reads the actual amount from the token's own emitted transfer event, not from the envelope's signed-ceiling or facilitator-supplied args) — the three settlements above are the proof the fix holds, not the discovery of the bug.