The discovery layer for x402-payable services on Stellar.
Live now: periplo-testnet.fly.dev,
the facilitator running on stellar:testnet. Try
GET / or
GET /supported directly,
no setup required.
The Bazaar has a real external seller in it, not just our own demo
resource. agentpayments.fi built its
own resource server, pointed it at this facilitator, and settled a real
payment on stellar:testnet, no coordination beyond following
docs/SELLERS.md. Try
GET /discovery/search?query=conformance
directly: their resource is the top result, alongside ours, found by a
buyer who has never seen either service before. This is what the
catalog is for, demonstrated by someone who isn't us.
Reviewing this for the RFP round?
docs/FOR-REVIEWERS.md is a ten-minute,
human-written path through this repo, not written for a Claude Code
session the way CLAUDE.md is.
Status: Phase 6, the upto Soroban contract, is complete. The
facilitator is live on stellar:testnet at
periplo-testnet.fly.dev. The rest of
Phase 10 is not done; see docs/DEFERRED.md. This
README states what is built, linked, tested, or hashed today. Everything
else is marked as planned.
There is no frontend yet. The developer hub (apps/hub) is Phase 9
and has not started. /browse, /playground, /status and the rest of
§10's routes do not exist. The facilitator's JSON API is the only
user-facing surface right now.
Twenty-plus real teams are competing for the same SCF #45 award. Here is
the one thing none of the others can currently show with real, linked
evidence: two direct competitors in the same round chose to build on the
upto payment spec this project opened upstream
(x402-foundation/x402#3098),
rather than fork their own.
- Rialto (Iam0TI,
0d1026/Rialto) opened a competing design against the same spec file (#3134, thestatelessprofile). It was credited and merged into#3098as a second named profile, not left as a rival PR for maintainers to arbitrate between. - AutoLayer (
autolayer-labs) engaged the same#3098thread and said directly that it "will not open a third competing spec PR," committing instead to implement whichever profile maintainers select and to send an implementation PR against the converged spec rather than a document of its own.
Neither commitment came from us asking; both are quoted, dated, and
linked, not paraphrased. Full chronological writeup, every link and
finding sourced rather than restated, is in
docs/UPTO-CONVERGENCE.md.
A third, different kind of external signal landed on the same thread on
2026-08-21: an independent implementer unaffiliated with this project,
davedumto, reviewed five real Stellar upto implementations in
source
(rail402, Rialto, openx402, LumenGate, and this project's contract via
#3098) against a proposed reconciliation of the spec, and named this
contract's own handling of nonce-TTL replay as one of the cases the
merged spec should require every implementation to answer correctly.
It already does, verified live on testnet, not just asserted; full
technical detail in docs/THREAT-MODEL.md.
This is the kind of evidence no competitor can manufacture: an
unsolicited, source-level review from someone outside the project,
not self-assessment. As of 2026-08-23, that thread is still active
(Rialto and davedumto continuing to work through interop details like
autoRevoke defaults and nonce-TTL edge cases) and #3134 itself
remains open and unmerged, not yet resolved.
2026-08-25: a separate, unrelated fix fed back into this same
convergence story. Moving our own facilitator's catalog to settle-only
(the "Automatic cataloging" bullet below, prompted by
x402-foundation/x402#3226)
turned up that pedro-pelicioni's stellarsight facilitator had
independently reached the same settle-only reading. We cited that
convergence, not as an isolated technical note but as further evidence
for this section's own thesis, in a nudge on
stellar/x402-stellar#72
asking directly whether #3098 or #3134 is what the wire spec
consolidates onto. Answered the next day: bomanaps recommended
consolidating on #3134, backed by real evidence (signed commits, a
deployed contract exercised on testnet for both G-account and
C-account payers, 7 of 7 tests passing), and bomanaps and
davedumto
(a third independent reviewer who compared five real Stellar upto
implementations in source) have since agreed on a concrete structure
for the merged document. Full, sourced chronology in
docs/UPTO-CONVERGENCE.md.
Also responded on #3098 itself to pedro-pelicioni's pricing-metadata
proposal for the bazaar extension
(#3181):
acknowledged it as complementary to upto, not competing, and corrected
our own earlier speculation once the actual design was published,
pricing lives at a different level of the wire structure than our own
dedupe key needs, so it doesn't solve that specific problem the way we'd
guessed it might, not a flaw in the proposal itself. Full writeup in
docs/UPTO-CONVERGENCE.md.
-
Monorepo tooling: pnpm workspaces, TypeScript 7 (strict,
noUncheckedIndexedAccess,exactOptionalPropertyTypes), Vitest, Biome, GitHub Actions CI. -
packages/licence-checkis the CI gate. It fails the build on any AGPL or copyleft transitive dependency (constraint: spec §1). It is unit-tested, including the exact AGPL-3.0-or-later case the spec names: the OpenZeppelin Relayer license. -
packages/evidence-checkis a second CI gate: on every push, it re-fetches every transaction hash cited below from Horizon, re-checks every cited GitHub issue/PR against the GitHub API, and re-confirms every internal doc link still resolves, so this evidence table is a self-auditing claim, not a photograph of the day it was written.docs/THREAT-MODEL.mdformalizes the spec §6 threat/control/test table into its own citable file, each row pointing at the real code and test behind it. -
packages/bazaaris the catalog trust boundary.checkRouteTemplatedecodes a route template fully, then rejects path traversal, absolute URLs, protocol-relative paths, backslash traversal, null bytes, and malformed or overlong encoding. Decoding before checking is what stops bypasses like%2e%2eand/%2f%2fevil.exampleagainst a naive check.softDropFieldskeeps every metadata field that validates and drops only the ones that fail, so one bad field never rejects the whole listing. 45 unit tests exercisecheckRouteTemplatealone; the gate requires ≥20. The whole repo has 156 tests as of Phase 5, including the live-Supabase integration suites, counted with a freshpnpm run cirun. -
conformance/baseline/holds real, captured HTTP transcripts against the publicx402.orgreference facilitator: its/supportedresponse forstellar:testnet, confirmingextra.areFeesSponsored: true, and confirmation that it has no discovery (Bazaar) endpoints today. That gap is what this project fills. -
supabase/migrationsholds the live catalog schema on a real Supabase project: theresourcestable, its full-text (gin) and vector (hnsw) retrieval indexes, and row-level security. Reads are public. Writes go through the service role only, verified with automated tests that run against the real project. Seepackages/bazaar/src/dbfor the typed client. -
apps/facilitatorimplementsverify,settle, andsupportedfor theexactscheme, built on@x402/coreand@x402/stellar. Settlement logic comes from those packages. It also now implementsupto(src/upto-stellar-scheme.ts, since no published@x402/stellarclass exists for it), registered the same way. A real settled transaction onstellar:testnetfor each scheme is recorded inconformance/RESULTS.md, with every hash checked independently against Horizon.uptoisn't configured on the live deployment yet (docs/DEFERRED.md). It is importable as a library, with no HTTP hop required, for self-facilitation inside a resource server, walked through end to end (a real, working example, not just described) indocs/SELF-FACILITATION.md. It also ships as a Hono app for hosted or self-hosted use. It is live at https://periplo-testnet.fly.dev: tryGET /,GET /health, orGET /supporteddirectly.stellar:pubnetis not configured because no mainnet key exists yet. See Deployment below for how it runs.Beyond our own settlement scripts, the official x402 e2e conformance suite (
x402-foundation/x402's owne2e/, not a Periplo-authored equivalent) was run end to end against the live deployment above via its documentedexternal-proxiesmechanism: realtypescript/http/axiosclient, realtypescript/http/expressserver, real/exact/stellarpayment, verdict✅ Test passed. A same-day follow-up ran it again with--extensions=bazaar, forwarding the facilitator's own/discovery/resources//discovery/searchroutes to the live deployment too: the suite's own Discovery Validation step, which calls those routes directly against the facilitator, confirmed the just-paid resource was cataloged and discoverable, verdict✅ Discovery Validation: PASSED, the strongest evidence of the two, since it's the exact capability this RFP funds. Both settled transaction hashes are independently checked against Horizon, same standard as every other hash in this README. A real gap found in the suite's own client bootstrapping along the way is filed as x402-foundation/x402#3187. Own PR proposed, mergeable, awaiting maintainer: x402-foundation/x402#3228 scopes EVM/SVM client signer derivation to the selected--families, the same pattern every other family already followed, plus a second gap found the same way: the harness's own preflight check validated facilitator env vars but never client env vars, so a family-scoped run could pass that check and still crash deep inside a client. Rebuilt and re-verified against current upstreammainbefore opening the PR, not just the original fix: the fulltypescript/ande2e/workspaces built clean, and calling the realcreateE2EClient()/runClientScenario()under five scenarios (no client creds, matching the exact reported crash; EVM-only; SVM-only with a real generated Ed25519 keypair; both; and the batch-settlement-without-EVM-creds guard) all passed. Fixing this benefits our own conformance infrastructure directly, not charity toward an unrelated repo: it's the same suite this README's own settled transactions above ran through. Full transcript and setup for both e2e runs indocs/conformance/2026-08-17-x402-e2e-stellar-exact.md.Reviewing
@x402/core, the package this facilitator is built directly on, as part of a wider pass over the dependency (not triggered by anything breaking in our own deployment, which only ever registers a single Stellar namespace), we found a real bug inx402Facilitator.derivePattern(): registering a facilitator against networks from more than one CAIP-2 namespace in a single call silently drops wildcard matching in every namespace involved, not just the second. Verified empirically against the published@x402/core@2.21.0, with a working reproduction contrasting mixed-namespace registration (fails) against single-namespace registration (works as intended). Filed as x402-foundation/x402#3172. Status: fixed, not just filed. We proposed the fix ourselves rather than waiting on a maintainer: x402-foundation/x402#3215, open and mergeable, 657 tests passing.derivePattern()now derives one wildcard pattern per namespace present in a registration, instead of collapsing the whole call to a single literal pattern from the first network. Our first regression test was itself wrong: it assumed a namespace with only one registered network should get wildcard coverage after the fix, which contradictsderivePattern()'s own pre-existing single-network rule, unrelated to this bug and correct as-is. We caught it because the test failed against our own correct fix, corrected the test to use multi-network namespaces, and posted the correction publicly on the original issue rather than letting a flawed original repro stand uncorrected. A separate, unrelated defect surfaced 2026-08-23: the PR'scheck-verified-commitscheck was failing because its one commit was unsigned. Fixed by amending it with a registered SSH signing key and force-pushing the same branch; GitHub's API confirmsverified: trueon the amended commit and the check now passes. Still open, awaiting maintainer review. -
Automatic cataloging lives in
apps/facilitator/src/discovery.ts. A payment carrying thebazaardiscovery extension is validated and written to the catalog on/settle. There is no separate registration step, no dashboard, and no API key.2026-08-25: settle-only, corrected from an earlier verify-or-settle reading. Cataloging used to run on
/verifytoo, wheneverisValid: true. That reading is conforming with the extension spec's own text, butisValid: trueonly proves a payload could settle, not that it did: no funds move on verify, so a catalog entry, and everyacceptsoption in it, could be produced for one HTTP request and no balance. This is exactly what x402-foundation/x402#3226 audited in public, with a reproduced example of it happening against a live facilitator. Our own ranking has no popularity or call-count column to inflate, but the catalog's contents, resources and payment options that were never actually paid for, could still be minted the same way. Fixed by removing the/verify-side write entirely;/settle'sresult.success(a real settled transfer, confirmed the same way every transaction in this README is confirmed) is now the only trigger. pedro-pelicioni (stellarsight, a second real Stellar Bazaar facilitator, credited elsewhere in this README and indocs/UPTO-CONVERGENCE.md) independently reached the same settle-only reading and confirmed it in the same GitHub thread, code checked directly rather than taken on the comment alone. Full writeup indocs/DEFERRED.md.pnpm run cigreen throughout, 255 tests.2026-08-27: this stopped being something we only observed. Added our own data point to
#3226directly, citingfae6daa9(the commit above), a third implementation reaching settle-only without coordinating with the other two. whawk46 named it explicitly as part of the consensus: "With Periplo, @pedro-pelicioni, and our datasets aligned, we have the consensus needed to make settle-only catalog provenance a normative requirement in the specification." The issue itself has since been retitled from an open question to a formal proposal: "Label catalog provenance, verify-only versus settled, so Bazaar counters mean something." Same pattern as the rest of this section: not just building on what this project proposed, now citing this project's own evidence to write the norm.2026-08-26:
extensions.bazaarwas echoed empty for every resource, found reconciling a real integrator's conformance report against the live deployment instead of assuming either side was right. Two of the report's three claimed gaps didn't match the raw 402 challenge (descriptionand the bazaar declaration were both fully populated there), but did matchGET /discovery/resourcesandGET /discovery/search, which both echoedextensions.bazaar: {}regardless. Root cause: the catalog tracked which extension keys a resource declared, never their actual payload, contradicting@x402/extensions/bazaar's ownDiscoveryResourcetype, which documents that field as "Extension payloads echoed from discovery." Fixed with a newextension_payloadscolumn, deployed, and verified against the live catalog with a real re-settled payment (12470945ac72..., Horizon-verified):GET /discovery/searchnow returns the full declaredinfo/schemaobject. Separately deleted a stale Phase 4 test fixture (financial_analysis_da8703fa-..., literal placeholderasset/payTovalues, surfacing in every search result regardless of relevance) that the same reconciliation turned up. The report's CORS-header claim, initially left unresolved, turned out to measure a different, unrelated deployment entirely (confirmed by the report's own author and independently corroborated), never Periplo.The first real external seller published and settled for real the same round (
agentpayments.fi), and found a genuine, money-relevant bug doing it:EXTENSION-RESPONSESnever reached their code from/settle, even though cataloging worked. Root cause verified before fixing, not assumed: a direct fetch to/settleshowed the header is sent correctly on the wire, so the gap traced to the installed@x402/core@2.22.0dependency itself, whoseHTTPFacilitatorClient. settle()/.verify()read the header only toconsole.logit internally, then discard it, never attaching it to what those methods return to the caller, confirmed reading the actual compiled source. Filed as x402-foundation/x402#3270, with a proposed fix (populate theextensionsfield those response types already declare but never use). Status: fixed on our own side the same day, not waiting on the upstream merge./settle's JSON body now also carries the outcome in that same already-declared-but-previously-emptyextensionsfield, verified through the real official client, not just a raw fetch: transaction10919a59342fc0cc..., Horizon-verified. A community member (Bartok9) built the upstream fix:#3278, open, not yet merged. Reviewed the real diff line by line before saying so, not on the strength of the PR description: it matches the body-wins/header-fallback precedence proposed in#3270exactly, and ships four regression tests, not the three the PR's own summary implies (the fourth, a malformed-header case, wasn't something we'd asked for, a good defensive addition on its own initiative). Went further than reading the diff: pulled#3278's own branch (bartok9/extension-responses-header-3270,54b136d), built its@x402/coredirectly, and ran a real payment through it against our own production facilitator,https://periplo-testnet.fly.dev, the exact deployment the original bug came from.verify()/settle()both real,settleResult.extensionspopulated, transaction892af0974bee..., Horizon-verified. Honest about scope: this exercised the body-wins branch, the only one reachable against a facilitator that already sendsextensionsin the body on our own side, not the header-only fallback, which stays covered only by Bartok9's own mocked tests. A separate, unrelated version-skew finding from the same run (a client-sidespendControlsguard newer than what we pin) is tracked indocs/DEFERRED.md, not repeated here. Added a second, independent real-world case for the same underlying bug (#3187, the e2e suite's eager EVM/SVM signer derivation): an unrelated Stellar facilitator, stellarsight, hit the identical crash in their own conformance run and needed the same decoy-key workaround, cited on#3228with their commit hash. Full writeup indocs/DEFERRED.md.It is built on the official
@x402/extensions/bazaarpackage, for the same reason the facilitator does not reimplement verify and settle. We keptpackages/bazaar's own stricterrouteTemplatecheck in place of the upstream equivalent;docs/INTEROP.mdexplains where and why. That work also surfaced a real bug in the upstream package itself, affectingmcp://tool/{toolName}URLs, the exact convention the Bazaar extension documents for MCP tools. We found it through the live integration test, not by reading the code, and filed it as x402-foundation/x402#3121. A fix is open against it as x402-foundation/x402#3138, built scheme-agnostic per a reviewer's suggested shape rather than anmcp://-specific patch. That same reviewer, whawk46, later found a real follow-on gap in the fix itself (the opaque-origin branch skipped the query/fragment stripping the function exists to do), we implemented the fix they suggested for it with a new regression test, and they reviewed that too, quoted verbatim: "LGTM as it stands — merge-ready from my side." Nine days later, still unmerged, whawk46 seconded their own review with a merge request, explicit about why the fix is a real defect rather than a cosmetic one, quoted verbatim: "a query string surviving into the canonical URL means the same resource indexes under as many identities as it has session parameters, which is the kind of thing that quietly inflates a catalog and is very hard to attribute afterward." The PR is open, mergeable, commented on three times by the person who reported the original bug, with a separate merge nudge from us the same week. Precision check, 2026-08-23: GitHub's own review API reports zero formal reviews on this PR (reviews: []); whawk46's "LGTM"/"nothing further" language is plain comment text, not a submitted GitHub review, and we have not confirmed whawk46 holds a maintainer or write-access role on this repo. Stated here as what it is: a positive, substantive comment from the person who reported the original bug, not a formal or authoritative approval. Still open, blocked on a maintainer actually merging it.Reviewing the same
@x402/extensions/bazaarpackage a second time turned up another real bug, this one inisValidRouteTemplateitself: its traversal and scheme-injection checks decoderouteTemplateonce, so a double percent-encoded payload survives the first decode still encoded and slips past both checks, verified directly against the function with two working repro payloads. Filed as x402-foundation/x402#3169. Status: no longer just filed and waiting, another contributor's fix is now independently verified with real code, not just read over. ygd58's PR decodes to a fixed point with a bounded pass budget instead of one extra fixed pass. We re-ran both original repro payloads directly against the actual diff, both correctly rejected, then went further than the original report asked for: triple-encoded payloads, and the decode-budget boundary checked from both sides (a payload needing exactly 4 passes still resolves and rejects; one needing 6 exceeds the 5-pass budget and rejects safely rather than hanging or passing through partially decoded). Recorded as a real review comment, not a thumbs-up.A separate one surfaced while working the OpenZeppelin smart-account issue below (
#839):@stellar/stellar-sdk'sAssembledTransaction.needsNonInvokerSigningBy()andsignAuthEntries()only ever look at the top-level node of aSOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES(CAP-71) auth entry, so a delegate signature that's still outstanding is never reported, verified with two real entries built via the SDK's ownbuildWithDelegatesEntry/authorizeEntry. CAP-71 isn't live on any network yet, so this has no impact today, but the bug is real in code already shipped. Filed as stellar/js-stellar-sdk#1655. Status: fixed, not just filed. stellar/js-stellar-sdk#1672, open and mergeable, 6663 tests passing.needsNonInvokerSigningBy()andsignAuthEntries()now walk the full delegate tree instead of the top-level node only. On 2026-08-23 the maintainer, roebee, asked directly on #1655 whether this behavior change (a documented public API now reports every unsigned node, not just the top-level one) should ship as a v17.x bug fix or wait for v18, given CAP-71 isn't live on any network yet. We replied recommending v17.x. Still open, not merged, the version question unresolved. Running the new tests surfaced a second, genuinely separate bug along the way, not the one we set out to fix:authorizeEntry()'s bare-signature fallback path inferspublicKeyfrom the entry's top-level address unconditionally, ignoringforAddress, so a signature correctly targeted at a delegate was verified against the wrong address and failed. Worked around narrowly, insidesignAuthEntries()'s own callback, without touchingauthorizeEntry()itself, since that's out of scope for this PR, and never given its own issue at the time. Opened now, on its own, closing that gap: stellar/js-stellar-sdk#1683, with the exact code path quoted,applyExpirationAndSignature's own correct fallback rule cited as the pattern the verification step should mirror, and a proposed fix. On 2026-08-23 roebee posted three implementation questions to pin the fix down: how to derive the public key when the signer callback returns a bareUint8Arrayinstead offorAddress, what the resulting error text should say, and whether the verify step should followapplyExpirationAndSignature's own fallback rule. We replied the same day: throw rather than silently derive fromforAddress, proposed error wording, and yes to matching the existing fallback rule, with this fix landing in the same PR as #1681's below since both converge on the same{ signature, publicKey }return shape. Status: filed, our proposed direction posted in reply to the maintainer's questions, not yet confirmed by roebee, not fixed, open.That same
authorizeEntry()bare-signature fallback path turned up a second, separate way to trip it: attempting a genuine classic Stellar multisig payment (a second Ed25519 key registered on the buyer's account, weight 1, signing instead of the master key, not simulated, verified on a real testnet transaction that registers the signer) foundAssembledTransaction.signAuthEntries()can't represent a non-master-key signer at all, for two separate reasons. First,signAuthEntries()'s ownaddressdefault callssignerAddress()onsignAuthEntryitself, a plain function per that option's own documented type (SignAuthEntryLike = SignAuthEntry | Signer | Keypair);signerAddress()'s guard requires an object, so it can never resolve for the simplest, most common, fully-documented usage, and silently falls back to an unrelated address instead. Second, even when a caller suppliesaddressexplicitly and sidesteps that (as@x402/stellar's own client does), the per-entry signing closure discards anysignerAddressa custom signer returns before handing a bare signature toauthorizeEntry(), landing on the exact fallback path above and verifying against the wrong key again, this time for a plainAddresscredential rather than a CAP-71 delegate. Filed as stellar/js-stellar-sdk#1681, with a proposed fix for both, checked against #1610 first to confirm it wasn't a duplicate. On 2026-08-23 roebee asked two questions to pin the fix: whether the missing-addressdefault should throw or keep the current silent fallback tothis.options.publicKey, and whether the signing closure should return{ signature, publicKey }(and if so, whether that lands together with #1683 or as separate PRs). We replied the same day: throw rather than fail silently, return{ signature, publicKey }, and land both fixes in one PR since they converge on the same return shape. Status: filed, our proposed direction posted in reply to the maintainer's questions, not yet confirmed by roebee, not fixed, open.On 2026-08-24, GitHub Copilot's automated review on #1672 itself surfaced two more real, independently-verified problems, not just style nits. First, the regression-test comment for the #1655 case 1/2 tests claimed both were "built the same way (buildWithDelegatesEntry / authorizeEntry, not hand-constructed XDR)". Checked against the actual test code, that's false: both hand-construct
SorobanAddressCredentialsWithDelegatesXDR directly, with placeholderscvBytes/scvVoidsignatures, same pattern as the file's ownaddrCreds/authEntryhelpers. Fixed the comment to describe what the test actually does. Second, and more substantive:signAuthEntries()'s own default callback returned{ signature, publicKey: target }unconditionally, wheretargetis only who the SDK asked to sign, discarding asignerAddressthe signing callback may report when the real signer differs, such as a delegate whose real key differs from the delegate node's own address. Reverting the fix reproduces a realsignature doesn't match payloadfailure fromauthorizeEntry; a new regression test signs with a different keypair than the one requested and confirms the default path still verifies. Both fixed and pushed to the same branch:pnpm exec vitest run test/unit, 130 files, 6664 tests passing.Both #1672 and #3215 are the first pair of contributions from this project to go through the checklist in
.claude/skills/claude-antigravity-setup/git.mdin full: humanized, no filler, a visible AI co-authorship trailer on every commit, and root cause confirmed with real, run code before proposing the fix, not just reasoned about.Investigating the discovery mechanism behind the
stellar-buildskill pack this project's own tooling uses (seedocs/SKILLS.md) turned up a bug in a different, adjacent repository:stellar/stellar-dev-skill, the site behind the pack's public skill index. 27 of 28 community-skill entries in itsllms.txt(an agent-fetchable index, per the llmstxt.org convention) linked to GitHub's rendered HTML page instead of the raw markdown, verified live against the deployedskills.stellar.org/llms.txt, not just the source. Root-caused to the repo's own contribution guide: its own example for adding a new entry used the wrong URL shape directly beneath the prose describing the correct one, which likely explains why 27 of 28 contributors made the same mistake independently. Fixed with a PR rather than just an issue: all 27 URLs, the contribution guide's example, and a new CI check preventing the mistake from recurring. Filed as stellar/stellar-dev-skill#103, merged 2026-08-28 by @kaankacar.On 2026-08-24, Copilot's review on #103 flagged that the new CI gate's own regex,
BLOB_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/blob\//, only matched the canonicalhttps://github.com/...form, missinghttp://github.com/...andwww.github.com/..., both of which serve the same HTML blob page the check exists to catch. Verified against the actual regex, confirmed real. Fixed by parsing withURLand comparing hostname (case-insensitive,www.stripped) and pathname instead of a fixed-scheme regex. Addedscripts/check-ecosystem-links.test.mjs(Node's built-in test runner, no new dependency) covering the canonical form and the three variants that slipped through before, wired into both CI workflows as a newpnpm test:ecosystem-linksstep.node --test: 7 passed; the real gate still passes against the live 28-entryECOSYSTEM_CARDSlist. Both fixed and pushed to the same branch.On 2026-08-27, Kaan Kaçar's own automated triage bot reviewed #103 directly and confirmed the original fix real (
generate-llms-txt.mjs:156writescopyValueverbatim intollms.txt, the deployed file held 28 blob links and 0 raw ones, all 27 rewritten URLs returned200 text/plain), then raised three further findings, each verified independently before fixing rather than accepted on the bot's word alone. First,mainhad gained a new ecosystem card (PMLL) since the branch was cut, still pointing at ablobURL; it merged cleanly (no conflict) but left oneblobcopyValuein the tree, which would have failedcheck:ecosystem-linkson the first deploy after merge. Fixed by rebasing ontoorigin/mainand rewriting that entry, its raw URL confirmed200before writing it. Second, the wrong-URL-shape example from the original fix was only corrected insite/README.md;site/src/app/page.tsx'sADD_SKILL_SNIPPET(the copy actually rendered live in the site's "Add your skill" block) andsite/CLAUDE.md's own contribution guide still carried it, both fixed to match. Third, rewriting everycopyValuetoraw.githubusercontent.comhad silently changed each community card's "View source" icon to open plain text instead of GitHub's rendered page, sincepage.tsxpassedsourceUrl={c.copyValue}, the same value for both. Decided on purpose rather than left as a side effect: a newecosystemSourceUrlhelper (site/src/lib/ecosystem-source-url.mjs) reconstructs thegithub.com/.../blob/...URL from the raw one instead of storing a second field that could drift, falling back to the original URL unchanged for a card not hosted on GitHub at all (stellarlight.xyz), verified against the real build output for both cases plus the newly-fixed PMLL entry. A smaller fix rode along:import.meta.urlcompared against a hand-builtfile://string doesn't survive a checkout path needing URL-encoding, silently skipping the whole check;import.meta.filenamecompares raw paths directly instead. Left open for @kaankacar, not decided unilaterally: whethercheck:ecosystem-linksshould run in the PR lane at all, sincepreview-pr.ymlskips fork PRs today. Fixed and pushed as8eb4c4d:pnpm lint,lint:ts, andbuildclean, the real gate reporting 29 entries and no blob URLs,node --test12/12 across both ecosystem-links test files.On 2026-08-28, the same bot's third triage round on
8eb4c4draised three more real, independently confirmed findings before this could go to @kaankacar for a human merge decision. First,check:ecosystem-links/test:ecosystem-linkshad never actually run in CI for this PR at all:site-ci-fork.yml, the only pre-merge lane a fork PR runs (preview-pr.ymlskips forks,deploy-pages.ymlonly runs post-merge), landed onmainafter this branch was cut and only ran install/lint/lint:ts/build. Fixed by adding both steps tosite-ci-fork.ymland correcting the workflow's own comment describing what it runs. Second,site/CLAUDE.mdstill said the site had "no test runner", now false sincenode:testwas already in use with zero new dependencies; fixed the stale claim (cross-referencing the file's own "Don't add" policy rather than rewriting it) and added the two new scripts to bothCLAUDE.md's andREADME.md's script lists, neither of which named them. Third,SkillCard.tsx's comment onshowOpenLink = copyValue !== sourceUrlstill described the pre-fix behavior (copyValue/sourceUrlalways equal for ecosystem cards, chip never rendering); withsourceUrlnow derived separately, all 28 GitHub-hosted cards' values differ, so the chip renders for them too, a second, direct link to the raw file alongside the header's link to the rendered page, confirmed on purpose in the real build output (PMLL's chip opens its rawSKILL.md,stellarlight.xyz's card still shows none) and the comment rewritten to describe the new behavior. Fixed and pushed as40e6ce6, all four checks (lint,lint:ts,check:ecosystem-links,test:ecosystem-linksat 12/12,build) clean. Kaan's bot then confirmed the fix live rather than on description alone:site-ci-fork.ymlprinted "29 entries checked, no blob-URL copyValue found" at40e6ce6, all 29copyValueURLs still resolved (28 raw at200 text/plain, the one non-GitHub entry at200 text/markdown), and the check now runs in all three CI lanes,site-ci-fork.ymlandpreview-pr.ymlpre-merge,deploy-pages.yml's copy left as a post-merge backstop rather than the only enforcement point. This PR touches.github/workflows/, so the bot didn't merge it itself; @kaankacar did, the same day, at254aff4f.The
uptospec thread itself went through the same evidence discipline as everything else here: an honest comparison against a competing design rather than defending our own, real fee numbers and independently-verified transaction XDR rather than trusting a PR description, and self-checks against external review that turned up four real gaps in our own code. All four now closed, each with its own commit and tests:GET /discovery/resourcesandGET /discovery/searchexist, the catalog's dedupe key isextra.uptoProfile-aware, and/supportednow reportsuptofor real viaUptoStellarScheme, a real scheme implementation registered againstx402Facilitator, with a real settled transaction through this facilitator's ownverify()/settle()recorded inconformance/RESULTS.md, not yet reflected on the live deployment (docs/DEFERRED.md). See "The ecosystem is converging on this spec" above for what two other teams in this same RFP round did with this thread, anddocs/UPTO-CONVERGENCE.mdfor the full chronological writeup.The facilitator reports the outcome through the
EXTENSION-RESPONSESheader:{"bazaar":{"status":"success"}}, or{"status":"rejected","rejectedReason":"routeTemplate failed validation"}. We verified this end to end against the real Supabase project: a catalog row appears for a valid HTTP or MCP listing, and a crafted hostilerouteTemplateproduces no row and a specific rejection reason.docs/SELLERS.mdis the seller-facing how-to, including per-parameter descriptions, which search ranking now reads.Real external QA (2026-08-19) found two dead rows already live in the catalog: an opaque-origin bug's
null/...URL, and an unrelated unreachablelocalhostURL from local testing, both cataloged before a write-time check ever existed. Every search query returned one of the two regardless of relevance, making ranking quality unjudgeable from outside. Fixed with a write-time gate,checkCatalogUrl, enforced insideupsertCatalogResourceitself rather than only at the one call site that produced the opaque-origin bug, so it covers both bug classes and any future one that writes a URL the same way. A real one-time backfill against the live Supabase project followed: two bad rows before, one correct row after, confirmed by re-querying the table, not from the migration's own reported success.We then cataloged one real, externally reachable resource (https://periplo-testnet.fly.dev/demo/temperature-convert) for the first time:
apps/facilitator/src/demo-resource.ts, a genuinely payment-gated temperature-conversion endpoint (self-facilitation, built on@x402/hono,@x402/core, and@x402/stellar, the same "do not reimplement the wire protocol" discipline as the rest of this project), deployed to the live facilitator. Real settlement:dde62ac5e67730a0751052a2dafc67dffc595df20bacbae9aaa1c758081deaea, Horizon-verified, and the resource is confirmed discoverable throughGET /discovery/search?query=temperature+conversionagainst the live deployment, not just asserted. Two more real bugs turned up deploying it:@hono/node-serverderives a request's scheme purely fromsocket.encrypted, with noX-Forwarded-Protoawareness, which is always false behind Fly's TLS-terminating proxy, fixed with an explicitresourceURL on the route config rather than patching proxy internals; and@x402/stellar's inherited 50,000-stroop fee ceiling was too low for real testnet Soroban fees that day (about 72,000 stroops, confirmed against Horizon's own fee stats), fixed with a configurable ceiling on the deployed facilitator. Full writeup inCLAUDE.md's Architecture section. -
packages/searchis hybrid retrieval: Postgrestsvector/GIN for lexical matching, pgvector/HNSW for semantic matching, fused with Reciprocal Rank Fusion. Embeddings come fromfastembed'sBGESmallENV15model, running locally with no API key and no per-call cost. Every payment that catalogs a resource embeds it automatically, in the same write path Phase 4 already uses.eval/is the honest measurement the spec asks for: 55 fixed resources, including deliberate near-duplicate clusters (geocodevs.reverse-geocode,weathervs.weather-forecastvs.air-quality, and more), and 300 graded queries, run withpnpm evalagainst the real Supabase project. Current numbers: nDCG@10 0.9346, MRR 0.9226, checked intoeval/baseline.json, with CI failing the build if nDCG@10 regresses more than 5%. An earlier, smaller set (20 resources, 40 queries, all in unrelated domains) scored 0.99, which turned out to be an overfitting signal rather than evidence of good ranking; the harder set above replaced it. The eval set is planned to grow further, toward 500 graded queries, and the search endpoint has not yet been hardened for production load. -
contracts/upto-settlementisUptoSettlement, the Soroban contract behindupto'scontractprofile:require_auth_for_argsrestricted to(authorization,)keeps the settled amount outside what the buyer signs, an atomic pull-pay-refund moves funds with no custody window, and a nonce in temporary storage enforces single use. 35 unit and property tests, plus acargo-fuzztarget that ran 47,630 executions against the ceiling/time-bound arithmetic with zero crashes. Deployed tostellar:testnet(CAK3R734WLT4JU2XMQOJ6NIB3BWGPI442CH44EFJG5AORMXFE7G4MQFW); a real partial settlement (buyer signs a ceiling, facilitator settles less) is recorded inconformance/RESULTS.md, independently checked against Horizon, closing all three on-chain assumptions the spec PR marks open.Two Phase 6b extensions, additional evidence, not an SCF tranche deliverable:
- Zero-settlement: complete. Real testnet transaction
2138c0418a85e1bb29c2eab6cea6c76b3b0231d894450a35905053f36403d358, verified against Horizon. The full ceiling is refunded when real usage is zero, and a second attempt with the same authorization was rejected on-chain (AuthorizationConsumed): the nonce is consumed even when the charge is zero. - OpenZeppelin integration: in progress, not complete.
budget.rs(budget reconciliation tied toactual_amount, not the signed ceiling) andcontracts/agent-smart-account(a realContextRule::CallContract, not simulated) are built, tested, and deployed tostellar:testnet. There is still no real, signed transaction where the smart account isauthorization.from; after exhausting independent isolation (version alignment, target-contract complexity, both ruled out), filed a detailed diagnostic issue againstOpenZeppelin/stellar-contracts, #839. Still open, still blocked. Seedocs/DEFERRED.md's Phase 6b section for what was tried and the path to closing it.
- Zero-settlement: complete. Real testnet transaction
The target is an x402 facilitator for Stellar: verify, settle, and
supported, for both stellar:testnet and stellar:pubnet, built on
@x402/stellar. It pairs with a Bazaar: an automatically-populated
catalog of x402-payable HTTP and MCP services, so an agent can find and
pay for a service without a human wiring up an integration first.
It also carries upto, a metered payment scheme for Stellar that a
plain SEP-41 allowance cannot express: it fails recipient binding
(transfer_from lets the spender choose any destination) and single-use
(an allowance is a standing balance). The network spec is open upstream at
x402-foundation/x402#3098
(issue #3097), marked
ready for review. It documents two conformant profiles: contract, this
project's design, described below, and stateless, an alternative
contributed by Iam0TI via
0d1026/Rialto and
x402-foundation/x402#3134,
credited and merged into the same spec rather than left as a second,
competing PR. The Soroban contract, contracts/upto-settlement, is
built, tested, and deployed to stellar:testnet
(CAK3R734WLT4JU2XMQOJ6NIB3BWGPI442CH44EFJG5AORMXFE7G4MQFW), with a real
settled transaction recorded in
conformance/RESULTS.md closing all three
on-chain assumptions the spec PR marks open.
This is a response to the Stellar Community Fund RFP, "X402 Facilitator
with Bazaar (discovery) support" (SCF #45, Q3 2026). See
docs/SPEC.md for full scope: the build specification
this repository is built against.
The Facilitator, the automatic-cataloging edge into Bazaar,
Search, and the UptoSettlement contract are all real and
deployed. The Hub and the MCP discovery server are still planned, and
the facilitator does not call UptoSettlement yet from its own HTTP
routes. Full diagram and a component-by-component walkthrough, moved to
its own file rather than duplicated here, in
docs/ARCHITECTURE.md.
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm licence-checkExact commands, local environment quirks (the Node version switch, real
integration-suite credentials, redeploying, checking CI's actual run
status rather than trusting the local gate) are in
docs/TOOLING.md.
Baseline transcripts backing the conformance claims above:
conformance/baseline/x402-org/supported.md,
conformance/baseline/x402-org/discovery-404.md,
conformance/baseline/x402-org/verify-settle-malformed.md.
Settled transaction evidence: conformance/RESULTS.md.
CI (.github/workflows/ci.yml, badge above) runs the same gate on every
push. We confirmed it green with an
organic push-triggered run
rather than a manual rerun. It was silently broken from Phase 1 to Phase
3 for two independently verified causes. Full timeline and raw evidence
are in docs/DEFERRED.md.
apps/facilitator is live on Fly.io, stellar:testnet only, at
https://periplo-testnet.fly.dev. It runs on 1 machine
(shared-cpu-1x, 512MB, region iad), kept running continuously, not
scaled to zero when idle: fly.facilitator.toml sets
min_machines_running = 1 deliberately, so the one machine never stops,
because a cold start would break the interactive verify/settle latency
spec §8 asks for. No periplo-mainnet app exists yet: there is no
mainnet fee-sponsor key to back one.
fly deploy --config fly.facilitator.toml --dockerfile Dockerfile.facilitator -a periplo-testnetRun this from the repo root. The Docker build context needs the pnpm
workspace root, even though the image only ships apps/facilitator.
Secrets (STELLAR_FEE_SPONSOR_SECRET, STELLAR_NETWORK) are set with
fly secrets set -a periplo-testnet. They are never committed or
placed in fly.facilitator.toml.
Apache-2.0. See LICENSE. No AGPL or other copyleft
dependency is permitted anywhere in the dependency path. This is
enforced in CI by packages/licence-check.
Pinned versions and their live-registry verification dates are tracked
in the build spec's manifest (docs/SPEC.md §2)
and re-checked incrementally per phase. First verified 2026-08-07;
re-verified 2026-08-19, the full pass spec §11 requires before
submission, done 8 days after the actual 2026-08-11 submission rather
than before it. docs/SPEC.md §2 has the full table and the reasoning
behind the two versions deliberately not bumped to the latest available
(@x402/core's family, held back one release for being under 24 hours
old at check time; soroban-sdk, held at the version the already-deployed
UptoSettlement contract was actually built against). See
docs/DEFERRED.md for why the first pass ran late.
docs/SPEC.md: the full build specification, phased 0 to 10.CLAUDE.md: repo guide for Claude Code sessions (commands, architecture, working rules).docs/TOOLING.md: exact commands and machine-specific setup, split out fromCLAUDE.mdso architecture and operational how-to don't compete for space in one file.docs/SKILLS.md: whichstellar-buildskills are actually available in the build environment, mapped to spec phases.docs/DEFERRED.md: everything deliberately not built yet, and every environment divergence from the spec's assumptions.docs/MEMORY.md: running log of why things were built the way they were.docs/ECOSYSTEM.md: a partial, dated snapshot of the competitive landscape. Regenerate it before relying on it.docs/SELLERS.md: how a resource server lists a Stellar service on the Bazaar (Phase 4).docs/SELF-FACILITATION.md: running the facilitator inside your own resource server, no external operator at all (deployment path 3, spec §5).docs/INTEROP.md: where Periplo's bazaar extension handling diverges from the canonical@x402/extensions/bazaarimplementation, and why (Phase 4).docs/ARCHITECTURE.md: the system diagram and a plain-English explanation of the stack.docs/DECENTRALIZATION.md: why the catalog is off-chain by design, and what "decentralized" actually means here (replicability, not on-chain storage).docs/INFRASTRUCTURE.md: what runs where, who pays for it today, and the honest state of who pays after the grant (not yet decided).docs/MAINTENANCE.md: how conformance is kept current as the upstream wire spec evolves, traced against this project's real history, not promised in the abstract.docs/PRIVACY.md: what Periplo collects (nothing personal, checked directly against the running code) and why.docs/UPTO-CONVERGENCE.md: theuptospec's chronological devlog, including two competitors converging on it instead of forking their own.docs/THREAT-MODEL.md: the spec §6 threat/ control/test table, formalized with a pointer to where each control actually lives and what proves it, citable directly without reading the full build spec first.docs/FOR-REVIEWERS.md: a one-page, human-written index for a panel reviewer, not a Claude Code session: what to look at, in what order, and what each link confirms, in under ten minutes.