/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 — check it locally before it is announced · 0.4 s
A rejected listing has historically been silent: acould not announce warning in the
seller’s own log at boot, or nothing at all when the index soft-drops a field, which is
exactly the signal docs/MONITORING.md reads as “probing or a broken
seller” — not something a seller watches for themselves. stellarsight-seller check runs
the index’s own integrity validator against your routes without a facilitator or index
running, so a bad host, a malformed routeTemplate or an over-length tag shows up before
the first announce, not after.
STELLARSIGHT_ALLOW_PRIVATE_RESOURCES=1 is the same escape hatch dev:seller and
dev:facilitator already set — without it, localhost is correctly refused as
unreachable by anyone outside your machine, the same way the live catalog would refuse it.
Exit code is 1 on any rejection and --json prints a machine-readable report, so this is
also the one-line CI gate: npx stellarsight-seller check || exit 1.
The command reads apps/seller/stellarsight.config.mjs — a companion declaration of the
same three routes through the real @stellarsight/express API, kept next to server.mjs
purely so this step has something to check (server.mjs implements the x402 challenge
inline; see its header for why). If you are using @stellarsight/express’s pay(...)
directly, there is no separate file to keep in sync: export the same pay your server
attaches to routes, and the CLI checks exactly what you serve — see the
packages/express README for the config-file contract.
Step 4 — 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 5 — 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: 60 seconds of machine time
Measured on a clean clone with/usr/bin/time -p, macOS, against live Stellar testnet —
except stellarsight-seller check, which never touches the network and was timed
separately, on Linux, for exactly that reason:
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.