Facilitator API#
Turnpike implements the standard x402 v2 facilitator surface. The contract is the protocol specification, not this document — where the two disagree, the specification wins and my implementation is wrong.
A note on field names. Exact request and response shapes are defined by the x402 v2 specification and the
@x402/stellarTypeScript types. This page describes semantics and behaviour. Integrators should treat the specification and the package types as authoritative for wire format.
POST /verify#
Checks whether a payment payload satisfies a set of payment requirements, without moving any money.
Checks performed
- The payload is a decodable transaction envelope carrying exactly one
InvokeHostFunctionoperation callingtransfer. - The entry authorises the invocation the requirements describe — asset contract, amount, recipient — checked both on the arguments and against the events the simulation emits.
- The payer has signed the authorization entry, and no other signature is outstanding.
- The expiration ledger is not further ahead than the facilitator accepts, measured against current ledger height.
- No facilitator-controlled account appears as payer or in the authorization entries.
- The simulated fee does not exceed the configured ceiling.
- Simulation succeeds. This is what catches an insufficient balance, a missing trustline, or an authorization that has already been spent — there is no separate “already settled” lookup.
Returns isValid, and on failure invalidReason (a machine-readable code) plus invalidMessage (a sentence). Note the field names differ between endpoints: verify uses invalidReason/invalidMessage, settle uses errorReason/errorMessage. There is no field literally named reason.
Verification is deliberately separate from settlement so a seller can confirm a payment is good, perform the work, and only then spend a transaction on settling it. If the work fails, nothing has been settled.
A known limitation, stated plainly. For a few seconds after a payment settles, /verify may still report that payload as valid, because the RPC read that would reveal the settlement has not yet caught up. This is not a double-spend risk — the subsequent settlement attempt fails — but an integrator building on /verify alone should know the window exists. Closing it properly is production-hardening work in Tranche 3.
POST /settle#
Submits the payment to Stellar and returns the result.
Sequence
- Re-verify. The payload is checked again at settlement time; state may have changed since
/verify. - Assemble the transaction with the payer’s authorization entry attached, using Turnpike’s account as the transaction source. (A channel-account pool would lease a source per settlement; that is Tranche 1 work, and today a single signer serialises settlements.)
- Submit through Soroban RPC, with the network fee paid by that same account.
- Poll for confirmation.
- Return the settled transaction hash.
Returns success, transaction (the hash), network, and on failure errorReason and errorMessage.
Settlement is idempotent with respect to a given payload: a payload that has already settled is rejected rather than settled twice.
Measured across 135 settlements on CI runners: minimum 1.3 s, median 3.9 s, maximum 7.2 s. A full conformance run — one payment plus nine negative cases — takes roughly 20 s locally. Concurrency is currently limited to one in-flight settlement; see Reliability and evidence.
GET /health#
Reports liveness plus the network, the facilitator’s address, and its fee-sponsorship posture. Used by docker compose health checks and by the demo server, which waits for it before accepting traffic.
GET /supported#
Advertises what this deployment can actually do: the schemes and networks it accepts, and per-network capability details.
For Stellar entries this includes the extra block carrying areFeesSponsored.
This field is not decorative. A client uses it to decide whether it needs to hold XLM. If a facilitator advertises sponsorship it does not provide, agents fail in a way that is difficult to diagnose from the client side. Turnpike reports what the running deployment actually does.
Error semantics#
Every rejection returns a specific, human-meaningful reason. No null values, no empty strings, no generic "error".
This is an explicit acceptance requirement of the RFP, and it is enforced rather than intended: the test suite asserts that no rejection path can return a null or empty reason, and the conformance harness exercises negative cases on every CI run — malformed payload, insufficient amount, wrong asset, replayed payload, unsupported scheme, unsupported network.
Rejections are grouped as follows.
| Class | Representative code | Retryable by the client? |
|---|---|---|
| Malformed request envelope | invalid_request_body | No — fix the client |
| Malformed payload | invalid_exact_stellar_payload_malformed, …_wrong_operation, …_wrong_function_name | No |
| Requirements mismatch | …_payload_wrong_asset, …_wrong_amount, …_wrong_recipient, and the …_event_wrong_* variants checked against simulation events | No |
| Missing or excess signatures | …_payload_missing_payer_signature, …_unexpected_pending_signatures | No |
| Expiration out of window | invalid_exact_stellar_signature_expiration_too_far | Yes — see the RPC-skew defect in Reliability |
| Simulation failed — insufficient balance, missing trustline, spent authorization | invalid_exact_stellar_payload_simulation_failed | Sometimes — after funding, or never if the payload is spent |
| Facilitator-safety violation | …_facilitator_is_payer, …_facilitator_in_auth, …_fee_exceeds_maximum | No |
| Unsupported scheme / network | unsupported_scheme_or_network | No — consult /supported |
| Settlement submission rejected, including a replayed payload | settle_exact_stellar_transaction_submission_failed | No |
| Transient chain error | upstream_rpc_unavailable | Yes |
Every code above maps to a human-readable sentence in facilitator/src/reasons.ts; a unit test reads the codes out of the installed @x402/stellar build and fails if any lacks one, so a dependency upgrade cannot silently introduce an unexplained rejection.
The last class is the one that caused a real production failure during development, and it is documented in detail in Reliability rather than glossed here.
Integration#
A seller integrates through the official x402 server middleware for their framework — Express, Hono, Fastify and others ship adapters — pointing it at a Turnpike instance. The middleware handles the 402 response and the verify/settle calls; the seller writes ordinary route handlers.
A buyer integrates through a standard x402 client such as @x402/fetch or @x402/axios. No Turnpike-specific client exists, and none should. If a buyer needs custom code to pay Turnpike, I have failed the conformance requirement that defines the project.