stellar:testnet, exactly how it uses Stellar, and what the award funds on top of
it. Everything below is in the public repository
github.com/pedro-pelicioni/stellarsight
(Apache-2.0), and the hosted deployment is stellarsight.xyz.
Claims here are written to be checked, not believed. Where a section states a number, the
command that produces it is named. Where something is not built, it says so in the same
sentence rather than in a footnote.
Table of contents
- System overview
- The payment path, in Stellar terms
- The Bazaar: the facilitator-side catalog
- Search
- The agent and seller surfaces
- The
uptoscheme (planned, Tranche 2) - Security and trust model
- Monitoring and operations
- Deployment topology
- Architecture mapped to the funded tranches
- Operating as a public good
1. System overview
1.1 The gap this fills, stated precisely
Stellar can already settle an x402 payment. The@x402/stellar package implements the
exact scheme, and this project composes it rather than reimplementing it. What Stellar
does not have is the other half of the protocol:
An agent that can pay but cannot discover is an agent with a wallet and no map. The RFP
names discovery as the highest-value part of the scope and says settlement is largely
solved. This architecture takes that literally: the catalog is the product, and the
payment loop exists around it so the catalog can be populated by something real.
1.2 The invariants
Three properties hold everywhere in this design, and every later section is downstream of them. The facilitator is non-custodial. It holds no user funds, has no deposit or withdrawal path, and cannot move money. Every settlement is a direct SEP-41 transfer from the buyer’s account to the seller’s, authorized by the buyer’s own signature over the full invocation. A fully compromised facilitator can refuse service and can waste its own sponsored fees. It cannot redirect a payment, change an amount, or move funds it was not authorized to move. The catalog is a trust boundary. Discovery metadata is attacker-controlled: clients echo theresource block back inside the payment payload, and everything the catalog
returns will be read by an LLM-driven agent that then sends money. Validation is therefore
mechanical and adversarial, and 66 of the repository’s tests exist only to attack it.
Listings are born from settled Stellar payments. A resource enters the catalog when a
settlement carrying the discovery extension succeeds, and the entry is bound to that
payment’s recipient. This is the anti-spam mechanism and it is Stellar-specific: account
and trustline reserves put a real cost on manufacturing fake listings, which an off-chain
registry cannot charge.
1.3 What runs today, and what the award funds
1.4 System architecture
The trust boundary is the edge ofcore: everything crossing into it from a seller or a
buyer is attacker-controlled and goes through the validator before it can reach the catalog
or the ledger.
1.5 The stack in one paragraph
Node ≥22, pure ESM, no build step outside the web console. One Express app is the facilitator; one npm workspace (packages/index) is the catalog, its ranker and its
integrity validator; the same modules are mounted three ways — locally on :4022, as
Vercel Functions under /discovery/*, and inside the facilitator process — so there is one
definition of the wire format and no surface can drift from it. State that must outlive a
process lives in a Redis-compatible store. Nothing else is stateful. All Stellar
cryptography is delegated to @x402/stellar; this project never signs or verifies by hand.
2. The payment path, in Stellar terms
2.1 Protocol surface
x402 v2 throughout. The 402 challenge travels in thePAYMENT-REQUIRED response
header, the signed payload arrives in PAYMENT-SIGNATURE, the receipt returns in
PAYMENT-RESPONSE, and cataloging outcomes are reported in EXTENSION-RESPONSES. The v1
spellings (X-PAYMENT, the challenge in the JSON body) are still accepted and emitted for
compatibility, and nothing depends on them.
Networks are CAIP-2 identifiers: stellar:testnet today, stellar:pubnet at Tranche 3.
GET /supported advertises:
2.2 The buyer signs an authorization entry, not a transaction
This is the Stellar-specific heart of the flow and the reason the fee model works. A Soroban authorization entry (CAP-0046-11) binds the payer’s signature to the entire invocation tree: the contract being called, the function, and every argument. It does not bind the transaction envelope, the source account, or the fee. So the buyer can authorize exactly this payment while a different account submits it and pays for it.2.3 SEP-41 / SAC
The payment itself is a SEP-41transfer on a Stellar Asset Contract. Any SEP-41 asset is
accepted; the scheme is asset-agnostic by construction.
On testnet the deployment issues its own classic asset (SXT) and wraps it with
createStellarAssetContract, so npm run setup runs start to finish with no Circle faucet
captcha and no API key. That is a deliberate developer-experience decision, not a shortcut:
the exact scheme accepts any SEP-41 token and USDC is only the default. Mainnet launches
on the Circle USDC SAC (Tranche 3, deliverable 3.1), with additional assets enabled by
configuration.
Amounts are integer strings in atomic units, 7 decimals. In x402 v2 the price field on
PaymentRequirements is amount; the v1 name maxAmountRequired fails
PaymentRequirementsSchema in the installed @x402/core, so an accepts entry built with
it is silently unusable. Both are read on input; only amount is emitted.
2.4 Fee sponsorship and the 500,000-stroop ceiling
The facilitator’sFEEPAYER account is both the transaction source and the fee-bump
signer, so the paying agent needs zero XLM — it holds only the asset it is paying with.
maxTransactionFeeStroops is a safety ceiling, not a fee that gets paid: @x402/stellar
simulates the transfer and refuses at /verify, before any money moves, if the
simulation-derived fee exceeds it. The library default is 50,000.
That default is too tight for this scheme and was breaking payments intermittently. A
SEP-41 SAC transfer with a sponsored fee bump simulates around 57,000 stroops on testnet
today, above the default, and the margin moves with network load — so the failure appears
under load and disappears when you go looking for it. Observed during development: four
consecutive /verify rejections at 57,031–57,038 stroops, then a settlement that squeaked
through at max_fee 57,227 an hour later.
The ceiling is therefore set to 500,000 stroops (0.05 XLM): 8.7× the observed
simulation, still small enough to catch a genuinely runaway transaction, which is what a
ceiling is for. The derivation is in the source next to the constant
(apps/facilitator/src/server.mjs), so a reviewer can audit the reasoning and not just the
number. Independently, the Vellar submission reports hitting the same 50,000 default as a
hard blocker for policy-governed payments and raising it to the same 500,000 — two projects
converging on the number from opposite directions.
2.5 Replay and expiry are enforced on-chain
There is deliberately no facilitator-side replay cache. Soroban consumes the authorization nonce when the call executes, andsignatureExpirationLedger bounds the
window. A cache would be a second source of truth that can be poisoned, out-of-sync, or
bypassed by a second facilitator instance; the ledger cannot. Client-side, a replayed or
expired authorization is mapped to a distinct machine-readable code
(STELLARSIGHT_REPLAY_REJECTED, STELLARSIGHT_AUTH_EXPIRED) so the caller can tell the two
apart.
2.6 One fee-payer is the current bottleneck, and it is measured
A Stellar account has one sequence number. Today every settlement is signed and fee-bumped from a singleFEEPAYER, so concurrent settlements do not run concurrently — they collide.
This is measured rather than assumed, by a controlled experiment
(npm run load:baseline, published in LOAD-BASELINE.md):
Same payment, same stack, same signer, same network; only the timing changed. The serial
control group is what makes the attribution honest:
@x402/stellar collapses a rejected
submission into settle_exact_stellar_transaction_submission_failed without surfacing the
underlying tx_bad_seq, so the error text alone proves nothing.
Tranche 1, deliverable 1.1 replaces this with a pool of channel accounts, each with its
own sequence number, round-robin leased, with sequence-drift quarantine and reconciliation,
targeting 25–50 concurrent settlements with zero sequence-number failures.
3. The Bazaar: the facilitator-side catalog
3.1 Cataloging is a side effect of getting paid
There is no seller registration step. When a settlement succeeds and its payload carries the discovery extension, the facilitator projects the payment into a catalog record and upserts it. The seller middleware additionally pre-registers a route at boot, so a resource is discoverable before its first payment; the settlement then promotes it, increments its observed settlement count, and clears any demo flag. Two properties make this trustworthy rather than merely convenient:- The listing is bound to the settled payment’s recipient. Payment terms come from the
settlement, not from seller-supplied text, so nobody can list a service under another
seller’s
payToor quote a price they do not charge. - The index stays off-chain. An on-chain registry is an explicit non-goal: the RFP calls it an optional stretch and it costs Soroban storage rent, TTL management, and a doubled settlement cost. The chain is the source of truth for payments; the catalog is an index over them.
3.2 One wire format, three mountings
The internal catalog record and the spec’sDiscoveryResource are different shapes, and
the projection between them lives in exactly one place (packages/index/src/discovery.mjs,
toDiscoveryResource). Three adapters mount it:
The spec puts the URL in
resource as a string with presentation fields at the top
level and payment terms in accepts[]; the internal record nests them. STELLARSIGHT-native
fields (id, settlements, seeded, _score, _explain, and flat mirrors of
accepts[0]) ride along as additive keys that a spec client ignores.
The two envelopes differ deliberately, and that asymmetry is the spec’s rather than ours:
the list endpoint returns items with offset pagination, search returns resources with a
cursor. GET /health on :4022 reports wireShape: "spec" so the agreement is checkable
rather than asserted.
3.3 Durability and graceful degradation
The catalog has three states, and/discovery/health reports which one is live:
A public Bazaar that answers out of the box beats a write-capable one that needs setup
nobody has done, so the read-only baseline must never break. A store that is configured but
unreachable degrades to the seeded catalog and says so on
/health rather than returning
500.
Both writers — the facilitator’s settle path and the authenticated announce path — persist
the post-validation record, never the raw request body, so the store can never be used
to smuggle a field past the validator. Durability is reported, not assumed: a rejected
durable write is surfaced in the response instead of being swallowed.
3.4 Catalog integrity
The facilitator is a trust boundary, so every discovery field is treated as hostile input. 66 adversarial tests enforce the rules; the ones that matter most:routeTemplatetraversal. The spec’s normative regex^/[a-zA-Z0-9_/:.\-~%]+$permits%, so the..check must run after percent-decoding, and must survive double and triple encoding (%252e%252e). Decoding is fixed-point, capped at five passes, and a malformed%fails closed.iconUrlSSRF. Rejects127.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.- Caps and control characters.
serviceName32, tags 5 × 32, description 512, dedupe before cap, control characters and RTL overrides stripped.
EXTENSION-RESPONSES with a non-null reason.
3.5 Provenance: demo breadth vs real resources
The catalog ships a 27-record demo corpus on.example hosts. It exists 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 seeded record is flagged seeded: true and
pinned to settlements: 0, so demo breadth can never inflate an observed-settlement total,
and the flag survives the wire projection.
?seeded=false returns only resources that were announced or paid for. Deleting the corpus
would hide the ranker; labelling it and making the split queryable costs nothing and is
checkable. A real announcement sharing an id with a seed record promotes it and clears the
flag.
4. Search
4.1 Ranking
Field-weighted BM25 (Okapi,k1 = 1.2, b = 0.75) over a bag of tokens built from
serviceName ×3, description ×2, tags ×2, parameter names and their per-parameter
descriptions ×2, output.format ×1 and URL path segments ×1. The analyzer casefolds, folds
accents, splits camelCase, strips stopwords and suffixes.
A quality prior is blended on top of relevance:
_explain with the four components, asserted by test to sum exactly to _score.
There is no LLM in the default ranking path. Results stay reproducible and query cost
stays at zero.
4.2 Measured quality, with a gate
npm run eval:search runs 50 hand-graded queries (eval/golden.jsonl)
through the real catalog.search:
Graded 0–3, exponential gain
2^rel - 1, judged documents at grade ≥2 counted as answers
for the binary metrics. CI fails the build on a regression greater than 0.02 against
eval/baseline.json. Thirteen tests check the metric arithmetic
itself, because a published nDCG is only worth the maths behind it.
The caveats belong next to the numbers. The corpus is the 27-record demo catalog, so
this is a known-item measurement, and the labels were written by the same person who wrote
the ranker. Tranche 1 takes the set to 150–200 queries plus a rolling sample from the live
catalog, which is where the second caveat stops applying.
4.3 What is not built
Two of the fifty queries have no right answer on purpose. Half of them still return something — BM25 will match a stray token — and that is published asno-match silence 0.5
rather than quietly excluded.
The two weakest real queries, will it rain tomorrow and logistics cost estimation,
return nothing at all: pure paraphrases with zero lexical overlap. That is precisely the
failure a semantic layer fixes, and it is the evidence behind Tranche 2’s CPU-only embedding
deliverable rather than a hunch. SEARCH-QUALITY.md documents the
cold-start problem honestly: popularity is worthless at launch and gameable forever, with
four unimplemented mitigations ranked.
5. The agent and seller surfaces
5.1 MCP
An MCP server exposes four tools —stellarsight_search, stellarsight_browse,
stellarsight_describe, stellarsight_pay — each with input and output schemas,
structuredContent, and a 17-code error enum where every rejection carries a non-null
reason. describe returns a call-construction brief: per-parameter types, descriptions,
examples and a howToCall block, so an agent can construct a valid call with no external
documentation.
The server holds no buyer keys on the discovery path. Signing stays client-side.
Today MCP is stdio-only. Tranche 2 adds a hosted HTTP endpoint and CI adapter tests proving
stock TypeScript, Go and Python discovery clients parse the responses.
5.2 Seller integration
@stellarsight/express is a drop-in paywall: price a route, take payment in a Stellar
token, and get listed in the Bazaar before the first payment.
/usr/bin/time in QUICKSTART-SELLER.md.
5.3 Conformance in both directions
Two harnesses, both driving unmodified upstream clients:npm run verify:conformance— a stock@x402/fetchclient (wrapFetchWithPayment, no STELLARSIGHT code on the path) completes 402 → sign → settle → 200 and prints the settled hash.npm run verify:api— 46 checks driving the realwithBazaar()client from@x402/extensionsagainst the handlers, re-validating everyacceptsentry with@x402/core’s ownPaymentRequirementsSchema.
x402Version: 2 and answered 402 in the v1 wire format, so an unmodified
client threw while our own client — which carried a lenient fallback — did not. The fix was
to adopt the SDK codecs and delete the fallback so the bug cannot return quietly. The RFP
says drift, not inability, is the failure mode being screened for; this is the test that
screens for it.
6. The upto scheme (planned, Tranche 2)
6.1 Why there is no contract here yet
exact settles one fixed price quoted before the request. Metered services — token
billing, bandwidth, inference — need upto: the buyer authorizes a ceiling and the seller
settles only what was consumed. Discovery without metered pricing lists services an agent
cannot pay correctly, which is why this sits inside a discovery submission.
The design is converging, and we are not going to fragment it.
stellar/x402-stellar#72 shows three
independent implementations arriving at the same shape — the client signs a ceiling, a
recipient, an expiry and a nonce, the actual amount stays unsigned, and a small contract
enforces actual ≤ max and single-use on-ledger — and
x402-foundation/x402#3134 proposes the
Stellar binding as a spec, open for review since 12 August 2026.
A fourth private design would add a data point, not a decision. What is genuinely missing is
narrower, and it is what this project is positioned to supply:
PaymentRequirements.amount holds one value. For exact it is the price; for upto the
only figure a seller can honestly publish before the call is the ceiling, which is not the
price and is usually much larger. A catalog that puts a ceiling where an agent reads “cost”
makes every metered service look expensive next to a fixed-price one — a bias against
exactly the services upto exists to enable. That is a discovery problem, invisible from
the settlement side, and it is the contribution we have made to the thread
(our comment,
full text in upto-position.md).
6.2 The contract shape
Deliberately minimal, because the audit surface should be one function. This is the shape the thread has converged on; where the upstream spec differs when it lands, the spec wins:- The payer signs
require_auth_for_args((token, pay_to, max)).actualis supplied at settlement and is bounded by both the contract and the token allowance, so none of the recipient, the token or the ceiling can be changed after signing. approveis nested inside the payer’s authorization tree; payout usestransfer_from, so funds move directly from payer topay_toand the contract never holds a balance.- Soroban consumes the authorization nonce when the call executes, so one signature settles once and cannot be replayed.
- Three clocks, normatively ordered: allowance expiration ≥ contract deadline ≥
settlement time, all derived from the operator’s advertised
maxTimeoutSeconds.
6.3 What Tranche 2 delivers
Three things, none of which is a fourth spec:- The discovery-side requirements, contributed to the open spec (deliverable 2.1). The three rows in bold in §6.1, argued upstream while #3134 is still under review.
- The scheme implemented as standardized, and proven interoperable (deliverable 2.2).
Settled testnet hashes for the partial, maximum and zero cases, a negative-test matrix
covering above-maximum, altered recipient, altered token, expired authorization, replay
and unexpected sub-invocations — and, the part that does not exist on Stellar today, an
interop report showing this facilitator settling a payload produced by a different
Stellar
uptoimplementation, published with both parties named. Three implementations exist and no two have been tested against each other, so interoperability here is currently an assertion rather than a measured property. - Metered pricing in the catalog (deliverable 2.7). The implementation of what §6.1 argues: listings that carry a ceiling and a unit or typical price, a budget filter that stops excluding cheap metered services, and ranking weighted by settled value rather than by call count.
7. Security and trust model
The full analysis is THREAT-MODEL.md — twelve threats, each with the control that answers it and the test that proves it. Summary:7.1 Trust boundaries
- Seller → facilitator. Discovery metadata is attacker-controlled and echoed back inside payment payloads. Mitigated by the integrity validator (§3.4).
- Buyer → facilitator. Payment payloads are attacker-controlled; all cryptographic
validity is delegated to
@x402/stellar, never reimplemented. - Facilitator → Stellar. Non-custodial by construction (§1.2).
- Catalog → agent. Listing text is untrusted content, not instructions.
7.2 Risk register (abbreviated)
7.3 Key management
OneFEEPAYER secret in the deployment environment; the module derives the signer at load,
so a missing secret fails at boot rather than on the first settlement. No user keys are ever
held. Tranche 3 adds hardware-backed key storage, a documented rotation runbook, and
sponsor-account balance alerting before mainnet.
7.4 Residual risk, stated plainly
Bus factor of one, no external audit yet, testnet only, and a single shared write token that is adequate for a demo catalog and inadequate for a public index with third-party sellers. Each has a named mitigation and a tranche.8. Monitoring and operations
MONITORING.md pairs every surface with a signal, a threshold and a response, marking what runs today (✅) against what is funded work (⬜). The load-bearing signals:
One severity split, because a solo maintainer with five severities has one severity: page
(fee-payer runway, settlement success, store unreachable, conformance failure) and ticket
(everything else).
Deliberately not monitored: per-payer behavioural profiling, and content moderation of
listings. Integrity validation is mechanical; judging what a service is would make the
facilitator an arbiter of what may be sold, which is the opposite of permissionless.
9. Deployment topology
9.1 Today
:4021 (facilitator), :4022 (index) and :4023 (seller).
Two serverless caveats, stated rather than hidden: /events is SSE from a function, so a
stream ends when the function’s clock does and the client reconnects; and /settle waits on
Stellar RPC, covered by maxDuration with room. Tranche 1 evaluates moving the settlement
path to a persistent host, and the cost is already in the budget.
9.2 What mainnet requires (Tranche 3)
Fail-closed startup unless the sponsor keys, the audited contract address, the asset allowlist and measured fee ceilings are all configured. Two Soroban RPC providers with failover. Circle USDC SAC as the launch asset. A public status page publishing 30 days of uptime against a 99% target with its exclusions stated — planned maintenance and upstream Stellar or RPC outages — because an availability number without exclusions is marketing.9.3 Self-hosting
Apache-2.0 end to end, with no AGPL anywhere in the dependency path — which is why the facilitator is self-hosted on@x402/stellar rather than depending on the OpenZeppelin
Channels relayer (AGPL-3.0-or-later, disqualifying for a permissively licensed project).
npm install && npm run setup && npm run dev:all reproduces the whole stack on testnet with
no faucet, no captcha and no API key.
10. Architecture mapped to the funded tranches
11. Operating as a public good
Decentralization. The payment path is non-custodial and the correctness of a payment never depends on trusting this operator (§1.2). Client helpers take a list of facilitators with health checking rather than a single hardcoded operator, so adoption does not tie the ecosystem to us. The whole stack is self-hostable, and the catalog’s data model is the spec’s, not ours — a competing Bazaar can serve the same records. Privacy. The catalog indexes services, not users. Operational metrics only: settlement latency and success rate, catalog size, discovery query latency. No behavioural profiling of payers, no usage data sold or shared. Payer addresses are already public on-chain; nothing is aggregated into a dossier. Maintenance and stewardship. Apache-2.0 from the first commit. Public CI that anyone can run, on both Node versions the project claims to support. Tranche 3 funds four months of post-launch operations and a handoff document naming the maintainer, the escalation path and the responsibility boundary — because a service that dies when the grant ends is what the RFP is trying to avoid. The failure mode the RFP screens for is drift as the spec moves, so nightly conformance is scheduled to continue past the grant window.Appendix A — Verify this in 60 seconds
Appendix B — Repository map
Apache-2.0 · Built in São Paulo, Brazil · github.com/pedro-pelicioni/stellarsight