An x402 facilitator for Stellar — verify, settle, supported
Conformance · Documentation · Known Gaps · Accensa org · x402 spec
Developer infrastructure for x402 on Stellar, built on the Apache-2.0
@x402/stellarpackage. Independent of the merchant back-office inaccensa-appandaccensa-contracts— a seller can use those without this, and an agent can use this without those.
Warning
This is a conformance spike, not a production facilitator. It exists to answer one question: can an unmodified canonical x402 client complete a payment against a facilitator we operate on Stellar testnet? As of 2026-08-26 the answer is yes across all five upstream server components — 10 of 10 scenarios in the upstream e2e suite, with settled transactions anyone can verify. See Conformance. It makes no availability claim and is not deployed anywhere you can reach.
x402 turns HTTP 402 into a machine-native payment flow: a client requests a resource, the
server replies 402 with terms, the client signs a payment authorization and retries, and
a facilitator verifies and settles on-chain before the resource is returned.
The facilitator is the piece a seller cannot easily run themselves. It has to validate Soroban authorization entries strictly — correctly signed, authorizing exactly the declared call, asset, amount and recipient, not replayed, not expired — submit the invocation, and cover the network fee so the buyer needs only the payment asset. Get any of that subtly wrong and the failure is silent: payments that look settled and are not, or authorizations that grant more than the payer understood.
@x402/stellar already ships ExactStellarScheme, which implements the
SchemeNetworkFacilitator interface — verify, settle, getExtra, getSigners —
and validates:
- auth-entry structure and credential type
- expiration against a maximum ledger
- facilitator safety — the facilitator must not be a party to the transfer
- absence of sub-invocations — no authorization the payer did not see
- payer signature status, and that no other signatures are pending
- via simulation, that there is exactly one transfer event matching the expected sender, recipient, amount and asset
None of that is reimplemented here. It is the part most dangerous to get subtly wrong, and rewriting it would duplicate the package the ecosystem is standardizing on.
This repo is the transport around it. @x402/core ships no facilitator router — it
gives you x402Facilitator with verify(), settle() and getSupported(), and the HTTP
surface is yours to write. That surface, plus configuration, caller authentication and
operational concerns, is what lives here.
Published docs for the whole organisation, including this service, are at https://accensa.github.io/accensa-app/docs/facilitator/overview.
In-repo, refer to the Documentation Hub for detailed role-based guides:
Reference material: Architecture · Bazaar discovery · MCP server · Conformance · Deployment · Operations · Authentication · feat/upstream-drift-watch Threat model · Audit readiness · Privacy · Upstream tracking ·
Business model · Threat model · Audit readiness · Privacy · main Glossary
Sibling repositories in the Accensa organisation:
accensa-app (merchant dashboard, indexer,
@accensa/sdk) and
accensa-contracts (Soroban receipt
anchoring and refund vault). A seller can use those without this, and an agent can use
this without those.
The service loads a .env file at startup in non-production environments
(NODE_ENV !== 'production') — nothing breaks when the file is absent, and
variables set in the real environment always win over .env, so a stale local
file cannot silently override what a deployment injected. Production skips the
file entirely: there, the environment comes from the orchestrator.
cp .env.example .env # then fill in FACILITATOR_SECRET
npm start # or: npm run dev (adds --watch)
curl localhost:3402/healthz
curl localhost:3402/readyzFACILITATOR_SECRET is a signing key. .env is gitignored — never commit it.
Payments on Stellar need funded accounts, and USDC-priced payments additionally need
trustlines — an account cannot hold or spend an issued asset until it has
authorized the issuer. This is the most common way a first x402 payment fails, so
it is documented (and scripted) rather than left to a deep stack trace. Two helpers
in scripts/ handle the testnet side, wired to npm:
npm run testnet:fund # scripts/fund-testnet-accounts.mjs
npm run testnet:usdc # scripts/prepare-testnet-usdc.mjsnpm run testnet:fundcreates three fresh accounts (client, server/payee, facilitator), funds them via Friendbot, opens a USDC trustline on each, and prints the credentials as env assignments (--json/--github-envfor other formats).npm run testnet:usdcputs existing payer/payee accounts into a pay-ready state: USDC trustlines on both, and a small USDC balance on the payer drawn fromTESTNET_USDC_TREASURY_SECRET(testnet-only; reportsusdc_ready=falsehonestly when the treasury is absent).
What trustlines are, who needs which, and the mainnet path (same changeTrust
mechanism, no Friendbot) are in the Seller Guide and
Buyer / Agent Guide; both examples
(examples/http-seller,
examples/mcp-agent) state their prerequisite
up front.
npm test # unit tests — no network, no funded account, no .env
npm run lint # eslint
npm run format:check # prettier, check only
npm run licenses # fails on any AGPL in the dependency pathAll four run in CI on every push and pull request, across Node 20 and 22.
The end-to-end conformance run is separate, because it needs testnet and two funded accounts:
FACILITATOR_SECRET=$(stellar keys show facilitator) npm start &
ALICE_SECRET=$(stellar keys show alice) npm run e2eThe accounts that end-to-end run needs are a chore on day one, and the scripts
for it exist: npm run testnet:fund generates and friendbot-funds the three
testnet accounts the suite needs (client, server payee, facilitator) and prints
them as env assignments, and npm run testnet:usdc gives the payer and payee
USDC trustlines plus a funded balance from a treasury account
(TESTNET_USDC_TREASURY_SECRET). Both are exactly what the conformance
workflow does before each run — see docs/CONFORMANCE.md.
Everything in scripts/, and where it is documented:
| Script | npm script | Purpose |
|---|---|---|
check-licenses.mjs |
npm run licenses |
Fails on any non-permissive (AGPL) dependency in the tree. |
check-env-doc.mjs |
npm run env:check |
Fails when a variable read in src/ is missing from .env.example. |
smoke-examples.mjs |
npm run smoke:examples |
Starts both examples and asserts real behaviour (402 challenge; MCP initialize/tools/list); the CI examples gate. |
e2e.mjs |
npm run e2e |
End-to-end x402 payment against a running facilitator on testnet. |
fund-testnet-accounts.mjs |
npm run testnet:fund |
Generates and friendbot-funds the three testnet accounts the suite needs. |
prepare-testnet-usdc.mjs |
npm run testnet:usdc |
Gives the payer and payee USDC trustlines and a funded balance. |
select-conformance-components.mjs |
(CI, conformance.yml) |
Decides which upstream e2e components a run exercises; documented in docs/CONFORMANCE.md. |
bench-http.mjs |
npm run bench |
Local throughput benchmark of the HTTP surface with stubbed collaborators. |
data_retention_job.js |
— | Not implemented. Exits non-zero on purpose: it is scheduled to enforce the docs/PRIVACY.md retention periods once a datastore exists, and nothing is purged until then. Tracked in Issue #50. |
The X402 Facilitator handles sensitive transaction and search query data. Our approach is to collect only what is necessary, and to aggressively purge it according to strict retention policies. For detailed information, see our Privacy Policy.
The transport emits one structured JSON line per request to stdout and exposes
Prometheus metrics on GET /metrics. Configure LOG_LEVEL (verbosity) and
METRICS_PORT (bind metrics to a separate, unauthenticated port) via the
environment — see .env.example and Operations
for the log fields and the alert to set on each metric.
Acceptance is tested at the wire level with stock SDK code, not by reading a claim. What holds today on testnet:
-
/supportedemits the Stellarextrablock includingareFeesSponsored - Every rejection carries a non-null
invalidReason— across malformed bodies, unregistered scheme/network pairs, and scheme-level failures - The spec's
payload: {transaction}shape is accepted verbatim - An unmodified canonical client completes a payment end-to-end
- Settled transaction hash published — see the conformance table below
- The x402 repository's e2e suite — 5 of 5 server components pass (10/10
scenarios across
express,fastify,hono,next,mcp, 2026-08-25/26) -
stellar:pubnet(code-path verified; on-mainnet proof pending funded keys [#17])
An unmodified typescript/http/fetch client received a 402 with terms, signed a payment
authorization, retried, and this facilitator verified and settled it on-chain — paying the
network fee itself, so areFeesSponsored is observed rather than merely advertised.
| Transaction | Ledger | Settled |
|---|---|---|
5f1bd15a…5558 |
4134781 | 08:15:33Z |
ff798145…0590 |
4134928 | 08:27:49Z |
Both report "successful": true from Horizon. Verify without trusting this file:
curl -s https://horizon-testnet.stellar.org/transactions/5f1bd15aec8ca3c6390689ed7fed82506f6c3d8eb8ed325a05a8b83974925558 \
| jq '{successful, ledger, created_at}'The August 14 run was the first with a real USDC treasury: one payment settled and four
scenarios failed — express, fastify, hono and mcp — with Payment response header not found or upstream's 402 facilitator_error, and the facilitator's four lines of
output made the failures undiagnosable. That was tracked in
#64 and blocked on
#7, request-scoped
structured logging, request correlation and /metrics.
Both are done. The daily upstream suite has passed 10 of 10 scenarios on two
consecutive nights (2026-08-25, 2026-08-26): all five server components (express,
fastify, hono, next, mcp) × the plain and upfront payment flows, each with a
settled transaction hash on testnet. The facilitator now logs one structured line per
request with redacted headers — a verify/settle outcome and rejection reason on every
call — plus an audit channel and /metrics, so a future failure is attributable to a
specific request instead of a four-line mystery.
The full record, including the treasury prerequisite that had to be solved first and how
to reproduce the runs, is in docs/CONFORMANCE.md.
Responses use the canonical field names — VerifyResponse carries invalidReason and
invalidMessage; SettleResponse carries errorReason, errorMessage, transaction
and network. The transport-layer HTTP rejections (such as 401 Unauthorized or 429 Too Many Requests) also conform to the VerifyResponse shape to ensure a client has one parser, not three. For an exhaustive taxonomy of all emitted reasons, see REASONS.md.
- Bazaar is built but unproven against a second implementation. Discovery, search and
automatic cataloging landed on 2026-08-12 and are documented in
docs/BAZAAR.md: a catalog datastore with migrations,GET /discovery/resourceswith the full upstream filter set,GET /discovery/searchwith lexical and hybrid (dense-embedding + reranking) retrieval, automatic cataloging off the payment path,EXTENSION-RESPONSESreporting, and an MCP server (docs/MCP.md). A search-evaluation harness and judgement set live ineval/. On 2026-08-14 another party's client read this catalog for the first time and its listing was rejected — upstream registers wildcard*route templates and this repo's validation hard-drops them asinvalid_routeTemplate(#65). - No deployment. There is a
Dockerfile, adocker-compose.ymlanddocs/DEPLOYMENT.md, but no instance is running at a URL anyone can hit. Availability targets and a status page are tracked in #19. - No persistence by default. The catalog has a PostgreSQL schema in
migrations/and uses it whenDATABASE_URLis set; the settlement path holds nothing durable, tracked in #10. WhenDATABASE_URLis set you can also addDATABASE_URL_REPLICAto offload settlement status reads and the reconciliation sweep onto a read replica (CQRS fallback to primary on replication lag) — seedocs/DEPLOYMENT.md(#121). exactonly. Theuptoscheme has no Stellar specification yet; design notes inaccensa-contracts/docs/ADR-002.- Pubnet is code-served but unproven on mainnet (#17).
stellar:pubnetis served with its own signer pool, RPC provider and fee ceiling, none shared with testnet, and that isolation plus both-networks/supportedadvertising and 7-decimal amounts are pinned bytest/pubnet-conformance.test.js. What remains is operational, not code: a canonical client completing a real USDC payment on mainnet with the settled hash published needs funded pubnet keys and a contracted RPC, which no CI secret can supply. State and the key-custody/rotation posture are indocs/DEPLOYMENT.md; the checkbox above stays unchecked until that proof lands.
Issues and pull requests welcome. Given the status above, the most useful contribution is a conformance failure: point a canonical client at it and report what breaks.
Apache-2.0 — see LICENSE. Chosen to match upstream @x402/* so work here can
be contributed back.
// fix