# PipRail — full reference for agents

> Last-Updated: 2026-06-19 · SDK-Version: 2.9.0 · MCP-Version: 0.8.0 · Type: APIService · License: MIT

> Accept and make x402 "402 Payment Required" crypto payments from any HTTP request, across 29 chains in 10 families, with no backend, no database, no account, and no fee. Payments settle straight into your own wallet, verified locally against your own RPC. `@piprail/sdk` is an open-source (MIT) TypeScript SDK that runs headless or in the browser. This file is the everything-in-one-fetch version: the pitch, real code, the chain list, and the error model.

## What it is

PipRail is three things, no server:

1. `@piprail/sdk` — a TypeScript SDK for x402 agent payments that works on any EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar, and the XRP Ledger. `npm install`, name your chain, add your wallet, done. One parameter (`chain`) picks everything.
2. `@piprail/mcp` — an MCP (Model Context Protocol) server that wraps the SDK so any MCP client (Claude Desktop, Cursor, Claude Code, Windsurf, VS Code, Cline) pays x402 URLs autonomously, capped by a local spend policy. No code, just an env block.
3. A static landing page at piprail.com.

There is no hosted backend, no facilitator, and no fee. Payments go straight to the developer's wallet and are verified locally against their own RPC, with an in-memory replay guard (pluggable for multi-instance). The x402 v2 spec explicitly allows resource servers to host verification themselves, so this backendless shape is spec-supported.

## What is x402

x402 revives the HTTP 402 "Payment Required" status code as a real protocol. A server answers a request with `402` plus a machine-readable challenge — price, token, chain, pay-to address, and a nonce. The caller pays on-chain, then retries the request with proof in the `payment-signature` header. The server verifies the proof against its own RPC and serves the resource, returning a receipt in the `payment-response` header. No API keys, no accounts, no invoices — an endpoint charges for itself, which is exactly what autonomous AI agents need.

## Install

```bash
npm install @piprail/sdk viem
```

`viem` is the peer dependency for EVM chains. Solana, TON, and other non-EVM families lazy-load their own libraries on first use, so pure-EVM installs never download them.

## Take payments — one line (Express)

```ts
import express from 'express'
import { requirePayment } from '@piprail/sdk'

express()
  .get('/report',
    requirePayment({ chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourWallet…' }),
    (_req, res) => res.json({ report: 'TOP SECRET' }),
  )
  .listen(3000)
```

That route now costs 0.05 USDC on Base, paid to your wallet. The first request gets a `402` with payment instructions; once the caller pays on-chain, the request goes through. You didn't paste a token address, run a server, deploy a contract, or sign up for anything.

## Take payments — any framework (createPaymentGate)

`requirePayment` is Express-only. Everywhere else (Next.js, Hono, Fastify, Cloudflare Workers, Bun, Deno) uses `createPaymentGate` — build a gate once, then switch on what `verify()` returns. Next.js App Router Route Handler:

```ts
import { createPaymentGate, toInvalidBody } from '@piprail/sdk'

// Build the gate once at module scope. It holds the payment requirement and an
// in-memory replay guard (pass isUsed/markUsed for multi-instance — e.g. Redis).
const gate = createPaymentGate({
  chain: 'base',
  token: 'USDC',
  amount: '0.05',
  payTo: '0xYourWallet…', // set from env in production
})

export async function GET(req: Request): Promise<Response> {
  // The proof of payment (if any) rides in the `payment-signature` header.
  const result = await gate.verify(req.headers.get('payment-signature') ?? undefined)

  if (result.kind === 'paid') {
    return Response.json(
      { report: 'TOP SECRET' },
      { headers: { 'payment-response': result.receiptHeader } },
    )
  }

  if (result.kind === 'challenge') {
    return Response.json(result.challenge, {
      status: 402,
      headers: { 'payment-required': result.requiredHeader },
    })
  }

  // Proof rejected (wrong amount, expired, replayed…): 402 + canonical x402 body.
  return Response.json(toInvalidBody(result), { status: 402 })
}
```

## Record every payment — onPaid + deliverReceipt

Pass `onPaid` to a gate to act on each settled payment. It fires after verification with an enriched `PaidReceipt` — the wire receipt (`transaction`, `network`, `asset`, `amount`, `payer`, `payTo`) PLUS the display fields the gate already had: `decimals`, `symbol`, `amountFormatted` (formatted from the settled amount), and a stable `idempotencyKey`. The hook may be sync OR async and is fully isolated: a thrown error or a rejected promise can never break the request or crash the process (route them to `onPaidError`). It's fire-and-forget by default (low latency); set `awaitOnPaid: true` to record the receipt BEFORE the resource is served. Delivery is at-least-once across instances (a custom `isUsed`/`markUsed` store + a race can deliver twice) — always dedupe on `idempotencyKey`. For a durable webhook the SDK ships `deliverReceipt(receipt, { url, secret, retries, timeoutMs, backoff, headers, onAttempt })`: it POSTs the receipt to YOUR endpoint with retries + exponential backoff, an HMAC-SHA256 signature (`piprail-signature: sha256=…`), and an `idempotency-key` header; it retries 408/429/5xx + transport errors, stops on a permanent 4xx, and NEVER throws (failure → `{ delivered: false, … }`). Isomorphic (global fetch + Web Crypto), zero new deps, PipRail hosts nothing — the URL is yours. `onPaid: (r) => deliverReceipt(r, { url, secret })`. Full page: https://docs.piprail.com/accepting-payments/receipts-and-onpaid/

## Notify on failure too — onFailed (the mirror of onPaid)

`onPaid` is half the picture; `onFailed` is its mirror — pass it to the same `requirePayment`/`createPaymentGate` and it fires whenever a SUBMITTED proof is REJECTED, i.e. `gate.verify()` returns `kind:'invalid'` (wrong amount, expired, replayed, unknown asset, wrong recipient, bad signature, …). It receives a `FailedPayment` — exactly `{ code: VerifyErrorCode; detail: string; transient: boolean }` — and nothing more: a rejection has NO settlement, so (unlike `PaidReceipt`) there is no tx hash, settled amount, or payer to report. The `code` is the SAME machine-readable `VerifyErrorCode` the buyer's client is told for that rejection (e.g. `amount_too_low`, `payment_expired`, `tx_already_used`, `transfer_not_found`), so both sides see one consistent reason; `detail` is the human string (e.g. `"Paid 40000, required 500000."`). `transient` is `true` for the two transient codes ONLY — `tx_not_found` / `insufficient_confirmations` — where the proof may still be settling and the buyer's client auto-retries (you'll then usually get `onPaid` if it succeeds), so alert only on `!transient` to avoid false alarms on normal RPC lag; `false` means a definitive rejection the buyer must fix. Like `onPaid` it may be sync OR async and is fully isolated — a synchronous throw OR an async rejection is caught and routed to `onFailedError(error, failure)` (the mirror of `onPaidError`, itself isolated), so a failure-notification hook can never break the request or escape as an unhandledRejection. It's fire-and-forget by default; set `awaitOnFailed: true` to run it before the 402 rejection is returned (the mirror of `awaitOnPaid`). It fires ONLY on a real rejection: NOT on a normal first-request `challenge` (no proof yet), and NOT when `verify()` THROWS (a transient RPC blip that re-throws, or a 5xx `SettlementError`) — those aren't payment verdicts. It works through BOTH `createPaymentGate` and the `requirePayment` middleware (it fires inside `gate.verify()`). The HONEST LIMIT: a failure the merchant never receives a request for — the buyer can't afford it, a `policy`/`onBeforePay` declines it, or the buyer abandons before paying — reaches ONLY the buyer, because a backendless gate is passive by design; `onFailed` covers every rejection that DOES reach the gate. On the BUYER side the symmetry is now complete too: the client's `payment-failed` event (`onEvent`) carries optional `code?: string` + `detail?: string` — the server's parsed reason (the SAME `code` the merchant's `onFailed` gets on a server rejection) OR a client decline reason (`'BUDGET'`/`'APPROVAL'`/`'POLICY'`) — and `payment-failed` now ALSO fires on a PRE-SEND DECLINE (a `policy` / `onBeforePay` refusal, or no settleable rail), where previously those ONLY threw; the typed `PaymentDeclinedError` throw is unchanged and zero funds still move, so a consumer watching `onEvent` alone now learns of EVERY failure type, not just server rejections. `FailedPayment` is exported from `@piprail/sdk`; a complete runnable two-sided demo (merchant + buyer, success + failure persisted to a SQLite ledger with a /ledger dashboard) lives at examples/basics/payment-system/. Full page: https://docs.piprail.com/accepting-payments/receipts-and-onpaid/

## Make payments — wrap fetch (PipRailClient)

```ts
import { PipRailClient } from '@piprail/sdk'

const client = new PipRailClient({
  wallet: { key: process.env.AGENT_KEY },
  chain: 'base',
})

const res = await client.fetch('https://api.example.com/report') // pays the 402 for you
const data = await res.json()
```

On a `402`, the client reads the challenge, sends the payment on-chain, waits for confirmation, and retries with proof — all inside `client.fetch`. The same app can take payments with `requirePayment`/`createPaymentGate` and make them with `PipRailClient`.

**Pay across chains — one wallet per chain (`MultiChainPayer`).** A `PipRailClient` is bound to ONE chain + ONE wallet (an EVM key can't sign a Solana tx). To pay a 402 on whatever chain it asks for, give a `MultiChainPayer` one wallet per chain: `MultiChainPayer.fromWallets({ wallets: { base: { key }, solana: { key }, xrpl: { key } }, policy })`. It exposes the same `fetch`/`get`/`post`/`planPayment`/`canAfford`/`quote`/`discover`/`register`/`spent`/`budget` and satisfies the same `PayingClient` interface, so the agent toolkit and the MCP wrap it unchanged. On a 402 it surveys every funded chain and pays the FIRST chain you listed that can settle (your preference order — there is no oracle to compare gas across different native coins; within a chain it picks the cheapest-gas rail). `schemes` (incl. the gasless `exact` rail) propagates to every chain. When no chain can settle, the merged `fundingHint` (and the `PaymentDeclinedError`) names every funded chain's own blocker — "top up X USDC on base · add ~Y POL gas on polygon" — with per-rail machine-readable `blockers`. Built on the exported `planAcross(clients, url)` / `fetchAcross(clients, url)` primitives. In the MCP server it's zero-code: set `PIPRAIL_CHAINS=base,polygon,solana` with a `PIPRAIL_<CHAIN>_KEY` each. Caps (`maxAmount`/`maxTotal`) are in the token's true decimals (≈ $ for USDC/USDT; native-coin units on a `native` rail) and `maxTotal` is per-(chain, token).

**Pay any standard x402 server (opt-in).** By default the client pays PipRail's backendless `onchain-proof` rail. Add `schemes: ['onchain-proof', 'exact']` and it ALSO pays the standard, ratified x402 `exact` rail (EIP-3009) — so a PipRail agent can transact with any standard x402 server (the dominant `exact`-on-Base-via-CDP web), not just PipRail's own gates. The agent signs the authorization with its own wallet (the token's domain re-derived on-chain) and the server/facilitator broadcasts it, so the buyer pays ~0 gas; the same spend `policy` gates it before any signature. Works on EVM — EIP-3009 (USDC; EURC on Ethereum/Base/Avalanche) or Permit2 (any ERC-20 without EIP-3009, e.g. Binance-Peg USDC on BNB Chain, via the canonical x402ExactPermit2Proxy) — plus Solana (SVM, any SPL token), Algorand (any ASA, via an atomic-group fee pool), and Aptos (any Fungible Asset, via a fee-payer/sponsored transaction per AIP-39). Proven on Base (EIP-3009), BNB (Permit2), Solana (SVM), Algorand, and Aptos mainnet.

## In the browser — no build, no npm

`@piprail/sdk` is browser-clean and runs from any npm-mirroring CDN, so a plain HTML page can take or make payments with no bundler:

```html
<script type="module">
  import { PipRailClient } from 'https://esm.sh/@piprail/sdk'   // or jsDelivr: .../npm/@piprail/sdk@2/+esm
  import { createWalletClient, custom } from 'https://esm.sh/viem'
  // In a browser, sign with the visitor's wallet (MetaMask) — never a raw key in the page.
  const client = new PipRailClient({ chain: 'base', wallet: { walletClient: createWalletClient({ transport: custom(window.ethereum) }) } })
  const res = await client.fetch('https://api.example.com/paid')  // 402 → wallet signs → 200
</script>
```

The CDN resolves `viem` and any chain lib the SDK lazy-imports — no second script tag. The merchant gate (`createPaymentGate`) needs only a wallet address, so it runs in the browser too. Verified end-to-end, including a real on-chain payment made FROM a browser. Try it live (the SDK runs on the page): https://piprail.com/demo — and the copy-paste HTML lives at examples/basics/browser/. Keep raw `{ key }` wallets on the server only.

## Built for agents — spend safely

A funded key on the internet needs guardrails. Opt in to a `policy` and the client refuses anything outside it before any on-chain send. All opt-in, all local, no backend; omit it and the client behaves exactly as before.

```ts
const client = new PipRailClient({
  wallet: { key: process.env.AGENT_KEY },
  chain: 'base',
  policy: {
    maxAmount: '0.10',        // never pay more than $0.10 for one call
    maxTotal: '5.00',         // never spend more than $5 total (per token)
    chains: ['base'],         // only on Base
    tokens: ['USDC'],         // only in USDC
    hosts: ['*.example.com'], // only these hosts
  },
  onBeforePay: (q) => Number(q.amountFormatted) <= 0.05, // final say on each payment
})

// 1) Learn the price WITHOUT paying.
const q = await client.quote('https://api.example.com/report')
//  → { amountFormatted: '0.05', symbol: 'USDC', chain: 'base', withinPolicy: true, … } | null

// 2) Know the GAS too — the native-coin fee to send it (you pay USDC, but burn ETH/SOL/TRX for gas).
const est = await client.estimateCost('https://api.example.com/report')
//  → { quote: {…}, cost: { feeSymbol: 'ETH', feeFormatted: '0.000105', basis: 'estimated', … } } | null

// 3) Pay (auto). Over-budget / declined → throws PaymentDeclinedError; nothing moves.
const res = await client.fetch('https://api.example.com/report')

// 4) Account for it.
client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05', … }], records }
```

The budget can't be fooled: `maxAmount`/`maxTotal` are enforced against the token's true decimals (the SDK's own, via the driver), so a server can't slip past a cap by understating the price, and an unrecognised asset is refused unless you set `allowUnknownTokens`.

## Plan before you pay — `planPayment(url)` (the killer agent feature)

`quote()` gives the price and `estimateCost()` the gas; **`planPayment(url)`** closes the loop — one read-only call that checks, against your wallet's OWN holdings, whether a 402 will actually go through, and if not, exactly what to top up. No funds move.

```ts
const plan = await client.planPayment(url)
if (plan.payable) {
  await client.fetch(url, { autoRoute: true })   // pays plan.best — the cheapest settleable rail
} else {
  console.log(plan.fundingHint)
  // "Have the USDC, but need ~0.000021 ETH for gas on base (have 0)."
  // "Recipient 2OT6…GC5E4 can't receive on algorand yet — must opt into the USDC ASA."
}
```

For every rail the 402 offers on your chain, the plan reads **token balance + native-coin gas + recipient-readiness** (trustline / ATA / storage_deposit / ASA opt-in / activation) and returns: `payable` + `best` (cheapest settleable rail); `options[]` each with typed `blockers` (INSUFFICIENT_TOKEN / INSUFFICIENT_GAS / RECIPIENT_NOT_READY / OUTSIDE_POLICY), soft `warnings`, a `shortfall`, the live `balance`, and `recipient.fix`; and a one-sentence `fundingHint`. It NEVER throws for a read hiccup (a throttled RPC → `state: 'unknown'` + a warning, never a false "broke"), returns `null` when the URL isn't gated, and explains "offered on solana, base — you're on xrpl" rather than erroring. `client.canAfford(url)` is the one-boolean convenience. `fetch(url, { autoRoute: true })` (opt-in, default off) pays the cheapest settleable rail or refuses (PaymentDeclinedError + hint) before any send. `planAcross(clients, url)` merges plans across chains, payable-first (no oracle — cross-coin ties break on client order). The official x402 client picks `accepts[0]` blind; only a backendless, self-custodial, 29-chain SDK can answer "can I actually pay this, and where" from your own RPC. Driver contract: `balanceOf(wallet, asset)` + `recipientReady(payTo, asset)`, both RPC-read-only and never-throw, on all 10 families.

## Hand an LLM a budget-bound wallet

`paymentTools(client)` returns framework-agnostic tool descriptors (name + description + JSON Schema + `invoke`) — drop them into MCP, the Vercel AI SDK, OpenAI/Anthropic function-calling, or LangChain. The budget rides on the client, so the model can't overspend.

```ts
import { paymentTools } from '@piprail/sdk'
const tools = paymentTools(client) // → [piprail_discover, piprail_quote_payment, piprail_plan_payment, piprail_pay_request, piprail_register, piprail_budget, piprail_guide]
```

## Pay via MCP (@piprail/mcp)

Where `paymentTools()` is for code, `@piprail/mcp` is the zero-code path: an MCP server wrapping the SDK so an agent in any MCP client pays x402 URLs on its own, budget-bound, with nothing to integrate. Run it over stdio:

```bash
npx -y @piprail/mcp
```

Add it to your client with your wallet key and (optionally) a budget — defaults are small and safe (0.10 per payment, 10.00 lifetime per token, USDC on Base). Claude Desktop example:

```json
{ "mcpServers": { "piprail": { "command": "npx", "args": ["-y", "@piprail/mcp"],
    "env": { "PIPRAIL_PRIVATE_KEY": "0x…", "PIPRAIL_CHAIN": "base", "PIPRAIL_MAX_AMOUNT": "0.10", "PIPRAIL_MAX_TOTAL": "10.00" } } } }
```

Seven tools appear: `piprail_discover` (find payable x402 endpoints on the open indexes), `piprail_quote_payment` (price without paying), `piprail_plan_payment` (balance + gas + recipient-readiness + policy, read-only, across every rail), `piprail_pay_request` (fetch + pay on the cheapest affordable rail), `piprail_register` (list an x402 endpoint so other agents can find it), `piprail_budget` (read the remaining spend budget + time leash, read-only), and `piprail_guide` (the agent contract). The discovery/budget/guide tools are read-only — they never move funds. A failed pay returns a structured `{ ok:false, code, reason, explain, ref?, reasonCode? }` so the agent reasons over it instead of crashing or double-spending. Two modes: Mode A (headless, default — the budget+time envelope IS the consent) and Mode B (`PIPRAIL_CONFIRM=1` — the human approves each pay via MCP elicitation, fail-safe to not-pay). The time envelope adds `PIPRAIL_TTL` (session deadline) + `PIPRAIL_WINDOW_TOTAL`/`PIPRAIL_WINDOW_SECONDS` (rolling rate-limit). Listed in the official MCP registry as `io.github.piprail/mcp`. Config is ENV-ONLY (never CLI args — a key in argv leaks): `PIPRAIL_PRIVATE_KEY` (optional — omit it to boot a read-only server serving discover/quote/plan/register/budget/guide; only paying needs a key), `PIPRAIL_CHAIN` (default base), `PIPRAIL_MAX_AMOUNT`, `PIPRAIL_MAX_TOTAL`, `PIPRAIL_TOKENS` (default USDC; USDT on Tron/TON), `PIPRAIL_HOSTS`, `PIPRAIL_RPC_URL`, `PIPRAIL_ALLOW_UNKNOWN_TOKENS` (keep false), `PIPRAIL_NEAR_ACCOUNT_ID` (NEAR only). The wallet key is the chain's native form (EVM/Tron hex `0x…`, Sui `suiprivkey1…`, Aptos `ed25519-priv-0x…` or hex, Solana base58, TON 24-word mnemonic, Algorand 25-word mnemonic, Stellar `S…` seed, XRPL `s…` seed, NEAR `ed25519:…` + account id). Clients: Claude Desktop, Cursor, Claude Code, Windsurf, VS Code (top-level key is `servers`), Cline. EVM works out of the box; non-EVM pulls its SDK peer on demand; one wallet per chain per instance — register the server once per chain for multi-rail. Why it's safe: the spend policy is enforced before any on-chain send and checked against the token's TRUE decimals, so a malicious server can't understate a price to slip past a cap; an over-budget request returns `{ declined: true, reason }` and nothing moves. No custody, no backend — runs locally with your key against your own RPC. MIT. Page: https://piprail.com/mcp · npm: https://www.npmjs.com/package/@piprail/mcp · README: https://github.com/piprail/piprail/blob/main/mcp/README.md

## Integrations

PipRail drops into the frameworks where agents already live by wrapping the published @piprail/mcp server — there's no new code to build, just one config entry. Live today: **OpenClaw** (a ClawHub skill — `clawhub install piprail` — that hands an OpenClaw agent the 7 piprail_* tools, budget-bound, via one mcp.servers entry) and **Hermes** (NousResearch's agent runtime — add one mcp_servers block to ~/.hermes/config.yaml, or the Hermes MCP catalog entry `hermes mcp install piprail`, and the agent gets the same 7 tools, capped by a spend policy). More frameworks (Vercel AI SDK, Mastra, ElizaOS) are on the way. Any MCP client can use PipRail today — see https://docs.piprail.com/integrations/

## 60-second walkthrough: give your agent a wallet

The MCP path is install → add one env block → restart. The whole flow:

### 1. Install — one command, no build

```bash
npx -y @piprail/mcp
```

It runs over stdio and speaks MCP. Nothing to install ahead of time, no setup call.

### 2. Add it to your client (Claude Desktop shown)

Settings → Developer → Edit Config, or edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) / `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "piprail": {
      "command": "npx",
      "args": ["-y", "@piprail/mcp"],
      "env": {
        "PIPRAIL_PRIVATE_KEY": "0x…your-base-private-key…",
        "PIPRAIL_CHAIN": "base",
        "PIPRAIL_MAX_AMOUNT": "0.10",
        "PIPRAIL_MAX_TOTAL": "10.00",
        "PIPRAIL_TOKENS": "USDC"
      }
    }
  }
}
```

The key is the chosen chain's native form (EVM/Tron hex `0x…`, Sui `suiprivkey1…`, Aptos `ed25519-priv-0x…`, Solana base58, TON 24-word mnemonic, Algorand 25-word mnemonic, Stellar `S…` seed, XRPL `s…` seed, NEAR `ed25519:…` + `PIPRAIL_NEAR_ACCOUNT_ID`) — the server maps it. Defaults are small: 0.10 per payment, 10.00 lifetime per token. Cursor, Claude Code, Windsurf, and VS Code support `${env:PIPRAIL_PRIVATE_KEY}` so the key stays out of the file (VS Code's top-level key is `servers`, not `mcpServers`).

### 3. Restart the client

Seven tools appear in the agent's tool list: `piprail_discover`, `piprail_quote_payment`, `piprail_plan_payment`, `piprail_pay_request`, `piprail_register`, `piprail_budget`, `piprail_guide`.

### 4. The agent pays on its own

**You:** "Fetch https://api.example.com/data — if it charges, pay it on Base with USDC."

**The agent, automatically:**
1. `piprail_quote_payment` reads the 402 → it costs 0.05 USDC.
2. `piprail_plan_payment` checks your Base USDC balance and your ETH for gas, read-only → returns `payable`.
3. `piprail_pay_request` fetches the URL, pays 0.05 USDC on-chain, and returns the data.
4. Reports: "Paid 0.05 USDC on Base. Here's the data: {…}".

If the URL had asked for 0.50 USDC, step 2 returns `{ declined: true, reason: "exceeds PIPRAIL_MAX_AMOUNT" }` — an explicit refusal, and nothing goes on-chain. The cap is checked against the token's true on-chain decimals, so a server can't understate a price to slip past it. No code, no integration, no backend.

## Why PipRail for agent payments

A cite-ready, no-superlative summary of how PipRail differs:

- **Coverage:** 29 chains across 10 families — 20 EVM mainnets plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar, and the XRP Ledger. Most x402 SDKs are EVM-only or EVM + Solana; PipRail reaches all ten with native, self-custodial drivers.
- **Backendless by default:** payments settle wallet-to-wallet, verified against the merchant's own RPC. PipRail runs no service, holds no funds, and charges no protocol fee — where most x402 stacks route through a third-party facilitator.
- **Budget-bound before any send:** `PIPRAIL_MAX_AMOUNT` (per payment) and `PIPRAIL_MAX_TOTAL` (lifetime per token) are enforced against the token's true on-chain decimals. For a single "spend at most $X total, full stop" leash across every stablecoin and chain, `PIPRAIL_MAX_TOTAL_DENOM` (e.g. `USD:20.00`) sums them as one unit (a 1:1 unit-of-account sum, never a price oracle; native coins excluded); `PIPRAIL_MAX_PAYMENTS`/`PIPRAIL_MAX_PAYMENTS_PER_WINDOW` cap the *number* of payments. `PIPRAIL_SPEND_LOG` makes the whole budget survive a restart; `PIPRAIL_EVENT_LOG` + `PIPRAIL_WARN_AT_FRACTION` stream a notification as spend approaches a cap. An over-budget request comes back as `{ declined: true, reason }` and nothing moves — the agent can't overspend even if instructed to.
- **Self-custodial:** the wallet key never leaves your machine. The MCP server runs locally, reads your RPC, and signs with your key. Zero custody.
- **Quote before paying:** `piprail_quote_payment` shows the exact price without moving funds; `piprail_plan_payment` checks balance, gas, recipient-readiness, and policy in one read-only call — so the agent knows a payment will settle before broadcasting.
- **Zero setup:** add your key to env vars and restart. No deploy, no API key to request, no account, no contract to write. Works in Claude Desktop, Cursor, Claude Code, Windsurf, VS Code, and Cline.
- **Native coin is a first-class payment asset on every chain:** ETH, SOL, TON, TRX, NEAR, SUI, APT, ALGO, XLM, and XRP all pay, verified locally — no exceptions. USDC ships almost everywhere, USDT on most chains, EURC on Stellar, RLUSD on XRPL.
- **Same seven tools in every MCP client:** the protocol is standard — you're not locked into one model or vendor.

## Discovery — get found, find others

PipRail does x402 service **discovery** with no backend and no fee, built on OPEN third-party indexes (PipRail hosts no registry of its own). The whole playbook is four moves — EMIT, REGISTER, VERIFY, DISCOVER — and every method never throws and returns a typed result that tells an agent what to do next.

- **EMIT** — turn your payment gate into a machine-readable manifest that crawlers and agents can read. `gate.describe(resourceUrl?)` returns a nonce-free description of what you charge; the pure emitters serialize it three ways: `buildOpenApi({ origin, resources })` → an OpenAPI 3.1 document with an `x-payment-info` block on each priced operation (this is the field the open indexes crawl) plus a root `x-generator: "@piprail/sdk · https://piprail.com"` attribution stamp (opt out with `attribution: false`); `buildWellKnownX402(...)` → a `/.well-known/x402` document; `buildX402DnsTxt(...)` → a `_x402` DNS TXT record. All pure functions — serve the output however you like. NOTE: x402scan REQUIRES a resolvable input schema or it rejects the listing — the SIMPLEST path is the gate's `discovery` option (`createPaymentGate({ …, discovery: true })` or a `{ method, queryParams, output }` descriptor), which emits an `extensions.bazaar` block IN the 402 challenge so the gate alone is x402scan-listable (no extra file). `buildBazaarExtension(...)` builds the block standalone. CAVEAT: the open indexes' agents are overwhelmingly standard `exact` clients — a default `onchain-proof`-only gate gets listed but they can't pay it, so advertise an `exact` rail (`exact: { settle: { facilitator } }`) to be payable, not just listed.
- **REGISTER** — `client.register(url, opts?)` lists your x402 endpoint on the open indexes. Default target **402 Index** (no auth, no signature, any chain); **x402scan** is an optional second target (SIWX, EVM-signed, Base & Solana only). The index probes your URL and only lists real 402 endpoints. Crucially, every `RegisterOutcome` carries an agent-readable LIFECYCLE — `visibility: 'live' | 'pending-review' | 'not-listable'` + a plain-language `note` — so "ok:true" never means "searchable now": **402 Index returns `pending-review`** (probed on submit, then searchable once it passes automated health + payment checks — no domain verification required, proven by the live demo; verify your domain for instant, guaranteed approval — see VERIFY), **x402scan returns `live`** (but `discover()` doesn't read it), **Bazaar is `not-listable`** for PipRail (facilitator-coupled). The per-index facts are one import, `DIRECTORY_INFO` (review mode, auth, chains, `readByDiscover`, caveat). Attribution is **on by default** (opt out with `attribution: false`): a `via: "@piprail/sdk"` provenance field + a tasteful `· Built with @piprail/sdk` description suffix (deduped, length-guarded — never double-stamped, never fabricated). Standalone: `register402Index(...)`, `registerX402Scan(...)`.
- **VERIFY (402 Index domain → instant go-live)** — a self-registered 402 Index listing becomes searchable once it passes automated checks; verifying your domain is the **instant, guaranteed** path — it flips that listing (and every pending listing on the domain) to approved/searchable at once, with a `domain_verified` badge. `client.claimDomain(urlOrDomain, { contactEmail? })` returns a `verificationHash` to serve as the entire body of `verificationUrl` (your `https://<domain>/.well-known/402index-verify.txt`); then `client.verifyDomain(urlOrDomain)` → `{ ok, status:'verified', servicesCount }`. Standalone: `claim402IndexDomain(...)`, `verify402IndexDomain(...)`. No funds move.
- **DISCOVER** — `client.discover({ query?, network?, limit? })` returns payable x402 resources merged from the open indexes. It reads **CDP Bazaar + 402 Index** — **NOT x402scan** (its reads are paid), so a live x402scan listing won't appear here; don't read that absence as failure (passing `sources:['x402scan']` explicitly yields `[]`). Free, read-only. `network: 'self'` (default) filters to the client's bound chain; `'any'` searches all; or a CAIP-2 id (e.g. `eip155:8453`). Standalone: `searchOpenIndexes({ sources, query, limit })`.
- **Every chain, present and future:** EMIT is pure serialization (no chain logic), REGISTER's default (402 Index) has no chain restriction, and DISCOVER keeps networks it can't resolve rather than hiding them — so discovery covers every chain PipRail supports and every one added later. The only chain limit anywhere is the optional x402scan register target (Base/Solana). Internally, `normalizeNetwork(network)` maps chain slugs to CAIP-2 (via the `SLUG_TO_CAIP2` map) so `network: 'self'` filters precisely; adding a chain adds one entry there.
- **Attribution, done tastefully (default on, opt-out):** discovery spreads the word without spam — the `x-generator` stamp in emitted manifests, a `User-Agent: @piprail/sdk (+https://piprail.com)` on every discovery request, and on register a `via: "@piprail/sdk"` provenance field + a compact `· Built with @piprail/sdk` description suffix (deduped, length-guarded — never double-stamped or fabricated; opt out with `attribution: false`). Nothing is sent that an index forbids; registration only ever lists endpoints that really return 402.
- **Ownership proof:** `client.discoverySigner()` (EVM today; `null` on families without a verified scheme) signs the bare origin string so an index can confirm you control the endpoint — the recoverable proof some indexes use for a trust badge.
- **Status:** discovery is experimental and additive — the pay/accept core is the stable product, and discovery defaults are off where they touch the network beyond a read. Page: https://piprail.com/discovery · full reference (the three moves, a step-by-step walkthrough, what you need at each step, honest caveats): https://github.com/piprail/piprail/blob/main/sdk/DISCOVERY.md

## Accept several chains at once

`requirePayment` (and `createPaymentGate`) take an `accept: [...]` array — one challenge payable on any of several chains/tokens across all ten families. The agent pays with whatever it holds.

```ts
requirePayment({
  accept: [
    { chain: 'base',   token: 'USDC', amount: '0.05', payTo: '0xYourEvmWallet…' },
    { chain: 'tron',   token: 'USDT', amount: '0.05', payTo: 'TYourTronWallet…' },
    { chain: 'xrpl',   token: 'USDC', amount: '0.05', payTo: 'rYourXrplWallet…' },
    { chain: 'solana', token: 'USDC', amount: '0.05', payTo: 'YourSolWallet…' },
  ],
})
```

- Gate: each option resolves through its own driver and is listed in the challenge's `accepts[]`, sharing one nonce. Give a per-option `payTo` for each non-EVM chain (address shapes differ per family).
- Payer: a `PipRailClient` is bound to one chain; it picks the offered accept whose network it supports and its `policy` allows, pays that one, and ignores the rest.
- Verify: the gate selects the matching requirement by network + asset and re-derives every checked field from its own trusted spec — a forged `accepted` echo can't redirect it, and the same proof can't be redeemed twice.

## One word picks the chain

```ts
requirePayment({ chain: 'base',     token: 'USDC', amount: '0.05', payTo }) // USDC on Base
requirePayment({ chain: 'arbitrum', token: 'USDC', amount: '0.05', payTo }) // USDC on Arbitrum
requirePayment({ chain: 'bnb',      token: 'USDT', amount: '1',    payTo }) // USDT on BNB
requirePayment({ chain: 'solana',   token: 'USDC', amount: '0.05', payTo }) // USDC on Solana
requirePayment({ chain: 'ton',      token: 'USDT', amount: '1',    payTo }) // USD₮ on TON
requirePayment({ chain: 'tron',     token: 'USDT', amount: '1',    payTo }) // USD₮ on Tron
```

Any other EVM chain works by passing a viem `Chain` or `{ id, rpcUrl }` plus `token: { address, decimals }`. Custom tokens are supported per family: SPL `{ mint, decimals }`, TON jetton `{ master, decimals }`, Stellar `{ issuer, code, decimals }`, XRPL `{ issuer, currencyHex, decimals }`, TRC-20 `{ address, decimals }`, NEP-141 `{ contractId, decimals }`, Sui `{ coinType, decimals }`.

## The 29 chains, by family

- EVM (20 mainnets): Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Avalanche, HyperEVM, Monad, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, Kaia. USDC on all except Kaia; USDT on all except Base, World Chain, Sei, HyperEVM, and Monad (their "USDT" is USDT0/LayerZero, not Tether-native). Kaia is the inverse — Tether-native USD₮ only, no Circle-native USDC; born from Kakao + LINE.
- Solana: USDC, USDT.
- TON: USD₮ only (native USDC doesn't exist on TON).
- Tron: USD₮ (TRC-20) + native TRX. The largest USDT rail; a transfer burns real TRX, so gas estimates matter most here. (No native USDC on Tron — Circle discontinued it.)
- NEAR: USDC, USDT (both native NEP-141).
- Sui: USDC.
- Aptos: USDC + USD₮ (both native Fungible Assets) + native APT (digest-bound, Template B like Sui). The only Move L1 with both Circle-native USDC and Tether-native USD₮. PipRail offers BOTH the backendless `onchain-proof` default AND the gasless `exact` rail (a fee-payer / sponsored transaction per AIP-39: the buyer signs the transfer and pays 0 APT; self-settle with your own relayer — no facilitator needed — or a keyless facilitator). Live-proven on mainnet.
- Algorand: USDC (native Circle ASA) + native ALGO. Memo-bound (Template A) — the nonce rides in the 1KB transaction note; USDC-only (Tether deprecated USDT on Algorand). Part of the official x402 standard — PipRail offers BOTH the backendless `onchain-proof` default AND the gasless `exact` rail (an atomic-group fee pool: the buyer signs an ASA transfer at fee 0 and pays 0 ALGO; self-settle with your own relayer, or a keyless facilitator). Live-proven on mainnet.
- Stellar: USDC, EURC (7-decimal assets, memo-bound).
- XRP Ledger (XRPL): USDC, RLUSD (receiving needs a one-time trustline).

Every token address was verified on-chain before shipping. USDC is the common denominator across families.

## Per-chain setup & caveats (read before shipping NEAR, TON, Stellar, XRPL, Tron, Algorand)

Most chains need nothing beyond a wallet. `token: 'native'` (paying in the chain's own coin) is accepted on EVERY family — EVM, Solana, Sui, Aptos, Algorand, Stellar, XRPL, TON, NEAR, and Tron (native TRX, digest-bound). No exceptions. Custom tokens work everywhere with no allowlist.

- EVM / Solana / Sui: no receiver setup — any valid address receives native or token immediately (on Solana the payer's tx creates the recipient's token account and pays its ~0.00204 SOL rent; pass the recipient's WALLET address as payTo). Sui ships USDC only (no built-in USDT). On BNB Chain, all four built-in stablecoins (USDC, USDT, FDUSD, USD1) are 18 decimals; FDUSD + USD1 are EIP-3009 (gasless `exact` via transferWithAuthorization, no Permit2 approve), while USDC/USDT are Binance-Peg (the `exact` rail uses Permit2). The SDK auto-selects per token.
- NEAR — native is zero-setup; tokens need storage_deposit. `token:'native'` pays in NEAR (24dp) via digest-binding — NO `storage_deposit`, and a transfer even creates a fresh implicit recipient (the easy path; NEAR is the volatile gas coin, so for stable pricing pay in a token). For USDC/USDT (or a custom NEP-141 `{ contractId, decimals }`): the recipient AND the payer must each be NEP-145 `storage_deposit`-registered on that exact token contract once (~0.00125 NEAR) before they can receive/hold it. Wallet: `{ accountId, key }` (key = an `ed25519:…` secret). Built-in USDC is Circle's native contract, not the bridged `…factory.bridge.near`. Do not route through NEAR Intents/solvers.
- TON — USD₮ (no USDC) + API-keyed RPC (the ONE extra step for TON). Native USDC doesn't exist on TON; pay in `'USDT'`, `'native'` (Gram — ticker `GRAM`, formerly Toncoin/TON; token renamed 2026-06-15, network unchanged at `tvm:-239`), or a custom jetton `{ master, decimals }`. The keyless public toncenter RPC is rate-limited and stalls confirm/verify, so you MUST pass an API-keyed, archival endpoint as `rpcUrl` (on both the gate and the client): `rpcUrl: 'https://toncenter.com/api/v2/jsonRPC?api_key=YOUR_KEY'`. Get a free key in ~30s from @tonapibot on Telegram (or toncenter.com) — no card, no KYC — and embed it in the URL. That single parameter is the entire TON setup. No receiver setup (the payer's gas auto-deploys the jetton wallet on first receipt). Wallet: `{ key }` (a 24-word mnemonic) (version v4 default, or v5r1).
- Tron — USD₮ (or native TRX) + real gas. Pay in `'USDT'`, `'native'` (TRX, digest-bound), or a custom TRC-20 `{ address, decimals }`. USD₮ is the default since TRX is volatile gas, but native TRX works too (a plain TransferContract, verified on the solidity node). No native USDC (Circle discontinued it). Gas is paid in TRX and a USD₮ transfer burns real Energy (~30k unstaked) — budget TRX with `client.estimateCost(url)`; a first native send to a brand-new recipient also pays Tron's ~1 TRX account-creation fee. No receiver setup. Wallet: `{ key }` (hex). Finality waits ~19 blocks (~57s).
- Stellar — trustline + funded account. Pay in native XLM, `'USDC'`, `'EURC'`, or `{ issuer, code, decimals }`. Receiving an issued asset needs the recipient to EXIST (funded above the ~1 XLM base reserve) AND hold a one-time trustline for that code+issuer (+0.5 XLM reserve each); the payer needs its own trustline too. Reserves are locked, not spent. Wallet: `{ key }` (`S…` seed).
- XRPL — activation + trustline. Pay in native XRP, `'USDC'`, `'RLUSD'`, or `{ issuer, currencyHex, decimals }`. Receiving an IOU needs the recipient to be an activated account (~1 XRP base reserve) AND hold a one-time trustline to the issuer's currency; native XRP needs neither. Reserves locked, not spent. RLUSD's issuer requires a DestinationTag — the SDK sets a nonce-derived one automatically. Wallet: `{ key }` (`s…` seed).
- Algorand — USDC needs a one-time ASA opt-in; native ALGO is zero-setup. Pay in native ALGO, `'USDC'`, or a custom ASA `{ assetId, decimals }`. To receive USDC the recipient must opt into the USDC ASA once (a 0-amount self-transfer, +0.1 ALGO min balance, recoverable) — a not-opted-in recipient surfaces RECIPIENT_NOT_READY; native ALGO needs no opt-in. ~3s finality, flat 0.001 ALGO fee; the nonce binds inside the transaction note (Template A). USDC-only (Tether deprecated USDT on Algorand). Algorand's `exact` scheme is part of the official x402 standard; PipRail supports it gaslessly via an atomic-group fee pool (the buyer pays 0 ALGO), self-settled with your own relayer (no facilitator needed — backendless) or via a keyless facilitator, alongside the `onchain-proof` default. Live-proven on mainnet. Wallet: `{ key }` (25-word mnemonic) or `{ account }`.

Universal: every public default RPC is rate-limited — pass your own `rpcUrl` in production (there's no separate API-key field; fold any key into the URL). Full reference: https://github.com/piprail/piprail/blob/main/sdk/CHAINS.md

## Error model

Two channels, and only two:

1. THROWN — a typed `PipRailError` subclass with a stable `.code`, for config / flow / wallet / affordability problems the caller acts on. Catch with `err instanceof PipRailError` or branch on `err.code`. Notable codes: `INSUFFICIENT_FUNDS` (`InsufficientFundsError` — wallet can't cover the transfer plus fees/reserve/trustline; affordability always converges on this one error); `PAYMENT_DECLINED` (`PaymentDeclinedError` — the client refused to pay before any send, because the spend policy or an `onBeforePay` hook said no — nothing moves on-chain); `NON_REPLAYABLE_BODY` (`init.body` isn't replayable, e.g. a one-shot stream).
2. RETURNED — server-side verification of an on-chain proof returns a `VerifyResult` `{ ok: false, error, detail }` where `error` is a `VerifyErrorCode`. These never throw for an RPC hiccup. Values include `tx_not_found` (proof not on chain yet or a transient RPC read failed — transient, safe to retry), `payment_expired` (older than the replay window — definitive), and `tx_already_used` (the proof was already redeemed — replay, raised by the gate, not the driver).

The rule of thumb for agents: a thrown `PaymentDeclinedError` means your own budget/policy stopped the payment before anything happened; a thrown `InsufficientFundsError` means top up the wallet; a returned `tx_not_found` is transient (retry shortly); `payment_expired` and `tx_already_used` are definitive (re-request a fresh challenge).

## Writing — the blog

The PipRail blog (https://piprail.com/blog) collects essays from the team on x402, agent payments, and the trust layer the agent economy still has to build. The first essay is coming soon.

## Links

- Website: https://piprail.com
- Blog (essays on x402, agent payments & the trust layer): https://piprail.com/blog
- Live demo (try x402 in your browser, no install): https://piprail.com/demo
- npm: https://www.npmjs.com/package/@piprail/sdk
- npm: @piprail/mcp (MCP server): https://www.npmjs.com/package/@piprail/mcp
- MCP registry (io.github.piprail/mcp): https://registry.modelcontextprotocol.io
- MCP setup guide: https://piprail.com/mcp
- MCP README: https://github.com/piprail/piprail/blob/main/mcp/README.md
- Discovery guide (emit, register & find x402 endpoints): https://piprail.com/discovery
- Discovery reference (DISCOVERY.md): https://github.com/piprail/piprail/blob/main/sdk/DISCOVERY.md
- GitHub (MIT): https://github.com/piprail/piprail
- SDK README: https://github.com/piprail/piprail/blob/main/sdk/README.md
- Per-chain setup & caveats: https://github.com/piprail/piprail/blob/main/sdk/CHAINS.md
- Concepts: https://github.com/piprail/piprail/blob/main/examples/CONCEPTS.md
- Error codes: https://github.com/piprail/piprail/blob/main/sdk/ERRORS.md
- Agent guidelines: https://github.com/piprail/piprail/blob/main/AGENTS.md
- Examples: https://github.com/piprail/piprail/blob/main/examples/README.md
