@piprail/sdk 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/DISCOVERY.md +420 -0
- package/ERRORS.md +9 -0
- package/README.md +86 -4
- package/STANDARDS.md +4 -4
- package/dist/index.cjs +647 -27
- package/dist/index.d.cts +392 -2
- package/dist/index.d.ts +392 -2
- package/dist/index.js +624 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -113,11 +113,91 @@ For **every rail the 402 offers on your chain**, the plan reads **token balance
|
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
115
|
import { paymentTools } from '@piprail/sdk'
|
|
116
|
-
const tools = paymentTools(client) // → [piprail_quote_payment, piprail_plan_payment, piprail_pay_request]
|
|
116
|
+
const tools = paymentTools(client) // → [piprail_discover, piprail_quote_payment, piprail_plan_payment, piprail_pay_request, piprail_register]
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
Each descriptor also carries advisory **`annotations`** (MCP-style `ToolAnnotations` — `title`,
|
|
120
|
+
`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`): the three reads are flagged
|
|
121
|
+
**read-only**, `piprail_pay_request` is flagged **value-moving** (the one tool that spends), and
|
|
122
|
+
`piprail_register` is non-destructive — so an MCP client can render the right consent. They're hints,
|
|
123
|
+
not the boundary; the spend policy is. `@piprail/mcp` advertises them on the wire.
|
|
124
|
+
|
|
119
125
|
See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
|
|
120
126
|
|
|
127
|
+
## Be discoverable — find and be found ($0, no backend)
|
|
128
|
+
|
|
129
|
+
A 402 endpoint is payable, but nobody can *find* it. PipRail closes that gap by building on the
|
|
130
|
+
**open** x402 indexes that already exist (402 Index, the CDP Bazaar read API, x402scan) — **nothing
|
|
131
|
+
PipRail-hosted, no registry, no database.** All opt-in; the pay path is untouched. There's **no PipRail
|
|
132
|
+
account and no x402 sign-up anywhere** — the only thing ever registered is a merchant's own URL, by one
|
|
133
|
+
call. The complete reference — including a **step-by-step walkthrough of exactly what each step needs**
|
|
134
|
+
(wallet? signing? sign-up? — spoiler: 402 Index needs none) — is **[DISCOVERY.md](./DISCOVERY.md)** (§7).
|
|
135
|
+
|
|
136
|
+
> **Experimental.** Discovery integrates with third-party open indexes whose conventions are young
|
|
137
|
+
> and moving — treat this layer as experimental. The read path + 402 Index register are live-verified;
|
|
138
|
+
> x402scan SIWX isn't yet. Note **402 Index probes your URL and only lists endpoints that actually
|
|
139
|
+
> return a `402`** — so register a *deployed* gate, not a marketing page. (DISCOVERY.md §10 has the log.)
|
|
140
|
+
|
|
141
|
+
**1) List a resource you run** — one call, no auth, no signature:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const client = new PipRailClient({ wallet: { privateKey: KEY }, chain: 'base' })
|
|
145
|
+
|
|
146
|
+
await client.register('https://api.example.com/report', { name: 'Market Report', priceUsd: 0.05 })
|
|
147
|
+
// → [{ source: '402index', ok: true, detail: 'Listed on 402 Index (searchable at 402index.io).' }]
|
|
148
|
+
// Add targets: ['402index', 'x402scan'] to also register on x402scan via one wallet signature
|
|
149
|
+
// (SIWX — Base/Solana only). It moves no funds; you're listing on third-party open directories.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Works on every chain.** 402 Index needs no signature and has no chain allowlist, so *any* chain —
|
|
153
|
+
a preset, a non-EVM family, or a custom `{ id, rpcUrl }` chain — can be listed and found; `discover()`
|
|
154
|
+
never silently hides a resource whose chain it can't resolve. (x402scan is the one Base/Solana-only
|
|
155
|
+
*bonus* target.)
|
|
156
|
+
|
|
157
|
+
**Built-with attribution (tasteful, honest).** Your emitted `/openapi.json` carries an
|
|
158
|
+
`x-generator: "@piprail/sdk"` stamp by default (opt out with `attribution: false`), and every index
|
|
159
|
+
request sends a `User-Agent: @piprail/sdk` — so the tech spreads through the files indexes crawl and
|
|
160
|
+
the logs operators read, never by spamming listings. An opt-in `register(url, { attribution: true })`
|
|
161
|
+
adds a best-effort `via` tag; it's off by default (it's your listing).
|
|
162
|
+
|
|
163
|
+
**2) Find resources to pay** — read the open indexes (free), filtered to your chain by default:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const hits = await client.discover({ query: 'weather', maxPrice: 0.01 })
|
|
167
|
+
// → [{ resource, name, source, priceUsd, rails: [...] }, …]
|
|
168
|
+
const res = await client.fetch(hits[0].resource) // then quote → plan → pay as usual
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`network` defaults to `'self'` (your chain only); pass `'any'` to search every chain, or a CAIP-2 id
|
|
172
|
+
(`'eip155:8453'`) for a specific one. Chain slugs map to CAIP-2 through the `SLUG_TO_CAIP2` table —
|
|
173
|
+
adding a chain adds one entry there (see [DISCOVERY.md](./DISCOVERY.md) §2.5), and an unresolved
|
|
174
|
+
network is kept, never hidden.
|
|
175
|
+
|
|
176
|
+
**3) Emit a discovery file** — turn your gate's config into the artifacts a crawler reads (pure, no
|
|
177
|
+
I/O); serve the result as a static file on **your own** origin:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { createPaymentGate, buildOpenApi, buildX402DnsTxt } from '@piprail/sdk'
|
|
181
|
+
|
|
182
|
+
const gate = createPaymentGate({ chain: 'base', token: 'USDC', amount: '0.05', payTo })
|
|
183
|
+
const openapi = buildOpenApi({
|
|
184
|
+
origin: 'https://api.example.com',
|
|
185
|
+
resources: [await gate.describe('https://api.example.com/report')],
|
|
186
|
+
})
|
|
187
|
+
// serve `openapi` at https://api.example.com/openapi.json — each priced op carries an `x-payment-info`
|
|
188
|
+
// block (the field indexes crawl) plus a default root `x-generator` attribution stamp.
|
|
189
|
+
// buildWellKnownX402(...) emits the legacy /.well-known/x402 file; buildX402DnsTxt(...) the _x402 DNS line
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
For an LLM/MCP these are two more tools — **`piprail_discover`** (find) and **`piprail_register`**
|
|
193
|
+
(be found) — on top of the three payment tools, so `paymentTools(client)` / `@piprail/mcp` expose **five**.
|
|
194
|
+
|
|
195
|
+
> **Two honest caveats.** The open indexes assume the mainstream `exact` scheme, so to be *usefully*
|
|
196
|
+
> listed also offer a standard `exact` USDC rail on Base/Solana (`discover()` results are
|
|
197
|
+
> cross-scheme; `fetch()` pays only PipRail `onchain-proof` rails directly). And **x402scan indexes
|
|
198
|
+
> Base/Solana only** — 402 Index has no such limit, so it's the default register target. There is no
|
|
199
|
+
> single ratified discovery standard yet; OpenAPI-first is an emerging multi-vendor convention.
|
|
200
|
+
|
|
121
201
|
### Accept several chains at once
|
|
122
202
|
|
|
123
203
|
`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:
|
|
@@ -560,7 +640,7 @@ The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
|
|
|
560
640
|
|
|
561
641
|
## API
|
|
562
642
|
|
|
563
|
-
**`requirePayment(options)`** → Express middleware · **`createPaymentGate(options)`** → `{ challenge, verify }`
|
|
643
|
+
**`requirePayment(options)`** → Express middleware · **`createPaymentGate(options)`** → `{ challenge, verify, describe }` (`describe()` → static discovery metadata for the emitters)
|
|
564
644
|
|
|
565
645
|
| Option | Default | Notes |
|
|
566
646
|
|---|---|---|
|
|
@@ -592,7 +672,9 @@ Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `acc
|
|
|
592
672
|
| `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
|
|
593
673
|
| `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` |
|
|
594
674
|
|
|
595
|
-
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.
|
|
675
|
+
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) · **`discover(opts?)`** (find resources on the open indexes → `DiscoveredResource[]`) · **`register(url, opts?)`** (list a resource on the open indexes → `RegisterOutcome[]`) · **`discoverySigner()`** (the wallet's discovery signer, EVM today, or `null`). 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.
|
|
676
|
+
|
|
677
|
+
**Discovery (opt-in, $0, nothing hosted):** `client.discover()` / `client.register()`, the standalone `searchOpenIndexes` / `register402Index` / `registerX402Scan`, and the pure emitters `buildOpenApi` / `buildWellKnownX402` / `buildX402DnsTxt` (fed by `gate.describe()`). See [Be discoverable](#be-discoverable--find-and-be-found-0-no-backend).
|
|
596
678
|
|
|
597
679
|
**Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
|
|
598
680
|
|
|
@@ -609,7 +691,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
|
|
|
609
691
|
| Aptos | `{ privateKey }` (ed25519-priv-0x… AIP-80) or `{ account }` |
|
|
610
692
|
| Algorand | `{ mnemonic }` (25 words) or `{ account }` (algosdk `{ addr, sk }`) |
|
|
611
693
|
|
|
612
|
-
**Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling
|
|
694
|
+
**Hand an LLM a wallet:** `paymentTools(client)` → five framework-agnostic tool descriptors (`piprail_discover` · `piprail_quote_payment` · `piprail_plan_payment` · `piprail_pay_request` · `piprail_register`) for MCP / AI SDK / function-calling, budget enforced by the client.
|
|
613
695
|
|
|
614
696
|
**Bring your own chain family:** the SDK is built on a tiny `PaymentDriver` contract — `resolve(chain)` returns a bound network with `resolveToken` / `describeAsset` / `assertValidPayTo` / `bindWallet` / `send` / `confirm` / `estimateCost` / `balanceOf` / `recipientReady` / `verify`. Register your own with `registerDriver(...)`; the protocol layer never changes (see [Architecture](#architecture-under-the-hood)).
|
|
615
697
|
|
package/STANDARDS.md
CHANGED
|
@@ -25,7 +25,7 @@ zero-config path still read in one line? If a feature can't be opt-in, reconside
|
|
|
25
25
|
## 1. The layering (never violate)
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
protocol layer index · server · client · x402 · policy · ledger · agent · errors · util/*
|
|
28
|
+
protocol layer index · server · client · x402 · policy · ledger · agent · discovery · indexes · errors · util/*
|
|
29
29
|
(chain-agnostic — ZERO viem / @solana / @ton / @stellar imports)
|
|
30
30
|
│ depends only on …
|
|
31
31
|
▼
|
|
@@ -36,9 +36,9 @@ chain drivers drivers/<family>/ chains · wallet · pay · verify · index
|
|
|
36
36
|
registry.ts (routes a chain → family) index.ts (eager EVM + lazy auto-mount of the rest)
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
- **The protocol layer is chain-agnostic.** `server`/`client`/`x402`/`policy`/`ledger`/`agent
|
|
40
|
-
import only `drivers/types.ts` + pure utils — never a chain library.
|
|
41
|
-
lazy-chunk invariant (below).
|
|
39
|
+
- **The protocol layer is chain-agnostic.** `server`/`client`/`x402`/`policy`/`ledger`/`agent`/
|
|
40
|
+
`discovery`/`indexes` import only `drivers/types.ts` + pure utils — never a chain library.
|
|
41
|
+
Verified by the lazy-chunk invariant (below).
|
|
42
42
|
- **Drivers mirror each other** file-for-file (`chains`/`wallet`/`pay`/`verify`/`index`),
|
|
43
43
|
functions family-suffixed (`payEvm`/`verifyStellar`). A new contract method is implemented in
|
|
44
44
|
**all** families.
|