@piprail/sdk 1.14.0 → 1.15.1
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 +40 -0
- package/README.md +27 -754
- package/dist/{algorand-OIHGJN5S.cjs → algorand-EJ3S2V7E.cjs} +17 -17
- package/dist/{algorand-7EUZYL2Z.js → algorand-F3OYB534.js} +1 -1
- package/dist/{aptos-WDWZOU25.cjs → aptos-GJGIZHNI.cjs} +16 -16
- package/dist/{aptos-CDEYDDM5.js → aptos-SUXOVP7B.js} +1 -1
- package/dist/{chunk-H3A4KWLJ.js → chunk-ILPABTI2.js} +6 -0
- package/dist/{chunk-FTKVCP6K.cjs → chunk-PA6YD3HL.cjs} +17 -11
- package/dist/index.cjs +493 -115
- package/dist/index.d.cts +264 -12
- package/dist/index.d.ts +264 -12
- package/dist/index.js +403 -25
- package/dist/{near-DT6LRIKB.js → near-LM7S3WUD.js} +1 -1
- package/dist/{near-FUH3VAXT.cjs → near-ZJLZE26R.cjs} +19 -19
- package/dist/{solana-QUVXPKBZ.cjs → solana-MPPE6K24.cjs} +14 -14
- package/dist/{solana-3TRYD4QB.js → solana-WDKWWF33.js} +1 -1
- package/dist/{stellar-IK3UML6O.js → stellar-FIJPQZVW.js} +1 -1
- package/dist/{stellar-APZEBFAD.cjs → stellar-XHLLNHQP.cjs} +21 -21
- package/dist/{sui-L7BQNJWO.cjs → sui-6CVLEXLA.cjs} +17 -17
- package/dist/{sui-VSE63WQM.js → sui-B7AVN7NK.js} +1 -1
- package/dist/{ton-QHGQLJX2.js → ton-CHJ26BVA.js} +1 -1
- package/dist/{ton-5DLKKOFE.cjs → ton-RNEFN25G.cjs} +14 -14
- package/dist/{tron-2N2GA62O.js → tron-DD3JDROV.js} +1 -1
- package/dist/{tron-HHIT6WKY.cjs → tron-TKJHNFGM.cjs} +24 -24
- package/dist/{xrpl-2GZMDYW5.js → xrpl-GTUPP6SK.js} +1 -1
- package/dist/{xrpl-USEG4AHX.cjs → xrpl-XN2NBNGI.cjs} +21 -21
- package/package.json +1 -5
- package/CHAINS.md +0 -179
- package/DISCOVERY.md +0 -420
- package/ERRORS.md +0 -269
- package/STANDARDS.md +0 -128
package/DISCOVERY.md
DELETED
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
# PipRail discovery — the complete reference
|
|
2
|
-
|
|
3
|
-
How a PipRail user — a human merchant **or** an AI agent — becomes **discoverable**, and how an
|
|
4
|
-
agent **finds** payable resources. This is the single source of truth for the discovery feature.
|
|
5
|
-
Companion docs: [README.md](./README.md) (the full API), [STANDARDS.md](./STANDARDS.md) (how it's
|
|
6
|
-
built), [ERRORS.md](./ERRORS.md) (the error model). Background research lives in
|
|
7
|
-
`.claude/research/x402-discovery.md` (the "what is this") and `x402-discovery-integration.md` (the
|
|
8
|
-
"exactly how").
|
|
9
|
-
|
|
10
|
-
> **One line:** PipRail makes you discoverable by building on the **open** x402 indexes that already
|
|
11
|
-
> exist (402 Index, the CDP Bazaar read API, x402scan) — **it hosts nothing of its own**: no
|
|
12
|
-
> registry, no database, no backend, no fee. Every piece is opt-in; the pay path is untouched.
|
|
13
|
-
|
|
14
|
-
> **⚠️ Status: EXPERIMENTAL.** Discovery integrates with **third-party** open indexes whose wire
|
|
15
|
-
> shapes are a moving, unratified convention — treat this whole layer as experimental and expect to
|
|
16
|
-
> re-verify the integration over time. The **read** path + the **402 Index register** flow are
|
|
17
|
-
> live-verified (see the log in §10); **x402scan SIWX is not yet live-tested** — exercise it against
|
|
18
|
-
> x402scan before relying on it. The pay path and the rest of the SDK are stable; only this layer
|
|
19
|
-
> carries the experimental flag.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 1. The problem (discovery is NOT part of x402)
|
|
24
|
-
|
|
25
|
-
The x402 protocol answers exactly one question: *"how do I pay for THIS url?"* You hit a URL, get a
|
|
26
|
-
`402` with a machine-readable challenge, pay on-chain, retry with proof, get `200`. It does **not**
|
|
27
|
-
answer *"what payable URLs exist?"*
|
|
28
|
-
|
|
29
|
-
So a fresh PipRail merchant is in a bind:
|
|
30
|
-
|
|
31
|
-
- A **seller** adds `requirePayment()` to `https://api.acme.com/report`. It's now payable — but
|
|
32
|
-
nobody knows the URL exists. A shop with no sign, on a street with no name.
|
|
33
|
-
- A **buyer** (an AI agent with a budget-bound wallet) wants to *buy a weather feed under $0.01 on
|
|
34
|
-
Base*. It has no phone book — it can only pay URLs a human already handed it.
|
|
35
|
-
|
|
36
|
-
That missing phone book is **discovery**. It's a separate, optional layer built *around* x402.
|
|
37
|
-
|
|
38
|
-
**Why PipRail doesn't host its own directory.** A registry/database is a backend we'd run forever —
|
|
39
|
-
a bill that never reaches $0, uptime, an open write endpoint that invites spam + a moderation queue.
|
|
40
|
-
That would turn PipRail from *"a tool you `npm install`"* into *"a platform you sign up for"* — the
|
|
41
|
-
exact thing the project is defined against. So instead we **consume and contribute to the open
|
|
42
|
-
indexes that already exist**, and host nothing.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 2. The open infrastructure we build on
|
|
47
|
-
|
|
48
|
-
All three are external, open, and already running. PipRail reads from and writes to them; it operates
|
|
49
|
-
none of them.
|
|
50
|
-
|
|
51
|
-
| Index | Read (find) | Write (be listed) | Chains | PipRail role |
|
|
52
|
-
|---|---|---|---|---|
|
|
53
|
-
| **402 Index** (402index.io) | ✅ free, no auth | ✅ **`POST /register` — no auth, no signature, no payment** | any | **Primary register target** + a free read source. A superset (it also re-ingests the CDP Bazaar). |
|
|
54
|
-
| **CDP Bazaar** (api.cdp.coinbase.com) | ✅ free, no key | ❌ listed only when the CDP **facilitator settles** your payment | any | **Read-only source.** PipRail uses no facilitator, so PipRail merchants don't auto-list here — discoverability comes from 402 Index / x402scan. |
|
|
55
|
-
| **x402scan** (x402scan.com) | 💲 paid ($0.01–0.02, off by default) | ✅ **SIWX** (one wallet signature; facilitator-free) | **Base + Solana only** | **Secondary register target** (the strongest ownership model); a paid, opt-in read. |
|
|
56
|
-
|
|
57
|
-
> The honest framing: the most prominent directory, the CDP Bazaar, is a *facilitator network
|
|
58
|
-
> effect* — it lists you only when Coinbase's facilitator settles your payment, which PipRail never
|
|
59
|
-
> uses. That's why discoverability for a backendless merchant flows through 402 Index (and x402scan),
|
|
60
|
-
> not the Bazaar. PipRail can still freely **read** the Bazaar to help an agent find *other* people's
|
|
61
|
-
> endpoints.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## 2.5 Works on EVERY chain (the guarantee)
|
|
66
|
-
|
|
67
|
-
**No matter the chain — a built-in preset, a non-EVM family, or a custom `{ id, rpcUrl }` chain we
|
|
68
|
-
don't ship — a PipRail user can be indexed and found, and an agent can discover them.** This is a
|
|
69
|
-
hard guarantee, proven by the test suite (`test/discovery-e2e.test.ts` parametrizes every family +
|
|
70
|
-
a custom chain) and by running it for real:
|
|
71
|
-
|
|
72
|
-
- **Emit** is pure serialization — it works for any chain's rails, full stop.
|
|
73
|
-
- **Register** defaults to **402 Index**, which needs no signature and has no chain allowlist, so it
|
|
74
|
-
lists **every** chain. `payment_network` is optional metadata: it's the chain slug when you
|
|
75
|
-
configured the client with one (`'base'`, `'tron'`, …), and omitted for a custom `{ id, rpcUrl }`
|
|
76
|
-
chain (pass `network` explicitly if you want it). No chain is ever turned away.
|
|
77
|
-
- **Discover** filters by delegating to the bound driver's own `supports()`, and — critically — a
|
|
78
|
-
rail whose network it can't resolve to CAIP-2 is **kept, never silently hidden**. So discovery is
|
|
79
|
-
never empty on a custom or unmapped chain; at worst it returns a re-checkable extra the agent
|
|
80
|
-
confirms at quote time.
|
|
81
|
-
|
|
82
|
-
The **one** chain-limited piece is the *optional* x402scan register target (Base/Solana only, its
|
|
83
|
-
own limit). It's a bonus, never the path — 402 Index already covers everyone. So: **402 Index +
|
|
84
|
-
emit + discover = universal discovery on every chain.**
|
|
85
|
-
|
|
86
|
-
**Future chains.** A chain we add later inherits all of this for free — register and emit need no
|
|
87
|
-
discovery change at all, and `discover()` already never hides an unmapped chain. The only
|
|
88
|
-
discovery touch in the add-a-chain procedure is a one-line entry in `indexes.ts`'s `SLUG_TO_CAIP2`
|
|
89
|
-
(slug → the family's exact `caip2`), which sharpens `'self'` filtering precision; it's on the
|
|
90
|
-
`add-chain-integration` checklist. Omitting it degrades nothing that matters — the resource is
|
|
91
|
-
still found.
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
## 3. The three moves
|
|
96
|
-
|
|
97
|
-
Discovery is three opt-in capabilities. A merchant uses Emit + Register to **be found**; an agent
|
|
98
|
-
uses Discover to **find**. Defaults are byte-identical to before — omit all three and nothing changes.
|
|
99
|
-
|
|
100
|
-
### 3.1 EMIT — turn a gate's config into a discovery file (pure, no I/O)
|
|
101
|
-
|
|
102
|
-
A gate already knows its price/asset/chain/`payTo`. `gate.describe()` exposes that as static,
|
|
103
|
-
**nonce-free** metadata (discovery metadata is long-lived; a live challenge mints a nonce, this does
|
|
104
|
-
not). The three pure emitters turn it into the file formats crawlers read. The merchant serves the
|
|
105
|
-
result as a **static file on their own origin** — the one wiring step, no backend.
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { createPaymentGate, buildOpenApi, buildWellKnownX402, buildX402DnsTxt } from '@piprail/sdk'
|
|
109
|
-
|
|
110
|
-
const gate = createPaymentGate({ chain: 'base', token: 'USDC', amount: '0.05', payTo })
|
|
111
|
-
const resource = await gate.describe('https://api.example.com/report')
|
|
112
|
-
// → { url, description?, accepts: PaymentRail[] } (PaymentRail = scheme/network/asset/payTo/
|
|
113
|
-
// amount/amountFormatted/decimals/symbol?/maxTimeoutSeconds)
|
|
114
|
-
|
|
115
|
-
// (a) OpenAPI-first — the convention the live indexes parse. Serve at /openapi.json.
|
|
116
|
-
const openapi = buildOpenApi({ origin: 'https://api.example.com', resources: [resource] })
|
|
117
|
-
|
|
118
|
-
// (b) Legacy x402scan origin file. Serve at /.well-known/x402.
|
|
119
|
-
const wellKnown = buildWellKnownX402({ origin: 'https://api.example.com', resources: [resource] })
|
|
120
|
-
|
|
121
|
-
// (c) The experimental _x402 DNS pointer — paste into your zone.
|
|
122
|
-
const dns = buildX402DnsTxt({ host: 'api.example.com', discoveryUrl: 'https://api.example.com/openapi.json' })
|
|
123
|
-
// → { name: '_x402.api.example.com', type: 'TXT', value: 'v=x4021;url=https://api.example.com/openapi.json' }
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
| Function | Output | Serve at |
|
|
127
|
-
|---|---|---|
|
|
128
|
-
| `buildOpenApi(input)` | a minimal valid **OpenAPI 3.1** doc — one path per resource pathname (resources sharing a pathname merge, keyed by HTTP method), `x-payment-info` per paid op, optional `x-agentcash-provenance.ownershipProofs` | `https://<origin>/openapi.json` (primary) |
|
|
129
|
-
| `buildWellKnownX402(input)` | `{ version: 1, resources: [urls], ownershipProofs? }` | `https://<origin>/.well-known/x402` (legacy) |
|
|
130
|
-
| `buildX402DnsTxt({ host, discoveryUrl, descriptor? })` | `{ name: '_x402.<host>', type: 'TXT', value: 'v=x4021;[descriptor=…;]url=…' }` | a DNS TXT record (experimental) |
|
|
131
|
-
|
|
132
|
-
`ManifestInput` = `{ origin, resources, ownershipProofs?, title?, version?, attribution? }`. All three
|
|
133
|
-
emitters are **pure** — no network, no chain library — so they're deterministic and trivially testable.
|
|
134
|
-
They emit exactly the rails you pass; to be *usefully* listed on the open indexes, also offer a standard
|
|
135
|
-
`exact` rail (see §6).
|
|
136
|
-
|
|
137
|
-
**Spreading the word — three tasteful, honest channels (no spam, no rule-breaking).**
|
|
138
|
-
|
|
139
|
-
1. **`x-generator` stamp (default on, opt-out).** `buildOpenApi` marks the document root with
|
|
140
|
-
`x-generator: "@piprail/sdk · https://piprail.com"` — a standard, unobtrusive "built with" mark
|
|
141
|
-
(like Swagger/Hugo emit). It lives in the `/openapi.json` the merchant serves on their *own*
|
|
142
|
-
origin — the very file the open indexes **crawl** — so the attribution rides along wherever a
|
|
143
|
-
PipRail merchant is found. Metadata only; opt out with `attribution: false`.
|
|
144
|
-
2. **`User-Agent` on every index request (always on).** All reads/registers send
|
|
145
|
-
`User-Agent: @piprail/sdk (+https://piprail.com)` — the standard bot-UA-with-contact-URL
|
|
146
|
-
convention, so index operators see PipRail-driven traffic in their logs. It's a request *header*,
|
|
147
|
-
so it can never affect an index's body validation (zero risk of breaking a register), and the
|
|
148
|
-
browser keeps its own UA where it must. *(Live-verified: the server echoes it back.)*
|
|
149
|
-
3. **Opt-in `via` listing tag (default OFF).** `register(url, { attribution: true })` adds
|
|
150
|
-
`via: '@piprail/sdk'` to the listing payload. **Off by default** — it's the *merchant's* listing
|
|
151
|
-
on a third party, so we never tag it without being asked — and **best-effort** (an index may
|
|
152
|
-
ignore an unknown field). *(Live-verified safe: 402 Index tolerates the field — a tagged register
|
|
153
|
-
gets the exact same URL-probe response as an untagged one, never a field rejection.)*
|
|
154
|
-
|
|
155
|
-
We do **not** hijack the listing's `provider` (that's the merchant's), and the always-on channels (1
|
|
156
|
-
+ 2) are the reliable ones; (3) is purely opt-in. Honest attribution through the channels that
|
|
157
|
-
already exist — never spam.
|
|
158
|
-
|
|
159
|
-
**Ownership proof (optional trust badge).** Sign the **bare origin string** with the `payTo` key and
|
|
160
|
-
pass it as `ownershipProofs`. x402scan verifies `recoverMessageAddress(origin, sig) === payTo`.
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
const signer = await client.discoverySigner() // EVM today; null on families without it
|
|
164
|
-
const proof = signer ? [await signer.signMessage('https://api.example.com')] : undefined
|
|
165
|
-
const openapi = buildOpenApi({ origin: 'https://api.example.com', resources: [resource], ownershipProofs: proof })
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### 3.2 REGISTER — list yourself on the open registries
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
const client = new PipRailClient({ wallet: { privateKey: KEY }, chain: 'base' })
|
|
172
|
-
|
|
173
|
-
const outcomes = await client.register('https://api.example.com/report', {
|
|
174
|
-
name: 'Market Report',
|
|
175
|
-
priceUsd: 0.05,
|
|
176
|
-
// targets: ['402index'] // default — no auth, no signature
|
|
177
|
-
// targets: ['402index', 'x402scan'] // also x402scan via SIWX (EVM + Base/Solana)
|
|
178
|
-
})
|
|
179
|
-
// → [{ source: '402index', ok: true, status: 200, detail: 'Listed on 402 Index (searchable at 402index.io).' }]
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`RegisterOptions` = `{ name?, description?, priceUsd?, asset?, network?, method?, targets? }`. The
|
|
183
|
-
`network` slug defaults to the client's `chain` when it's a slug (e.g. `'base'`). Returns one
|
|
184
|
-
`RegisterOutcome` (`{ source, ok, status?, detail?, listingUrl? }`) **per target** — a target the
|
|
185
|
-
chain can't satisfy is reported `{ ok: false, detail }`, **never thrown**:
|
|
186
|
-
|
|
187
|
-
| target | what happens |
|
|
188
|
-
|---|---|
|
|
189
|
-
| `'402index'` (default) | one `POST` — no auth/signature/payment. The reliable path on every chain. |
|
|
190
|
-
| `'x402scan'` | **SIWX**: `POST` → `402` challenge → sign EIP-4361 with the wallet key → resend with the `SIGN-IN-WITH-X` header. The SDK checks **only** for an EVM `discoverySigner` locally (returns `{ ok:false }` on a non-EVM family); the **Base/Solana-only** limit is enforced by x402scan itself, so any other chain comes back `{ ok:false }` with the HTTP status it returns. **Experimental** — the SIWX handshake is a moving convention; validate against x402scan before relying on it. |
|
|
191
|
-
| `'bazaar'` | honestly refused (`{ ok:false }`) — the Bazaar has no write endpoint (facilitator-settle only). |
|
|
192
|
-
|
|
193
|
-
Standalone equivalents (no client): `register402Index(input)` and `registerX402Scan({ url }, signer)`.
|
|
194
|
-
|
|
195
|
-
### 3.3 DISCOVER — find payable resources (read-only, free)
|
|
196
|
-
|
|
197
|
-
```ts
|
|
198
|
-
const hits = await client.discover({ query: 'weather', maxPrice: 0.01 })
|
|
199
|
-
// → DiscoveredResource[] ({ resource, source, name?, description?, priceUsd?, rails: DiscoveredRail[] })
|
|
200
|
-
const res = await client.fetch(hits[0].resource) // then the usual quote → plan → pay
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
`DiscoverOptions` = `{ query?, network?, maxPrice?, sources?, limit? }`:
|
|
204
|
-
|
|
205
|
-
| option | default | meaning |
|
|
206
|
-
|---|---|---|
|
|
207
|
-
| `query` | — | free-text; matched against name/description/resource (Bazaar is filtered client-side, 402 Index server-side via `?q=`). |
|
|
208
|
-
| `network` | `'self'` | `'self'` = only resources payable on the client's bound chain · a **CAIP-2** id = that chain · `'any'` = every chain. |
|
|
209
|
-
| `maxPrice` | — | coarse pre-filter: drop results whose *advertised* USD price exceeds it (results with no price pass through — `quote()` gives the exact figure). |
|
|
210
|
-
| `sources` | `['bazaar','402index']` | which open indexes to read (both free). |
|
|
211
|
-
| `limit` | `20` | max results per source before merge. |
|
|
212
|
-
|
|
213
|
-
Results from all sources are **merged and deduped by resource URL** (first source wins). Standalone:
|
|
214
|
-
`searchOpenIndexes({ query?, sources?, limit?, signal? })`.
|
|
215
|
-
|
|
216
|
-
**Network filtering is forgiving by design.** An index reports networks as slugs (`'base'`) or CAIP-2
|
|
217
|
-
(`'eip155:8453'`). `normalizeNetwork()` maps known slugs to the exact CAIP-2 each driver binds (every
|
|
218
|
-
family is covered; Solana's reference is the 32-char-truncated form). For `network: 'self'` the filter
|
|
219
|
-
delegates to the driver's own `net.supports()`, and — crucially — a rail whose network it **cannot
|
|
220
|
-
resolve** is **kept, not silently hidden** (a re-checkable false positive beats an invisible
|
|
221
|
-
resource; the agent's next `quote()`/`planPayment()` rejects a wrong chain anyway).
|
|
222
|
-
|
|
223
|
-
---
|
|
224
|
-
|
|
225
|
-
## 4. The signing primitive — `discoverySigner`
|
|
226
|
-
|
|
227
|
-
One **optional** addition to the `PaymentDriver` contract:
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
// drivers/types.ts — ResolvedNetwork
|
|
231
|
-
discoverySigner?(wallet: WalletHandle): DiscoverySigner | null
|
|
232
|
-
// DiscoverySigner = { address: string; signMessage(message: string): Promise<string> }
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
- **Discovery only** — ownership proofs + SIWX registration. It **never signs a payment**.
|
|
236
|
-
- **EVM today** (eip191 via the wallet client; works for `{ privateKey }` and `{ walletClient }`).
|
|
237
|
-
Recoverable with viem's `recoverMessageAddress` — exactly how x402scan verifies origin ownership.
|
|
238
|
-
- **Optional by design** — a family omits it until an open index verifies its signatures. The
|
|
239
|
-
primary register path (402 Index) needs no signature, so families without it lose nothing there;
|
|
240
|
-
`register(..., { targets: ['x402scan'] })` returns a clear `{ ok:false }` for them.
|
|
241
|
-
- It is the SDK's **first optional contract method**, so it does *not* trigger the "implement in all
|
|
242
|
-
families" rule that applies to required methods.
|
|
243
|
-
|
|
244
|
-
`client.discoverySigner()` surfaces it (or `null`) so a merchant can generate an ownership proof.
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## 5. Agent / MCP tools
|
|
249
|
-
|
|
250
|
-
`paymentTools(client)` ships five descriptors; the MCP server is a pass-through, so they appear in
|
|
251
|
-
`@piprail/mcp` automatically:
|
|
252
|
-
|
|
253
|
-
| tool | does |
|
|
254
|
-
|---|---|
|
|
255
|
-
| **`piprail_discover`** `{ query?, network?, maxPrice?, limit? }` | find payable resources on the open indexes — the phone book. |
|
|
256
|
-
| `piprail_quote_payment` `{ url }` | price a gated URL without paying. |
|
|
257
|
-
| `piprail_plan_payment` `{ url }` | check you *can* pay (balance/gas/recipient) across every rail. |
|
|
258
|
-
| `piprail_pay_request` `{ url, method?, body? }` | pay the 402 and return the result. |
|
|
259
|
-
| **`piprail_register`** `{ url, name?, description?, priceUsd? }` | list a resource you run (402 Index, no signature). |
|
|
260
|
-
|
|
261
|
-
The discover tool returns a compact list (`resource, name, source, priceUsd, networks`) for the model
|
|
262
|
-
to pick from, then quote → pay. Because index results are cross-scheme, the model should always
|
|
263
|
-
`quote()` a chosen resource (re-hitting the live URL) before paying.
|
|
264
|
-
|
|
265
|
-
---
|
|
266
|
-
|
|
267
|
-
## 6. The honest caveats (never glossed)
|
|
268
|
-
|
|
269
|
-
1. **Scheme.** PipRail 402s use `scheme: 'onchain-proof'`; the open indexes assume the mainstream
|
|
270
|
-
**`exact`** scheme. A naive PipRail 402 risks being marked "skipped." **To be *usefully* indexed,
|
|
271
|
-
also advertise a standard `exact` USDC rail on Base/Solana.** `discover()` results are
|
|
272
|
-
cross-scheme: `client.fetch()` pays only `onchain-proof` rails directly; paying a discovered
|
|
273
|
-
`exact` resource uses the already-exported experimental `drivers/evm/exact.ts` interop.
|
|
274
|
-
2. **x402scan is Base/Solana only** (enforced server-side by x402scan — the SDK does no local chain
|
|
275
|
-
check before calling `registerX402Scan`). 402 Index has no such
|
|
276
|
-
limit, so it's the default register target and covers every family.
|
|
277
|
-
3. **There is no single ratified discovery standard.** The ratified x402 v2 spec defines discovery
|
|
278
|
-
only as the read-only facilitator Bazaar. OpenAPI-first (`x-payment-info`) is an **emerging
|
|
279
|
-
multi-vendor convention** (an early IETF draft, Merit Systems + Tempo Labs) — emit it, but treat
|
|
280
|
-
it as a moving target, never "the standard." The `_x402` DNS draft is expired; emit it as a
|
|
281
|
-
nice-to-have only.
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
## 7. Step-by-step walkthrough (and exactly what you need)
|
|
286
|
-
|
|
287
|
-
Two roles. **A merchant lists their own endpoint so agents can find it; an agent finds and pays.**
|
|
288
|
-
There is **no PipRail account and no x402 sign-up anywhere** — you never "register your SDK" with us
|
|
289
|
-
or with x402. The only thing that's ever "registered" is a *merchant's own URL* on a public index,
|
|
290
|
-
and they do it themselves with one call.
|
|
291
|
-
|
|
292
|
-
### 7a. Merchant — be found (each step says what it needs)
|
|
293
|
-
|
|
294
|
-
1. **Gate the route.** `requirePayment({ chain, token, amount, payTo })`.
|
|
295
|
-
*You need:* your **receiving wallet address** (`payTo`) — a public address, **not** a private key.
|
|
296
|
-
*No signing, no sign-up.* The route now returns `402` (payable) but is invisible.
|
|
297
|
-
2. **(Optional) Emit a discovery file.** `const r = await gate.describe(url)` →
|
|
298
|
-
`buildOpenApi({ origin, resources: [r] })` → serve the JSON at `https://<origin>/openapi.json`.
|
|
299
|
-
*You need:* nothing — it's pure, no keys, no network. It's a static file on your own server.
|
|
300
|
-
3. **Register so agents can find you.** `await client.register(url, { name, priceUsd })`.
|
|
301
|
-
- **402 Index — the default.** **No sign-up, no API key, no signature, no wallet.** One HTTPS POST;
|
|
302
|
-
402 Index probes your URL (it must return a real `402`) and lists it. Searchable in seconds.
|
|
303
|
-
- **x402scan — optional.** Add `targets: ['402index', 'x402scan']`. This one signs a **SIWX**
|
|
304
|
-
challenge with **your own wallet's key** (one signature — *no funds move*). Base/Solana only.
|
|
305
|
-
This is the **only** signing on the be-found side, and it's optional.
|
|
306
|
-
4. **(Optional) Ownership badge.** Sign your bare origin string with your `payTo` key
|
|
307
|
-
(`const s = await client.discoverySigner(); await s.signMessage(origin)`) and pass it as
|
|
308
|
-
`buildOpenApi({ ownershipProofs: [...] })`. A trust badge on indexes that verify it; never required.
|
|
309
|
-
5. **Found.** Agents discover you through the open indexes. Nothing is hosted by PipRail.
|
|
310
|
-
|
|
311
|
-
### 7b. Agent — find & pay
|
|
312
|
-
|
|
313
|
-
1. **Discover.** `await client.discover({ query })` — reads the open indexes (free). *No key, no sign-up.*
|
|
314
|
-
2. **Quote.** `await client.quote(resource)` — the exact live price. *No funds move.*
|
|
315
|
-
3. **Plan.** `await client.planPayment(resource)` — can this wallet actually settle it? *No funds move.*
|
|
316
|
-
4. **Pay.** `await client.fetch(resource)` — *you need:* a **funded wallet** (it signs + broadcasts the
|
|
317
|
-
payment, then verifies locally). The payment goes **merchant-direct** — no facilitator, and the
|
|
318
|
-
index never touches the money.
|
|
319
|
-
|
|
320
|
-
### 7c. What you need at each step (the whole truth, one table)
|
|
321
|
-
|
|
322
|
-
| Step | Wallet? | Private key / signing? | Sign-up / account? | Cost |
|
|
323
|
-
|---|---|---|---|---|
|
|
324
|
-
| Gate an endpoint | a receiving **address** only | **no** | **no** | free |
|
|
325
|
-
| Emit `/openapi.json` | — | **no** | **no** | free |
|
|
326
|
-
| **Register · 402 Index** (default) | — | **no** | **no** | free |
|
|
327
|
-
| Register · x402scan (optional) | your own | yes — **1 SIWX signature, no funds move** | **no** | free |
|
|
328
|
-
| Ownership badge (optional) | your own | yes — sign the origin string | **no** | free |
|
|
329
|
-
| Discover | — | **no** | **no** | free |
|
|
330
|
-
| Quote / plan | — | **no** | **no** | free |
|
|
331
|
-
| **Pay** a discovered API | a **funded** wallet | yes — the on-chain payment tx | **no** | the price + gas |
|
|
332
|
-
|
|
333
|
-
**The fastest path to discoverable** is the bold row pair: gate it, then `client.register(url)` —
|
|
334
|
-
**no wallet, no signature, no account, free.** Everything else is optional polish.
|
|
335
|
-
|
|
336
|
-
---
|
|
337
|
-
|
|
338
|
-
## 8. Constraint compliance
|
|
339
|
-
|
|
340
|
-
- **No backend / DB / registry of our own.** Emit = a static file the *merchant* hosts; discover /
|
|
341
|
-
register = runtime calls to *third-party* open indexes; payment is merchant-direct + local verify.
|
|
342
|
-
- **Protocol layer stays chain-agnostic** (STANDARDS §1): `discovery.ts` + `indexes.ts` import only
|
|
343
|
-
`x402.ts`/`drivers/types.ts` + pure utils — zero chain libraries (verified by the lazy-chunk grep).
|
|
344
|
-
- **Opt-in, defaults unchanged.** `discover`/`register`/`discoverySigner`/the emitters/`gate.describe`
|
|
345
|
-
are all new optional surface; the zero-config pay path is byte-identical.
|
|
346
|
-
- **Read-style, never throws.** Search returns `[]` on a dead/garbage index; register returns
|
|
347
|
-
`{ ok:false, detail }` on any failure; the pure emitters can't fail at runtime. (See ERRORS.md.)
|
|
348
|
-
|
|
349
|
-
---
|
|
350
|
-
|
|
351
|
-
## 9. Full API surface
|
|
352
|
-
|
|
353
|
-
```ts
|
|
354
|
-
// Emit (pure)
|
|
355
|
-
buildOpenApi(input: ManifestInput): OpenApiDocument
|
|
356
|
-
buildWellKnownX402(input: ManifestInput): WellKnownX402
|
|
357
|
-
buildX402DnsTxt(input: { host; discoveryUrl; descriptor? }): X402DnsRecord
|
|
358
|
-
gate.describe(resourceUrl?): Promise<ResourceDescription>
|
|
359
|
-
|
|
360
|
-
// Register (developer-invoked I/O; never throws)
|
|
361
|
-
client.register(url, opts?: RegisterOptions): Promise<RegisterOutcome[]>
|
|
362
|
-
register402Index(input: RegisterInput): Promise<RegisterOutcome>
|
|
363
|
-
registerX402Scan({ url }, signer: DiscoverySigner): Promise<RegisterOutcome>
|
|
364
|
-
|
|
365
|
-
// Discover (read-only I/O; never throws)
|
|
366
|
-
client.discover(opts?: DiscoverOptions): Promise<DiscoveredResource[]>
|
|
367
|
-
searchOpenIndexes(opts?: SearchOpenIndexesOptions): Promise<DiscoveredResource[]>
|
|
368
|
-
normalizeNetwork(network: string): string
|
|
369
|
-
|
|
370
|
-
// Sign (discovery only)
|
|
371
|
-
client.discoverySigner(): Promise<DiscoverySigner | null>
|
|
372
|
-
|
|
373
|
-
// Types
|
|
374
|
-
PaymentRail · ResourceDescription · ManifestInput · OpenApiDocument · OpenApiOperation ·
|
|
375
|
-
WellKnownX402 · X402DnsRecord · DiscoverySource · DiscoveredResource · DiscoveredRail ·
|
|
376
|
-
RegisterOutcome · RegisterInput · SearchOpenIndexesOptions · DiscoverOptions · RegisterOptions ·
|
|
377
|
-
DiscoverySigner
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
---
|
|
381
|
-
|
|
382
|
-
## 10. Experimental status & live-integration log
|
|
383
|
-
|
|
384
|
-
Discovery is **experimental** because it depends on third-party open indexes (402 Index, CDP Bazaar,
|
|
385
|
-
x402scan) whose APIs and conventions are young and moving. The SDK code is stable and tested; what's
|
|
386
|
-
experimental is the *integration contract* with those external services. Keep this log current.
|
|
387
|
-
|
|
388
|
-
**Live integration test — 2026-06-06** (the SDK's own functions, run against the real services):
|
|
389
|
-
|
|
390
|
-
| What | Result |
|
|
391
|
-
|---|---|
|
|
392
|
-
| `searchOpenIndexes({ sources: ['bazaar'] })` — CDP Bazaar, free | ✅ 20 resources normalized; all `exact`-scheme on `eip155:8453` (confirms the cross-scheme caveat). |
|
|
393
|
-
| `searchOpenIndexes({ sources: ['402index'], query })` | ✅ real `{services:[…]}` parsed; the **x402 protocol filter dropped L402/MPP** on live data. |
|
|
394
|
-
| `client.discover({ network: 'any' })` | ✅ both indexes merged + deduped (sources: `bazaar` + `402index`). |
|
|
395
|
-
| `client.discover()` (default `self`) | ✅ filtered to the client's chain; the never-hide invariant held on real data. |
|
|
396
|
-
| `register402Index(...)` (write, no auth) | ✅ POST succeeded end-to-end; **402 Index PROBES the URL** and returned **HTTP 422** for a non-402 URL: *"Your endpoint returned HTTP 200 instead of 402."* Our code reported `{ ok:false, status:422, detail }` **without throwing**, and surfaces the index's own reason. |
|
|
397
|
-
| `registerX402Scan(...)` (SIWX write) | ⏳ **NOT yet live-tested.** EVM signing is correct in isolation, but the SIWX handshake against x402scan is unverified — still experimental. |
|
|
398
|
-
| **`User-Agent` attribution** | ✅ confirmed sent over the wire (`@piprail/sdk (+https://piprail.com)` echoed back by an external header service). |
|
|
399
|
-
| **Opt-in `via` listing tag** | ✅ confirmed **safe**: a `register(..., { attribution: true })` to 402 Index returns the *identical* URL-probe response as an untagged one — the field is tolerated, never causes a rejection. |
|
|
400
|
-
|
|
401
|
-
**Key facts learned live:**
|
|
402
|
-
- **402 Index validates by probing** — it will only list a URL that actually returns a `402`. So a
|
|
403
|
-
successful registration requires a **real, deployed, public** x402 endpoint (PipRail has none to
|
|
404
|
-
test with — a marketing site returns 200 and is correctly rejected). This also means our test
|
|
405
|
-
created **no junk listing**. The error reason is now surfaced in `RegisterOutcome.detail`.
|
|
406
|
-
- **Read is free and works today** on both CDP Bazaar and 402 Index with no key.
|
|
407
|
-
- **402 Index totals (2026-06-06):** ~63k endpoints (x402: ~61k), ~1.6k services — a real, populated index.
|
|
408
|
-
|
|
409
|
-
**Before relying on it in production:** (1) register a real deployed x402 endpoint and confirm a
|
|
410
|
-
`200`/listed outcome end-to-end; (2) live-test the x402scan SIWX path; (3) re-verify the index wire
|
|
411
|
-
shapes (they drift — this doc's parser is defensive but the conventions are unratified).
|
|
412
|
-
|
|
413
|
-
---
|
|
414
|
-
|
|
415
|
-
## 11. Sources & further reading
|
|
416
|
-
|
|
417
|
-
- `.claude/research/x402-discovery.md` — the from-scratch explainer (concepts, formats, glossary).
|
|
418
|
-
- `.claude/research/x402-discovery-integration.md` — the source-level integration plan + verification log.
|
|
419
|
-
- 402 Index — https://402index.io · CDP Bazaar — https://docs.cdp.coinbase.com/x402/bazaar ·
|
|
420
|
-
x402scan — https://github.com/Merit-Systems/x402scan · x402 spec — https://github.com/coinbase/x402
|