@piprail/sdk 2.16.2 → 3.1.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 (37) hide show
  1. package/CHANGELOG.md +448 -0
  2. package/README.md +1 -0
  3. package/dist/{algorand-Q3TUQLPK.cjs → algorand-4FTTEV7X.cjs} +212 -51
  4. package/dist/{algorand-25FMBCT3.js → algorand-KJ5XHSMT.js} +164 -3
  5. package/dist/{aptos-VBJONBFY.cjs → aptos-5QBXJ6NV.cjs} +255 -39
  6. package/dist/{aptos-SBV6SGDP.js → aptos-H5VLH2QL.js} +217 -1
  7. package/dist/{chunk-QONQSZHJ.cjs → chunk-6XTNI2OQ.cjs} +44 -44
  8. package/dist/{chunk-OXEFPLZA.cjs → chunk-6ZRAIQXF.cjs} +4 -4
  9. package/dist/chunk-GVCGUSTE.js +30 -0
  10. package/dist/{chunk-V2IJ5HUW.cjs → chunk-MZAJQYM3.cjs} +1 -0
  11. package/dist/chunk-MZXVXC3C.cjs +30 -0
  12. package/dist/{chunk-2CX7XRZK.js → chunk-TZDVZCTC.js} +1 -0
  13. package/dist/{chunk-C52H5TYB.js → chunk-YXBQKBDH.js} +2 -2
  14. package/dist/index.cjs +2906 -1262
  15. package/dist/index.d.cts +679 -21
  16. package/dist/index.d.ts +679 -21
  17. package/dist/index.js +2678 -1034
  18. package/dist/{ledger-Crc1bZox.d.cts → ledger-DkHUORUe.d.cts} +7 -0
  19. package/dist/{ledger-Crc1bZox.d.ts → ledger-DkHUORUe.d.ts} +7 -0
  20. package/dist/{near-OLKCMTBI.js → near-5X2CQGML.js} +197 -1
  21. package/dist/{near-5LTTDU6G.cjs → near-XSQXEIHT.cjs} +242 -46
  22. package/dist/node.d.cts +2 -2
  23. package/dist/node.d.ts +2 -2
  24. package/dist/{solana-AI2G7V33.cjs → solana-TIEJV742.cjs} +218 -53
  25. package/dist/{solana-HTKDRTD3.js → solana-WRXL54MR.js} +174 -9
  26. package/dist/{stellar-E2KWEV2E.cjs → stellar-5GMZJBTA.cjs} +200 -30
  27. package/dist/{stellar-EUFZLX6J.js → stellar-YQLOIFDD.js} +172 -2
  28. package/dist/{sui-KCIITCYH.js → sui-JFQSDNSZ.js} +165 -3
  29. package/dist/{sui-Q3NJOJZS.cjs → sui-WIZEAO7E.cjs} +185 -23
  30. package/dist/{ton-J7TQWRN4.cjs → ton-AGTKLQA5.cjs} +264 -22
  31. package/dist/{ton-WH2JVQOO.js → ton-OZKFNRLT.js} +244 -2
  32. package/dist/{tron-FXBXDNEY.js → tron-HG3IOOCG.js} +283 -1
  33. package/dist/{tron-HIPMOX7S.cjs → tron-S4WX7OHY.cjs} +323 -41
  34. package/dist/{xrpl-RUOB37QH.js → xrpl-5K444RGX.js} +238 -6
  35. package/dist/{xrpl-SVVS445B.cjs → xrpl-G2FKFXRI.cjs} +289 -57
  36. package/package.json +12 -2
  37. package/dist/{chunk-QU25LSVS.js → chunk-5GRBEMCA.js} +44 -44
package/CHANGELOG.md CHANGED
@@ -4,6 +4,452 @@ 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
+ ## [3.1.0] — 2026-09-09 — spendable is not held, and a swap refused for no reason
8
+
9
+ ### Fixed
10
+
11
+ - 🔴 **A spend cap could be breached by CONCURRENT payments.** A cap is read when a quote is
12
+ priced and written when the payment settles, and a whole network round trip sits between the
13
+ two. Six simultaneous `fetch()` calls against a `maxTotal` of `'2.50'` each priced against the
14
+ same "spent so far", each passed, and four settled: 4.00 spent against a 2.50 leash while every
15
+ individual check was correct. `maxPayments` and `maxTotalPerDenom` leaked the same way. It is
16
+ the same read-await-write shape as the replay race, on the buyer's side of the wire, and the
17
+ leash is the entire safety story for the default `budgeted` mode.
18
+
19
+ `authorize()` now RESERVES the budget synchronously before anything is signed or sent, so an
20
+ in-flight payment is visible to the next one's check. `recordSpend()` commits the reservation
21
+ as the real record lands, and every failure path releases it, so a refused or failed payment
22
+ never permanently consumes the leash. Sequential behaviour is unchanged: a 3.00 cap still
23
+ spends exactly 3.00, no more and no fewer.
24
+
25
+ - 🔴 **A custom `isUsed`/`markUsed` replay store could redeem ONE proof N times, concurrently.**
26
+ The gate's built-in set has always reserved a ref synchronously, which is what stops two
27
+ simultaneous requests carrying the same proof from both settling. The CUSTOM-store branch did
28
+ not: it `await`ed `isUsed(ref)` before verification and wrote only after it, so every concurrent
29
+ request read "unused" and every one settled. Five simultaneous requests redeemed one payment
30
+ five times. The gate now takes the same synchronous in-process reservation BEFORE consulting a
31
+ custom store, so a single instance is safe whichever store is configured. Across several
32
+ processes that set cannot help, so an atomic check-and-reserve (Redis `SET NX`) is still what
33
+ protects a multi-instance deployment, and the documented example now uses one: it previously
34
+ showed `exists()` then `set()`, which is the exact non-atomic pattern the surrounding text
35
+ warned against. Found by a concurrency sweep; the suite had no concurrent replay test at all.
36
+
37
+ - 🔴 **A sovereign agent could deliver N goods for ONE payment.** `piprail_sell` gives every offer
38
+ its own gate, and a gate's replay set is scoped to itself, so cross-offer replay was guarded by
39
+ a store-level `isUsed`/`markUsed` pair sharing the race above. Sequentially it refused correctly;
40
+ five concurrent `piprail_collect` calls against five offers all collected the same settlement,
41
+ which is the normal shape of a shop with more than one buyer. `collect` now reserves the proof
42
+ ref synchronously before any await, and releases it if the payment does not settle.
43
+
44
+ - **A chain id that is not a chain id is refused at config time.** `{ id: NaN }` (and negative,
45
+ fractional or unsafe-integer ids) resolved happily, and the gate went on to publish
46
+ `network: "eip155:NaN"` in a live 402 — an unparseable CAIP-2 that a standard x402 client cannot
47
+ read. It failed closed later, but surfaced as `tx_not_found` at payment time rather than as the
48
+ configuration error it is.
49
+
50
+ - **A gate must charge more than zero.** `amount: '0'` built a rail whose amount check any transfer
51
+ satisfies, so a paywall could read as configured and gate nothing. A metered `upto` SETTLE of
52
+ zero stays legitimate, because that is the settled amount and not the advertised one.
53
+ `piprail_sell` inherits the same floor, so a zero price can no longer mint an offer.
54
+
55
+ - **A facilitator's cold start no longer costs the buyer gas.** The lazy `/supported` probe that
56
+ discovers a facilitator's fee payer timed out at 8s. These are serverless hosts: measured
57
+ 2026-09-09, `x402.dexter.cash` answered in **8497ms cold** and ~310ms warm, so a cold facilitator
58
+ read as absent, the gasless `exact` rail was dropped, and the buyer paid gas instead. The probe
59
+ now allows 15s. It only runs for a family that cannot resolve `exact` without a fee payer
60
+ (Solana; EVM never reaches it), at most once per gate, and it still fails safe with the same
61
+ `skipReason` when the host really is down. Live re-probe after the change: **9/9 registry hosts
62
+ answering, 0 contradicted claims**.
63
+
64
+ - **Authority is sealed on the client instance.** `paymentTools()` picks a model's tool set from
65
+ `canAgentSell()` / `canAgentSwap()`, which read `mode()`. Those were plain prototype methods, so
66
+ any code holding the client could reassign one and turn a budgeted client's eight tools into
67
+ sovereign's fourteen. A MODEL could never do that — it sends JSON tool arguments and does not
68
+ hold the object — so this is defence in depth for a client passing through an agent framework,
69
+ a plugin, or middleware that wraps objects. All three are now non-writable and non-configurable:
70
+ authority is set once, by whoever provisioned the key.
71
+
72
+ - 🔴 **`planPayment` called a native payment affordable that the chain then refused, on FOUR
73
+ families.** Some chains require an account to retain a minimum it can never send, and
74
+ affordability was measured against the raw balance on every one of them:
75
+
76
+ | | retained minimum |
77
+ |---|---|
78
+ | Solana | the account's rent-exempt minimum |
79
+ | XRPL | a base reserve, plus an increment for each owned object (a trustline is one) |
80
+ | Stellar | `(2 + subentries) x` the base reserve, and a trustline is a subentry |
81
+ | Algorand | 0.1 ALGO, plus 0.1 for every ASA opted into |
82
+
83
+ A live wallet holding 0.0011 SOL was told it could send 0.0005 SOL; the transfer failed
84
+ simulation with a bare `SendTransactionError` after the agent had already signed, which is the
85
+ exact outcome the pre-flight check exists to prevent.
86
+
87
+ `WalletBalance.token` is now documented as the SPENDABLE figure (the reserve deducted) with
88
+ `native` staying the true balance for gas, all four drivers report it that way (reading the
89
+ chain's own figure where it offers one), and the client measures a native payment against it.
90
+ A family with no reserve reports the two as equal and is unchanged. The refusal names the
91
+ exact shortfall, and a payment comfortably inside the spendable balance still goes through.
92
+
93
+ - **XRPL: a throttled ledger read no longer surfaces as a serializer error.** `Sequence` and
94
+ `LastLedgerSequence` are UInt32 fields, so a failed or rate-limited pre-flight read left one
95
+ `undefined` (and `undefined + 20` is `NaN`), and xrpl.js rejected the transaction with
96
+ "Cannot construct UInt32 from given value": no field, no cause and no remedy, from a library
97
+ the caller never imported. Found under a batch of live mainnet payments, where the public
98
+ cluster throttles. Both reads are now checked before the transaction is built, and the refusal
99
+ says the thing that matters most on an ambiguous payment error: nothing was signed or
100
+ submitted, so retrying cannot double-pay.
101
+
102
+ ### Added
103
+
104
+ - **`releaseUsed` — the third replay hook.** Optional, and only meaningful when `isUsed` RESERVES
105
+ (the `SET NX` shape). Without it, making a custom store atomic traded a double-spend for a worse
106
+ failure: a transient RPC error left the reservation standing, so a buyer whose funds had already
107
+ moved could never redeem the proof. The built-in store has always released on failure; this is
108
+ how a custom store does the same. Supplying it without `isUsed`/`markUsed` throws, because it
109
+ would never fire.
110
+
111
+ ## [3.0.0] — 2026-09-09 — agent modes: a wallet an agent can EARN with, not only spend from
112
+
113
+ ### BREAKING
114
+
115
+ - **`ResolvedNetwork` gains a required `addressOf(wallet)`.** Only affects code that implements
116
+ a CUSTOM driver via `registerDriver`; every built-in family already has it. The migration is one
117
+ method: return the bound wallet's own address, derived from the key, with no RPC read. Nothing
118
+ else in the public API changed, and no existing call site needs touching.
119
+
120
+ ### Added
121
+
122
+ - **Agent modes: capability follows AUTHORITY, and `sovereign` gives an agent BOTH halves of a
123
+ wallet.** A new `mode` option on `PipRailClient` (`'supervised' | 'budgeted' | 'sovereign'`,
124
+ default `'budgeted'`) and `PIPRAIL_MODE` on the MCP server. **The default is unchanged**: omit
125
+ it and `paymentTools()` returns exactly the same eight tools, in the same order, so nothing
126
+ moves for anyone who does not opt in.
127
+
128
+ Before this, whether a model could swap depended on which PACKAGE it imported: a model driving
129
+ `@piprail/mcp` never could, while the same wallet driven through the SDK always could. That is
130
+ the wrong axis. `mode` moves the decision onto the only question that matters, which is who is
131
+ answerable for the wallet, and it is set by whoever provisions the key. A model can never set
132
+ it: `mode()` reads, nothing writes, and no tool takes a `mode` argument.
133
+
134
+ ⭐ **`'sovereign'` is the whole wallet, not a bigger allowance.** A budget only ever lets an
135
+ agent SPEND, however large the cap. Sovereign appends five tools: `piprail_quote_swap` and
136
+ `piprail_swap`, plus the seller tools **`piprail_sell`**, **`piprail_collect`** and
137
+ **`piprail_earnings`**, so an agent can be PAID as well as pay. `piprail_sell` prices something
138
+ and returns an x402 challenge; because a challenge is only data, the agent hands it to a buyer
139
+ over any channel it already has, with **no web server and no open port anywhere**.
140
+ `piprail_collect` verifies the returned proof against the chain, and only `paid: true` clears
141
+ delivery. Each proof is single-use, so a replay is never a second sale.
142
+
143
+ **Two properties make selling safe to grant a model where spending is not.** The receiving side
144
+ holds **no private key at all** (`payTo` is a public address), so the earning half cannot spend
145
+ and cannot be drained even if the host is taken. And `payTo` defaults to the agent's **own**
146
+ address, which it otherwise has no way to learn, so it is never told to trust one a buyer sent.
147
+
148
+ Swapping is bounded by a separate instrument, because the payment caps cannot bound it: a swap
149
+ moves the holder's own funds between denominations, so `maxAmount`, `maxTotal`, the count caps
150
+ and the session TTL all pass over it untouched. `swapPolicy` adds a ceiling per swap, a slippage
151
+ ceiling and an optional destination allowlist, and the ceiling binds on the quote's on-chain
152
+ `maxSpend`, never the estimate. The MCP server **refuses to boot** on
153
+ `PIPRAIL_MODE=sovereign` without `PIPRAIL_MAX_PER_SWAP`, so an operator finds out while reading
154
+ their own config rather than mid-spend. Selling needs no ceiling, because it takes money rather
155
+ than spending it.
156
+
157
+ - **`client.balanceOf(assets)` + the `piprail_wallet` tool: an agent can finally see its own
158
+ balance sheet.** A sovereign agent had thirteen tools and could not answer *"what do I have?"* —
159
+ `budget()` reports how much of an allowance is left, which is a different question, and
160
+ `planPayment()` answers it only for one URL at a time. Owning finances starts with seeing them.
161
+ An unavailable read reports `null`, **never `0`**: an agent told it holds nothing when the RPC
162
+ merely failed would fire-sale to recover from a loss that never happened. On a `MultiChainPayer`
163
+ the holdings span every chain, because reporting only the primary would call an agent broke
164
+ while it held funds one client along.
165
+
166
+ - **`ResolvedNetwork.addressOf(wallet)` and `client.address()`: a wallet can finally report where
167
+ it gets PAID.** Every driver already derived this internally for `balanceOf`; nothing exposed
168
+ it. Without it an agent handed a key it never chose has no way to learn its own address, so it
169
+ could pay for anything and be paid for nothing. Implemented across all ten families. Pure:
170
+ derived from the key, no RPC read, nothing moved. Also `client.chain()`, and `canAgentSell()`
171
+ alongside the existing `canAgentSwap()`. `MultiChainPayer` delegates all of them to the primary
172
+ client, without which sovereign mode would be unreachable through the MCP, which always wraps
173
+ its accounts in one.
174
+
175
+ ### Fixed
176
+
177
+ - **An agent paying twice in a row collided with itself on EVM.** Found live on Base: the first
178
+ payment settled and the next two were refused by the node with "nonce lower than the current
179
+ nonce of the account", because viem asks the RPC for the nonce on every send and two payments
180
+ issued back to back both read the same value before either was mined. It failed SAFELY (nothing
181
+ broadcast, nothing recorded as spent, `send` called exactly once with no retry), so it never
182
+ risked a double payment, but paying several times in quick succession is the ordinary behaviour
183
+ of an autonomous agent rather than an edge case. A raw `{ key }` is now bound through viem's
184
+ `nonceManager`, which assigns nonces locally per (chain, account) and reconciles with the chain.
185
+ A caller who brings their own `walletClient` keeps their own nonce policy, untouched. The same
186
+ three back-to-back Base payments now all settle.
187
+
188
+ - **TON reported a BOUNCEABLE address as "where I get paid".** `addressOf` was added to all ten
189
+ families at once, and checking each against the address recorded in its funded test wallet came
190
+ back 9/10: TON derived `EQ…` where the wallet file (and every TON wallet UI) says `UQ…`, the
191
+ same account in the other encoding. A bounceable address returns funds to the sender when the
192
+ destination contract is not yet initialised, and a wallet contract stays uninitialised until it
193
+ has sent its first transaction, so a freshly generated agent advertising `EQ…` would have
194
+ bounced the first payment it was ever sent. Verification was never affected (both forms parse to
195
+ one account); what was wrong is the address an agent hands out. Now 10/10 against ground truth.
196
+
197
+ - **🔴 One payment could collect TWO offers.** Found by attacking the seller tools. Each offer
198
+ owns its own gate, and a gate's replay set is scoped to itself, which is right for
199
+ `requirePayment` (one gate, one resource) and wrong for a store of offers: two offers priced the
200
+ same to the same address are indistinguishable to any driver, because a driver can only ask "did
201
+ this settlement move at least X to this address?". A buyer could pay for a haiku and collect the
202
+ expensive listing with the same money. Two bindings now close it, both mutation-proven and
203
+ re-verified on mainnet: `collect` refuses a proof minted for a different offer
204
+ (`code: 'wrong_offer'`) before the chain is consulted, and the used-proof set belongs to the
205
+ SELLER rather than to each gate, so one settlement is dead everywhere once redeemed.
206
+
207
+ - **`mode: 'supervised'` supervised nothing, and `PIPRAIL_MODE=supervised` wired nothing.** The
208
+ mode↔confirm inference ran ONE WAY: `PIPRAIL_CONFIRM=1` meant supervised, but naming the mode
209
+ did not turn confirmation on. An operator who asked for a human in the loop got an agent that
210
+ paid without asking anybody, with the banner, the docs and the mode name all agreeing it was
211
+ supervised. Both directions now agree; a config that says both things at once is refused rather
212
+ than quietly resolved; and the SDK refuses `mode: 'supervised'` with no `onBeforePay` to perform
213
+ it. `sovereign` + confirmation is not a contradiction and stays allowed.
214
+
215
+ - **A SWAP was invisible to the approver.** `onBeforePay` never sees a swap, because a swap is not
216
+ a payment, which is the whole reason `swapPolicy` bounds it instead. But an operator who wired
217
+ an approver did not mean "ask me before payments and let value move silently any other way", so
218
+ a supervised sovereign agent could swap its whole balance without one prompt. New
219
+ **`onBeforeSwap`**, same fail-safe contract, wired automatically by `@piprail/mcp` whenever
220
+ confirmation is on. Proven against the bug: a refusing approver was never called and the swap
221
+ executed.
222
+
223
+ - **The SDK let `'sovereign'` hand a model the swap tools with NO ceiling.** `@piprail/mcp`
224
+ already refused to boot in that state, so the safer surface could be sidestepped by importing
225
+ the other one. Proven before the fix: a 99,999-unit swap executed with nothing refusing it.
226
+ `mode: 'sovereign'` now requires `swapPolicy.maxPerSwap` in the SDK too.
227
+
228
+ - **The MCP banner under-reported what the agent could do.** It printed a hardcoded eight tools
229
+ even in sovereign mode, where the model actually holds thirteen. It is now handed the real list
230
+ at startup, so an operator can see the capability they granted.
231
+
232
+ - **Docs claimed the MCP has no swap tool "and there will not be one".** True when written and
233
+ false since modes shipped, in `mcp/README.md` and the tools reference. Both now describe the
234
+ mode-dependent contract, and a sync rule pins it so the claim cannot rot again.
235
+
236
+ - **Swapping reaches every family: Aptos, TON and Tron close the last three gaps.** Seven routes
237
+ became **ten, across 18 chains, with 21 mainnet proofs**. Nothing about the existing routes
238
+ changed, and swapping stays opt-in.
239
+
240
+ **Aptos via Hyperion** and **Tron via SunSwap V2** need **no API at all**: the quote is an
241
+ on-chain read and the swap is a contract call the user's own key signs. There is no host to go
242
+ down, no key to rotate and no vendor in the middle. That was deliberate on Tron, whose own front
243
+ end talks to an undocumented, obfuscated hostname a payments SDK should not depend on. **TON via
244
+ STON.fi** uses a keyless REST simulation and one message the wallet signs.
245
+
246
+ ⭐ **Three of them are exact-output natively**, which no earlier route was. Hyperion's
247
+ `exact_output_swap_entry`, SunSwap's `swapTokensForExactTokens` and STON.fi's `reverse_swap`
248
+ all take the invoice amount and cap the input **on-chain**, so an x402 invoice is priced from
249
+ its output rather than sized from a probe. Proven to the unit on Aptos: four swaps delivered
250
+ exactly 0.05 USDT, 0.04 USDC, 0.03 USDC and 0.02 APT. On TON the guarantee is a floor rather
251
+ than a point: STON.fi's `reverse_swap` fixes the ask side, and PipRail pads the request so the
252
+ router's on-chain `min_ask_units` is at or above the invoice. Slightly more can arrive; less is
253
+ refused by the router.
254
+
255
+ **Live-proven on mainnet**: four Aptos swaps (both stable directions plus native APT in and out)
256
+ and two TON swaps (native in, jetton in, which the simulation routed through two *different*
257
+ router versions). **Tron ships WITHOUT a proof and says so on the site and in the docs**: the
258
+ quote is verified live and the swap executes cleanly in simulation, but Tron charges about
259
+ 230,629 ENERGY per swap, which without staked energy is roughly 23 TRX (~$7.79) *regardless of
260
+ trade size*, and the test wallets hold 8.1 TRX. A new `unproven` field carries that reason, and
261
+ a guard test fails if a route ever ships with neither a proof nor a stated reason.
262
+
263
+ Adds two OPTIONAL peer dependencies for TON only (`@ston-fi/sdk`, `@ston-fi/api`, both MIT and
264
+ lazily imported); the lazy-chunk invariant still holds, so a pure-EVM install downloads neither.
265
+ Aptos and Tron needed no new dependency at all.
266
+
267
+ **Four more bugs found the same way as always, by spending real money.** Aptos aborted with
268
+ `ESQRT_PRICE_LIMIT_UNAVAILABLE` because a concentrated-liquidity swap needs a directional price
269
+ bound and `0` is not one; the bounds are now read off the chain rather than copied from another
270
+ DEX. The entry function's two amount slots are "what leaves, what arrives", **not** "the exact
271
+ one, the limit", and having them backwards aborts with `EINSUFFICIENT_BALANCE`, which reads like
272
+ a funding problem and is not one. A pool's `token0` field comes back as all-zeros for native-APT
273
+ pools, so the direction is now derived by comparing addresses. And pricing every fee tier blindly
274
+ burned the public node's anonymous compute quota (40k units per 300s), after which good routes
275
+ came back as "no route" — the quote now cheap-filters empty pools before paying to price them.
276
+
277
+ - **Robinhood Chain (`chain: 'robinhood'`, eip155:4663) — the 30th chain, and the first with a
278
+ Paxos-issued stablecoin.** The tokenized-equity Arbitrum Orbit L2, mainnet since 2026-07-01,
279
+ running 100ms blocks and settling to Ethereum. viem ships no preset and it is absent from
280
+ chainid.network, so the chain is defined inline from the values Robinhood publishes.
281
+
282
+ **One stablecoin on purpose: USDG**, the Paxos Global Dollar, which is what the chain's own
283
+ markets quote against. Its address was agreed by four independent sources before shipping —
284
+ Paxos's published list, our own on-chain read (`Global Dollar`, 6dp), and both facilitators'
285
+ `/supported`. **Circle issues no native USDC here** (checked against Circle's own contract
286
+ list), so no USDC or USDT preset ships: a bridged one would break the issuer-native token rule.
287
+ Native ETH is a payment asset as on every EVM chain.
288
+
289
+ **Gasless from day one, proven twice.** USDG is EIP-3009 **and** EIP-2612, verified by calling
290
+ the contract rather than by scanning bytecode — a bytecode scan said neither was present, and
291
+ was wrong. Its EIP-712 domain is `name: "Global Dollar", version: "1"`, which we confirmed by
292
+ recomputing the on-chain `DOMAIN_SEPARATOR`. Two keyless facilitators already settle there and
293
+ **both were live-settled by us**, with buyer *and* merchant paying zero ETH:
294
+ Ultravioleta DAO (tx `0x35361e57…`) and Dexter (tx `0x1ba66e3d…`, permit2, ~$0.0066 floor).
295
+ A self-settled EIP-3009 round trip also passed end to end (tx `0xe6d35460…`): 402 → sign →
296
+ settle → 200, replay rejected as `tx_already_used`, payer ETH delta exactly zero.
297
+
298
+ **Swapping works there too**, through the existing KyberSwap route — native ETH → 0.360518 USDG,
299
+ tx `0x424299e6…`. Worth knowing: at 100ms blocks a route goes stale fast, and the default
300
+ 0.5% slippage reverted before 3% cleared. Reaching the chain at all is a bridge; Relay moved
301
+ ETH from Base in about three seconds for roughly $0.04, and Across and LI.FI both list it.
302
+
303
+ - **`quoteSwap()` / `swap()`: an optional helper for when you hold the wrong token.** A 402 names a
304
+ token, a wallet holds what it holds, and when those disagree `planPayment()` reported
305
+ `INSUFFICIENT_TOKEN` and stopped. This turns that dead end into an option the caller may take or
306
+ ignore.
307
+
308
+ **It is opt-in and it stays that way.** Paying never swaps, planning never swaps, and there is
309
+ deliberately no `autoSwap` flag: converting one asset into another is a priced, irreversible act,
310
+ and a payment library should not do it on your behalf because it noticed you were short. A test
311
+ asserts the byte-identical default (STANDARDS §0), so "not using it costs you nothing" is machine
312
+ checked rather than promised in prose.
313
+
314
+ Ships in **two tiers, kept apart by the type system**. Tier 1 (`source.kind: 'protocol'`) is
315
+ Stellar (`PathPaymentStrictReceive` to your own account) and the XRP Ledger (a cross-currency
316
+ `Payment` to your own address, auto-bridged through XRP): the ledger itself swaps, so there is
317
+ **no third party, no API key, no extra dependency and no integrator fee**. Tier 2
318
+ (`source.kind: 'provider'`) is **Solana via Jupiter**, **9 live-probed EVM chains via KyberSwap**,
319
+ **Sui via Aftermath**, **NEAR via Ref Finance**, **Algorand via Vestige**, **Aptos via
320
+ Hyperion**, **TON via STON.fi** and **Tron via SunSwap V2**, because those chains have no
321
+ protocol-level swap. **Ten routes across 18 of the 30 chains.** On a chain with no route,
322
+ `quoteSwap()` answers `null` and `swap()` throws `UnsupportedNetworkError` naming every venue
323
+ that does exist, read from the registry rather than typed into the message.
324
+
325
+ NEAR uses **Ref Finance rather than NEAR Intents**, deliberately: Intents puts a solver in
326
+ possession of the funds mid-flight, which is the intermediary this project exists to remove. Ref
327
+ is one `ft_transfer_call` receipt chain inside a single transaction the user signs.
328
+
329
+ The **swap coverage map is data** (`SWAP_PROVIDERS`), shaped like `KNOWN_FACILITATORS` and
330
+ carrying the same admission rule. The website table is generated from it, a sync rule and a guard
331
+ test hold the logos, site data and docs to it, and a new `swaps` domain documents the update order.
332
+
333
+ **Providers had to earn their place.** Each was probed live from a plain keyless server request:
334
+ KyberSwap 200 ✅, Jupiter 200 with `platformFee: null` ✅, while 0x (401), 1inch (401), Odos (530),
335
+ OpenOcean (403), Squid (400), Rango (401) and thirdweb Bridge (401) all failed and were rejected.
336
+ PipRail never sets a platform or integrator fee field on any provider.
337
+
338
+ **Proven with 21 real mainnet swaps across 9 chains**, both tiers, both directions on every
339
+ chain that has two, each verified by reading the transaction back from a public node, and
340
+ re-verifiable any time with `npm run verify:proofs`. The full table with hashes is in the docs. One known failure is recorded rather than hidden: FDUSD→USDC
341
+ on BNB reverts with `TRANSFER_FROM_FAILED` despite confirmed balance and allowance, and is
342
+ documented as unexplained.
343
+
344
+ **Live testing found nine real bugs, all fixed**, every one of which passed a typechecker and a
345
+ unit suite first: some tokens revert on a non-zero to non-zero ERC-20 `approve` (the allowance is
346
+ now zeroed first); XRPL path steps carry fields xrpl.js cannot always encode (now stripped);
347
+ XRPL public path finding is intermittent, returning a route then nothing with no error (now
348
+ retried); XRPL float precision silently discarded valid routes, making a whole direction look
349
+ unsupported (now parsed with a ceiling); and a malformed Stellar issuer threw out of a
350
+ never-throw method while one unparseable Horizon candidate could poison a valid quote. Four more
351
+ came from the new families, and **four of the nine made an entire chain look unsupported when it
352
+ was not**: setting `accept-encoding` by hand defeated fetch's gzip handling and broke NEAR
353
+ outright; NEAR pool ids are strings in the indexer and numbers on the contract; Vestige returns
354
+ `amount_out: 0` for a dust probe rather than an error, so a fixed probe size reported no route on
355
+ a pair that routes fine; and Sui's public JSON-RPC is being deprecated.
356
+
357
+ **The no-fee claim is self-verifying.** Rather than hardcoding it, every quote inspects the
358
+ provider's own fee fields and the note SAYS SO if one ever appears, instead of repeating a claim
359
+ that has quietly become false.
360
+
361
+ 🔴 **PipRail still runs no price oracle** (STANDARDS §7). A swap rate is a price, so every
362
+ `SwapQuote` carries a `source` naming who produced it: `kind: 'protocol'` when the ledger's own
363
+ order books priced it (Stellar, XRPL), `kind: 'provider'` when a named venue did. The distinction
364
+ lives in the type system, not in prose.
365
+
366
+ Slippage rides **on-chain** (`sendMax` / `SendMax`, `amountInMax`, `amount_in_max`, TON's
367
+ `min_ask_units` floor), so a market that moves past your tolerance fails the transaction rather
368
+ than overspending. Nothing is swapped in that case; chains that charge for a reverted
369
+ transaction (EVM, Aptos, Tron) still take the gas. Default 0.5%, ceiling 10%, integer maths
370
+ that rounds in the user's favour, and a malformed `slippageBps` throws `RangeError` before any
371
+ read happens rather than coming back as a silent `null`.
372
+
373
+ New exports: `SwapRequest`, `SwapQuote`, `SwapReceipt`, `SwapSide`, `SwapQuoteSource`,
374
+ `summarizeSwap`, `resolveSlippageBps`, `applySlippage`, `DEFAULT_SLIPPAGE_BPS`,
375
+ `MAX_SLIPPAGE_BPS`. Docs: [Swapping tokens](https://docs.piprail.com/making-payments/swapping/).
376
+
377
+ Surveyed and deliberately **not** bundled: thirdweb Bridge, LI.FI, Squid, Relay, deBridge,
378
+ Across, Rango. thirdweb Bridge as the worked example requires an API key at the type level,
379
+ charges a 0.30% protocol fee an integrator cannot disable, and is EVM-only for cross-chain
380
+ routing, so nine of PipRail's ten families cannot even be expressed in its API.
381
+
382
+ ### Fixed
383
+
384
+ - **Tron could not have sold a token at all.** SunSwap V2's router moves the input with
385
+ `transferFrom` and the driver had no approve step, so native-TRX-in worked while every TRC-20
386
+ direction would have failed on allowance after the energy was burned. Nothing caught it
387
+ because the route ships without a mainnet proof. Adding the approve then exposed a second bug:
388
+ a fresh allowance slot measures **99,764 energy ≈ 9.98 TRX**, and the approve ceiling had been
389
+ set to 10 TRX because "an approve is cheap". Both paths are simulated against Tether's real
390
+ Tron contract and pinned by tests; a token-in swap now budgets roughly 33 TRX.
391
+
392
+ - **Both shipped TON proofs pointed at the gas refund, not the swap.** STON.fi returns unused
393
+ forward gas to the same wallet a second after the swap, and the driver reported whichever
394
+ wallet transaction was newest, so the recorded reference was an *incoming* `excesses` message
395
+ with no outgoing message at all. The swaps were real; the evidence pointed at the wrong leg. The
396
+ driver now reports the newest transaction the wallet itself signed (`external-in`), the registry
397
+ carries the corrected hashes, and `verify:proofs` rejects a TON reference of the wrong shape.
398
+
399
+ - **STON.fi's "reverse" simulation was not exact-output.** Asked for 50000 at 1% tolerance it
400
+ returns an on-chain floor of 49500, which the router will deliver, short-paying an invoice. The
401
+ request is now padded so the floor is at or above the invoice, the quote is refused when it is
402
+ not, and every number the swap later parses is validated in the never-throw quote.
403
+
404
+ - **Aptos paid the account that quoted, not the one that signed.** The recipient was captured at
405
+ quote time and reused at swap time; every other family derives it from the signing wallet.
406
+
407
+ - **`quoteSwap()` swallowed a malformed `slippageBps` into `null` on a read-only client** because
408
+ validation ran after the wallet check. It now runs first, so a caller's bug throws `RangeError`
409
+ regardless of what else is wrong.
410
+
411
+ - **A mined transaction is not a successful swap — `swap()` could report success for a swap that
412
+ failed.** On EVM, `waitForTransactionReceipt` resolves for a **reverted** transaction exactly as
413
+ it does for a mined one; the driver returned unconditionally, so a reverted swap handed back a
414
+ `SwapReceipt` carrying a real transaction hash while nothing had moved. On Solana the same class
415
+ of hole existed one step earlier: `sendRawTransaction` returns once the RPC accepts the bytes, so
416
+ a transaction that then failed on-chain was also reported as a completed swap. Both now check and
417
+ throw `InsufficientFundsError`. This is the worst failure shape available to a payments SDK — an
418
+ agent believes it holds the token and goes on to pay an invoice it cannot cover — and it affected
419
+ **every EVM chain and Solana**, not one chain. Found on Robinhood Chain, whose 100ms blocks stale
420
+ a route often enough to surface it; Sui and NEAR already checked their status correctly.
421
+
422
+ - **A strict x402 v2 facilitator refused every PipRail settlement, and we read it as a missing
423
+ chain.** PipRail sent a v2 body carrying only the v1 `paymentRequirements` key. A facilitator
424
+ that validates the v2 envelope needs `accepted` and `resource` at the request root, and answered
425
+ `400 data did not match any variant of untagged enum VerifyRequestEnvelope`. A v2 body now carries
426
+ both spellings, so the older facilitators keep reading `paymentRequirements` while a strict v2
427
+ facilitator gets what it requires. `resource` comes from the merchant's own gate config, never the
428
+ client's echo, and is always sent complete because the field is required rather than optional.
429
+
430
+ The cost of the bug was not the 400 itself. It was recorded in `facilitators.ts` for months as
431
+ "Ultravioleta DAO's NEAR listing is aspirational, not settle-capable" — a conclusion about
432
+ somebody else's implementation drawn from an error about our own request.
433
+
434
+ ### Added
435
+
436
+ - **NEAR is live on a keyless facilitator, the first x402 facilitator settlement on `near:mainnet`.**
437
+ Ultravioleta DAO settles the NEP-141 USDC rail through a NEP-366 meta-transaction with the buyer
438
+ paying zero NEAR (tx `8pVe4oeykCT2WpCwToNbkxjdgSvwNNtJUrGezDCHKDda`), so `exact: true` is now
439
+ zero-config gasless on NEAR.
440
+ - **Ultravioleta DAO on Avalanche**, buyer holding zero AVAX
441
+ (tx `0xe9a88e4fa978a16599b6188444d87ea63d43122e5fad16493568c2a596a79436`).
442
+ - NEAR receipts are now re-verifiable by the facilitator-probe tx checker and link to an explorer on
443
+ piprail.com. Both tools matched only EVM, Solana and Algorand hash shapes, so a NEAR receipt would
444
+ have shipped as unlinked, unchecked text.
445
+
446
+ ### Notes
447
+
448
+ - A sweep of all 21 mainnets Ultravioleta DAO advertises found two more it does **not** settle:
449
+ Solana (`/verify` returns `isValid: true`, then `/settle` fails `contract_call_failed`) and
450
+ Algorand (`/verify` cannot deserialize an Algorand `exact` payload). Both are locked by tests so
451
+ the next sweep does not re-run them. Advertised is not settled — only a receipt is.
452
+
7
453
  ## [2.16.2] — 2026-09-06 — every facilitator now carries a verifiable receipt
8
454
 
9
455
  ### Changed
@@ -2196,6 +2642,8 @@ straight into your wallet. The API is small and self-contained.
2196
2642
  [1.5.0]: https://www.npmjs.com/package/@piprail/sdk
2197
2643
  [1.4.0]: https://www.npmjs.com/package/@piprail/sdk
2198
2644
  [1.3.1]: https://www.npmjs.com/package/@piprail/sdk
2645
+ [3.1.0]: https://www.npmjs.com/package/@piprail/sdk
2646
+ [3.0.0]: https://www.npmjs.com/package/@piprail/sdk
2199
2647
  [1.3.0]: https://www.npmjs.com/package/@piprail/sdk
2200
2648
  [1.2.0]: https://www.npmjs.com/package/@piprail/sdk
2201
2649
  [1.1.1]: https://www.npmjs.com/package/@piprail/sdk
package/README.md CHANGED
@@ -78,6 +78,7 @@ The same app can **take** payments and **make** them. → [Making payments](http
78
78
  | **[Getting started](https://docs.piprail.com/getting-started/introduction/)** | Install · quickstart · how it works |
79
79
  | **[Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)** | `requirePayment` · `createPaymentGate` · [presets](https://docs.piprail.com/accepting-payments/merchant-presets/) (`createPaywall` / `createTipJar`) · [framework adapters](https://docs.piprail.com/accepting-payments/framework-adapters/) · the `exact` rail · the `upto` metered rail |
80
80
  | **[Making payments](https://docs.piprail.com/making-payments/piprail-client/)** | `PipRailClient` · `quote` · `estimateCost` · `planPayment` · auto-route · `MultiChainPayer` |
81
+ | **[Swapping tokens](https://docs.piprail.com/making-payments/swapping/)** | `quoteSwap` · `swap` — OPTIONAL, opt-in, never automatic. Keyless routes only, no fee, no API key |
81
82
  | **[Verifiable receipts](https://docs.piprail.com/accepting-payments/verifiable-receipts/)** | Chain-grounded, anyone-verifiable receipts (no key) · optional EIP-712 attestation |
82
83
  | **[Spend controls](https://docs.piprail.com/spend-controls/payment-policy/)** | Per-token + cross-token grand total · payment-count caps · time envelope · durable budget · the spend ledger |
83
84
  | **[Agent toolkit](https://docs.piprail.com/agent-toolkit/payment-tools/)** | `paymentTools` · the agent guide · NL renderers |