Skip to main content
Goal: your own paid HTTP endpoint, settling real x402 payments on Stellar testnet, returned by /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.
Check: curl -s localhost:4021/supported | jq returns the exact / stellar:testnet kind with extra.areFeesSponsored: true.

Step 2 — declare your endpoint · your call

Open apps/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 seller POSTs 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:
Your endpoint should be the top hit. If it is not there, list the whole catalog and look for your 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 mounting packages/index/src/http.mjs, so on :4022: /discovery/search returns its results under items, not the spec’s resources; there is no /discovery/health; and unknown paths get Express’s HTML 404 instead of the JSON 404. The spec-shaped envelope, /discovery/health and the JSON 404 are what the hosted deployment serves. This drift is recorded in CONTRACT.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

This drives an unmodified @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.
Open that hash on stellar.expert and read 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 use apps/seller. Anything that speaks x402 v2 can point at this facilitator — it is the standard three endpoints:
Two wire details worth having in front of you, because most third-party material still shows the v1 shapes and they will cost you an afternoon otherwise:
  1. The 402 challenge belongs in the PAYMENT-REQUIRED response header, base64-encoded. @x402/core reads a JSON body only when x402Version === 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.
  2. v2 PaymentRequirements uses amount, not maxAmountRequired, and the resource metadata moved to PaymentRequired.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.
Use @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

Reading stellarsight.xyz is open to anyone. Writing to it is not self-serve today, and that is worth stating plainly rather than glossing:
Without a valid token the endpoint returns 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.