Facilitator-role engine: verify and settle x402 payments yourself.
While X402.Facilitator is the client of a remote facilitator, this
module is the facilitator itself. It assembles the SDK's local
verification core (X402.Verify.EVM), JSON-RPC client (X402.RPC),
transaction encoder (X402.Transaction), and signer behaviour
(X402.Signer) into the engine behind the facilitator API's three
operations, speaking the same wire shapes as the reference facilitators:
verify/3— the full exact-EVM / upto-EVM verify checklist at:fulllevel, returning thePOST /verifyresponse (%{"isValid" => true, "payer" => ...}/%{"isValid" => false, "invalidReason" => ..., "payer" => ...}).settle/3— re-verifies independently (the schemes' normative requirement), then broadcasts the settlement transaction and awaits its receipt, returning thePOST /settleresponse (%{"success" => true, "transaction" => ..., ...}/%{"success" => false, "errorReason" => ..., ...}).supported/1— theGET /supportedresponse derived from the configured networks.
Expose the engine over HTTP with X402.Plug.Facilitator, or call it
directly from your own transport.
Payment kinds
The engine settles every EVM flow X402.Verify.EVM verifies:
| Scheme | extra.assetTransferMethod | Settlement transaction |
|---|---|---|
exact | absent / "eip3009" | asset.transferWithAuthorization(...) |
exact | "permit2" | x402ExactPermit2Proxy.settle(...) |
upto | — | x402UptoPermit2Proxy.settle(...) for amount |
The requirements select the authorization used throughout verification, settlement, payer reporting, pending lookup, and receipt checking. Extra unsigned authorization objects do not change that selection; a missing selected authorization never falls back to another transfer method.
For upto the requirements' extra.facilitatorAddress must be this
engine's signer address (the proxy only accepts the witness facilitator
as sender); other facilitators' payments are rejected with
upto_facilitator_mismatch. The settled amount is the requirements'
amount, which re-verification proves to be at most the signed
permitted.amount.
Fee-payer safety
The facilitator's signing key pays gas, so what it signs is structurally
constrained: settlement transactions are always built by this module with
to set to the verified requirements' asset (EIP-3009) or the fixed
x402 Permit2 proxy address (Permit2), value 0, and calldata produced
exclusively by X402.EIP3009.transfer_calldata/3 /
X402.Permit2.exact_settle_calldata/2 / X402.Permit2.upto_settle_calldata/3
from the authorization fields the signature verification just proved.
The single exception is ERC-6492 counterfactual settlement: the engine signs
caller-supplied calldata ONLY toward explicitly allowlisted factory
addresses (:eip6492_allowed_factories), capped by
:max_deploy_gas_limit — with the default empty allowlist it never does,
and counterfactual payments are rejected fail-closed at verify and at
settle's re-verify. Deployed ERC-1271 smart wallets are fully supported
either way.
Settlement pipeline
- Reconcile: with a
:pending_settlement_storeconfigured, a payload whose earlier attempt already broadcast a transaction is re-awaited instead of re-verified and re-broadcast. - Re-verify via
X402.Verify.EVMat:fulllevel (transfer simulation off by default — verify already simulated; see:simulate_in_settle— while the atomic ERC-6492 counterfactual simulation always runs, being the only possible proof of a counterfactual signature). - For a verified counterfactual payment whose wallet is still
undeployed, broadcast the wrapper's factory calldata (allowlist-gated,
:max_deploy_gas_limit-capped) and require a successful deploy receipt first. - Build the settlement calldata —
transferWithAuthorizationor the proxy'ssettle(shared with verification's simulation encoding). - One batched RPC round-trip:
eth_estimateGas(with a safety margin),eth_maxPriorityFeePerGas+eth_feeHistory(falling back toeth_gasPriceon nodes without EIP-1559 fee APIs), andeth_getTransactionCount(pending). - Encode the EIP-1559 transaction (
X402.Transaction), sign its keccak digest through the configuredX402.Signer(the signer must support raw digest signing —X402.Signer.LocalKeydoes; the 27/28 recovery id it returns is normalized to the EIP-1559yParity), and broadcast viaeth_sendRawTransaction. - Poll
eth_getTransactionReceiptuntil confirmation or timeout. A confirmed receipt must also carry the matching ERC-20Transferevent before success is reported. A broadcast whose confirmation cannot be established returns the spec's non-terminal"settlement_pending"with the transaction hash so callers can reconcile on chain, recording the attempt in the:pending_settlement_storewhen one is configured.
Hooks
X402.Hooks callbacks wrap both operations, mirroring the reference
facilitator's lifecycle hooks: before_* returning {:halt, reason}
turns into a rejected wire response (not an exception), after_* runs on
successful results and may replace them, and on_*_failure runs for both
rejected wire responses and infrastructure errors and may {:recover, result} with a replacement response. The internal re-verify inside
settle/3 runs without hooks — the settle hooks already wrap it.
Example
{:ok, rpc} = X402.RPC.new(rpc_url: "https://sepolia.base.org", finch: MyApp.Finch)
{:ok, signer} = X402.Signer.LocalKey.new(System.fetch_env!("PRIVATE_KEY"))
{:ok, engine} =
X402.Facilitator.Engine.new(
rpc: rpc,
signer: signer,
networks: ["eip155:84532"]
)
{:ok, %{"isValid" => true, "payer" => payer}} =
X402.Facilitator.Engine.verify(engine, payment_payload, requirements)
{:ok, %{"success" => true, "transaction" => tx_hash}} =
X402.Facilitator.Engine.settle(engine, payment_payload, requirements)Telemetry
Emits [:x402, :facilitator_engine, :verify] and
[:x402, :facilitator_engine, :settle] with :status metadata.
Summary
Payment Verification
Verifies a payment payload against requirements, returning the
POST /verify wire response.
Payment Settlement
Settles a payment, returning the POST /settle wire response.
Facilitator Discovery
Returns the GET /supported wire response for this engine.
Types
A facilitator wire response (/verify or /settle shape).
Payment Verification
@spec verify(t(), map(), map()) :: {:ok, wire_response()} | {:error, term()}
Verifies a payment payload against requirements, returning the
POST /verify wire response.
Returns {:ok, response} for every protocol-level outcome — including
rejected payments, which come back as
%{"isValid" => false, "invalidReason" => reason, "payer" => payer} with
the canonical cross-SDK reason string (X402.Verify.EVM.reason_string/1).
{:error, reason} is reserved for infrastructure failures (RPC transport
errors, missing crypto dependencies, hook crashes) where no verdict about
the payment exists; transports should map it to an opaque 500.
Payment Settlement
@spec settle(t(), map(), map()) :: {:ok, wire_response()} | {:error, term()}
Settles a payment, returning the POST /settle wire response.
Independently re-verifies the payment first (normative for the exact-EVM
scheme), then broadcasts transferWithAuthorization and awaits the
receipt — see the module documentation for the full pipeline. Rejected or
failed settlements come back as {:ok, %{"success" => false, "errorReason" => reason, ...}}; a broadcast whose confirmation could not
be established returns the non-terminal "settlement_pending" reason
with the transaction hash. {:error, reason} is reserved for
infrastructure failures where nothing was broadcast.
Facilitator Discovery
@spec supported(t()) :: wire_response()
Returns the GET /supported wire response for this engine.
One exact and one upto kind per configured network, no extensions,
and the signer's address under the eip155:* family. Resource servers
advertising upto requirements forward that signer address as
extra.facilitatorAddress — the engine only settles upto payments whose
witness binds it.
Examples
{:ok, engine} = X402.Facilitator.Engine.new(rpc: rpc, signer: signer, networks: ["eip155:84532"])
X402.Facilitator.Engine.supported(engine)
#=> %{
# "kinds" => [
# %{"x402Version" => 2, "scheme" => "exact", "network" => "eip155:84532"},
# %{"x402Version" => 2, "scheme" => "upto", "network" => "eip155:84532"}
# ],
# "extensions" => [],
# "signers" => %{"eip155:*" => ["0x..."]}
# }
Types
@type t() :: %X402.Facilitator.Engine{ eip6492_allowed_factories: [String.t()], gas_limit_margin_percent: non_neg_integer(), hooks: module(), max_deploy_gas_limit: pos_integer(), max_gas_limit: pos_integer(), networks: [String.t()], nonce_manager: GenServer.server() | nil, pending_settlement_store: X402.Facilitator.PendingSettlementStore.adapter() | nil, receipt_interval_ms: pos_integer(), receipt_timeout_ms: pos_integer(), rpc: X402.RPC.t(), signer: X402.Signer.t(), simulate: boolean(), simulate_in_settle: boolean(), verify_chain_id: boolean() }
A validated engine configuration built by new/1.
A facilitator wire response (/verify or /settle shape).
Functions
@spec new(keyword()) :: {:ok, t()} | {:error, NimbleOptions.ValidationError.t()}
Builds a validated engine configuration.
Options
:rpc- Required. AnX402.RPCconfiguration for the served network.:signer- Required. The fee-payer signer (a struct implementingX402.Signer). Its key pays settlement gas and must support signing raw 32-byte digests.:networks- Required. Non-empty list of CAIP-2 networks this engine serves (currentlyeip155:<chainId>only). Verify and settle requests for other networks are rejected withinvalid_network.:hooks- Lifecycle hook module implementingX402.Hooks. The default value isX402.Hooks.Default.:simulate(boolean/0) - Whetherverify/3simulatestransferWithAuthorizationviaeth_call. Even whenfalse, verify keeps the atomic ERC-6492 counterfactual simulation — the only possible proof of a counterfactual signature — so verify predicts settle, whose re-verify always keeps that proof too. The default value istrue.:simulate_in_settle(boolean/0) - Whether the independent re-verify insidesettle/3also simulates. Off by default, matching the reference facilitators — verify already simulated, andeth_estimateGasre-simulates right before broadcast. Even when off, the re-verify keeps the atomic ERC-6492 counterfactual simulation, the only possible proof of a counterfactual signature. The default value isfalse.:verify_chain_id(boolean/0) - Whether verification cross-checkseth_chainIdagainst the CAIP-2 network. The default value istrue.:gas_limit_margin_percent(non_neg_integer/0) - Safety margin added toeth_estimateGas(percent). The default value is20.:receipt_timeout_ms(pos_integer/0) - How longsettle/3waits for the transaction receipt before returning the non-terminal"settlement_pending"response. The default value is60000.:receipt_interval_ms(pos_integer/0) - Interval betweeneth_getTransactionReceiptpolls. The default value is1000.:nonce_manager- OptionalX402.Facilitator.NonceManager(pid/name) serializing fee-payer nonces. Without it, each settlement reads the pending nonce from the node, which races under concurrent settles — configure the manager for any deployment that settles concurrently. The default value isnil.:max_gas_limit(pos_integer/0) - Absolute gas ceiling per settlement transaction (margin included). A legitimatetransferWithAuthorizationcosts well under 100k gas; an estimate above this ceiling means the asset contract is burning the fee payer's gas and the settlement is refused — the fee payer never broadcasts unbounded-gas transactions against unvetted bytecode. The default value is200000.:eip6492_allowed_factories(list ofString.t/0) - Factory contract addresses (case-insensitive) trusted to deploy counterfactual ERC-6492 smart wallets during settlement. Threaded into verification so verify predicts settle. The default empty list keeps the fail-closed behavior: every counterfactual payment is rejected and the engine never signs caller-supplied factory calldata. The default value is[].:max_deploy_gas_limit(pos_integer/0) - Absolute gas ceiling for the ERC-6492 factory deployment transaction (margin included). Smart-account deployments legitimately cost far more than a transfer (~300k gas), so the deployment carries its own ceiling; the transfer keeps:max_gas_limit. The default value is600000.:pending_settlement_store- Optional{module, store}adapter implementingX402.Facilitator.PendingSettlementStore. When configured,settle/3records broadcasts whose confirmation could not be established and reconciles a retried payload against the already-broadcast transaction instead of broadcasting a second one. The default value isnil.