The discovery layer for x402-payable services on Stellar.
Try it now, no setup:
periplo-testnet.fly.dev/demo/play —
one click, no wallet, a real Stellar payment settles in ~15 seconds and
you get a real transaction hash. The facilitator itself runs on
stellar:testnet at periplo-testnet.fly.dev;
try GET /,
GET /supported, or
GET /status directly.
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.
The developer hub itself (apps/hub) is still Phase 9, not started.
/browse, /playground, and the rest of §10's human-facing routes
don't exist yet. One real, minimal, live exception: GET /demo/play, a
one-click, wallet-less demo (a visitor's browser generates a one-time
Stellar key, funds it, and pays a real endpoint, no prior setup) — see
"Pay a real request, no wallet needed" below. Everything else is still
the facilitator's JSON API.
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.
2026-09-01: past "two competitors build on our spec" to "we found a gap
neither of them saw, because we're the only one who actually built the
hard part." Both #3098 and #3134 claim Stellar C-accounts are
supported "transparently" by the one Soroban mechanism (
require_auth_for_args) that makes upto expressible at all. That claim
is verified for a C-account whose own __check_auth signs directly. It
is not verified for a delegated or session-key smart-account signer, the
pattern an autonomous agent actually needs: a scoped key, gated by the
account's own spending policy. This project already tried to build
exactly that, in Phase 6b, and hit a real wall no one else in either
thread had hit or documented: __check_auth traps unreachable on every
construction tried, seven ruled-out hypotheses, filed as
OpenZeppelin/stellar-contracts#839.
Root-caused and closed 2026-09-02 (an SDK auth-discovery gap, not a
contract bug, via #863):
a real Signer::Delegated smart-account authorization has since settled
on testnet for a single-context call
(428021a6…),
while the full two-context upto settle path through the delegated
account is still open, now failing with a typed UnvalidatedContext
rather than the original opaque trap. Rewrote #3098 to state the
caveat plainly rather than let a
reviewer implement against an unqualified claim, and to actually
implement the maintainer-agreed consolidation structure (stateless as
the base profile, contract as the secondary profile carrying required
test vectors) that had otherwise sat agreed-but-undrafted for five days.
Commit signed and verified, check-verified-commits passing, full
writeup with the exact evidence in docs/UPTO-CONVERGENCE.md.
Same day, a second finding, this time about settlement rather than
authentication. batch-settlement has no Stellar network binding
upstream at all yet, EVM and SVM only. Its whole design needs voucher
signatures with no meaningful expiry, deliberately provided by both
existing bindings. require_auth_for_args, the exact mechanism that
makes upto work, cannot provide that: the Soroban host hard-rejects
any signed authorization whose expiration exceeds 180 days out,
confirmed directly against real testnet
(state_archival.max_entry_ttl), not assumed. upto never hit this
limit since its own authorizations only need to survive minutes, not
the months or years a long-lived payment channel implies. A real
alternative exists and is precedented, not invented: raw Ed25519
signature verification, already Stellar's own reference pattern in
rs-soroban-env's own example contract, matching what Solana's binding
already does for the identical reason. Filed as
x402-foundation/x402#3341
with a sketch of the direction, not a spec commitment. Two structurally
different findings in one day, both requiring the same thing: having
actually built require_auth_for_args-based settlement on Soroban, not
read about it.
2026-09-08: a third party turned the convergence into a document, and
credited a section to work done here. davedumto opened
x402-foundation/x402#3428,
a convergence document that names #3098 (this project's contract
profile) and #3134 as the two existing specs it builds on, writes
their agreement as MUST requirements, and names five divergences
without picking a winner. Its section 7.5, profile disambiguation, is
credited in the PR body as "raised by @HeylmStoned and developed by
@Eras256 on 2026-08-26," the extra.uptoProfile discriminator point
from the #3134 thread. Not a mention: a named section of the emerging
standard attributed to work done here. Open, waiting on the TSC to say
where it should land, no action needed from this side. 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.Two more pieces, both real and evidenced, not just described:
createFacilitatorCore'schannelAccountSecretsoption pools extra signer accounts per network on top of the primary fee-sponsor — the library's ownExactStellarScheme/UptoStellarSchemealready round-robin across whatever signers they're given, using each one's own account (and so its own sequence number) as the transaction source, this just configures more than one. 4 concurrentsettle()calls against a 4-account pool all succeeded, landed in the same ledger, using 4 distinct source accounts, real evidence inconformance/RESULTS.md, addressing spec §2/§7's own "the facilitator's sequence number is the bottleneck under bursty traffic" requirement. Not a novel idea, and not presented as one: a channel-account pool is a documented mitigation for this exact bottleneck, and several other teams in this same RFP round ship or design one too. What's here is real and evidenced, not just planned.GET /statusreports operational telemetry (uptime, latency p50/p95, error rate, catalog size, last settled transaction per network), self-hosted and aggregate-only, spec §8/§9. Both live onhttps://periplo-testnet.fly.devas of 2026-09-03: tryGET /statusorGET /supporteddirectly (the latter now lists 4 signer addresses, the channel pool). A real bug found and fixed the same day deploying this:/statuskept reporting an emptylastSettledTransactionafter a real settlement, because the telemetry call only lived in the HTTP/settleroute, which self-facilitation (the demo resource below) never calls. Fixed, redeployed, re-verified live: seeconformance/RESULTS.md. Several other teams in this round document plans for a similar operational telemetry surface; this one is real, live, and returning real numbers today, not a roadmap item.
Not a unique idea, said plainly: at least one other real submission
in this same RFP round already ships a live, browser-based, no-wallet
demo, more developed than this one — multiple payment scenarios
including a metered upto ceiling and a deliberate failure case, and
real testnet USDC rather than this project's own self-issued test
asset. What follows is real and verified, not a claim to being first or
only.
GET /demo/play is a one-click demo of the whole x402 loop, built on
top of the existing /demo/temperature-convert resource: a visitor's
browser generates a one-time Stellar keypair, funds it with testnet XLM
via friendbot, gets a small test-token balance via this facilitator's
own /demo/play/faucet endpoint (a transaction the facilitator
partially signs server-side and the browser completes and submits
itself — the ephemeral secret never leaves the tab), then uses that key
to sign and settle a real payment. No Freighter, no pre-funded account,
no x402 knowledge required. Verified twice, for real, end to end
(apps/facilitator/scripts/demo-play-full-verify.ts, the same logic the
page's own browser bundle runs): a real 402 challenge, a real signed
payment, a real settled transaction confirmed on Horizon, payer in the
settlement result matching the one-time key that authorized it. ~15
seconds end to end, measured, not the sub-10-second target a first
framing of this feature assumed — Stellar's own ~5-second ledger close
time, multiplied across three sequential real transactions (friendbot
funding, the trustline+token onboarding transaction, the actual demo
payment), makes single digits physically unreachable on testnet, not a
code inefficiency to optimize away.
A real user found a real bug this session's own Node scripts couldn't
have caught, by actually clicking the button. resourceUrl's default
had no value/from/to query string; the 402 challenge succeeds
without one (the price doesn't depend on it), so an unparameterized
click paid in full and then hit a 400 on the actual conversion —
charged for nothing. demo-play-full-verify.ts never exercised this
because it hardcoded its own query string directly, bypassing the real
default entirely. Fixed (query string baked into resourceUrl itself,
the one place every caller shares), redeployed.
The genuine browser click-through this feature's own first draft had
flagged as unverified is now closed for real, not just re-asserted.
This build session's own sandbox lacks the system libraries a real
headless Chromium needs (libnspr4, libnss3, others) with no root to
apt install them — worked around without root by apt-get download-ing each .deb individually and extracting with
dpkg-deb -x into a local directory, then pointing LD_LIBRARY_PATH
at it.
demo-play-browser-verify.mjs
then ran for real: navigated to the live /demo/play, clicked the
actual "Pay and convert" button, waited on the real DOM, got a real
result — transaction
5d020d7e...,
Horizon-confirmed, GET /status on the live deployment shows the same
hash.
Live now: try it yourself at
periplo-testnet.fly.dev/demo/play.
Deployed 2026-09-03. Full history — three real transactions from this
round alone, the query-param bug, the telemetry bug, and the real
browser verification — in conformance/RESULTS.md.
Wire-format compliance is usually shown by comparing what a facilitator
returns against a reference response, or by pointing an unmodified
canonical client at your own deployment. Both are real evidence, and
other teams in this round do exactly that. This is a different, more
concrete claim: a payment built and signed with this project's own
client code was verify()-ed and settled through a completely
independent, third-party facilitator this project doesn't operate,
confirmed live beforehand and independently re-confirmed on Horizon
afterward, with the settling transaction's source account belonging to
that third party, not this one. See
apps/facilitator/scripts/interop-x402-org-demo.ts
and the evidence row in conformance/RESULTS.md.
Worth naming a different, real kind of interoperability this project does not have: at least one other team in this round has built and deployed actual catalog federation between cooperating facilitator instances — one operator's Bazaar registering and ingesting another's listings over a real HTTP API, live today. A further, more ambitious design for fully decentralized peer discovery is documented in that same project as an explicit, honestly-labeled proposal, not yet implemented. Federation and a single completed cross-facilitator settlement are different claims; this project has only demonstrated the second.
docs/RFP-COMPLIANCE.md quotes every
requirement and evaluation criterion from the SCF Handbook's own RFP
Track page and the "X402 Facilitator with Bazaar" listing, and maps
each one, row by row, to real evidence already in this repo — a link,
a test, or a transaction hash, the same standard as everywhere else in
this README — with every genuinely open item named rather than implied
covered. Reviewing the other real submissions in this round, none
appears to publish a dedicated requirement-by-requirement mapping as
its own document; several cite specific parts of their own response
inline within a longer architecture write-up, a real but different
thing.
Beyond our own settlement scripts, the official x402 e2e conformance
suite (x402-foundation/x402's own e2e/, not a Periplo-authored
equivalent) was run end to end against the live deployment above via its
documented external-proxies mechanism: real typescript/http/axios
client, real typescript/http/express server, real /exact/stellar
payment, verdict ✅ Test passed. A same-day follow-up ran it again with
--extensions=bazaar, forwarding the facilitator's own
/discovery/resources//discovery/search routes 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, merged 2026-08-31 by @phdargen (dd258756...):
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 upstream main before opening the
PR, not just the original fix: the full typescript/ and e2e/
workspaces built clean, and calling the real createE2EClient()/
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 in
docs/conformance/2026-08-17-x402-e2e-stellar-exact.md.
Independently reinforced before it merged: an unrelated Stellar
facilitator, stellarsight, hit the identical #3187 crash in their
own conformance run and needed the same decoy-key workaround, cited
directly on this PR with their commit hash, full writeup in
docs/DEFERRED.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 in
x402Facilitator.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 contradicts derivePattern()'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's check-verified-commits
check 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 confirms verified: true on the amended commit and the
check now passes. Still open, awaiting maintainer review.
2026-09-01: a deliberate, proactive pass over the latest published
@x402/core@2.24.0, @x402/stellar@2.24.0, and
@stellar/stellar-sdk@17.0.1, searched rather than waited for, on the
same "reimplementing a check found a gap the type promised" pattern as
derivePattern() above. ExactStellarScheme's feeBumpSigner is
documented by getSigners() as a facilitator address, but the internal
set every facilitator-safety check actually consulted
(signingAddresses) never included it, filed as
x402-foundation/x402#3332.
Own PR, merged 2026-09-03T07:02:08Z by @phdargen:
x402-foundation/x402#3336
adds a separate facilitatorSafetyAddresses set rather than merging into
signingAddresses directly, since that set also backs signer selection
in settle() and feeBumpSigner has no entry in the signer map, a
distinction that would have been an easy regression to introduce
carelessly. resolveSettlementOverrideAmount() never enforced its own
documented invariant, "the resolved amount must be <= the authorized
maximum in PaymentRequirements," for any of its three input formats
(raw atomic units, percent, dollar), filed as
x402-foundation/x402#3334.
The maintainer (@phdargen) closed #3334 on 2026-09-08 as a duplicate
of the PR below, treating it as the canonical fix for this.
Status: fix written and open, not just filed.
x402-foundation/x402#3338
centralizes one check at the end of the function rather than duplicating
it per branch, verified with a real repro against the installed package
before writing the fix (a "500%" override against a 1,000,000-unit
ceiling resolved to 5,000,000 with no error) and the full @x402/core
suite green before and after (691 to 695 tests). areFeesSponsored: false is accepted with no validation at construction time, then breaks
every settlement through the official client elsewhere with no
indication the facilitator's own config is the cause, filed as
x402-foundation/x402#3333.
The behavior fix we first drafted (throw at construction) turned out to
be wrong: an existing test,
"should use custom areFeesSponsored", deliberately asserts that
constructing with false succeeds. So
x402-foundation/x402#3339
lands as a doc-only PR instead, cross-referencing the limitation on the
constructor's own JSDoc where getExtra() already states it two methods
away.
One finding didn't survive its own fix, and that's the correct
outcome, not a failure.
stellar/js-stellar-sdk#1699
reported that XdrLargeInt.toU128()/.toU256()/.toU64() silently
wrap a negative value instead of throwing RangeError, based on reading
XdrLargeInt's own unit tests in isolation. Writing the actual fix and
running the package's full test suite, not just the touched file, the
same discipline #3172's self-correction above already established,
surfaced test/unit/base/numbers/sc_int.test.ts, a block explicitly
labeled "from scint_test.js" (a shared, cross-SDK test-vector file).
It deliberately constructs a negative ScInt and asserts
.toU128()/.toU256() return exactly the two's-complement
reinterpretation reported as a bug: intentional, cross-SDK-consistent
design, not an oversight. The patch was reverted before ever being
pushed; the issue was corrected with the full evidence and closed by
the same session that filed it. Full writeup, including a stray fork
found 89 commits behind upstream mid-round and fixed before it could
affect anything, in docs/DEFERRED.md.
-
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.Update, 2026-08-30: the same finding rippled across three separate SDK implementations. wnjoon, who triaged
#3270in the thread, independently confirmed the identical gap in the Go client (go/http/facilitator_client.go'sverifyHTTP/settleHTTPdiscard the decoded header the same way the TypeScript client does) and opened#3301themselves, explicitly refing this issue and#3278as its Go parity follow-up, deliberately keeping#3270open rather than closing it against a single-language fix. A Python draft addressing the same gap inpython/x402/http/facilitator_client.py'sHTTPFacilitatorClient/HTTPFacilitatorClientSyncis open asPhilBot402/x402#4(a fork PR, still a draft, not yet opened against the upstream repo itself), also linking back to#3270directly. Three independent SDK-language fixes now trace to the one issue this project opened: TypeScript (#3278, Bartok9), Go (#3301, wnjoon), Python (PhilBot402/x402#4, draft). All three open, none merged as of 2026-08-30, verified live before writing this (see the 2026-09-02 update below for what actually merged).Resolved upstream, 2026-08-31, not by the three PRs above directly.
#3270closed the same day (stateReason: COMPLETED), but the fix that actually closed it was a maintainer's own separate PR,#3306(Python, phdargen), merged 2026-08-31T15:59:00Z, two seconds before the issue closed. Its own description rejects the shape#3278/#3301originally shipped, merging the header data into the existingextensionsfield, calling that "the wrong shape" since it leaks a server-only sidechannel into a field the resource server forwards to buyers viaPAYMENT-RESPONSE. It introduces a separateextension_responsesfield instead, excluded from that buyer-facing encoding.#3278was revised to match before merging, confirmed against its real merged diff: it now populatesextensionResponses, explicitly "Never merges intoextensions," and merged separately, 2026-08-31T17:34:56Z, after the issue had already closed.#3301(Go) and the Python draft remain open, unmerged, presumably needing the same realignment.Resolved in full, 2026-09-02.
#3301(Go) merged2026-09-02T12:57:19Zby phdargen, confirmed against its real merged diff, not assumed from the "presumably" above: it carries the same realignment#3278/#3306did, populatingExtensionResponses(notextensions) with a dedicated regression test,TestEncodePaymentResponseHeaderOmitsExtensionResponses, that fails outright if any extension data leaks intoPAYMENT-RESPONSE. The Python draft,PhilBot402/x402#4, remains open in its author's own fork, never opened against the upstream repo itself. Three SDK-language fixes now trace to this one issue: TypeScript and Go both merged upstream, Python resolved separately by#3306, with a fourth, independent Python attempt left open in a fork that was never proposed upstream. No@x402/corerelease ships this shape yet (npmlatestis still2.24.0, from 2026-08-27), not a precondition either way:/settlenow sendsextensionResponsesalongside the existingextensionsfield (neither replaces the other), the same way it already sentextensionsahead of any client reading it. Verified against the real mergedhttpFacilitatorClient.tsonmainbefore writing this, not assumed: the officialHTTPFacilitatorClientdoesn't actually readextensionResponsesfrom the body at all,settleResponseSchemadoesn't declare it, so it gets populated purely from theEXTENSION-RESPONSESheader this facilitator already sends correctly, no body change needed for that client specifically. The body field is for any other caller reading the JSON directly.Protocol 28 ("Adapter") readiness, checked for real against testnet ahead of the 2026-09-16 mainnet vote, not just a version bump. Commit
32f5d56:@stellar/stellar-sdkupgraded16.2.0→16.3.0, the LTS release backporting full Protocol 28 XDR support (CAP-85's newContractExecutablevariant, CAP-83) onto the stable v16 API, not17.0.1(npmlatest), because@x402/stellar, this project's own dependency, still hard-pins^16.0.1; a v17 bump would have split the install into two incompatible major versions of the same package. The same commit settled three real transactions onstellar:testnetunder the new protocol, each Horizon-verified:exact(e102cf87...),upto'scontractprofile direct against the deployed contract (265d5c4e...), andupto'scontractprofile through this facilitator's own HTTP-route code (383b8319...). The run wasn't cosmetic: the same commit fixes a real bug it found,apps/facilitator/scripts/settle-demo.tshad never readMAX_TRANSACTION_FEE_STROOPSthe way the deployedserve.tsdoes, so it failed on the real, current testnet fee (95,461stroops that day, already above the72,000that first required the override in August). Also left a corroborating comment onOpenZeppelin/stellar-contracts#865, filed independently the same day by a different project on the same GitHub account with a stronger repro (a realerror[E0004]compiling againstsoroban-sdk 28.0.0-rc.1): the sameContractExecutablematch gap, hit at a differentsoroban-sdkpin (26.1.1), a second, independent confirmation of the same root cause. Full detail inCLAUDE.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: closed 2026-09-09, fixed by another contributor adopting the same approachpackages/bazaar'scheckRouteTemplatealready takes. Same pattern as#3121and#3270: we reported it, a third party (ygd58) fixed it. ygd58's PR #3213 decodes to a fixed point with a bounded pass budget instead of one extra fixed pass, the same bounded-repeated decode Periplo's owncheckRouteTemplateruns, and merged to x402mainon 2026-09-09. Before the merge 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. No npm release ships this fix yet (@x402/extensionsis still at2.25.0, tagged before the merge), sopackages/bazaar's own check stays load-bearing until one does.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. Rebased ontov17.0.1on 2026-08-28 once upstream cut that release mid-PR-lifetime, settling the question by circumstance rather than a maintainer decision: the PR can now only ship as a later v17.x patch, not v18. 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; the proposed fix (verify againstforAddresson the naked-signature path inbase/auth.ts, exactly the one line proposed in the issue) landed in code as part of #1672 on 2026-09-01, prompted by a second maintainer's review, Ryang-21, see below — not yet confirmed by roebee, and the issue itself stays open on GitHub until #1672 actually merges, not claimed closed early.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.A second Copilot review, 2026-08-29, after the
v17.0.1rebase, found a real regression we introduced ourselves: resolving theCHANGELOG.mdconflict during the rebase had deleted the entirev17.0.0release section, jumping straight fromv17.0.1tov17.0.0-rc.2. Restored verbatim from upstreammain, verified byte-for-byte identical to the real released section, fixed and pushed the same day. Three further comments from that review are still open, unresolved design questions onassembled_transaction.tsrather than regressions: whetherneedsNonInvokerSigningBy()should exclude a delegate an account's own policy may never require, whether its public documentation should describe contract-address delegates (not just accounts), and whethersignAuthEntries()should preserve an entry's existing expiration once any node is signed rather than overwrite it with a fresh one on a later signer. Status: rebased, mergeable, three real review findings from Copilot pending a fix, not a version question anymore.On 2026-09-01, a second real maintainer, Ryang-21, distinct from roebee's ongoing engagement above, left a formal review on #1672 itself:
CHANGES_REQUESTED, five inline comments, each naming a genuine silent-signature-corruption or dead-end path, not a style nit. In order: reusing a freshexpirationdefault across sequential multi-party signers silently invalidates an earlier signer's signature (Error(Auth, InvalidAction)on the real host, no client-side warning first) — the same defect Copilot's own third open question above had already flagged, now confirmed and fixed by a second, independent reviewer; a missing!signer.signedguard let a repeatsignAuthEntries()call for an already-signed address re-sign and re-bump the same shared expiration through a second door; a contract-address (C...) delegate target reached an unreachablesignatureScValbranch, surfacing only an opaque strkey error; a pre-#1672 customauthorizeEntrywith 4 declared parameters silently dropped the newforAddress(5th) argument, writing a delegate's signature to the entry's top level instead of the delegate node; and the #1683 fix belonged inbase/auth.tsitself, not the narrowerassembled_transaction.tsworkaround this PR had shipped with. All five fixed in one commit,0dd1c624, each with its own new regression test (the expiration-reuse test needed a rewrite mid-session from a passthrough mock, which never actually exercised the check, to real signing viacontract.basicNodeSigner, caught before pushing). Replied inline to each of the five threads plus one summary comment, each referencing the real commit rather than asserting the fix happened. Mergedupstream/mainafterward (81466fd9, a merge commit, not a rebase, so Ryang-21's existing comment anchors didn't move) to pick up #1693/#1698, oneCHANGELOG.mdconflict resolved by hand. Re-verified in full post-merge: 132 test files, 6743 tests passing, bothtsc -pinvocations clean,eslint src/clean, generated docs confirmed current by the repo's own pre-push hook. Signed and verified:gh api repos/Eras256/js-stellar-sdk/commits/81466fd9 --jq '.commit.verification.verified'returnstrue. CI (Tests, CodeQL, Docs build, Code Formatting, Guide snippets, e2e) showsaction_required, confirmed via the Actions API rather than assumed from the check list alone: the standard GitHub gate for an external PR awaiting a maintainer's manual approval to run workflows, not a failure on this branch. Status: all five of Ryang-21's review points fixed with dedicated tests and evidence, merge conflict resolved and pushed, signed, nothing further actionable from this side until a maintainer approves CI and review resumes. Copilot's first two open design questions above (excluding a never-required delegate, documenting contract-address delegates) remain genuinely open, distinct from what Ryang-21's review covered.Resuming the broader search this session had paused for the Ryang-21 review, the same defect class recurred independently, the same day, in a different official SDK:
StellarCN/py-stellar-base's ownauthorize_entry()(stellar_sdk/auth.py) signs each CAP-71-01 delegate correctly in isolation, but sequential signing across multiple delegates silently overwrites the entry's one sharedsignature_expiration_ledgerfield, invalidating an earlier delegate's already-stored signature whenever two calls use different expiration values, with no error raised at either call. The library's own docstrings already state the requirement in prose, twice ("every signer of one entry must use the samevalid_until_ledger_sequence, otherwise earlier signatures are invalidated"), but nothing in code enforces it, andAssembledTransaction.authorize()/sign_auth_entries()never reach this path at all: they only target the top-level address by design, soauthorize_entry(..., for_address=...)is the only documented way to sign a delegates entry with this library, not a fallback. Reproduced for real againsta0e9f8b7(the commit last touchingauth.py): two delegates signed with expirations 1000 and 2000, then the payload the network would actually verify the first delegate's stored signature against (rebuilt from the entry's current state, expiration 2000) compared byte-for-byte against the payload it was actually signed with (expiration 1000) — confirmed different. Checked for duplicates first (none found; the PR that added this support,#1189, has no discussion of this case in its own review). Filed as StellarCN/py-stellar-base#1215 with a proposed fix (raise a clearValueErrorinauthorize_entry()when an already-partially-signed entry's stored expiration disagrees with the one just passed, instead of silently overwriting it), not just the finding. Severity calibrated the same honest way as #1655's own entry above: CAP-71 isn't live on any network yet, so this has no impact on a real transaction today, but the bug is real in code already shipped, in the only path this library offers for it. No PR opened yet, per this repo's ownCONTRIBUTING.md, which asks contributors to check in before starting work on a significant change; the issue includes a full fix sketch so that step is fast once a maintainer responds.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. The diagnostic issue filed againstOpenZeppelin/stellar-contracts, #839, was root-caused and closed 2026-09-02: the trap was an SDK auth-discovery gap, not a contract bug (via #863). A real signed testnet transaction where aSigner::Delegatedsmart account isauthorization.fromnow exists for a single-context call (428021a6…, Horizon-verified). The full two-contextUptoSettlement.settle()+ nestedtransferthrough the smart account is still open, now failing with a typedError(Contract, #3002)(UnvalidatedContext) rather than the opaque trap. Seedocs/DEFERRED.md's Phase 6b section for the exact state and the remaining candidates.
- 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.
2026-09-02: checked against Protocol 28 ("Adapter") ahead of its
2026-09-16 mainnet vote, real testnet (which has run it since
2026-08-27), not just read about. @stellar/stellar-sdk bumped
16.2.0 → 16.3.0, the real LTS release that backports full Protocol
28 XDR support (CAP-85's new ContractExecutable variant, CAP-83)
onto the stable v16 API — not the newer 17.0.1, which @x402/stellar
(this project's own dependency, spec §1: build on it, don't
reimplement) still hard-pins away from via its own ^16.0.1
requirement, checked directly rather than assumed. Reviewed every
place this project's own code (TS and Rust) could match exhaustively
on a contract's executable type: nowhere does, confirmed by search, not
inferred. Then ran the real settlement cycle against live testnet under
the new protocol: exact, and upto's contract profile both direct
against the deployed contract and through this facilitator's own
HTTP-route code, all three settled and Horizon-verified, recorded in
conformance/RESULTS.md. One real bug found and fixed along the way:
the exact demo script had never read the same fee-ceiling override
serve.ts does, so it failed on the real, current testnet fee
(95,461 stroops, already above the 72,000 that first required the
override in August). Full writeup, including a related-but-not-Periplo's-own
finding in OpenZeppelin's stellar-accounts crate, in CLAUDE.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 at 2.22.0 pending evaluation of real,
breaking-shaped changes in 2.23.0, spendControls's new default cap,
renamed exports, a new required createSIWxPayload argument, not
release age; 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.docs/RFP-COMPLIANCE.md: every RFP Track requirement and evaluation criterion, quoted from the SCF Handbook, mapped one row at a time to real evidence in this repo, and every genuinely open item stated plainly.