stellarsight
Health Uyari
- License — License: Apache-2.0
- No description — Repository has no description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Uyari
- network request — Outbound network request in apps/agent/src/bazaar.mjs
- process.env — Environment variable access in apps/agent/src/pay.mjs
- network request — Outbound network request in apps/agent/src/pay.mjs
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
STELLARSIGHT
Find what to pay for on Stellar.
The facilitator-side Bazaar discovery layer for x402 — the piece that does not exist in public code today — and the whole payment loop around it, running end to end on Stellar testnet.
Apache-2.0 · stellar:testnet · 19 settled x402 payments · 161 tests, 0 failing · nDCG@10 0.864, measured
For evaluators — verify this in 60 seconds
You do not have to take any claim in this README on trust. Every one of them is checkable:
| Claim | How to check it | Time |
|---|---|---|
| Payments really settle on Stellar | Open c1acc578… → successful: true |
10s |
| The buyer needs zero XLM — fees are sponsored | On that transaction, fee_account is the facilitator's FEEPAYER, not the payer |
15s |
| Catalog integrity is real, not decorative | npm test → 161 tests, 0 failing (66 of them adversarial) |
30s |
| Search quality is a number, not a plan | npm run eval:search → nDCG@10 0.864, Recall@20 0.905, MRR@10 0.920 over a 50-query graded set, with a regression gate in CI |
20s |
| We publish our own worst number | docs/LOAD-BASELINE.md — the same payments succeed 4/4 serially and 1/10 concurrently on today's single fee-payer. That gap is what Tranche 1 buys |
60s |
| It stays that way | CI runs the suite, the 46 conformance checks and the site build on Node 22 and 24, with a real Redis so nothing skips for want of one | 10s |
| You can actually run it | npm install && npm run setup — no captcha, no faucet, no API key |
2 min |
| A developer can ship on it | docs/QUICKSTART-SELLER.md — clone → paid, discoverable endpoint. Every command timed with /usr/bin/time |
59s |
That last row is the one worth pausing on. Almost every x402-on-Stellar project requires a
Circle faucet captcha and an OpenZeppelin Channels API key before it will start. This one
requires neither, by design — see Two blockers removed.
Why this is not another paywall demo
Most x402 builds are variations on one idea: an agent paying for an API, a metered service,
a channel-mode feed, a middleware kit. They are good examples. They are also built on ground
that is already solved, and the SCF #45 RFP says so in plain language:
"settlement on Stellar is largely solved; the novel work is discovery, the agent facing
interface, the upto scheme upstream, and conformance that holds as the spec moves."
So we did not build a payment demo. We built the part that is missing, then built the
payment demo around it so you can watch the missing part work.
The gap, precisely
The bazaar extension spec defines /discovery/resources and /discovery/search |
✅ exists |
@x402/extensions/bazaar implements them |
❌ its own README states it ships only client and server helpers, and no facilitator-side catalog implementation |
| Stellar has a Bazaar | ❌ stellar/x402-stellar#50 — "Explore Bazaar support for Stellar" — open and unassigned since April 2026. The SDF repo's Dockerfile still reads bazaar not used |
An agent that can pay but cannot discover is an agent with a wallet and no map. STELLARSIGHT is
the map.
The public discovery API
Live at stellarsight.xyz. The same catalog that packages/index
serves on :4022 also deploys as Vercel functions, so the Bazaar is a public, hosted
endpoint any agent can call — which is what the RFP asks for, and what does not exist for
Stellar anywhere else. The facilitator answers on the same origin (/supported, /verify,/settle) and so does a real paid API under /v1/*, which announces itself into the catalog:
discovery, payment and settlement are one deployment. Run the commands below and they answer.
| Method | Endpoint | What it does |
|---|---|---|
GET |
/discovery/resources |
Paginated catalog, with the spec's type, payTo, scheme, network, extensions, limit, offset filters |
GET |
/discovery/search |
Natural-language search. Results arrive under resources, with partialResults, pagination { limit, cursor }, and _explain per result |
GET |
/discovery/health |
Catalog mode, record counts, durable-store transport, and the commit being served |
POST |
/discovery/resources |
Auto-cataloging. Requires Authorization: Bearer <STELLARSIGHT_WRITE_TOKEN> |
| any | /discovery/<anything else> |
404 JSON naming the endpoints that do exist — never HTML, never a silent 200 |
?seeded=false is the additive filter that answers "what here can I actually pay for?".
The catalog ships a 27-record demo corpus on .example hosts so the ranker has a realistic
spread to rank — completeness and freshness vary on purpose, which is what makes _explain
legible instead of constant. Every one of those records is flagged seeded: true and
pinned to settlements: 0, the flag survives the wire projection, and one query separates
them from the real ones. Hiding the corpus would hide the ranker; labelling it costs
nothing and is checkable.
CORS is *, because the point is for other people's agents to call it. Every rejection —404 on an unknown path, 503 when writes are not enabled (no durable store, or a store
with no STELLARSIGHT_WRITE_TOKEN set), 401 when writes are enabled and the caller did
not present that token — carries a non-null, human-readable reason that names what to do
about it.
# Natural-language search over the catalog, ranked
curl -s 'https://stellarsight.xyz/discovery/search?query=invoice%20ocr&limit=3' | jq \
'.resources[] | {resource, score: ._score, name: .serviceName}'
# The full score breakdown on the top hit — BM25 / completeness / settlements / recency
curl -s 'https://stellarsight.xyz/discovery/search?query=convert%20dollars%20to%20reais&limit=1' \
| jq '.resources[0]._explain'
# List, with the spec filters. Note the envelopes differ deliberately: the list
# endpoint returns `items` with offset pagination, search returns `resources`
# with a cursor — that asymmetry is the spec's, not ours.
curl -s 'https://stellarsight.xyz/discovery/resources?type=mcp&limit=5' \
| jq '.total, .items[].resource'
# Which mode the catalog is in, how many records, which commit is serving them
curl -s https://stellarsight.xyz/discovery/health | jq
Real output, at the time of writing:
$ curl -s 'https://stellarsight.xyz/discovery/search?query=invoice%20ocr&limit=3' …
0.7736 Invoice OCR
$ curl -s https://stellarsight.xyz/discovery/health …
mode=kv transport=redis records=30 writable=true commit=608080b
$ curl -s -o /dev/null -w '%{http_code} %{content_type}' https://stellarsight.xyz/discovery/nope
404 application/json; charset=utf-8
/discovery/health reports the commit it is serving, so a claim in this README can always
be checked against the code that is actually deployed.
Read that output as a snapshot, not a standing claim — it is the answer at the commit named
in it, and commit is there so you can tell whether what you are reading still describes
what is deployed. Run the command yourself for the current one.
mode: kv means a durable Redis store is attached, so auto-catalogued resources survive
cold starts. With no store configured the same code runs read-only from the seeded catalog
and says so on /discovery/health rather than failing. writable: false alongside kv is
the deliberate third state: the store makes writes possible, STELLARSIGHT_WRITE_TOKEN
makes them permitted, and an unauthenticated write endpoint on a public discovery index is
a spam magnet. See docs/DEPLOY.md for the reasoning.
Both endpoints are validated against the shipped @x402/extensions types bynpm run verify:api, which drives the real withBazaar() client at them and re-checks everyaccepts entry with @x402/core's own PaymentRequirementsSchema. The two envelopes differ
deliberately, and that asymmetry is the spec's rather than ours: the list endpoint returnsitems with offset pagination, search returns resources with a cursor.
That claim used to read "spec-exact — the same field names", asserted by reading the field
names this repo emits. It was false: search returned items, no item carried accepts, andwithBazaar(client).search() handed a stock consumer undefined. See
Where we had drifted. The assertion now observes what the client
returns instead of restating what the server believes.
CORS is * because the point is for other people's agents to call it.
The endpoints import packages/index directly; the ranking and the catalog-integrity
validation are the same code the local facilitator runs, not a reimplementation. Out of
the box the deployment serves a read-only catalog seeded at cold start. Attach a
Redis/KV store and a write token and the auto-cataloging write path turns on;/discovery/health reports which of the two is active.
What it looks like
The Sight Board. Every result is a sight — the observation a navigator takes to fix
position. Numbered, ranked, with a bearing readout, and a _EXPLAIN disclosure that breaks
the score into BM25 / metadata completeness / settlements / recency, each with its numeric
contribution and the matched terms with their tf, idf and field weight. Searching
re-orders the board with a FLIP animation.
The Catalog Integrity ledger, on the right, is a replay, and the panel says so on its
own first line. A fixed hostile corpus is pushed through the real validator byapps/web/scripts/gen-integrity.mjs at build time, and
every rule, verdict and reason it renders is a string thatcreateCatalog().upsert() actually returned — stamped with
the commit that produced it. It is evidence the validator works. It is not a claim that
anyone attacked the catalog today, and when the index does report live verdicts the panel
switches its label to say that instead.
REJECTED resource.url javascript:alert(1)
resource.url is missing or invalid
SOFT-DROP routeTemplate /v1/%252e%252e/thing
routeTemplate contains path traversal ".." after decoding
SOFT-DROP resource.iconUrl http://169.254.169.254/latest/meta-data/
iconUrl host rejected: IP literal host (decimal/octal/hex)
SOFT-DROP resource.tags[5]:over-limit ["invoice","inv","invoices", … 96 more]
99 tags submitted, catalog keeps 5 — overflow dropped to contain index pollution
That third and fourth line matter: the record survives. Soft drop means a hostile field is
discarded and the legitimate metadata around it is kept — which is exactly what the spec
requires and exactly the invariant that is easy to get wrong.
An earlier version of this section said the ledger was live and not a mockup. It was neither:
the rows were hand-written, seven of their eight rule names existed nowhere inpackages/index/src/integrity.mjs, and the caps they quoted (16 tags, 2,000 characters)
contradicted the ones the code enforces (5 and 512). The generator exists so that cannot
recur — the numbers above are now read back off the validator's own output rather than
restated, so a drift between doc and code shows up as a failing build artifact instead of a
sentence nobody rechecked. npm test runs the 66 adversarial cases the corpus is drawn from.
Scoped against SCF #45, RFP Track
STELLARSIGHT is built against the RFP "X402 Facilitator with Bazaar (discovery) support", which
names the Bazaar discovery layer as the highest-value part of the scope and says it should
carry the largest share of the budget. Every component maps to a numbered requirement:
| RFP req. | In this repo | Status |
|---|---|---|
| 3.2 Bazaar discovery layer — "the core new capability", "the hardest part of the scope" | packages/index — /discovery/resources + /discovery/search readable by the stock @x402/extensions client, BM25 hybrid ranking with a published formula and per-result _explain, auto-cataloging from the discovery extension, soft-drop validation, EXTENSION-RESPONSES reporting |
Working |
| 3.2 catalog integrity — "the facilitator is a trust boundary" | 66 adversarial tests: routeTemplate traversal under single / double / triple percent-encoding, iconUrl SSRF evasion, tag flooding, external $ref |
66/66 passing |
| 3.1 Facilitator — verify / settle / supported, fee sponsorship, self-facilitation | apps/facilitator — self-hosted on Apache-2.0 @x402/stellar, extra.areFeesSponsored, non-null reason on every rejection |
Working, testnet |
| 3.3 Agent-facing MCP interface | apps/agent — 4 MCP tools with input and output schemas, 17-code error enum |
Settled payments via MCP |
| 3.6 Conformance — "drift, not inability, is the failure mode being screened for" | npm run verify:conformance — an unmodified @x402/fetch client driven through a real 402 → sign → settle → 200. It caught v1 drift in our own seller |
Documented below |
| 3.2 seller helpers — per-parameter descriptions that make an endpoint legible to an agent | apps/seller, declared via declareDiscoveryExtension |
Working |
| UX — "docs to a paid, discoverable endpoint appearing in the Bazaar in well under an hour" | docs/QUICKSTART-SELLER.md — four steps, each ending in a curl check. A resource is listed on seller boot and re-cataloged on settle, so it is discoverable before its first payment |
59s of commands, measured |
What we deliberately did not build, and why: no on-chain registry (the RFP itself calls
it an optional stretch and explains the rent/TTL cost and the doubled settlement cost); no
mainnet; no audit; no upto implementation — that scheme has an active design
discussion opened on 3 August 2026 that
deserves a considered answer rather than a rushed one.
The point is to leave behind a piece of public infrastructure the Stellar ecosystem is
currently missing, permissively licensed, that anyone can fork and run.
Architecture
seller ──declares metadata──► STELLARSIGHT INDEX ◄──natural-language search── agent
│ ▲ │
│ │ auto-cataloged on settle (bazaar ext) │
└──────────► SELF-HOSTED FACILITATOR ◄────── 402 → sign → settle ────────────┘
│
stellar:testnet
| Component | What it is |
|---|---|
packages/index |
Catalog + BM25 hybrid search with explainable ranking, catalog-integrity validation |
packages/express |
Drop-in x402 paywall middleware for Express: price a route, take payment in a Stellar token, and get listed in the bazaar before the first payment. 45 of the 161 tests are its. On npm: npm i @stellarsight/express |
api/discovery |
Vercel functions serving that same catalog as a public hosted API — no logic of their own |
apps/facilitator |
Self-hosted x402 facilitator on @x402/stellar, sponsoring network fees |
apps/seller |
Paid API declaring discovery metadata with per-parameter descriptions |
apps/agent |
MCP server + payment client + narrated CLI |
apps/web |
Landing page and live console |
Two blockers removed by design
The two things that normally stall an x402 setup on Stellar were eliminated — not by
shortcut, but by decisions that are also architecturally better.
1. No faucet, no captcha. Rather than depending on Circle's web faucet for testnet USDC,
STELLARSIGHT issues its own SEP-41 asset (SXT) and wraps it in a SAC. The Stellar exact
scheme accepts any SEP-41 token — USDC is only the default. npm run setup therefore runs
start to finish with no web forms and no API keys.
2. No third-party facilitator. The facilitator is self-hosted on the Apache-2.0
package. That removes any dependency on the OpenZeppelin Relayer / OZ Channels — which is
AGPL-3.0-or-later, and therefore unusable by any project needing a permissive license —
while demonstrating the self-facilitation path the RFP asks for in 3.1.
The FEEPAYER account sponsors network fees, so the paying agent needs zero XLM.
Running it
npm install
npm run setup # generates accounts, issues the SXT asset, adds trustlines — all testnet
npm run dev:all # facilitator :4021 · index :4022 · seller :4023
npm run dev:web # console + landing on :5173
npm run demo # full loop: discover → 402 → sign → settle → 200
npm test # 161 tests
npm run verify:api # 46 checks, incl. the stock withBazaar() client against the handlers
npm run verify:conformance # stock @x402/fetch client pays the seller, end to end
npm run eval:search # 50 graded queries -> nDCG@10 / Recall@20 / MRR, with a CI gate
npm run load:baseline # serial vs concurrent settlement, the single-fee-payer "before"
No API keys. No captcha. No mainnet. No real money.
Listing your own paid endpoint — the seller side, timed step by step from a clean clone —
is docs/QUICKSTART-SELLER.md.
Deployment — including the routing trap where a SPA catch-all silently swallows/discovery/* — is documented in docs/DEPLOY.md.
Search ranking
The RFP states that search quality is the hardest part of the scope and the part existing
catalogs most often leave unimplemented. So the ranking here is not a .includes() filter:
- BM25,
k1 = 1.2,b = 0.75, over a field-weighted document —serviceName×3,description×2,tags×2, parameter names and their per-parameter descriptions ×2,output.format×1, URL path segments ×1. - Blend:
1.00·bm25 + 0.12·completeness + 0.08·popularity + 0.05·recency. The quality
prior caps at 0.25 against relevance's 1.00 — quality breaks ties, it never
overrides relevance. A test asserts that a 900k-settlement record loses to a
zero-settlement, 200-day-stale record when the query matches the latter. _explainper result, with the four parts asserted by test to sum exactly to_score.
Measured, not asserted
The evaluation is no longer a plan. npm run eval:search runs a 50-query human-graded
golden set (eval/golden.jsonl) through the real ranker — the samecatalog.search that /discovery/search serves — and publishes:
| Metric | Value |
|---|---|
| nDCG@10 | 0.864 |
| Recall@20 | 0.905 |
| MRR@10 | 0.920 |
| Precision@1 | 0.896 |
| No-match silence | 0.500 |
Full method and the weakest queries: docs/SEARCH-EVAL.md.
CI fails the build if any of these falls more than 0.02 beloweval/baseline.json, so a ranking change that quietly costs
relevance cannot merge unnoticed — it either holds the numbers or updates the baseline
on purpose, in the diff.
Read them as what they are: a known-item measurement over the 27-record demo corpus,
graded by the same person who wrote the ranker. Both facts inflate confidence and both are
stated in the report rather than buried. The Tranche 1 deliverable replaces this with
150–200 queries plus a rolling sample drawn from the live catalog, which nobody here
authored.
Two of the fifty queries have no right answer on purpose (quantum teleportation as a service). Half of them still return something — BM25 will match a stray token — and that
number is published as no-match silence 0.5 instead of quietly excluded. The two worst
real queries are will it rain tomorrow and logistics cost estimation, which return
nothing at all: pure paraphrases with zero lexical overlap, i.e. exactly the failure a
semantic layer fixes. That is the evidence behind the Tranche 2 deliverable, not a hunch.
docs/SEARCH-QUALITY.md documents the retrieval rationale and an
explicit cold-start section stating plainly that popularity is worthless at launch and
gameable forever — with four unimplemented mitigations ranked.
Conformance
We built against the shipped dist rather than against examples, because on this spec the
two disagree. Below: two places where the surrounding material lags the spec, and one place
where we were the party that had drifted.
Where the documentation lags the spec
Neither of these is a defect in x402. Both matchspecs/transports-v2/http.md
exactly — it is the third-party material around the spec that still shows v1 shapes. They
are listed because reading dist instead of trusting an example is what kept us on the
right side of them, not because anything upstream needs fixing.
- v2
PaymentRequirementsusesamount, notmaxAmountRequired, and resource metadata
moved toPaymentRequired.resourceas aResourceInfo. The v1 layout is still what most
examples show. Our facilitator and index read both shapes. - v2 signs into the
PAYMENT-SIGNATURErequest header, notX-PAYMENT.X-PAYMENTis
the v1 spelling and is still what much of the surrounding documentation instructs. Our
client sends both; our seller accepts both.
Where we had drifted
Our own reference seller advertised x402Version: 2 and then answered 402 in the v1 wire
format: the entire PaymentRequired object in the JSON body, no PAYMENT-REQUIRED header,
and a paywall that read only x-payment.
The spec is unambiguous — "The PAYMENT-REQUIRED header is the canonical HTTP transport
location for the PaymentRequired object", with response bodies called a server
implementation concern and the spec's own 402 example shipping {}. @x402/core implements
exactly that: getPaymentRequiredResponse falls back to the body only whenbody.x402Version === 1. Ours said 2. So an unmodified @x402/fetch client did this:
THREW: Failed to parse payment requirements: Invalid payment required response
We had not noticed, because our own client carried a fallback that accepted any body with anaccepts array. Our agent could pay our seller. Nobody else's could.
We found it by pointing a stock client at ourselves — the acceptance test the RFP
specifies, and the only test that could have caught it. The fix: emit PAYMENT-REQUIRED andPAYMENT-RESPONSE using @x402/core's own codecs, accept PAYMENT-SIGNATURE, and delete
the fallback in our client, so the bug cannot return quietly. The v1 spellings and the JSON
body are still emitted for backward compatibility; nothing depends on them.
npm run verify:conformance is that test, kept. It drives an unmodified @x402/fetch client
— wrapFetchWithPayment, no STELLARSIGHT code anywhere on the path — through a real
402 → sign → settle → 200 against a running seller, and prints the settled hash:
1. Unpaid probe — the 402 must carry a PAYMENT-REQUIRED header
PASS HTTP 402 Payment Required
PASS PAYMENT-REQUIRED decoded — x402Version 2, 1 requirement(s)
2. Stock client — wrapFetchWithPayment drives 402 -> sign -> settle -> 200
PASS HTTP 200 in 8903ms
3. Settlement receipt — PAYMENT-RESPONSE header
PASS PAYMENT-RESPONSE decoded, success=true
CONFORMANCE CHECK PASSED
tx 15c4fa24785ac42b1287d9336ad219552b07d7ff81cdf86c18edbc5c250e9726
An earlier version of this README framed the PAYMENT-REQUIRED requirement as an x402 defect
worth filing upstream. It was not — the SDK was right and we were wrong. Retracting that here
is cheaper than being corrected by a reviewer, and a conformance bug we found in ourselves,
with a stock-client test now standing over it, is the stronger story regardless.
Catalog integrity
The facilitator is a trust boundary. Clients echo the resource block back inside the
payment payload, so every discovery field is attacker-controlled.
routeTemplate— the normative regex^/[a-zA-Z0-9_/:.\-~%]+$permits%, so the..check must run after percent-decoding, and must survive double and triple encoding
(%252e%252e). Malformed%fails closed.iconUrl— SSRF evasions:127.0.0.1, decimal2130706433,0x7f.1,0177.0.0.1,[::1],0.0.0.0,169.254.169.254, percent-encoded hosts, userinfo tricks, and thedata:/file:/javascript:schemes.serviceName/tags— control characters, RTL override, length caps, dedupe before
cap, and the survival invariant: an invalid field is dropped, the surrounding metadata
is kept.
Each test cites the spec rule it enforces.
Testnet transactions
Real hashes produced by this code, with explorer links:docs/TESTNET-TXS.md.
Twenty-four in total, and the split matters: 16 are x402 payments — the demo loop and the
stock-client conformance run — and 8 are setup and cleanup, meaning trustlines, the SAC
deploy, minting the test asset, and returning a legacy balance. Only the 16 payment rows are
evidence that the payment path works; the two newest of them settled entirely through the
hosted stack.
Technical architecture
The full engineering document is docs/ARCHITECTURE.md: the Soroban
authorization-entry flow, SEP-41/SAC settlement, the fee-bump ceiling and how it was
derived, the catalog's trust boundary, the planned upto contract, the security model, the
monitoring plan, the deployment topology, and how each of those maps to a funded tranche.
Start there if you are evaluating this rather than running it.
Durability, throughput and the security posture
Three things a reviewer will look for, answered here rather than in a support thread.
Cataloging survives the process
A settlement through the hosted facilitator writes the post-validation record to the
same durable store api/discovery/* reads. Before, the settle path only ever touched the
instance's own heap: a third-party seller could settle, watch EXTENSION-RESPONSES saysuccess, and never appear in the public catalog, because the instance that catalogued
them was gone. Auto-cataloging that does not outlive the process is a demo, not a Bazaar.
Durability is reported, never assumed — /health on the facilitator carriesdurableCataloging: { enabled, transport }, and a store that rejects a write says so in
the settle response instead of silently claiming success. With no store configured the
behaviour is unchanged and the reason is stated.
One fee-payer is the bottleneck, measured
docs/LOAD-BASELINE.md, produced by npm run load:baseline:
| Attempted | Succeeded | |
|---|---|---|
| Serial (one at a time) | 4 | 4 — 100% |
| Concurrent | 10 | 1 — 10% |
Same payment, same stack, same signer, same network. Only the timing changed. That is the
controlled experiment, and it is the only honest way to attribute the failures: upstream@x402/stellar collapses a rejected submission intosettle_exact_stellar_transaction_submission_failed without surfacing the underlyingtx_bad_seq, so the message alone proves nothing. A channel-account pool is the first
funded deliverable, and this file is the "before" it has to beat.
Threat model and monitoring, written before mainnet
docs/THREAT-MODEL.md— assets, trust boundaries, and twelve
threats each mapped to the control that answers it and the test that proves it. The
gaps are marked as gaps: no per-seller identity yet, no alerting on fee-payer drain yet,
sequence contention undefended.docs/MONITORING.md— the signal, threshold and response for each
of those surfaces, with what exists today marked ✅ and what is funded work marked ⬜.docs/upto-position.md— why there is nouptocontract here
yet: four independent Stellar implementations disagree on whether zero settlement submits
a transaction and on what happens to the residual allowance. Shipping a fifth
incompatible contract adds a data point, not a decision. The position is written against
stellar/x402-stellar#72.
License
Apache-2.0, public from the first commit.
github.com/pedro-pelicioni/stellarsight
Built in São Paulo, Brazil.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi