# Solvador Docs
> Developer documentation for Solvador — an x402 payment facilitator that verifies and settles stablecoin payments on 15 networks (11 EVM chains plus Solana, NEAR, the XRP Ledger, and Starknet), so resource servers never touch gas, keys, or RPC plumbing.
Facilitator API base URL: https://api.solvador.com
Dashboard (API keys, settlements, billing): https://dashboard.solvador.com
Endpoints: POST /verify (no auth), POST /settle (X-API-Key), GET /supported, GET /discovery/resources, GET /discovery/search
---
# Introduction
URL: https://docs.solvador.com/
Section: Getting Started
> Solvador is an x402 payment facilitator that verifies and settles stablecoin payments on 15 networks, confidentially and compliantly.
Solvador is a hosted [x402](https://www.x402.org) payment facilitator. Point your resource server at `https://api.solvador.com`, drop in an API key, and settle stablecoin payments on mainnet — on Base, Arbitrum, Optimism, Polygon, Avalanche, Celo, Linea, Unichain, World Chain, Monad, Robinhood Chain, Solana, NEAR, the XRP Ledger, and Starknet, including the chains other facilitators don't reach.
## How x402 works in 60 seconds
x402 is an open standard for HTTP-native payments, built around the `402 Payment Required` status code:
1. A client requests a paid resource from your server.
2. Your server responds with `402 Payment Required` and a list of payment requirements (network, asset, amount, recipient).
3. The client signs a payment payload matching one of those requirements and retries the request.
4. Your server forwards the payload to a **facilitator** to `/verify` it, serves the resource, then calls `/settle` to execute the payment on-chain.
Solvador is the facilitator leg of that flow: it checks signatures, balances, amounts, and timing, then submits the transaction on-chain and returns the result. See the [x402 specification](https://www.x402.org) for full protocol details.
## Why a hosted facilitator
Settling payments yourself means holding private keys, funding gas on every chain you accept, and running RPC infrastructure. With Solvador your server stays **gasless**: it never holds a key, never funds gas, and never submits a transaction. You get one API across 15 networks and three payment schemes.
## What you get
- **`POST /verify`** — payment verification, authenticated with your API key. Free and unmetered.
- **`POST /settle`** — on-chain settlement, authenticated with the same API key.
- **15 mainnet networks** — 11 EVM chains plus Solana, NEAR, the XRP Ledger, and Starknet.
- **Three payment schemes** — [`exact`](/schemes/exact), [`upto`](/schemes/upto), and [`batch-settlement`](/schemes/batch-settlement).
- **Protocol extensions** — [Bazaar resource discovery, settle idempotency, and gasless approvals](/extensions).
- **A dashboard** — settlement history, metrics, API keys, and billing at [dashboard.solvador.com](https://dashboard.solvador.com).
## Next steps
Accept your first x402 payment through Solvador in five minutes.
Every chain and asset Solvador settles on.
Fixed-price, metered, and high-frequency payment models.
Endpoints, request and response shapes, and error handling.
---
Solvador on the web: [solvador.com](https://solvador.com) · [dashboard.solvador.com](https://dashboard.solvador.com) · [blog.solvador.com](https://blog.solvador.com) · [Discord](https://discord.gg/53XpC68sqF)
---
# Quickstart
URL: https://docs.solvador.com/quickstart
Section: Getting Started
> Accept your first x402 payment through Solvador in five minutes.
This guide takes an Express server from zero to charging for a route with x402, using Solvador as the facilitator.
## Set up with AI
In a hurry? Paste this one-liner into your coding agent — Claude Code, Cursor, or any assistant that can fetch URLs and edit your project — and it will do the whole integration for you. The manual steps below are the same flow.
```text
Read https://docs.solvador.com/setup-prompt.md and integrate Solvador into my project by following it.
```
The [prompt itself](https://docs.solvador.com/setup-prompt.md) is human-readable — it walks the
agent through the docs at [llms-full.txt](https://docs.solvador.com/llms-full.txt), asks before
writing code, and leaves dashboard steps and secrets to you. If your assistant cannot fetch
URLs, open the prompt and paste its contents instead.
## 1. Create an API key
Sign in at [dashboard.solvador.com](https://dashboard.solvador.com) with Google or GitHub, open the **API Keys** tab, and create a key.
The key is shown in plaintext exactly once, at creation. Store it immediately — for example as a `SOLVADOR_KEY` environment variable. If you lose it, delete the key and create a new one.
Both `/verify` and `/settle` require the key. `/supported` and the discovery endpoints are open, so capability discovery works without an account.
## 2. Install the x402 SDK
```bash
npm install @x402/core @x402/express
```
## 3. Point your resource server at Solvador
Create an `HTTPFacilitatorClient` for `https://api.solvador.com` and inject your API key with the `createAuthHeaders` hook:
```ts title="server.ts"
import express from "express";
import { paymentMiddleware } from "@x402/express";
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
const app = express();
const auth = { "X-API-Key": process.env.SOLVADOR_KEY! };
const facilitator = new HTTPFacilitatorClient({
url: "https://api.solvador.com",
createAuthHeaders: async () => ({
verify: auth,
settle: auth,
supported: {},
}),
});
const routes = {
"GET /premium": {
accepts: {
scheme: "exact",
network: "eip155:8453", // Base
price: "$0.01",
payTo: "0xYourReceivingAddress",
},
},
};
app.use(paymentMiddleware(routes, new x402ResourceServer(facilitator)));
app.get("/premium", (_req, res) => {
res.json({ data: "the content your clients pay for" });
});
app.listen(3000);
```
That's the whole integration: the middleware answers unpaid requests with `402 Payment Required`, verifies incoming payment payloads through Solvador, and settles them on-chain after serving the response.
## 4. Test it
Request the protected route without a payment:
```bash
curl -i http://localhost:3000/premium
```
You get a `402` response whose body lists the payment requirements (scheme, network, asset, amount, and recipient). Any x402-compatible client or wallet can read those requirements, sign a payment payload, and retry the request — the middleware and Solvador handle the rest.
## 5. Go live
- The **Free plan** includes 10 settlements per month with no card required — enough to verify your integration end to end. See [Plans & Quotas](/platform/plans) for paid tiers and pay-as-you-go.
- Pick the networks you want to accept from the [supported networks list](/networks); adding one is a single entry in your route's `accepts`.
- Watch settlements arrive in real time on the [dashboard](https://dashboard.solvador.com).
## What next?
Move beyond fixed prices with upto and batch-settlement.
Call /verify and /settle directly, without the SDK.
---
# Supported Networks
URL: https://docs.solvador.com/networks
Section: Getting Started
> 15 mainnet networks — 11 EVM chains plus Solana, NEAR, the XRP Ledger, and Starknet.
Solvador settles on 15 mainnet networks. Networks are addressed by their [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) identifier in payment requirements and API responses.
| Network | CAIP-2 ID | Default asset | Schemes |
| --- | --- | --- | --- |
| Base | `eip155:8453` | USDC | `exact`, `upto`, `batch-settlement` |
| Arbitrum One | `eip155:42161` | USDC | `exact`, `upto`, `batch-settlement` |
| Optimism (OP Mainnet) | `eip155:10` | USDC | `exact`, `upto`, `batch-settlement` |
| Polygon PoS | `eip155:137` | USDC | `exact`, `upto`, `batch-settlement` |
| Avalanche C-Chain | `eip155:43114` | USDC | `exact`, `upto`, `batch-settlement` |
| Celo | `eip155:42220` | USDC | `exact`, `upto`, `batch-settlement` |
| Linea | `eip155:59144` | USDC | `exact`, `upto`, `batch-settlement` |
| Unichain | `eip155:130` | USDC | `exact`, `upto`, `batch-settlement` |
| World Chain | `eip155:480` | USDC | `exact`, `upto`, `batch-settlement` |
| Monad | `eip155:143` | USDC | `exact`, `upto`, `batch-settlement` |
| Robinhood Chain | `eip155:4663` | USDG | `exact`, `upto`, `batch-settlement` |
| Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | USDC | `exact` |
| NEAR | `near:mainnet` | USDC (NEP-141) | `exact` |
| XRP Ledger | `xrpl:0` | XRP / RLUSD | `exact` |
| Starknet | `starknet:SN_MAIN` | USDC | `exact` |
## EVM networks
All 11 EVM networks are addressed as `eip155:`. USDC payments settle via ERC-3009 `transferWithAuthorization` — the payer signs off-chain and Solvador submits on-chain, so neither the payer nor your server spends gas. Smart-wallet payers are supported too: deployed wallets verify via ERC-1271, and counterfactual (not-yet-deployed) wallets via ERC-6492 signatures. See [`exact`](/schemes/exact) for details.
### Robinhood Chain and USDG
Robinhood Chain's default stablecoin is **USDG** (Global Dollar, `0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, 6 decimals). USDG supports neither EIP-3009 nor EIP-2612, so `exact` runs through **Permit2**: the payer makes a one-time on-chain approval to the canonical Permit2 contract, then signs off-chain like on every other chain.
## Solana
Standard x402 `exact` payments on SVM, settled in USDC. Payment payloads are constructed by any x402 Solana client; Solvador verifies and submits them.
## NEAR
`exact` on NEAR uses **relayer-sponsored NEP-366 delegate actions**: the payer signs a delegate action, and Solvador's relayer wraps it in an outer transaction and pays the NEAR gas. Payers need no NEAR balance for gas.
## XRP Ledger
XRPL uses a **keyless** model: the payer signs the complete transaction — which carries its own (sub-cent) network fee — and Solvador verifies it by simulation, then submits it. A settlement succeeds only when the transaction is validated on-ledger with `tesSUCCESS`.
XRPL settlements are **unlimited on every plan** — they never count against your monthly quota. See [Plans & Quotas](/platform/plans).
## Starknet
`exact` on Starknet uses **SNIP-9 outside execution**: the payer signs a SNIP-12 typed-data message authorizing exactly one USDC `transfer` from their own account contract, and Solvador's executor account submits it on-chain via `execute_from_outside_v2` and pays the gas. No token approvals, no client gas, and replay protection is enforced on-chain by single-use SNIP-9 nonces. Payment requirements carry a required `extra.feePayer` field, taken verbatim from [`GET /supported`](/api/supported); it is the address the payer's signature is bound to, so only Solvador can execute the authorization. Works with any SNIP-9 v2 account (Ready, Braavos, OpenZeppelin SRC-9).
## Live source of truth
The machine-readable list of every supported (scheme, network) pair is served at [`GET /supported`](/api/supported). Prefer querying it over hardcoding network lists — new networks appear there first.
---
# Migrate from CDP
URL: https://docs.solvador.com/migrate-from-cdp
Section: Getting Started
> Move an x402 v2 resource server from the Coinbase Developer Platform facilitator to Solvador. Same facilitator interface, a static API key instead of JWT signing, and 15 mainnet networks including Base, Polygon, Arbitrum, World Chain, Solana, NEAR, the XRP Ledger, and Starknet.
Solvador and the Coinbase Developer Platform (CDP) facilitator both implement the standard **x402 v2 facilitator interface**. In practice that means migrating comes down to swapping the facilitator client in your resource server. Your routes, prices, schemes, CAIP-2 network identifiers, and receiving address all stay as they are. Your paying clients change **nothing**: the `402` flow they see is identical before and after.
## What changes
| | CDP facilitator | Solvador |
| --- | --- | --- |
| Base URL | `https://api.cdp.coinbase.com/platform/v2/x402` | `https://api.solvador.com` |
| Authentication | Per-request JWT signed with `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET` | A static [`X-API-Key` header](/platform/api-keys) on `/verify` and `/settle`. `/supported` and discovery are open |
| Extra dependency | `@coinbase/cdp-sdk` | None, just the plain `HTTPFacilitatorClient` from `@x402/core` |
| Networks | Base, Polygon, Arbitrum, World Chain, Solana (plus Base Sepolia and Solana Devnet testnets) | [15 mainnet networks](/networks): those five plus Optimism, Avalanche, Celo, Linea, Unichain, Monad, Robinhood Chain, NEAR, the XRP Ledger, and Starknet |
| Pricing | 1,000 free transactions per month, then $0.001 each | [A free plan, fixed monthly plans, or pay-as-you-go](/platform/plans) (1,000 free per month, then $0.001). XRPL settlements are never metered |
Nothing changes at the protocol level: requests still carry `"x402Version": 2`, networks are still CAIP-2 identifiers like `eip155:8453`, the `exact` scheme works the same way, and the lifecycle is still `/verify` first, `/settle` after.
## 1. Create a Solvador API key
Sign in at [dashboard.solvador.com](https://dashboard.solvador.com) with Google or GitHub, open the **API Keys** tab, and create a key. This one key replaces the CDP key pair, and there is no request signing involved.
The key is shown in plaintext exactly once, at creation. Store it right away, for example as a `SOLVADOR_KEY` environment variable.
## 2. Swap the facilitator client
**Before**, with the CDP client from `@coinbase/cdp-sdk/x402`:
```ts title="server.ts"
import { x402ResourceServer } from "@x402/core/server";
import { createCdpFacilitatorClient } from "@coinbase/cdp-sdk/x402";
// Reads CDP_API_KEY_ID and CDP_API_KEY_SECRET from the environment
const facilitator = createCdpFacilitatorClient();
const server = new x402ResourceServer(facilitator);
```
**After**, with a standard `HTTPFacilitatorClient` pointed at Solvador:
```ts title="server.ts"
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
const auth = { "X-API-Key": process.env.SOLVADOR_KEY! };
const facilitator = new HTTPFacilitatorClient({
url: "https://api.solvador.com",
createAuthHeaders: async () => ({
verify: auth,
settle: auth,
supported: {},
}),
});
const server = new x402ResourceServer(facilitator);
```
Everything downstream of the facilitator client stays untouched: `paymentMiddleware`, your routes object, and your scheme registrations. `payTo` is just a receiving address, so you can keep settling to the wallet you already use, including one that CDP provisioned for you.
Rolling back is the same two lines in reverse. Both clients implement the same interface, so you can keep the old construction behind an environment switch during the cutover.
### If you used `createX402Server`
The CDP SDK's high-level `createX402Server` provisions a CDP server wallet and wires up the facilitator in one call. With Solvador you bring your own receiving address instead. Declare your routes with an explicit `payTo` (any address you control, including the wallet CDP created for you) and pass in the facilitator client above. The [quickstart](/quickstart) shows the complete Express setup.
## 3. Remove the CDP credentials
Once traffic settles through Solvador, remove `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, and `CDP_WALLET_SECRET` from the resource server's environment, and uninstall `@coinbase/cdp-sdk` if nothing else imports it. Keep them if the same service still uses CDP wallets for signing or for client-side payments.
## 4. Verify the cutover
1. Request a paid route without a payment and check that the `402` response lists your requirements unchanged.
2. `POST /verify` a signed payment with your new key. Verification consumes no quota, so this works before you pick a plan.
3. Settle a real payment. The **Free plan** includes 10 settlements per month, which is enough to prove the integration end to end. Watch it appear in the [dashboard](https://dashboard.solvador.com).
4. Query [`GET /supported`](/api/supported) and confirm every scheme and network pair you rely on is listed.
## Differences to plan for
- **Testnets.** Solvador settles on mainnet only. Keep staging pointed at a testnet facilitator, such as the CDP development environment or `https://x402.org/facilitator`, and point production at Solvador. Unmetered verification plus the Free plan's settlements cover end-to-end tests against mainnet.
- **Bazaar listings don't carry over.** The CDP Bazaar and [Solvador's discovery catalog](/api/discovery) are separate indexes. Your resource re-enters the catalog on its first verified payment that echoes the [`bazaar` extension](/extensions#bazaar-resource-discovery). No manual submission needed.
- **Gasless approvals.** Solvador sponsors the one-time Permit2 approval for `permit()`-capable (EIP-2612) tokens such as USDC through the [`eip2612GasSponsoring` extension](/extensions#eip2612gassponsoring). Tokens that support neither EIP-3009 nor EIP-2612 need one approval paid by the payer.
## What you gain
- **Ten more mainnets**: [Optimism, Avalanche, Celo, Linea, Unichain, Monad, Robinhood Chain, NEAR, the XRP Ledger, and Starknet](/networks). Each one is a single line added to your route's `accepts`.
- **[`upto`](/schemes/upto) and [`batch-settlement`](/schemes/batch-settlement) on all 11 EVM networks**, for metered pricing and high-frequency micropayments.
- **[Settle idempotency](/extensions#payment-identifier)**: send a `payment-identifier` with every settle and retries become safe by construction.
- **Unmetered XRPL**: settlements on `xrpl:0` never count against your quota, on any plan.
The full Express integration in five minutes.
Every chain and asset Solvador settles on.
---
# Payment Schemes
URL: https://docs.solvador.com/schemes
Section: Payments
> Choose between exact, upto, and batch-settlement — fixed-price, metered, and high-frequency payment models.
Every x402 payment requirement names a **scheme** — the `scheme` field determines what the payer signs and how settlement executes on-chain. Solvador supports three schemes.
## Comparison
| Scheme | Networks | The payer signs | Best for |
| --- | --- | --- | --- |
| [`exact`](/schemes/exact) | All 15 networks | A fixed amount | Fixed per-request pricing |
| [`upto`](/schemes/upto) | 11 EVM networks | A maximum; you settle actual usage | Metered, usage-based pricing |
| [`batch-settlement`](/schemes/batch-settlement) | 11 EVM networks | A deposit into an escrow channel, then off-chain vouchers | High-frequency micropayments |
## How to choose
- **Start with `exact`.** It is the standard x402 scheme, works on every network Solvador supports, and fits any "one request, one price" API.
- **Use `upto` when the price isn't known up front** — per-token LLM billing, per-second streaming, per-byte transfer. The payer authorizes a ceiling and you settle only what was consumed.
- **Use `batch-settlement` when per-request settlement is too expensive** — thousands of sub-cent requests from the same payer, redeemed in a single on-chain claim.
Fixed-amount payments on all 15 networks.
Sign a maximum, settle actual usage. EVM only.
Escrow channels with off-chain vouchers. EVM only.
---
# exact
URL: https://docs.solvador.com/schemes/exact
Section: Payments
> Fixed-amount payments — the standard x402 scheme, supported on all 15 networks.
`exact` is the standard x402 scheme: the payer authorizes precisely the amount stated in the payment requirements. It is the only scheme available on every network Solvador supports, and the right default for fixed per-request pricing.
## EVM
On the 11 EVM networks, `exact` settles USDC via **ERC-3009 `transferWithAuthorization`**. The payer signs an off-chain authorization; Solvador submits it on-chain. Neither the payer nor your server spends gas.
### Smart wallets
Smart-contract wallets are first-class payers:
- **Deployed wallets** verify signatures via ERC-1271.
- **Counterfactual wallets** — not yet deployed on-chain — are accepted via **ERC-6492** signatures. On the wallet's first payment, Solvador deploys it through its factory at settle time. Wallets from well-known audited factories are supported, including Coinbase Smart Wallet, Safe, Alchemy LightAccount, ZeroDev Kernel, and Biconomy.
### Tokens without EIP-3009
Tokens that don't implement EIP-3009 (such as USDG on Robinhood Chain) settle through **Permit2**: the payer makes a one-time on-chain approval to the canonical Permit2 contract, after which every payment is an off-chain signature like anywhere else. For `permit()`-capable tokens, that one-time approval can itself be gasless — see the [`eip2612GasSponsoring` extension](/extensions#eip2612gassponsoring).
## Solana
`exact` on SVM settles USDC using standard x402 Solana payment payloads.
## NEAR
`exact` on NEAR uses **NEP-366 delegate actions**: the payer signs a delegate action and Solvador's relayer wraps it in an outer transaction, paying the NEAR gas. Payers need no NEAR for gas.
## XRP Ledger
XRPL is **keyless**: the payer signs the complete transaction, which carries its own sub-cent network fee. Solvador verifies the signed blob by simulation; at settle time it re-verifies, submits, and reports success only when the transaction is validated on-ledger with `tesSUCCESS`. XRPL settlements [never count against your quota](/platform/plans#xrpl-is-unlimited).
## Starknet
`exact` on Starknet uses **SNIP-9 outside execution**. The payer signs a SNIP-12 typed-data message that authorizes exactly one USDC `transfer` call from their own account contract; Solvador's executor submits it via `execute_from_outside_v2` and pays the gas, so payers need no gas token and no approvals. Requirements on Starknet carry one extra required field, `extra.feePayer`: the executor address the signature is bound to as the SNIP-9 caller. Resource servers copy it verbatim from [`GET /supported`](/api/supported); client SDKs set it as the typed-data `Caller` automatically. Replay is prevented on-chain by single-use SNIP-9 nonces, and settlement succeeds only after the transaction is accepted on L2 with the expected `Transfer` event.
## Example payment requirements
A resource server advertising `exact` on Base for $0.01 in USDC produces requirements like:
```json
{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xYourReceivingAddress",
"maxTimeoutSeconds": 300,
"extra": {}
}
```
`amount` is in atomic token units (USDC has 6 decimals, so `10000` = $0.01). x402 client SDKs construct the signed payment payload from these requirements automatically — you never build scheme payloads by hand. For the wire format of the signed payload itself, see the [x402 specification](https://www.x402.org) and the `@x402/*` client packages.
---
# upto
URL: https://docs.solvador.com/schemes/upto
Section: Payments
> Variable-amount payments — the payer signs a maximum, you settle only what was used. EVM only.
`upto` is for prices you can't know until the work is done: per-token LLM billing, per-second streaming, per-byte transfer. The payer signs a **Permit2** authorization for a *maximum* amount; when your server settles, it specifies the amount actually consumed, up to that maximum.
## How it works
1. Your server advertises `upto` requirements with the ceiling amount.
2. The payer signs a Permit2 authorization for that maximum.
3. Your server verifies the payload, does the metered work, and calls [`/settle`](/api/settle) with the actual amount consumed.
4. Solvador transfers the actual amount — never more than the signed maximum.
The [settle response](/api/settle#response) reports what was actually charged in its `amount` field:
```json
{
"success": true,
"transaction": "0x6fe1…c40b",
"network": "eip155:8453",
"payer": "0xPayerAddress",
"amount": "4200"
}
```
Here the payer authorized up to some maximum, but only `4200` atomic units ($0.0042 in USDC) were settled.
## Gasless approvals
Permit2-based flows need a one-time on-chain approval from the payer to the canonical Permit2 contract. For `permit()`-capable tokens like USDC, Solvador sponsors that approval so payer onboarding is fully gasless — advertised as the [`eip2612GasSponsoring` extension](/extensions#eip2612gassponsoring) in [`GET /supported`](/api/supported).
## Availability
`upto` is registered on all 11 EVM networks. It is not available on Solana, NEAR, the XRP Ledger, or Starknet — use [`exact`](/schemes/exact) there.
---
# batch-settlement
URL: https://docs.solvador.com/schemes/batch-settlement
Section: Payments
> Escrow-channel payments for high-frequency use — deposit once, collect vouchers off-chain, claim on-chain when it counts.
`batch-settlement` is an x402 scheme for many small payments between one payer and one payee. Instead of settling every request on-chain, the payer locks funds in an on-chain **escrow channel**, then issues signed off-chain **vouchers** — one per paid request. The payee redeems accumulated vouchers in a single on-chain **claim**. Thousands of sub-cent requests become one transaction.
## Operation lifecycle
A `batch-settlement` payment payload carries a `type` field that selects one of four on-chain operations:
1. **`deposit`** — the payer funds the escrow channel. The channel's identity commits to its configuration (`channelConfig`), including the payer and the authorizer address below.
2. **`claim`** — the payee redeems a set of vouchers in one transaction. Each voucher represents one paid request; the payload's `claims` array carries them.
3. **`settle`** — closes out the channel balance.
4. **`refund`** — returns unclaimed funds to the payer.
All four operations flow through the same [`/verify`](/api/verify) and [`/settle`](/api/settle) endpoints as any other scheme — the `type` field does the multiplexing. x402 client SDKs construct these payloads; see the `@x402/evm` package for the channel and voucher wire formats.
## The receiver authorizer
For `batch-settlement` kinds, [`GET /supported`](/api/supported) advertises an `extra.receiverAuthorizer` — an EIP-712 signing address operated by Solvador that co-signs claim and refund messages. The authorizer address is committed into each channel's identity at deposit time, so clients must use the address currently advertised in `/supported` when opening a channel.
## Billing units
A `claim` counts **one settlement unit per voucher** against your plan quota — a claim redeeming 25 vouchers consumes 25 units. `deposit`, `settle`, and `refund` operations count zero units: only claims represent real revenue. See [Plans & Quotas](/platform/plans).
## Availability
`batch-settlement` is registered on all 11 EVM networks. Settlement executes on chains where the canonical x402 batch-settlement contract is deployed (a deterministic CREATE2 deployment, so it has the same address on every chain that has it).
---
# Extensions
URL: https://docs.solvador.com/extensions
Section: Payments
> Protocol extensions Solvador advertises — gasless approvals, settle idempotency, resource discovery, and builder attribution.
Solvador advertises its protocol extensions in the `extensions` array of [`GET /supported`](/api/supported). Clients and resource servers opt in per payment by echoing an extension in the payment payload.
## eip2612GasSponsoring
Permit2-based flows ([`upto`](/schemes/upto), and [`exact`](/schemes/exact) for tokens without EIP-3009) require a one-time on-chain approval from the payer to the canonical Permit2 contract. For `permit()`-capable tokens such as USDC, Solvador sponsors that approval — the payer's onboarding is fully gasless, with no native token needed on any chain.
## payment-identifier
Settle idempotency. Include an `id` in your payment payload and Solvador guarantees at-most-once settlement for it:
- **Same `id`, same payment** — the cached settle response is replayed verbatim. No second on-chain transaction, no additional quota consumed.
- **Same `id`, different payment** — the request is rejected with `409 Conflict`.
- **Malformed `id`** (or missing where the payload declares one is required) — `400 Bad Request`.
- **A failed settle releases the `id`**, so a retry with the same `id` re-executes rather than replaying the failure.
Identifiers are scoped to your API key — different Solvador accounts never collide.
Send a `payment-identifier` on every settle. It makes retries after timeouts and crashes safe by construction — you can always re-send the same request without risking a double charge.
## Bazaar (resource discovery)
The Bazaar is an open catalog of x402-payable resources. When a payment payload echoes the `bazaar` extension, Solvador catalogs the resource once the payment **verifies** — a resource can only enter the catalog behind a valid payment, which keeps spam out by construction. Successful settles refresh the catalog entry.
The outcome of a cataloging attempt is reported in the `EXTENSION-RESPONSES` response header of `/verify` and `/settle`. The catalog itself is public — query it at [`GET /discovery/resources` and `GET /discovery/search`](/api/discovery).
## Builder codes (ERC-8021)
Solvador can stamp an [ERC-8021](https://eips.ethereum.org/EIPS/eip-8021) builder-code suffix onto settlement calldata for on-chain attribution, including codes supplied by the resource server and the client alongside its own. Attribution is additive metadata only — it never changes settlement semantics.
---
# Confidential Settlement
URL: https://docs.solvador.com/confidential-settlement
Section: Payments
> Hide which merchant received a payment, plus revenue and balances, behind encrypted state on Base. Zero changes for payers.
import Mermaid from "../../src/components/mermaid";
Every x402 payment is normally public: anyone can read which address got paid, how much, and
add it up into a live revenue feed for your business. Confidential settlement removes that
surface. Payments settle into Solvador's privacy contract on Base instead of your address, and
your credit happens inside encrypted state powered by [Inco Lightning](https://www.inco.org/).
On-chain observers see a payer paying the Solvador contract. They cannot see which merchant was
paid, what any merchant has earned, or what any balance holds.
The payer side is completely unchanged. Payers sign the same plain USDC authorization as any
`exact` payment on Base, with any unmodified x402 client. Only the `payTo` differs.
The whole flow in one picture: the x402 handshake is standard `exact`, settlement is a plain
USDC pull into the contract, and your credit is applied later as an encrypted batch write.
(unmodified client)
participant M as Merchant API
(resource server)
participant S as Solvador
(facilitator)
participant C as Contract
(Base mainnet)
rect rgba(255, 255, 255, 0.03)
Note over P,M: 01 · x402 handshake, unchanged exact
P->>M: GET /api/resource
M-->>P: 402, payTo = contract
Note over P: signs EIP-3009
P->>M: retry + signature
end
rect rgba(255, 255, 255, 0.03)
Note over M,C: 02 · settle, plain USDC pull
M->>S: POST /verify
S-->>M: verified
M->>S: POST /settle
S->>C: settle(auth, fee)
Note over C: USDC pull only,
no merchant on-chain
S-->>M: signed receipt
M-->>P: 200 OK
end
rect rgba(255, 255, 255, 0.03)
Note over S,C: 03 · daily flush, deferred credit
Note over S: encrypts slots + totals
S->>C: flushCredits()
Note over C: oblivious credit inside
the encrypted anonymity set
end`}
/>
Confidential settlement runs on **Base only**, settles **USDC** via the `exact` scheme, and
works exclusively through the Solvador facilitator. A third-party facilitator executing the
same offer would transfer funds without crediting you.
## What stays private, what doesn't
Hidden on-chain: the receiving merchant of each payment, per-merchant revenue and payment
counts, and merchant balances. Merchants are grouped into encrypted anonymity sets; a credit is
an oblivious write that reveals nothing about which member it touched.
Still visible: the payer's address, each payment's amount and timing (the USDC pull into the
contract is a normal transfer), and your withdrawals (plain USDC from the contract to your
withdrawal address). Withdrawing round amounts on your own schedule keeps withdrawals hard to
correlate with your revenue.
## Enabling it
Open the **Confidential** tab in the dashboard and connect your withdrawal wallet on Base.
Enabling assigns you an encrypted slot on-chain and takes a few seconds.
The withdrawal address is **permanent**. The contract rejects any attempt to reassign it, and
no Solvador key can override that. It must be a wallet you hold the keys to: withdrawals are
signed by this exact wallet, so exchange deposit addresses will not work. Losing the wallet
means losing access to future withdrawals.
## Offer modes
After enabling, pick how your 402 responses advertise the option:
- **Default**: the plain offer only, straight to your address.
- **Confidential**: the private variant only.
- **Both**: two entries in one 402, confidential listed first. Clients pick the first entry
they support, so confidential-aware clients go private and everything else keeps working.
Your resource server can follow the dashboard selection live, with no redeploys, by asking the
facilitator before building each 402:
```
GET /confidential/status
X-API-Key:
```
```json
{
"available": true,
"network": "eip155:8453",
"contractAddress": "0xd40a3e9ef28e3a711b079ea4ba1fb0c7b031f7b2",
"configured": true,
"mode": "both",
"enabled": true,
"merchantAddress": "0xYourWithdrawalAddress"
}
```
Branch on `mode`. A confidential offer sets `payTo` to the contract and marks itself in
`extra`:
```json
{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xd40a3e9ef28e3a711b079ea4ba1fb0c7b031f7b2",
"extra": {
"confidential": true,
"merchant": "0xYourWithdrawalAddress",
"assetTransferMethod": "eip3009"
}
}
```
`extra.merchant` tells the facilitator whose encrypted slot to credit. It is read off-chain
only and never written to the chain in plaintext. Settlement is rejected unless the merchant
belongs to the API key that submits it, so another key cannot route payments into your slot,
and you cannot route payments into someone else's.
## Receipts
Every confidential settlement returns a **signed receipt** in the settle response: the payment
nonce, your merchant address, the amount, and a facilitator signature over them. Receipts are
your proof of payment (the chain deliberately cannot provide one) and they are directly
redeemable on-chain. If a credit were ever missing, the receipt alone claims the funds from the
contract without Solvador's cooperation. Keep them; the dashboard also stores and lists every
receipt for you.
## Credits and withdrawals
Credits are applied in a daily batch at **00:00 UTC**. Batching is part of the privacy design:
each merchant gets at most one encrypted write per day regardless of how many payments arrived,
so payment patterns don't leak through credit timing. Until the daily flush, a payment shows as
**pending** in the dashboard; after it, the amount is **available**.
Withdrawals spend available balance only and are non-custodial end to end:
1. Your wallet signs the withdrawal request. The debit is applied obliviously, so an
insufficient balance reveals nothing on-chain.
2. Your wallet decrypts the confirmation attestation in the browser. Only your wallet can.
3. Solvador submits the finalization and pays its gas. USDC arrives at your withdrawal
address.
If finalization ever stalls for 24 hours, the debit can be reversed on-chain with
`cancelWithdraw` from your wallet. No Solvador key can pause, block, or seize withdrawals; the
contract enforces that only your wallet moves your funds.
(browser + wallet)
participant S as Solvador
(facilitator)
participant C as Contract
(Base mainnet)
participant T as Inco TEE
(covalidators)
W->>C: requestWithdraw()
Note over C: optimistic encrypted debit,
insufficient balance leaks nothing
C-->>W: okFlag handle
W->>T: attestedDecrypt()
Note over W,T: only the merchant wallet
can decrypt
T-->>W: okValue + signatures
W->>S: id, okValue, signatures
S->>C: finalizeWithdraw(), gas sponsored
C-->>W: USDC to the withdrawal address`}
/>
## Contract
The settlement contract is deployed on Base mainnet at
[`0xd40a3e9ef28e3a711b079ea4ba1fb0c7b031f7b2`](https://basescan.org/address/0xd40a3e9ef28e3a711b079ea4ba1fb0c7b031f7b2),
with source verified on Blockscout and Sourcify. Any confidential-tier fee is shown in the
dashboard before you enable; the payer never pays more than the advertised price.
---
# API Overview
URL: https://docs.solvador.com/api
Section: API Reference
> Base URL, authentication, and conventions for the Solvador facilitator API.
## Base URL
```
https://api.solvador.com
```
Solvador implements the standard **x402 v2 facilitator interface** — any x402 SDK's `HTTPFacilitatorClient` works against it unchanged. You can also call the endpoints directly; every request and response body is JSON.
## Endpoints
| Method | Path | Auth |
| --- | --- | --- |
| `POST` | [`/verify`](/api/verify) | `X-API-Key` |
| `POST` | [`/settle`](/api/settle) | `X-API-Key` |
| `GET` | [`/supported`](/api/supported) | None |
| `GET` | [`/discovery/resources`](/api/discovery) | None |
| `GET` | [`/discovery/search`](/api/discovery) | None |
## Authentication
`/verify` and `/settle` require authentication: pass your API key in the `X-API-Key` header. `/supported` and the discovery endpoints are open. Keys are created in the [dashboard](/platform/api-keys). A missing or invalid key returns `401` with a JSON body:
```json
{ "error": "invalid API key" }
```
## Errors
Transport-level errors use a JSON envelope of the form `{ "error": ... }` with these status codes:
| Status | Meaning |
| --- | --- |
| `400` | Malformed request — e.g. an invalid [`payment-identifier`](/extensions#payment-identifier) |
| `401` | Missing or invalid API key (`/verify` and `/settle`) |
| `402` | Plan quota exhausted, or pay-as-you-go account suspended — see [Plans & Quotas](/platform/plans) |
| `409` | `payment-identifier` conflict: same `id`, different payment |
| `500` | Internal error; the body's `error` field carries the message |
Note the x402-level distinction: a payment that **fails verification** is still HTTP `200` — the body carries `isValid: false` with an `invalidReason`. Likewise a **failed settlement** is HTTP `200` with `success: false` and an `errorReason`. HTTP error codes are reserved for problems with the request itself, not with the payment.
## Versioning
Request and response bodies follow x402 **v2**: every verify/settle request carries `"x402Version": 2` alongside the payment payload and requirements.
---
# POST /verify
URL: https://docs.solvador.com/api/verify
Section: API Reference
> Check that a payment payload is valid and settleable. Requires an API key; free and unmetered.
```
POST https://api.solvador.com/verify
```
Validates a payment payload against payment requirements: signature, payer balance, amount, timing, and scheme-specific rules. `/verify` requires your [API key](/platform/api-keys) in the `X-API-Key` header. It consumes no quota and has no on-chain side effects, so verify as often as you like.
## Request
```json
{
"x402Version": 2,
"paymentPayload": {
"x402Version": 2,
"accepted": {
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xYourReceivingAddress",
"maxTimeoutSeconds": 300,
"extra": {}
},
"payload": {
"authorization": { "from": "0xPayerAddress", "...": "..." },
"signature": "0x…"
}
},
"paymentRequirements": {
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xYourReceivingAddress",
"maxTimeoutSeconds": 300,
"extra": {}
}
}
```
- **`paymentPayload`** — the signed payload produced by an x402 client. Its inner `payload` object is scheme-specific; client SDKs construct it, and your server passes it through untouched.
- **`paymentRequirements`** — the requirements your server advertised in its `402` response, echoed back so Solvador can check the payload against them.
## Response
Always HTTP `200` for a processed verification, valid or not:
```json
{
"isValid": true,
"payer": "0xPayerAddress"
}
```
```json
{
"isValid": false,
"invalidReason": "insufficient_funds",
"invalidMessage": "payer balance is below the required amount",
"payer": "0xPayerAddress"
}
```
| Field | Type | Description |
| --- | --- | --- |
| `isValid` | boolean | Whether the payment can be settled as-is |
| `invalidReason` | string? | Machine-readable failure code, present when `isValid` is `false` |
| `invalidMessage` | string? | Human-readable detail |
| `payer` | string? | The paying address recovered from the payload |
| `extensions` | object? | Per-extension response data |
## Bazaar side effect
If the payload opts into the [`bazaar` extension](/extensions#bazaar-resource-discovery) and verifies successfully, the resource is cataloged for discovery. The cataloging outcome is reported in the `EXTENSION-RESPONSES` response header.
## Errors
| Status | Meaning |
| --- | --- |
| `400` | Malformed [`payment-identifier`](/extensions#payment-identifier) in the payload |
| `401` | Missing or invalid API key |
| `500` | Internal error |
---
# POST /settle
URL: https://docs.solvador.com/api/settle
Section: API Reference
> Execute a verified payment on-chain. Requires an API key.
```
POST https://api.solvador.com/settle
```
Submits the payment on-chain and returns the result. Requires your API key in the `X-API-Key` header. Successful mainnet settlements count against your [plan quota](/platform/plans) — except on the XRP Ledger, which is always free of quota.
Every settlement (successful or failed) is recorded and appears in your [dashboard](https://dashboard.solvador.com) with its network, scheme, payer, amount, and transaction hash. To be notified the moment a settlement runs instead of polling, subscribe a [webhook](/platform/webhooks) to `settlement.succeeded` and `settlement.failed`.
## Request
The body is identical in shape to [`/verify`](/api/verify) — `x402Version`, `paymentPayload`, `paymentRequirements`:
```bash
curl https://api.solvador.com/settle \
-H "Content-Type: application/json" \
-H "X-API-Key: $SOLVADOR_KEY" \
-d '{
"x402Version": 2,
"paymentPayload": { "…": "…" },
"paymentRequirements": {
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xYourReceivingAddress",
"maxTimeoutSeconds": 300,
"extra": {}
}
}'
```
## Response
Always HTTP `200` for a processed settlement, successful or not:
```json
{
"success": true,
"transaction": "0x6fe1…c40b",
"network": "eip155:8453",
"payer": "0xPayerAddress"
}
```
```json
{
"success": false,
"errorReason": "invalid_signature",
"errorMessage": "authorization signature does not match the payer",
"transaction": "",
"network": "eip155:8453"
}
```
| Field | Type | Description |
| --- | --- | --- |
| `success` | boolean | Whether the payment settled on-chain |
| `transaction` | string | On-chain transaction hash / identifier (empty on failure) |
| `network` | string | CAIP-2 network the settlement ran on |
| `payer` | string? | The paying address |
| `amount` | string? | Actual settled amount in atomic units — present when it can differ from the authorized maximum, as with [`upto`](/schemes/upto) |
| `errorReason` | string? | Machine-readable failure code, present when `success` is `false` |
| `errorMessage` | string? | Human-readable detail |
| `extensions` | object? | Per-extension response data |
## Idempotency
Include a [`payment-identifier`](/extensions#payment-identifier) `id` in the payload and retries become safe: re-sending the same `id` with the same payment replays the cached response verbatim — no second on-chain transaction, no additional quota. The same `id` with a *different* payment is rejected with `409`. A failed settle releases the `id` so your retry re-executes.
## Quota
When your monthly quota is exhausted, `/settle` returns `402` with an explanatory body — `/verify` keeps working:
```json
{
"error": "quota_exceeded",
"plan": "free",
"limit": 10,
"usedThisMonth": 10,
"upgrade": "/api/billing/upgrade"
}
```
Upgrading in the [dashboard's Plans tab](/platform/plans) takes effect immediately. XRP Ledger settlements never count against quota.
## Errors
| Status | Meaning |
| --- | --- |
| `400` | Malformed [`payment-identifier`](/extensions#payment-identifier) |
| `401` | Missing or invalid API key |
| `402` | Quota exhausted, or pay-as-you-go account suspended |
| `409` | `payment-identifier` conflict: same `id`, different payment |
| `500` | Internal error |
---
# GET /supported
URL: https://docs.solvador.com/api/supported
Section: API Reference
> Machine-readable list of every scheme, network, and extension this facilitator supports.
```
GET https://api.solvador.com/supported
```
No authentication. Use it for capability discovery instead of hardcoding networks or schemes — new networks and schemes appear here first.
## Response
```json
{
"kinds": [
{ "x402Version": 2, "scheme": "exact", "network": "eip155:8453" },
{ "x402Version": 2, "scheme": "upto", "network": "eip155:8453" },
{
"x402Version": 2,
"scheme": "batch-settlement",
"network": "eip155:8453",
"extra": { "receiverAuthorizer": "0xAuthorizerAddress" }
},
{ "x402Version": 2, "scheme": "exact", "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" },
{ "x402Version": 2, "scheme": "exact", "network": "near:mainnet" },
{
"x402Version": 2,
"scheme": "exact",
"network": "xrpl:0",
"extra": { "areFeesSponsored": false }
},
{
"x402Version": 2,
"scheme": "exact",
"network": "starknet:SN_MAIN",
"extra": { "feePayer": "0xExecutorAddress" }
}
],
"extensions": ["eip2612GasSponsoring", "bazaar", "payment-identifier"],
"signers": {
"eip155": ["0xFacilitatorSigner"],
"solana": ["FacilitatorSignerPubkey"],
"starknet:*": ["0xExecutorAddress"]
}
}
```
The example above is truncated — the real response lists every (scheme, network) pair: `exact` on all 15 networks, `upto` and `batch-settlement` on the 11 EVM networks. On Starknet the `extra.feePayer` value is required by clients: it must be copied verbatim into the payment requirements, and the payer's signature is bound to it.
| Field | Description |
| --- | --- |
| `kinds` | Every supported (scheme, network) pair, with optional scheme-specific `extra` data |
| `extensions` | [Protocol extensions](/extensions) this facilitator advertises |
| `signers` | Network family → the facilitator's signing addresses on it |
Notable `extra` values:
- **`batch-settlement` kinds** carry `extra.receiverAuthorizer` — the EIP-712 authorizer address that must be committed into escrow channels at deposit time. Always read it from here; see [batch-settlement](/schemes/batch-settlement#the-receiver-authorizer).
- **The XRPL kind** carries `extra.areFeesSponsored: false` — the payer's signed transaction pays its own network fee (see [Supported Networks](/networks#xrp-ledger)).
---
# Discovery API
URL: https://docs.solvador.com/api/discovery
Section: API Reference
> Query the Bazaar catalog of x402 resources that verify and settle through Solvador.
The **Bazaar** is an open catalog of x402-payable resources. A resource enters the catalog only when a payment for it actually verifies through Solvador — spam can't get in without paying, by construction. Resource servers opt in by declaring the [`bazaar` extension](/extensions#bazaar-resource-discovery) on their routes so clients echo it in payment payloads.
Both endpoints are public — no authentication.
## GET /discovery/resources
```
GET https://api.solvador.com/discovery/resources
```
Lists cataloged resources, newest first. All query parameters are optional:
| Parameter | Type | Description |
| --- | --- | --- |
| `type` | string | Resource type filter |
| `payTo` | string | Filter by receiving address |
| `scheme` | string | Filter by payment scheme (`exact`, `upto`, `batch-settlement`) |
| `network` | string | Filter by CAIP-2 network (e.g. `eip155:8453`) |
| `extensions` | string | Filter by declared extensions |
| `limit` | number | Page size |
| `offset` | number | Pagination offset |
```bash
curl "https://api.solvador.com/discovery/resources?network=eip155:8453&limit=10"
```
A catalog entry describes the resource and the payment kinds it accepts:
```json
{
"resource": "https://api.example.com/reports",
"type": "http",
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"payTo": "0xReceivingAddress"
}
],
"lastUpdated": "2026-07-01T12:00:00.000Z"
}
```
## GET /discovery/search
```
GET https://api.solvador.com/discovery/search?query=weather
```
Full-text search over the catalog. `query` is **required**; the same filters as `/discovery/resources` (`type`, `payTo`, `scheme`, `network`, `extensions`, `limit`) can be combined with it.
```bash
curl "https://api.solvador.com/discovery/search?query=weather&scheme=exact"
```
## Errors
| Status | Meaning |
| --- | --- |
| `400` | `/discovery/search` called without a `query` parameter |
| `500` | Internal error |
---
# API Keys
URL: https://docs.solvador.com/platform/api-keys
Section: Platform
> Create and manage the keys that authenticate your /settle calls.
API keys are the only credential your integration needs. They authenticate [`POST /verify`](/api/verify) and [`POST /settle`](/api/settle); every settlement made with a key is attributed to your account and appears in your dashboard.
## Creating a key
1. Sign in at [dashboard.solvador.com](https://dashboard.solvador.com) with Google or GitHub.
2. Open the **API Keys** tab.
3. Create a key and give it a recognizable name.
The full key is displayed exactly once, at creation. After that the dashboard only shows a masked prefix. Store it immediately in your secret manager or environment — if it's lost, delete the key and create a new one.
## Using a key
Pass the key in the `X-API-Key` header. Both `/verify` and `/settle` require it. `/supported` and the discovery endpoints are open. With the x402 SDK, inject it via the `createAuthHeaders` hook:
```ts
const facilitator = new HTTPFacilitatorClient({
url: "https://api.solvador.com",
createAuthHeaders: async () => ({
verify: auth,
settle: auth,
supported: {},
}),
});
```
## Key hygiene
- Keep keys **server-side only** — never ship one in client or browser code.
- Load keys from environment variables or a secret manager, not from source control.
- Create **separate keys for staging and production**; you can hold multiple keys per account.
- Rotate by creating a new key, deploying it, then deleting the old one — zero downtime.
## The rest of the dashboard
Beyond API keys, the [dashboard](https://dashboard.solvador.com) gives you **Overview** (settlement activity at a glance), **Metrics** (settlements and volume over time, broken down by network and asset), **Settlements** (every settlement attributed to your keys, with network, scheme, payer, amount, and transaction hash), and **Plans** ([quota and billing](/platform/plans)).
---
# Webhooks
URL: https://docs.solvador.com/platform/webhooks
Section: Platform
> Receive a signed HTTP callback on every settlement. Subscribe to settlement.succeeded and settlement.failed, verify the HMAC-SHA256 signature, and reconcile payments without polling.
Webhooks push a signed HTTP request to your server every time a settlement runs, so you do not have to poll [`/settle`](/api/settle) results or the dashboard. Each settlement that is attributed to one of your [API keys](/platform/api-keys) can notify one or more endpoints you configure, and success and failure are separate events you subscribe to independently.
A webhook is the recommended way to fulfill orders, credit balances, send receipts, or update your own database the moment a payment settles onchain.
## Creating an endpoint
1. Sign in at [dashboard.solvador.com](https://dashboard.solvador.com) with Google or GitHub.
2. Open the **Webhooks** tab.
3. Click **Add endpoint**, enter the HTTPS URL that will receive deliveries, and choose which events to subscribe to.
4. Copy the **signing secret** shown after creation. You will use it to verify that every delivery genuinely came from Solvador.
You can register several endpoints. Each one has its own signing secret and its own set of subscribed events, so you can, for example, send successes to your fulfillment service and failures to an alerting service.
The signing secret authenticates every delivery. Treat it like a password: keep it server-side, never commit it to source control, and never expose it in client code. You can re-reveal or roll the secret at any time from the endpoint's actions in the dashboard.
## Events
You subscribe each endpoint to one or both of these event types. One settlement produces exactly one event.
| Event | When it fires |
| --- | --- |
| `settlement.succeeded` | A settlement completed onchain (`success: true` from [`/settle`](/api/settle)). |
| `settlement.failed` | A settlement was processed but did not succeed (`success: false`, for example an invalid signature or insufficient funds). |
A settlement that throws a transport or internal error (an HTTP `500` from `/settle`) does not produce an event, because no settlement result was recorded. Only processed settlements, successful or failed, are delivered.
For batched settlement schemes such as [`batch-settlement`](/schemes/batch-settlement), each underlying payment in the batch produces its own event. Several events can therefore arrive close together, and they will share the same onchain `txHash`.
## Event payload
Every delivery is a JSON `POST` with an envelope that wraps the settlement data. A `settlement.succeeded` body looks like this:
```json
{
"id": "evt_9f3c8b2a-1d4e-4a77-9b1c-2c0f7e5a1b3d",
"type": "settlement.succeeded",
"created": "2026-07-21T12:34:56.000Z",
"data": {
"id": "9f3c8b2a-1d4e-4a77-9b1c-2c0f7e5a1b3d",
"network": "eip155:8453",
"scheme": "exact",
"payer": "0xPayerAddress",
"payee": "0xYourReceivingAddress",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"txHash": "0x6fe1…c40b",
"status": "settled",
"opType": null,
"units": 1,
"settledAt": "2026-07-21T12:34:56.000Z"
}
}
```
A `settlement.failed` body carries the same envelope, with `status` set to `failed` and two extra fields inside `data` that explain the failure:
```json
{
"id": "evt_5a1b3d2c-…",
"type": "settlement.failed",
"created": "2026-07-21T12:35:10.000Z",
"data": {
"id": "5a1b3d2c-…",
"network": "eip155:8453",
"scheme": "exact",
"payer": "0xPayerAddress",
"payee": "0xYourReceivingAddress",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "10000",
"txHash": "",
"status": "failed",
"opType": null,
"units": 0,
"settledAt": "2026-07-21T12:35:10.000Z",
"errorReason": "invalid_signature",
"errorMessage": "authorization signature does not match the payer"
}
}
```
### Envelope fields
| Field | Type | Description |
| --- | --- | --- |
| `id` | string | Unique event id, formatted `evt_`. Stable across retries and across endpoints, so use it to deduplicate. |
| `type` | string | `settlement.succeeded` or `settlement.failed`. |
| `created` | string | ISO 8601 timestamp of the settlement. |
| `data` | object | The settlement, described below. Mirrors the row shown in your dashboard's Settlements tab. |
### `data` fields
| Field | Type | Description |
| --- | --- | --- |
| `id` | string | The settlement (transaction) id. Same value used to build the event `id`. |
| `network` | string | CAIP-2 network the settlement ran on, for example `eip155:8453` or `solana:5eykt4Us…`. |
| `scheme` | string | The payment scheme: `exact`, `upto`, or `batch-settlement`. |
| `payer` | string | The paying address. |
| `payee` | string | The receiving address (`payTo`). |
| `asset` | string | Token contract or mint address. |
| `amount` | string | Settled amount in atomic units, as a string to stay big-integer safe. |
| `txHash` | string | Onchain transaction hash. Shared across a batch, and empty when a failed settlement produced no transaction. |
| `status` | string | `settled` or `failed`. |
| `opType` | string? | The batch operation type (`deposit`, `claim`, `settle`, `refund`) for `batch-settlement`, or `null` for `exact` and `upto`. |
| `units` | number | Billable payment units for this settlement. |
| `settledAt` | string | ISO 8601 timestamp, equal to the envelope `created`. |
| `errorReason` | string? | Machine-readable failure code. Present only on `settlement.failed`. |
| `errorMessage` | string? | Human-readable failure detail. Present only on `settlement.failed`. |
## Delivery headers
Every delivery carries these headers:
| Header | Description |
| --- | --- |
| `Content-Type` | Always `application/json`. |
| `Solvador-Signature` | The HMAC signature, formatted `t=,v1=`. See [Verifying the signature](#verifying-the-signature). |
| `Solvador-Event-Id` | The envelope `id` (`evt_…`). Use it to deduplicate. |
| `Solvador-Event-Type` | `settlement.succeeded` or `settlement.failed`. |
| `Solvador-Delivery-Id` | A per-attempt-series id (`whd_…`) for support and debugging. |
| `User-Agent` | `Solvador-Webhooks/1.0`. |
## Verifying the signature
Every delivery is signed with HMAC-SHA256 using your endpoint's signing secret. Verifying the signature proves the request came from Solvador and that the body was not altered in transit. Always verify before you act on a delivery.
The `Solvador-Signature` header has the form `t=,v1=`, where:
- `t` is the unix timestamp (in seconds) when the request was signed.
- `v1` is the lowercase hex HMAC-SHA256 of the timestamp, a literal dot, and the raw body joined together, that is `t + "." + rawBody`, keyed with your signing secret. `rawBody` is the raw request body bytes, exactly as received.
To verify:
1. Read the raw request body as a string, before any JSON parsing or re-serialization.
2. Read `t` and every `v1` value from the `Solvador-Signature` header.
3. Compute `HMAC_SHA256(secret, t + "." + rawBody)` and hex-encode it.
4. Compare it against each `v1` in constant time. Accept if any matches.
5. Optionally reject deliveries whose `t` is more than a few minutes old, to bound replay.
Sign and verify the **raw request body**, not a parsed then re-serialized object. Re-serializing can reorder keys or change whitespace, which changes the bytes and breaks the signature. In Express, capture the raw body (for example with `express.raw({ type: "application/json" })`) on the webhook route.
A Node.js example, framework-agnostic aside from how you obtain the raw body:
```js
const crypto = require("crypto");
// rawBody: the exact request body string
// header: the value of the Solvador-Signature header
// secret: your endpoint's signing secret (whsec_...)
function verifySolvadorWebhook(rawBody, header, secret) {
const parts = header.split(",").map((p) => p.trim());
const t = parts.find((p) => p.startsWith("t="))?.slice(2);
const signatures = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
if (!t || signatures.length === 0) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`, "utf8")
.digest("hex");
// A roll can send old and new signatures during a grace window: accept any match.
const signatureOk = signatures.some(
(sig) =>
sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)),
);
if (!signatureOk) return false;
// Optional replay window: reject if older than 5 minutes.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(t));
return ageSeconds <= 300;
}
```
An Express handler that uses it:
```js
const express = require("express");
const app = express();
app.post(
"/webhooks/solvador",
express.raw({ type: "application/json" }),
(req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verifySolvadorWebhook(
rawBody,
req.header("Solvador-Signature") ?? "",
process.env.SOLVADOR_WEBHOOK_SECRET,
);
if (!ok) return res.status(400).send("invalid signature");
const event = JSON.parse(rawBody);
// Deduplicate on event.id, then handle the event.
// Respond fast; do slow work asynchronously.
res.status(200).send("ok");
},
);
```
## Responding to a delivery
Return any `2xx` status to acknowledge a delivery. Anything else, including `3xx` redirects, counts as a failure and is retried.
- Respond within **10 seconds**. Deliveries time out after that and are treated as failed.
- Respond first, then do slow work asynchronously. Do not run long database writes or downstream calls before you acknowledge.
- The response body is ignored. Only the status code matters.
## Retries and failure handling
If a delivery does not return `2xx`, Solvador retries it with an increasing backoff, up to **8 attempts** spread over roughly **24 hours**:
| Attempt | Sent after the previous failure |
| --- | --- |
| 1 | immediately |
| 2 | 10 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 6 hours |
| 8 | 12 hours |
After the eighth attempt fails, the delivery is marked `failed` and is not retried again. An endpoint that keeps failing for a long streak is automatically disabled to protect your server and ours; a disabled endpoint stops receiving events until you re-enable it from the dashboard, which also clears its failure count.
Because a settlement never depends on webhook delivery, an outage on your side never affects payments. Deliveries are simply queued, retried, and, if they keep failing, recorded as failed in the delivery log.
## Idempotency and ordering
Delivery is at-least-once, so the same event can arrive more than once (for example when your server returns `2xx` after a network timeout already counted the attempt as failed).
- **Deduplicate on the event `id`** (also sent as the `Solvador-Event-Id` header). It is stable across retries. Make your handler idempotent, so processing the same event twice is a no-op.
- **Do not rely on ordering.** Concurrent delivery, independent retries, and batch fan-out mean events can arrive out of order. Key your state on the settlement, using `txHash` or the event `id`, rather than on arrival order.
## Testing and the delivery log
The **Webhooks** tab gives you tools to develop and debug against real deliveries:
- **Send test** delivers a synthetic `settlement.succeeded` event to a single endpoint, so you can confirm your receiver and signature verification work end to end before real traffic arrives.
- **Deliveries** opens the delivery log for an endpoint: recent attempts with their status, the HTTP status you returned, and the attempt count.
- **Redeliver** re-queues an existing delivery, which is useful after you fix a bug on your side.
During local development you can point an endpoint at a tunnel such as [webhook.site](https://webhook.site) or an HTTPS tunnel to your machine, then use **Send test** to inspect the exact headers and body.
## Rotating the signing secret
You can reveal the current signing secret or roll it at any time from the endpoint's actions.
Rolling generates a new secret and invalidates the old one. To rotate without downtime, roll in the dashboard, then update the secret in your environment and redeploy. During a roll's grace window a delivery may be signed with both the old and the new secret, sending two `v1` values in the `Solvador-Signature` header, so a verifier that accepts any matching `v1` (as in the example above) keeps working across the change.
## Security
- **HTTPS only.** Webhook URLs must use `https://`. Plain `http://` is rejected.
- **No private targets.** URLs that resolve to loopback, link-local, or private address ranges (for example `localhost`, `127.0.0.1`, `10.0.0.0/8`, or the cloud metadata address `169.254.169.254`) are rejected, and the address is re-checked at delivery time to prevent DNS rebinding.
- **Signing secrets are stored encrypted** and are only ever revealed to you, the account owner, in the dashboard. Solvador signs each delivery on the server side, so the secret never leaves your account.
- **Keep your receiver's secret server-side.** Never verify signatures in client code.
---
# Plans & Quotas
URL: https://docs.solvador.com/platform/plans
Section: Platform
> Free, Starter, Pro, Max, and Pay-as-you-go — what counts as a settlement unit and what each plan includes.
Plans meter **settlement units** per calendar month. Verification authenticates with the same API key but is always free and unmetered.
| Plan | Settlements / month | Price |
| --- | --- | --- |
| Free | 10 | $0 |
| Starter | 6,500 | $5 / mo |
| Pro | 45,000 | $39 / mo |
| Max | 260,000 | $229 / mo |
| Pay-as-you-go | No cap | $0.001 per settlement after 1,000 free each month |
## What counts as a settlement unit
- A successful [`exact`](/schemes/exact) or [`upto`](/schemes/upto) settle = **1 unit**.
- A [`batch-settlement`](/schemes/batch-settlement) `claim` = **1 unit per voucher** it redeems; `deposit`, `settle`, and `refund` operations = 0 units.
- Failed settlements and [idempotent replays](/extensions#payment-identifier) = **0 units**.
- Only settlements on quota-counted mainnets consume units — which excludes the XRP Ledger. All other networks count, including Starknet, where Solvador's executor sponsors the gas:
## XRPL is unlimited
Settlements on the XRP Ledger (`xrpl:0`) **never count against quota, on any plan** — including Free. XRPL payments are payer-signed transactions that carry their own network fee, so there's nothing to meter.
## Pay-as-you-go
Pay-as-you-go replaces the monthly cap with metered billing:
- The first **1,000 units each calendar month (UTC)** are free.
- Each unit beyond that costs **$0.001**.
- Usage accrues as a balance; your card on file is charged when the balance crosses **$5** (rounded up to the whole cent).
- Starting pay-as-you-go requires adding a card in the dashboard. If an automatic charge fails repeatedly, the account is suspended until the card is updated — while suspended, mainnet `/settle` calls return `402` (XRPL keeps working).
## When quota runs out
`/settle` on a quota-counted mainnet returns `402` with an explanatory body ([example](/api/settle#quota)); `/verify` keeps working. Upgrading takes effect immediately.
## Managing your plan
Everything lives in the **Plans** tab of the [dashboard](https://dashboard.solvador.com): upgrades, pay-as-you-go setup, and cancellation. Cancelling a paid tier keeps it active until the end of the current billing period.
---
# Swap Settlement
URL: https://docs.solvador.com/experimental/swap-settlement
Section: Experimental
> Pay in one token, settle in another. Experimental — not yet available.
Swap settlement is experimental and **not yet available** on Solvador. This page describes the direction of the feature — there is no API surface to integrate against today, and nothing here is a commitment to a particular design.
Today, the payer must hold the exact asset a resource server prices in — a server charging USDC on Base is paid in USDC on Base. **Swap settlement** explores relaxing that: letting a payer pay in a different token, with conversion happening at settlement time, so servers keep pricing in their preferred stablecoin without narrowing who can pay them.
Nothing about swap settlement appears in [`GET /supported`](/api/supported) — when the feature ships, it will be advertised there first, and this page will move out of Experimental with full integration docs.
Interested, or have a use case that needs this? Tell us on [Discord](https://discord.gg/53XpC68sqF) or follow [the blog](https://blog.solvador.com) for availability news.