Vellar SDK
ExplorerGitHubOpen Vellar Wallet

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:

  1. Request the resource → get 402 + requirements.
  2. Build the SEP-41 transfer(from = smart account, to = payTo, amount).
  3. Sign the wallet's auth entry with your session key (V1 credentials).
  4. Retry with the PAYMENT-SIGNATURE header.
  5. 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 classic G... keypair, x402 on Stellar supports that directly and needs no smart account at all; see examples/buyer-classic.mjs in 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. From vellar-sdk 0.6.1 this is also thrown at createVellarWallet construction when rpcUrl is missing, empty, or not a parseable URL (pass it as x402.rpcUrl or top-level rpcUrl). On earlier versions the same mistake surfaced later, as a raw TypeError: Invalid URL from inside @stellar/stellar-sdk during wallet.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:

SignerFlowPrompt
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. createPasskeyX402Signer produces 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 SignerLimits that 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:

ErrorWhen
MaxAmountExceededErrorThe server asked for more than your maxAmount. Nothing signed.
DisallowedAssetErrorNo offered asset was in allowedAssets. Nothing signed.
NoUsablePaymentOptionErrorNo option matched the scheme/network, or none advertised areFeesSponsored: true.
InvalidRequirementsErrorA requirement was malformed (e.g. a non-integer amount).
PaymentRejectedErrorThe facilitator rejected the payment at verify — e.g. an over-budget payment blocked by the on-chain policy. Carries the reason.
X402NotConfiguredErrorwallet.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.