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/stellar TypeScript 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 InvokeHostFunction operation calling transfer.
  • 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

  1. Re-verify. The payload is checked again at settlement time; state may have changed since /verify.
  2. 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.)
  3. Submit through Soroban RPC, with the network fee paid by that same account.
  4. Poll for confirmation.
  5. 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.

ClassRepresentative codeRetryable by the client?
Malformed request envelopeinvalid_request_bodyNo — fix the client
Malformed payloadinvalid_exact_stellar_payload_malformed, …_wrong_operation, …_wrong_function_nameNo
Requirements mismatch…_payload_wrong_asset, …_wrong_amount, …_wrong_recipient, and the …_event_wrong_* variants checked against simulation eventsNo
Missing or excess signatures…_payload_missing_payer_signature, …_unexpected_pending_signaturesNo
Expiration out of windowinvalid_exact_stellar_signature_expiration_too_farYes — see the RPC-skew defect in Reliability
Simulation failed — insufficient balance, missing trustline, spent authorizationinvalid_exact_stellar_payload_simulation_failedSometimes — after funding, or never if the payload is spent
Facilitator-safety violation…_facilitator_is_payer, …_facilitator_in_auth, …_fee_exceeds_maximumNo
Unsupported scheme / networkunsupported_scheme_or_networkNo — consult /supported
Settlement submission rejected, including a replayed payloadsettle_exact_stellar_transaction_submission_failedNo
Transient chain errorupstream_rpc_unavailableYes

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.