"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 whatupsert() 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.
resourceis a URL string. The presentation fields move to the top level.acceptsis required — without it a client cannot construct a payment from a search result. x402 v2PaymentRequirementsnames the priceamount, NOTmaxAmountRequired; the v1 name failsPaymentRequirementsSchemain the installed@x402/core.lastUpdatedis ISO 8601, where the record keeps epoch ms inlastSeenAt.
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 }+ headerEXTENSION-RESPONSESGET /discovery/resources?type&payTo&scheme&network&extensions&limit&offset->{ x402Version, items: DiscoveryResource[], pagination: { limit, offset, total } }(plus flattotal/limit/offset)GET /discovery/search?query&limit&cursor&...filters->{ x402Version, resources: DiscoveryResource[], partialResults, pagination: { limit, cursor } }EXTENSION-RESPONSESheader = base64(JSON) of{ bazaar: { status: "success"|"processing"|"rejected", rejectedReason? } }
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 fromINDEX_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 inapps/web/public/assets/. Web must degrade gracefully (CSS-only
fallback) if an asset file is missing.