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 asinvalid_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:/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 inpackages/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 inpackages/search/src/service.ts, returned by GET /discovery/search with HTTP 400.