@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 +160 -0
- package/ERRORS.md +182 -0
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/STANDARDS.md +123 -0
- package/dist/chunk-3TQJJ4SQ.js +157 -0
- package/dist/chunk-CQREG5LE.cjs +8 -0
- package/dist/chunk-FURB5RP7.js +8 -0
- package/dist/chunk-WQWNPAYQ.cjs +157 -0
- package/dist/index.cjs +1681 -0
- package/dist/index.d.cts +4578 -0
- package/dist/index.d.ts +4578 -0
- package/dist/index.js +1681 -0
- package/dist/near-4P5XNMMB.cjs +346 -0
- package/dist/near-RVXGF7TW.js +346 -0
- package/dist/solana-7PZG3CDO.js +342 -0
- package/dist/solana-F7H4YDW5.cjs +342 -0
- package/dist/stellar-BPPQTLNI.cjs +389 -0
- package/dist/stellar-PAZ352JL.js +389 -0
- package/dist/sui-6N4ZPAGD.js +304 -0
- package/dist/sui-XV4YYSGV.cjs +304 -0
- package/dist/ton-E5RLUPD2.cjs +366 -0
- package/dist/ton-EFZKQAAK.js +366 -0
- package/dist/tron-243DT6PF.js +372 -0
- package/dist/tron-3UDH7KGF.cjs +372 -0
- package/dist/xrpl-6NRFT5CA.cjs +449 -0
- package/dist/xrpl-7GWXDAVZ.js +449 -0
- package/package.json +143 -0
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.
|