x402 Payments
x402 Agentic Payments
Pay x402 (HTTP-402) resources from a Vellar smart account — the "give your agent a budget, not your keys" flow. An autonomous agent holds a scoped session key that pays for x402-protected APIs on its own, but only within a budget enforced on-chain by a spending-limit policy. The agent never holds the account's admin passkey, and it cannot spend beyond its per-window cap.
The SDK exposes this on the wallet handle as wallet.x402.
Requires
vellar-sdk≥ 0.4.0.
How it works
x402 is an open protocol built on HTTP 402 Payment Required. A resource server
answers an unpaid request with 402 plus payment requirements (amount, asset,
recipient, network); the client builds a signed payment and retries; a hosted
facilitator verifies and settles it on-chain. On Stellar the payment is a
SEP-41 token transfer authorized by a Soroban authorization entry.
Vellar pays as a smart account (a C-address). wallet.x402.fetch() runs the
whole exchange:
- Request the resource → get
402+ requirements. - Build the SEP-41
transfer(from = smart account, to = payTo, amount). - Sign the wallet's auth entry with your session key (V1 credentials).
- Retry with the
PAYMENT-SIGNATUREheader. - The facilitator verifies by re-simulation — which runs the account's
__check_auth, and therefore the budget policy — then settles on-chain.
Because verification re-simulates, an over-budget or wrong-token payment is rejected before it settles.
Don't need on-chain budget enforcement? This page — and
wallet.x402— is specifically for paying from a Vellar smart account, since that's where the policy-enforced budget comes from. If you just want to pay with a plain classicG...keypair, x402 on Stellar supports that directly and needs no smart account at all; seeexamples/buyer-classic.mjsin the facilitator repo for proven, working code — same protocol, a simpler auth-entry signature shape.
Enabling x402
Pass x402 config to createVellarWallet with a signer (who pays) and a
simulationSourceAccount (any funded classic account, used only to simulate —
the facilitator rebuilds the transaction and pays the network fee):
import { createVellarWallet, createSessionKeySigner } from "vellar-sdk";
const vellar = createVellarWallet({
network: "testnet",
appName: "My Agent",
kit,
sac,
backend,
isValidAddress,
x402: {
// Agent flow: a scoped ed25519 session key signs headlessly (no passkey).
signer: createSessionKeySigner({
address: walletCAddress, // the smart account that pays
secretKey: sessionKeySecret, // the session key attached to it
}),
simulationSourceAccount: aFundedGAccount,
// Soroban RPC used for x402 simulation — required.
rpcUrl: "https://soroban-testnet.stellar.org",
},
});
Need a funded G... account for simulationSourceAccount? On testnet,
Friendbot funds any keypair for free:
curl "https://friendbot.stellar.org?addr=G..." — see the
Quickstart.
Want to test against a live seller without running your own? The deployed demo seller accepts real testnet USDC, which you can obtain headlessly in two steps — see Paying the deployed demo seller.
simulationSourceAccount must be a different funded account from the
payer — never the wallet's own address. The facilitator rebuilds the
transaction with itself as the source before submitting, so yours is only
ever used to simulate; but simulate from the payer itself and Soroban
authorizes with source-account credentials instead, which the exact scheme
rejects outright (invalid_exact_stellar_payload_unsupported_credential_type).
Any funded G... keypair works here — it never signs and is never charged.
Without x402 config, calls on wallet.x402 throw X402NotConfiguredError.
Troubleshooting
X402NotConfiguredError. Fromvellar-sdk0.6.1 this is also thrown atcreateVellarWalletconstruction whenrpcUrlis missing, empty, or not a parseable URL (pass it asx402.rpcUrlor top-levelrpcUrl). On earlier versions the same mistake surfaced later, as a rawTypeError: Invalid URLfrom inside@stellar/stellar-sdkduringwallet.x402.fetch()— if you see that, this config is the cause.
Auth entry expiration
A Soroban authorization entry is valid only up to a specific ledger sequence
number, not wall-clock time. The SDK sets this via expirationLedgerOffset —
ledgers added to the current ledger when the entry is signed.
Testnet closes a ledger roughly every 5 seconds. Once expired the signature is dead: verify and settle fail and no retry of the same payload can succeed — sign fresh.
The upto contract enforces a separate ceiling:
expiration_ledger must not exceed current_ledger + 17,280 (~24h at
5s/ledger). This is the contract's replay-protection window, independent of your
wallet config.
Do not confuse this with agent key expiresAt (Agent
Keys) — that bounds how long a key may sign at all. Different
mechanisms, different layers.
Paying a resource
const { response, paid, settlement } = await vellar.x402.fetch(
"https://api.example.com/paid",
{
maxAmount: 1_000_000n, // hard per-request ceiling, in the asset's base units
// allowedAssets: [usdcSac], // optional — restrict which asset(s) you'll pay in
},
);
if (paid && settlement) {
console.log("settled on-chain:", settlement.transaction);
}
const data = await response.json(); // the unlocked resource
fetch returns { response, paid, settlement }: the unlocked Response, whether
a payment was made, and (when paid) the on-chain settlement { transaction, payer, asset, amount, network }. If the resource needs no payment, it passes
through untouched with paid: false.
For callers managing their own transport, wallet.x402.createPayment(requirements, opts) signs a payment and returns the PAYMENT-SIGNATURE header value without
sending it.
The two signers
Both satisfy the same SmartAccountX402Signer interface, so the client is
signer-agnostic:
| Signer | Flow | Prompt |
|---|---|---|
createSessionKeySigner({ address, secretKey }) | Agent — a scoped ed25519 session key signs headlessly. Bounded on-chain by the spending-limit policy attached to it. | None — an agent runs unattended. |
createPasskeyX402Signer({ address, webAuthn }) | Human — a person pays from the web app. webAuthn is a seam you wire to your passkey ceremony. | One passkey prompt per payment. |
⚠️ The passkey signer does not settle today.
createPasskeyX402Signerproduces a correct signature shape, but no deployed facilitator currently accepts human passkey-signed x402 payments — a payment built with it will not settle. Build on the session-key path (createSessionKeySigner), which settles end to end against the hosted facilitator. This applies to hackathon projects especially: don't build around the passkey signer.
Both produce V1 (sorobanCredentialsAddress) credentials in the smart-wallet
signature format the account's __check_auth expects. (The SDK builds this
itself rather than delegating to passkey-kit, which upgrades to V2 credentials
that hosted facilitators reject.)
maxAmount is a guard, not the budget
There are two independent protections, and it matters that they are different:
maxAmount(client-side) — the SDK refuses to sign a payment whose required amount exceeds this ceiling, guarding against an over-charging or misconfigured server. It is only as trustworthy as the client process. It is not the budget.- The spending-limit policy (on-chain) — the real, enforced budget. The agent
session key is attached with
SignerLimitsthat require a spending-limit policy to co-sign any transfer; the policy caps cumulative spend per fixed window inside__check_auth(up to 2× the cap can move in a short span around a window reset — see Honesty). Even if client code is bypassed, the chain refuses to move funds beyond that bound.
Use the token-scoped spending-limit policy for an agent budget so only one specific token's transfers count against it — then "give your agent $10/day of USDC" means exactly that: USDC only, capped, on-chain.
Facilitators and the fee ceiling
The exact scheme requires sponsored fees — the facilitator rebuilds the
transaction with its own source and pays the Stellar fee, so the smart account
needs no XLM. An option advertises this as
areFeesSponsored: true in its extra
object, and the SDK rejects any option that does not.
A policy-governed payment runs the policy inside __check_auth, costing more
resource fee than a plain transfer. Hosted facilitators cap the fee they sponsor
(the reference x402.org facilitator defaults to 50,000 stroops), which a
policy-governed payment can exceed — such a payment needs a facilitator with a
higher ceiling. A plain transfer stays well under the default.
Vellar runs one. The x402 Facilitator
(https://vellar-facilitator.onrender.com, testnet) ships with a raised
ceiling specifically so policy-governed agent payments settle instead of
being rejected — plus Bazaar discovery so agents can find payable resources.
That facilitator also speaks a second, experimental scheme alongside exact:
upto, for metered pricing where the buyer authorizes a ceiling
and the facilitator settles the actual usage, enforced on-ledger. wallet.x402
doesn't build upto payments yet — this page's flow is exact only.
Errors
The client throws typed errors:
| Error | When |
|---|---|
MaxAmountExceededError | The server asked for more than your maxAmount. Nothing signed. |
DisallowedAssetError | No offered asset was in allowedAssets. Nothing signed. |
NoUsablePaymentOptionError | No option matched the scheme/network, or none advertised areFeesSponsored: true. |
InvalidRequirementsError | A requirement was malformed (e.g. a non-integer amount). |
PaymentRejectedError | The facilitator rejected the payment at verify — e.g. an over-budget payment blocked by the on-chain policy. Carries the reason. |
X402NotConfiguredError | wallet.x402 used without x402 config. |
Lower-level
For a client without a wallet handle, createX402Client({ signer, rpcUrl, network, simulationSourceAccount }) returns the same fetch / createPayment
API. See Advanced for the exported building blocks.
