Documentation. Your keys never move.

The complete reference for building on stealthPAY: the @stealthsend/x402 server SDK, the self-hosted sidecar, the facilitator API, and the viewing-key Console. Settlement is feeless native XST; the private rail is XSS — same SDK, same flow. Everything speaks the open x402 standard (v2). For the conceptual tour, read developers.

01

Quickstart

Gate a route, pay it from an agent

Install the SDK, wrap a route with paymentRequired, and price it in atomic XST units (1 XST = 1,000,000 units; the transparent floor is 10000 = 0.01 XST). An unpaid request gets a 402; the facilitator settles; your handler runs with the receipt on req.x402. Zero dependencies, Node ≥ 18, ESM.

server · gate a route
api-server.mjs · node:http
// npm i @stealthsend/x402 — zero deps, Node >= 18, ESM import { createServer } from "node:http"; import { paymentRequired } from "@stealthsend/x402"; const gate = paymentRequired({ facilitator: process.env.X402_FACILITATOR, // http://127.0.0.1:8402 amount: "10000", // atomic units = 0.01 XST description: "Generate an agent codename", settleMode: "confirmed", // wait for 1-conf (~5s) }); createServer((req, res) => { gate(req, res, () => { // reached only after settlement; receipt is on req.x402 res.end(JSON.stringify({ ok: true, paidWith: req.x402.transaction })); }); }).listen(8800);
402 if unpaid · 200 once settledServer stays stateless
agent · pay a 402
agent-runner.mjs · xst-wallet-napi
// 1. the call returns 402 with the payment requirements const r1 = await fetch(url); const body = await r1.json(); // { x402Version:2, error, resource, accepts:[…] } const req = body.accepts[0]; // 2. build + grind + sign the feeless XST payment (in your wallet/sidecar) const header = await wallet.buildX402Payment(JSON.stringify(req), body.resource.url); // 3. replay with the PAYMENT-SIGNATURE header — served once settled const r2 = await fetch(url, { headers: { "PAYMENT-SIGNATURE": header } });
Channel · XST / XSSThe spending key never leaves the wallet
Both halves are lifted from the runnable demo at demo/x402-agents — named agents paying a paywalled “AI API” per call, request → 402 → build+grind+sign → retry → 200 in about a block.
02

Concepts & keys

Who holds what

Four parties, four credentials. Only the wallet holds a key that can move funds. There is no “sign up” on the pay side — just Connect, which generates a key on first run (non-custodial, seed shown once) or signs a challenge if a wallet is present. The server stores nothing.

Your keys, your funds, our brain. The pay side authenticates by signature; the Console (the SaaS governance layer — seats, tiers, visibility) authenticates humans by magic link / OAuth; agent spend authority is a bearer token mapped to a capped sub-account — never a raw key on the wire.
03

Server SDK

@stealthsend/x402

Two pieces, zero dependencies: paymentRequired(options) middleware for Express / Connect / plain node:http, and FacilitatorClient, a typed wrapper over the facilitator surface.

paymentRequired optionTypeDetail
facilitatorstring (URL)Facilitator base URL, e.g. http://127.0.0.1:8402.
amountstring | numberAtomic units (1 XST = 1e6). "10000" = 0.01 XST.
resourcestringOptional. Defaults to the request URL.
descriptionstringOptional. Shown in the challenge.
settleMode'confirmed' | 'mempool' | 'auto'Finality to wait for. auto picks by amount.
timeoutSecondsnumberOptional. Clamped 10–110; default 90.

On a request with no PAYMENT-SIGNATURE header it mints a challenge and answers 402 with the standard body. With the header it settles (the facilitator resolves the challenge from the payment, so the server stays stateless), sets PAYMENT-RESPONSE (base64 JSON), exposes the receipt as req.x402, and calls next().

the 402 body (x402 v2)
HTTP 402
{ "x402Version": 2, "error": "payment required", "resource": { "url": "https://api.example.com/insight" }, "accepts": [{ "scheme": "exact", "network": "stealth:mainnet", "amount": "10000", "asset": "XST", "payTo": "S…", // fresh, single-use "maxTimeoutSeconds": 90, "extra": { "anchorHeight": 145678, "anchorHash": "…" } }] }
Request header PAYMENT-SIGNATUREResponse header PAYMENT-RESPONSE
FacilitatorClient(base)CallsReturns
supported()GET /supported{ kinds: [{ x402Version, scheme, network }] }
challenge(opts)POST /challengePaymentRequirements (fresh payTo + tip anchor)
verify(header, reqs?)POST /verify{ isValid, invalidReason? } — advisory
settle(header, reqs?)POST /settle{ success, network, transaction, extensions, errorReason? }
04

Facilitator API

x402-facilitatord · default :8402

The facilitator verifies and settles. It holds no spend keys and no funds at risk. CORS is open (*); only /grind is authenticated. Protocol is x402 v2.

EndpointDoesDetail
GET /supportedScheme / network{ kinds:[{ x402Version:2, scheme:"exact", network }] }
POST /challengeMint a 402Body needs exactly one of amount | fiat. Returns PaymentRequirements with a fresh single-use payTo and a tip anchor.
POST /verifyAdvisory check{ x402Version:2, paymentHeader, paymentRequirements? }{ isValid, invalidReason? }. Read-only, never broadcasts.
POST /settleBroadcast + confirmSame body. Re-verifies, broadcasts, waits for finality. Idempotent — extensions.alreadySettled marks a replay.
POST /grind extAssisted feeworkBearer auth. { transaction, anchorHeight, anchorHash, mcost }{ work }. For thin clients; carries no keys.
GET /stats extTelemetry{ settledCount, totalXst, recent:[…] } — the live settlement feed.
GET /rate extFiat referenceOperator-attested XST/USD TWAP. 404 when fiat quoting is off — stealthPAY never operates rate infra.
Agents price and policies cap in XST. A fiat equivalent is an optional convenience from a thin external feed; because the facilitator never holds XST, its price is never our risk to carry.
05

Sidecar

Self-hosted signer + policy engine

Runs in your infra, bound to 127.0.0.1. Holds only capped agent credentials (model A), enforces caps / rate / allowlists, signs and settles. Every route except /healthz takes the agent’s bearer token (Authorization: Bearer sk_…).

config.json
sidecar
{ "facilitator": "http://127.0.0.1:8402", "network": "stealth:testnet", "port": 8088, "ledgerPath": "./sidecar-ledger.json", // caps survive restart "operatorXpub": "xpub…", // model A: derives each capped sub-account "agents": { "research-agent-v2": { "token": "sk_research_demo", // bearer auth for this agent "deriveIndex": 0, "capDaily": 25.0, // native XST "capTotal": 100.0, "ratePerHour": 5, "allow": ["api.example.com"], // resource host allowlist "enabled": true } } }
Bearer per agent · caps in native XSTKeys stay in your infra
EndpointBody / authReturns
GET /healthznone{ ok, mode, network, agents }
GET /policybearer{ agent, capDaily, capTotal, ratePerHour, allow, enabled }
GET /balancebearer{ spentDay, remainingDaily, spentTotal, remainingTotal, pending, rateLastHour }
POST /payx402 requirement (+ twoPhase)Policy-check, sign, settle. Returns the built payment + { ok, agent, amountXST, chargeId, pending }.
POST /settled{ chargeId }Capture a reserved charge → { ok, settledXST } (two-phase).
POST /cancel{ chargeId }Release a reserved charge → { ok, released:true }.
Reserved (in-flight) charges count against caps while pending, so two concurrent builds can’t both slip under one cap. /pay is idempotent on an Idempotency-Key (TTL 15 min); a duplicate in flight returns 409. With twoPhase:false a successful sign settles immediately; with true it waits for /settled or /cancel.
06

Console API

Read-only, org-scoped

The Console is the viewing-key dashboard — balances, per-agent spend, the live tx feed, receipts, and policy. Its API is read-only and scoped to an organization. Humans authenticate by magic link or OAuth; the viewing key decrypts shielded XSS data client-side, so the Console can see, never spend.

EndpointDoesDetail
GET /api/meSessionThe signed-in user + org memberships / roles.
GET /api/org/{id}/overviewSummaryOrg balances, spend, headline metrics.
GET /api/org/{id}/agentsAgentsPer-agent caps, spend, credential status.
GET /api/org/{id}/transactionsActivityThe settlement feed for the org.
GET /api/org/{id}/receiptsReceiptsSettled-payment receipts for export / reconciliation.
authSign inPOST /auth/magic/start + /auth/magic/verify, or /auth/oauth/{provider}/start + /callback; /auth/logout.
07

Networks & units

Two rails, one SDK
Transparent

stealthCORE · XST

Networks stealth:mainnet / stealth:testnet, asset XST. Feeless, ~5s blocks. Per-payment floor is MIN_TXOUT_AMOUNT = 0.01 XST (10000 units).
Shielded

stealthPRIVATE · XSS

Networks stealth-shielded:mainnet / …:testnet, asset XSS. No dust floor — true sub-cent pricing via pool settlement. Amount and counterparty stay off the public record.
Units. 1 XST = 1,000,000 atomic units (6 decimals); amounts on the wire are integer strings. Settle modes. confirmed waits for a block (~5s); mempool returns on broadcast; auto picks by size — at or above X402_AUTO_THRESHOLD (default 0.1 XST) it confirms, below it returns on mempool.
08

Shielded payments

The XSS rail, end to end

Same exact scheme, privacy carried by the network string. The flow differs in a few real ways:

09

Payment proofs

OVK disclosure · keyless verification

A shielded payment is private, so there is no public receipt. The payer can voluntarily disclose one payment with a self-contained PaymentProof, recovered from their own outgoing notes via the Outgoing Viewing Key. The OVK never leaves the wallet; verification needs no keys.

stealth-payment-proof (v1)
exported from the wallet
{ "version": 1, "type": "stealth-payment-proof", "network": "mainnet", "payment": { "cm": "…32 bytes hex…", "recipient_ak": "…8 bytes hex…", "value": 2500000, "memo": "invoice #4242", "rho0": "…", "rho1": "…" }, "inclusion": { "anchor_root": "…32 bytes hex…", "position": 1234, "path": ["…"] // POOL_DEPTH = 32 entries }, "reference": { "action_id": "…", "block_height": 145678 } }
One note per proof · sender-cooperativeaction_id links the agent action

Verification runs three keyless checks: the opening — H(value‖ak‖ρ₀‖ρ₁) = cm; inclusion — root_from_path(cm, position, path) = anchor_root; and anchor — that anchor_root is a canonical XSS anchor. The stealthPAY extension exports the proof from an XSS transaction; verification happens on StealthMonitor via POST /verify-disclosure (or GET /anchor/{root} against your own node).

10

Addresses

Validate before you send
KindFormatDetail
XST P2PKHBase58CheckVersion byte 0x3e (‘S’), sha256d checksum. The per-challenge transparent payTo.
XST stealth (v2)Base58CheckVersion 0x2b: flags ‖ scan_pubkey(33) ‖ spend_pubkey(33) ‖ prefix. DKSAP receiving address.
XSS shieldedbech32mHRP xss (main) / txss (test); 41-byte payload version(0) ‖ ak(8) ‖ enc_pk(32).
Crypto sends are irreversible. The XST validator is base58check + double-SHA-256 + version byte; the XSS validator is bech32m with a length, version, and ak-canonicity check. Example XSS (main): xss1qz44nf77y4nxh0k7pn77gzd3ny4cmtc25enllzqlhss6chfm6lt0e5vdvegl33a7pyvmr62d
11

Errors & status

What the surfaces return
Status / reasonWhereMeaning
402 Payment RequiredGated routeNo settled payment — body carries the x402 challenge.
200 OK + req.x402Gated routeSettled; your handler ran. Receipt is on req.x402 / PAYMENT-RESPONSE.
verify invalidReasonFacilitatorwrong_amount, unknown_challenge, challenge_expired, inputs_spent, script_invalid, feework_insufficient, …
403 policySidecar /payCap (daily/total), rate, or allowlist would be breached. Nothing signed.
409 duplicateSidecar /payA payment for this Idempotency-Key is already in flight.
401 unauthorizedSidecar / ConsoleMissing or unknown bearer token / session.

Start building.

Gate a route, run your sidecar, point your agents. Your keys never move.