@piprail/sdk 1.3.0 → 1.4.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 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 **Tron** read those sections before you ship them.
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, digest-bound). No
26
- > exceptions. On NEAR, native is the **zero-setup** path: no `storage_deposit`, and a transfer
27
- > even creates a fresh recipient (the NEP-141 token path still needs `storage_deposit`).
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 }` · TON `{ master, 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,57 @@ 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.4.0] — 2026-06-04
8
+
9
+ A new chain **family** — **Algorand** — the **10th driver family**, bringing the built-in count to
10
+ **28 chains across 10 families (19 EVM)**. Algorand is genuinely part of the **official x402
11
+ standard** (its `exact` scheme is merged into the canonical x402 repo and the `@x402/avm` package),
12
+ and one of the loudest agentic-commerce chains of 2026 — but the incumbent x402 path there is
13
+ **facilitator-mediated**, so PipRail is the **first facilitator-free, backendless, verify-locally
14
+ x402 SDK on Algorand**. Fully backward-compatible; `algosdk` is a lazy-loaded optional peer, so
15
+ pure-EVM (and other) installs never download it.
16
+
17
+ ### Added
18
+ - **Algorand (`chain: 'algorand'`, CAIP-2 `algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k`)** — native
19
+ Circle **USDC** (ASA `31566704`, 6 dp) + native **ALGO** (6 dp). The USDC ASA was verified live on
20
+ mainnet (algod `/v2/assets/31566704` → unit-name `USDC`, decimals 6, creator = Circle's `2UEQ…`
21
+ account, url `centre.io/usdc`) before shipping. **USDC-only:** Tether deprecated USDT on Algorand
22
+ (frozen 2025-09-01), so it's intentionally omitted — pass it as a custom `{ assetId, decimals }`.
23
+ - **Template A (memo-bound, like Stellar/XRPL/NEAR):** every Algorand transaction carries an
24
+ arbitrary **note field (≤1KB)**, so the challenge nonce rides in it verbatim (no hashing needed —
25
+ a UUID dwarfs nothing of the 1KB cap). `verify()` re-derives the watched account from the
26
+ **trusted `accept.payTo`** (never the client ref), reads its recent inbound transfers via the
27
+ indexer, and matches `note === nonce` + recipient + asset + amount + recency — a proof is
28
+ cryptographically bound to its challenge. Native ALGO is a `pay` txn; USDC/ASAs are `axfer`; both
29
+ carry the note. Amounts are integer base units (like EVM). `algosdk` is an **optional peer
30
+ (`>=3 <4`)**, lazy-loaded on first use; the built EVM bundle stays free of any static `algosdk`
31
+ import (its own chunk).
32
+ - **Receive prerequisite:** to receive a USDC/ASA, the recipient must **opt into the ASA** (a
33
+ one-time 0-amount self-transfer) — conceptually identical to an XRPL/Stellar trustline. A submit
34
+ failure for a not-opted-in recipient maps to the typed `RecipientNotReadyError`; native ALGO needs
35
+ no opt-in.
36
+
37
+ **Live-proven on Algorand mainnet — both assets, 12/12.** Real 402 → pay → confirm → verify → 200
38
+ round-trips, each with balance moved + replay rejected (`tx_already_used`) + all agent surfaces
39
+ green: **native ALGO** 6/6 (tx `AXXJVYAP7BLK6C76AWCJ3XA5HTECIRSCNRQ2WLFRNSZ6CD5GH32Q`) and
40
+ **USDC** 6/6 (tx `INWCUUBAMIBYOPPUOBWXEHZQAQL6KSV7DPEEVGKAI64Z46TRQKOA`, merchant +0.05 USDC).
41
+ Also verified against the test contract (typecheck + 441 tests + build + the lazy-chunk invariant).
42
+ Funding follow-up: file an Algorand **xGov retroactive** grant for the shipped open-source SDK
43
+ (SDKs/libraries are a named eligible category).
44
+
45
+ ## [1.3.1] — 2026-06-04
46
+
47
+ Aptos pay-path fix surfaced by the live mainnet test — no API change, fully compatible with 1.3.0.
48
+
49
+ ### Fixed
50
+ - **Aptos: cap `maxGasAmount` (50k) on the Fungible-Asset transfer.** Aptos validates
51
+ `max_gas_amount × gas_unit_price` against the sender's balance *before* execution, so the SDK
52
+ default (200k units) made a tiny transfer demand ~0.5 APT held just to be admitted — a wallet
53
+ with a modest APT balance was rejected with `INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE` even
54
+ though the transfer itself uses a fraction of that. A `primary_fungible_store::transfer` (even
55
+ one that creates the recipient's primary store) stays well under 50k gas units, so the cap keeps
56
+ ample gas headroom while the upfront fee requirement stays small. Live-validated on Aptos mainnet.
57
+
7
58
  ## [1.3.0] — 2026-06-04
8
59
 
9
60
  A new chain **family** — **Aptos** — the **9th driver family** and the only Move L1 with BOTH
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 nine families: EVM, Solana, TON, Tron, NEAR, Sui, Stellar, XRPL, Aptos,
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
 
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
 
@@ -90,7 +90,7 @@ See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK w
90
90
 
91
91
  ### Accept several chains at once
92
92
 
93
- `requirePayment` (and `createPaymentGate`) take an **`accept: [...]`** array — one challenge that's payable on **any** of several chains/tokens, across **all nine families** (EVM, Solana, TON, Tron, Stellar, XRPL, NEAR, Sui, Aptos). The agent pays with whatever it holds:
93
+ `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
94
 
95
95
  ```ts
96
96
  requirePayment({
@@ -133,7 +133,7 @@ requirePayment({ chain: 'ton', token: 'native', amount: '1', payTo }) /
133
133
  requirePayment({ chain: 'xrpl', token: 'native', amount: '1', payTo }) // XRP
134
134
  ```
135
135
 
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 nine 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.)
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, 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
137
 
138
138
  `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
139
 
@@ -168,6 +168,7 @@ Every token address below was verified on-chain (symbol + decimals) before shipp
168
168
  | `'near'` | NEAR | USDC, USDT |
169
169
  | `'sui'` | Sui | USDC |
170
170
  | `'aptos'` | Aptos | USDC, USDT |
171
+ | `'algorand'` | Algorand | USDC |
171
172
  | `'stellar'` | Stellar | USDC, EURC |
172
173
  | `'xrpl'` | XRP Ledger | USDC, RLUSD |
173
174
 
@@ -179,6 +180,8 @@ Every token address below was verified on-chain (symbol + decimals) before shipp
179
180
 
180
181
  **Sui note:** **USDC only** — no native USDT on Sui (Wormhole-bridged only). Native SUI works with `token: 'native'`.
181
182
 
183
+ **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.
184
+
182
185
  **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
186
 
184
187
  ### Using TON? Grab one free API key (≈30 seconds)
@@ -475,9 +478,9 @@ Two layers, one contract. Worth knowing if you're extending the SDK or auditing
475
478
 
476
479
  - **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
480
  - **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`/…). Eight today: `evm`, `solana`, `ton`, `stellar`, `xrpl`, `tron`, `near`, `sui`. Adding one = copy the five files, implement the contract, `registerDriver` — the protocol layer never changes.
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).
481
+ - **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.
482
+ - **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.
483
+ - **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
484
  - **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
485
  - **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
486
 
@@ -499,11 +502,12 @@ A failed payment is almost always one of two things, and PipRail tells them apar
499
502
 
500
503
  | Chain | The recipient must… | Sender also needs |
501
504
  |---|---|---|
502
- | **EVM · Solana · Sui · Tron** | nothing (just be a valid address; Solana's token account is auto-created by the SDK) | native gas |
505
+ | **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
506
  | **TON** | nothing for native; a jetton wallet auto-deploys on first receipt (sender pays the gas) | TON for gas |
504
507
  | **NEAR** | nothing for native; for a token, be `storage_deposit`-registered on it (NEP-145, ~0.00125 NEAR, one-time) | NEAR for gas |
505
508
  | **Stellar** | exist (created with ≥1 XLM base reserve); for USDC/EURC, hold a **trustline** (+0.5 XLM each) | base + trustline reserves |
506
509
  | **XRP Ledger** | be **activated** — hold ≥1 XRP base reserve to exist; for USDC/RLUSD, a **trustline** | keep its own 1 XRP reserve |
510
+ | **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
511
 
508
512
  > 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
513
 
@@ -526,7 +530,7 @@ The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
526
530
  |---|---|---|
527
531
  | `chain` | — | `'base'` / `'bnb'` / `'solana'` / `'ton'` / …, a viem `Chain`, or `{ id, rpcUrl }` (single-chain form) |
528
532
  | `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 |
533
+ | `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
534
  | `accept` | — | Multi-chain form: `[{ chain, token, amount, payTo?, rpcUrl? }, …]` — offer several chains in one challenge |
531
535
  | `payTo` | — | Wallet that receives the payment (per-option fallback in the multi form) |
532
536
  | `description` | — | Optional text shown to the agent in the challenge (what the payment is for) |
@@ -566,6 +570,8 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
566
570
  | Tron | `{ privateKey }` (32-byte hex — secp256k1) |
567
571
  | NEAR | `{ accountId, privateKey }` (privateKey = ed25519:… secret) |
568
572
  | Sui | `{ privateKey }` (suiprivkey1… bech32) or `{ keypair }` |
573
+ | Aptos | `{ privateKey }` (ed25519-priv-0x… AIP-80) or `{ account }` |
574
+ | Algorand | `{ mnemonic }` (25 words) or `{ account }` (algosdk `{ addr, sk }`) |
569
575
 
570
576
  **Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling), budget enforced by the client.
571
577
 
@@ -576,7 +582,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
576
582
  ## Requirements
577
583
 
578
584
  - 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).
585
+ - `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
586
 
581
587
  ## License & trademark
582
588
 
@@ -0,0 +1,363 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+ var _chunkIQGT65WScjs = require('./chunk-IQGT65WS.cjs');
11
+
12
+ // src/drivers/algorand/index.ts
13
+ var _algosdk = require('algosdk'); var _algosdk2 = _interopRequireDefault(_algosdk);
14
+
15
+ // src/drivers/algorand/chains.ts
16
+ var ALGO_DECIMALS = 6;
17
+ var ALGO_SYMBOL = "ALGO";
18
+ var ALGORAND_MAINNET = {
19
+ caip2: "algorand:wGHE2Pwdvd7S12BL5FaOP20EGYesN73k",
20
+ defaultAlgod: "https://mainnet-api.algonode.cloud",
21
+ defaultIndexer: "https://mainnet-idx.algonode.cloud",
22
+ tokens: {
23
+ // Circle USDC — ASA id + 6 decimals verified live on mainnet algod
24
+ // (/v2/assets/31566704 → unit-name "USDC", decimals 6, creator = Circle) before shipping.
25
+ // USDC-only: Tether deprecated USDT on Algorand, so it's intentionally omitted.
26
+ USDC: { assetId: 31566704, decimals: 6, symbol: "USDC" }
27
+ }
28
+ };
29
+ function algorandAssetId(assetId) {
30
+ return String(assetId);
31
+ }
32
+ function parseAlgorandAssetId(asset) {
33
+ if (asset === "native") return null;
34
+ if (!/^\d+$/.test(asset)) return null;
35
+ const n = Number(asset);
36
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
37
+ }
38
+
39
+ // src/drivers/algorand/pay.ts
40
+ async function payAlgorand(params) {
41
+ const { client, sk, sender, accept } = params;
42
+ const note = new TextEncoder().encode(accept.extra.nonce);
43
+ const amount = BigInt(accept.amount);
44
+ const assetId = parseAlgorandAssetId(accept.asset);
45
+ try {
46
+ const { txn, txId } = await client.build({
47
+ sender,
48
+ receiver: accept.payTo,
49
+ amount,
50
+ note,
51
+ ...assetId === null ? {} : { assetId }
52
+ });
53
+ await client.signSend({ txn, sk });
54
+ return txId;
55
+ } catch (err) {
56
+ const mapped = mapAlgorandError(err, accept.payTo);
57
+ if (mapped) throw mapped;
58
+ throw _nullishCoalesce(_chunkIQGT65WScjs.toInsufficientFundsError.call(void 0, err), () => ( err));
59
+ }
60
+ }
61
+ function mapAlgorandError(err, payTo) {
62
+ const m = err instanceof Error ? err.message : String(err);
63
+ if (/must optin/i.test(m) || /missing from/i.test(m) && m.includes(payTo)) {
64
+ return new (0, _chunkIQGT65WScjs.RecipientNotReadyError)(
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
+ { cause: err }
67
+ );
68
+ }
69
+ if (/overspend|below min|min(imum)? balance|tried to spend|balance \d+ below|asset \d+ missing from|insufficient|underflow/i.test(
70
+ m
71
+ )) {
72
+ return new (0, _chunkIQGT65WScjs.InsufficientFundsError)(
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
+ { cause: err }
75
+ );
76
+ }
77
+ return null;
78
+ }
79
+ function firstLine(message) {
80
+ return message.split("\n")[0].slice(0, 160);
81
+ }
82
+
83
+ // src/drivers/algorand/verify.ts
84
+ async function verifyAlgorand(params) {
85
+ const { reader, accept } = params;
86
+ const nonce = accept.extra.nonce;
87
+ const required = BigInt(accept.amount);
88
+ const wantAssetId = parseAlgorandAssetId(accept.asset);
89
+ let txs;
90
+ try {
91
+ txs = await reader.transactionsForAccount(accept.payTo, 50);
92
+ } catch (e) {
93
+ return rpcFailed(nonce);
94
+ }
95
+ const tx = txs.find((t) => typeof t.note === "string" && t.note === nonce);
96
+ if (!tx) return notFound(nonce);
97
+ if (typeof tx.roundTime === "number") {
98
+ const ageSeconds = Math.floor(Date.now() / 1e3) - tx.roundTime;
99
+ if (Number.isFinite(ageSeconds) && ageSeconds > accept.maxTimeoutSeconds) {
100
+ return {
101
+ ok: false,
102
+ error: "payment_expired",
103
+ detail: `Payment is ${ageSeconds}s old; max allowed is ${accept.maxTimeoutSeconds}s.`
104
+ };
105
+ }
106
+ }
107
+ const isNative = wantAssetId === null;
108
+ const typeOk = isNative ? tx.txType === "pay" : tx.txType === "axfer";
109
+ const assetOk = isNative ? tx.assetId == null : tx.assetId === wantAssetId;
110
+ if (!typeOk || tx.receiver !== accept.payTo || !assetOk) {
111
+ return {
112
+ ok: false,
113
+ error: "transfer_not_found",
114
+ detail: `Algorand tx ${tx.id} carries our nonce but has no matching ${isNative ? "ALGO" : `ASA ${wantAssetId}`} transfer to ${accept.payTo}.`
115
+ };
116
+ }
117
+ let paid = 0n;
118
+ try {
119
+ paid = tx.amount ? BigInt(tx.amount) : 0n;
120
+ } catch (e2) {
121
+ paid = 0n;
122
+ }
123
+ if (paid < required) {
124
+ return { ok: false, error: "amount_too_low", detail: `Paid ${paid}, required ${required}.` };
125
+ }
126
+ return {
127
+ ok: true,
128
+ receipt: {
129
+ scheme: "onchain-proof",
130
+ success: true,
131
+ network: accept.network,
132
+ transaction: tx.id,
133
+ asset: accept.asset,
134
+ amount: accept.amount,
135
+ payer: _nullishCoalesce(tx.sender, () => ( "")),
136
+ payTo: accept.payTo,
137
+ verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
138
+ }
139
+ };
140
+ }
141
+ function notFound(nonce) {
142
+ return {
143
+ ok: false,
144
+ error: "transfer_not_found",
145
+ detail: `No matching Algorand payment found for nonce ${nonce} (not yet settled, or wrong recipient/amount/asset/note).`
146
+ };
147
+ }
148
+ function rpcFailed(nonce) {
149
+ return {
150
+ ok: false,
151
+ error: "tx_not_found",
152
+ detail: `Could not read the Algorand indexer for nonce ${nonce} (transient RPC failure) \u2014 retry.`
153
+ };
154
+ }
155
+
156
+ // src/drivers/algorand/wallet.ts
157
+
158
+ function assertAlgorandWallet(wallet, network) {
159
+ if (typeof wallet !== "object" || wallet === null) {
160
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
161
+ `chain ${network} is Algorand; wallet must be { mnemonic } (25 words) or { account }.`
162
+ );
163
+ }
164
+ if ("privateKey" in wallet || "walletClient" in wallet) {
165
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
166
+ `chain ${network} is Algorand; an EVM/Aptos wallet can't be used \u2014 pass { mnemonic } (25 words) or { account }.`
167
+ );
168
+ }
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, _chunkIQGT65WScjs.WrongFamilyError)(
171
+ `chain ${network} is Algorand; that looks like another family's wallet \u2014 pass { mnemonic } (25 words) or { account }.`
172
+ );
173
+ }
174
+ if (!("mnemonic" in wallet) && !("account" in wallet)) {
175
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
176
+ `chain ${network} is Algorand; wallet must be { mnemonic } (25 words) or { account }.`
177
+ );
178
+ }
179
+ return wallet;
180
+ }
181
+ function resolveAlgorandWallet(config) {
182
+ if (config.account) {
183
+ return { addr: String(config.account.addr), sk: config.account.sk };
184
+ }
185
+ if (config.mnemonic != null) {
186
+ try {
187
+ const { addr, sk } = _algosdk2.default.mnemonicToSecretKey(config.mnemonic);
188
+ return { addr: addr.toString(), sk };
189
+ } catch (cause) {
190
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
191
+ "Algorand wallet { mnemonic } is not a valid 25-word Algorand mnemonic.",
192
+ { cause }
193
+ );
194
+ }
195
+ }
196
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)("Algorand wallet needs { mnemonic } (25 words) or { account }.");
197
+ }
198
+
199
+ // src/drivers/algorand/index.ts
200
+ var algorandDriver = {
201
+ family: "algorand",
202
+ resolve(opts) {
203
+ if (opts.chain !== "algorand") return null;
204
+ const algodUrl = _nullishCoalesce(opts.rpcUrl, () => ( ALGORAND_MAINNET.defaultAlgod));
205
+ return makeAlgorandNetwork(ALGORAND_MAINNET, algodUrl);
206
+ }
207
+ };
208
+ function makeAlgorandNetwork(preset, algodUrl) {
209
+ const algod = new _algosdk2.default.Algodv2("", algodUrl, "");
210
+ const indexer = new _algosdk2.default.Indexer("", preset.defaultIndexer, "");
211
+ const network = preset.caip2;
212
+ const reader = {
213
+ async transactionsForAccount(account, limit) {
214
+ const res = await indexer.lookupAccountTransactions(account).limit(limit).do();
215
+ return (_nullishCoalesce(res.transactions, () => ( []))).map((t) => adaptTxn(t)).filter((r) => r !== null);
216
+ }
217
+ };
218
+ const payClient = {
219
+ async build(transfer) {
220
+ const suggestedParams = await algod.getTransactionParams().do();
221
+ const common = {
222
+ sender: transfer.sender,
223
+ receiver: transfer.receiver,
224
+ amount: transfer.amount,
225
+ note: transfer.note,
226
+ suggestedParams
227
+ };
228
+ const txn = transfer.assetId === void 0 ? _algosdk2.default.makePaymentTxnWithSuggestedParamsFromObject(common) : _algosdk2.default.makeAssetTransferTxnWithSuggestedParamsFromObject({
229
+ ...common,
230
+ assetIndex: transfer.assetId
231
+ });
232
+ return { txn, txId: txn.txID() };
233
+ },
234
+ async signSend({ txn, sk }) {
235
+ const signed = txn.signTxn(sk);
236
+ await algod.sendRawTransaction(signed).do();
237
+ }
238
+ };
239
+ return {
240
+ family: "algorand",
241
+ network,
242
+ supports: (n) => n === network,
243
+ resolveToken(token) {
244
+ if (token === "native") {
245
+ return { asset: "native", decimals: ALGO_DECIMALS, symbol: ALGO_SYMBOL };
246
+ }
247
+ if (typeof token === "string") {
248
+ const info = preset.tokens[token.toUpperCase()];
249
+ if (!info) {
250
+ const known = Object.keys(preset.tokens).join(", ") || "(none built in)";
251
+ throw new (0, _chunkIQGT65WScjs.UnknownTokenError)(
252
+ `token "${token}" isn't built in for Algorand (known: ${known}). Pass { assetId, decimals } for a custom ASA, or use 'native'.`
253
+ );
254
+ }
255
+ return { asset: algorandAssetId(info.assetId), decimals: info.decimals, symbol: info.symbol };
256
+ }
257
+ _chunkIQGT65WScjs.rejectForeignToken.call(void 0, token, "algorand", network);
258
+ const t = token;
259
+ if (typeof t.assetId !== "number" || typeof t.decimals !== "number") {
260
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
261
+ `chain ${network} is Algorand; a custom token must be { assetId, decimals }.`
262
+ );
263
+ }
264
+ return {
265
+ asset: algorandAssetId(t.assetId),
266
+ decimals: t.decimals,
267
+ ...t.symbol ? { symbol: t.symbol } : {}
268
+ };
269
+ },
270
+ describeAsset(asset) {
271
+ if (asset === "native") return { symbol: ALGO_SYMBOL, decimals: ALGO_DECIMALS };
272
+ for (const info of Object.values(preset.tokens)) {
273
+ if (algorandAssetId(info.assetId) === asset) {
274
+ return { symbol: info.symbol, decimals: info.decimals };
275
+ }
276
+ }
277
+ return null;
278
+ },
279
+ assertValidPayTo(payTo) {
280
+ if (payTo.startsWith("0x")) {
281
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
282
+ `chain ${network} is Algorand, but payTo "${payTo}" looks like an EVM address.`
283
+ );
284
+ }
285
+ if (!_algosdk2.default.isValidAddress(payTo)) {
286
+ throw new (0, _chunkIQGT65WScjs.WrongFamilyError)(
287
+ `chain ${network} is Algorand, but payTo "${payTo}" is not a valid Algorand address.`
288
+ );
289
+ }
290
+ },
291
+ bindWallet(wallet) {
292
+ return { _native: assertAlgorandWallet(wallet, network) };
293
+ },
294
+ async send(wallet, accept) {
295
+ const signer = resolveAlgorandWallet(wallet._native);
296
+ return payAlgorand({ client: payClient, sk: signer.sk, sender: signer.addr, accept });
297
+ },
298
+ async confirm(ref) {
299
+ try {
300
+ const info = await _algosdk2.default.waitForConfirmation(algod, ref, 10);
301
+ return { height: String(_nullishCoalesce(info.confirmedRound, () => ( 0))) };
302
+ } catch (err) {
303
+ throw new (0, _chunkIQGT65WScjs.ConfirmationTimeoutError)(`Algorand tx ${ref} did not confirm in time.`, {
304
+ cause: err
305
+ });
306
+ }
307
+ },
308
+ async estimateCost() {
309
+ return _chunkIQGT65WScjs.nativeCost.call(void 0, {
310
+ symbol: ALGO_SYMBOL,
311
+ decimals: ALGO_DECIMALS,
312
+ fee: 1000n,
313
+ basis: "heuristic",
314
+ detail: "min fee 1000 \xB5Algos (1 transaction)"
315
+ });
316
+ },
317
+ async verify(_ref, accept) {
318
+ return verifyAlgorand({ reader, accept });
319
+ }
320
+ };
321
+ }
322
+ function adaptTxn(raw) {
323
+ const t = raw;
324
+ if (!t || typeof t.id !== "string") return null;
325
+ const note = t.note && t.note.length ? decodeNote(t.note) : void 0;
326
+ const base = {
327
+ id: t.id,
328
+ txType: String(_nullishCoalesce(t.txType, () => ( ""))),
329
+ ...note !== void 0 ? { note } : {},
330
+ ...t.sender != null ? { sender: String(t.sender) } : {},
331
+ ...typeof t.roundTime === "number" ? { roundTime: t.roundTime } : {}
332
+ };
333
+ if (t.paymentTransaction) {
334
+ const pt = t.paymentTransaction;
335
+ return {
336
+ ...base,
337
+ txType: "pay",
338
+ ...pt.receiver != null ? { receiver: String(pt.receiver) } : {},
339
+ ...pt.amount != null ? { amount: String(pt.amount) } : {}
340
+ };
341
+ }
342
+ if (t.assetTransferTransaction) {
343
+ const att = t.assetTransferTransaction;
344
+ return {
345
+ ...base,
346
+ txType: "axfer",
347
+ ...att.receiver != null ? { receiver: String(att.receiver) } : {},
348
+ ...att.amount != null ? { amount: String(att.amount) } : {},
349
+ ...att.assetId != null ? { assetId: Number(att.assetId) } : {}
350
+ };
351
+ }
352
+ return base;
353
+ }
354
+ function decodeNote(bytes) {
355
+ try {
356
+ return new TextDecoder().decode(bytes);
357
+ } catch (e3) {
358
+ return void 0;
359
+ }
360
+ }
361
+
362
+
363
+ exports.algorandDriver = algorandDriver;