Skip to content

Repository files navigation

x402-facilitator-stellar

An x402 facilitator for Stellar — verify, settle, supported

CI Status Status: conformance spike License Apache 2.0 Stellar testnet x402 v2

Conformance · Documentation · Known Gaps · Accensa org · x402 spec

Developer infrastructure for x402 on Stellar, built on the Apache-2.0 @x402/stellar package. Independent of the merchant back-office in accensa-app and accensa-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-14 the answer is yes, twice, with settled transactions anyone can verify — and four of the five upstream server components still fail. See Conformance for both halves. It makes no availability claim and is not deployed anywhere you can reach.

The Problem

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.

Why Not Reimplement It

@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.

Documentation

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 · Threat model · Audit readiness · Privacy · 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.

Running Locally

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/readyz

FACILITATOR_SECRET is a signing key. .env is gitignored — never commit it.

Tests

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 path

All 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 e2e

Privacy and Data Minimisation

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.

Conformance

Acceptance is tested at the wire level with stock SDK code, not by reading a claim. What holds today on testnet:

  • /supported emits the Stellar extra block including areFeesSponsored
  • 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 — two, below
  • The x402 repository's e2e suite — 1 of 5 server components passes
  • stellar:pubnet

Settled on Stellar testnet, 2026-08-14

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}'

Four of five scenarios still fail

typescript/http/next passes. express, fastify, hono and mcp do not — two with Payment response header not found, two with upstream's 402 facilitator_error. This reproduced identically across two runs in which the harness ordered the combinations differently, so it is structural rather than flaky. Exactly one settlement occurs per run; the four failures never reach the chain.

It cannot currently be diagnosed, because this facilitator emits four lines of output across an entire run — three startup banners and an exit code. Two of the failures mean this service returned an error, and there is no record of what it was. That makes #7 the blocking item rather than a nice-to-have; the investigation is #64.

The full record, including the treasury prerequisite that had to be solved first and how to reproduce both results, 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.

Known Gaps

  • 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/resources with the full upstream filter set, GET /discovery/search with lexical and hybrid (dense-embedding + reranking) retrieval, automatic cataloging off the payment path, EXTENSION-RESPONSES reporting, and an MCP server (docs/MCP.md). A search-evaluation harness and judgement set live in eval/. 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 as invalid_routeTemplate (#65).
  • No deployment. There is a Dockerfile, a docker-compose.yml and docs/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 when DATABASE_URL is set; the settlement path holds nothing durable, tracked in #10.
  • exact only. The upto scheme has no Stellar specification yet; design notes in accensa-contracts/docs/ADR-002.

Contributing

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.

Contributors

License

Apache-2.0 — see LICENSE. Chosen to match upstream @x402/* so work here can be contributed back.

About

Conformance spike: a minimal x402 facilitator for Stellar built on @x402/stellar

Topics

Resources

Security policy

Stars

15 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages