"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": the shared routes are the only ones
there are.
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? } }POST /mcp-> MCP over Streamable HTTP, stateless (one JSON-RPC request perPOST;GETanswers405).stellarsight_search/_browse/_describeare live;stellarsight_payis registered and refused on the hosted origin.
/verify and /settle are rate limited; nothing else is. Over the configured
per-caller budget they answer 429 with Retry-After and
\{ ok: false, code: "STELLARSIGHT_RATE_LIMITED", reason, scope, limit, windowSeconds, retryAfterSeconds } — a transport refusal, deliberately not the isValid/success
shape, because a rate limit is not a verdict about the payment. /supported, /health
and /events are never limited: /supported is an RFP acceptance criterion and must
answer a stock client unconditionally. Defaults are 120 requests per 60s per caller;
FACILITATOR_RATE_LIMIT=0 disables it. GET /health reports the policy in force under
rateLimit, and the facilitator’s fee under fee.
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 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.