Skip to main content
Monorepo, plain npm workspaces, Node 22, ESM ("type": "module"). No TypeScript build step anywhere except apps/web (Vite). Everything must run with node <file>.mjs or npm run dev.

Ports (fixed, do not change)

Shared env — /.env at repo root (written by scripts/setup-testnet.mjs)

packages/index — public API (ESM named exports from packages/index/src/index.mjs)

The INTERNAL record shape

This is what upsert() takes, what the store holds and what list()/search() return. It is not what goes on the wire — see the next section.

The WIRE shape — DiscoveryResource

packages/index/src/discovery.mjs (toDiscoveryResource) projects the internal record onto the type the shipped SDK declares (@x402/extensions/dist/esm/index-*.d.mts). All three transport adapters go through it: This register previously carried a KNOWN DRIFT here: the facilitator hand-rolled its own /discovery/resources and /discovery/search and returned catalog.list() / catalog.search() verbatim, so :4022 served the INTERNAL record shape while the deployment served the wire shape. It is closed. The facilitator now calls mountDiscoveryRoutes(indexApp, catalog), and apps/agent/src/bazaar.mjs reads either shape through one fieldsOf() accessor rather than reaching for rec.resource.url. GET /health on :4022 reports wireShape: "spec" when the shared routes are mounted, and "internal-stub" in the one degraded case that remains — packages/index failing to import at all, where the facilitator falls back to its in-memory stub and says so rather than pretending to be spec-shaped.
Three things a stock consumer needs that the internal record does not give it, and the reasons they are easy to get wrong:
  1. resource is a URL string. The presentation fields move to the top level.
  2. accepts is required — without it a client cannot construct a payment from a search result. x402 v2 PaymentRequirements names the price amount, NOT maxAmountRequired; the v1 name fails PaymentRequirementsSchema in the installed @x402/core.
  3. lastUpdated is ISO 8601, where the record keeps epoch ms in lastSeenAt.

HTTP surfaces — do not rename fields

  • GET /supported -> { kinds: [{ x402Version: 2, scheme: "exact", network: "stellar:testnet", extra: { areFeesSponsored: true, asset } }] }
  • POST /verify -> { isValid, invalidReason|null, payer }
  • POST /settle -> { success, errorReason|null, transaction, network, payer } + header EXTENSION-RESPONSES
  • GET /discovery/resources?type&payTo&scheme&network&extensions&limit&offset -> { x402Version, items: DiscoveryResource[], pagination: { limit, offset, total } } (plus flat total/limit/offset)
  • GET /discovery/search?query&limit&cursor&...filters -> { x402Version, resources: DiscoveryResource[], partialResults, pagination: { limit, cursor } }
  • EXTENSION-RESPONSES header = base64(JSON) of { bazaar: { status: "success"|"processing"|"rejected", rejectedReason? } }
The two discovery envelopes differ deliberately, and so does their pagination. DiscoveryResourcesResponse names the array items and paginates by offset/total; SearchDiscoveryResourcesResponse names it resources and paginates by cursor. withBazaar() returns the parsed body untransformed, so a search response carrying only items makes search.resources undefined and throws on iteration. Search currently ALSO emits items as a deprecated duplicate alias of the same array, for one release. New consumers must read resources. None of this is asserted by reading the field names this repo emits — that is a belief, not an observation, and it is how the items/resources divergence shipped in the first place. npm run verify:api imports the real withBazaar from @x402/extensions, drives it against the actual handlers over a socket, and validates every accepts entry with @x402/core’s own PaymentRequirementsSchema. Change a field name here and that harness is what tells you.

apps/web contract

Reads from INDEX_URL. MUST render fully with a baked-in fallback fixture at apps/web/src/data/fixture.json when the API is unreachable — the demo cannot depend on localhost being up. Show a small “LIVE / DEMO” pill reflecting which source is active. Routes: / (landing), /console (live search + payment loop viewer).

Assets

Generated assets land in apps/web/public/assets/. Web must degrade gracefully (CSS-only fallback) if an asset file is missing.