Skip to content

Discover & register

Discovery is two read/write moves against the open x402 directories that already exist — PipRail hosts none of them. client.discover({ query }) reads them to find payable resources; client.register(url) lists a resource you run. Both are $0, move no funds, and never throw for a read/transport problem — a dead or changed index simply contributes nothing.

discover() reads the open indexes, merges and dedupes them by resource URL, and by default returns only resources payable on this client’s chain. Each result is a DiscoveredResource carrying its advertised rails[], so a chosen resource feeds straight into the read-only trio.

import { PipRailClient } from '@piprail/sdk'
const client = new PipRailClient({
chain: 'base',
wallet: { privateKey: process.env.AGENT_KEY! },
})
const found = await client.discover({ query: 'weather' })
// → DiscoveredResource[] — [] if every index is down or empty (never throws)
for (const r of found) {
// priceUsd is the index's advertised figure when it reports one — often absent
console.log(r.resource, r.priceUsd ?? '(no advertised price)', r.source)
// feed r.resource into quote() / planPayment() to confirm + pay
}

Then pipe a result into quote()planPayment()fetch() to actually pay it.

OptionDefaultPurpose
queryFree-text, matched against name / description / resource URL.
network'self''self' (this client’s chain), 'any' (every chain), or a CAIP-2 id / chain slug like 'base'.
maxPriceDrop results whose advertised price exceeds this number. Results with no advertised price pass through.
sources['bazaar', '402index']Which open indexes to read.
limit20Max results per source before merge.

network: 'self' is the useful default: it returns only what this wallet can actually pay, matched via the bound driver’s own supports() so it works on every family, including custom chains. A rail whose network can’t be resolved is kept rather than hidden — discovery is never silently empty on an unmapped chain.

// Look across all chains, then decide later with planAcross()
const all = await client.discover({ query: 'image', network: 'any', maxPrice: 1 })
// → DiscoveredResource[] across every chain (filter/plan locally)
interface DiscoveredResource {
resource: string // the gated URL — quote/pay this
source: DiscoverySource // which index surfaced it ('bazaar' | '402index' from discover())
name?: string
description?: string
category?: string
priceUsd?: number // advertised price, when the index reports one (402 Index)
rails: DiscoveredRail[] // the advertised payment options (cross-scheme)
}

register() lists a resource on the open registries so agents can find it. The default target is 402 Index — one POST, no auth, no signature, no payment. It returns one RegisterOutcome per target; a target the chain can’t satisfy comes back { ok: false, detail }, never a throw.

const [outcome] = await client.register('https://api.example.com/report', {
name: 'Daily report',
priceUsd: 0.1, // advertised metadata only — no oracle reads this
asset: 'USDC',
})
console.log(outcome.ok, outcome.visibility, outcome.note)
// → true 'pending-review' '402 Index probes your URL on submit, then lists it as PENDING REVIEW … retry discover() later.'
OptionDefaultPurpose
namethe URL’s hostDisplay name for the listing.
descriptionListing description.
priceUsdAdvertised price (metadata only — no oracle reads it).
assetPayment asset symbol, e.g. 'USDC'.
networkthe client’s chainPayment network slug, e.g. 'base'.
method'GET'HTTP method the resource answers on.
targets['402index']Which indexes to list on. Add 'x402scan' for the SIWX path.
attributionfalseOpt-in via: '@piprail/sdk' tag on the listing.

Listing is asynchronous, so each outcome carries a visibility and a one-line note — don’t read ok: true as “searchable now.”

interface RegisterOutcome {
source: DiscoverySource
ok: boolean
status?: number // HTTP status, when a request was made
detail?: string // success summary or the reason it didn't list
listingUrl?: string
visibility?: ListingVisibility // 'live' | 'pending-review' | 'not-listable'
note?: string // agent-readable caveat for this source
}
visibilityMeaning
'live'Findable now — search it immediately.
'pending-review'Accepted, but reviewed/propagated before it’s publicly findable; retry discover() later.
'not-listable'It didn’t list — a failure, or this index structurally can’t list a PipRail resource.

The per-source lifecycle facts live in DIRECTORY_INFO (importable), so an agent can reason about an index — auth, chains, whether discover() reads it — without embedding directory knowledge. getDirectoryInfo(source) returns one DirectoryInfo:

import { getDirectoryInfo } from '@piprail/sdk'
const info = getDirectoryInfo('402index')
info.auth // 'none'
info.readByDiscover // true — discover() reads 402 Index
info.onSuccess // 'pending-review'
info.review // 'probe-sync' — a synchronous URL probe (not facilitator-coupled)
SourceRead by discover()Write authNotes
bazaaryes— (facilitator-only)Free to read. Can’t be written to — Bazaar catalogs only what its own facilitator settles, and PipRail uses none.
402indexyesnoneThe primary register target: one POST, no auth. Lists pending-review until your domain is verified.
x402scannoSIWXBase/Solana only; needs one wallet signature and a resolvable input schema. A live listing here won’t appear in discover().

Adding 'x402scan' to targets lists via Sign-In-With-X — one wallet signature, facilitator-free, but Base/Solana-only and EVM signing today. It needs a discoverySigner (the EVM families have one); a chain family without one returns { ok: false, detail } rather than throwing.

const outcomes = await client.register('https://api.example.com/report', {
targets: ['402index', 'x402scan'], // x402scan needs an EVM signer + a Base/Solana rail
})
// → RegisterOutcome[] — one per target, in target order
for (const o of outcomes) {
console.log(o.source, o.ok, o.visibility) // e.g. 'x402scan' true 'live'
}

The open SIWX handshake is a moving convention — validate against x402scan before relying on it. x402scan also requires a resolvable input schema, which you supply by emitting an /openapi.json or the extensions.bazaar block in your 402 body.