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.
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:
- 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.
- Bind the payee. The execution intent must settle to exactly the on-chain recipient the signature authorized.
- Guard against replay. A given
(signer, nonce)executes once. - 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.
- Run the cap gate. The existing mandate verifier applies the per-transaction and running caps, unchanged.
- 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"
}
}authorizationis the raw EIP-3009 fields.valueis atomic token units and the numeric fields are decimal strings (a uint256 does not fit a JS number).domainis the token's own EIP-712 domain, taken explicitly so the verifier works for any EIP-3009 token you declare.intent.payeemust equalauthorization.to.max_amount_minorand the optionaltotal_cap_minorare 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.
| Code | HTTP | When |
|---|---|---|
missing_required_field | 400 | The body does not match the schema |
malformed_authorization | 400 | An address or field is not well formed |
bad_signature | 401 | The signature does not recover a valid signer |
signer_mismatch | 401 | The recovered signer is not the declared from |
authorization_not_yet_valid | 422 | Submitted before validAfter |
authorization_expired | 422 | Submitted after validBefore |
payee_authorization_mismatch | 422 | intent.payee is not the on-chain to |
amount_below_minimum | 422 | The authorized value is below the minimum settleable amount |
authorization_replayed | 409 | This (signer, nonce) already executed |
| over-cap / purpose denial | 403 | The translated mandate was refused by the cap gate |
settlement_failed | 502 | The on-chain settlement submission failed |
Honesty about settlement
The executor picks the settlement network from the key's environment, before any signature work: a test-mode request settles on Base Sepolia and nothing can override that, and a live request settles on Base mainnet only where the deployment sets X402_MAINNET_ENABLED. Without the flag a live settlement is refused with 501 x402_mainnet_not_enabled rather than downgraded to testnet, and any network other than those two is refused outright (settlement_network_refused). Mainnet settlement also carries a hard per-payment ceiling (X402_MAINNET_MAX_USDC, default 20 USDC) enforced before the transfer, on top of the mandate cap, and uses a separate relayer key so funding the sell side is its own operator decision. On CodeSpar's hosted production the flag is on. Non-x402 production settlement, including Pix, is a separate track. 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
- Agent identity and trust (KYA) - verifiable identity and the offline mandate check
- Wallets - internal mandates and the cap gate the facilitator reuses
- Audit chain - where the sealed receipt lands
- Crypto pay - the internal x402 spend path