/discovery/search.
Every command below adds up to about a minute of machine time. That is measured, not
estimated — the table at the end has the numbers and how they were taken. The rest of the clock
is you writing one route object, and that is the only part this document cannot make faster.
The SCF #45 RFP sets the bar this document is written against:
“A developer should get from docs to a paid, discoverable endpoint appearing in the Bazaar in well under an hour.”Below is that path, timed. No API keys, no captcha, no faucet, no wallet extension. Every command is copy-paste and every step ends in something you can check with
curl.
If you only want to read the catalog rather than publish to it, you do not need any of this —
stellarsight.xyz/discovery/search
is public and CORS-open. This document is for the seller side.
What you need first
Step 1 — clone and bootstrap · 49 s
npm run setup generates the seller, buyer, issuer and fee-payer keypairs, funds them from
Friendbot, issues SXT, deploys its SAC and adds the trustlines. It is idempotent — running
it twice reuses the .env it already wrote. It appends every hash it submits to
docs/TESTNET-TXS.md.
Check: .env now contains ASSET_SAC, SELLER_PUBLIC, PAYER_SECRET and FEEPAYER_SECRET.
curl -s localhost:4021/supported | jq returns the exact / stellar:testnet kind
with extra.areFeesSponsored: true.
Step 2 — declare your endpoint · your call
Openapps/seller/src/server.mjs and add an entry to the
ROUTES array. That array is the single source of truth — pricing, the HTTP handler, and the
discovery metadata all come from the same object, so they cannot drift apart.
declareDiscoveryExtension is the stock export from @x402/extensions — not a STELLARSIGHT
wrapper. The per-parameter description fields are the part worth slowing down for: they carry
×2 weight in the ranker (see SEARCH-QUALITY.md), and they are the
difference between an agent finding your endpoint and finding somebody else’s.
This is the one step with no number on it, deliberately. Every other step is a command whose
duration was measured; this one is you deciding what your API does and how to describe it to a
machine. Copying the block above and changing the strings takes a minute. Writing a description
good enough to win a search takes as long as it takes.
Save the file and restart the seller — there is no file watcher, dev:all runs plain node.
On boot it announces every route in ROUTES to the index.
Step 3 — you are already in the Bazaar · one curl
Two independent paths put you in the catalog, and you get both: Pre-registration on boot. The sellerPOSTs each route to the index when it starts, and
re-announces every 30 seconds (server.mjs:563). Your
endpoint is discoverable before it has ever been paid — which matters, because a catalog
that only lists resources after their first payment cannot be used to find a new resource in
order to pay it.
Auto-cataloging on settle. When a payment settles, the facilitator reads the bazaar
extension off the payload and upserts the resource, incrementing its settlement count
(facilitator/server.mjs:493). This is the spec’s
bazaar extension doing what it is for, and the settle response carries an
EXTENSION-RESPONSES header reporting whether the catalog accepted or rejected your record.
Check:
resource URL:
The local index is not the hosted one, and the differences will bite you. The facilitator hand-rolls these two routes rather than mountingpackages/index/src/http.mjs, so on:4022:/discovery/searchreturns its results underitems, not the spec’sresources; there is no/discovery/health; and unknown paths get Express’s HTML 404 instead of the JSON 404. The spec-shaped envelope,/discovery/healthand the JSON 404 are what the hosted deployment serves. This drift is recorded inCONTRACT.md— it is a known gap, not a surprise, and closing it is a funded deliverable.
Step 4 — get paid, with a client you did not write · 10 s
@x402/fetch client — wrapFetchWithPayment, no STELLARSIGHT code
anywhere on the path — through a real 402 → sign → settle → 200 against your running seller,
and prints the settled transaction hash. It is the acceptance test the RFP asks for, and it is
the one that matters: it proves other people’s agents can pay you, not just ours.
successful: true off the ledger. Note fee_account: it is the facilitator’s FEEPAYER, not
the buyer. The paying agent holds zero XLM.
Check: search again. Your record now carries settlements: 1, and the settlements
component of _explain is non-zero:
Total: 59 seconds of machine time
Measured on a clean clone with/usr/bin/time -p, macOS, against live Stellar testnet:
An earlier version of this document said 13 minutes. That was an estimate and it was wrong —
it was dominated by a made-up five minutes for “declare your endpoint”, which is not machine
time at all. The commands take a minute. Step 2 takes however long you take to write one route
object, and pretending to know that number was the mistake.
Nothing here is padded for safety:
npm run setup really does submit five transactions to
public testnet and wait for each to close, which is where 78% of the minute goes.
Bringing your own server
You do not have to useapps/seller. Anything that speaks x402 v2 can point at this
facilitator — it is the standard three endpoints:
- The 402 challenge belongs in the
PAYMENT-REQUIREDresponse header, base64-encoded.@x402/corereads a JSON body only whenx402Version === 1. A v2 challenge that lives only in the body is invisible to a stock client — this repo shipped exactly that bug and documents it in the README. - v2
PaymentRequirementsusesamount, notmaxAmountRequired, and the resource metadata moved toPaymentRequired.resource. Both names appear in the wild because the index still accepts either on the way in; CONTRACT.md is the canonical statement of which field is emitted where.
@x402/core’s own codecs (encodePaymentRequiredHeader, decodePaymentSignatureHeader,
encodePaymentResponseHeader) rather than hand-rolling base64, and the wire format cannot
drift from what a stock client decodes.
To appear in the catalog, attach the bazaar extension to your 402 challenge under
extensions, exactly as apps/seller does at
server.mjs:347. The facilitator picks it up on settle.
Publishing to the hosted Bazaar at stellarsight.xyz
Readingstellarsight.xyz is open to anyone. Writing to it is not self-serve today, and that is
worth stating plainly rather than glossing:
401 with a non-null reason. An unauthenticated
public write endpoint on a catalog is a spam vector, so it is refused by design
(serverless.mjs:479) — but the consequence is real:
a third-party developer cannot currently list themselves on the hosted index without asking
the operator for a token.
The fix is ownership-verified self-serve registration — proving control of the resource
origin, then issuing a scoped token automatically — and it is a funded deliverable rather than
something already built. Until it lands, the honest statement is: self-hosted listing is
instant and unrestricted; hosted listing needs a token from the operator.
When it does not work
Every rejection this codebase emits carries a non-null, human-readable
reason naming what to
do about it. If you hit one that does not,
open an issue — that is a bug.