Skip to content

The open indexes

PipRail hosts no directory of its own. To be found, and to find others, it reads from and writes to the open x402 directories that already exist. There are three, and they behave differently — different auth, different chains, different timing before a listing is searchable.

This page is the per-index reference. The high-level client.discover() / client.register() wrap these functions; reach for the low-level ones when you want to read a single index, register without a client, or branch on an index’s behaviour before you call it.

The three sources, named by the exported DiscoverySource union ('bazaar' | '402index' | 'x402scan'):

SourceReadsWrites
bazaarCDP Bazaar — free, keyless read of the facilitator catalognone (settle-coupled — see below)
402index402 Index — free readno-auth POST (the primary register target)
x402scannot read by discover()one wallet signature (SIWX), Base/Solana only

DIRECTORY_INFO is the single source of truth for how each index behaves: a static map an agent can branch on without embedding directory knowledge in its own code. getDirectoryInfo(source) takes a DiscoverySource and returns one DirectoryInfo entry.

import { getDirectoryInfo } from '@piprail/sdk'
const info = getDirectoryInfo('402index')
// → DirectoryInfo { source, review, auth, chains, onSuccess, readByDiscover, caveat }
info.auth // 'none'
info.readByDiscover // true — discover() reads this index
info.onSuccess // 'pending-review' — a fresh listing isn't searchable yet

Each DirectoryInfo carries these fields:

FieldMeaning
sourceThe DiscoverySource this describes.
reviewHow a listing is gated: 'probe-sync' (the index fetches your URL on submit) or 'settle-coupled' (cataloged only when a facilitator settles a payment).
authAuth to write a listing: 'none', 'siwx', or 'facilitator-only'.
chainsCAIP-2 chains this index will list, or null for any chain the resource advertises.
onSuccessThe visibility a successful listing reaches: 'live', 'pending-review', or 'not-listable'.
readByDiscoverWhether this SDK’s discover() reads this index.
caveatA one-line, agent-readable note: why a register might fail, or what to expect after.

Branch on the facts rather than guessing. The map, as PipRail ships it:

bazaar402indexx402scan
reviewsettle-coupledprobe-syncprobe-sync
authfacilitator-onlynonesiwx
chainsnull (any)null (any)Base + Solana only
onSuccessnot-listablepending-reviewlive
readByDiscoveryesyesno

searchOpenIndexes() takes a SearchOpenIndexesOptions and returns DiscoveredResource[]. It reads the open indexes in parallel and merges the hits, deduped by resource URL (the first source in sources wins). It defaults to the two free indexes.

import { searchOpenIndexes } from '@piprail/sdk'
const hits = await searchOpenIndexes({ query: 'weather' })
// → DiscoveredResource[] — each: { resource, source, rails, name?, description?, category?, priceUsd? }

It never throws. Any index that errors, times out, or changes shape simply contributes [], so a dead index never breaks the rest of your search (no try/catch needed):

const hits = await searchOpenIndexes({ sources: ['bazaar', '402index'], limit: 50 })
// → DiscoveredResource[] — if 402index is down, you still get bazaar's results (never an empty throw)

The options object is the exported SearchOpenIndexesOptions:

OptionDefaultPurpose
queryFree-text, filtered client-side against name / description / resource.
sources['bazaar', '402index']Which indexes to read (both free).
limit20Max results per source, before dedupe.
signalAn AbortSignal to cancel the reads.

Each DiscoveredResource is normalized to one shape across sources. Its rails are cross-scheme and best-effort — indexes mostly carry the standard exact scheme, so a DiscoveredRail is looser than a live accepts[] entry: a required scheme / network, plus optional asset / amount / payTo / symbol. Feed a chosen resource straight into quote() to get the authoritative offer.

Registering on 402 Index — register402Index

Section titled “Registering on 402 Index — register402Index”

402 Index is the friction-free write path: a single POST, no auth, no signature, no payment. It takes a RegisterInput and returns a RegisterOutcome.

import { register402Index } from '@piprail/sdk'
const outcome = await register402Index({
url: 'https://api.example.com/report',
description: 'Daily market report',
priceUsd: 0.1, // advertised-price METADATA (402 Index field), not a PipRail-computed price
asset: 'USDC',
network: 'base',
})
// → RegisterOutcome { source: '402index', ok: true, status: 200, detail: '…' }
// (BARE — visibility/note unset until decorateOutcome runs; see below)

It returns a RegisterOutcome and never throws for an HTTP or transport problem — failures come back as { ok: false, detail }, so branch on outcome.ok rather than wrapping it in a try/catch. 402 Index probes your URL on submit, so an endpoint that doesn’t actually return a 402 is rejected (the reason is surfaced in detail).

if (!outcome.ok) console.error(outcome.detail) // the index's own reason

The RegisterInput fields:

FieldNotes
urlRequired — the gated resource.
nameDefaults to the URL’s hostname.
description / priceUsdListing metadata.
asset / networkPayment symbol (e.g. 'USDC') and network slug (e.g. 'base').
methodHTTP method the resource answers on. Defaults to GET.
attributionOpt-in (default off) — adds a best-effort via: '@piprail/sdk' tag to the listing.

A self-registered listing comes back pending review (onSuccess: 'pending-review') — it isn’t searchable until 402 Index approves it. To get instant approval (and flip every pending listing on the domain to live), verify your domain — see Domain verification.

Registering on x402scan — registerX402Scan

Section titled “Registering on x402scan — registerX402Scan”

x402scan needs one wallet signature (Sign-In-With-X / SIWX): the function POSTs your URL, receives an EIP-4361 challenge, signs it with your key, and resends. It’s facilitator-free, but Base/Solana only and EVM-signing today. It returns a RegisterOutcome.

import { PipRailClient, registerX402Scan } from '@piprail/sdk'
const client = new PipRailClient({
chain: 'base',
wallet: { privateKey: process.env.AGENT_KEY! },
})
// A DiscoverySigner = { address, signMessage } — the bound EVM wallet exposes one.
const signer = await client.discoverySigner()
if (!signer) throw new Error('x402scan SIWX needs an EVM signer; this chain has none.')
const outcome = await registerX402Scan(
{ url: 'https://api.example.com/report' },
signer,
)
// → RegisterOutcome { source: 'x402scan', ok: true, status: 200, detail: 'Listed on x402scan (SIWX).' }

The signer is a DiscoverySigner — an address plus a signMessage(message) that returns the signature. Like the others, it never throws; a failed handshake returns { ok: false } with the index’s reason in detail.

x402scan also needs a resolvable input schema for your resource — emit one from /openapi.json or the bazaar extension so the listing validates. On success it goes live immediately on x402scan.com.

CDP Bazaar has no register endpoint (auth: 'facilitator-only', review: 'settle-coupled'). It catalogs a resource only when its own facilitator settles a payment for it. PipRail verifies locally with no facilitator, so a PipRail resource structurally can’t be listed there — onSuccess is 'not-listable'. You can still read Bazaar to find others; to be found, list on 402 Index or x402scan.

This is exactly the kind of fact you don’t want to hard-code. Branch on DIRECTORY_INFO instead:

import { getDirectoryInfo } from '@piprail/sdk'
if (getDirectoryInfo('bazaar').onSuccess === 'not-listable') {
// skip bazaar as a register target — it can't list a backendless resource
}