@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/CHANGELOG.md ADDED
@@ -0,0 +1,160 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@piprail/sdk` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ versions follow [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [1.0.0] — 2026-06-02
8
+
9
+ The multi-chain rewrite and first stable release. **24 chains across 8 families**
10
+ (17 EVM + Solana, TON, Tron, NEAR, Sui, Stellar, XRPL), plus agent spend controls,
11
+ a gas/cost estimator, and an agent toolkit — one parameter still picks everything.
12
+ Everything below is **opt-in**; the zero-config client and gate are unchanged.
13
+
14
+ > The earlier 0.1.x–0.2.0 preview line (single-chain) has been withdrawn from npm;
15
+ > `npm install @piprail/sdk` now resolves to 1.0.0.
16
+
17
+ ### Agent spend controls (client)
18
+ - **`policy`** on `PipRailClient` — `maxAmount` (per call) + `maxTotal` (lifetime,
19
+ per token) ceilings and `chains` / `tokens` / `hosts` allowlists. A 402 outside
20
+ the policy is refused with the new **`PaymentDeclinedError`** (`PAYMENT_DECLINED`)
21
+ **before any on-chain send**. Caps are enforced against the token's **true**
22
+ decimals (via the new driver `describeAsset`), so a server can't understate a price.
23
+ - **`client.quote(url)`** — learn the price of a gated URL **without paying** (returns
24
+ a `PipRailQuote`, or `null` when the URL isn't gated). Flags a `symbolMismatch` when
25
+ a challenge's stated symbol disagrees with the real token.
26
+ - **`onBeforePay(quote)`** — a final approval hook per payment; returning `false`
27
+ (or throwing) declines without paying.
28
+ - **`client.spent()`** — an in-memory ledger snapshot, aggregated per token.
29
+
30
+ ### Multi-chain accepts (gate)
31
+ - `requirePayment` / `createPaymentGate` accept an **`accept: [{ chain, token, amount,
32
+ payTo? }, …]`** array — one challenge offers several chains, and the agent pays with
33
+ whatever it holds. `verify()` re-derives every checked field from the server's own
34
+ requirement for the claimed network (a forged echo can't redirect it). The legacy
35
+ single-chain form is unchanged.
36
+
37
+ ### Agent toolkit
38
+ - **`paymentTools(client)`** — framework-agnostic tool descriptors (name + description +
39
+ JSON Schema + `invoke`) for MCP, the Vercel AI SDK, OpenAI/Anthropic function-calling,
40
+ or LangChain. The client's budget rides along, so the model can't overspend.
41
+
42
+ ### x402 `exact`-scheme interop (experimental, EVM)
43
+ - Building blocks to pay servers on the mainstream x402 `exact` scheme (EIP-3009 +
44
+ facilitator): `parseExactRequirements`, `buildExactAuthorization`,
45
+ `encodeXPaymentHeader`, `chainIdForExactNetwork`. Not wired into the default client
46
+ flow — hand-roll with these and validate against your target facilitator.
47
+
48
+ ### Gas / cost estimator
49
+ - **`client.estimateCost(url)`** — learn the **network fee (gas)** to pay a gated URL,
50
+ WITHOUT paying. Returns a `PipRailCostQuote` (`{ quote, cost }`): the payment quote
51
+ plus a `CostEstimate` — the fee in the chain's **native coin** (you pay USDC but burn
52
+ ETH/SOL/TON/XLM/XRP/TRX on gas, a separate balance). Best-effort + labelled (`cost.basis`):
53
+ live-RPC where cheap (`'estimated'`), a typical-cost constant otherwise (`'heuristic'`);
54
+ never throws. So an agent budgets the *total* — payment + gas — before any funds move.
55
+ Most valuable on Tron, where a USD₮ transfer costs real TRX.
56
+ - New driver-contract method **`estimateCost(accept, opts?)`** (required), implemented across
57
+ all eight families. The per-chain fee math (EVM gas × price, Solana lamports, Tron energy ×
58
+ price via `triggerConstantContract`, XRPL drops, …) is extracted in each driver and shaped
59
+ uniformly by one shared `nativeCost()` helper (`util/cost.ts`). `opts.from` sharpens
60
+ sender-dependent fees (Tron energy).
61
+ - `WalletInput` now includes XRPL's `{ seed }` / `{ wallet }` and documents Tron's
62
+ `{ privateKey }`, so every built-in family is type-correct on `PipRailClient`.
63
+
64
+ ### Driver contract
65
+ - Added **`describeAsset(asset)`** to `ResolvedNetwork` (trusted decimals/symbol for a
66
+ known asset, or `null`), implemented across EVM/Solana/TON/Stellar/XRPL/Tron/NEAR/Sui.
67
+
68
+ ### Chains
69
+ - Now **24 chains built in** (17 EVM + Solana + TON + Tron + NEAR + Sui + Stellar + XRPL).
70
+ Beyond 0.1.0's set, this cycle added the **Sei** + **Injective** EVM presets, **Stellar**,
71
+ **Tron**, the **XRP Ledger**, and now **NEAR** and **Sui**. One parameter still picks
72
+ everything; the non-EVM families auto-mount on first use (pure-EVM installs never
73
+ download their libs).
74
+ - **NEAR** (`chain: 'near'`, optional peer `near-api-js`) — the "user-owned AI" chain, with
75
+ **both native USDC + USDT** (`ft_metadata`-verified; Circle's `17208628…` and Tether's
76
+ `usdt.tether-token.near`, NOT bridged). Template A binding (nonce in the NEP-141
77
+ `ft_transfer` memo) **verified by tx hash** — proof ref `<accountId>:<txHash>`, and only an
78
+ ft_transfer event from the trusted token contract counts (provenance). **NEP-141 only**
79
+ (native NEAR isn't a payment asset); recipients need a one-time NEP-145 `storage_deposit`.
80
+ Wallets are `{ accountId, privateKey }`; custom NEP-141 via `{ contractId, decimals }`.
81
+ - **Sui** (`chain: 'sui'`, optional peer `@mysten/sui` v2 — `SuiJsonRpcClient`) — Move L1, sub-second finality, native
82
+ Circle **USDC** (`suix_getCoinMetadata`-verified; no native USDT on Sui). Template B
83
+ (digest-bound): the proof is the tx digest, verified via balance changes + single-use.
84
+ Ships the standard self-gas `Coin<USDC>` transfer; Sui's protocol-level **gasless** stablecoin
85
+ path (no sponsor/relayer) is a documented future enhancement, not claimed on this path.
86
+ Wallets are `{ privateKey }` (suiprivkey1…) or `{ keypair }`; custom coins via `{ coinType, decimals }`.
87
+ - **Tron** (`chain: 'tron'`, optional peer `tronweb`) — the largest USDT rail (~45% of
88
+ all USDT). Ships **USD₮ (TRC-20) only** — native USDC doesn't exist on Tron, and it's
89
+ **TRC-20 only** (native TRX isn't a payment asset). Digest-bound (Template B): the
90
+ proof is the txid, verified on the **solidity/confirmed node** and single-use. Wallets
91
+ are `{ privateKey }`; custom TRC-20 via `{ address, decimals }`.
92
+ - **XRP Ledger** (`chain: 'xrpl'`, optional peer `xrpl`) — native **USDC + RLUSD**, plus
93
+ native XRP. Memo-bound (Template A): the nonce rides in a Memo (binding) + a derived
94
+ DestinationTag (deliverability). Verification compares **`delivered_amount`**, never
95
+ `Amount`, to defeat `tfPartialPayment`; receiving an IOU needs a one-time trustline.
96
+ Wallets are `{ seed }`; custom IOUs via `{ issuer, currencyHex, decimals }`.
97
+ - Every token address verified on-chain before shipping (XRPL issuer Domains →
98
+ circle.com / ripple.com, codes via `gateway_balances`; Tron USD₮ decimals 6 / symbol
99
+ USDT via TronGrid).
100
+
101
+ ## [0.1.0] — 2026-06-01
102
+
103
+ Initial release of the standalone PipRail SDK. One job: accept x402
104
+ "402 Payment Required" payments on any EVM chain **and Solana**, with no
105
+ hosted service, no account, no database, and no fee — payments settle
106
+ straight into your wallet. The API is small and self-contained.
107
+
108
+ ### Accept payments
109
+ - `requirePayment(options)` — Express/Connect middleware that gates a route.
110
+ Issues the `402` challenge, then verifies the payment on-chain and calls
111
+ `next()`.
112
+ - `createPaymentGate(options)` — framework-agnostic core (`challenge` +
113
+ `verify`) for Hono, Fastify, Workers, Next.js, Bun, Deno, Adonis, etc.
114
+ - Payments are verified **locally against the chain's RPC** — that the tx
115
+ succeeded, has enough confirmations, moved at least the required amount of
116
+ the right token to `payTo`, and was mined recently. No third party.
117
+ - In-memory replay protection (a used-tx set + a recency window), overridable
118
+ via `isUsed` / `markUsed` for multi-instance deploys.
119
+
120
+ ### Make payments
121
+ - `PipRailClient` — wraps `fetch`; on a `402` it pays on-chain, waits for
122
+ confirmation, and retries with proof. `fetch` / `get` / `post` methods and
123
+ `onEvent` observability. EVM wallets are `{ privateKey }` or a viem
124
+ `{ walletClient }`; Solana wallets are `{ secretKey }` or `{ signer }`.
125
+
126
+ ### Chains
127
+ - **15 EVM mainnets + Solana + TON**, selected by name: `'ethereum'`, `'base'`,
128
+ `'arbitrum'`, `'optimism'`, `'polygon'`, `'bnb'`, `'avalanche'`, `'mantle'`,
129
+ `'sonic'`, `'linea'`, `'scroll'`, `'celo'`, `'zksync'`, `'unichain'`,
130
+ `'worldchain'`, `'solana'`, and `'ton'` — each with canonical USDC (and USDT
131
+ where it exists) pre-filled. **Every token address was verified on-chain
132
+ before shipping**, and each chain's default RPC was checked live.
133
+ - **TON** (the Telegram blockchain) ships USD₮ (Tether) — verified on-chain.
134
+ Native USDC does **not** exist on TON (Circle doesn't issue it there), so it's
135
+ intentionally absent; pass a custom jetton via `{ master, decimals }` for
136
+ USDe / bridged tokens. TON payments use jettons (TEP-74); the proof carries
137
+ the gate's nonce as the transfer comment, so it's bound to its challenge, and
138
+ verification reads the merchant's own jetton wallet (a look-alike jetton can't
139
+ satisfy it). Wallets are `{ mnemonic }` (24 words) or `{ keyPair }`.
140
+ - `token` is **required** — a gate always states exactly what it accepts
141
+ (`'USDC'` / `'USDT'` / `'native'` / a custom `{ address, decimals }` or
142
+ `{ mint, decimals }`). The symbol resolves to the right contract + decimals;
143
+ there is no silent default.
144
+ - Solana and TON **auto-mount** on first use — name `chain: 'solana'` or
145
+ `chain: 'ton'` and the driver loads itself with one lazy import, so pure-EVM
146
+ installs never download them. No setup call; just install the peer deps
147
+ (`@solana/web3.js @solana/spl-token bs58`, or `@ton/ton @ton/core @ton/crypto`).
148
+ - Any other EVM chain works by passing a viem `Chain` or `{ id, rpcUrl }`
149
+ plus a `{ address, decimals }` token. No allowlist, no testnet presets —
150
+ test against mainnet with small amounts.
151
+ - Built on a `PaymentDriver` contract (EVM + Solana ship; register your own
152
+ with `registerDriver`). `CHAINS` and `resolveChain` are exported too.
153
+
154
+ ### Notes
155
+ - Self-custody throughout: the payer signs and broadcasts their own transfer
156
+ to your wallet; PipRail never holds funds.
157
+ - `viem ^2.21` is a peer dependency. Node 20+ or a modern browser.
158
+
159
+ [1.0.0]: https://www.npmjs.com/package/@piprail/sdk
160
+ [0.1.0]: https://www.npmjs.com/package/@piprail/sdk
package/ERRORS.md ADDED
@@ -0,0 +1,182 @@
1
+ # PipRail error handling — the standard
2
+
3
+ This is the **single source of truth** for how `@piprail/sdk` reports errors. It is
4
+ deliberately small and uniform: every module — the client, the server gate, the registry,
5
+ and every chain driver (EVM, Solana, TON, Stellar, and any future family) — follows it
6
+ *exactly*, so a human developer, a merchant server, or an AI agent always gets a **typed,
7
+ understandable** reason, never an opaque chain-library blob.
8
+
9
+ > If you're adding a chain/family/token, the `add-chain-integration` skill points here.
10
+ > Follow §5 (the driver contract) verbatim and your module is consistent by construction.
11
+
12
+ ---
13
+
14
+ ## 1. Two channels, and only two
15
+
16
+ Every failure surfaces through exactly one of two chain-agnostic channels:
17
+
18
+ | | Channel | Shape | For |
19
+ |---|---|---|---|
20
+ | **1** | **THROWN** | a typed [`PipRailError`](src/errors.ts) subclass with a stable `.code` | config / flow / wallet / registry / affordability problems the caller acts on |
21
+ | **2** | **RETURNED** | `VerifyResult` `{ ok: false, error, detail }` where `error` is a `VerifyErrorCode` | the outcome of verifying an on-chain proof (server side) |
22
+
23
+ - **Thrown** errors are caught with `err instanceof PipRailError`, or branched on `err.code`
24
+ (a stable `SCREAMING_SNAKE` string). They never leak a raw `viem`/`@solana`/`@ton`/
25
+ `@stellar` error for a condition the SDK recognises.
26
+ - **Returned** `VerifyResult` is how a driver's `verify()` reports *why a proof was rejected*
27
+ without throwing. The gate turns `{ ok: false, error, detail }` into the canonical 402
28
+ body `{ x402Version: 2, status: 'invalid', error, detail }` (built once by
29
+ [`toInvalidBody`](src/server.ts)); the client relays it to the agent.
30
+
31
+ Rule of thumb: **config/flow/wallet/registry/affordability → throw; proof-verification
32
+ outcome → return.** Replay (`tx_already_used`) is the one verify-style code emitted by the
33
+ *gate* (not a driver), because only the gate owns the used-proof set.
34
+
35
+ ---
36
+
37
+ ## 2. Channel 1 — thrown `PipRailError`
38
+
39
+ Base class [`PipRailError`](src/errors.ts) (abstract; `.name` = the subclass name; supports
40
+ `{ cause }`). All are exported from the package root.
41
+
42
+ | `.code` | Class | Thrown when | Thrown by |
43
+ |---|---|---|---|
44
+ | `WRONG_FAMILY` | `WrongFamilyError` | wallet / `payTo` / token given in another family's shape (or a malformed same-family shape) | every driver (`bindWallet`, `assertValidPayTo`, `resolveToken`) |
45
+ | `UNKNOWN_TOKEN` | `UnknownTokenError` | a built-in token symbol the chain doesn't ship (e.g. `token: 'DOGE'`) | every driver (`resolveToken`) |
46
+ | `INSUFFICIENT_FUNDS` | `InsufficientFundsError` | wallet can't cover the transfer (+ fees/reserve/trustline) | every driver (`send`) — see §6 |
47
+ | `WRONG_CHAIN` | `WrongChainError` | a bring-your-own `walletClient` is on a different chain than configured | EVM wallet adapter; client pre-send guard |
48
+ | `CONFIRMATION_TIMEOUT` | `ConfirmationTimeoutError` | broadcast OK but the tx didn't confirm within the driver's window (re-check the ref) | every driver (`confirm`) |
49
+ | `PAYMENT_TIMEOUT` | `PaymentTimeoutError` | the **server** didn't respond within `retryTimeoutMs` *after* a confirmed payment | client |
50
+ | `MAX_RETRIES_EXCEEDED` | `MaxRetriesExceededError` | server kept returning 402 after the proof confirmed — **message embeds the last server `error — detail`** | client |
51
+ | `PAYMENT_DECLINED` | `PaymentDeclinedError` | the client refused to pay BEFORE any send — over the spend `policy` (amount/total/chain/token/host), or an `onBeforePay` hook returned false / threw | client |
52
+ | `INVALID_ENVELOPE` | `InvalidEnvelopeError` | a 402 had no parseable x402 challenge | client |
53
+ | `NO_COMPATIBLE_ACCEPT` | `NoCompatibleAcceptError` | the challenge offered no `accepts[]` entry for the client's network | client |
54
+ | `NON_REPLAYABLE_BODY` | `NonReplayableBodyError` | `init.body` isn't replayable (e.g. a one-shot stream) | client |
55
+ | `MISSING_DRIVER` | `MissingDriverError` | a family's **optional peer deps aren't installed** (the lazy `import()` failed) — message names the exact `npm install` and sets `{ cause }` | registry loaders |
56
+ | `UNSUPPORTED_NETWORK` | `UnsupportedNetworkError` | no driver for the family, or the driver's `resolve()` returned `null` (unrecognised `chain`) | registry |
57
+
58
+ `MISSING_DRIVER` vs `UNSUPPORTED_NETWORK` is a deliberate split: *deps not installed* vs
59
+ *chain not supported*. Don't reuse one for the other.
60
+
61
+ ---
62
+
63
+ ## 3. Channel 2 — returned `VerifyErrorCode`
64
+
65
+ A closed `snake_case` union ([`x402.ts`](src/x402.ts)). A driver's `verify()` returns one of
66
+ these on `{ ok: false, error, detail }`. **The compiler enforces the set** — you can't invent
67
+ a code, and you must use the same code other drivers use for the same condition.
68
+
69
+ | code | meaning | transient? | who emits it |
70
+ |---|---|---|---|
71
+ | `tx_not_found` | proof tx not on chain yet (RPC lag) or a transient RPC read failed | **transient** | all drivers |
72
+ | `insufficient_confirmations` | mined, but `< minConfirmations` | **transient** | EVM (chains with a discrete confirmation count) |
73
+ | `tx_reverted` | the tx is on chain but failed / reverted | definitive | all |
74
+ | `no_meta` | the tx carries no metadata to inspect | definitive | Solana |
75
+ | `wrong_recipient` | paid, but not to `payTo` | definitive | EVM / Solana native path |
76
+ | `amount_too_low` | paid to `payTo`, but `< required` | definitive | all |
77
+ | `transfer_not_found` | no matching transfer (asset / amount / nonce) to `payTo` | definitive | all |
78
+ | `payment_expired` | older than `maxTimeoutSeconds` (replay window) | definitive | all |
79
+ | `tx_already_used` | this proof was already redeemed (replay) | definitive | the **gate** (not drivers) |
80
+
81
+ **Family-specificity is structural, not drift.** Account-watch chains (TON, Stellar) scan the
82
+ merchant account and can't tell "wrong recipient" from "no payment", so both collapse to
83
+ `transfer_not_found`; `no_meta` is Solana-only; `insufficient_confirmations` needs a discrete
84
+ confirmation count (EVM). Likewise EVM/Solana digest verifiers report a short token payment as
85
+ `transfer_not_found` (no nonce binding to point at), while nonce-bound chains (TON/Stellar)
86
+ can say `amount_too_low`. All correct.
87
+
88
+ **`transient`/`definitive` are informational.** The built-in client retries **every** code up
89
+ to `maxPaymentRetries` with a short backoff (which absorbs RPC lag) — it does *not* branch on
90
+ the code. A consumer building a custom client may branch on it.
91
+
92
+ ---
93
+
94
+ ## 4. What the agent receives
95
+
96
+ - **Rejected proof →** a `402` with body `{ x402Version: 2, status: 'invalid', error, detail }`.
97
+ Always build it with [`toInvalidBody(result)`](src/server.ts) so Express, Hono, Fastify,
98
+ Workers, etc. emit the *identical* envelope.
99
+ - **Client gave up →** `MaxRetriesExceededError` whose message embeds the last server
100
+ `error — detail` (e.g. `… Last server rejection: amount_too_low — Paid 40000, required
101
+ 500000.`), and a `payment-failed` event carrying the same reason.
102
+ - **Client refused to pay →** `PaymentDeclinedError` thrown *before* any on-chain send — the
103
+ quote exceeded the client's `policy`, or an `onBeforePay` hook returned false. Nothing moved.
104
+ - **Config / flow / wallet problem →** a thrown `PipRailError` with a stable `.code`.
105
+
106
+ Observability hooks never change control flow: the gate wraps `onPaid`, and the client routes
107
+ every event through a private `safeEmit()` that swallows handler throws — a logging bug can't
108
+ abort a payment.
109
+
110
+ ---
111
+
112
+ ## 5. The driver error contract (follow this verbatim)
113
+
114
+ Every `PaymentDriver` / `ResolvedNetwork` method has a fixed error behaviour:
115
+
116
+ | method | on error |
117
+ |---|---|
118
+ | `resolve(opts)` | recognise + bind, **or return `null`** (registry maps `null` → `UnsupportedNetworkError`). Never throw a raw chain error for unrecognised input. |
119
+ | `resolveToken(token)` | unknown built-in symbol → `UnknownTokenError`; a foreign-family object token → `WrongFamilyError` (call the shared [`rejectForeignToken(token, family, network)`](src/drivers/shared.ts)); a malformed own-family token → `WrongFamilyError`. |
120
+ | `assertValidPayTo(payTo)` | a non-family address → `WrongFamilyError`. |
121
+ | `bindWallet(wallet)` | a foreign / unusable wallet shape → `WrongFamilyError`. |
122
+ | `send(wallet, accept)` | wrap the broadcast; map affordability → `InsufficientFundsError` (§6); **rethrow everything else unchanged** (never swallow). |
123
+ | `verify(ref, accept)` | **return** a `VerifyResult` with a canonical `VerifyErrorCode`. **Guard every RPC read** so a transient failure returns `tx_not_found` — `verify()` must not throw for an RPC hiccup. Re-derive the watched account from the trusted `accept`, never the client ref. |
124
+ | `confirm(ref, n)` | broadcast-but-not-confirmed / timeout → `ConfirmationTimeoutError`. |
125
+
126
+ ### 6. Affordability converges on one error, by two mechanisms
127
+
128
+ "Wallet can't pay" must always surface as **`InsufficientFundsError`** — but the *detection*
129
+ is per-chain, because each library exposes a different signal:
130
+
131
+ - **Message-regex drivers (Solana, TON):** `send()` does
132
+ `catch (err) { throw toInsufficientFundsError(err) ?? err }`. The shared
133
+ [`toInsufficientFundsError`](src/errors.ts) matches the common "can't afford it" messages and
134
+ returns `null` on a miss (so the original error propagates unchanged — never swallowed).
135
+ - **Structured-error drivers (EVM, Stellar):** detect from typed data — EVM walks viem's
136
+ `BaseError` chain for a nested `InsufficientFundsError`; Stellar reads Horizon
137
+ `result_codes` (and treats an unfunded source account's `loadAccount` 404 as the same). Both
138
+ then *also* fall through to `toInsufficientFundsError` as a message-level backstop, so the
139
+ two paths can't drift in vocabulary.
140
+
141
+ Either way the caller sees one `InsufficientFundsError` with `.code === 'INSUFFICIENT_FUNDS'`.
142
+
143
+ ---
144
+
145
+ ## 7. Registry / loader pattern
146
+
147
+ - EVM is registered eagerly (`viem` is the one hard peer dep). Solana / TON / Stellar mount
148
+ lazily via a single dynamic `import()` in [`drivers/index.ts`](src/drivers/index.ts) the
149
+ first time their `chain` is named — no setup call.
150
+ - A failed lazy `import()` → `MissingDriverError` naming the exact `npm install` + `{ cause }`.
151
+ The in-flight promise isn't cached on failure, so a later call can retry.
152
+ - No driver for the family, or `resolve()` → `null` → `UnsupportedNetworkError`.
153
+ - Add a family = one loader entry + a mirrored `drivers/<family>/` folder. Nothing else in the
154
+ protocol layer changes.
155
+
156
+ ---
157
+
158
+ ## 8. Shared building blocks (don't reinvent per chain)
159
+
160
+ | Helper | Where | Purpose |
161
+ |---|---|---|
162
+ | `toInsufficientFundsError(err)` | [`errors.ts`](src/errors.ts) | message → `InsufficientFundsError \| null` (the affordability backstop) |
163
+ | `rejectForeignToken(token, family, network)` | [`drivers/shared.ts`](src/drivers/shared.ts) | uniform `WrongFamilyError` for a foreign-family object token (data-driven; a new family is auto-covered) |
164
+ | `toInvalidBody(result)` | [`server.ts`](src/server.ts) | the canonical 402 'invalid' JSON body for every framework adapter |
165
+ | `delay(ms)` | [`util/async.ts`](src/util/async.ts) | the one poll/confirm delay shared by all drivers |
166
+ | `safeEmit(event)` | client (private); the gate mirrors it with an inline try/catch around `onPaid` | observability hooks never abort the flow |
167
+
168
+ ---
169
+
170
+ ## 9. New-module error checklist
171
+
172
+ ```
173
+ - [ ] verify() returns ONLY canonical VerifyErrorCode values (compiler-enforced); same code
174
+ as other drivers for the same condition; every RPC read guarded → tx_not_found on failure.
175
+ - [ ] send() wraps the broadcast and maps affordability → InsufficientFundsError
176
+ (toInsufficientFundsError for message-only chains; structured detection + that backstop otherwise).
177
+ - [ ] confirm() → ConfirmationTimeoutError on broadcast-but-not-confirmed.
178
+ - [ ] resolveToken(): unknown symbol → UnknownTokenError; foreign token → rejectForeignToken(...).
179
+ - [ ] bindWallet() / assertValidPayTo() → WrongFamilyError for the wrong shape, message names the right one.
180
+ - [ ] loader entry throws MissingDriverError with the exact `npm install` + { cause }.
181
+ - [ ] No raw chain error escapes for a condition the SDK recognises; the rest rethrow unchanged.
182
+ ```
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PipRail
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.