Skip to main content

Facilitator

Ingest an externally-signed x402 (EIP-3009) authorization, verify it, translate it onto an internal mandate, execute it under the cap gate, and seal a receipt.

3 min read
View MarkdownEdit on GitHub

Facilitator

Why it exists

Not every authorization is minted inside CodeSpar. An agent elsewhere may sign a payment authorization with its own wallet key. The facilitator accepts that externally-signed authorization, proves it, and then runs it through the same governance every internal spend goes through: cap gate, execution, and a sealed receipt.

What it does

The facilitator takes an x402 payment authorization (an EIP-3009 transferWithAuthorization) that was signed outside CodeSpar by a foreign wallet, plus an execution intent, and runs one pipeline:

  1. Verify the foreign signature. x402 "exact" is an EIP-712 typed-data signature over the token's own domain, so it verifies offline by recovering the signer and checking it equals the declared sender. No chain call, no CodeSpar secret. The trust root here is the foreign wallet's key, not our HMAC.
  2. Bind the payee. The execution intent must settle to exactly the on-chain recipient the signature authorized.
  3. Guard against replay. A given (signer, nonce) executes once.
  4. Translate onto an internal mandate. The foreign authorization becomes a normal internal mandate, so the rest of the stack treats it like any other spend.
  5. Run the cap gate. The existing mandate verifier applies the per-transaction and running caps, unchanged.
  6. Settle and seal. The transfer settles on the x402 rail, and a Control Record receipt is sealed with a rail-native delivery proof (the settlement transaction hash).

The point is that a foreign-signed authorization gets the same cap enforcement, the same audit trail, and the same receipt as an authorization minted here.

Endpoint

POST /v1/facilitator/x402/executions

Authenticated in the /v1 dual scope (bearer API key or dashboard service auth). The org, project, and environment come from the authenticated context. See Authentication.

Request

{
  "authorization": {
    "from": "0xPayerAddress",
    "to": "0xRecipientAddress",
    "value": "1500000",
    "validAfter": "0",
    "validBefore": "1893456000",
    "nonce": "0x<32-byte-hex>"
  },
  "domain": {
    "name": "USD Coin",
    "version": "2",
    "chainId": 84532,
    "verifyingContract": "0xTokenContract"
  },
  "signature": "0x<65-byte-signature>",
  "intent": {
    "payee": "0xRecipientAddress",
    "resource": "data-api.example/v1/rates",
    "purpose": "market-data",
    "max_amount_minor": 2000,
    "total_cap_minor": 50000,
    "resource_url": "https://data-api.example/v1/rates"
  }
}
  • authorization is the raw EIP-3009 fields. value is atomic token units and the numeric fields are decimal strings (a uint256 does not fit a JS number).
  • domain is the token's own EIP-712 domain, taken explicitly so the verifier works for any EIP-3009 token you declare.
  • intent.payee must equal authorization.to. max_amount_minor and the optional total_cap_minor are the caps enforced by the gate, in minor units.

Response

200 OK:

{
  "status": "executed",
  "foreign_authorization_id": "fauth_…",
  "mandate_id": "…",
  "execution_id": "…",
  "signer": "0xPayerAddress",
  "settlement": {
    "tx_hash": "0x…",
    "network": "base-sepolia",
    "money_moved": true,
    "adapter": "x402-facilitator-base-sepolia"
  },
  "receipt": { "id": "…", "state": "…", "chain": "…" }
}

A companion read endpoint, GET /v1/facilitator/x402/executions/:id, returns a stored authorization and its outcome (status, reject code, linked mandate, execution, receipt, and settlement transaction).

Error cases

Each pre-settlement refusal is recorded as evidence and mapped to a status code.

CodeHTTPWhen
missing_required_field400The body does not match the schema
malformed_authorization400An address or field is not well formed
bad_signature401The signature does not recover a valid signer
signer_mismatch401The recovered signer is not the declared from
authorization_not_yet_valid422Submitted before validAfter
authorization_expired422Submitted after validBefore
payee_authorization_mismatch422intent.payee is not the on-chain to
amount_below_minimum422The authorized value is below the minimum settleable amount
authorization_replayed409This (signer, nonce) already executed
over-cap / purpose denial403The translated mandate was refused by the cap gate
settlement_failed502The on-chain settlement submission failed

Honesty about settlement

Today, facilitator execution settles on testnet rails only. The executor selects the network from the request environment (a test-mode request settles on a test network), and the v1 settlement client refuses anything other than that test network, so a demo can never move mainnet or production funds. Production settlement, including Pix, is a separate and deliberately gated track that is not enabled in v1. The money_moved flag in the response tells you whether a real transfer settled on the rail that ran.

Roadmap

The facilitator is neutral by design: it verifies a foreign authorization and translates it onto an internal mandate, and everything after that (cap gate, execution, receipt) is shared machinery. The v1 accepts x402 inbound. The same pipeline is the intended ingestion path for other agent-payment protocols (AP2 and ACP), which would add their own signature-verification and translation front ends and reuse the rest unchanged.

See also

Facilitator | CodeSpar