Reference
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 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.
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,
},
});
Without x402 config, wallet.x402 throws X402NotConfiguredError.
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) {
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. |
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 over a rolling window inside__check_auth. Even if client code is bypassed, the chain refuses to move funds beyond the cap.
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. The SDK rejects an option that does not advertise sponsored fees.
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.
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 sponsored fees. |
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.