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
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
- 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
Agent identity and trust (KYA)
Verifiable agent identity (did:web + Ed25519), signed reputation, and the register / rotate / revoke key lifecycle. Know Your Agent for the money side.
Router observability
Operator walkthrough for /dashboard/router — per (provider × canonical_tool) attempts, success rate, p50 latency, last error, and a 24h sparkline.