This is the governing build document for this repository, adapted from the session that kicked off the build (2026-08-06/07) and committed here so it survives across sessions instead of living only in chat context.
CLAUDE.mdpoints here. Phase status and environment divergences are tracked separately indocs/DEFERRED.md. This file is the brief, not the status report; don't hand-edit it to reflect progress, updatedocs/DEFERRED.mdandconformance/RESULTS.mdinstead. Budget figures and submission-process administrivia (tranche schedule, form-specific requirements) live in the actual SCF submission, not here. This file is engineering scope and gates only.
You are the lead engineer building Periplo, the discovery layer for x402-payable services on Stellar. This is a submission to the Stellar Community Fund RFP Track, responding to the open RFP "X402 Facilitator with Bazaar (discovery) support" (SCF #45, Q3 2026).
RFP Track is decided by panel review only, no community vote, and acceptance is tested at the wire level: an unmodified canonical x402 client is pointed at the service and either completes a payment end to end or it doesn't. A conformance claim in prose doesn't substitute for that.
Therefore: conformance at the wire level is the product. Correct settlement plus a non-conformant wire format produces an unusable service.
This machine has stellar-build installed: 45 skills in ~/.claude/skills/,
the Raven MCP server, and a local data layer. Reach for these instead of
recalling Stellar facts from memory.
Raven MCP (stellar-raven, https://raven.stellar.buzz/mcp) is the official
hosted MCP for the Stellar ecosystem, exposing search (Stellar docs + ecosystem
discovery) and execute (queries against live ecosystem data).
Query Raven before asserting any Stellar fact: protocol behaviour, SEP
details, RPC semantics, current network limits. If Raven and this document
disagree, Raven is the live source; note the divergence and update the document.
Skills to invoke by phase:
| Phase | Skill | What it gives you |
|---|---|---|
| all | standards |
SEP/CAP map, ecosystem references, MCP server list |
| 1, 4 | agentic-payments |
x402 on Stellar: facilitator flow, fee-sponsored clients, MPP. The closest thing to a reference for this build. |
| 2, 5 | data |
Stellar data access patterns, indexing |
| 3, 10 | dapp |
JS stellar-sdk, transaction building, simulation, error handling |
| 3 | assets |
SEP-41, the Stellar Asset Contract bridge, trustlines, authorization flags |
| 6 | smart-contracts |
Soroban contract anatomy, storage/TTL, auth, testing, security. Routes to development.md, testing.md, security.md. |
| 6 | tyler-architect |
Architect persona for the UptoSettlement design review |
| 9 | dapp |
Freighter and Stellar Wallets Kit for /playground |
| 10 | deploy-stellar-mainnet |
The devnet → mainnet checklist with Soroban-specific gates |
| every gate | code-review, review-edge-case-hunter |
Run both before declaring a gate passed |
Do not use the SCF skills (scf-submission-drafter, scf-prescreen-checker,
scf-budget-builder, scf-competitor-analyst) during the build. They belong to
the submission workflow, which is a separate task from writing this code.
Note on the environment: a UserPromptSubmit hook rewrites prompts and a
PostToolUse hook traces skill usage locally. Both are expected. Disable with
STELLAR_BUILD_NO_REPROMPT=1 / STELLAR_BUILD_NO_TRACE=1 if they interfere with
a debugging session.
Violating any of these invalidates the work.
-
Licence: Apache-2.0. Every dependency must be compatible with permissive redistribution and with operating the code as a network service. No AGPL anywhere in the dependency path. Specifically excluded: the OpenZeppelin Relayer, its x402 plugin, and the relayer SDK (AGPL-3.0-or-later). Run a licence check in CI and fail the build on any copyleft transitive.
-
Build on
@x402/stellar(Apache-2.0). Do not reimplement verify/settle. Settlement on Stellar is solved. The novel work is discovery, the agent-facing interface, theuptoscheme upstream, and conformance. -
Non-custodial by construction. The facilitator never takes custody and is never the source of funds. It sponsors network fees only. Tampering with a payment must fail signature verification. Assert this at boot: the process must refuse to start if configured with a key that can move user funds.
-
Never use the words "SDK" or "Developer" in the project name, repository title, or the top-level description. Name the capability, not the category. (Package directories may use them; the public identity may not.)
-
No invented scope. Build only what §3 lists. If you believe something else is needed, write it in
docs/DEFERRED.mdand move on. -
Every rejection carries a non-null
reason. An agent must be able to reason about failure without parsing prose. -
Honest README. Never state a capability that lacks a link, a test, or a transaction hash. Prefer "not implemented" to an optimistic claim.
-
Reference repositories are READ-ONLY unless they carry a permissive licence. Most of the Stellar prior art worth studying:
scrimp,stellar-mpp-demo,yardstick-demo,Drand-Relay,stellar-playground,confidential-walletand others listed inCLAUDE.md, publishes no licence, which means all rights reserved. Public visibility is not permission.You may: read them, learn the approach, and write your own implementation from your own understanding. You may not: copy code, files or configuration, or add them as a dependency.
Only these six are ✅ dependable:
OpenZeppelin/stellar-contracts(MIT), andstellar-agent-kit,soroban-diagnose,stellar-dev-skill,fKALE,Klink(Apache-2.0). The CI licence gate exists to catch violations of this rule. Do not work around it.
Versions confirmed against the npm registry and crates.io on 2026-08-07,
re-verified 2026-08-19 (§11's pre-submission pass, done 8 days late, see
docs/DEFERRED.md). Pin these. If a version has moved, update it and note
the change in the commit body. Do not silently drift.
| Package | Version | Licence |
|---|---|---|
pnpm |
11.22.0 | MIT |
typescript |
7.0.2 | Apache-2.0 |
vitest |
4.1.11 | MIT |
@playwright/test |
1.62.1 | Apache-2.0 |
@biomejs/biome |
2.5.9 | MIT OR Apache-2.0 |
zod |
4.4.3 | MIT |
hono |
4.13.3 | MIT |
@types/node |
26.2.0 | MIT |
tsx |
4.23.12 | MIT |
@x402/core · @x402/stellar · @x402/hono |
2.22.0 | Apache-2.0 |
@x402/fetch |
2.23.0 | Apache-2.0 |
@stellar/stellar-sdk |
16.2.0 | Apache-2.0 |
@modelcontextprotocol/sdk |
1.30.0 | MIT |
@supabase/supabase-js |
2.112.3 | MIT |
next |
16.3.1 | MIT |
react |
19.2.8 | MIT |
tailwindcss |
4.3.3 | MIT |
soroban-sdk (crate) |
27.0.5 | Apache-2.0 |
stellar-xdr (crate) |
27.0.0 | Apache-2.0 |
2026-08-19 re-verification notes, real deviations from a blind bump, not silent drift:
@x402/core/@x402/stellar/@x402/honoare pinned to 2.22.0, not the actual latest (2026-08-30 re-check: 2.24.0, published 2026-08-27, three days old and two releases ahead; a realpnpm installagainst it today pulls it cleanly, so the release-age gate that justified the 2026-08-19 wording no longer applies and that reasoning is retired). The real, current reason to stay pinned:typescript/packages/core/CHANGELOG.mdshows 2.23.0 (the release actually carrying new content; 2.24.0 is "bumped to align version with dependent packages," nothing more) made real, breaking-shaped changes this project hasn't evaluated against its own usage yet, not just accumulated age:spendControls, a new default $1 USD cap on non-pegged client assets (the same guard already flagged as a version-skew finding indocs/DEFERRED.md, from probing a client built against a newer@x402/core); renamed exports (DEFAULT_STABLECOINS/USDC_CONFIG/DEFAULT_ASSET_BY_NETWORK→DEFAULT_ASSETS); andcreateSIWxPayloadgaining a new required third argument. This is the real trust-critical dependency the deployed fee-sponsor signs through; bumping past those changes without checking Periplo's own usage against each one is a deliberate upgrade decision, not a version-age wait.soroban-sdkstays at 27.0.5, not the actual latest (27.0.6, a plain patch bump): the deployedUptoSettlementcontract (CAK3R734WLT4JU2XMQOJ6NIB3BWGPI442CH44EFJG5AORMXFE7G4MQFW) was built and verified against 27.0.5 specifically (wasm hash cross-checked live against stellar.expert). The contract has no upgrade mechanism, so bumping the source pin without redeploying would leave source and the live contract mismatched; redeploying a new instance is a bigger, outward-facing decision than a version bump, held for explicit confirmation rather than done here.stellar-xdr's table entry is corrected from28.0.0to 27.0.0: it was never a direct pin, it's resolved transitively by whateversoroban-sdkversion is actually used, confirmed by readingcontracts/upto-settlement/Cargo.lockdirectly rather than assumed. The original28.0.0entry was wrong from the start, not a drift.next/react/tailwindcss/@modelcontextprotocol/sdk/@playwright/test: re-checked live,nextmoved (16.3.0 → 16.3.1), the rest didn't. None are installed anywhere in the codebase yet (Phase 7/9 haven't started), so there is nothing to test the bump against; the table reflects the current registry latest for whenever they are introduced.
Node ≥ 22. pnpm workspaces. TypeScript strict, noUncheckedIndexedAccess,
exactOptionalPropertyTypes. Biome for lint/format (not ESLint + Prettier).
ExactStellarScheme: client, facilitator and server variantscreateEd25519Signer(privateKey, defaultNetwork): SEP-43 signer implementingSignAuthEntryandSignTransactionClientStellarSigner,FacilitatorStellarSigner: typesgetRpcUrl(network, config?),getRpcClient(network, config?),getNetworkPassphrase(network)validateStellarDestinationAddress(address),validateStellarAssetAddress(address)
Its src/ contains exact/ only. There is no upto/. That is Phase 6.
- CAIP-2 network ids:
stellar:testnet,stellar:pubnet. Both are committed deliverables, never one or the other. - Ledger-based expiration, not timestamps. Default ≈ 12 ledgers ≈ 60 s,
derived from
maxTimeoutSeconds. Use the live network estimate for seconds per ledger where available; fall back to 5. - SEP-41 Soroban tokens only. Classic assets are out of scope.
- USDC on Stellar has 7 decimals, not 6. Amounts are
i128strings. - An account needs a trustline before it can receive a SEP-41 asset. Handle this in onboarding and examples; surface it as a distinct, actionable error.
- Facilitators sponsor fees: advertise
extra.areFeesSponsored: true. - Settlement fee =
simulationResourceFee + inclusionBuffer(buffer ≥ 100 stroops), derived from a fresh settle-time simulation. Never reuse the client's fee bid. - Soroban's simulator records
require_auth()without verifying signatures. Simulation is not authorization verification. Verify signatures explicitly. - Throughput: the facilitator is the transaction source, so its sequence number is the bottleneck under bursty agent traffic. Use channel accounts.
| # | Component | Path |
|---|---|---|
| 1 | Facilitator: verify, settle, supported, both networks |
apps/facilitator |
| 2 | Bazaar: catalog, automatic cataloging, trust boundary | packages/bazaar |
| 3 | Search: hybrid retrieval + measured relevance | packages/search |
| 4 | MCP discovery server | packages/mcp |
| 5 | Seller / buyer helper libraries | packages/helpers |
| 6 | upto on Stellar: spec + Soroban contract + upstream impl |
contracts/, spec/ |
| 7 | Developer hub (role-based docs + live testnet examples) | apps/hub |
| 8 | Conformance harness against the x402 e2e suite | conformance/ |
| 9 | Two end-to-end example integrations | examples/ |
Do not build: an institutional dashboard, an admin console, user accounts,
billing UI, a wallet, a block explorer, analytics beyond operational telemetry,
or any page not required by the developer hub in §10. Scope sprawl is a
documented rejection reason in this programme. If tempted, write it in
docs/DEFERRED.md.
Not all components carry equal weight. Use this to judge how much depth each phase deserves. If a phase is running past its allocation, cut scope inside it rather than borrowing from another. The RFP requires the Bazaar (catalog + search) to carry the largest share of the total effort, and the allocation below reflects that.
| Line | Share | Phases |
|---|---|---|
| Search + relevance evaluation | 25% | 2, 5 |
| Catalog + automatic cataloging + trust boundary | 19% | 1, 4 |
upto: spec + Soroban contract + upstream |
16% | 6 |
Facilitator on @x402/stellar, both networks |
13% | 3 |
| MCP discovery server | 9% | 7 |
| Developer hub | 8% | 9 |
| Conformance + e2e both networks | 5% | 0, 8 |
| Examples + production + runbook | 5% | 10 |
Search is the largest single line, as the RFP requires. The hub is the smallest meaningful one: it exists because the RFP names it explicitly, not because a product is being built around it.
These shapes are validated by e2e/extensions/bazaar.ts in the x402 repository.
Reproduce them exactly. Do not "improve" field names or nesting.
Query filters: type, payTo, network, extensions, limit, offset.
Natural-language query (not q, confirmed against the official
@x402/extensions/bazaar client types and the x402 e2e test's own probe;
this doc originally had q, corrected during Phase 4 while reading the
primary source for something else, see docs/INTEROP.md §3), cursor
pagination, partialResults when matches were truncated. GET /discovery/*
is live (apps/facilitator/src/discovery-routes.ts, Phase 5 work) and
passed the official x402 e2e suite's own Discovery Validation step
(conformance/RESULTS.md, 2026-08-17).
{
"x402Version": 2,
"resources": [ { "resource": "…", "type": "http" } ],
"partialResults": false,
"pagination": { "limit": 50, "cursor": null }
}- Catalog key is the tuple
resource.url+input.toolName. - Expected resource URL form:
mcp://tool/{toolName}. discoveryInfo.input.transportis"streamable-http"or"sse".
Base64-encoded JSON reporting the cataloging outcome, so a seller can tell whether a listing landed and why not:
{ "bazaar": { "status": "success" } }
{ "bazaar": { "status": "rejected", "rejectedReason": "info failed schema validation" } }Must emit the Stellar extra block including areFeesSponsored, and accept the
spec payload: { transaction } format verbatim.
Commit at the end of every phase with a conventional-commit message. The commit history is part of the deliverable; reviewers read it.
Monorepo, Biome, TypeScript strict, Vitest, CI (GitHub Actions), Apache-2.0
LICENSE, .env.example, licence-check script that fails on AGPL.
Before writing any facilitator code, characterise the reference behaviour.
The public x402.org facilitator supports stellar:testnet with no API key and
correctly returns extra: { areFeesSponsored: true }. Point a stock client at it,
capture every request and response verbatim, and commit the transcripts to
conformance/baseline/. That transcript is your specification of "conformant":
it is cheaper to match observed behaviour than to infer it from prose.
Do the same against other multi-chain facilitators claiming Stellar support. Advertised support and reachable support are not the same thing, and any gap you document is evidence of conformance discipline for the submission.
Gate: pnpm install && pnpm typecheck && pnpm lint && pnpm test exits 0, and
conformance/baseline/ contains real captured transcripts.
packages/bazaar: routeTemplate validation and soft-drop extraction.
The facilitator is a trust boundary: clients echo the resource block into the
payment payload, so a hostile client can attempt to poison the catalog with
forged metadata or a crafted routeTemplate.
routeTemplate must be percent-decoded BEFORE traversal checks. A naive
includes("..") is walked straight past by %2e%2e. Decode repeatedly (bounded)
to catch double encoding, normalise backslashes, reject null bytes, absolute URLs
and protocol-relative paths. Return the original template as the catalog key:
returning the decoded form would collapse two distinct encodings onto one entry.
Soft-drop: a metadata field failing its rule is dropped; the listing is not rejected wholesale.
Gate: ≥ 20 unit tests pass, including encoded, double-encoded and
backslash traversal, malformed percent-encoding, and /%2f%2fevil.example.
Postgres schema, migrations, row-level security, and the retrieval indexes.
create table resources (
id uuid primary key default gen_random_uuid(),
url text not null,
route_template text,
tool_name text,
type text not null check (type in ('http','mcp')),
network text not null, -- CAIP-2
pay_to text not null,
asset text not null,
amount text not null, -- i128 as string
description text,
parameters jsonb default '{}'::jsonb,
accepts jsonb not null default '[]'::jsonb,
extensions text[] not null default '{}',
last_updated timestamptz not null default now(),
fts tsvector generated always as (
to_tsvector('english',
coalesce(description,'') || ' ' ||
coalesce(jsonb_path_query_array(parameters,'$.*')::text,''))
) stored,
embedding vector(512),
unique (url, route_template, tool_name)
);
create index on resources using gin (fts);
create index on resources using hnsw (embedding vector_ip_ops);RLS: the catalog is public-read; writes only via the service role used by the facilitator. Never expose the service key to a browser.
Gate: migrations apply cleanly to a fresh database; RLS policy tests pass.
apps/facilitator on Hono. verify, settle, supported for exact on both
networks, built on @x402/stellar.
Validate Soroban auth entries strictly: correctly signed, authorizing exactly the
declared call / asset / amount / recipient, not replayed, not expired. Support
classic keypairs (G-accounts) and custom __check_auth accounts (C-accounts).
Facilitator safety, all of these, or funds are at risk:
- The client-supplied transaction source MUST NOT be the facilitator.
- The client-supplied operation source MUST NOT be the facilitator.
- The facilitator MUST NOT be the
fromaddress. - The facilitator MUST NOT appear as a signer in any client auth entry.
- Simulation MUST emit only the expected balance changes and no others.
Testnet is free and needs no API key. Mainnet pricing is configurable, never hard-wired, so a self-hoster can change or remove it. Document the model. Caller authentication, metering and rate limiting are your design choice: document the mechanism and make it configurable.
Package three deployment paths, all first-class:
- Hosted: the managed service you operate.
- Self-hosted: someone forks and runs their own instance.
- Self-facilitation inside a resource server: a seller embeds the facilitator in their own process and needs no external operator at all.
Path 3 is explicitly required by the RFP and is the one most implementations forget. Design the package boundaries so it works: the facilitator core must be importable as a library, not only runnable as a service.
Do not foreclose batch-settlement or auth-capture. Both are deferred
(batch-settlement needs a Soroban escrow, a voucher store, double-spend
prevention and its own audit; upto covers the metered case that auth-capture
would serve). Keep the scheme dispatch open so either can be added later without
restructuring.
2026-09-01: the deferral has a real technical reason now, not just scope.
batch-settlement's long-lived-channel design needs voucher signatures with
no meaningful expiry (both the EVM and SVM bindings deliberately give
vouchers none, bounding redemption through the channel's own state instead).
require_auth_for_args, the mechanism that made upto work, cannot do
this: the Soroban host hard-rejects any signatureExpirationLedger beyond
current_ledger + max_entry_ttl - 1, confirmed live against real testnet
(stellar network settings --network testnet) at 3,110,400 ledgers, 180
days. upto never hit this since its own authorizations only need to
survive minutes. A real path exists (raw env.crypto().ed25519_verify(),
already Stellar's own reference pattern in rs-soroban-env's
simple_account example, mirroring what SVM's own binding already does),
just not the upto-shaped one. Filed as
x402-foundation/x402#3341,
sketch only, not a commitment to build it next.
Gate: a settled transaction hash on stellar:testnet, recorded in
conformance/RESULTS.md.
When the facilitator receives a PaymentPayload carrying the discovery
extension, validate info against the supplied schema and catalog the resource
with no separate registration step. Manual registration may exist only as a
secondary path; anything requiring a seller to act after payment gets skipped.
Catalog both HTTP endpoints and MCP tools. Emit EXTENSION-RESPONSES.
Interoperate: Stellar must not become a walled garden. A Stellar listing
must be representable consistently with how other facilitators represent theirs.
Take the transcripts captured in Phase 0, diff your catalog entries against how
the same resource appears in a multi-chain facilitator's index, and record any
divergence in docs/INTEROP.md with the reason. If a divergence is a bug on
their side, file it upstream: interop bug reports are named in the RFP as a
strong signal of conformance discipline.
Also ship seller-side helpers so a resource server declares discovery metadata correctly with minimal boilerplate, including per-parameter descriptions. Those descriptions are what make an endpoint legible to an agent, and they are the primary input to search ranking in Phase 5. A seller who omits them gets found less, so make the helper make them easy.
Gate: integration test, a payment carrying the extension results in a
catalog row and a success header; a crafted hostile routeTemplate results in
a rejected header with a specific reason and no row.
packages/search. Hybrid retrieval, and the honest measurement of it.
Search quality is a deliverable, not a detail. It is the hardest part of the scope and the part existing catalogs most often leave unimplemented.
- Lexical: Postgres
tsvector+ GIN. - Semantic: pgvector
HNSWwithvector_ip_ops, embeddings over the discoveryinfostructure: resource description, per-parameter descriptions, and MCP tool schema. - Fusion: Reciprocal Rank Fusion,
1 / (k + rank)withk = 50, separatefull_text_weightandsemantic_weight. Implement as a single Postgres function joining two CTEs with a full outer join. - Evaluation:
eval/golden.jsonlwith graded relevance judgements. Compute nDCG@10 and MRR in CI on every release. Publish the number.
Gate: pnpm eval prints nDCG@10 and MRR over ≥ 30 golden queries, and CI
fails if nDCG@10 regresses more than 5% against the committed baseline.
The deepest technical contribution, and the one no other bidder will have.
upto requires four properties a SEP-41 allowance cannot provide:
single-use authorization, time bounds, recipient binding, and a maximum. An
allowance fails recipient binding (transfer_from lets the spender choose any
to) and single-use (an allowance is a standing balance). A contract is
required. Document this reasoning explicitly: it is the argument that
demonstrates you understood the problem rather than the spec.
Build contracts/upto-settlement (Rust, soroban-sdk 27.0.5):
- The client signs via
require_auth_for_argsrestricted to(authorization,), excludingactual_amount. A plainrequire_auth()would authorize the full argument list including the charge, forcing the client to know it at signing time and collapsinguptointoexact. This is the mechanism that makesuptoexpressible on Soroban. - Atomic pull-and-refund: auth entries commit to exact sub-invocation arguments,
so the contract pulls
max_amountand refunds the remainder in the same transaction. No custody window. Assert a zero contract balance at the end. - Nonce in temporary storage. The deadline dominates the nonce: an entry only
needs to survive until
deadline_ledger, after which the authorization is unusable regardless of nonce state. Bound the window so TTL always covers it.
Then author spec/scheme_upto_stellar.md following the structure of
specs/schemes/exact/scheme_exact_stellar.md, and prepare the upstream
contribution as typescript/packages/mechanisms/stellar/src/upto/, mirroring
the existing src/exact/.
Three assumptions are unverified. Test each on testnet before claiming the spec is correct; if reality differs, change the spec, not the test:
require_auth_for_argsaccepts a root tuple of(authorization,)while the token transfer rides as a sub-invocation formax_amount.- Pull → pay → refund fits inside Soroban's per-transaction read, write, instruction and memory limits.
temporary()TTL can always coverdeadline_ledger − current_ledger.
Gate: cargo test passes; contract deployed to testnet; a settled upto
transaction hash recorded; each of the three assumptions is a passing test or a
documented spec change.
Real-world signal after the gate, not part of it: two direct SCF #45
competitors (Rialto, AutoLayer) chose to build on this spec upstream
rather than fork their own. Separately, an independent implementer
(davedumto), reviewing five Stellar upto implementations (rail402,
Rialto, openx402, LumenGate, and this one), flagged nonce-TTL replay
handling as a required test vector, citing rail402's specific guard;
this contract's mechanism was independently verified in response and
found to prevent the bug class structurally. A concrete consolidation
direction has since emerged from the same thread (#3134), implemented
2026-09-01 by rewriting #3098 itself. Full sourced writeup:
docs/UPTO-CONVERGENCE.md and README.md's "The ecosystem is converging
on this spec" section, not repeated here.
A guarantee this spec does not currently make, stated plainly rather
than left implicit. require_auth_for_args (the mechanism above) is
documented as dispatching "transparently" to a C-account's own
__check_auth, on both this project's spec draft and the competing one.
That is verified only 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). Building
exactly that in Phase 6b hit a real, unresolved wall:
__check_auth traps unreachable on every construction tried, filed as
OpenZeppelin/stellar-contracts#839,
open. Anything in this spec that reads as "C-accounts work with
upto" means the directly-signing case only, until that issue closes.
Do not extend the claim to delegated signers without new evidence.
packages/mcp on @modelcontextprotocol/sdk 1.30.0. Let an agent search the
Stellar Bazaar and make a paid call from inside an agent runtime, wrapping the
discover → pay → retry loop behind MCP tools:
search_services: natural-language query over the Bazaarcall_paid_service: proxy that handles the 402 loop
Structured, deterministic inputs and outputs with machine-readable error codes.
Gate: the server registers in Claude Desktop / any MCP client and completes a paid testnet call end to end.
This is the acceptance criterion. Treat it as the highest-value phase.
Register Periplo with the x402 e2e suite as an external proxy:
e2e/facilitators/external-proxies/periplo/ containing run.sh and:
{
"name": "periplo",
"type": "facilitator",
"language": "typescript",
"protocolFamilies": ["stellar"],
"x402Versions": [2],
"extensions": ["bazaar"],
"environment": {
"required": ["PORT", "STELLAR_SECRET", "STELLAR_NETWORK"]
}
}The runner must be a CLI parseable from run.sh, listen on the given port, emit
the expected logs, and exit 0 on success / 1 on failure.
Acceptance requires all of:
- An unmodified canonical client completes a payment end to end on both networks.
/supportedemits the Stellarextracontract includingareFeesSponsored.- The spec
payload: { transaction }format is accepted verbatim. - A passing run of the x402 repo's e2e suite for both networks.
- A published settled transaction hash per network, per scheme.
- A non-null
reasonon every rejection.
Record every result in conformance/RESULTS.md with hashes and timestamps.
Gate: the e2e suite passes with Periplo selected as facilitator, Stellar as protocol, and Bazaar as extension.
apps/hub on Next.js 16.3.1 + Tailwind 4.3.3. See §10 for exact scope.
Gate: Playwright E2E covers each role path; Lighthouse accessibility ≥ 95.
Two end-to-end example integrations, both runnable from a clean clone:
- A paid API that becomes discoverable and gets paid by an agent.
- An MCP-driven agent that discovers and pays with no pre-baked integration: this is the one that proves the Bazaar earns its existence.
Fly.io deployment to both networks, runbook, monitoring, public telemetry endpoint.
Security review routes through the Audit Bank, not your own budget. v1 ships
no new Soroban contract on the settlement path, so the review covers an off-chain
service and its cryptographic validation rather than a full contract audit.
Scope it that way when you apply. The UptoSettlement contract is a separate,
smaller review. Audit fees are not an eligible budget line; the Audit Bank is
a distinct programme.
Public communication channels. The RFP asks for a commitment to regularly
update the community, and suggests open-source and decentralised channels.
Set up a Matrix room and a Mastodon or Bluesky account before launch,
link them from the hub, and commit to a stated cadence in docs/MAINTENANCE.md.
This is cheap, it is explicitly requested, and skipping it is a gratuitous
deduction.
Gate: both networks live; /health green; runbook rehearsed once; example 2
completes discovery → payment with zero prior integration.
Implement and test each. A test that proves the attack is blocked is worth more than a paragraph saying it is.
| Threat | Control | Test |
|---|---|---|
Catalog poisoning via routeTemplate |
Decode-then-validate, traversal rejection | Phase 1 suite |
| Listing spoofing (seller impersonation) | Bind listings to the verified payer; never trust client-echoed payTo |
Integration |
| Replay | Nonce + ledger deadline, enforced before settlement (MAX_WINDOW_LEDGERS, rejects_window_exceeding_the_contract_maximum/accepts_window_exactly_at_the_contract_maximum in contracts/upto-settlement; full writeup in docs/THREAT-MODEL.md) |
Contract test |
| Fund redirection | Recipient read from signed auth entry, never from an argument | Contract + facilitator test |
| Facilitator drain | The five safety checks in Phase 3 | Adversarial fixtures |
| Front-running settlement | Facilitator binding in the authorization | Contract test |
| Simulation false-pass | Explicit signature verification independent of simulation | Unit test with an unsigned payload |
| Injection / SSRF via resource URLs | HTTPS-only, no private ranges, no redirects followed | Unit test |
| Secret leakage | Redact in logs; no service key in any client bundle | Lint rule + CI grep |
| Dependency compromise | Lockfile committed, osv-scanner, licence gate |
CI job |
Run osv-scanner and a licence audit in CI. Fail on any AGPL transitive.
| Layer | Tool | Requirement |
|---|---|---|
| Unit | Vitest 4.1.11 | ≥ 85% line coverage on packages/* |
| Contract | cargo test |
Every error variant has a test |
| Fuzz | cargo-fuzz |
≥ 3 targets on authorization decoding and amount arithmetic |
| Integration | Vitest + testnet | Verify, settle, catalog, search against live RPC |
| Search relevance | custom harness | nDCG@10, MRR, fixed seeds, CI regression gate |
| E2E (web) | Playwright 1.62.1 | Each developer-hub role path |
| E2E (protocol) | x402 e2e suite | Both networks, exact and upto, Bazaar extension |
| Security | adversarial fixtures | Every row of §6 |
| Load | k6 or autocannon | Bursty agent traffic; prove channel accounts remove the sequence bottleneck |
Facilitator → Fly.io. Two apps: periplo-testnet, periplo-mainnet.
# fly.toml
[http_service]
internal_port = 8402
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1 # never 0, cold start breaks agent latency SLOs
[http_service.concurrency]
type = "requests"
soft_limit = 200
hard_limit = 400
[[vm]]
memory = "512mb"
cpu_kind = "shared"
cpus = 1
[checks.health]
type = "http"
path = "/health"
interval = "15s"
timeout = "2s"Secrets via fly secrets set, never in fly.toml. The mainnet app must fail to
boot if a signing key capable of moving user funds is present.
Hub → Vercel or Fly static. Database → Supabase, pgvector and full-text
enabled, RLS on.
Targets: ≥ 99% uptime on public endpoints. Discovery queries are fast lookups; verify/settle latency suits interactive agent use. State the degraded path for settlement and for indexing separately.
Publish operational telemetry: requests served, error rate, latency p50/p95, catalog size. Aggregate only.
The submission form asks for this explicitly. Answer it with architecture:
- No PII. No cookies. No IP retention. Aggregate operational metrics only.
- Self-hosted telemetry; no third-party analytics on the hub.
- Anchor hashes, never raw request bodies.
- Collecting nothing personal is a stronger answer than any privacy policy, and it reduces GDPR exposure to approximately nothing.
This exists because the RFP names it: a role-based developer guide modeled on the Algorand x402 developer hub, organized around what the reader is building, with at least a seller path, a buyer and agent path, and an operator path, each linking live testnet examples so a developer can run the flow.
Build these pages and no others:
| Route | Purpose |
|---|---|
/ |
What Periplo is, in one screen. Live catalog count. Links to the three paths. |
/sellers |
Make an endpoint payable and discoverable. Copy-paste snippet, per-parameter metadata, how to confirm the listing landed via EXTENSION-RESPONSES. |
/buyers |
Pay for a service from code or an agent runtime. Client setup, trustline step, the 402 loop. |
/operators |
Self-host the facilitator. Configuration, fee model, channel accounts, runbook. |
/browse |
Human view of the catalog: search box, filters (type, network, payTo), result cards showing price, asset and parameters. |
/playground |
Run a real testnet payment in the browser. Wallet connect, pick a catalogued service, pay, see the settled hash. |
/status |
Operational telemetry: uptime, latency p50/p95, error rate, catalog size, last settled transaction per network. |
/conformance |
The e2e results table with transaction hashes per network per scheme. |
Design requirements: server components by default, no client JS on docs pages, dark and light themes, keyboard-navigable, WCAG AA contrast, all code blocks copyable, every claim linked to a hash or a test. Target: a developer gets from the hub to a paid, discoverable endpoint appearing in the Bazaar in well under an hour, that is a stated RFP requirement, so instrument it and measure it once with a real first-time user.
No marketing page. No pricing page. No sign-up. No dashboard.
README.md: honest status, verify-it-yourself commands, architecture diagram in Mermaid, licence.docs/ARCHITECTURE.md: the diagram plus a plain-English explanation of the stack. Both are required by the submission form.docs/DECENTRALIZATION.md: the index is off-chain by design; an on-chain Soroban registry adds rent that must be extended or entries are evicted, and per-payment anchoring roughly doubles settlement cost. Decentralization is achieved through replicability: permissive licence, first-class self-hosting, and an interoperable catalog format, so no operator is a single point of failure. State this position explicitly rather than leaving it implied.docs/INFRASTRUCTURE.md: what runs where, and who pays for it after the grant.docs/MAINTENANCE.md: how conformance is maintained as the discovery conventions evolve under the x402 Foundation: how spec changes are monitored, how quickly conformance updates ship, and what happens at grant end. Drift, not inability, is the failure mode this RFP screens for.docs/PRIVACY.md: §9, as a document.docs/INTEROP.md: how a Periplo listing maps to how other facilitators represent the same resource, with every divergence and its reason.conformance/RESULTS.md: the evidence table.conformance/baseline/: the Phase 0 transcripts from the reference facilitator.docs/DEFERRED.md: everything deliberately not built, with reasons.
Stellar tech stack currency. The RFP requires the most recent stable release. The manifest in §2 was verified on 2026-08-07; before submission, re-verify each pinned version and state the verification date in the README. A stale pin is a small thing that signals a large one.
- One phase per session block. Do not start a phase until the previous gate passes. Report the gate command and its exit code before proceeding.
- Never claim a passing test you have not run. Run it, paste the output.
- If a documented API does not behave as this spec describes, trust reality. Update the spec, note the divergence in the commit body, and continue.
- Commit at every gate. Conventional commits. The history is a deliverable.
- When blocked, write the blocker in
docs/DEFERRED.mdand continue with everything that does not depend on it. Do not stall the whole build on one unknown. - Ask before adding any dependency not in §2. Then verify its licence.
- All code, comments, commit messages and documentation in English.
-
pnpm install && pnpm typecheck && pnpm lint && pnpm testexits 0 -
cargo test && cargo clippy -- -D warnings && cargo fmt --checkexits 0 - Facilitator live on
stellar:testnetandstellar:pubnet - Settled transaction hash published per network, per scheme
- Unmodified canonical client completes a payment on both networks
- x402 e2e suite passes with Bazaar extension selected
-
GET /discovery/searchreturns ranked results; nDCG@10 published - Automatic cataloging works; hostile
routeTemplaterejected with a reason - MCP server completes a discover → pay → retry loop in an agent runtime
-
scheme_upto_stellar.mdopened as an upstream PR -
UptoSettlementdeployed to testnet with the three assumptions resolved - Developer hub live, all eight routes, accessibility ≥ 95
- Two example integrations runnable from a clean clone, one of them an agent that discovers and pays with no pre-baked integration
- Facilitator usable three ways: hosted, self-hosted, self-facilitated in-process
-
docs/INTEROP.mdshows a Periplo listing beside another facilitator's - Security review complete via the Audit Bank, findings resolved
- Matrix room and Mastodon/Bluesky account live, linked from the hub
- No AGPL in the dependency path, verified in CI
- Every README claim carries a link, a test, or a hash
- Every pinned version in §2 re-verified, with the date stated in the README
- Upstream contribution history current (README.md's own list, not repeated here): every real bug found and filed, every PR's actual merge status stated honestly, not asserted "fixed" before it is
{ "x402Version": 2, "items": [ { "resource": "https://seller.example/weather", "description": "Current conditions by city", "mimeType": "application/json", "type": "http", "x402Version": 2, "accepts": [ /* PaymentRequirements[] */ ], "lastUpdated": "2026-08-07T12:00:00.000Z", "extensions": { } } ], "pagination": { "limit": 50, "offset": 0, "total": 0 } }