@piprail/sdk 1.0.0

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 ADDED
@@ -0,0 +1,497 @@
1
+ # @piprail/sdk
2
+
3
+ **Accept crypto payments from any HTTP request — on any EVM chain, Solana, TON, Tron, NEAR, Sui, Stellar, and the XRP Ledger — in a couple of lines.**
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.
6
+
7
+ ```bash
8
+ npm install @piprail/sdk viem
9
+ ```
10
+
11
+ ## Take payments — one line
12
+
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
+ ## Built for agents — spend safely
44
+
45
+ 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.
46
+
47
+ ```ts
48
+ const client = new PipRailClient({
49
+ wallet: { privateKey: process.env.AGENT_KEY },
50
+ chain: 'base',
51
+ policy: {
52
+ maxAmount: '0.10', // never pay more than $0.10 for one call
53
+ maxTotal: '5.00', // never spend more than $5 total (per token)
54
+ chains: ['base'], // only on Base
55
+ tokens: ['USDC'], // only in USDC
56
+ hosts: ['*.example.com'], // only these hosts
57
+ },
58
+ onBeforePay: (q) => Number(q.amountFormatted) <= 0.05, // final say on each payment
59
+ })
60
+
61
+ // 1) Learn the price WITHOUT paying — decide if it's worth it.
62
+ const q = await client.quote('https://api.example.com/report')
63
+ // → { amountFormatted: '0.05', symbol: 'USDC', chain: 'base', withinPolicy: true, … } | null
64
+
65
+ // 2) Know the GAS too — the native-coin fee to SEND it (you pay USDC, but burn ETH/SOL/TRX for gas).
66
+ const est = await client.estimateCost('https://api.example.com/report')
67
+ // → { quote: {…}, cost: { feeSymbol: 'ETH', feeFormatted: '0.000105', basis: 'estimated', … } } | null
68
+
69
+ // 3) Pay (auto). Over-budget / declined → throws PaymentDeclinedError; nothing moves.
70
+ const res = await client.fetch('https://api.example.com/report')
71
+
72
+ // 4) Account for it.
73
+ client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05', … }], records }
74
+ ```
75
+
76
+ **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.
77
+
78
+ **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.
79
+
80
+ ### Hand an LLM a budget-bound wallet
81
+
82
+ `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.
83
+
84
+ ```ts
85
+ import { paymentTools } from '@piprail/sdk'
86
+ const tools = paymentTools(client) // → [piprail_quote_payment, piprail_pay_request]
87
+ ```
88
+
89
+ See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
90
+
91
+ ### Accept several chains at once
92
+
93
+ `requirePayment` (and `createPaymentGate`) take an **`accept: [...]`** array — one challenge that's payable on **any** of several chains/tokens, across **all eight families** (EVM, Solana, TON, Tron, Stellar, XRPL, NEAR, Sui). The agent pays with whatever it holds:
94
+
95
+ ```ts
96
+ requirePayment({
97
+ accept: [
98
+ { chain: 'base', token: 'USDC', amount: '0.05', payTo: '0xYourEvmWallet…' },
99
+ { chain: 'tron', token: 'USDT', amount: '0.05', payTo: 'TYourTronWallet…' },
100
+ { chain: 'xrpl', token: 'USDC', amount: '0.05', payTo: 'rYourXrplWallet…' },
101
+ { chain: 'solana', token: 'USDC', amount: '0.05', payTo: 'YourSolWallet…' },
102
+ ],
103
+ })
104
+ ```
105
+
106
+ How the multi-chain case is handled, end-to-end:
107
+
108
+ - **Gate:** each option resolves through its own driver (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.
109
+ - **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.
110
+ - **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.
111
+
112
+ ## One word picks the chain
113
+
114
+ ```ts
115
+ requirePayment({ chain: 'base', token: 'USDC', amount: '0.05', payTo }) // USDC on Base
116
+ requirePayment({ chain: 'arbitrum', token: 'USDC', amount: '0.05', payTo }) // USDC on Arbitrum
117
+ requirePayment({ chain: 'bnb', token: 'USDT', amount: '1', payTo }) // USDT on BNB
118
+ requirePayment({ chain: 'solana', token: 'USDC', amount: '0.05', payTo }) // USDC on Solana
119
+ requirePayment({ chain: 'ton', token: 'USDT', amount: '1', payTo }) // USD₮ on TON
120
+ requirePayment({ chain: 'tron', token: 'USDT', amount: '1', payTo }) // USD₮ on Tron
121
+ requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.05', payTo }) // USDC on the XRP Ledger
122
+ requirePayment({ chain: 'near', token: 'USDC', amount: '0.05', payTo }) // USDC on NEAR
123
+ requirePayment({ chain: 'sui', token: 'USDC', amount: '0.05', payTo }) // USDC on Sui
124
+
125
+ // Prefer the chain's native coin? Same one-liner — token: 'native'.
126
+ requirePayment({ chain: 'ethereum', token: 'native', amount: '0.001', payTo }) // ETH
127
+ requirePayment({ chain: 'base', token: 'native', amount: '0.001', payTo }) // ETH on Base
128
+ requirePayment({ chain: 'bnb', token: 'native', amount: '0.01', payTo }) // BNB
129
+ requirePayment({ chain: 'solana', token: 'native', amount: '0.1', payTo }) // SOL
130
+ requirePayment({ chain: 'ton', token: 'native', amount: '1', payTo }) // TON
131
+ requirePayment({ chain: 'xrpl', token: 'native', amount: '1', payTo }) // XRP
132
+ ```
133
+
134
+ **Native or stablecoin — your choice, on most chains.** Every gate accepts the chain's native coin (ETH, BNB, POL, AVAX, SOL, TON, XLM, XRP, SUI, …) 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, 7 on Stellar, 6 on XRPL). Verification, replay protection, and self-custody are identical to the stablecoin path. (**Two exceptions — token-only chains:** **Tron** is TRC-20-only and **NEAR** is NEP-141-only; both ship USDC/USDT but their native coin isn't a payment asset — a Tron/NEAR token transfer is what binds + verifies.)
135
+
136
+ `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.
137
+
138
+ ### Built-in chains (mainnet)
139
+
140
+ Every token address below was verified on-chain (symbol + decimals) before shipping.
141
+
142
+ | `chain` | Network | Tokens |
143
+ |---|---|---|
144
+ | `'ethereum'` | Ethereum | USDC, USDT |
145
+ | `'base'` | Base | USDC |
146
+ | `'arbitrum'` | Arbitrum | USDC, USDT |
147
+ | `'optimism'` | Optimism | USDC, USDT |
148
+ | `'polygon'` | Polygon | USDC, USDT |
149
+ | `'bnb'` | BNB Chain | USDC, USDT |
150
+ | `'avalanche'` | Avalanche | USDC, USDT |
151
+ | `'mantle'` | Mantle | USDC, USDT |
152
+ | `'sonic'` | Sonic | USDC, USDT |
153
+ | `'linea'` | Linea | USDC, USDT |
154
+ | `'scroll'` | Scroll | USDC, USDT |
155
+ | `'celo'` | Celo | USDC, USDT |
156
+ | `'zksync'` | zkSync Era | USDC, USDT |
157
+ | `'unichain'` | Unichain | USDC, USDT |
158
+ | `'worldchain'` | World Chain | USDC |
159
+ | `'sei'` | Sei | USDC |
160
+ | `'injective'` | Injective | USDC, USDT |
161
+ | `'solana'` | Solana | USDC, USDT |
162
+ | `'ton'` | TON | USDT |
163
+ | `'tron'` | Tron | USDT |
164
+ | `'near'` | NEAR | USDC, USDT |
165
+ | `'sui'` | Sui | USDC |
166
+ | `'stellar'` | Stellar | USDC, EURC |
167
+ | `'xrpl'` | XRP Ledger | USDC, RLUSD |
168
+
169
+ **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).
170
+
171
+ **Tron note:** native **USDC doesn't exist on Tron either** (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. Tron is **TRC-20 only**: native TRX isn't a payment asset (pass USDT or a custom TRC-20).
172
+
173
+ **NEAR note:** ships **both native USDC + USDT** (Circle's native USDC `17208628…`, NOT the bridged `…factory.bridge.near`; Tether's native `usdt.tether-token.near`). NEAR is **NEP-141 only** — native NEAR isn't a payment asset (its transfer carries no memo to bind). A recipient must be **`storage_deposit`-registered** on the token once before it can receive (see the NEAR section).
174
+
175
+ **Sui note:** **USDC only** — no native USDT on Sui (Wormhole-bridged only). Native SUI works with `token: 'native'`.
176
+
177
+ 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.
178
+
179
+ ### Any other chain or token — no allowlist
180
+
181
+ 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:
182
+
183
+ ```ts
184
+ requirePayment({
185
+ chain: { id: 1313161554, rpcUrl: 'https://mainnet.aurora.dev' }, // any EVM chain
186
+ token: { address: '0x…', decimals: 6, symbol: 'USDC' }, // any ERC-20
187
+ amount: '0.05',
188
+ payTo,
189
+ })
190
+
191
+ // On Solana, a custom SPL token is { mint, decimals }:
192
+ requirePayment({ chain: 'solana', token: { mint: '…', decimals: 6 }, amount: '0.05', payTo })
193
+
194
+ // On TON, a custom jetton is { master, decimals }:
195
+ requirePayment({ chain: 'ton', token: { master: 'EQ…', decimals: 6 }, amount: '0.05', payTo })
196
+
197
+ // On Stellar, a custom classic asset is { issuer, code, decimals }:
198
+ requirePayment({ chain: 'stellar', token: { issuer: 'G…', code: 'XYZ', decimals: 7 }, amount: '0.05', payTo })
199
+
200
+ // On the XRP Ledger, a custom issued currency is { issuer, currencyHex, decimals }:
201
+ requirePayment({ chain: 'xrpl', token: { issuer: 'r…', currencyHex: '5553444300000000000000000000000000000000', decimals: 6 }, amount: '0.05', payTo })
202
+
203
+ // On Tron, a custom TRC-20 is { address, decimals } (Base58 T… contract):
204
+ requirePayment({ chain: 'tron', token: { address: 'T…', decimals: 6 }, amount: '0.05', payTo })
205
+
206
+ // On NEAR, a custom NEP-141 is { contractId, decimals }:
207
+ requirePayment({ chain: 'near', token: { contractId: 'token.near', decimals: 6 }, amount: '0.05', payTo })
208
+
209
+ // On Sui, a custom coin is { coinType, decimals }:
210
+ requirePayment({ chain: 'sui', token: { coinType: '0x…::usdc::USDC', decimals: 6 }, amount: '0.05', payTo })
211
+ ```
212
+
213
+ > **Production:** the built-in chains use public RPCs (rate-limited). Pass your own `rpcUrl` for real traffic.
214
+
215
+ ## Solana
216
+
217
+ 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:
218
+
219
+ ```bash
220
+ npm install @solana/web3.js @solana/spl-token bs58
221
+ ```
222
+
223
+ ```ts
224
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
225
+
226
+ // No setup call — naming the chain mounts the driver.
227
+ requirePayment({ chain: 'solana', token: 'USDC', amount: '0.05', payTo: 'YourBase58Wallet…' })
228
+ new PipRailClient({ wallet: { secretKey: SOLANA_SECRET }, chain: 'solana' })
229
+ ```
230
+
231
+ 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.
232
+
233
+ ## TON
234
+
235
+ 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:
236
+
237
+ ```bash
238
+ npm install @ton/ton @ton/core @ton/crypto
239
+ ```
240
+
241
+ ```ts
242
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
243
+
244
+ requirePayment({ chain: 'ton', token: 'USDT', amount: '1', payTo: 'EQ…or UQ…' })
245
+ new PipRailClient({ wallet: { mnemonic: process.env.TON_MNEMONIC }, chain: 'ton' })
246
+ ```
247
+
248
+ 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₮.
249
+
250
+ ## Tron
251
+
252
+ 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:
253
+
254
+ ```bash
255
+ npm install tronweb
256
+ ```
257
+
258
+ ```ts
259
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
260
+
261
+ requirePayment({ chain: 'tron', token: 'USDT', amount: '1', payTo: 'T…' })
262
+ new PipRailClient({ wallet: { privateKey: process.env.TRON_KEY }, chain: 'tron' })
263
+ ```
264
+
265
+ 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; Tron is TRC-20 only** — native USDC doesn't exist there, and native TRX isn't a payment asset (pass USDT or a custom `{ address, decimals }`). 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.
266
+
267
+ ## Stellar
268
+
269
+ 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:
270
+
271
+ ```bash
272
+ npm install @stellar/stellar-sdk
273
+ ```
274
+
275
+ ```ts
276
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
277
+
278
+ requirePayment({ chain: 'stellar', token: 'USDC', amount: '0.05', payTo: 'G…' })
279
+ new PipRailClient({ wallet: { secret: process.env.STELLAR_SECRET }, chain: 'stellar' })
280
+ ```
281
+
282
+ 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.
283
+
284
+ ## XRP Ledger
285
+
286
+ 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:
287
+
288
+ ```bash
289
+ npm install xrpl
290
+ ```
291
+
292
+ ```ts
293
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
294
+
295
+ requirePayment({ chain: 'xrpl', token: 'USDC', amount: '0.05', payTo: 'r…' })
296
+ new PipRailClient({ wallet: { seed: process.env.XRPL_SEED }, chain: 'xrpl' })
297
+ ```
298
+
299
+ 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.
300
+
301
+ ## NEAR
302
+
303
+ 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:
304
+
305
+ ```bash
306
+ npm install near-api-js
307
+ ```
308
+
309
+ ```ts
310
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
311
+
312
+ requirePayment({ chain: 'near', token: 'USDC', amount: '0.05', payTo: 'merchant.near' })
313
+ new PipRailClient({ wallet: { accountId: 'agent.near', privateKey: process.env.NEAR_KEY }, chain: 'near' })
314
+ ```
315
+
316
+ NEAR wallets are `{ accountId, privateKey }` (privateKey = an `ed25519:…` secret); `payTo` is a NEAR account id (`name.near` or a 64-hex implicit account). **Both USDC + USDT are native and built in** (Circle's `17208628…`, Tether's `usdt.tether-token.near`); NEAR is **NEP-141 only** — native NEAR isn't a payment asset. The challenge nonce rides in the NEP-141 `ft_transfer` **`memo`** (Template A binding) and is verified by tx hash (NEAR has no account-history RPC): the proof is `<accountId>:<txHash>`, and verify only trusts an `ft_transfer` event emitted by the real token contract (provenance). **`storage_deposit` (real):** a recipient must be NEP-145-registered on the token once (~0.00125 NEAR) before it can receive, or `ft_transfer` panics — register `payTo` out of band. The payer needs a little **NEAR for gas** + the mandatory 1 yoctoNEAR per transfer. (Never route through NEAR Intents/solvers — that re-adds a facilitator; a plain `ft_transfer` is what we do.)
317
+
318
+ ## Sui
319
+
320
+ 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:
321
+
322
+ ```bash
323
+ npm install @mysten/sui
324
+ ```
325
+
326
+ ```ts
327
+ import { requirePayment, PipRailClient } from '@piprail/sdk'
328
+
329
+ requirePayment({ chain: 'sui', token: 'USDC', amount: '0.05', payTo: '0x…' })
330
+ new PipRailClient({ wallet: { privateKey: process.env.SUI_KEY }, chain: 'sui' })
331
+ ```
332
+
333
+ 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".
334
+
335
+ ## How it works
336
+
337
+ ```
338
+ Agent Your server
339
+ │ GET /report │
340
+ │ ───────────────────────────────────────►│ requirePayment
341
+ │ ◄──────────── 402 + payment-required ────│ (issues a challenge)
342
+ │ │
343
+ │ pay on-chain (one transfer to payTo) │
344
+ │ ───────────────────► [the chain] │
345
+ │ ◄── proof (tx hash / signature) ───── │
346
+ │ │
347
+ │ GET /report + payment-signature │
348
+ │ ───────────────────────────────────────►│ verifies the tx against
349
+ │ ◄──────────── 200 + your content ────────│ its own RPC, then next()
350
+ ```
351
+
352
+ 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.
353
+
354
+ ## Receipts — record every payment
355
+
356
+ 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.
357
+
358
+ ```ts
359
+ // (1) The onPaid hook — fires on every settled payment.
360
+ requirePayment({
361
+ chain: 'base', token: 'USDC', amount: '0.05', payTo,
362
+ onPaid: (receipt) => db.payments.insert(receipt),
363
+ })
364
+
365
+ // (2) Or read it off the framework-agnostic gate result.
366
+ const r = await gate.verify(headerValue)
367
+ if (r.kind === 'paid') await db.payments.insert(r.receipt)
368
+ ```
369
+
370
+ The receipt:
371
+
372
+ | Field | Example | Meaning |
373
+ |---|---|---|
374
+ | `transaction` | `0x9af…` · Solana signature · Sui digest | the on-chain transaction id |
375
+ | `payer` | `0x2b…` / `alice.near` | who paid |
376
+ | `payTo` | your wallet | who received |
377
+ | `asset` | USDC contract / coinType | token paid |
378
+ | `amount` | `50000` | amount, in base units |
379
+ | `network` | `eip155:8453` | which chain (CAIP-2) |
380
+ | `verifiedAt` | ISO timestamp | when the gate verified it |
381
+ | `scheme` | `'onchain-proof'` | settlement scheme (x402 v2) |
382
+ | `success` | `true` | settlement succeeded (always `true` — failures return a 402, never a receipt) |
383
+
384
+ 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.
385
+
386
+ ## Security model
387
+
388
+ What local verification guarantees, and what to know:
389
+
390
+ - **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.
391
+ - **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`).
392
+ - **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.
393
+ - **Confirmations.** `minConfirmations` (default 1) gates access; raise it for higher-value payments on chains with cheaper reorgs.
394
+
395
+ ## Any framework
396
+
397
+ `requirePayment` is Express/Connect middleware. For Hono, Fastify, Workers, Next.js, Bun, Deno — anything with `fetch` — build a gate and switch on the result:
398
+
399
+ ```ts
400
+ import { createPaymentGate, toInvalidBody } from '@piprail/sdk'
401
+
402
+ const gate = createPaymentGate({ chain: 'base', token: 'USDC', amount: '0.05', payTo })
403
+
404
+ export async function handler(req: Request): Promise<Response> {
405
+ const r = await gate.verify(req.headers.get('payment-signature') ?? undefined)
406
+ if (r.kind === 'paid') return Response.json(data, { headers: { 'payment-response': r.receiptHeader } })
407
+ if (r.kind === 'challenge') return Response.json(r.challenge, { status: 402, headers: { 'payment-required': r.requiredHeader } })
408
+ return Response.json(toInvalidBody(r), { status: 402 }) // canonical 402 body on every adapter
409
+ }
410
+ ```
411
+
412
+ 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`).
413
+
414
+ ## Architecture (under the hood)
415
+
416
+ Two layers, one contract. Worth knowing if you're extending the SDK or auditing it.
417
+
418
+ - **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.
419
+ - **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
420
+ - **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`/…). Eight today: `evm`, `solana`, `ton`, `stellar`, `xrpl`, `tron`, `near`, `sui`. Adding one = copy the five files, implement the contract, `registerDriver` — the protocol layer never changes.
421
+ - **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`. A build-time invariant asserts the main bundle has **zero** static imports of those libs — only per-family lazy chunks.
422
+ - **Two verification templates.** *Template A (memo-bound)* — Stellar, XRPL, TON, NEAR — carries the challenge nonce inside the transfer (memo / tag / comment), so the proof is cryptographically bound to its challenge. *Template B (digest-bound)* — EVM, Solana, Tron, Sui — binds via a single-use proof set + recipient + amount + a tight recency window (use a persistent `isUsed`/`markUsed` store in production).
423
+ - **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`).
424
+ - **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).
425
+
426
+ ## Errors
427
+
428
+ Every failure is **typed and understandable** — never a raw chain-library blob. Two channels:
429
+
430
+ - **Thrown** — a `PipRailError` subclass with a stable `.code` (`INSUFFICIENT_FUNDS`, `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.
431
+ - **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`).
432
+
433
+ The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
434
+
435
+ ## API
436
+
437
+ **`requirePayment(options)`** → Express middleware &nbsp;·&nbsp; **`createPaymentGate(options)`** → `{ challenge, verify }`
438
+
439
+ | Option | Default | Notes |
440
+ |---|---|---|
441
+ | `chain` | — | `'base'` / `'bnb'` / `'solana'` / `'ton'` / …, a viem `Chain`, or `{ id, rpcUrl }` (single-chain form) |
442
+ | `amount` | — | Human-readable, e.g. `'0.05'` (single-chain form) |
443
+ | `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) — required for the single form |
444
+ | `accept` | — | Multi-chain form: `[{ chain, token, amount, payTo?, rpcUrl? }, …]` — offer several chains in one challenge |
445
+ | `payTo` | — | Wallet that receives the payment (per-option fallback in the multi form) |
446
+ | `description` | — | Optional text shown to the agent in the challenge (what the payment is for) |
447
+ | `rpcUrl` | chain default | Your own RPC (recommended in production) |
448
+ | `minConfirmations` | `1` | Confirmations before access is granted |
449
+ | `maxTimeoutSeconds` | `600` | Reject payments older than this (replay window) |
450
+ | `onPaid` | — | `(receipt) => void` on a verified payment (see [Receipts](#receipts--record-every-payment)) |
451
+ | `isUsed` / `markUsed` | in-memory | Replay store hooks — share across instances (e.g. Redis `SET NX`) |
452
+ | `generateNonce` | `crypto.randomUUID()` | Custom per-challenge nonce generator |
453
+
454
+ Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `accept` array (multi) — not both.
455
+
456
+ **`new PipRailClient({ wallet, chain, rpcUrl?, policy?, onBeforePay?, maxPaymentRetries?, retryTimeoutMs?, onEvent? })`**
457
+
458
+ | Option | Default | Notes |
459
+ |---|---|---|
460
+ | `wallet` | — | Keys for the chosen family (see the wallet table below) |
461
+ | `chain` | — | Which chain to pay on — same selector as the gate |
462
+ | `rpcUrl` | chain default | Your own RPC (recommended in production) |
463
+ | `policy` | — | Spend guardrails: `maxAmount`, `maxTotal` (per token), `chains`, `tokens`, `hosts`, `allowUnknownTokens`. Over-limit → `PaymentDeclinedError` before any send |
464
+ | `onBeforePay` | — | `(quote) => boolean \| Promise<boolean>` — final approval per payment; `false`/throw declines |
465
+ | `maxPaymentRetries` | `3` | Re-sends with proof after paying (absorbs RPC propagation lag) |
466
+ | `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
467
+ | `onEvent` | — | `(event) => void` observability: `payment-required` · `payment-broadcast` · `payment-confirmed` · `payment-settled` · `payment-failed` |
468
+
469
+ 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`) · **`spent()`** (per-asset ledger snapshot).
470
+
471
+ **Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
472
+
473
+ | Family | `wallet` shape |
474
+ |---|---|
475
+ | EVM | `{ privateKey }` (0x… hex) or a viem `{ walletClient }` |
476
+ | Solana | `{ secretKey }` (Uint8Array or base58) or `{ signer }` |
477
+ | TON | `{ mnemonic }` (24 words) or `{ keyPair }` (+ `version: 'v5r1'` for W5) |
478
+ | Stellar | `{ secret }` (S… seed) or `{ keypair }` |
479
+ | XRPL | `{ seed }` (s… seed) or `{ wallet }` |
480
+ | Tron | `{ privateKey }` (32-byte hex — secp256k1) |
481
+ | NEAR | `{ accountId, privateKey }` (privateKey = ed25519:… secret) |
482
+ | Sui | `{ privateKey }` (suiprivkey1… bech32) or `{ keypair }` |
483
+
484
+ **Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling), budget enforced by the client.
485
+
486
+ **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` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
487
+
488
+ **Universal x402 (experimental):** building blocks to pay servers on the mainstream x402 `exact` scheme (EIP-3009 + facilitator) — `parseExactRequirements`, `buildExactAuthorization`, `encodeXPaymentHeader`. EVM-only; validate against your target facilitator before production.
489
+
490
+ ## Requirements
491
+
492
+ - Node 20+ or a modern browser.
493
+ - `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).
494
+
495
+ ## License
496
+
497
+ MIT — pure open source.
package/STANDARDS.md ADDED
@@ -0,0 +1,123 @@
1
+ # PipRail SDK — the build standard
2
+
3
+ How we build *anything* in `@piprail/sdk`. This is the repeatable procedure so every
4
+ feature lands the same way and the SDK stays the simplest, clearest agent-payments SDK on
5
+ the market. Companion docs: **[ERRORS.md](./ERRORS.md)** (the error standard) and the
6
+ **`add-chain-integration`** skill (adding a chain/token/family). When those apply, they win
7
+ for their topic; this doc covers everything else.
8
+
9
+ ---
10
+
11
+ ## 0. The prime directive — simplicity is the product
12
+
13
+ Every change must make the SDK *easier*, never heavier. Before adding anything, ask: does the
14
+ zero-config path still read in one line? If a feature can't be opt-in, reconsider it.
15
+
16
+ - **Opt-in, defaults unchanged.** New capability is a new optional field/method. Omitting it
17
+ leaves behaviour byte-identical. (`policy`, `onBeforePay`, `accept[]`, `quote()` all obey this.)
18
+ - **No backend, no database, no auth, no dashboard, no fee, no hosted facilitator.** Ever. If a
19
+ feature needs one of those, it's the wrong feature for this SDK.
20
+ - **One obvious way.** Prefer one clear API over flags. `token` is required so a gate is never
21
+ ambiguous; `chain` is one word. Don't add a second way to do the same thing.
22
+
23
+ ---
24
+
25
+ ## 1. The layering (never violate)
26
+
27
+ ```
28
+ protocol layer index · server · client · x402 · policy · ledger · agent · errors · util/*
29
+ (chain-agnostic — ZERO viem / @solana / @ton / @stellar imports)
30
+ │ depends only on …
31
+
32
+ driver contract drivers/types.ts (PaymentDriver / ResolvedNetwork)
33
+ ▲ implemented by …
34
+
35
+ chain drivers drivers/<family>/ chains · wallet · pay · verify · index (family-symmetric)
36
+ registry.ts (routes a chain → family) index.ts (eager EVM + lazy auto-mount of the rest)
37
+ ```
38
+
39
+ - **The protocol layer is chain-agnostic.** `server`/`client`/`x402`/`policy`/`ledger`/`agent`
40
+ import only `drivers/types.ts` + pure utils — never a chain library. Verified by the
41
+ lazy-chunk invariant (below).
42
+ - **Drivers mirror each other** file-for-file (`chains`/`wallet`/`pay`/`verify`/`index`),
43
+ functions family-suffixed (`payEvm`/`verifyStellar`). A new contract method is implemented in
44
+ **all** families.
45
+ - **Pure logic is a pure module.** Anything decidable without I/O (amount math, policy, ledger
46
+ aggregation) lives in its own dependency-free, unit-testable file. `policy.ts`/`ledger.ts`
47
+ import no driver; `util/units.ts` imports nothing.
48
+ - **Lazy-chunk invariant.** The built EVM entry must contain **zero static** `@solana`/`@ton`/
49
+ `@stellar` imports (they load on first use). New optional-peer code goes under `drivers/<family>/`
50
+ and is reached only via the dynamic loader in `drivers/index.ts`.
51
+
52
+ ---
53
+
54
+ ## 2. Errors — one standard
55
+
56
+ Follow **[ERRORS.md](./ERRORS.md)** exactly. Two channels only: a **thrown** `PipRailError`
57
+ subclass with a stable `SCREAMING_SNAKE` `.code` (config/flow/wallet/registry/affordability), or
58
+ a **returned** `VerifyResult` with a closed `VerifyErrorCode` (proof verification). A new thrown
59
+ error gets a row in ERRORS.md §2 and is exported from the root. Never leak a raw chain-library
60
+ error for a condition the SDK recognises. Observability hooks (`onEvent`, `onPaid`,
61
+ `onBeforePay`) never abort the flow on a throw — isolate them.
62
+
63
+ ---
64
+
65
+ ## 3. Adding a feature — the procedure
66
+
67
+ 1. **Write the plan first** under `.claude/plans/<feature>/` (one README + numbered phases,
68
+ referencing the exact files/lines). Tests-as-contract: the test changes *before* the behaviour.
69
+ 2. **Put it in the right layer** (§1). Pure logic → its own module. Chain-specific → the driver
70
+ (add to the contract + all families if it's cross-family).
71
+ 3. **Make it opt-in** (§0). Add an optional option/method; default leaves today's behaviour.
72
+ 4. **Type it precisely**, export the public types from `index.ts`, and keep internals private.
73
+ 5. **Document everywhere** (§5).
74
+ 6. **Test every spectrum** (§4) and pass the gate (§6).
75
+
76
+ ## 4. The test contract
77
+
78
+ `test/` (Vitest) **is** the spec. For every feature:
79
+
80
+ - **Unit (pure):** truth tables for pure modules (`policy`, `ledger`, `units`), deterministic
81
+ vectors for crypto (`exact` via signature recovery). No I/O.
82
+ - **Flow (fake driver + stubbed `fetch`):** register a fake `ResolvedNetwork`; stub `globalThis.fetch`.
83
+ Assert the happy path **and** that refusals happen **before** side effects (e.g. a `send` spy
84
+ stays at 0 when policy declines).
85
+ - **Adversarial — try to break it:** a hostile/buggy server (lies about decimals/symbol, forged
86
+ `accepted`, malformed 402), boundary inputs (excess precision, ports in hosts, zero/huge amounts),
87
+ concurrency (parallel payments), and replay. Whatever breaks, fix — then keep the test.
88
+ - **Symmetry:** a cross-family test that exercises the same behaviour on every driver
89
+ (e.g. `describe-asset.test.ts`).
90
+
91
+ ## 5. Documentation (a feature isn't done until all are updated)
92
+
93
+ - **`README.md`** — a section + the API table.
94
+ - **`site/src/pages/index.astro`** — a landing block in the existing visual language, if it's
95
+ user-facing.
96
+ - **`ERRORS.md`** — any new error code.
97
+ - **`CHANGELOG.md`** — an `Unreleased` entry.
98
+ - **`examples/`** — a runnable example if it changes how an agent/merchant integrates.
99
+
100
+ ## 6. The verification gate (must be green before "done")
101
+
102
+ ```bash
103
+ npm run typecheck # src type-checks
104
+ npm run typecheck:test # src + tests type-check together (tests are excluded from the build)
105
+ npm test # full Vitest suite
106
+ npm run build # tsup build succeeds
107
+ # lazy-chunk invariant — the EVM bundle pulls in no non-EVM chain lib:
108
+ grep -E "from ?['\"]@(solana|ton|stellar)" dist/index.js # → expect NO matches
109
+ ```
110
+
111
+ `prepublishOnly` runs build + test + both typechecks. Never ship with any of these red.
112
+
113
+ ---
114
+
115
+ ## 7. Known, intentional limitations (document; don't silently fix with complexity)
116
+
117
+ - **`policy.maxTotal` under high concurrency is best-effort.** It's checked against spend recorded
118
+ *so far*; many payments in flight at once could race past it. Agents that need a hard concurrent
119
+ cap should serialise (the common case is sequential `await`ed calls). We don't add a reservation
120
+ system — it would cost more simplicity than it's worth. State limits like this; never hide them.
121
+ - **`policy.chains` string entries match the configured selector form.** A `'base'` entry matches a
122
+ client configured with `'base'`; an `{ id }` entry matches by resolved network. Use the same form
123
+ you configured the client with (the pure policy layer can't resolve a name → id without the EVM table).