@piprail/sdk 1.5.1 → 1.7.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/README.md CHANGED
@@ -52,7 +52,7 @@ const client = new PipRailClient({
52
52
  maxAmount: '0.10', // never pay more than $0.10 for one call
53
53
  maxTotal: '5.00', // never spend more than $5 total (per token)
54
54
  chains: ['base'], // only on Base
55
- tokens: ['USDC'], // only in USDC
55
+ tokens: ['USDC'], // only in USDC (use 'native' to also allow the chain's coin)
56
56
  hosts: ['*.example.com'], // only these hosts
57
57
  },
58
58
  onBeforePay: (q) => Number(q.amountFormatted) <= 0.05, // final say on each payment
@@ -75,6 +75,8 @@ client.spent() // → { count, byAsset: [{ symbol:'USDC', totalFormatted:'0.05',
75
75
 
76
76
  **The budget can't be fooled.** `maxAmount`/`maxTotal` are enforced against the token's **true** decimals (the SDK's own, via the driver) — a server can't slip past a cap by understating the price, and an asset the SDK can't recognise is refused unless you set `allowUnknownTokens`. `quote()` even flags a `symbolMismatch` when a challenge's stated symbol disagrees with the real token.
77
77
 
78
+ **`policy.tokens` takes symbols *or* `'native'`.** List stablecoin symbols (`'USDC'`, `'USDT'`, …) and/or the chain-agnostic alias **`'native'`** to allow the chain's own coin (ETH/BNB/TRX/XLM/…) on any family — the same word the accept side uses (`token: 'native'`), so you never name per-chain tickers (the real ticker works too). It only ever matches a genuinely native asset, so it never loosens a stablecoin-only list. The MCP server's `PIPRAIL_TOKENS` is the same allowlist.
79
+
78
80
  **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
81
 
80
82
  ### Plan before you pay — `planPayment()` (never fumble a payment)
@@ -116,6 +118,73 @@ const tools = paymentTools(client) // → [piprail_quote_payment, piprail_plan_p
116
118
 
117
119
  See [`examples/agent-tools.mjs`](../examples/agent-tools.mjs) for MCP / AI-SDK wiring.
118
120
 
121
+ ## Be discoverable — find and be found ($0, no backend)
122
+
123
+ A 402 endpoint is payable, but nobody can *find* it. PipRail closes that gap by building on the
124
+ **open** x402 indexes that already exist (402 Index, the CDP Bazaar read API, x402scan) — **nothing
125
+ PipRail-hosted, no registry, no database.** All opt-in; the pay path is untouched. There's **no PipRail
126
+ account and no x402 sign-up anywhere** — the only thing ever registered is a merchant's own URL, by one
127
+ call. The complete reference — including a **step-by-step walkthrough of exactly what each step needs**
128
+ (wallet? signing? sign-up? — spoiler: 402 Index needs none) — is **[DISCOVERY.md](./DISCOVERY.md)** (§7).
129
+
130
+ > **Experimental.** Discovery integrates with third-party open indexes whose conventions are young
131
+ > and moving — treat this layer as experimental. The read path + 402 Index register are live-verified;
132
+ > x402scan SIWX isn't yet. Note **402 Index probes your URL and only lists endpoints that actually
133
+ > return a `402`** — so register a *deployed* gate, not a marketing page. (DISCOVERY.md §10 has the log.)
134
+
135
+ **1) List a resource you run** — one call, no auth, no signature:
136
+
137
+ ```ts
138
+ const client = new PipRailClient({ wallet: { privateKey: KEY }, chain: 'base' })
139
+
140
+ await client.register('https://api.example.com/report', { name: 'Market Report', priceUsd: 0.05 })
141
+ // → [{ source: '402index', ok: true, detail: 'Listed on 402 Index (searchable at 402index.io).' }]
142
+ // Add targets: ['402index', 'x402scan'] to also register on x402scan via one wallet signature
143
+ // (SIWX — Base/Solana only). It moves no funds; you're listing on third-party open directories.
144
+ ```
145
+
146
+ **Works on every chain.** 402 Index needs no signature and has no chain allowlist, so *any* chain —
147
+ a preset, a non-EVM family, or a custom `{ id, rpcUrl }` chain — can be listed and found; `discover()`
148
+ never silently hides a resource whose chain it can't resolve. (x402scan is the one Base/Solana-only
149
+ *bonus* target.)
150
+
151
+ **Built-with attribution (tasteful, honest).** Your emitted `/openapi.json` carries an
152
+ `x-generator: "@piprail/sdk"` stamp by default (opt out with `attribution: false`), and every index
153
+ request sends a `User-Agent: @piprail/sdk` — so the tech spreads through the files indexes crawl and
154
+ the logs operators read, never by spamming listings. An opt-in `register(url, { attribution: true })`
155
+ adds a best-effort `via` tag; it's off by default (it's your listing).
156
+
157
+ **2) Find resources to pay** — read the open indexes (free), filtered to your chain by default:
158
+
159
+ ```ts
160
+ const hits = await client.discover({ query: 'weather', maxPrice: 0.01 })
161
+ // → [{ resource, name, source, priceUsd, rails: [...] }, …]
162
+ const res = await client.fetch(hits[0].resource) // then quote → plan → pay as usual
163
+ ```
164
+
165
+ **3) Emit a discovery file** — turn your gate's config into the artifacts a crawler reads (pure, no
166
+ I/O); serve the result as a static file on **your own** origin:
167
+
168
+ ```ts
169
+ import { createPaymentGate, buildOpenApi, buildX402DnsTxt } from '@piprail/sdk'
170
+
171
+ const gate = createPaymentGate({ chain: 'base', token: 'USDC', amount: '0.05', payTo })
172
+ const openapi = buildOpenApi({
173
+ origin: 'https://api.example.com',
174
+ resources: [await gate.describe('https://api.example.com/report')],
175
+ })
176
+ // serve `openapi` at https://api.example.com/openapi.json (the OpenAPI-first convention indexes parse)
177
+ // buildWellKnownX402(...) emits the legacy /.well-known/x402 file; buildX402DnsTxt(...) the _x402 DNS line
178
+ ```
179
+
180
+ For an LLM/MCP this is two tools: **`piprail_discover`** (find) and **`piprail_register`** (be found).
181
+
182
+ > **Two honest caveats.** The open indexes assume the mainstream `exact` scheme, so to be *usefully*
183
+ > listed also offer a standard `exact` USDC rail on Base/Solana (`discover()` results are
184
+ > cross-scheme; `fetch()` pays only PipRail `onchain-proof` rails directly). And **x402scan indexes
185
+ > Base/Solana only** — 402 Index has no such limit, so it's the default register target. There is no
186
+ > single ratified discovery standard yet; OpenAPI-first is an emerging multi-vendor convention.
187
+
119
188
  ### Accept several chains at once
120
189
 
121
190
  `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:
@@ -558,7 +627,7 @@ The full standard every module follows is **[ERRORS.md](./ERRORS.md)**.
558
627
 
559
628
  ## API
560
629
 
561
- **`requirePayment(options)`** → Express middleware &nbsp;·&nbsp; **`createPaymentGate(options)`** → `{ challenge, verify }`
630
+ **`requirePayment(options)`** → Express middleware &nbsp;·&nbsp; **`createPaymentGate(options)`** → `{ challenge, verify, describe }` (`describe()` → static discovery metadata for the emitters)
562
631
 
563
632
  | Option | Default | Notes |
564
633
  |---|---|---|
@@ -590,7 +659,9 @@ Provide **either** `chain` + `token` + `amount` (single) **or** a non-empty `acc
590
659
  | `retryTimeoutMs` | `30000` | Timeout for the retry leg after broadcast |
591
660
  | `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` |
592
661
 
593
- 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.
662
+ 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.
663
+
664
+ **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).
594
665
 
595
666
  **Wallets by family** — the `chain` selector routes; each driver validates its own key format (a mismatch throws `WrongFamilyError`):
596
667
 
@@ -607,7 +678,7 @@ Methods: `fetch` · `get` · `post` (return the gated `Response` after settlemen
607
678
  | Aptos | `{ privateKey }` (ed25519-priv-0x… AIP-80) or `{ account }` |
608
679
  | Algorand | `{ mnemonic }` (25 words) or `{ account }` (algosdk `{ addr, sk }`) |
609
680
 
610
- **Hand an LLM a wallet:** `paymentTools(client)` → framework-agnostic tool descriptors (MCP / AI SDK / function-calling), budget enforced by the client.
681
+ **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.
611
682
 
612
683
  **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)).
613
684
 
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. Verified by the
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.