@piprail/sdk 1.13.1 → 1.15.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.
Files changed (30) hide show
  1. package/CHAINS.md +3 -2
  2. package/CHANGELOG.md +69 -0
  3. package/ERRORS.md +17 -2
  4. package/README.md +57 -4
  5. package/STANDARDS.md +4 -0
  6. package/dist/{algorand-MXUSKX46.cjs → algorand-EJ3S2V7E.cjs} +17 -17
  7. package/dist/{algorand-WGVF4KTU.js → algorand-F3OYB534.js} +1 -1
  8. package/dist/{aptos-YT7SXWPF.cjs → aptos-GJGIZHNI.cjs} +16 -16
  9. package/dist/{aptos-LPBLSEIQ.js → aptos-SUXOVP7B.js} +1 -1
  10. package/dist/{chunk-SVMGHASK.js → chunk-ILPABTI2.js} +18 -1
  11. package/dist/{chunk-MDLZJGLY.cjs → chunk-PA6YD3HL.cjs} +35 -18
  12. package/dist/index.cjs +800 -163
  13. package/dist/index.d.cts +472 -42
  14. package/dist/index.d.ts +472 -42
  15. package/dist/index.js +695 -58
  16. package/dist/{near-K6BDBABG.js → near-LM7S3WUD.js} +1 -1
  17. package/dist/{near-7ZDNISUX.cjs → near-ZJLZE26R.cjs} +19 -19
  18. package/dist/{solana-PU7N2M64.cjs → solana-MPPE6K24.cjs} +14 -14
  19. package/dist/{solana-S3UFI3FE.js → solana-WDKWWF33.js} +1 -1
  20. package/dist/{stellar-Q5PO23SC.js → stellar-FIJPQZVW.js} +1 -1
  21. package/dist/{stellar-VDQOFQEO.cjs → stellar-XHLLNHQP.cjs} +21 -21
  22. package/dist/{sui-FKSMLKRF.cjs → sui-6CVLEXLA.cjs} +17 -17
  23. package/dist/{sui-WOXRKJXS.js → sui-B7AVN7NK.js} +1 -1
  24. package/dist/{ton-WPTXGLVK.js → ton-CHJ26BVA.js} +1 -1
  25. package/dist/{ton-VK6KRJHP.cjs → ton-RNEFN25G.cjs} +14 -14
  26. package/dist/{tron-6GXBXTR4.js → tron-DD3JDROV.js} +1 -1
  27. package/dist/{tron-WLOF5OUV.cjs → tron-TKJHNFGM.cjs} +24 -24
  28. package/dist/{xrpl-HEAPEXAM.js → xrpl-GTUPP6SK.js} +1 -1
  29. package/dist/{xrpl-CMNI25BV.cjs → xrpl-XN2NBNGI.cjs} +21 -21
  30. package/package.json +1 -1
package/CHAINS.md CHANGED
@@ -13,7 +13,7 @@ read those sections before you ship them.
13
13
 
14
14
  | Chain(s) | Pay in native coin? | Built-in stablecoins | Receiver needs setup? | Wallet input |
15
15
  |---|:--:|---|---|---|
16
- | **EVM** (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, HyperEVM, Monad, Kaia, + any EVM chain) | ✅ ETH/BNB/POL/… | USDC (all **except Kaia**) · USDT (all **except Base, World Chain, Sei, HyperEVM, Monad**) | No | `{ privateKey }` |
16
+ | **EVM** (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, HyperEVM, Monad, Kaia, + any EVM chain) | ✅ ETH/BNB/POL/… | USDC (all **except Kaia**) · USDT (all **except Base, World Chain, Sei, HyperEVM, Monad**) · **EURC** (Ethereum, Base, Avalanche) | No | `{ privateKey }` |
17
17
  | **Solana** | ✅ SOL | USDC · USDT | No (payer creates the recipient's token account) | `{ secretKey }` |
18
18
  | **Sui** | ✅ SUI | USDC (no USDT) | No | `{ privateKey }` (`suiprivkey1…`) |
19
19
  | **Aptos** | ✅ APT | USDC · USDT | No (primary FA store auto-creates) | `{ privateKey }` (`ed25519-priv-0x…`) |
@@ -44,8 +44,9 @@ read those sections before you ship them.
44
44
  ## Chains with no caveats
45
45
 
46
46
  ### EVM — Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, …
47
- - **Pay in:** native coin (`'native'`), `'USDC'`, `'USDT'`, or a custom `{ address, decimals }`.
47
+ - **Pay in:** native coin (`'native'`), `'USDC'`, `'USDT'`, `'EURC'` (where issued), or a custom `{ address, decimals }`.
48
48
  - **USDT gap:** built in on every preset **except Base, World Chain, Sei, HyperEVM, and Monad** (USDC only there). **Kaia** is the inverse — **USD₮ only** (no Circle-native USDC on Kaia).
49
+ - **EURC:** Circle's euro stablecoin is built in on **Ethereum, Base, and Avalanche** (EIP-3009, 6-dp, addresses verified on-chain). Its EIP-712 domain name differs per deployment (`"Euro Coin"` on Ethereum/Avalanche, `"EURC"` on Base) — the SDK reads it on-chain, so `exact` payments are correct everywhere. Like USDC, it's `exact`-payable.
49
50
  - **Decimals:** on **BNB Chain**, Binance-Peg USDC/USDT are **18 decimals**, not 6 (the SDK handles it; don't hardcode 6).
50
51
  - **Stablecoin provenance — issuer-native vs bridged (every shipped address verified on-chain 2026-06-08, incl. bridge markers).** Every address is the correct, canonical, 1:1-redeemable dollar token on its chain; what varies is *who issues it*. You request it as `'USDC'` / `'USDT'` either way — provenance matters only if you specifically require issuer-native settlement.
51
52
  - **USDC** is **Circle-native** on every preset **except** **BNB** (Binance-Peg, 18-dp), **Mantle** (OP canonical-bridge), and **Scroll** (Bridged-USDC-Standard) — the last two are backed 1:1 by Circle USDC on Ethereum but are **not** Circle-issued on that chain (absent from Circle's native-USDC list).
package/CHANGELOG.md CHANGED
@@ -4,6 +4,73 @@ All notable changes to `@piprail/sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.15.0] — 2026-06-10 — the trusted agent wallet (budget-bound, time-boxed, asks-first)
8
+
9
+ A minor, fully additive layer — defaults byte-identical, no new error code, protocol
10
+ layer stays viem-free.
11
+
12
+ ### Added — a TIME dimension on `PaymentPolicy` (Mode A)
13
+ - Four opt-in fields make the spend leash a *clock* too: `ttlSeconds` / `expiresAt` (a
14
+ session deadline — past it EVERY pay is refused with `reasonCode:'SESSION_EXPIRED'`,
15
+ TERMINAL) and `windowTotal` + `windowSeconds` (an optional rolling rate-limit, per
16
+ `(network, asset)`). All default off → behaviour unchanged. A half-armed window
17
+ (`windowTotal` without `windowSeconds`) or an unsafe `ttlSeconds` throws at construction.
18
+ - `client.budget(): SessionBudget` and `PaymentPlan.session` surface the remaining money +
19
+ time leash so a headless agent can SEE it before paying. `client.remaining(): SpendRemaining[]`
20
+ gives the per-asset cap, ledger-scoped. All read-only, never throw, process-scoped (reset on restart).
21
+ - `PolicyDecision` gains a typed `code` (`PolicyDenyCode`); `PayBlocker` gains `OUTSIDE_WINDOW`.
22
+
23
+ ### Added — agent ergonomics (the model-facing contract)
24
+ - `PaymentDeclinedError.reasonCode` (`'POLICY' | 'BUDGET' | 'OUTSIDE_WINDOW' | 'SESSION_EXPIRED'
25
+ | 'APPROVAL'`) — a typed discriminator so an agent branches on the cause (and spots a TERMINAL
26
+ decline) without parsing prose. No new `.code`.
27
+ - The `piprail_pay_request` tool now funnels EVERY `PipRailError` into a structured
28
+ `{ ok:false, code, reason, explain, ref?, reasonCode?, declined? }` — never an uncaught crash, so a
29
+ broadcast-but-unconfirmed timeout reaches the agent with its `.ref` and the never-re-pay rule.
30
+ - New pure exports: `summarizePlan` / `explainDecline` / `formatSpendReport` (NL renderers, wired into
31
+ the tool outputs), `classifyChallenge` + `ChallengeTriage` (scheme/chain triage), and
32
+ `PIPRAIL_AGENT_GUIDE` / `agentGuide` (the cross-tool contract).
33
+ - `paymentTools()` now returns **7** tools — the original 5, plus read-only `piprail_budget` and
34
+ `piprail_guide` appended last (the first five are byte-identical in name + order).
35
+
36
+ ## [1.14.0] — 2026-06-10
37
+
38
+ ### Added — pay the standard `exact` rail (opt-in, EVM + EIP-3009)
39
+ - **`PipRailClient` can now PAY standard x402 `exact` rails**, not just PipRail's native
40
+ `onchain-proof` — so an agent can transact with ANY standard v2 x402 server (the dominant
41
+ `exact`-on-Base-via-CDP web), while PipRail's own gates stay backendless. **Opt-in** via a new
42
+ `schemes` option (default `['onchain-proof']` — the zero-config path is byte-identical):
43
+ `new PipRailClient({ chain: 'base', wallet, schemes: ['onchain-proof', 'exact'] })`, with a per-call
44
+ override `fetch(url, { schemes: ['exact'] })`. EVM + EIP-3009 only (USDC/EURC); silently ignored on
45
+ non-EVM chains, for USDT/native, or for a token the SDK can't price (those keep `onchain-proof`).
46
+ - The buyer signs an EIP-3009 authorization with **its own** wallet and the server / merchant-chosen
47
+ facilitator broadcasts it — the buyer pays ~0 gas and PipRail hosts/settles nothing. `quote()`,
48
+ `planPayment()`, `estimateCost()`, `canAfford()`, `autoRoute`, and `planAcross()` are now truthful
49
+ across both schemes (an exact rail is priced gasless: `cost.fee === '0'`, never an `INSUFFICIENT_GAS`
50
+ blocker). The EIP-712 domain is **re-derived on-chain** (`name()`/`version()`), never trusted from
51
+ the server's `extra`. The same `policy` + `onBeforePay` gate it BEFORE any signature.
52
+ **Verify against your target facilitator before production.**
53
+ - **EURC is now a built-in EVM preset token** on Ethereum, Base, and Avalanche (on-chain-verified;
54
+ EIP-3009, 6 decimals) — so the `exact` buyer recognises it and the "USDC/EURC" coverage is real, not
55
+ aspirational. (Its EIP-712 domain name differs per deployment — `"Euro Coin"` on Ethereum/Avalanche,
56
+ `"EURC"` on Base — which the buyer re-derives on-chain; the symbol is display-only.)
57
+ - `X402ExactAcceptEntry.extra.name`/`version` are now OPTIONAL (the exact-EVM scheme only requires
58
+ `assetTransferMethod`) — matching the spec; the buyer ignores them (it re-derives on-chain), the gate
59
+ still populates them from its own on-chain read. The `payment-settled` event now also carries the
60
+ conformant `settle?: SettleOutcome` (a third-party facilitator's lean SettleResponse, when there's no
61
+ rich receipt).
62
+ - New exports: `buildExactSignatureHeader`, `parseSettleResponse` (+ the `SettleOutcome` type), the
63
+ `PaymentScheme` type, and the `UnsupportedSchemeError` (`code: 'UNSUPPORTED_SCHEME'`). New driver SPI
64
+ method `payExact?` (optional, EVM-only). `@piprail/mcp` adds the `PIPRAIL_SCHEMES` env (unset ⇒ the
65
+ SDK default, so the MCP's zero-config posture is unchanged).
66
+
67
+ ### Changed (additive — minor, but type-affecting)
68
+ - `PayOption.accept` and the `payment-required` event's `accept` are now `X402AnyAccept` (was
69
+ `X402AcceptEntry`). A consumer that reads `accept.extra.nonce`/`minConfirmations` without a `scheme`
70
+ guard should narrow on `accept.scheme === 'onchain-proof'` first.
71
+ - The buyer emits **v2 only** for `exact` (`PAYMENT-SIGNATURE` + the `accepted`-envelope). v1-only
72
+ servers (which never parse as a v2 challenge here) are out of scope for this milestone.
73
+
7
74
  ## [1.13.1] — 2026-06-10
8
75
 
9
76
  ### Fixed
@@ -624,6 +691,8 @@ straight into your wallet. The API is small and self-contained.
624
691
  to your wallet; PipRail never holds funds.
625
692
  - `viem ^2.21` is a peer dependency. Node 20+ or a modern browser.
626
693
 
694
+ [1.15.0]: https://www.npmjs.com/package/@piprail/sdk
695
+ [1.14.0]: https://www.npmjs.com/package/@piprail/sdk
627
696
  [1.13.1]: https://www.npmjs.com/package/@piprail/sdk
628
697
  [1.13.0]: https://www.npmjs.com/package/@piprail/sdk
629
698
  [1.12.0]: https://www.npmjs.com/package/@piprail/sdk
package/ERRORS.md CHANGED
@@ -56,7 +56,8 @@ Base class [`PipRailError`](src/errors.ts) (abstract; `.name` = the subclass nam
56
56
  | `MAX_RETRIES_EXCEEDED` | `MaxRetriesExceededError` | server kept returning 402 after broadcast — **message embeds the last server `error — detail`, and carries `.ref`** | client |
57
57
  | `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 |
58
58
  | `INVALID_ENVELOPE` | `InvalidEnvelopeError` | a 402 had no parseable x402 challenge | client |
59
- | `NO_COMPATIBLE_ACCEPT` | `NoCompatibleAcceptError` | the challenge offered no `accepts[]` entry for the client's network | client |
59
+ | `NO_COMPATIBLE_ACCEPT` | `NoCompatibleAcceptError` | the challenge offered no `accepts[]` entry the client can pay on its network + enabled `schemes` (message names the enabled schemes) | client |
60
+ | `UNSUPPORTED_SCHEME` | `UnsupportedSchemeError` | asked to pay a scheme the bound family/asset/signer can't settle, with no fallback: `exact` on a non-EVM family, a non-EIP-3009 token (USDT/native/plain ERC-20), or a contract / EIP-1271 / EIP-7702 signer | client / EVM `exact` (`payExact`) |
60
61
  | `NON_REPLAYABLE_BODY` | `NonReplayableBodyError` | `init.body` isn't replayable (e.g. a one-shot stream) | client |
61
62
  | `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 |
62
63
  | `UNSUPPORTED_NETWORK` | `UnsupportedNetworkError` | no driver for the family, or the driver's `resolve()` returned `null` (unrecognised `chain`) | registry |
@@ -113,9 +114,23 @@ the code. A consumer building a custom client may branch on it.
113
114
  `error — detail` (e.g. `… Last server rejection: amount_too_low — Paid 40000, required
114
115
  500000.`), and a `payment-failed` event carrying the same reason.
115
116
  - **Client refused to pay →** `PaymentDeclinedError` thrown *before* any on-chain send — the
116
- quote exceeded the client's `policy`, or an `onBeforePay` hook returned false. Nothing moved.
117
+ quote exceeded the client's `policy` (amount/total/chain/token/host, or the session's **time
118
+ envelope**), or an `onBeforePay` hook returned false. Nothing moved. It carries an optional typed
119
+ `reasonCode` (`'POLICY' | 'BUDGET' | 'OUTSIDE_WINDOW' | 'SESSION_EXPIRED' | 'APPROVAL'`) so an agent
120
+ branches on the cause — and recognises a **TERMINAL** `SESSION_EXPIRED` / `APPROVAL` it must not
121
+ retry — without parsing the message. The session TTL (`SESSION_EXPIRED`) and rolling window
122
+ (`OUTSIDE_WINDOW`) reuse this EXISTING `PaymentDeclinedError` (`.code` stays `'PAYMENT_DECLINED'`):
123
+ **no new error class, no new `VerifyErrorCode`** — only the closed `PayBlocker` union gains `OUTSIDE_WINDOW`.
117
124
  - **Config / flow / wallet problem →** a thrown `PipRailError` with a stable `.code`.
118
125
 
126
+ > **The agent toolkit funnels all of this.** The `piprail_pay_request` tool catches **every**
127
+ > `PipRailError` and returns a structured `{ ok:false, code, reason, explain, ref?, reasonCode?,
128
+ > declined? }` instead of letting it crash the agent loop — so a broadcast-but-unconfirmed
129
+ > `PAYMENT_TIMEOUT`/`MAX_RETRIES_EXCEEDED`/`CONFIRMATION_TIMEOUT` reaches the model with its `.ref` and
130
+ > the never-re-pay rule (via `explainDecline`). Only a genuine non-`PipRailError` bug rethrows.
131
+ > The pure renderers (`render.ts`) and `classifyChallenge` (`classify.ts`) are viem-free protocol-layer
132
+ > modules; `render.ts`'s VALUE import of `errors.ts` is allowed (errors.ts is chain-agnostic).
133
+
119
134
  Observability hooks never change control flow: the gate wraps `onPaid`, and the client routes
120
135
  every event through a private `safeEmit()` that swallows handler throws — a logging bug can't
121
136
  abort a payment.
package/README.md CHANGED
@@ -59,6 +59,22 @@ exact: { settle: { facilitator: 'https://x402.org/facilitator' } }
59
59
 
60
60
  EVM + EIP-3009 tokens only (USDC, EURC — not USDT, not native; those stay `onchain-proof`). Omit `exact` and the gate is byte-identical to today. Proven end-to-end: a real `@x402/fetch` reference client settles against a PipRail gate on Base mainnet.
61
61
 
62
+ ### Pay *any* x402 server — the `exact` rail, buyer side
63
+
64
+ The mirror image: let your **`PipRailClient` pay** standard x402 servers (the dominant `exact`-on-Base-via-CDP web), not just PipRail's own gates. Opt in with `schemes` — default `['onchain-proof']`, so the zero-config client is byte-identical:
65
+
66
+ ```ts
67
+ const client = new PipRailClient({
68
+ chain: 'base',
69
+ wallet: { privateKey: process.env.AGENT_KEY },
70
+ schemes: ['onchain-proof', 'exact'], // also pay standard exact rails
71
+ })
72
+ await client.fetch('https://api.somevendor.com/paid') // pays whichever rail it offers
73
+ // or per call: client.fetch(url, { schemes: ['exact'] })
74
+ ```
75
+
76
+ On an `exact` rail the client signs an EIP-3009 authorization with **its own** wallet and the server / merchant-chosen facilitator broadcasts it — so the **buyer pays ~0 gas** and PipRail hosts/settles nothing. The EIP-712 token domain is **re-derived on-chain** (never trusted from the challenge), the same `policy` + `onBeforePay` gate it **before any signature**, and `quote()`/`planPayment()`/`estimateCost()` are truthful across both schemes (an exact rail prices gasless). EVM + EIP-3009 only (USDC/EURC); silently ignored on non-EVM chains, for USDT/native, or for a token the SDK can't price — those stay `onchain-proof`. **Verify against your target facilitator before production.**
77
+
62
78
  ## Built for agents — spend safely
63
79
 
64
80
  A funded key loose on the internet needs guardrails. Opt in to a `policy` and the client refuses anything outside it **before any on-chain send** — plus learn a price without paying it, approve each payment, and read back exactly what you spent. All opt-in, all local, no backend; omit it and the client behaves exactly as before.
@@ -132,17 +148,53 @@ For **every rail the 402 offers on your chain**, the plan reads **token balance
132
148
 
133
149
  ```ts
134
150
  import { paymentTools } from '@piprail/sdk'
135
- const tools = paymentTools(client) // → [piprail_discover, piprail_quote_payment, piprail_plan_payment, piprail_pay_request, piprail_register]
151
+ const tools = paymentTools(client)
152
+ // → [piprail_discover, piprail_quote_payment, piprail_plan_payment,
153
+ // piprail_pay_request, piprail_register, piprail_budget, piprail_guide]
136
154
  ```
137
155
 
138
156
  Each descriptor also carries advisory **`annotations`** (MCP-style `ToolAnnotations` — `title`,
139
- `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`): the three reads are flagged
157
+ `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`): the reads are flagged
140
158
  **read-only**, `piprail_pay_request` is flagged **value-moving** (the one tool that spends), and
141
159
  `piprail_register` is non-destructive — so an MCP client can render the right consent. They're hints,
142
160
  not the boundary; the spend policy is. `@piprail/mcp` advertises them on the wire.
143
161
 
144
162
  See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
145
163
 
164
+ ### A budget that's also a clock — the time envelope (Mode A)
165
+
166
+ The spend policy already caps *amount* and *total*; it can also bound *time*, so a headless agent
167
+ runs free **inside** a budget **and** time envelope and the policy *is* the consent — no per-pay ask.
168
+ All opt-in; omit them and behaviour is byte-identical.
169
+
170
+ ```ts
171
+ const client = new PipRailClient({
172
+ chain: 'base', wallet,
173
+ policy: {
174
+ maxAmount: '0.10', maxTotal: '5.00', tokens: ['USDC'],
175
+ ttlSeconds: 3600, // the whole session expires in 1h — then EVERY pay is refused
176
+ windowTotal: '1.00', windowSeconds: 600, // …and ≤ $1 of USDC in any rolling 10-minute window
177
+ },
178
+ })
179
+
180
+ client.budget() // → { session: { expiresAt, secondsRemaining }, byAsset: [...] } — read the leash before paying
181
+ client.remaining() // → per-(network,asset) remaining cap (ledger-scoped)
182
+ ```
183
+
184
+ A pay past the deadline throws `PaymentDeclinedError` with **`reasonCode: 'SESSION_EXPIRED'`** (TERMINAL
185
+ — don't retry); a window breach is `'OUTSIDE_WINDOW'`. The window needs **both** `windowTotal` and
186
+ `windowSeconds` (one alone throws at construction — a leash can't be half-armed). State is in-memory and
187
+ **resets on restart** (the session *is* the process); durable limits are the caller's pluggable concern.
188
+
189
+ ### Legible to the model — the decline contract + the guide
190
+
191
+ `piprail_pay_request` funnels **every** failure into a structured `{ ok:false, code, reason, explain,
192
+ ref?, reasonCode?, declined? }` — never an uncaught crash — so a broadcast-but-unconfirmed timeout
193
+ reaches the agent with its `.ref` and the **never-re-pay** rule, not a double-spend. Pure helpers make
194
+ the rest legible: `summarizePlan` / `explainDecline` / `formatSpendReport` (one-line English),
195
+ `classifyChallenge` (wrong-chain vs unpayable-scheme), and `PIPRAIL_AGENT_GUIDE` (the whole contract,
196
+ distilled for an LLM). `piprail_budget` lets the agent self-check its leash; `piprail_guide` returns the contract.
197
+
146
198
  ## Be discoverable — find and be found ($0, no backend)
147
199
 
148
200
  A 402 endpoint is payable, but nobody can *find* it. PipRail closes that gap by building on the
@@ -253,7 +305,8 @@ For an LLM/MCP these are two more tools — **`piprail_discover`** (find) and **
253
305
 
254
306
  > **Two honest caveats.** The open indexes assume the mainstream `exact` scheme, so to be *usefully*
255
307
  > listed also offer a standard `exact` USDC rail on Base/Solana (`discover()` results are
256
- > cross-scheme; `fetch()` pays only PipRail `onchain-proof` rails directly). And **x402scan indexes
308
+ > cross-scheme; `fetch()` pays PipRail `onchain-proof` rails by default, and standard `exact` rails too
309
+ > once you opt in with `schemes: ['onchain-proof', 'exact']` — EVM/EIP-3009). And **x402scan indexes
257
310
  > Base/Solana only** — 402 Index has no such limit, so it's the default register target. There is no
258
311
  > single ratified discovery standard yet; OpenAPI-first is an emerging multi-vendor convention.
259
312
 
@@ -758,7 +811,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
758
811
 
759
812
  **Bring your own chain family:** the SDK is built on a tiny `PaymentDriver` contract — `resolve(chain)` returns a bound network with `resolveToken` / `describeAsset` / `assertValidPayTo` / `bindWallet` / `send` / `confirm` / `estimateCost` / `balanceOf` / `recipientReady` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
760
813
 
761
- **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.
814
+ **Universal x402 (`exact` scheme):** the supported way to pay any standard x402 server is `new PipRailClient({ …, schemes: ['onchain-proof', 'exact'] })` (see [Pay *any* x402 server](#pay-any-x402-server--the-exact-rail-buyer-side)) — the client signs the EIP-3009 authorization on-chain-derived-domain and the server/facilitator settles. The low-level codecs `parseExactRequirements` / `buildExactAuthorization` (`@deprecated` — trusts the server-supplied domain, local-key only) / `encodeXPaymentHeader` remain for hand-rolled or v1 clients. EVM + EIP-3009 only; validate against your target facilitator before production.
762
815
 
763
816
  ## Requirements
764
817
 
package/STANDARDS.md CHANGED
@@ -109,6 +109,10 @@ npm test # full Vitest suite
109
109
  npm run build # tsup build succeeds
110
110
  # lazy-chunk invariant — the EVM bundle pulls in no non-EVM chain lib:
111
111
  grep -E "from ?['\"]@(solana|ton|stellar)" dist/index.js # → expect NO matches
112
+ # viem-free protocol layer — the chain-agnostic core never imports a chain SDK
113
+ # (includes the pure agent-ergonomics modules render/classify/agentGuide; render.ts's
114
+ # VALUE import of errors.ts is allowed — errors.ts is chain-agnostic, the grep targets viem):
115
+ grep -lE "from ['\"]viem" src/client.ts src/x402.ts src/policy.ts src/ledger.ts src/server.ts src/agent.ts src/render.ts src/classify.ts src/agentGuide.ts # → expect NO matches
112
116
  ```
113
117
 
114
118
  `prepublishOnly` runs build + test + both typechecks. Never ship with any of these red.
@@ -7,7 +7,7 @@
7
7
 
8
8
 
9
9
 
10
- var _chunkMDLZJGLYcjs = require('./chunk-MDLZJGLY.cjs');
10
+ var _chunkPA6YD3HLcjs = require('./chunk-PA6YD3HL.cjs');
11
11
 
12
12
  // src/drivers/algorand/index.ts
13
13
  var _algosdk = require('algosdk'); var _algosdk2 = _interopRequireDefault(_algosdk);
@@ -55,13 +55,13 @@ async function payAlgorand(params) {
55
55
  } catch (err) {
56
56
  const mapped = mapAlgorandError(err, accept.payTo);
57
57
  if (mapped) throw mapped;
58
- throw _nullishCoalesce(_chunkMDLZJGLYcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
58
+ throw _nullishCoalesce(_chunkPA6YD3HLcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
59
59
  }
60
60
  }
61
61
  function mapAlgorandError(err, payTo) {
62
62
  const m = err instanceof Error ? err.message : String(err);
63
63
  if (/must optin/i.test(m) || /missing from/i.test(m) && m.includes(payTo)) {
64
- return new (0, _chunkMDLZJGLYcjs.RecipientNotReadyError)(
64
+ return new (0, _chunkPA6YD3HLcjs.RecipientNotReadyError)(
65
65
  `Algorand recipient ${payTo} hasn't opted into this asset \u2014 it must opt in (a 0-amount asset transfer to itself) before it can receive. (Algorand: ${firstLine(m)})`,
66
66
  { cause: err }
67
67
  );
@@ -69,7 +69,7 @@ function mapAlgorandError(err, payTo) {
69
69
  if (/overspend|below min|min(imum)? balance|tried to spend|balance \d+ below|asset \d+ missing from|insufficient|underflow/i.test(
70
70
  m
71
71
  )) {
72
- return new (0, _chunkMDLZJGLYcjs.InsufficientFundsError)(
72
+ return new (0, _chunkPA6YD3HLcjs.InsufficientFundsError)(
73
73
  `Algorand payment failed: the sender can't cover it \u2014 token balance, ALGO for fees, the 0.1-ALGO minimum balance, or a missing asset opt-in on the sender. (Algorand: ${firstLine(m)})`,
74
74
  { cause: err }
75
75
  );
@@ -157,22 +157,22 @@ function rpcFailed(nonce) {
157
157
 
158
158
  function assertAlgorandWallet(wallet, network) {
159
159
  if (typeof wallet !== "object" || wallet === null) {
160
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
160
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
161
161
  `chain ${network} is Algorand; wallet must be { mnemonic } (25 words) or { account }.`
162
162
  );
163
163
  }
164
164
  if ("privateKey" in wallet || "walletClient" in wallet) {
165
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
165
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
166
166
  `chain ${network} is Algorand; an EVM/Aptos wallet can't be used \u2014 pass { mnemonic } (25 words) or { account }.`
167
167
  );
168
168
  }
169
169
  if ("secretKey" in wallet || "signer" in wallet || "secret" in wallet || "keypair" in wallet || "keyPair" in wallet || "seed" in wallet || "accountId" in wallet) {
170
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
170
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
171
171
  `chain ${network} is Algorand; that looks like another family's wallet \u2014 pass { mnemonic } (25 words) or { account }.`
172
172
  );
173
173
  }
174
174
  if (!("mnemonic" in wallet) && !("account" in wallet)) {
175
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
175
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
176
176
  `chain ${network} is Algorand; wallet must be { mnemonic } (25 words) or { account }.`
177
177
  );
178
178
  }
@@ -187,13 +187,13 @@ function resolveAlgorandWallet(config) {
187
187
  const { addr, sk } = _algosdk2.default.mnemonicToSecretKey(config.mnemonic);
188
188
  return { addr: addr.toString(), sk };
189
189
  } catch (cause) {
190
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
190
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
191
191
  "Algorand wallet { mnemonic } is not a valid 25-word Algorand mnemonic.",
192
192
  { cause }
193
193
  );
194
194
  }
195
195
  }
196
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)("Algorand wallet needs { mnemonic } (25 words) or { account }.");
196
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)("Algorand wallet needs { mnemonic } (25 words) or { account }.");
197
197
  }
198
198
 
199
199
  // src/drivers/algorand/index.ts
@@ -248,16 +248,16 @@ function makeAlgorandNetwork(preset, algodUrl) {
248
248
  const info = preset.tokens[token.toUpperCase()];
249
249
  if (!info) {
250
250
  const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
251
- throw new (0, _chunkMDLZJGLYcjs.UnknownTokenError)(
251
+ throw new (0, _chunkPA6YD3HLcjs.UnknownTokenError)(
252
252
  `token "${token}" isn't built in for Algorand (known: ${known}). Pass { assetId, decimals } for a custom ASA, or use 'native'.`
253
253
  );
254
254
  }
255
255
  return { asset: algorandAssetId(info.assetId), decimals: info.decimals, symbol: info.symbol };
256
256
  }
257
- _chunkMDLZJGLYcjs.rejectForeignToken.call(void 0, token, "algorand", network);
257
+ _chunkPA6YD3HLcjs.rejectForeignToken.call(void 0, token, "algorand", network);
258
258
  const t = token;
259
259
  if (typeof t.assetId !== "number" || typeof t.decimals !== "number") {
260
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
260
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
261
261
  `chain ${network} is Algorand; a custom token must be { assetId, decimals }.`
262
262
  );
263
263
  }
@@ -278,12 +278,12 @@ function makeAlgorandNetwork(preset, algodUrl) {
278
278
  },
279
279
  assertValidPayTo(payTo) {
280
280
  if (payTo.startsWith("0x")) {
281
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
281
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
282
282
  `chain ${network} is Algorand, but payTo "${payTo}" looks like an EVM address.`
283
283
  );
284
284
  }
285
285
  if (!_algosdk2.default.isValidAddress(payTo)) {
286
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
286
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
287
287
  `chain ${network} is Algorand, but payTo "${payTo}" is not a valid Algorand address.`
288
288
  );
289
289
  }
@@ -300,13 +300,13 @@ function makeAlgorandNetwork(preset, algodUrl) {
300
300
  const info = await _algosdk2.default.waitForConfirmation(algod, ref, 10);
301
301
  return { height: String(_nullishCoalesce(info.confirmedRound, () => ( 0))) };
302
302
  } catch (err) {
303
- throw new (0, _chunkMDLZJGLYcjs.ConfirmationTimeoutError)(`Algorand tx ${ref} did not confirm in time.`, {
303
+ throw new (0, _chunkPA6YD3HLcjs.ConfirmationTimeoutError)(`Algorand tx ${ref} did not confirm in time.`, {
304
304
  cause: err
305
305
  });
306
306
  }
307
307
  },
308
308
  async estimateCost() {
309
- return _chunkMDLZJGLYcjs.nativeCost.call(void 0, {
309
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
310
310
  symbol: ALGO_SYMBOL,
311
311
  decimals: ALGO_DECIMALS,
312
312
  fee: 1000n,
@@ -7,7 +7,7 @@ import {
7
7
  nativeCost,
8
8
  rejectForeignToken,
9
9
  toInsufficientFundsError
10
- } from "./chunk-SVMGHASK.js";
10
+ } from "./chunk-ILPABTI2.js";
11
11
 
12
12
  // src/drivers/algorand/index.ts
13
13
  import algosdk2 from "algosdk";
@@ -6,7 +6,7 @@
6
6
 
7
7
 
8
8
 
9
- var _chunkMDLZJGLYcjs = require('./chunk-MDLZJGLY.cjs');
9
+ var _chunkPA6YD3HLcjs = require('./chunk-PA6YD3HL.cjs');
10
10
 
11
11
  // src/drivers/aptos/index.ts
12
12
  var _tssdk = require('@aptos-labs/ts-sdk');
@@ -55,14 +55,14 @@ async function payAptos(params) {
55
55
  const res = await client.signSubmit({ signer, transaction });
56
56
  return res.hash;
57
57
  } catch (err) {
58
- if (err instanceof _chunkMDLZJGLYcjs.InsufficientFundsError) throw err;
58
+ if (err instanceof _chunkPA6YD3HLcjs.InsufficientFundsError) throw err;
59
59
  if (isAptosAffordability(err)) {
60
- throw new (0, _chunkMDLZJGLYcjs.InsufficientFundsError)(
60
+ throw new (0, _chunkPA6YD3HLcjs.InsufficientFundsError)(
61
61
  err instanceof Error ? err.message : "Insufficient APT/token balance for the payment.",
62
62
  { cause: err }
63
63
  );
64
64
  }
65
- throw _nullishCoalesce(_chunkMDLZJGLYcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
65
+ throw _nullishCoalesce(_chunkPA6YD3HLcjs.toInsufficientFundsError.call(void 0, err), () => ( err));
66
66
  }
67
67
  }
68
68
  function isAptosAffordability(err) {
@@ -146,22 +146,22 @@ function txNotFound(hash) {
146
146
 
147
147
  function assertAptosWallet(wallet, network) {
148
148
  if (typeof wallet !== "object" || wallet === null) {
149
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
149
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
150
150
  `chain ${network} is Aptos; wallet must be { privateKey } (ed25519-priv-0x\u2026) or { account }.`
151
151
  );
152
152
  }
153
153
  if ("walletClient" in wallet) {
154
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
154
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
155
155
  `chain ${network} is Aptos; a viem { walletClient } can't be used \u2014 pass { privateKey } (ed25519-priv-0x\u2026) or { account }.`
156
156
  );
157
157
  }
158
158
  if ("secretKey" in wallet || "signer" in wallet || "mnemonic" in wallet || "keypair" in wallet || "keyPair" in wallet || "secret" in wallet || "seed" in wallet || "accountId" in wallet) {
159
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
159
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
160
160
  `chain ${network} is Aptos; that looks like another family's wallet \u2014 pass { privateKey } (ed25519-priv-0x\u2026) or { account }.`
161
161
  );
162
162
  }
163
163
  if (!("privateKey" in wallet) && !("account" in wallet)) {
164
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
164
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
165
165
  `chain ${network} is Aptos; wallet must be { privateKey } (ed25519-priv-0x\u2026) or { account }.`
166
166
  );
167
167
  }
@@ -173,13 +173,13 @@ function resolveAptosAccount(config) {
173
173
  try {
174
174
  return _tssdk.Account.fromPrivateKey({ privateKey: new (0, _tssdk.Ed25519PrivateKey)(config.privateKey) });
175
175
  } catch (cause) {
176
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
176
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
177
177
  "Aptos wallet { privateKey } is not a valid ed25519 secret (ed25519-priv-0x\u2026 or 0x\u2026 hex).",
178
178
  { cause }
179
179
  );
180
180
  }
181
181
  }
182
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)("Aptos wallet needs { privateKey } (ed25519-priv-0x\u2026) or { account }.");
182
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)("Aptos wallet needs { privateKey } (ed25519-priv-0x\u2026) or { account }.");
183
183
  }
184
184
 
185
185
  // src/drivers/aptos/index.ts
@@ -251,16 +251,16 @@ function makeAptosNetwork(preset, rpcUrl) {
251
251
  const info = preset.tokens[token.toUpperCase()];
252
252
  if (!info) {
253
253
  const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
254
- throw new (0, _chunkMDLZJGLYcjs.UnknownTokenError)(
254
+ throw new (0, _chunkPA6YD3HLcjs.UnknownTokenError)(
255
255
  `token "${token}" isn't built in for Aptos (known: ${known}). Pass { metadata, decimals } for a custom Fungible Asset, or use 'native'.`
256
256
  );
257
257
  }
258
258
  return { asset: info.metadata, decimals: info.decimals, symbol: info.symbol };
259
259
  }
260
- _chunkMDLZJGLYcjs.rejectForeignToken.call(void 0, token, "aptos", network);
260
+ _chunkPA6YD3HLcjs.rejectForeignToken.call(void 0, token, "aptos", network);
261
261
  const t = token;
262
262
  if (!t.metadata || typeof t.decimals !== "number") {
263
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
263
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
264
264
  `chain ${network} is Aptos; a custom token must be { metadata, decimals }.`
265
265
  );
266
266
  }
@@ -287,7 +287,7 @@ function makeAptosNetwork(preset, rpcUrl) {
287
287
  valid = false;
288
288
  }
289
289
  if (!valid || evmLike) {
290
- throw new (0, _chunkMDLZJGLYcjs.WrongFamilyError)(
290
+ throw new (0, _chunkPA6YD3HLcjs.WrongFamilyError)(
291
291
  `chain ${network} is Aptos, but payTo "${payTo}" is not a valid Aptos address (0x + 32 bytes).`
292
292
  );
293
293
  }
@@ -309,11 +309,11 @@ function makeAptosNetwork(preset, rpcUrl) {
309
309
  const tx = await aptos.waitForTransaction({ transactionHash: ref });
310
310
  return { height: String(_nullishCoalesce(tx.version, () => ( "0"))) };
311
311
  } catch (err) {
312
- throw new (0, _chunkMDLZJGLYcjs.ConfirmationTimeoutError)(`Aptos tx ${ref} did not finalize in time.`, { cause: err });
312
+ throw new (0, _chunkPA6YD3HLcjs.ConfirmationTimeoutError)(`Aptos tx ${ref} did not finalize in time.`, { cause: err });
313
313
  }
314
314
  },
315
315
  async estimateCost() {
316
- return _chunkMDLZJGLYcjs.nativeCost.call(void 0, {
316
+ return _chunkPA6YD3HLcjs.nativeCost.call(void 0, {
317
317
  symbol: APT_SYMBOL,
318
318
  decimals: APT_DECIMALS,
319
319
  fee: 100000n,
@@ -6,7 +6,7 @@ import {
6
6
  nativeCost,
7
7
  rejectForeignToken,
8
8
  toInsufficientFundsError
9
- } from "./chunk-SVMGHASK.js";
9
+ } from "./chunk-ILPABTI2.js";
10
10
 
11
11
  // src/drivers/aptos/index.ts
12
12
  import { Aptos, AptosConfig, Network, AccountAddress } from "@aptos-labs/ts-sdk";
@@ -37,7 +37,14 @@ var PaymentTimeoutError = class extends PipRailError {
37
37
  };
38
38
  var MaxRetriesExceededError = class extends PipRailError {
39
39
  code = "MAX_RETRIES_EXCEEDED";
40
- /** The already-broadcast proof ref — recover with it, don't re-pay. */
40
+ /**
41
+ * The proof ref — recover with it, don't re-pay. Its meaning depends on the
42
+ * scheme: for `onchain-proof` it's the already-broadcast transaction ref
43
+ * (re-verify or re-submit it). For a standard `exact` rail it's the EIP-3009
44
+ * authorization NONCE (a `0x…` 32-byte value, NOT a tx hash) — re-PRESENT the
45
+ * same signed authorization, never re-sign a fresh nonce; check the token's
46
+ * `authorizationState(from, nonce)` before assuming it didn't settle.
47
+ */
41
48
  ref;
42
49
  constructor(message, options) {
43
50
  super(message, options);
@@ -46,6 +53,12 @@ var MaxRetriesExceededError = class extends PipRailError {
46
53
  };
47
54
  var PaymentDeclinedError = class extends PipRailError {
48
55
  code = "PAYMENT_DECLINED";
56
+ /** Why it was declined, as a typed enum (a hint; `.code` is the reliable channel). */
57
+ reasonCode;
58
+ constructor(message, options) {
59
+ super(message, options);
60
+ this.reasonCode = options?.reasonCode;
61
+ }
49
62
  };
50
63
  var ConfirmationTimeoutError = class extends PipRailError {
51
64
  code = "CONFIRMATION_TIMEOUT";
@@ -59,6 +72,9 @@ var InvalidEnvelopeError = class extends PipRailError {
59
72
  var NoCompatibleAcceptError = class extends PipRailError {
60
73
  code = "NO_COMPATIBLE_ACCEPT";
61
74
  };
75
+ var UnsupportedSchemeError = class extends PipRailError {
76
+ code = "UNSUPPORTED_SCHEME";
77
+ };
62
78
  var NonReplayableBodyError = class extends PipRailError {
63
79
  code = "NON_REPLAYABLE_BODY";
64
80
  };
@@ -168,6 +184,7 @@ export {
168
184
  SettlementError,
169
185
  InvalidEnvelopeError,
170
186
  NoCompatibleAcceptError,
187
+ UnsupportedSchemeError,
171
188
  NonReplayableBodyError,
172
189
  WrongFamilyError,
173
190
  UnknownTokenError,