products.json
The catalog is data: one JSON file, one entry per product, validated at load — an invalid catalog is rejected as a whole and the previous one keeps serving.
{
"products": [
{
"sku": "library", // stable key, unique
"title": "Whole Library",
"description": "optional — shown on the store and in discovery",
"price": "$0.01", // fixed pricing … OR:
// "pricing": { "mode": "demand", "floor": "$0.001", "ceiling": "$0.10",
// "step": 0.1, "windowMinutes": 15 },
"route": "GET /library/*", // "METHOD /path" — wildcard for folders
"contentDir": "./content/library",// exactly ONE of contentDir | contentPath | bundlePath | proxyUrl
"network": "eip155:84532", // optional, inherits .env NETWORK
"mimeType": "text/markdown", // optional; folders infer per file
"preview": true, // optional: excerpt md/txt on the store
"discoverable": true // optional: list in x402 registries (Bazaar)
}
]
}
Rules the validator enforces
- Exactly one of
price/pricing, and exactly one ofcontentPath/bundlePath/contentDir/proxyUrl contentDirproducts must use a wildcard route (GET /x/*); file products must notnetworkmatcheseip155:<chainId>; prices are"$0.05"strings or positive numbers; skus unique- demand pricing: floor < ceiling, step in (0,1), window ≥ 1 minute
Directory products
One folder = one price. Every file dropped in is instantly listed and purchasable; removing it delists it — no reload. The store page lists directory contents live (dotfiles and secret extensions are never listed or served).
Proxy products — paywall any endpoint
proxyUrl makes a live upstream service sellable — the founding idea taken literally:
{ "sku": "ask", "title": "Ask the corpus (per query)", "price": "$0.02",
"route": "POST /ask", "proxyUrl": "http://127.0.0.1:8403/soil/search",
"discoverable": true }
After payment, the gateway forwards the request — method, JSON body, query string, and (for wildcard routes) the subpath — and streams the upstream response back. An unreachable upstream answers 502. The upstream should bind localhost and skip its own payment handling: the gateway sells, it serves. This is how a retrieval "front desk" (e.g. the x402-packager) plugs in: the gateway owns payments, logging, redelivery and discovery; the upstream owns the brains.
Demand pricing
Each window, the repricer counts settled sales: none → price decays by step toward floor; some → it rises toward ceiling. The floor/ceiling bounds are the safety property (quote-spam can manipulate the signal; bounds cap the damage). For one window after a change both old and new price verify, so in-flight quotes never fail. Current price persists in the DB across restarts.
Where products get listed — and how to confirm it
Three independent listing surfaces:
- Your own
/catalog.json— always lists everything, instantly. Agents that know your URL need nothing else. - Bazaar (the facilitator's discovery index) — products with
discoverableenabled carry a machine-readable discovery declaration; the index learns about them from their x402 traffic through the facilitator, so a new product typically appears after its first real payment challenges, not instantly. SetPUBLIC_ORIGINin.env(e.g.https://store.example.com) so announced URLs are public, not localhost. - Chain explorers (x402scan and similar) — discover sellers from on-chain settlements; nothing to configure.
Confirming listings: the admin Discovery page queries the facilitator's Bazaar index live and shows listed / not-yet-listed / discovery-off per product. Programmatically, a bazaar-extended facilitator client does the same: withBazaar(client).extensions.bazaar.listResources() from @x402/extensions/bazaar.
payTo is never in the catalog
The receiving wallet and payment scheme come from .env — the catalog is safe to commit and share.