Facilitator API.
The five endpoints of x402.aeron.sh: verify, settle, supported, status, healthz — with every field, every refusal reason, and what to do about each.
The facilitator is one Cloudflare Worker with five endpoints and no state. It settles the x402 exact scheme on two rails — USDG on Robinhood Chain and USDC on Arc — and it is deliberately strict: every check below is a refusal, not a warning.
A payment says which rail it belongs to. /verify and /settle read the network in paymentRequirements and answer on that chain; nothing is inferred from the hostname, and a network this facilitator does not settle is refused by name. If you want one chain and one answer, https://x402-arc.aeron.sh is the same service with Arc alone.
- Base URL
- https://x402.aeron.sh
- Rails
- eip155:4663 — USDG · eip155:5042 — USDC on Arc
- Auth
- none until 30 September 2026, credits after
- CORS
- not sent — call it server-side
- Content type
- application/json on POST
| Endpoint | Purpose |
|---|---|
GET /supported | The scheme and every network this facilitator settles. |
GET /status | Every rail: network, asset, relayer address, and the gas it has left. |
GET /healthz | Liveness, and whether settlement is enabled. |
POST /verify | Is this authorization real, unspent, and funded? No state changes. |
POST /settle | Verify, then submit the transfer on chain and return the hash. |
GET /supported
curl -s https://x402.aeron.sh/supported
{
"kinds": [
{"x402Version":2,"scheme":"exact","network":"eip155:4663"},
{"x402Version":2,"scheme":"exact","network":"eip155:5042"}
],
"extensions": [],
"signers": {"eip155:*":["0x2b94e4188da990EcF84d13A5AbE69BC9523015A7"]}
}One eip155:* entry means one relayer signs on every rail. A rail with its own signer is listed under its own network id instead, and a rail running verify-only is not listed at all — signers names who actually submits, which is what Aeronscan reads to attribute a settlement.
GET /status
The flat fields describe the first rail and are unchanged; networks carries all of them. Each rail has its own gas budget in whatever that chain charges gas in — ether on Robinhood Chain, USDC on Arc, since on Arc the dollar is the native asset. When one reaches zero, /settle fails on that rail and buyers see payment failures, so alert on every relayerNative, not just the top-level number.
curl -s https://x402.aeron.sh/status
{
"name": "Aeron Facilitator",
"network": "eip155:4663", // the first rail, for monitors that predate the second
"chain": "Robinhood Chain",
"asset": "0x5fc5360d0400a0fd4f2af552add042d716f1d168",
"assetSymbol": "USDG",
"scheme": "exact",
"settlement": "enabled", // or "verify-only"
"relayer": "0x2b94e4188da990EcF84d13A5AbE69BC9523015A7",
"relayerNative": "0.100773054387092",
"gasSymbol": "ETH",
"relayerEth": "0.100773054387092", // the same number under its old name
"networks": [
{
"network": "eip155:4663", "chain": "Robinhood Chain",
"asset": "0x5fc5360d0400a0fd4f2af552add042d716f1d168", "assetSymbol": "USDG",
"settlement": "enabled", "relayer": "0x2b94e4188da990EcF84d13A5AbE69BC9523015A7",
"relayerNative": "0.100773054387092", "gasSymbol": "ETH",
"explorer": "https://explorer.chain.robinhood.com"
},
{
"network": "eip155:5042", "chain": "Arc",
"asset": "0x3600000000000000000000000000000000000000", "assetSymbol": "USDC",
"settlement": "enabled", "relayer": "0x2b94e4188da990EcF84d13A5AbE69BC9523015A7",
"relayerNative": "4.812331", "gasSymbol": "USDC",
"explorer": "https://explorer.arc.io"
}
],
"endpoints": ["/supported","/verify","/settle","/status","/healthz"]
}POST /verify
Body is the same for both POST endpoints:
{
"x402Version": 1,
"paymentPayload": {
"x402Version": 1,
"scheme": "exact",
"network": "eip155:4663",
"payload": {
"signature": "0x…",
"authorization": {
"from": "0x7a1c…9e42",
"to": "0x3fd0…b118",
"value": "4000",
"validAfter": "1756703940",
"validBefore": "1756704600",
"nonce": "0x9f3c…d201"
}
}
},
"paymentRequirements": {
"scheme": "exact",
"network": "eip155:4663",
"maxAmountRequired": "4000",
"payTo": "0x3fd0…b118",
"asset": "0x5fc5360d0400a0fd4f2af552add042d716f1d168"
}
}Fields
| Field | Rule |
|---|---|
payload.signature | 0x + 130 hex characters. Must recover to authorization.from under the token’s EIP-712 domain. |
authorization.from | The payer. Must hold at least value of that rail’s asset at verification time. |
authorization.to | Must equal paymentRequirements.payTo, case-insensitive. |
authorization.value | Decimal string, atomic units. Must equal maxAmountRequired exactly. |
authorization.validAfter | Unix seconds, strictly in the past. Sign it as now - 60; equal to now is refused. |
authorization.validBefore | Unix seconds, strictly in the future. Leave room for settlement — the reference wallet uses now + maxTimeoutSeconds + 540. |
authorization.nonce | 0x + 64 hex characters, random. Checked against the token’s authorizationState; single use per payer. |
network | eip155:4663 or eip155:5042, in the payload and in the requirements. The requirements pick the rail; a payload naming a different one is refused, not reconciled. |
asset | The asset of the network you named: USDG on eip155:4663, USDC at 0x3600…0000 on eip155:5042. Any other token is refused, including the right token on the wrong rail. |
paymentRequirements tolerates extra keys, so you can send the whole offer you published in your 402 — resource, description, mimeType, maxTimeoutSeconds, extra are carried and ignored.
Response
{ "isValid": true, "payer": "0x7a1c…9e42" }
{ "isValid": false, "invalidReason": "insufficient USDG balance" } // no payer key on a refusalRefusal reasons
Every one is returned verbatim in invalidReason. They are worth surfacing to your caller: an agent operator reading a log needs to know whether to retry, top up, or fix the seller.
| Reason | Means |
|---|---|
unsupported network eip155:8453; this facilitator settles eip155:4663, eip155:5042 | No rail here settles that chain. The list in the message is the live one — a rail whose RPC is down is left out of it. |
wrong network; expected eip155:5042 | The requirements chose a rail and the payload named a different one. |
unsupported asset; eip155:5042 settles USDC only | asset is not that rail’s token. USDG on the Arc rail lands here. |
authorization.to does not match payTo | The buyer signed a transfer to a different recipient than the one you asked for. |
authorization.value does not match maxAmountRequired | Amount mismatch. Exact means exact. |
authorization not valid yet | validAfter is now or later. Backdate it by a minute. |
authorization expired | validBefore has passed. The buyer must sign again. |
invalid signature | The signature does not recover to from — usually a wrong EIP-712 domain or a mutated field. |
authorization nonce already used | That authorization has been spent. Do not deliver twice on it. |
insufficient USDG balance | The payer cannot cover it. Top up and sign again. The Arc rail says insufficient USDC balance. |
malformed request body | Returned with HTTP 400 when the body fails schema validation. |
POST /settle
Settlement re-verifies first, then submits transferWithAuthorization from the relayer and waits for the receipt. Allow up to 60 seconds; a short client timeout can abort a payment that in fact settled.
{
"success": true,
"transaction": "0xca33d2264843720330a8ace2d93a98fd3f7ef52d4711e9e48f3f3bab48d84dce",
"network": "eip155:4663",
"payer": "0x7a1c…9e42"
}// verification failed: nothing was submitted, the authorization is unspent
{ "success": false, "transaction": null, "network": "eip155:4663",
"errorReason": "authorization nonce already used" }errorReason | Means |
|---|---|
| Any refusal reason above | Verification failed inside settle. Nothing was submitted. |
settlement disabled: no relayer key configured | This facilitator is running verify-only. Check /status. |
transaction reverted | Submitted and mined, but the token rejected it. transaction holds the hash — read the revert on the explorer. |
| anything else | The RPC or relayer error, verbatim. Out of gas at the relayer surfaces here. |
Status codes
| Code | When |
|---|---|
200 | The request was well formed. A refusal is a 200 with isValid: false or success: false — read the body, not the status. |
400 | The body failed schema validation. |
404 | Unknown path, or the wrong method for a known one. |
503 | {"error":"facilitator unavailable"} — chain setup failed on a cold isolate, usually an RPC problem. Retry. |
The EIP-712 domains
Only needed if you sign authorizations yourself. One per rail — a domain is a property of the token on its chain, not of this service — and pinned rather than discovered, because rediscovering it on every cold isolate is enough to trip a public RPC’s rate limit. Sign against the domain of the chain you are paying on; the other one recovers to a different address and the payment reads as forged.
{
"name": "Global Dollar",
"version": "1",
"chainId": 4663,
"verifyingContract": "0x5fc5360d0400a0fd4f2af552add042d716f1d168"
}{
"name": "USDC",
"version": "2",
"chainId": 5042,
"verifyingContract": "0x3600000000000000000000000000000000000000"
}Verify it yourself against the contract’s own DOMAIN_SEPARATOR rather than trusting this page: npm run check:token in the facilitator repo does exactly that.
Run your own
The facilitator is MIT licensed and deploys as a single Worker: aeronlabs/aeron-facilitator. A chain is a preset there, and NETWORKS="robinhood,arc-mainnet" is how one Worker serves both. You need a relayer key with a little gas on each chain you serve — ether on Robinhood Chain, USDC on Arc — and, in practice, a keyed RPC endpoint: the public Robinhood RPC meters by IP, and Cloudflare’s egress is shared, so a Worker on the public endpoint gets rate-limited into 503s.
Running your own is also the answer if you want a gas policy other than the one here. Aeronscan indexes settlements by the address that submitted them, so your facilitator appears under its own name, with its real numbers, without registering anywhere.
Updated 20 September 2026. Everything on this page is read from the running services; report a drift at github.com/aeronlabs.