x402 conformance: baseline the reference facilitator and publish the report - #153
Conversation
Establishes what the reference facilitator actually does, so "conformant" means something other than our own reading of the spec. Everything in the document was executed against live testnet. A real exact payment was verified and settled through the public x402.org facilitator with an unmodified @stellar/stellar-sdk 16.2.0 client - tx 988dca9e7a204a59534912ff502fd81056f571e35f2cb87812bdfdc3292d9f17, ledger 4425809. Fee sponsorship is confirmed empirically: the submitted transaction's source is the facilitator's own account, not the payer's. Also records /supported verbatim, a reason-per-rejection table built by mutating one field at a time on that same valid payload, and five divergences - chief among them that an unsupported network, scheme or version returns HTTP 500 unexpected_error rather than a rejection, which tells a stock client to retry a request that can never succeed. Separately, and not a conformance finding: facilitator.stellar.org, which this repo hard-codes as the default X402_FACILITATOR_URL in src/middleware/x402.ts and src/api/websocket.ts, does not resolve (NXDOMAIN).
|
@DevTobis Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Miracle656
left a comment
There was a problem hiding this comment.
Approved and merging. I independently verified the load-bearing claims rather than taking them on trust, and all of them hold.
Against live Horizon testnet for 988dca9e…d9f17:
successful: True
ledger: 4425809
source_account: GC6CSXBV4C6RL3HEDTW57KXYXSSXKAWKGYDEOSATXM3XNKXSR2VRYN3K
fee_charged: 23059
The source account is the facilitator's, not the payer's GBJ72MK5… — so extra.areFeesSponsored: true is backed by an observed on-chain fact, not by an advertised flag. I also re-fetched /supported live and it matches what you recorded. That is the difference between a conformance document and an opinion, and it is why this belongs in the repo.
The scope note is what earns the document its credibility. Saying plainly that this is not a run of the x402 repo's own e2e suite, that the suite isn't runnable against an arbitrary facilitator without vendoring its harness, and that this is weaker than running their suite but stronger than reading the spec — that is the paragraph a reviewer looks for and almost never finds. A conformance report that overstates its method is worse than none, because people stop checking.
Three findings I want to make sure don't get lost:
- HTTP 500
unexpected_errorfor an unsupported network/scheme/version. This is the serious one. A 500 tells a stock client to retry, so a request that can never succeed gets retried forever. That is a client-side hang caused by a server-side status code choice. - Already-expired auth collapses into the generic
…simulation_failed, indistinguishable from a replay or an insufficient balance — while the too-far-in-future direction gets its own named reason. The asymmetry is worth knowing before Lens's own facilitator is measured against it. - CAIP-2 holds only for
x402Version: 2. The two v1 rows use legacybase-sepolia/solana-devnet. A client that reads/supportedand assumes CAIP-2 throughout mis-parses those rows — an easy bug to write and a hard one to find.
Building the rejection table by mutating one field at a time off the same payload that actually settled is the right method: it proves a specific reason was available to the facilitator, so a generic one is a real finding rather than an artifact of sending garbage.
And the aside at the end is not an aside. facilitator.stellar.org returns NXDOMAIN — I confirmed it, Non-existent domain, connection code 000. Lens hard-codes it as the default X402_FACILITATOR_URL in src/middleware/x402.ts:22, src/api/websocket.ts:10 and .env.example. So any deployment that doesn't set that variable fails every payment verification, and this document is what surfaced it. I'm fixing the default to https://x402.org/facilitator — which I verified is live and serves stellar:testnet — in a follow-up commit on main, and crediting this PR.
Excellent work. Noting the effort/reward mismatch too: docs-only diffs are the least glamorous thing to submit and this one found a production bug.
X402_FACILITATOR_URL defaulted to https://facilitator.stellar.org, which returns NXDOMAIN. Any deployment that did not set the variable explicitly therefore failed every payment verification and settlement — the default was not merely stale, it pointed at nothing. Surfaced by the conformance baseline in #153, which settled a real testnet payment through https://x402.org/facilitator (tx 988dca9e…d9f17, ledger 4425809) and recorded its /supported response. Verified again here: the old host does not resolve, the new one is live and advertises stellar:testnet with areFeesSponsored: true. Credit: DevTobis (#153).
closes #127
Summary
docs/x402-conformance.md— what the publicx402.orgfacilitator actually does onstellar:testnet, established by running against it rather than by reading the spec. Everything in the document was executed live on 2026-08-31; nothing is inferred.Headline: a payment settled end to end
An
exactpayment onstellar:testnet, verified and settled by the public facilitator with an unmodified@stellar/stellar-sdk16.2.0 client — no patched SDK, no local shims.988dca9e7a204a59534912ff502fd81056f571e35f2cb87812bdfdc3292d9f17successful: true1.0000000areFeesSponsored: trueverified empirically, not just read off/supported: the submitted transaction's source account is the facilitator's own (GC6CSXBV…YN3K), not the payer's, and the 23,059-stroop fee was charged to it.Divergences found
Five, ranked. The first is the one that will actually hurt an integrator:
500unexpected_error, not a rejection.stellar:pubnet,uptoon Stellar, andx402Version: 1all produce a 500. These are client errors —/supportedalready publishes the right answer — and a stock HTTP client reads 500 as "server broke, retry", so it will hammer a facilitator that can never say yes. Thex402Version: 1case is worse: the message reads "No facilitator registered for scheme: exact and network: stellar:testnet", which is untrue as written — that pair is registered, at v2. The message names scheme and network but omits the version, which is the field that actually caused the miss.invalidMessage. Every structured rejection is exactly{"isValid":false,"invalidReason":"…"}— the field is absent, not null. It appears only on the 400/500 paths.invalid_exact_stellar_payload_simulation_failed, the same reason an insufficient balance gives. Those need opposite client responses (re-sign and retry vs. stop, already spent). The spec has an explicit expiry rule, so a specific reason is available — and note the other direction is handled correctly, with its owninvalid_exact_stellar_signature_expiration_too_far.payeris echoed inconsistently — present on every rejection class exceptwrong_asset./supportedmixes CAIP-2 with legacy names — the twox402Version: 1rows usebase-sepoliaandsolana-devnet, so "parsenetworkas CAIP-2" is wrong unless version is checked first.On the RFP's checks, one by one
/supportedverbatim against the stated contract — captured. The Stellar entry is{"x402Version":2,"scheme":"exact","network":"stellar:testnet","extra":{"areFeesSponsored":true}}. Matches. All 11 kinds tabulated. Stellar mainnet is not served, so nothing about mainnet can be inferred from this facilitator.payload: {transaction}accepted verbatim — yes, first attempt, no adjustment.invalidReason; no for anything human-readable (divergence 2).signatureExpirationLedger, replayed payload, plus nine malformed-payload cases. Each was built by mutating one field of the payload that actually settled, so a specific reason was genuinely available to the facilitator in every case. Full table in the doc.Two things I want to be straight about
This is not a run of the x402 repo's own e2e suite. That suite is not runnable against an arbitrary facilitator without vendoring its harness. What I did instead is the check the RFP actually describes — a canonical client built straight from the published spec (pinned at commit
c4d2de65) with the stock SDK, completing a payment end to end. Weaker than running their suite; stronger than reading the spec; and the settled hash is verifiable by anyone. The doc says so in its own scope note rather than letting the claim drift.I have not filed the upstream issue yet. The issue asks for divergences to be filed on the x402 repo and linked here, and divergence 1 clearly deserves it. I stopped short because that is a public post to a third-party project under a maintainer's identity, which felt like your call rather than mine. Say the word and I will open it and add the link — the writeup is ready.
Separately: a live bug in this repo
facilitator.stellar.org— hard-coded as the defaultX402_FACILITATOR_URLinsrc/middleware/x402.tsandsrc/api/websocket.ts— does not resolve. Google Public DNS returnsStatus: 3(NXDOMAIN), with the SOA forstellar.org.in the authority section: the parent zone exists, the label does not.Any Lens deployment that has not set
X402_FACILITATOR_URLis gating paid routes behind an unreachable facilitator. This is exactly the "advertised support and reachable support are not the same thing" gap the RFP screens for, except it is ours. I have kept it out of this PR's fix scope — it wants its own issue and its own change — but it is recorded in the doc and worth acting on before it reaches a deployment.What this gives #124–#126
The doc closes with six concrete requirements for our facilitator derived from the gaps above, including matching the reference implementation's reason identifiers (
wrong_recipient,wrong_asset,wrong_amount,signature_expiration_too_far,unsupported_credential_type) so a client can treat both uniformly.It also records the one detail that costs an hour to rediscover: the transaction source must differ from the payer, or Soroban simulation returns a
sorobanCredentialsSourceAccountentry and the facilitator correctly rejects it withinvalid_exact_stellar_payload_unsupported_credential_type. The spec implies this but never says it.A changeset is included per CONTRIBUTING.