@piprail/sdk 1.15.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,822 +1,59 @@
1
1
  # @piprail/sdk
2
2
 
3
- **Accept crypto payments from any HTTP request — on any EVM chain, Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar, and the XRP Ledger — in a couple of lines.**
3
+ **Accept and make [x402](https://x402.org) crypto payments — on any EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar & the XRP Ledger — in a couple of lines.**
4
4
 
5
- No middleman. No database. No fee. No account. Payments settle **straight into your wallet**, verified locally against your own RPC. Drop one middleware in front of a route and it's paid-only; point an agent at a paid URL and it pays itself.
5
+ No middleman, no database, no fee, no account. Payments settle **straight into your wallet**, verified locally against your own RPC. Gate a route to make it paid-only; point an agent at a paid URL and it pays itself.
6
6
 
7
7
  ```bash
8
8
  npm install @piprail/sdk viem
9
9
  ```
10
10
 
11
- ## Take payments one line
11
+ > ### 📖 Full documentation → **[docs.piprail.com](https://docs.piprail.com)**
12
+ > The docs are the single, searchable **source of truth** — every function, option, chain, and example, plus the MCP server, spend controls, discovery, and the complete error model. This README is just the front door.
12
13
 
13
- ```ts
14
- import express from 'express'
15
- import { requirePayment } from '@piprail/sdk'
16
-
17
- express()
18
- .get('/report',
19
- requirePayment({ chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourWallet…' }),
20
- (_req, res) => res.json({ report: 'TOP SECRET' }),
21
- )
22
- .listen(3000)
23
- ```
24
-
25
- 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.
26
-
27
- ## Make payments — wrap fetch
28
-
29
- ```ts
30
- import { PipRailClient } from '@piprail/sdk'
31
-
32
- const client = new PipRailClient({
33
- wallet: { privateKey: process.env.AGENT_KEY },
34
- chain: 'base',
35
- })
36
-
37
- const res = await client.fetch('https://api.example.com/report') // pays the 402 for you
38
- const data = await res.json()
39
- ```
40
-
41
- 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` and **make** them with `PipRailClient`. Built for autonomous agents: install, add a wallet, monetize or pay — nothing else to wire up.
42
-
43
- ## Universal payments — get paid by *any* x402 client
44
-
45
- PipRail's envelope is x402 **v2**-conformant, and its default `onchain-proof` scheme is backendless (the payer broadcasts, you verify locally — zero merchant key, zero merchant gas). To **also** be payable by a standard x402 client (Coinbase's `x402-fetch`, `@x402/fetch`, anything speaking the ratified `exact`/EIP-3009 scheme), opt into a standard `exact` rail — advertised **alongside** `onchain-proof` in the same 402:
46
-
47
- ```ts
48
- requirePayment({
49
- chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourWallet…',
50
- exact: { settle: 'self', relayer: { privateKey: process.env.RELAYER_KEY } },
51
- })
52
- ```
53
-
54
- Now the gate offers two rails: a standard x402 client picks `exact`; a PipRail client picks `onchain-proof`. The `exact` rail is **self-settled** — your own `relayer` key broadcasts the client's signed EIP-3009 authorization, so the **payer spends no gas** (you do, to receive). The EIP-712 token domain is read from the contract, so it's correct on every chain (USDC's domain name is `"USD Coin"`, not the symbol). Prefer not to run a relayer key? Delegate settlement to a third-party facilitator you choose — PipRail still hosts nothing:
55
-
56
- ```ts
57
- exact: { settle: { facilitator: 'https://x402.org/facilitator' } }
58
- ```
59
-
60
- EVM + EIP-3009 tokens only (USDC, EURC — not USDT, not native; those stay `onchain-proof`). Omit `exact` and the gate is byte-identical to today. Proven end-to-end: a real `@x402/fetch` reference client settles against a PipRail gate on Base mainnet.
61
-
62
- ### Pay *any* x402 server — the `exact` rail, buyer side
63
-
64
- The mirror image: let your **`PipRailClient` pay** standard x402 servers (the dominant `exact`-on-Base-via-CDP web), not just PipRail's own gates. Opt in with `schemes` — default `['onchain-proof']`, so the zero-config client is byte-identical:
65
-
66
- ```ts
67
- const client = new PipRailClient({
68
- chain: 'base',
69
- wallet: { privateKey: process.env.AGENT_KEY },
70
- schemes: ['onchain-proof', 'exact'], // also pay standard exact rails
71
- })
72
- await client.fetch('https://api.somevendor.com/paid') // pays whichever rail it offers
73
- // or per call: client.fetch(url, { schemes: ['exact'] })
74
- ```
75
-
76
- On an `exact` rail the client signs an EIP-3009 authorization with **its own** wallet and the server / merchant-chosen facilitator broadcasts it — so the **buyer pays ~0 gas** and PipRail hosts/settles nothing. The EIP-712 token domain is **re-derived on-chain** (never trusted from the challenge), the same `policy` + `onBeforePay` gate it **before any signature**, and `quote()`/`planPayment()`/`estimateCost()` are truthful across both schemes (an exact rail prices gasless). EVM + EIP-3009 only (USDC/EURC); silently ignored on non-EVM chains, for USDT/native, or for a token the SDK can't price — those stay `onchain-proof`. **Verify against your target facilitator before production.**
77
-
78
- ## Built for agents — spend safely
79
-
80
- A funded key loose on the internet needs guardrails. Opt in to a `policy` and the client refuses anything outside it **before any on-chain send** — plus learn a price without paying it, approve each payment, and read back exactly what you spent. All opt-in, all local, no backend; omit it and the client behaves exactly as before.
81
-
82
- ```ts
83
- const client = new PipRailClient({
84
- wallet: { privateKey: process.env.AGENT_KEY },
85
- chain: 'base',
86
- policy: {
87
- maxAmount: '0.10', // never pay more than $0.10 for one call
88
- maxTotal: '5.00', // never spend more than $5 total (per token)
89
- chains: ['base'], // only on Base
90
- tokens: ['USDC'], // only in USDC (use 'native' to also allow the chain's coin)
91
- hosts: ['*.example.com'], // only these hosts
92
- },
93
- onBeforePay: (q) => Number(q.amountFormatted) <= 0.05, // final say on each payment
94
- })
95
-
96
- // 1) Learn the price WITHOUT paying — decide if it's worth it.
97
- const q = await client.quote('https://api.example.com/report')
98
- // → { amountFormatted: '0.05', symbol: 'USDC', chain: 'base', withinPolicy: true, … } | null
99
-
100
- // 2) Know the GAS too — the native-coin fee to SEND it (you pay USDC, but burn ETH/SOL/TRX for gas).
101
- const est = await client.estimateCost('https://api.example.com/report')
102
- // → { quote: {…}, cost: { feeSymbol: 'ETH', feeFormatted: '0.000105', basis: 'estimated', … } } | null
103
-
104
- // 3) Pay (auto). Over-budget / declined → throws PaymentDeclinedError; nothing moves.
105
- const res = await client.fetch('https://api.example.com/report')
106
-
107
- // 4) Account for it.
108
- client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05', … }], records }
109
- ```
110
-
111
- **The budget can't be fooled.** `maxAmount`/`maxTotal` are enforced against the token's **true** decimals (the SDK's own, via the driver) — a server can't slip past a cap by understating the price, and an asset the SDK can't recognise is refused unless you set `allowUnknownTokens`. `quote()` even flags a `symbolMismatch` when a challenge's stated symbol disagrees with the real token.
112
-
113
- **`policy.tokens` takes symbols *or* `'native'`.** List stablecoin symbols (`'USDC'`, `'USDT'`, …) and/or the chain-agnostic alias **`'native'`** to allow the chain's own coin (ETH/BNB/TRX/XLM/…) on any family — the same word the accept side uses (`token: 'native'`), so you never name per-chain tickers (the real ticker works too). It only ever matches a genuinely native asset, so it never loosens a stablecoin-only list. The MCP server's `PIPRAIL_TOKENS` is the same allowlist.
114
-
115
- **Know the gas before you pay.** `client.estimateCost(url)` returns the quote **and** a `CostEstimate` — the network fee in the chain's **native coin** (you pay in USDC but burn ETH / SOL / TON / XLM / XRP / TRX on gas, a separate balance the agent must keep topped up). It's best-effort and labelled (`cost.basis`): a live-RPC read where cheap (`'estimated'` — EVM gas price, XRPL fee), a typical-cost constant otherwise (`'heuristic'`), and it never throws. Most valuable on **Tron**, where a USD₮ transfer can cost real TRX. So an agent can budget the *total* — payment **+** gas — before any funds move. Every driver implements it; the math is extracted per-chain and shaped uniformly by one shared `nativeCost()` helper.
116
-
117
- ### Plan before you pay — `planPayment()` (never fumble a payment)
118
-
119
- `quote()` tells you 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 fix. No funds move.
120
-
121
- ```ts
122
- const plan = await client.planPayment(url)
123
-
124
- if (plan?.payable) {
125
- await client.fetch(url, { autoRoute: true }) // pays plan.best — the cheapest rail you can settle
126
- } else {
127
- console.log(plan?.fundingHint)
128
- // "Have the USDC, but need ~0.000021 ETH for gas on base (have 0)."
129
- // "Recipient 2OT6…GC5E4 can't receive on algorand yet — must opt into the USDC ASA."
130
- // "Top up 0.04 USDC on base (have 0.01)."
131
- }
132
- ```
133
-
134
- 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:
135
- - **`payable`** + **`best`** — the cheapest rail you can actually settle (recipient confirmed able to receive);
136
- - **`options[]`** — each rail with typed **`blockers`** (`INSUFFICIENT_TOKEN` · `INSUFFICIENT_GAS` · `RECIPIENT_NOT_READY` · `OUTSIDE_POLICY`), soft **`warnings`** (`SYMBOL_MISMATCH`, `THIN_GAS_MARGIN`, `BALANCE_UNREADABLE`, …), a **`shortfall`**, the live **`balance`**, and **`recipient.fix`**;
137
- - **`fundingHint`** — one human sentence on exactly what to top up.
138
-
139
- **Why it's the agent unlock.** The official x402 client picks `accepts[0]` blind and learns it can't pay only when the broadcast reverts (no token, no gas) or the transfer silently strands (recipient not set up to receive). `planPayment` turns those runtime failures into a pre-checked decision — and on the no-facilitator path *you* pay your own gas, so "I hold USDC but no ETH" is a first-class answer, not a crash. It **never throws for a read hiccup** (a throttled RPC surfaces as `state: 'unknown'` + a warning, never a false "broke"), returns `null` when the URL isn't gated, and *explains* "this is offered on solana, base — you're on xrpl" instead of erroring. `client.canAfford(url)` is the one-boolean convenience.
140
-
141
- **Auto-route (opt-in).** `new PipRailClient({ autoRoute: true })` (or `fetch(url, { autoRoute: true })`) makes `fetch` pay the cheapest *settleable* rail instead of the first policy-passing one — refusing with `PaymentDeclinedError` + the funding hint before any send. **Default off; the zero-config path is unchanged.**
142
-
143
- **Across chains.** A client is bound to one chain; **`planAcross([baseClient, solanaClient, …], url)`** runs each plan in parallel and merges them payable-first, so an agent holding funds on several chains learns which to use. (No price oracle — cross-coin ties break on the order you list the clients.)
144
-
145
- ### Hand an LLM a budget-bound wallet
146
-
147
- `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 in a couple of lines. The budget rides on the client, so the model can't overspend.
148
-
149
- ```ts
150
- import { paymentTools } from '@piprail/sdk'
151
- const tools = paymentTools(client)
152
- // → [piprail_discover, piprail_quote_payment, piprail_plan_payment,
153
- // piprail_pay_request, piprail_register, piprail_budget, piprail_guide]
154
- ```
155
-
156
- Each descriptor also carries advisory **`annotations`** (MCP-style `ToolAnnotations` — `title`,
157
- `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`): the reads are flagged
158
- **read-only**, `piprail_pay_request` is flagged **value-moving** (the one tool that spends), and
159
- `piprail_register` is non-destructive — so an MCP client can render the right consent. They're hints,
160
- not the boundary; the spend policy is. `@piprail/mcp` advertises them on the wire.
161
-
162
- See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
163
-
164
- ### A budget that's also a clock — the time envelope (Mode A)
165
-
166
- The spend policy already caps *amount* and *total*; it can also bound *time*, so a headless agent
167
- runs free **inside** a budget **and** time envelope and the policy *is* the consent — no per-pay ask.
168
- All opt-in; omit them and behaviour is byte-identical.
169
-
170
- ```ts
171
- const client = new PipRailClient({
172
- chain: 'base', wallet,
173
- policy: {
174
- maxAmount: '0.10', maxTotal: '5.00', tokens: ['USDC'],
175
- ttlSeconds: 3600, // the whole session expires in 1h — then EVERY pay is refused
176
- windowTotal: '1.00', windowSeconds: 600, // …and ≤ $1 of USDC in any rolling 10-minute window
177
- },
178
- })
179
-
180
- client.budget() // → { session: { expiresAt, secondsRemaining }, byAsset: [...] } — read the leash before paying
181
- client.remaining() // → per-(network,asset) remaining cap (ledger-scoped)
182
- ```
183
-
184
- A pay past the deadline throws `PaymentDeclinedError` with **`reasonCode: 'SESSION_EXPIRED'`** (TERMINAL
185
- — don't retry); a window breach is `'OUTSIDE_WINDOW'`. The window needs **both** `windowTotal` and
186
- `windowSeconds` (one alone throws at construction — a leash can't be half-armed). State is in-memory and
187
- **resets on restart** (the session *is* the process); durable limits are the caller's pluggable concern.
188
-
189
- ### Legible to the model — the decline contract + the guide
190
-
191
- `piprail_pay_request` funnels **every** failure into a structured `{ ok:false, code, reason, explain,
192
- ref?, reasonCode?, declined? }` — never an uncaught crash — so a broadcast-but-unconfirmed timeout
193
- reaches the agent with its `.ref` and the **never-re-pay** rule, not a double-spend. Pure helpers make
194
- the rest legible: `summarizePlan` / `explainDecline` / `formatSpendReport` (one-line English),
195
- `classifyChallenge` (wrong-chain vs unpayable-scheme), and `PIPRAIL_AGENT_GUIDE` (the whole contract,
196
- distilled for an LLM). `piprail_budget` lets the agent self-check its leash; `piprail_guide` returns the contract.
197
-
198
- ## Be discoverable — find and be found ($0, no backend)
14
+ ---
199
15
 
200
- A 402 endpoint is payable, but nobody can *find* it. PipRail closes that gap by building on the
201
- **open** x402 indexes that already exist (402 Index, the CDP Bazaar read API, x402scan) — **nothing
202
- PipRail-hosted, no registry, no database.** All opt-in; the pay path is untouched. There's **no PipRail
203
- account and no x402 sign-up anywhere** — the only thing ever registered is a merchant's own URL.
204
- **The four steps below are the whole playbook** — an agent can follow them top to bottom (every method
205
- never throws and returns a typed result that says what to do next); [DISCOVERY.md](./DISCOVERY.md) is
206
- the deep reference.
207
-
208
- > **Experimental.** Discovery integrates with third-party open indexes whose conventions are young
209
- > and moving — treat this layer as experimental. The read path + 402 Index register are live-verified;
210
- > x402scan SIWX isn't yet. Note **402 Index probes your URL and only lists endpoints that actually
211
- > return a `402`** — so register a *deployed* gate, not a marketing page. (DISCOVERY.md §10 has the log.)
212
-
213
- **1) List a resource you run** — one call, no auth, no signature, no funds:
214
-
215
- ```ts
216
- const client = new PipRailClient({ wallet: { privateKey: KEY }, chain: 'base' })
217
-
218
- const outcomes = await client.register('https://api.example.com/report', {
219
- name: 'Market Report', priceUsd: 0.05, asset: 'USDC',
220
- targets: ['402index', 'x402scan'], // 402index is the default; x402scan adds SIWX (Base/Solana)
221
- })
222
- // Each outcome carries its LIFECYCLE — read `visibility` + `note`. "ok:true" ≠ "searchable now":
223
- // • 402index → { ok:true, visibility:'pending-review', note:'… verify your domain for instant approval' }
224
- // • x402scan → { ok:true, visibility:'live', note:"… discover() does NOT read x402scan" }
225
- ```
226
-
227
- **2) Flip 402 Index `pending-review` → searchable** — verify the domain you control (no funds, no sign-up):
228
-
229
- ```ts
230
- const claim = await client.claimDomain('https://api.example.com/report', { contactEmail: 'you@example.com' })
231
- // Serve claim.verificationHash as the ENTIRE body of claim.verificationUrl
232
- // (https://api.example.com/.well-known/402index-verify.txt) — then:
233
- await client.verifyDomain('api.example.com') // → { ok:true, status:'verified', servicesCount }
234
- // Now every pending listing on that domain is approved + searchable.
235
- ```
236
-
237
- **Works on every chain.** 402 Index needs no signature and has no chain allowlist, so *any* chain —
238
- a preset, a non-EVM family, or a custom `{ id, rpcUrl }` chain — can be listed and found; `discover()`
239
- never silently hides a resource whose chain it can't resolve. (x402scan is the one Base/Solana-only
240
- *bonus* target.)
241
-
242
- **Built-with attribution (tasteful, honest).** Your emitted `/openapi.json` carries an
243
- `x-generator: "@piprail/sdk"` stamp by default (opt out with `attribution: false`), and every index
244
- request sends a `User-Agent: @piprail/sdk` — so the tech spreads through the files indexes crawl and
245
- the logs operators read, never by spamming listings. An opt-in `register(url, { attribution: true })`
246
- adds a best-effort `via` tag; it's off by default (it's your listing).
247
-
248
- **3) Find resources to pay** — read the open indexes (free), filtered to your chain by default:
249
-
250
- ```ts
251
- const hits = await client.discover({ query: 'weather', maxPrice: 0.01 })
252
- // → [{ resource, name, source, priceUsd, rails: [...] }, …]
253
- const res = await client.fetch(hits[0].resource) // then quote → plan → pay as usual
254
- ```
255
-
256
- `discover()` reads **402 Index + CDP Bazaar**, **not x402scan** (its reads are paid) — a live x402scan
257
- listing won't appear here, so don't read that absence as failure. `network` defaults to `'self'` (your
258
- chain); pass `'any'` for every chain, or a CAIP-2 id (`'eip155:8453'`). Slugs map to CAIP-2 via
259
- `SLUG_TO_CAIP2`; an unresolved network is kept, never hidden.
260
-
261
- **4) Make your endpoint self-describing.** **x402scan REQUIRES an input schema or it won't list you.**
262
- The simplest path: set the gate's `discovery` option — it emits an `extensions.bazaar` block **in the 402
263
- itself**, so the challenge alone is x402scan-listable (no extra file to serve):
264
-
265
- ```ts
266
- // `discovery: true` for a no-input GET, or describe the request:
267
- const gate = createPaymentGate({
268
- chain: 'base', token: 'USDC', amount: '0.05', payTo,
269
- exact: { settle: { facilitator: 'https://facilitator.payai.network' } }, // be payable by exact clients (see caveat)
270
- discovery: { method: 'GET', output: { type: 'json', example: { ok: true } } },
271
- })
272
- ```
273
-
274
- Optionally also serve the static discovery files (richer listings; OpenAPI carries the `x-generator`
275
- attribution stamp) from your own origin — all pure, no I/O:
276
-
277
- ```ts
278
- import { buildOpenApi, buildWellKnownX402, buildX402DnsTxt } from '@piprail/sdk'
279
- const desc = await gate.describe('https://api.example.com/report')
280
- const openapi = buildOpenApi({ origin: 'https://api.example.com', resources: [desc] }) // → /openapi.json
281
- const wellKnown = buildWellKnownX402({ resources: [desc] }) // → /.well-known/x402
282
- // buildX402DnsTxt(...) emits the _x402 DNS line too.
283
- ```
284
-
285
- > **Caveat — be *payable*, not just listed.** The open indexes' agents are overwhelmingly standard
286
- > `exact` clients; a default `onchain-proof`-only gate gets listed but they can't pay it. Advertise an
287
- > `exact` rail (above) so a discovered resource is actually payable.
288
-
289
- **Know each index before you call** — the facts are one import, `DIRECTORY_INFO`, and `register()`
290
- projects them onto every outcome (`visibility` + `note`), so an agent never has to guess:
291
-
292
- | Index | Write auth | Chains | On a successful register | Read by `discover()`? |
293
- |---|---|---|---|---|
294
- | **402 Index** (default) | none | any | `pending-review` → `verifyDomain()` for instant approval | ✅ yes |
295
- | **x402scan** | one wallet sig (SIWX) | Base / Solana | `live` on x402scan.com | ❌ no (paid reads) |
296
- | **CDP Bazaar** | — (facilitator-only) | — | `not-listable` for PipRail (backendless) | ✅ read-only |
297
-
298
- ```ts
299
- import { DIRECTORY_INFO } from '@piprail/sdk'
300
- DIRECTORY_INFO['x402scan'].readByDiscover // false — branch on this, don't guess
301
- ```
302
-
303
- For an LLM/MCP these are two more tools — **`piprail_discover`** (find) and **`piprail_register`**
304
- (be found) — on top of the three payment tools, so `paymentTools(client)` / `@piprail/mcp` expose **five**.
305
-
306
- > **Two honest caveats.** The open indexes assume the mainstream `exact` scheme, so to be *usefully*
307
- > listed also offer a standard `exact` USDC rail on Base/Solana (`discover()` results are
308
- > cross-scheme; `fetch()` pays PipRail `onchain-proof` rails by default, and standard `exact` rails too
309
- > once you opt in with `schemes: ['onchain-proof', 'exact']` — EVM/EIP-3009). And **x402scan indexes
310
- > Base/Solana only** — 402 Index has no such limit, so it's the default register target. There is no
311
- > single ratified discovery standard yet; OpenAPI-first is an emerging multi-vendor convention.
312
-
313
- ### Accept several chains at once
314
-
315
- `requirePayment` (and `createPaymentGate`) take an **`accept: [...]`** array — one challenge that's payable on **any** of several chains/tokens, across **all ten families** (EVM, Solana, TON, Tron, Stellar, XRPL, NEAR, Sui, Aptos, Algorand). The agent pays with whatever it holds:
316
-
317
- ```ts
318
- requirePayment({
319
- accept: [
320
- { chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourEvmWallet…', rpcUrl: BASE_RPC },
321
- { chain: 'tron', token: 'USDT', amount: '0.05', payTo: 'TYourTronWallet…', rpcUrl: TRON_RPC },
322
- { chain: 'xrpl', token: 'USDC', amount: '0.05', payTo: 'rYourXrplWallet…' },
323
- { chain: 'solana', token: 'USDC', amount: '0.05', payTo: 'YourSolWallet…', rpcUrl: SOL_RPC },
324
- ],
325
- })
326
- ```
327
-
328
- Each option takes its **own optional `rpcUrl`** (falling back to the top-level `rpcUrl` when omitted), so a multi-chain merchant pins a reliable endpoint **per chain** — one throttled public RPC can't take down verification for the others. (The `rpcUrl` is used server-side only; it's never leaked into the challenge.) **In production, set it on every chain** — public RPCs are rate-limited.
329
-
330
- How the multi-chain case is handled, end-to-end:
331
-
332
- - **Gate:** each option resolves through its own driver with its own `rpcUrl` (its `payTo` is validated and its token resolved) and is listed in the challenge's `accepts[]`, sharing one nonce. `payTo` falls back to the top-level `payTo` when omitted — but address shapes differ per family, so give a per-option `payTo` for each non-EVM chain.
333
- - **Payer:** a `PipRailClient` is bound to **one** chain (its `chain` + wallet). It picks the offered accept whose network it supports **and** its `policy` allows, pays that one, and ignores the rest. `quote(url)` and `estimateCost(url)` price/estimate **that** chosen chain — so to compare cost across chains, point one client per chain at the same URL and compare their `estimateCost` results.
334
- - **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 (a wrong asset/network simply doesn't match). The same proof can't be redeemed twice.
335
-
336
- ## One word picks the chain
337
-
338
- ```ts
339
- requirePayment({ chain: 'base', token: 'USDC', amount: '0.05', payTo }) // USDC on Base
340
- requirePayment({ chain: 'arbitrum', token: 'USDC', amount: '0.05', payTo }) // USDC on Arbitrum
341
- requirePayment({ chain: 'bnb', token: 'USDT', amount: '1', payTo }) // USDT on BNB
342
- requirePayment({ chain: 'solana', token: 'USDC', amount: '0.05', payTo }) // USDC on Solana
343
- requirePayment({ chain: 'ton', token: 'USDT', amount: '1', payTo }) // USD₮ on TON
344
- requirePayment({ chain: 'tron', token: 'USDT', amount: '1', payTo }) // USD₮ on Tron
345
- requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.05', payTo }) // USDC on the XRP Ledger
346
- requirePayment({ chain: 'near', token: 'USDC', amount: '0.05', payTo }) // USDC on NEAR
347
- requirePayment({ chain: 'sui', token: 'USDC', amount: '0.05', payTo }) // USDC on Sui
348
-
349
- // Prefer the chain's native coin? Same one-liner — token: 'native'.
350
- requirePayment({ chain: 'ethereum', token: 'native', amount: '0.001', payTo }) // ETH
351
- requirePayment({ chain: 'base', token: 'native', amount: '0.001', payTo }) // ETH on Base
352
- requirePayment({ chain: 'bnb', token: 'native', amount: '0.01', payTo }) // BNB
353
- requirePayment({ chain: 'solana', token: 'native', amount: '0.1', payTo }) // SOL
354
- requirePayment({ chain: 'ton', token: 'native', amount: '1', payTo }) // TON
355
- requirePayment({ chain: 'xrpl', token: 'native', amount: '1', payTo }) // XRP
356
- ```
357
-
358
- **Native or stablecoin — your choice, on every chain.** Every gate accepts the chain's native coin (ETH, BNB, POL, AVAX, SOL, TON, XLM, XRP, SUI, NEAR, **TRX**, …) just as readily as a stablecoin — set `token: 'native'` and the SDK fills in the right decimals (18 on EVM, 9 on Solana/TON/Sui, 8 on Aptos, 7 on Stellar, 6 on XRPL/Tron/Algorand, 24 on NEAR). Verification, replay protection, and self-custody are identical to the stablecoin path — across **all ten families, no exceptions**. (On **NEAR**, native is the zero-setup path — no `storage_deposit` — while the NEP-141 token path needs registration; see the NEAR note. On **Tron**, USD₮ is the default since TRX is volatile gas, but native TRX works too.)
359
-
360
- `token` is **required** — every gate states exactly what it accepts, so there's never any doubt whether a route takes USDC, USDT, or the native coin. Name a built-in symbol (`'USDC'`, `'USDT'`), use `'native'` for the chain's own coin (ETH, BNB, SOL, TON, XLM, …), or pass a custom token by address. The symbol is all you write — the SDK fills in the contract + decimals.
361
-
362
- ### Built-in chains (mainnet)
363
-
364
- Every token address below was verified on-chain (symbol + decimals) before shipping.
365
-
366
- | `chain` | Network | Tokens |
367
- |---|---|---|
368
- | `'ethereum'` | Ethereum | USDC, USDT |
369
- | `'base'` | Base | USDC |
370
- | `'arbitrum'` | Arbitrum | USDC, USDT |
371
- | `'optimism'` | Optimism | USDC, USDT |
372
- | `'polygon'` | Polygon | USDC, USDT |
373
- | `'bnb'` | BNB Chain | USDC, USDT |
374
- | `'avalanche'` | Avalanche | USDC, USDT |
375
- | `'mantle'` | Mantle | USDC, USDT |
376
- | `'sonic'` | Sonic | USDC, USDT |
377
- | `'linea'` | Linea | USDC, USDT |
378
- | `'scroll'` | Scroll | USDC, USDT |
379
- | `'celo'` | Celo | USDC, USDT |
380
- | `'zksync'` | zkSync Era | USDC, USDT |
381
- | `'unichain'` | Unichain | USDC, USDT |
382
- | `'worldchain'` | World Chain | USDC |
383
- | `'sei'` | Sei | USDC |
384
- | `'injective'` | Injective | USDC, USDT |
385
- | `'hyperevm'` | HyperEVM (Hyperliquid) | USDC |
386
- | `'monad'` | Monad | USDC |
387
- | `'kaia'` | Kaia (ex-Klaytn) | USDT |
388
- | `'solana'` | Solana | USDC, USDT |
389
- | `'ton'` | TON | USDT |
390
- | `'tron'` | Tron | USDT |
391
- | `'near'` | NEAR | USDC, USDT |
392
- | `'sui'` | Sui | USDC |
393
- | `'aptos'` | Aptos | USDC, USDT |
394
- | `'algorand'` | Algorand | USDC |
395
- | `'stellar'` | Stellar | USDC, EURC |
396
- | `'xrpl'` | XRP Ledger | USDC, RLUSD |
397
-
398
- **TON note:** native **USDC does not exist on TON** (Circle doesn't issue it there) — so it's intentionally absent. USD₮ (Tether) is native and built in; for USDe / bridged tokens pass a custom jetton (below).
399
-
400
- **Tron note:** native **USDC doesn't exist on Tron** (Circle discontinued it; the only USDC there is a third-party bridge) — so it's intentionally absent. USD₮ (TRC-20) is native and built in, and is the default since TRX is volatile gas. **Native TRX is also supported** (`token: 'native'`, digest-bound) for completeness — or pass a custom TRC-20.
401
-
402
- **NEAR note:** **native NEAR works** (`token: 'native'`, 24dp) and is the **zero-setup** path — no `storage_deposit`, and a transfer even *creates* a fresh implicit recipient. Or pay in a token: ships **both native USDC + USDT** (Circle's native USDC `17208628…`, NOT the bridged `…factory.bridge.near`; Tether's native `usdt.tether-token.near`) — but a NEP-141 recipient (and the payer) must be **`storage_deposit`-registered** on that token once before it can receive (see CHAINS.md). NEAR is the volatile gas coin, so for stable pricing pay in USDC/USDT; for no-setup flows, native NEAR is ideal.
403
-
404
- **Sui note:** **USDC only** — no native USDT on Sui (Wormhole-bridged only). Native SUI works with `token: 'native'`.
405
-
406
- **Algorand note:** **USDC only** — Tether deprecated USDT on Algorand (frozen 2025-09-01), so it's intentionally absent (pass it as a custom `{ assetId, decimals }`). Native ALGO works with `token: 'native'` (the zero-setup path). To **receive** USDC the recipient must **opt into the ASA** once (a 0-amount self-transfer — like a trustline); a not-opted-in recipient surfaces `RECIPIENT_NOT_READY`. The challenge nonce binds inside the transaction's note field (Template A). Algorand's `exact` scheme is part of the official x402 standard; the incumbent on-chain path there uses a hosted facilitator, so PipRail is the backendless, no-facilitator option.
407
-
408
- **Stellar / XRPL note:** to **receive** an issued asset (USDC/EURC on Stellar; USDC/RLUSD on XRPL) the recipient needs a one-time **trustline** for that asset, and the account must already exist / be activated (a small native reserve — **locked, not spent**). Native XLM/XRP need no trustline. The payer needs its own trustline too.
409
-
410
- ### Using TON? Grab one free API key (≈30 seconds)
411
-
412
- TON is the only chain with a one-time setup step — and it's tiny. TON's free public RPC
413
- (toncenter) is **rate-limited**, so without your own key, payment confirmation stalls or
414
- times out. The fix is exactly **one parameter**: a `rpcUrl` with a free key in the URL.
415
-
416
- 1. **Get a free key** — message **[@tonapibot](https://t.me/tonapibot)** on Telegram (or sign
417
- up at [toncenter.com](https://toncenter.com/)). ~30 seconds, no card, no KYC.
418
- 2. **Drop it into `rpcUrl`** on the gate (and the client) — that's it:
16
+ ## Charge for an endpoint
419
17
 
420
18
  ```ts
421
- const TON_RPC = 'https://toncenter.com/api/v2/jsonRPC?api_key=YOUR_KEY' // ← your free key in the URL
19
+ import { requirePayment } from '@piprail/sdk'
422
20
 
423
- // Take a TON payment — one extra field vs any other chain:
424
21
  app.get('/report',
425
- requirePayment({ chain: 'ton', token: 'USDT', amount: '0.05', payTo: 'UQ…', rpcUrl: TON_RPC }),
22
+ requirePayment({ chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourWallet…' }),
426
23
  (_req, res) => res.json({ report: 'TOP SECRET' }),
427
24
  )
428
-
429
- // Pay on TON — same one extra field:
430
- const client = new PipRailClient({ chain: 'ton', wallet: { mnemonic }, rpcUrl: TON_RPC })
431
- ```
432
-
433
- That's the **whole** TON setup. Everything else is automatic: USD₮ is built in (native USDC
434
- doesn't exist on TON), native TON works too (`token: 'native'`), and the merchant needs no
435
- setup — the payer's gas deploys its jetton wallet on first receipt. **Skip the key → rate
436
- limits; add it → TON is as seamless as every other chain.**
437
-
438
- > 📖 **Per-chain setup, caveats & wallet formats → [CHAINS.md](CHAINS.md).** Exactly what each chain needs *before* it can pay or receive — the NEAR `storage_deposit`, Stellar/XRPL trustlines, TON API key, Tron gas, which chains accept `native`, and the wallet shape per family. **Most chains need nothing; NEAR, TON, Stellar, XRPL and Tron have caveats — read them before shipping those.**
439
-
440
- If a chain you need doesn't ship the token you want, pass it by address (below). `token` is required on every gate — no silent default.
441
-
442
- ### Any other chain or token — no allowlist
443
-
444
- Don't see your chain? Pass a [viem](https://viem.sh) `Chain` or a bare `{ id, rpcUrl }`, plus the exact token to be paid in — you have full control:
445
-
446
- ```ts
447
- requirePayment({
448
- chain: { id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev' }, // any EVM chain
449
- token: { address: '0x…', decimals: 6, symbol: 'USDC' }, // any ERC-20
450
- amount: '0.05',
451
- payTo,
452
- })
453
-
454
- // On Solana, a custom SPL token is { mint, decimals }:
455
- requirePayment({ chain: 'solana', token: { mint: '…', decimals: 6 }, amount: '0.05', payTo })
456
-
457
- // On TON, a custom jetton is { master, decimals }:
458
- requirePayment({ chain: 'ton', token: { master: 'EQ…', decimals: 6 }, amount: '0.05', payTo })
459
-
460
- // On Stellar, a custom classic asset is { issuer, code, decimals }:
461
- requirePayment({ chain: 'stellar', token: { issuer: 'G…', code: 'XYZ', decimals: 7 }, amount: '0.05', payTo })
462
-
463
- // On the XRP Ledger, a custom issued currency is { issuer, currencyHex, decimals }:
464
- requirePayment({ chain: 'xrpl', token: { issuer: 'r…', currencyHex: '5553444300000000000000000000000000000000', decimals: 6 }, amount: '0.05', payTo })
465
-
466
- // On Tron, a custom TRC-20 is { address, decimals } (Base58 T… contract):
467
- requirePayment({ chain: 'tron', token: { address: 'T…', decimals: 6 }, amount: '0.05', payTo })
468
-
469
- // On NEAR, a custom NEP-141 is { contractId, decimals }:
470
- requirePayment({ chain: 'near', token: { contractId: 'token.near', decimals: 6 }, amount: '0.05', payTo })
471
-
472
- // On Sui, a custom coin is { coinType, decimals }:
473
- requirePayment({ chain: 'sui', token: { coinType: '0x…::usdc::USDC', decimals: 6 }, amount: '0.05', payTo })
474
-
475
- // On Aptos, a custom Fungible Asset is { metadata, decimals }:
476
- requirePayment({ chain: 'aptos', token: { metadata: '0x…', decimals: 6 }, amount: '0.05', payTo })
477
-
478
- // On Algorand, a custom ASA is { assetId, decimals }:
479
- requirePayment({ chain: 'algorand', token: { assetId: 12345678, decimals: 6 }, amount: '0.05', payTo })
480
- ```
481
-
482
- > **Production:** the built-in chains use public RPCs (rate-limited). Pass your own `rpcUrl` for real traffic.
483
-
484
- ## Solana
485
-
486
- Solana works exactly like an EVM chain — just name it. The driver **auto-mounts** on first use (one lazy import), so pure-EVM installs never download the Solana libraries. The only step is installing the peer deps:
487
-
488
- ```bash
489
- npm install @solana/web3.js @solana/spl-token bs58
490
- ```
491
-
492
- ```ts
493
- import { requirePayment, PipRailClient } from '@piprail/sdk'
494
-
495
- // No setup call — naming the chain mounts the driver.
496
- requirePayment({ chain: 'solana', token: 'USDC', amount: '0.05', payTo: 'YourBase58Wallet…' })
497
- new PipRailClient({ wallet: { secretKey: SOLANA_SECRET }, chain: 'solana' })
498
- ```
499
-
500
- EVM wallets are `{ privateKey }` (or a viem `{ walletClient }`); Solana wallets are `{ secretKey }` (a `Uint8Array` or base58 string) or `{ signer }`. Mismatching a wallet or `payTo` to the wrong family throws a clear `WrongFamilyError` on first use.
501
-
502
- ## TON
503
-
504
- TON (the Telegram blockchain) works the same way — name it. The driver **auto-mounts** on first use, so pure EVM/Solana installs never download the TON libraries. Install the peer deps:
505
-
506
- ```bash
507
- npm install @ton/ton @ton/core @ton/crypto
508
- ```
509
-
510
- ```ts
511
- import { requirePayment, PipRailClient } from '@piprail/sdk'
512
-
513
- requirePayment({ chain: 'ton', token: 'USDT', amount: '1', payTo: 'EQ…or UQ…' })
514
- new PipRailClient({ wallet: { mnemonic: process.env.TON_MNEMONIC }, chain: 'ton' })
515
- ```
516
-
517
- TON wallets are `{ mnemonic }` (24 words — a `string[]` or one space-separated string) or a ready `{ keyPair }`; add `version: 'v5r1'` for a W5 wallet (default is `v4`). USD₮ is built in (verified on-chain); native **USDC doesn't exist on TON**. Payments use [jettons](https://docs.ton.org/develop/dapps/asset-processing/jettons): the proof carries the gate's nonce as the transfer comment, so a TON proof is **bound to the challenge** that issued it, and verification reads the merchant's own jetton wallet — a look-alike jetton can't satisfy it. Note the payer needs a little **TON for gas** (~0.05) to send a jetton, on top of the USD₮.
518
-
519
- ## Tron
520
-
521
- Tron is the single largest stablecoin-payment rail on earth (~45% of all USDT). Name it — the driver **auto-mounts** on first use, so other installs never download the Tron library. Install the peer dep:
522
-
523
- ```bash
524
- npm install tronweb
525
- ```
526
-
527
- ```ts
528
- import { requirePayment, PipRailClient } from '@piprail/sdk'
529
-
530
- requirePayment({ chain: 'tron', token: 'USDT', amount: '1', payTo: 'T…' })
531
- new PipRailClient({ wallet: { privateKey: process.env.TRON_KEY }, chain: 'tron' })
532
- ```
533
-
534
- Tron wallets are `{ privateKey }` (a 32-byte hex key — Tron uses secp256k1, like EVM). `payTo` is a Base58 `T…` address (an `0x…` address throws `WrongFamilyError`). **USD₮ (TRC-20) is built in, and native TRX is also supported** (`token: 'native'`, digest-bound) — native USDC doesn't exist on Tron (pass a custom `{ address, decimals }` for other TRC-20s). Verification is **digest-bound** (the proof is the txid): the merchant verifies the confirmed transfer on the **solidity node** (the finality gate) and the proof is single-use — so for multi-instance deployments use a persistent `isUsed`/`markUsed` store and keep `maxTimeoutSeconds` tight. The payer needs a little **TRX for energy/bandwidth** to send; receiving USDT needs no account setup.
535
-
536
- ## Stellar
537
-
538
- Stellar is payment-native (~5s finality, sub-cent fees), with native Circle **USDC + EURC**. Name it `'stellar'` — the driver **auto-mounts** on first use. Install the peer dep:
539
-
540
- ```bash
541
- npm install @stellar/stellar-sdk
542
- ```
543
-
544
- ```ts
545
- import { requirePayment, PipRailClient } from '@piprail/sdk'
546
-
547
- requirePayment({ chain: 'stellar', token: 'USDC', amount: '0.05', payTo: 'G…' })
548
- new PipRailClient({ wallet: { secret: process.env.STELLAR_SECRET }, chain: 'stellar' })
549
- ```
550
-
551
- Stellar wallets are `{ secret }` (an `S…` secret seed) or a ready `{ keypair }` (a stellar-sdk `Keypair`); `payTo` is a `G…` account. USDC + EURC are built in (both Circle issuers verified live on Horizon mainnet); native XLM works with `token: 'native'`. Assets are **7-decimal**. The challenge nonce binds via the transaction **memo** — a `MEMO_HASH = sha256(nonce)` (Template A) — so a Stellar proof is **bound to its challenge**; verification reads the payment to `payTo` on Horizon and matches the memo hash, amount, and the asset's `CODE:ISSUER`. **To RECEIVE USDC/EURC the merchant account needs a one-time trustline** (`changeTrust` to the issuer) plus the XLM base reserve; native XLM needs neither.
552
-
553
- ## XRP Ledger
554
-
555
- XRPL is payment-native (~3–5s finality), with native USDC + Ripple's RLUSD. Name it `'xrpl'` — the driver **auto-mounts** on first use. Install the peer dep:
556
-
557
- ```bash
558
- npm install xrpl
559
- ```
560
-
561
- ```ts
562
- import { requirePayment, PipRailClient } from '@piprail/sdk'
563
-
564
- requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.05', payTo: 'r…' })
565
- new PipRailClient({ wallet: { seed: process.env.XRPL_SEED }, chain: 'xrpl' })
566
- ```
567
-
568
- XRPL wallets are `{ seed }` (an `s…` secret seed) or a ready `{ wallet }` (an xrpl.js `Wallet`); `payTo` is a classic `r…` address. USDC + RLUSD are built in (both issuers verified live on mainnet); native XRP works with `token: 'native'`. The challenge nonce rides in a **Memo** (the cryptographic binding) plus a derived **DestinationTag** for deliverability, so an XRPL proof is **bound to its challenge**. Verification compares **`delivered_amount`** — what actually arrived — never `Amount`, which closes the `tfPartialPayment` attack. **To RECEIVE USDC/RLUSD the merchant account needs a one-time trustline** (`TrustSet`) plus the XRPL base reserve; native XRP needs neither.
569
-
570
- ## NEAR
571
-
572
- NEAR is the "user-owned AI" chain (its co-founder co-authored the Transformer paper), with native USDC **and** USDT. Name it `'near'` — the driver **auto-mounts** on first use. Install the peer dep:
573
-
574
- ```bash
575
- npm install near-api-js
576
- ```
577
-
578
- ```ts
579
- import { requirePayment, PipRailClient } from '@piprail/sdk'
580
-
581
- requirePayment({ chain: 'near', token: 'USDC', amount: '0.05', payTo: 'merchant.near' })
582
- new PipRailClient({ wallet: { accountId: 'agent.near', privateKey: process.env.NEAR_KEY }, chain: 'near' })
583
- ```
584
-
585
- NEAR wallets are `{ accountId, privateKey }` (privateKey = an `ed25519:…` secret); `payTo` is a NEAR account id (`name.near` or a 64-hex implicit account). **Native NEAR is supported** (`token: 'native'`, 24dp) and is the **zero-setup** path — digest-bound (proof `<accountId>:<txHash>`, verified by tx hash + recency + single-use), needing **no `storage_deposit`**; a native transfer even *creates* a fresh implicit recipient. **Or pay in a token:** both USDC + USDT are native and built in (Circle's `17208628…`, Tether's `usdt.tether-token.near`) — the NEP-141 path is memo-bound (the nonce rides in the `ft_transfer` **`memo`**, verified by tx hash; verify only trusts an `ft_transfer` event from the real token contract), but **`storage_deposit` is required:** a recipient (and the payer) must be NEP-145-registered on that token once (~0.00125 NEAR) before it can receive/hold it, or `ft_transfer` panics. The payer needs a little **NEAR for gas** either way. (Never route through NEAR Intents/solvers — that re-adds a facilitator; plain transfers are what we do.)
586
-
587
- ## Sui
588
-
589
- Sui is a Move L1 with sub-second finality + native Circle USDC (and protocol-level gasless stablecoin transfers). Name it `'sui'` — the driver **auto-mounts** on first use. Install the peer dep:
590
-
591
- ```bash
592
- npm install @mysten/sui
593
- ```
594
-
595
- ```ts
596
- import { requirePayment, PipRailClient } from '@piprail/sdk'
597
-
598
- requirePayment({ chain: 'sui', token: 'USDC', amount: '0.05', payTo: '0x…' })
599
- new PipRailClient({ wallet: { privateKey: process.env.SUI_KEY }, chain: 'sui' })
600
- ```
601
-
602
- Sui wallets are `{ privateKey }` (a `suiprivkey1…` bech32 secret) or a ready `{ keypair }` (an Ed25519Keypair); `payTo` is a Sui `0x…` address (32-byte). **USDC only** — no native USDT on Sui; native SUI works with `token: 'native'`. Verification is **digest-bound** (the proof is the tx digest, like EVM/Solana): the merchant reads the tx's balance changes — a positive change of the required coin type to `payTo` — and the proof is single-use, so for multi-instance deployments use a persistent `isUsed`/`markUsed` store and keep `maxTimeoutSeconds` tight. The driver ships the standard self-gas `Coin<USDC>` transfer (the payer needs a USDC coin object + a little SUI for gas); Sui's protocol-level **gasless** stablecoin path is a separate tx shape and a future enhancement — so this path isn't marketed as "gasless".
603
-
604
- ## How it works
605
-
606
25
  ```
607
- Agent Your server
608
- │ GET /report │
609
- │ ───────────────────────────────────────►│ requirePayment
610
- │ ◄──────────── 402 + payment-required ────│ (issues a challenge)
611
- │ │
612
- │ pay on-chain (one transfer to payTo) │
613
- │ ───────────────────► [the chain] │
614
- │ ◄── proof (tx hash / signature) ───── │
615
- │ │
616
- │ GET /report + payment-signature │
617
- │ ───────────────────────────────────────►│ verifies the tx against
618
- │ ◄──────────── 200 + your content ────────│ its own RPC, then next()
619
- ```
620
-
621
- Verification is local and confirms the transaction **succeeded, is recent, and actually moved the required amount of the right token to `payTo`** — then your handler runs and returns the data. The same proof can't be redeemed twice. **Self-custody throughout:** the payer signs and broadcasts their own transfer straight to your wallet; PipRail never holds funds and never takes a cut of a payment.
622
-
623
- It's a **pull** model: the caller hands you the exact tx ref in the `payment-signature` header, so `verify()` does a **targeted lookup on your own RPC** and answers **synchronously, in the same request** — no chain listener, no indexer, no accounts DB, no async notify. Why that matters vs. "just send a raw transfer" — with runnable proof and an honest scorecard — is laid out in [`examples/why-402/`](../examples/why-402/) (and the [root README](../README.md#-why-402-and-not-just-a-raw-transfer)).
624
26
 
625
- ## Receipts record every payment
27
+ That route now costs **0.05 USDC on Base**, paid straight to your wallet. One parameter picks the chain. → [Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)
626
28
 
627
- Every verified payment produces an `X402Receipt` with exactly what you'd persist — the on-chain tx ref, who paid, the amount, and the token. The SDK stays **database-free**; it hands you the data and you store it however you like.
29
+ ## Let an agent pay for it
628
30
 
629
31
  ```ts
630
- // (1) The onPaid hook — fires on every settled payment.
631
- requirePayment({
632
- chain: 'base', token: 'USDC', amount: '0.05', payTo,
633
- onPaid: (receipt) => db.payments.insert(receipt),
634
- })
635
-
636
- // (2) Or read it off the framework-agnostic gate result.
637
- const r = await gate.verify(headerValue)
638
- if (r.kind === 'paid') await db.payments.insert(r.receipt)
639
- ```
640
-
641
- The receipt:
642
-
643
- | Field | Example | Meaning |
644
- |---|---|---|
645
- | `transaction` | `0x9af…` · Solana signature · Sui digest | the on-chain transaction id |
646
- | `payer` | `0x2b…` / `alice.near` | who paid |
647
- | `payTo` | your wallet | who received |
648
- | `asset` | USDC contract / coinType | token paid |
649
- | `amount` | `50000` | amount, in base units |
650
- | `network` | `eip155:8453` | which chain (CAIP-2) |
651
- | `verifiedAt` | ISO timestamp | when the gate verified it |
652
- | `scheme` | `'onchain-proof'` | settlement scheme (x402 v2) |
653
- | `success` | `true` | settlement succeeded (always `true` — failures return a 402, never a receipt) |
654
-
655
- On the payer side, the client surfaces the same receipt via the `payment-settled` event (`onEvent`) and `client.spent()` keeps a running per-asset ledger.
656
-
657
- ## Security model
658
-
659
- What local verification guarantees, and what to know:
660
-
661
- - **No third party.** The proof is a real on-chain transaction; your server checks it against your own RPC. Nothing is hosted in between and PipRail never holds funds.
662
- - **Replay protection.** Each gate keeps an in-memory used-proof set, so one transaction can be redeemed once; a recency window (`maxTimeoutSeconds`, default 600s) rejects stale payments. Running multiple instances? Share the set with `isUsed` / `markUsed` (e.g. Redis `SET NX`).
663
- - **Proof binding.** A proof is a public transaction hash, bound to *amount + token + `payTo` + recency* — not to the caller's identity. So **use a dedicated `payTo` per paid resource** (don't reuse a wallet that also receives unrelated transfers), and treat the recency window as the exposure bound. For contested or high-value endpoints where you need the proof cryptographically tied to the payer, open an issue — payer-bound proofs (the caller signs the challenge nonce with the paying key) are a planned opt-in.
664
- - **Confirmations.** `minConfirmations` (default 1) gates access; raise it for higher-value payments on chains with cheaper reorgs.
665
-
666
- ## Any framework
667
-
668
- `requirePayment` is Express/Connect middleware. For Hono, Fastify, Workers, Next.js, Bun, Deno — anything with `fetch` — build a gate and switch on the result:
669
-
670
- ```ts
671
- import { createPaymentGate, toInvalidBody } from '@piprail/sdk'
672
-
673
- const gate = createPaymentGate({ chain: 'base', token: 'USDC', amount: '0.05', payTo })
674
-
675
- export async function handler(req: Request): Promise<Response> {
676
- const r = await gate.verify(req.headers.get('payment-signature') ?? undefined)
677
- if (r.kind === 'paid') return Response.json(data, { headers: { 'payment-response': r.receiptHeader } })
678
- if (r.kind === 'challenge') return Response.json(r.challenge, { status: 402, headers: { 'payment-required': r.requiredHeader } })
679
- return Response.json(toInvalidBody(r), { status: 402 }) // canonical 402 body on every adapter
680
- }
681
- ```
682
-
683
- Reuse one gate per route — its in-memory replay guard stops a proof being spent twice. Running multiple instances? Pass your own `isUsed` / `markUsed` (e.g. Redis `SET NX`).
684
-
685
- ## In the browser — no build, no npm
686
-
687
- The SDK is browser-clean (no Node-only globals in the protocol layer), so a plain HTML page can take **or** make payments straight from a CDN — every npm-mirroring CDN serves it automatically:
32
+ import { PipRailClient } from '@piprail/sdk'
688
33
 
689
- ```html
690
- <script type="module">
691
- import { PipRailClient } from 'https://esm.sh/@piprail/sdk' // or jsDelivr: .../npm/@piprail/sdk@1/+esm
692
- // In a browser, sign with the visitor's wallet — never a raw key (page source is public):
693
- import { createWalletClient, custom } from 'https://esm.sh/viem'
694
- const walletClient = createWalletClient({ transport: custom(window.ethereum) })
34
+ const client = new PipRailClient({ chain: 'base', wallet: { privateKey: process.env.AGENT_KEY } })
695
35
 
696
- const client = new PipRailClient({ chain: 'base', wallet: { walletClient } })
697
- const res = await client.fetch('https://api.example.com/paid') // 402 → wallet signs → 200
698
- </script>
36
+ const res = await client.fetch('https://api.example.com/report') // hits the 402, pays it, retries with proof
699
37
  ```
700
38
 
701
- - **Which chains run in the browser.** EVM (viem) works out of the box; **Solana, Sui, and NEAR** load their libs from the CDN too (an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) pins each to a browser-ESM build — see [`examples/browser/`](../examples/browser)). A few chains' libraries (**TON, Tron, XRPL, Stellar**) don't ship a clean browser ESM build yet, so use those **server-side** — the identical one line, on Node/Bun/Deno/Workers. The lazy import means a pure-EVM page never downloads any of them.
702
- - **The merchant gate runs anywhere.** `createPaymentGate` needs only a `payTo` address — no key — so building challenges and verifying proofs works in the browser too (the typical *deployment* is still a server, since a browser can't receive inbound HTTP to gate).
703
- - **Both halves verified on Node and in a real browser**, against the published package. Runnable showcase: [`examples/browser/`](../examples/browser) — a single HTML file with a live, in-browser 402 demo; or try it hosted at [piprail.com/demo](https://piprail.com/demo).
704
- - **Keys:** raw `{ privateKey }` wallets belong only in a **server's** environment. In a browser, use an injected `walletClient` as above.
705
-
706
- ## Architecture (under the hood)
707
-
708
- Two layers, one contract. Worth knowing if you're extending the SDK or auditing it.
709
-
710
- - **The protocol layer is chain-agnostic.** `server.ts` (`requirePayment`/`createPaymentGate`), `client.ts` (`PipRailClient`), `x402.ts` (wire envelopes), `policy.ts`, `ledger.ts`, and `agent.ts` depend **only** on the `PaymentDriver` contract in `drivers/types.ts` — zero `viem`, zero `@solana/web3.js`, zero chain SDK. The chain is data the caller passes, not an allowlist the SDK ships.
711
- - **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `balanceOf` · `recipientReady` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
712
- - **Families mirror each other file-for-file.** Each lives in `drivers/<family>/` as `chains` · `wallet` · `pay` · `verify` · `index`, with family-suffixed functions (`payEvm`/`paySui`/…, `verifyEvm`/`verifyNear`/…). Ten today: `evm`, `solana`, `ton`, `stellar`, `xrpl`, `tron`, `near`, `sui`, `aptos`, `algorand`. Adding one = copy the five files, implement the contract, `registerDriver` — the protocol layer never changes.
713
- - **Routing + lazy auto-mount.** `registry.ts` maps a `chain` value to its family synchronously (`familyForChain`). EVM is always present (viem is a hard peer); every non-EVM family **loads itself on first use** via one dynamic `import()`, so a pure-EVM install never downloads `@solana`/`@ton`/`@stellar`/`xrpl`/`tronweb`/`near-api-js`/`@mysten/sui`/`@aptos-labs/ts-sdk`/`algosdk`. A build-time invariant asserts the main bundle has **zero** static imports of those libs — only per-family lazy chunks.
714
- - **Two verification templates.** *Template A (memo-bound)* — Stellar, XRPL, TON, NEAR, Algorand — carries the challenge nonce inside the transfer (memo / tag / comment / note), so the proof is cryptographically bound to its challenge. *Template B (digest-bound)* — EVM, Solana, Tron, Sui, Aptos — binds via a single-use proof set + recipient + amount + a tight recency window (use a persistent `isUsed`/`markUsed` store in production).
715
- - **Gas estimation.** Every driver's `estimateCost` extracts its own per-chain fee math, shaped into one uniform `CostEstimate` by the shared `nativeCost()` helper (`util/cost.ts`).
716
- - **The tests are the contract** (`test/`, Vitest), and two living standards govern any change: **[ERRORS.md](./ERRORS.md)** (how every module reports errors) and **STANDARDS.md** (how anything in the SDK is built + the verification gate). Runnable examples — including a local Anvil end-to-end — live in [`examples/`](../examples).
717
-
718
- ## Errors
719
-
720
- Every failure is **typed and understandable** — never a raw chain-library blob. Two channels:
721
-
722
- - **Thrown** — a `PipRailError` subclass with a stable `.code` (`INSUFFICIENT_FUNDS`, `RECIPIENT_NOT_READY`, `WRONG_FAMILY`, `UNKNOWN_TOKEN`, `CONFIRMATION_TIMEOUT`, `MAX_RETRIES_EXCEEDED`, `PAYMENT_DECLINED`, …). Catch with `err instanceof PipRailError` or branch on `err.code`. Affordability always surfaces as one `InsufficientFundsError`, on every chain. A `policy`/`onBeforePay` refusal is `PaymentDeclinedError`, thrown before any send.
723
- - **Returned** — server-side `verify()` rejects a proof with a `VerifyErrorCode` (`amount_too_low`, `transfer_not_found`, `payment_expired`, `tx_reverted`, …). The gate emits a 402 body `{ x402Version: 2, status: 'invalid', error, detail }` (build it with `toInvalidBody`), and the client relays the reason — so a rejected agent learns *why* (`MaxRetriesExceededError: … amount_too_low — Paid 40000, required 500000`).
724
-
725
- ### "Why did my payment fail?" — payer vs. recipient
726
-
727
- A failed payment is almost always one of two things, and PipRail tells them apart so a human **or an AI agent** knows exactly what to fix — never an opaque `tecNO_DST_INSUF_XRP`:
728
-
729
- - **`INSUFFICIENT_FUNDS`** → the **payer** can't cover it. Fund the payer (more token, native gas, or the chain's reserve).
730
- - **`RECIPIENT_NOT_READY`** → the **recipient** isn't set up to receive *on this chain yet*. This is a **chain requirement, not an SDK bug** — most chains gate receiving behind some one-time state. Every such message says what's needed and the fix, **echoes the raw chain code** (e.g. `(XRPL: tecNO_DST_INSUF_XRP)`), and keeps the untouched chain error on `err.cause` for debugging.
731
-
732
- **What each chain needs to *receive* (and who sets it up):**
733
-
734
- | Chain | The recipient must… | Sender also needs |
735
- |---|---|---|
736
- | **EVM · Solana · Sui · Aptos · Tron** | nothing (just be a valid address; Solana's token account is auto-created by the SDK; Aptos's primary FA store auto-creates) | native gas |
737
- | **TON** | nothing for native; a jetton wallet auto-deploys on first receipt (sender pays the gas) | TON for gas |
738
- | **NEAR** | nothing for native; for a token, be `storage_deposit`-registered on it (NEP-145, ~0.00125 NEAR, one-time) | NEAR for gas |
739
- | **Stellar** | exist (created with ≥1 XLM base reserve); for USDC/EURC, hold a **trustline** (+0.5 XLM each) | base + trustline reserves |
740
- | **XRP Ledger** | be **activated** — hold ≥1 XRP base reserve to exist; for USDC/RLUSD, a **trustline** | keep its own 1 XRP reserve |
741
- | **Algorand** | nothing for native ALGO; for USDC, **opt into the ASA** once (a 0-amount self-transfer, ~0.1 ALGO min-balance bump) | ALGO for fees + its own opt-in |
742
-
743
- > These are anti-spam "state rent" rules built into each ledger — e.g. an XRPL account can't receive a sub-1-XRP first payment because that payment must create the account at its ≥1 XRP base reserve. PipRail surfaces them as `RECIPIENT_NOT_READY` with the fix, so a payment that "can't go through" is self-explanatory. Per-chain specifics live in **[CHAINS.md](./CHAINS.md)**.
39
+ The same app can **take** payments and **make** them. [Making payments](https://docs.piprail.com/making-payments/piprail-client/)
744
40
 
745
- ### Flaky RPC? No false unlocks, no double-pays
41
+ ---
746
42
 
747
- Public RPCs are rate-limited, so reads sometimes fail *after* a transaction is already on-chain. PipRail is built so that never costs you money or a leaked unlock:
43
+ ## Documentation
748
44
 
749
- - **The merchant never unlocks without a real payment.** If the gate's verification read fails, it returns `tx_not_found` → **402 (locked)**, never `paid`. Verification *fails closed* — an RPC outage can't be exploited to get free access. And the gate **releases the replay claim** on failure, so the payer can re-submit the *same* proof once the RPC recovers (the proof isn't burned).
750
- - **The payer never loses a broadcast payment.** If the transfer broadcasts but the client's own confirmation times out (a throttled RPC that lands the tx but 429s the status read), the client does **not** throw the proof away — it emits a `payment-unconfirmed` event, submits the proof to the server (the on-chain authority) with more patient retries, and **never re-broadcasts**. If it still can't confirm, it throws `MaxRetriesExceededError` / `PaymentTimeoutError` carrying **`.ref`**.
751
-
752
- > **Agent recovery rule:** on `MAX_RETRIES_EXCEEDED` / `PAYMENT_TIMEOUT`, read `err.ref` and **re-verify or re-submit that proof — never re-pay** (a fresh payment double-spends). The proof stays redeemable until the gate's `maxTimeoutSeconds` window (default 600s). The real fix for repeated lag is a dedicated `rpcUrl` (per chain in multi-accept) instead of the public default.
753
-
754
- The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
755
-
756
- ## API
757
-
758
- **`requirePayment(options)`** → Express middleware &nbsp;·&nbsp; **`createPaymentGate(options)`** → `{ challenge, verify, describe }` (`describe()` → static discovery metadata for the emitters)
759
-
760
- | Option | Default | Notes |
761
- |---|---|---|
762
- | `chain` | — | `'base'` / `'bnb'` / `'solana'` / `'ton'` / …, a viem `Chain`, or `{ id, rpcUrl }` (single-chain form) |
763
- | `amount` | — | Human-readable, e.g. `'0.05'` (single-chain form) |
764
- | `token` | — | `'USDC'` / `'USDT'`, `'native'`, or a custom `{ address, decimals }` (EVM/Tron) / `{ mint, decimals }` (Solana) / `{ master, decimals }` (TON) / `{ issuer, code, decimals }` (Stellar) / `{ issuer, currencyHex, decimals }` (XRPL) / `{ contractId, decimals }` (NEAR) / `{ coinType, decimals }` (Sui) / `{ metadata, decimals }` (Aptos) / `{ assetId, decimals }` (Algorand) — required for the single form |
765
- | `accept` | — | Multi-chain form: `[{ chain, token, amount, payTo?, rpcUrl? }, …]` — offer several chains in one challenge |
766
- | `payTo` | — | Wallet that receives the payment (per-option fallback in the multi form) |
767
- | `description` | — | Optional text shown to the agent in the challenge (what the payment is for) |
768
- | `rpcUrl` | chain default | Your own RPC (recommended in production) |
769
- | `minConfirmations` | `1` | Confirmations before access is granted |
770
- | `maxTimeoutSeconds` | `600` | Reject payments older than this (replay window) |
771
- | `onPaid` | — | `(receipt) => void` on a verified payment (see [Receipts](#receipts--record-every-payment)) |
772
- | `isUsed` / `markUsed` | in-memory | Replay store hooks — share across instances (e.g. Redis `SET NX`) |
773
- | `generateNonce` | `crypto.randomUUID()` | Custom per-challenge nonce generator |
774
- | `exact` | — | Opt in to **also** advertise a standard x402 `exact` rail (EIP-3009) so any standard client can pay: `{ settle: 'self', relayer: { privateKey } }` (your relayer broadcasts) or `{ settle: { facilitator } }` (delegate to a chosen facilitator). EVM + USDC/EURC only — see [Universal payments](#universal-payments--get-paid-by-any-x402-client) |
775
-
776
- Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `accept` array (multi) — not both.
777
-
778
- **`new PipRailClient({ wallet, chain, rpcUrl?, policy?, onBeforePay?, maxPaymentRetries?, retryTimeoutMs?, onEvent? })`**
779
-
780
- | Option | Default | Notes |
781
- |---|---|---|
782
- | `wallet` | — | Keys for the chosen family (see the wallet table below) |
783
- | `chain` | — | Which chain to pay on — same selector as the gate |
784
- | `rpcUrl` | chain default | Your own RPC (recommended in production) |
785
- | `policy` | — | Spend guardrails: `maxAmount`, `maxTotal` (per token), `chains`, `tokens`, `hosts`, `allowUnknownTokens`. Over-limit → `PaymentDeclinedError` before any send |
786
- | `onBeforePay` | — | `(quote) => boolean \| Promise<boolean>` — final approval per payment; `false`/throw declines |
787
- | `maxPaymentRetries` | `3` | Re-sends with proof after paying (absorbs RPC propagation lag) |
788
- | `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
789
- | `onEvent` | — | `(event) => void` observability: `payment-required` · `payment-broadcast` · `payment-confirmed` · `payment-unconfirmed` (broadcast OK, local confirm timed out → deferring to server) · `payment-settled` · `payment-failed` |
790
-
791
- Methods: `fetch` · `get` · `post` (return the gated `Response` after settlement) · **`quote(url)`** (price without paying → `PipRailQuote \| null`) · **`estimateCost(url)`** (price **+** native-coin gas estimate → `PipRailCostQuote \| null`) · **`planPayment(url)`** (affordability + recipient-readiness across the offered rails → `PaymentPlan \| null`) · **`canAfford(url)`** (→ `boolean`) · **`spent()`** (per-asset ledger snapshot) · **`discover(opts?)`** (find resources on the open indexes → `DiscoveredResource[]`) · **`register(url, opts?)`** (list a resource on the open indexes → `RegisterOutcome[]`) · **`discoverySigner()`** (the wallet's discovery signer, EVM today, or `null`). Pass `{ autoRoute: true }` to `fetch` (or set it on the client) to pay the cheapest *settleable* rail. Module-level **`planAcross(clients, url)`** plans across chains.
792
-
793
- **Discovery (opt-in, $0, nothing hosted):** `client.discover()` / `client.register()`, the standalone `searchOpenIndexes` / `register402Index` / `registerX402Scan`, and the pure emitters `buildOpenApi` / `buildWellKnownX402` / `buildX402DnsTxt` (fed by `gate.describe()`). See [Be discoverable](#be-discoverable--find-and-be-found-0-no-backend).
794
-
795
- **Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
796
-
797
- | Family | `wallet` shape |
45
+ | | |
798
46
  |---|---|
799
- | EVM | `{ privateKey }` (0x… hex) or a viem `{ walletClient }` |
800
- | Solana | `{ secretKey }` (Uint8Array or base58) or `{ signer }` |
801
- | TON | `{ mnemonic }` (24 words) or `{ keyPair }` (+ `version: 'v5r1'` for W5) |
802
- | Stellar | `{ secret }` (S… seed) or `{ keypair }` |
803
- | XRPL | `{ seed }` (s… seed) or `{ wallet }` |
804
- | Tron | `{ privateKey }` (32-byte hex — secp256k1) |
805
- | NEAR | `{ accountId, privateKey }` (privateKey = ed25519:… secret) |
806
- | Sui | `{ privateKey }` (suiprivkey1… bech32) or `{ keypair }` |
807
- | Aptos | `{ privateKey }` (ed25519-priv-0x… AIP-80) or `{ account }` |
808
- | Algorand | `{ mnemonic }` (25 words) or `{ account }` (algosdk `{ addr, sk }`) |
809
-
810
- **Hand an LLM a wallet:** `paymentTools(client)` → five framework-agnostic tool descriptors (`piprail_discover` · `piprail_quote_payment` · `piprail_plan_payment` · `piprail_pay_request` · `piprail_register`) for MCP / AI SDK / function-calling, budget enforced by the client.
811
-
812
- **Bring your own chain family:** the SDK is built on a tiny `PaymentDriver` contract — `resolve(chain)` returns a bound network with `resolveToken` / `describeAsset` / `assertValidPayTo` / `bindWallet` / `send` / `confirm` / `estimateCost` / `balanceOf` / `recipientReady` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
813
-
814
- **Universal x402 (`exact` scheme):** the supported way to pay any standard x402 server is `new PipRailClient({ …, schemes: ['onchain-proof', 'exact'] })` (see [Pay *any* x402 server](#pay-any-x402-server--the-exact-rail-buyer-side)) — the client signs the EIP-3009 authorization on-chain-derived-domain and the server/facilitator settles. The low-level codecs `parseExactRequirements` / `buildExactAuthorization` (`@deprecated` — trusts the server-supplied domain, local-key only) / `encodeXPaymentHeader` remain for hand-rolled or v1 clients. EVM + EIP-3009 only; validate against your target facilitator before production.
815
-
816
- ## Requirements
817
-
818
- - Node 20+ or a modern browser.
819
- - `viem ^2.21` (peer dep). Solana: `@solana/web3.js`, `@solana/spl-token`, `bs58` (optional peers). TON: `@ton/ton`, `@ton/core`, `@ton/crypto` (optional peers). Stellar: `@stellar/stellar-sdk` (optional peer). XRPL: `xrpl` (optional peer). Tron: `tronweb` (optional peer). NEAR: `near-api-js` (optional peer). Sui: `@mysten/sui` (optional peer). Aptos: `@aptos-labs/ts-sdk` (optional peer). Algorand: `algosdk` (optional peer).
47
+ | **[Getting started](https://docs.piprail.com/getting-started/introduction/)** | Install · quickstart · how it works |
48
+ | **[Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)** | `requirePayment` · `createPaymentGate` · the `exact` rail |
49
+ | **[Making payments](https://docs.piprail.com/making-payments/piprail-client/)** | `PipRailClient` · `quote` · `estimateCost` · `planPayment` · auto-route |
50
+ | **[Spend controls](https://docs.piprail.com/spend-controls/payment-policy/)** | Budgets · time envelope · the spend ledger |
51
+ | **[Agent toolkit](https://docs.piprail.com/agent-toolkit/payment-tools/)** | `paymentTools` · the agent guide · NL renderers |
52
+ | **[Discovery](https://docs.piprail.com/discovery/discover-and-register/)** | Find & be found on the open x402 indexes ($0, no backend) |
53
+ | **[Chains & tokens](https://docs.piprail.com/chains/overview/)** | Every chain, per-family setup & caveats |
54
+ | **[Errors](https://docs.piprail.com/errors/error-model/)** | The complete typed error model |
55
+ | **[MCP server](https://docs.piprail.com/mcp/overview/)** | Give any AI agent a budget-bound wallet |
56
+ | **[Reference](https://docs.piprail.com/reference/api/)** | The complete API surface |
820
57
 
821
58
  ## License & trademark
822
59