@piprail/sdk 1.3.1 → 1.5.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/CHAINS.md +31 -7
- package/CHANGELOG.md +74 -0
- package/ERRORS.md +13 -2
- package/README.md +46 -12
- package/dist/algorand-B67G4335.js +397 -0
- package/dist/algorand-IJJKE35X.cjs +397 -0
- package/dist/{aptos-MKZ5MAGL.cjs → aptos-X3G2UBYW.cjs} +44 -16
- package/dist/{aptos-DTAONNMM.js → aptos-YQWTGFRZ.js} +29 -1
- package/dist/{chunk-YJPWIK5L.cjs → chunk-IQGT65WS.cjs} +4 -2
- package/dist/{chunk-AGKC3C7Y.js → chunk-QDS6FBZP.js} +4 -2
- package/dist/index.cjs +358 -67
- package/dist/index.d.cts +162 -4
- package/dist/index.d.ts +162 -4
- package/dist/index.js +308 -17
- package/dist/{near-YX3XOASO.js → near-7MBBCDUE.js} +51 -1
- package/dist/{near-DISWUB7Y.cjs → near-GGUHLXAF.cjs} +76 -26
- package/dist/{solana-37F2PR5H.js → solana-7WJVZGDW.js} +23 -1
- package/dist/{solana-RJPNEFSN.cjs → solana-W24TCJV4.cjs} +39 -17
- package/dist/{stellar-ALOVOMFD.js → stellar-HV6VGZX3.js} +51 -1
- package/dist/{stellar-SUGNX52Z.cjs → stellar-YMY3K2YB.cjs} +70 -20
- package/dist/{sui-OLC5ID4X.js → sui-2WFWVFJX.js} +24 -1
- package/dist/{sui-HZWPHVU4.cjs → sui-32KVESR5.cjs} +40 -17
- package/dist/{ton-NIDWF77T.js → ton-DGZB7W4U.js} +24 -1
- package/dist/{ton-C4KTFXDL.cjs → ton-FIQGV2LC.cjs} +37 -14
- package/dist/{tron-LPMK57H7.js → tron-RLIL2FDI.js} +29 -1
- package/dist/{tron-DTU7NPEM.cjs → tron-ZSXAPZ2C.cjs} +52 -24
- package/dist/{xrpl-N6ZAJRGC.cjs → xrpl-2PKP7HOI.cjs} +81 -21
- package/dist/{xrpl-6ODQS7JR.js → xrpl-UEC2GYVV.js} +61 -1
- package/package.json +9 -2
package/CHAINS.md
CHANGED
|
@@ -6,7 +6,8 @@ themselves differ, and a few have **setup steps you must do before a wallet can
|
|
|
6
6
|
receive**. This page is the exact list.
|
|
7
7
|
|
|
8
8
|
**Most chains need nothing special.** The ones with caveats are **NEAR**, **TON**,
|
|
9
|
-
**Stellar**, **XRPL**, and **
|
|
9
|
+
**Stellar**, **XRPL**, **Tron**, and **Algorand** (USDC needs a one-time ASA opt-in) —
|
|
10
|
+
read those sections before you ship them.
|
|
10
11
|
|
|
11
12
|
## At a glance
|
|
12
13
|
|
|
@@ -15,6 +16,8 @@ receive**. This page is the exact list.
|
|
|
15
16
|
| **EVM** (Ethereum, Base, Arbitrum, Optimism, Polygon, BNB, Avalanche, Mantle, Sonic, Linea, Scroll, Celo, zkSync, Unichain, World Chain, Sei, Injective, + any EVM chain) | ✅ ETH/BNB/POL/… | USDC (all) · USDT (all **except Base, World Chain, Sei**) | No | `{ privateKey }` |
|
|
16
17
|
| **Solana** | ✅ SOL | USDC · USDT | No (payer creates the recipient's token account) | `{ secretKey }` |
|
|
17
18
|
| **Sui** | ✅ SUI | USDC (no USDT) | No | `{ privateKey }` (`suiprivkey1…`) |
|
|
19
|
+
| **Aptos** | ✅ APT | USDC · USDT | No (primary FA store auto-creates) | `{ privateKey }` (`ed25519-priv-0x…`) |
|
|
20
|
+
| **Algorand** | ✅ ALGO | **USDC only** (Tether deprecated USDT) | USDC: ⚠️ **ASA opt-in** · **native ALGO: none** | `{ mnemonic }` (25 words) |
|
|
18
21
|
| **Stellar** | ✅ XLM | USDC · EURC | ⚠️ **Yes — trustline + funded account** | `{ secret }` (`S…`) |
|
|
19
22
|
| **XRP Ledger** | ✅ XRP | USDC · RLUSD (no USDT) | ⚠️ **Yes — trustline + activated account** | `{ seed }` (`s…`) |
|
|
20
23
|
| **TON** | ✅ TON | **USD₮ only** (no USDC) | No (payer's gas auto-deploys the jetton wallet) | `{ mnemonic }` (24 words) |
|
|
@@ -22,12 +25,14 @@ receive**. This page is the exact list.
|
|
|
22
25
|
| **NEAR** | ✅ NEAR | USDC · USDT | tokens: ⚠️ `storage_deposit` · **native NEAR: none** | `{ accountId, privateKey }` |
|
|
23
26
|
|
|
24
27
|
> **`token: 'native'`** (paying in the chain's own coin) is accepted on **every family** —
|
|
25
|
-
> EVM, Solana, Sui, Stellar, XRPL, TON, NEAR, **and Tron** (native TRX,
|
|
26
|
-
> exceptions. On NEAR, native is the **zero-setup** path: no `storage_deposit`,
|
|
27
|
-
> even creates a fresh recipient (the NEP-141 token path still needs
|
|
28
|
+
> EVM, Solana, Sui, Aptos, Algorand, Stellar, XRPL, TON, NEAR, **and Tron** (native TRX,
|
|
29
|
+
> digest-bound). No exceptions. On NEAR, native is the **zero-setup** path: no `storage_deposit`,
|
|
30
|
+
> and a transfer even creates a fresh recipient (the NEP-141 token path still needs
|
|
31
|
+
> `storage_deposit`).
|
|
28
32
|
>
|
|
29
33
|
> **Custom tokens** work everywhere with no allowlist: EVM `{ address, decimals }` ·
|
|
30
|
-
> Solana `{ mint, decimals }` · Sui `{ coinType, decimals }` ·
|
|
34
|
+
> Solana `{ mint, decimals }` · Sui `{ coinType, decimals }` · Aptos `{ metadata, decimals }` ·
|
|
35
|
+
> Algorand `{ assetId, decimals }` · TON `{ master, decimals }` ·
|
|
31
36
|
> Tron `{ address, decimals }` · NEAR `{ contractId, decimals }` · Stellar
|
|
32
37
|
> `{ issuer, code, decimals }` · XRPL `{ issuer, currencyHex, decimals }`.
|
|
33
38
|
|
|
@@ -93,6 +98,23 @@ receive**. This page is the exact list.
|
|
|
93
98
|
- **Finality is slow-ish:** verification waits for the tx to solidify (~19 blocks, ~57s); until then it reads as `tx_not_found` and is retried.
|
|
94
99
|
- **Wallet:** `{ privateKey }` (32-byte hex, same format as EVM); addresses are Base58 `T…`.
|
|
95
100
|
|
|
101
|
+
### Algorand — USDC needs a one-time ASA opt-in (native ALGO doesn't)
|
|
102
|
+
- **Pay in:** `'native'` (ALGO, the zero-setup path), `'USDC'`, or a custom ASA `{ assetId, decimals }`. **USDC only for the stablecoin** — Tether deprecated/froze USDT on Algorand (2025-09-01), so it's not built in (pass it as a custom ASA if you must).
|
|
103
|
+
- **Receiving USDC needs an ASA opt-in.** Before an account can *receive* USDC (ASA `31566704`) — or any ASA — it must **opt into that asset** once: a 0-amount asset-transfer to itself, which raises its minimum balance by 0.1 ALGO (locked, recoverable). No opt-in → the payment fails and PipRail returns `RECIPIENT_NOT_READY`. **Native ALGO needs no opt-in.** The payer is implicitly opted-in if it already holds USDC. The one-time opt-in is plain `algosdk` (PipRail stays a payments SDK, not a wallet manager):
|
|
104
|
+
```ts
|
|
105
|
+
import algosdk from 'algosdk'
|
|
106
|
+
const algod = new algosdk.Algodv2('', 'https://mainnet-api.algonode.cloud', '')
|
|
107
|
+
const sp = await algod.getTransactionParams().do()
|
|
108
|
+
const optIn = algosdk.makeAssetTransferTxnWithSuggestedParamsFromObject({
|
|
109
|
+
sender: account.addr, receiver: account.addr, amount: 0, assetIndex: 31566704, suggestedParams: sp,
|
|
110
|
+
})
|
|
111
|
+
await algod.sendRawTransaction(optIn.signTxn(account.sk)).do() // one-time, per account per ASA
|
|
112
|
+
```
|
|
113
|
+
- **Fast + cheap:** ~3s single-step finality, flat 0.001 ALGO min fee. The challenge nonce rides in the transaction's **note field** (Template A), so the proof is bound to its challenge; verify reads the merchant account's inbound transfers via the indexer.
|
|
114
|
+
- **x402:** Algorand's `exact` scheme is part of the official x402 standard, but the incumbent on-chain path uses a hosted **facilitator** — PipRail is the **backendless, no-facilitator** option (payer broadcasts, merchant verifies locally).
|
|
115
|
+
- **Wallet:** `{ mnemonic }` (a 25-word Algorand recovery phrase) or `{ account }` (an algosdk `{ addr, sk }`).
|
|
116
|
+
- **Endpoints:** `rpcUrl` overrides the **algod** endpoint (submit/params); the verify-side **indexer** uses the public AlgoNode default (override needs are rare; the public indexer is production-grade for the inbound-transfer read).
|
|
117
|
+
|
|
96
118
|
### Stellar — the receiver needs a trustline + a funded account
|
|
97
119
|
- **Pay in:** `'native'` (XLM), `'USDC'`, `'EURC'`, or a custom `{ issuer, code, decimals }`.
|
|
98
120
|
- **Receiving an issued asset needs a one-time TRUSTLINE.** The merchant (`payTo`) must (1) **exist** on-chain (funded above the ~1 XLM base reserve) and (2) hold a **trustline** (`changeTrust`) for that exact `code+issuer` *before* it can receive. No trustline → the payment fails. Each trustline locks **+0.5 XLM** of reserve. The **payer** likewise needs its own trustline to hold/send the asset.
|
|
@@ -130,6 +152,7 @@ Fix the *recipient*, not the payer:
|
|
|
130
152
|
| `op_no_destination` | Stellar | the `payTo` account doesn't exist | create it with ≥1 XLM (base reserve) |
|
|
131
153
|
| `op_no_trust` | Stellar | recipient has no trustline for the asset | add the trustline (+0.5 XLM reserve) |
|
|
132
154
|
| `… is not registered` | NEAR | recipient isn't `storage_deposit`-registered on the token | call `storage_deposit` once (~0.00125 NEAR) |
|
|
155
|
+
| `must optin` / `asset … missing from <payTo>` | Algorand | recipient hasn't opted into the USDC ASA | opt the recipient into the ASA once (0-amount self-transfer, +0.1 ALGO min balance) |
|
|
133
156
|
|
|
134
157
|
Everything else (EVM, Solana, Sui, Tron, native TON/NEAR) needs no recipient setup, so you'll
|
|
135
158
|
only ever see `INSUFFICIENT_FUNDS` there if the payer is short. Full taxonomy: **[ERRORS.md](./ERRORS.md)**.
|
|
@@ -143,9 +166,10 @@ right asset to `payTo`), but binds the proof to your challenge differently:
|
|
|
143
166
|
|
|
144
167
|
- **Memo-bound** (the challenge nonce is written on-chain): **NEAR tokens** (ft_transfer
|
|
145
168
|
memo), **TON** (transfer comment), **Stellar** (`MEMO_HASH = sha256(nonce)`), **XRPL**
|
|
146
|
-
(Memo + a derived DestinationTag)
|
|
169
|
+
(Memo + a derived DestinationTag), **Algorand** (the transaction's note field — native ALGO
|
|
170
|
+
and USDC alike).
|
|
147
171
|
- **Digest-bound** (no on-chain nonce; the proof is the tx id, made single-use by the gate
|
|
148
|
-
+ a recency window): **EVM**, **Solana**, **Sui**, **Tron**, and **native NEAR**. For
|
|
172
|
+
+ a recency window): **EVM**, **Solana**, **Sui**, **Aptos**, **Tron**, and **native NEAR**. For
|
|
149
173
|
these, a persistent `isUsed`/`markUsed` store + a tight `maxTimeoutSeconds` are
|
|
150
174
|
load-bearing in multi-instance deployments (the default used-set is single-process).
|
|
151
175
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,80 @@ 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.5.0] — 2026-06-04
|
|
8
|
+
|
|
9
|
+
**The killer agent feature — `client.planPayment(url)`.** A read-only call that surveys a 402
|
|
10
|
+
across every rail it offers *on your chain* against your wallet's OWN holdings — **token balance +
|
|
11
|
+
native gas + recipient-readiness** (trustline / ATA / storage_deposit / ASA opt-in / activation) —
|
|
12
|
+
and tells you, crystal-clear, whether it's settleable, on which rail, and if not, exactly what to
|
|
13
|
+
top up. It completes the trio the SDK already ships: **`quote()` (what it costs) → `estimateCost()`
|
|
14
|
+
(the gas) → `planPayment()` (can I actually settle, and where).** Fully backward-compatible and
|
|
15
|
+
opt-in; defaults are unchanged. The official x402 client picks `accepts[0]` blind; PipRail is the
|
|
16
|
+
only backendless SDK that can answer "can I actually pay this?" across 28 chains with pure RPC
|
|
17
|
+
reads, no oracle/facilitator/bridge. Live-proven on Algorand mainnet (ready / recipient-not-ready /
|
|
18
|
+
insufficient / multi-rail-rank, 4/4).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`client.planPayment(url, init?)` → `PaymentPlan | null`.** Never throws for a read problem (a
|
|
22
|
+
transient/RPC failure surfaces as a rail in `state: 'unknown'` + a warning, never a false
|
|
23
|
+
"unaffordable"); returns `null` when the URL isn't 402-gated; and when the 402 offers no rail on
|
|
24
|
+
your chain it EXPLAINS that (status `blocked` + a hint) instead of throwing. The plan carries:
|
|
25
|
+
`payable` + `best` (the cheapest settleable rail), `options[]` (every rail with typed `blockers`
|
|
26
|
+
— `INSUFFICIENT_TOKEN`/`INSUFFICIENT_GAS`/`RECIPIENT_NOT_READY`/`OUTSIDE_POLICY` — plus soft
|
|
27
|
+
`warnings`, a `shortfall`, live `balance`, and `recipient.fix`), and a one-sentence `fundingHint`.
|
|
28
|
+
- **`client.canAfford(url)` → `boolean`** — convenience over the above.
|
|
29
|
+
- **`fetch(url, { autoRoute: true })` / `new PipRailClient({ autoRoute: true })`** — opt-in:
|
|
30
|
+
`fetch` pays the cheapest rail the wallet can ACTUALLY settle (not the first policy-passing one),
|
|
31
|
+
or throws `PaymentDeclinedError` carrying the funding hint before any send. **Default off** —
|
|
32
|
+
the zero-config path is byte-identical.
|
|
33
|
+
- **`planAcross(clients, url)`** — the cross-chain brain: give it one client per chain you fund and
|
|
34
|
+
it merges their plans, payable-first (no oracle, so the cross-coin tiebreak is your client order).
|
|
35
|
+
- **`piprail_plan_payment`** agent tool (budget-bound; `paymentTools(client)` now returns 3 tools).
|
|
36
|
+
- **Driver contract:** `balanceOf(wallet, asset)` + `recipientReady(payTo, asset)` on every family
|
|
37
|
+
(10/10), RPC-read-only and NEVER-throw (transient ⇒ `null`/`'unknown'`, per ERRORS.md §5). Real
|
|
38
|
+
receive-prerequisite probes on NEAR (`storage_balance_of`), Stellar/XRPL (trustline presence),
|
|
39
|
+
Algorand (ASA opt-in); truthful `'n/a'` on EVM/Solana/TON/Tron/Sui/Aptos (no prerequisite).
|
|
40
|
+
- New exported types: `PaymentPlan`, `PayOption`, `PayBlocker`, `PayWarning`, `RecipientReason`,
|
|
41
|
+
`WalletBalance` (and the previously-missing `AptosToken`/`AlgorandToken`).
|
|
42
|
+
|
|
43
|
+
## [1.4.0] — 2026-06-04
|
|
44
|
+
|
|
45
|
+
A new chain **family** — **Algorand** — the **10th driver family**, bringing the built-in count to
|
|
46
|
+
**28 chains across 10 families (19 EVM)**. Algorand is genuinely part of the **official x402
|
|
47
|
+
standard** (its `exact` scheme is merged into the canonical x402 repo and the `@x402/avm` package),
|
|
48
|
+
and one of the loudest agentic-commerce chains of 2026 — but the incumbent x402 path there is
|
|
49
|
+
**facilitator-mediated**, so PipRail is the **first facilitator-free, backendless, verify-locally
|
|
50
|
+
x402 SDK on Algorand**. Fully backward-compatible; `algosdk` is a lazy-loaded optional peer, so
|
|
51
|
+
pure-EVM (and other) installs never download it.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
- **Algorand (`chain: 'algorand'`, CAIP-2 `algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k`)** — native
|
|
55
|
+
Circle **USDC** (ASA `31566704`, 6 dp) + native **ALGO** (6 dp). The USDC ASA was verified live on
|
|
56
|
+
mainnet (algod `/v2/assets/31566704` → unit-name `USDC`, decimals 6, creator = Circle's `2UEQ…`
|
|
57
|
+
account, url `centre.io/usdc`) before shipping. **USDC-only:** Tether deprecated USDT on Algorand
|
|
58
|
+
(frozen 2025-09-01), so it's intentionally omitted — pass it as a custom `{ assetId, decimals }`.
|
|
59
|
+
- **Template A (memo-bound, like Stellar/XRPL/NEAR):** every Algorand transaction carries an
|
|
60
|
+
arbitrary **note field (≤1KB)**, so the challenge nonce rides in it verbatim (no hashing needed —
|
|
61
|
+
a UUID dwarfs nothing of the 1KB cap). `verify()` re-derives the watched account from the
|
|
62
|
+
**trusted `accept.payTo`** (never the client ref), reads its recent inbound transfers via the
|
|
63
|
+
indexer, and matches `note === nonce` + recipient + asset + amount + recency — a proof is
|
|
64
|
+
cryptographically bound to its challenge. Native ALGO is a `pay` txn; USDC/ASAs are `axfer`; both
|
|
65
|
+
carry the note. Amounts are integer base units (like EVM). `algosdk` is an **optional peer
|
|
66
|
+
(`>=3 <4`)**, lazy-loaded on first use; the built EVM bundle stays free of any static `algosdk`
|
|
67
|
+
import (its own chunk).
|
|
68
|
+
- **Receive prerequisite:** to receive a USDC/ASA, the recipient must **opt into the ASA** (a
|
|
69
|
+
one-time 0-amount self-transfer) — conceptually identical to an XRPL/Stellar trustline. A submit
|
|
70
|
+
failure for a not-opted-in recipient maps to the typed `RecipientNotReadyError`; native ALGO needs
|
|
71
|
+
no opt-in.
|
|
72
|
+
|
|
73
|
+
**Live-proven on Algorand mainnet — both assets, 12/12.** Real 402 → pay → confirm → verify → 200
|
|
74
|
+
round-trips, each with balance moved + replay rejected (`tx_already_used`) + all agent surfaces
|
|
75
|
+
green: **native ALGO** 6/6 (tx `AXXJVYAP7BLK6C76AWCJ3XA5HTECIRSCNRQ2WLFRNSZ6CD5GH32Q`) and
|
|
76
|
+
**USDC** 6/6 (tx `INWCUUBAMIBYOPPUOBWXEHZQAQL6KSV7DPEEVGKAI64Z46TRQKOA`, merchant +0.05 USDC).
|
|
77
|
+
Also verified against the test contract (typecheck + 441 tests + build + the lazy-chunk invariant).
|
|
78
|
+
Funding follow-up: file an Algorand **xGov retroactive** grant for the shipped open-source SDK
|
|
79
|
+
(SDKs/libraries are a named eligible category).
|
|
80
|
+
|
|
7
81
|
## [1.3.1] — 2026-06-04
|
|
8
82
|
|
|
9
83
|
Aptos pay-path fix surfaced by the live mainnet test — no API change, fully compatible with 1.3.0.
|
package/ERRORS.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This is the **single source of truth** for how `@piprail/sdk` reports errors. It is
|
|
4
4
|
deliberately small and uniform: every module — the client, the server gate, the registry,
|
|
5
|
-
and every chain driver (all
|
|
6
|
-
and any future one) — follows it
|
|
5
|
+
and every chain driver (all ten families: EVM, Solana, TON, Tron, NEAR, Sui, Stellar, XRPL, Aptos,
|
|
6
|
+
Algorand, and any future one) — follows it
|
|
7
7
|
*exactly*, so a human developer, a merchant server, or an AI agent always gets a **typed,
|
|
8
8
|
understandable** reason, never an opaque chain-library blob.
|
|
9
9
|
|
|
@@ -149,6 +149,17 @@ Every `PaymentDriver` / `ResolvedNetwork` method has a fixed error behaviour:
|
|
|
149
149
|
| `send(wallet, accept)` | wrap the broadcast; map **sender** affordability → `InsufficientFundsError` (§6) and **recipient** setup → `RecipientNotReadyError` (§6.1); **rethrow everything else unchanged** (never swallow). Every mapped throw carries `{ cause }` = the raw chain error. |
|
|
150
150
|
| `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. |
|
|
151
151
|
| `confirm(ref, n)` | broadcast-but-not-confirmed / timeout → `ConfirmationTimeoutError`. |
|
|
152
|
+
| `estimateCost(accept, opts?)` | **never throw** — guard the RPC read and fall back to a `'heuristic'` constant; always return a valid `CostEstimate`. |
|
|
153
|
+
| `balanceOf(wallet, asset)` | **never throw** — RPC-read-only. A field whose read was unavailable (transient/rate-limit) returns `null`, NOT `0` (a false 0 reads as "broke"). For `asset==='native'`, `token === native`. |
|
|
154
|
+
| `recipientReady(payTo, asset)` | **never throw** — report the receive prerequisite: `{ ready:'n/a' }` (no prerequisite on this family/native), `{ ready:true }`, `{ ready:false, reason }` (a `RecipientReason`), or `{ ready:'unknown' }` on a transient read. `'n/a'` must be TRUTHFUL — never a stand-in for "didn't check". |
|
|
155
|
+
|
|
156
|
+
> **`planPayment` is a RETURN-channel feature.** The client's `planPayment`/`canAfford` compose
|
|
157
|
+
> `balanceOf` + `recipientReady` + `estimateCost` + the policy verdict into a `PaymentPlan` — and,
|
|
158
|
+
> like `verify()`, they **return** the outcome rather than throwing: a transient read becomes a rail
|
|
159
|
+
> in `state:'unknown'` (+ a warning), an unsettleable rail carries typed `blockers`, and a 402 with
|
|
160
|
+
> no rail on the client's chain is *explained* in the plan. The only throw is `InvalidEnvelopeError`
|
|
161
|
+
> on an unparseable challenge. (`fetch({ autoRoute:true })` is the one place a plan turns into a
|
|
162
|
+
> THROWN `PaymentDeclinedError` — refusing before any send when nothing is settleable.)
|
|
152
163
|
|
|
153
164
|
### 6. Affordability converges on one error, by two mechanisms
|
|
154
165
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @piprail/sdk
|
|
2
2
|
|
|
3
|
-
**Accept crypto payments from any HTTP request — on any EVM chain, Solana, TON, Tron, NEAR, Sui, Stellar, and the XRP Ledger — in a couple of lines.**
|
|
3
|
+
**Accept crypto payments from any HTTP request — on any EVM chain, Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar, and the XRP Ledger — in a couple of lines.**
|
|
4
4
|
|
|
5
5
|
No middleman. No database. No fee. No account. Payments settle **straight into your wallet**, verified locally against your own RPC. Drop one middleware in front of a route and it's paid-only; point an agent at a paid URL and it pays itself.
|
|
6
6
|
|
|
@@ -77,20 +77,48 @@ client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05',
|
|
|
77
77
|
|
|
78
78
|
**Know the gas before you pay.** `client.estimateCost(url)` returns the quote **and** a `CostEstimate` — the network fee in the chain's **native coin** (you pay in USDC but burn ETH / SOL / TON / XLM / XRP / TRX on gas, a separate balance the agent must keep topped up). It's best-effort and labelled (`cost.basis`): a live-RPC read where cheap (`'estimated'` — EVM gas price, XRPL fee), a typical-cost constant otherwise (`'heuristic'`), and it never throws. Most valuable on **Tron**, where a USD₮ transfer can cost real TRX. So an agent can budget the *total* — payment **+** gas — before any funds move. Every driver implements it; the math is extracted per-chain and shaped uniformly by one shared `nativeCost()` helper.
|
|
79
79
|
|
|
80
|
+
### Plan before you pay — `planPayment()` (never fumble a payment)
|
|
81
|
+
|
|
82
|
+
`quote()` tells you the price and `estimateCost()` the gas — **`planPayment(url)`** closes the loop: **one read-only call** that checks, against your wallet's *own* holdings, whether a 402 will actually go through — and if not, exactly what to fix. No funds move.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const plan = await client.planPayment(url)
|
|
86
|
+
|
|
87
|
+
if (plan?.payable) {
|
|
88
|
+
await client.fetch(url, { autoRoute: true }) // pays plan.best — the cheapest rail you can settle
|
|
89
|
+
} else {
|
|
90
|
+
console.log(plan?.fundingHint)
|
|
91
|
+
// "Have the USDC, but need ~0.000021 ETH for gas on base (have 0)."
|
|
92
|
+
// "Recipient 2OT6…GC5E4 can't receive on algorand yet — must opt into the USDC ASA."
|
|
93
|
+
// "Top up 0.04 USDC on base (have 0.01)."
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For **every rail the 402 offers on your chain**, the plan reads **token balance + native-coin gas + recipient-readiness** (trustline / ATA / `storage_deposit` / ASA opt-in / activation) and returns:
|
|
98
|
+
- **`payable`** + **`best`** — the cheapest rail you can actually settle (recipient confirmed able to receive);
|
|
99
|
+
- **`options[]`** — each rail with typed **`blockers`** (`INSUFFICIENT_TOKEN` · `INSUFFICIENT_GAS` · `RECIPIENT_NOT_READY` · `OUTSIDE_POLICY`), soft **`warnings`** (`SYMBOL_MISMATCH`, `THIN_GAS_MARGIN`, `BALANCE_UNREADABLE`, …), a **`shortfall`**, the live **`balance`**, and **`recipient.fix`**;
|
|
100
|
+
- **`fundingHint`** — one human sentence on exactly what to top up.
|
|
101
|
+
|
|
102
|
+
**Why it's the agent unlock.** The official x402 client picks `accepts[0]` blind and learns it can't pay only when the broadcast reverts (no token, no gas) or the transfer silently strands (recipient not set up to receive). `planPayment` turns those runtime failures into a pre-checked decision — and on the no-facilitator path *you* pay your own gas, so "I hold USDC but no ETH" is a first-class answer, not a crash. It **never throws for a read hiccup** (a throttled RPC surfaces as `state: 'unknown'` + a warning, never a false "broke"), returns `null` when the URL isn't gated, and *explains* "this is offered on solana, base — you're on xrpl" instead of erroring. `client.canAfford(url)` is the one-boolean convenience.
|
|
103
|
+
|
|
104
|
+
**Auto-route (opt-in).** `new PipRailClient({ autoRoute: true })` (or `fetch(url, { autoRoute: true })`) makes `fetch` pay the cheapest *settleable* rail instead of the first policy-passing one — refusing with `PaymentDeclinedError` + the funding hint before any send. **Default off; the zero-config path is unchanged.**
|
|
105
|
+
|
|
106
|
+
**Across chains.** A client is bound to one chain; **`planAcross([baseClient, solanaClient, …], url)`** runs each plan in parallel and merges them payable-first, so an agent holding funds on several chains learns which to use. (No price oracle — cross-coin ties break on the order you list the clients.)
|
|
107
|
+
|
|
80
108
|
### Hand an LLM a budget-bound wallet
|
|
81
109
|
|
|
82
110
|
`paymentTools(client)` returns framework-agnostic tool descriptors (name + description + JSON Schema + `invoke`) — drop them into MCP, the Vercel AI SDK, OpenAI/Anthropic function-calling, or LangChain in a couple of lines. The budget rides on the client, so the model can't overspend.
|
|
83
111
|
|
|
84
112
|
```ts
|
|
85
113
|
import { paymentTools } from '@piprail/sdk'
|
|
86
|
-
const tools = paymentTools(client) // → [piprail_quote_payment, piprail_pay_request]
|
|
114
|
+
const tools = paymentTools(client) // → [piprail_quote_payment, piprail_plan_payment, piprail_pay_request]
|
|
87
115
|
```
|
|
88
116
|
|
|
89
117
|
See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
|
|
90
118
|
|
|
91
119
|
### Accept several chains at once
|
|
92
120
|
|
|
93
|
-
`requirePayment` (and `createPaymentGate`) take an **`accept: [...]`** array — one challenge that's payable on **any** of several chains/tokens, across **all
|
|
121
|
+
`requirePayment` (and `createPaymentGate`) take an **`accept: [...]`** array — one challenge that's payable on **any** of several chains/tokens, across **all ten families** (EVM, Solana, TON, Tron, Stellar, XRPL, NEAR, Sui, Aptos, Algorand). The agent pays with whatever it holds:
|
|
94
122
|
|
|
95
123
|
```ts
|
|
96
124
|
requirePayment({
|
|
@@ -133,7 +161,7 @@ requirePayment({ chain: 'ton', token: 'native', amount: '1', payTo }) /
|
|
|
133
161
|
requirePayment({ chain: 'xrpl', token: 'native', amount: '1', payTo }) // XRP
|
|
134
162
|
```
|
|
135
163
|
|
|
136
|
-
**Native or stablecoin — your choice, on every chain.** Every gate accepts the chain's native coin (ETH, BNB, POL, AVAX, SOL, TON, XLM, XRP, SUI, NEAR, **TRX**, …) just as readily as a stablecoin — set `token: 'native'` and the SDK fills in the right decimals (18 on EVM, 9 on Solana/TON/Sui, 7 on Stellar, 6 on XRPL/Tron, 24 on NEAR). Verification, replay protection, and self-custody are identical to the stablecoin path — across **all
|
|
164
|
+
**Native or stablecoin — your choice, on every chain.** Every gate accepts the chain's native coin (ETH, BNB, POL, AVAX, SOL, TON, XLM, XRP, SUI, NEAR, **TRX**, …) just as readily as a stablecoin — set `token: 'native'` and the SDK fills in the right decimals (18 on EVM, 9 on Solana/TON/Sui, 8 on Aptos, 7 on Stellar, 6 on XRPL/Tron/Algorand, 24 on NEAR). Verification, replay protection, and self-custody are identical to the stablecoin path — across **all ten families, no exceptions**. (On **NEAR**, native is the zero-setup path — no `storage_deposit` — while the NEP-141 token path needs registration; see the NEAR note. On **Tron**, USD₮ is the default since TRX is volatile gas, but native TRX works too.)
|
|
137
165
|
|
|
138
166
|
`token` is **required** — every gate states exactly what it accepts, so there's never any doubt whether a route takes USDC, USDT, or the native coin. Name a built-in symbol (`'USDC'`, `'USDT'`), use `'native'` for the chain's own coin (ETH, BNB, SOL, TON, XLM, …), or pass a custom token by address. The symbol is all you write — the SDK fills in the contract + decimals.
|
|
139
167
|
|
|
@@ -168,6 +196,7 @@ Every token address below was verified on-chain (symbol + decimals) before shipp
|
|
|
168
196
|
| `'near'` | NEAR | USDC, USDT |
|
|
169
197
|
| `'sui'` | Sui | USDC |
|
|
170
198
|
| `'aptos'` | Aptos | USDC, USDT |
|
|
199
|
+
| `'algorand'` | Algorand | USDC |
|
|
171
200
|
| `'stellar'` | Stellar | USDC, EURC |
|
|
172
201
|
| `'xrpl'` | XRP Ledger | USDC, RLUSD |
|
|
173
202
|
|
|
@@ -179,6 +208,8 @@ Every token address below was verified on-chain (symbol + decimals) before shipp
|
|
|
179
208
|
|
|
180
209
|
**Sui note:** **USDC only** — no native USDT on Sui (Wormhole-bridged only). Native SUI works with `token: 'native'`.
|
|
181
210
|
|
|
211
|
+
**Algorand note:** **USDC only** — Tether deprecated USDT on Algorand (frozen 2025-09-01), so it's intentionally absent (pass it as a custom `{ assetId, decimals }`). Native ALGO works with `token: 'native'` (the zero-setup path). To **receive** USDC the recipient must **opt into the ASA** once (a 0-amount self-transfer — like a trustline); a not-opted-in recipient surfaces `RECIPIENT_NOT_READY`. The challenge nonce binds inside the transaction's note field (Template A). Algorand's `exact` scheme is part of the official x402 standard; the incumbent on-chain path there uses a hosted facilitator, so PipRail is the backendless, no-facilitator option.
|
|
212
|
+
|
|
182
213
|
**Stellar / XRPL note:** to **receive** an issued asset (USDC/EURC on Stellar; USDC/RLUSD on XRPL) the recipient needs a one-time **trustline** for that asset, and the account must already exist / be activated (a small native reserve — **locked, not spent**). Native XLM/XRP need no trustline. The payer needs its own trustline too.
|
|
183
214
|
|
|
184
215
|
### Using TON? Grab one free API key (≈30 seconds)
|
|
@@ -474,10 +505,10 @@ The SDK is browser-clean (no Node-only globals in the protocol layer), so a plai
|
|
|
474
505
|
Two layers, one contract. Worth knowing if you're extending the SDK or auditing it.
|
|
475
506
|
|
|
476
507
|
- **The protocol layer is chain-agnostic.** `server.ts` (`requirePayment`/`createPaymentGate`), `client.ts` (`PipRailClient`), `x402.ts` (wire envelopes), `policy.ts`, `ledger.ts`, and `agent.ts` depend **only** on the `PaymentDriver` contract in `drivers/types.ts` — zero `viem`, zero `@solana/web3.js`, zero chain SDK. The chain is data the caller passes, not an allowlist the SDK ships.
|
|
477
|
-
- **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
|
|
478
|
-
- **Families mirror each other file-for-file.** Each lives in `drivers/<family>/` as `chains` · `wallet` · `pay` · `verify` · `index`, with family-suffixed functions (`payEvm`/`paySui`/…, `verifyEvm`/`verifyNear`/…).
|
|
479
|
-
- **Routing + lazy auto-mount.** `registry.ts` maps a `chain` value to its family synchronously (`familyForChain`). EVM is always present (viem is a hard peer); every non-EVM family **loads itself on first use** via one dynamic `import()`, so a pure-EVM install never downloads `@solana`/`@ton`/`@stellar`/`xrpl`/`tronweb`/`near-api-js`/`@mysten/sui`. A build-time invariant asserts the main bundle has **zero** static imports of those libs — only per-family lazy chunks.
|
|
480
|
-
- **Two verification templates.** *Template A (memo-bound)* — Stellar, XRPL, TON, NEAR — carries the challenge nonce inside the transfer (memo / tag / comment), so the proof is cryptographically bound to its challenge. *Template B (digest-bound)* — EVM, Solana, Tron, Sui — binds via a single-use proof set + recipient + amount + a tight recency window (use a persistent `isUsed`/`markUsed` store in production).
|
|
508
|
+
- **The `PaymentDriver` contract.** `resolve(chain)` → a bound `ResolvedNetwork` exposing `resolveToken` · `describeAsset` · `assertValidPayTo` · `bindWallet` · `send` · `confirm` · `estimateCost` · `balanceOf` · `recipientReady` · `verify`. That's the entire boundary every family implements and the protocol layer ever sees.
|
|
509
|
+
- **Families mirror each other file-for-file.** Each lives in `drivers/<family>/` as `chains` · `wallet` · `pay` · `verify` · `index`, with family-suffixed functions (`payEvm`/`paySui`/…, `verifyEvm`/`verifyNear`/…). Ten today: `evm`, `solana`, `ton`, `stellar`, `xrpl`, `tron`, `near`, `sui`, `aptos`, `algorand`. Adding one = copy the five files, implement the contract, `registerDriver` — the protocol layer never changes.
|
|
510
|
+
- **Routing + lazy auto-mount.** `registry.ts` maps a `chain` value to its family synchronously (`familyForChain`). EVM is always present (viem is a hard peer); every non-EVM family **loads itself on first use** via one dynamic `import()`, so a pure-EVM install never downloads `@solana`/`@ton`/`@stellar`/`xrpl`/`tronweb`/`near-api-js`/`@mysten/sui`/`@aptos-labs/ts-sdk`/`algosdk`. A build-time invariant asserts the main bundle has **zero** static imports of those libs — only per-family lazy chunks.
|
|
511
|
+
- **Two verification templates.** *Template A (memo-bound)* — Stellar, XRPL, TON, NEAR, Algorand — carries the challenge nonce inside the transfer (memo / tag / comment / note), so the proof is cryptographically bound to its challenge. *Template B (digest-bound)* — EVM, Solana, Tron, Sui, Aptos — binds via a single-use proof set + recipient + amount + a tight recency window (use a persistent `isUsed`/`markUsed` store in production).
|
|
481
512
|
- **Gas estimation.** Every driver's `estimateCost` extracts its own per-chain fee math, shaped into one uniform `CostEstimate` by the shared `nativeCost()` helper (`util/cost.ts`).
|
|
482
513
|
- **The tests are the contract** (`test/`, Vitest), and two living standards govern any change: **[ERRORS.md](./ERRORS.md)** (how every module reports errors) and **STANDARDS.md** (how anything in the SDK is built + the verification gate). Runnable examples — including a local Anvil end-to-end — live in [`examples/`](../examples).
|
|
483
514
|
|
|
@@ -499,11 +530,12 @@ A failed payment is almost always one of two things, and PipRail tells them apar
|
|
|
499
530
|
|
|
500
531
|
| Chain | The recipient must… | Sender also needs |
|
|
501
532
|
|---|---|---|
|
|
502
|
-
| **EVM · Solana · Sui · Tron** | nothing (just be a valid address; Solana's token account is auto-created by the SDK) | native gas |
|
|
533
|
+
| **EVM · Solana · Sui · Aptos · Tron** | nothing (just be a valid address; Solana's token account is auto-created by the SDK; Aptos's primary FA store auto-creates) | native gas |
|
|
503
534
|
| **TON** | nothing for native; a jetton wallet auto-deploys on first receipt (sender pays the gas) | TON for gas |
|
|
504
535
|
| **NEAR** | nothing for native; for a token, be `storage_deposit`-registered on it (NEP-145, ~0.00125 NEAR, one-time) | NEAR for gas |
|
|
505
536
|
| **Stellar** | exist (created with ≥1 XLM base reserve); for USDC/EURC, hold a **trustline** (+0.5 XLM each) | base + trustline reserves |
|
|
506
537
|
| **XRP Ledger** | be **activated** — hold ≥1 XRP base reserve to exist; for USDC/RLUSD, a **trustline** | keep its own 1 XRP reserve |
|
|
538
|
+
| **Algorand** | nothing for native ALGO; for USDC, **opt into the ASA** once (a 0-amount self-transfer, ~0.1 ALGO min-balance bump) | ALGO for fees + its own opt-in |
|
|
507
539
|
|
|
508
540
|
> These are anti-spam "state rent" rules built into each ledger — e.g. an XRPL account can't receive a sub-1-XRP first payment because that payment must create the account at its ≥1 XRP base reserve. PipRail surfaces them as `RECIPIENT_NOT_READY` with the fix, so a payment that "can't go through" is self-explanatory. Per-chain specifics live in **[CHAINS.md](./CHAINS.md)**.
|
|
509
541
|
|
|
@@ -526,7 +558,7 @@ The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
|
|
|
526
558
|
|---|---|---|
|
|
527
559
|
| `chain` | — | `'base'` / `'bnb'` / `'solana'` / `'ton'` / …, a viem `Chain`, or `{ id, rpcUrl }` (single-chain form) |
|
|
528
560
|
| `amount` | — | Human-readable, e.g. `'0.05'` (single-chain form) |
|
|
529
|
-
| `token` | — | `'USDC'` / `'USDT'`, `'native'`, or a custom `{ address, decimals }` (EVM/Tron) / `{ mint, decimals }` (Solana) / `{ master, decimals }` (TON) / `{ issuer, code, decimals }` (Stellar) / `{ issuer, currencyHex, decimals }` (XRPL) / `{ contractId, decimals }` (NEAR) / `{ coinType, decimals }` (Sui) — required for the single form |
|
|
561
|
+
| `token` | — | `'USDC'` / `'USDT'`, `'native'`, or a custom `{ address, decimals }` (EVM/Tron) / `{ mint, decimals }` (Solana) / `{ master, decimals }` (TON) / `{ issuer, code, decimals }` (Stellar) / `{ issuer, currencyHex, decimals }` (XRPL) / `{ contractId, decimals }` (NEAR) / `{ coinType, decimals }` (Sui) / `{ metadata, decimals }` (Aptos) / `{ assetId, decimals }` (Algorand) — required for the single form |
|
|
530
562
|
| `accept` | — | Multi-chain form: `[{ chain, token, amount, payTo?, rpcUrl? }, …]` — offer several chains in one challenge |
|
|
531
563
|
| `payTo` | — | Wallet that receives the payment (per-option fallback in the multi form) |
|
|
532
564
|
| `description` | — | Optional text shown to the agent in the challenge (what the payment is for) |
|
|
@@ -552,7 +584,7 @@ Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `acc
|
|
|
552
584
|
| `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
|
|
553
585
|
| `onEvent` | — | `(event) => void` observability: `payment-required` · `payment-broadcast` · `payment-confirmed` · `payment-unconfirmed` (broadcast OK, local confirm timed out → deferring to server) · `payment-settled` · `payment-failed` |
|
|
554
586
|
|
|
555
|
-
Methods: `fetch` · `get` · `post` (return the gated `Response` after settlement) · **`quote(url)`** (price without paying → `PipRailQuote \| null`) · **`estimateCost(url)`** (price **+** native-coin gas estimate → `PipRailCostQuote \| null`) · **`spent()`** (per-asset ledger snapshot).
|
|
587
|
+
Methods: `fetch` · `get` · `post` (return the gated `Response` after settlement) · **`quote(url)`** (price without paying → `PipRailQuote \| null`) · **`estimateCost(url)`** (price **+** native-coin gas estimate → `PipRailCostQuote \| null`) · **`planPayment(url)`** (affordability + recipient-readiness across the offered rails → `PaymentPlan \| null`) · **`canAfford(url)`** (→ `boolean`) · **`spent()`** (per-asset ledger snapshot). Pass `{ autoRoute: true }` to `fetch` (or set it on the client) to pay the cheapest *settleable* rail. Module-level **`planAcross(clients, url)`** plans across chains.
|
|
556
588
|
|
|
557
589
|
**Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
|
|
558
590
|
|
|
@@ -566,6 +598,8 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
|
|
|
566
598
|
| Tron | `{ privateKey }` (32-byte hex — secp256k1) |
|
|
567
599
|
| NEAR | `{ accountId, privateKey }` (privateKey = ed25519:… secret) |
|
|
568
600
|
| Sui | `{ privateKey }` (suiprivkey1… bech32) or `{ keypair }` |
|
|
601
|
+
| Aptos | `{ privateKey }` (ed25519-priv-0x… AIP-80) or `{ account }` |
|
|
602
|
+
| Algorand | `{ mnemonic }` (25 words) or `{ account }` (algosdk `{ addr, sk }`) |
|
|
569
603
|
|
|
570
604
|
**Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling), budget enforced by the client.
|
|
571
605
|
|
|
@@ -576,7 +610,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
|
|
|
576
610
|
## Requirements
|
|
577
611
|
|
|
578
612
|
- Node 20+ or a modern browser.
|
|
579
|
-
- `viem ^2.21` (peer dep). Solana: `@solana/web3.js`, `@solana/spl-token`, `bs58` (optional peers). TON: `@ton/ton`, `@ton/core`, `@ton/crypto` (optional peers). Stellar: `@stellar/stellar-sdk` (optional peer). XRPL: `xrpl` (optional peer). Tron: `tronweb` (optional peer). NEAR: `near-api-js` (optional peer). Sui: `@mysten/sui` (optional peer).
|
|
613
|
+
- `viem ^2.21` (peer dep). Solana: `@solana/web3.js`, `@solana/spl-token`, `bs58` (optional peers). TON: `@ton/ton`, `@ton/core`, `@ton/crypto` (optional peers). Stellar: `@stellar/stellar-sdk` (optional peer). XRPL: `xrpl` (optional peer). Tron: `tronweb` (optional peer). NEAR: `near-api-js` (optional peer). Sui: `@mysten/sui` (optional peer). Aptos: `@aptos-labs/ts-sdk` (optional peer). Algorand: `algosdk` (optional peer).
|
|
580
614
|
|
|
581
615
|
## License & trademark
|
|
582
616
|
|