Skip to main content
Every check Rail402 makes before it sponsors a Stellar payment, in the order it makes them, with the code it returns, where the check lives and which test proves it. A rejection is always an HTTP 200 body with isValid: false (/verify) or success: false (/settle), a code from packages/stellar/src/codes.ts and a non-empty human-readable reason (packages/facilitator/test/state.test.ts, “wire responses always carry a registered code and a non-empty reason”). /settle never relies on an earlier /verify: it runs stages 1–5 again, and upstream settlement runs stage 6 again before anything is signed or broadcast (see stage 8). Rail402 does not reimplement verification. Its own checks (stages 1–5) run first, before any simulation, so a payment that cannot settle is refused cheaply and with a specific code. The only RPC data they use is the latest ledger number: /verify answers from a reading up to 5 seconds old and refreshes it in the background, while /settle waits for a reading no older than 1 second (packages/facilitator/src/latest-ledger.ts, tested in latest-ledger.test.ts). The unmodified @x402/stellar verifier (stage 6) then runs every check of the exact Stellar scheme, including simulation. Rail402 only adds a diagnosis of why a simulation failed (stage 7) and makes settlement idempotent and crash-safe (stage 8). Test files, relative to the repository root: Integration tests run against a private Stellar network (docker compose --profile stellar up) with real accounts, a real token contract and real balance changes.

0. Request

apps/rail402/src/app.ts, before any payment logic. In this order:

1. Protocol envelope

preflight() in packages/facilitator/src/preflight.ts.

2. Operator policy on the requirements

checkRequirements() in packages/facilitator/src/preflight.ts. Limits come from configuration (configuration.md), never from the request.

3. Transaction structure

inspectExactTransaction() in packages/stellar/src/payload.ts. payload.transaction is taken verbatim; nothing in it is rewritten.

4. Accounts and amounts

preflight(). The facilitator’s accounts are the sponsor and every channel account.

5. Authorization and expiry

preflight().

6. Upstream verifier

ExactStellarScheme.verify from @x402/stellar 2.27.0, unmodified, called by packages/facilitator/src/scheme.ts. It simulates the transaction against the current ledger in enforcing mode, which verifies the payer’s signature and runs a smart account’s __check_auth, and then checks the simulated result. Its fee ceiling is checked at the inclusion-fee bid settlement would use: a percentile of the network’s recent fee stats, reused for 5 seconds and answered stale for up to 60 seconds while it is refreshed in the background (packages/facilitator/src/fees.ts, tested in fees.test.ts). Its codes, which Rail402 returns unchanged: These are covered by upstream’s own tests (typescript/packages/mechanisms/stellar/test in x402-foundation/x402), and on Rail402 by the settlement tests (every settlement passes through them) and the upstream e2e run. Tampering is caught here: changing the amount after signing fails simulation because the signature no longer verifies (settlement, “rejects tampering”).

7. Why a simulation failed

Upstream reports every failed simulation as invalid_exact_stellar_payload_simulation_failed. Rail402 re-simulates in enforcing mode and names the on-chain cause from the host’s diagnostic events (explainSimulation() in packages/facilitator/src/explain.ts, classifyTransferFailure() in packages/stellar/src/diagnostics.ts). The same mapping names the cause of a settlement transaction that was included but failed. Anything unrecognised keeps upstream’s invalid_exact_stellar_payload_simulation_failed.

8. Settlement

StellarExactScheme.settle() in packages/facilitator/src/scheme.ts runs stages 1–5 again. Then SettlementEngine.settle() in packages/facilitator/src/engine.ts admits the settlement through the sponsor guard, claims the authorization and leases a channel account. Upstream’s ExactStellarScheme.settle runs stage 6 again, and a rejection there is diagnosed as in stage 7; otherwise it settles the payment with the leased channel account as the transaction source and the sponsor paying the fee through a fee bump. The facilitator is never the source of the transferred funds (settlement, “payer debited, payTo credited, facilitator only pays the fee”). Only the recorded bytes are ever resubmitted, so a lost response or a restart can never produce a second transaction for the same authorization (settlement, “recovers after a lost submission response”).

Codes that are defined but not emitted

Kept so the code list matches the x402 specification and upstream, and so a response relayed from another component is still recognised: packages/stellar/test/codes.test.ts fails if a code is added without being listed here.

Bazaar cataloging

Cataloging runs after a successful settlement (and is previewed at /verify, which waits at most 250 ms for it) and never affects the payment. Its outcome is reported to the seller in the EXTENSION-RESPONSES header, which never exceeds 4,096 bytes: reasons are cut to 300 characters and an outcome too large for the budget is reduced to its status and code. Codes are in packages/bazaar/src/codes.ts. Discovery requests: a malformed or unknown query parameter returns discovery_invalid_parameter and an unknown listing discovery_listing_not_found (bazaar).

Response shapes

Verification and settlement outcomes are HTTP 200 with the x402 response body:
Transport-level rejections of /verify and /settle (stage 0) keep that shape, so the stock client raises a typed VerifyError or SettleError, and add an error object. Every other endpoint returns only the error object:

Transport codes

Defined in packages/errors/src/index.ts, shared by every endpoint. forbidden (403), method_not_allowed (405) and service_unavailable (503) are defined but not currently returned.

Search codes

Defined in packages/search/src/service.ts, returned by GET /discovery/search with HTTP 400.