x402 facilitator
Arclight runs an open x402 v2 facilitator on Arc. A seller returns HTTP 402 with payment requirements, the buyer signs a USDC or EURC authorization (EIP-3009), and the facilitator verifies it and settles it on chain. Both assets have 6 decimals, so amounts are in 6-decimal units either way.
Endpoints
Payment requirements for Arc
This is what your server puts in the 402 response. The extra block is the EIP-712 domain of the asset you are charging in, and it must match that asset: the facilitator refuses a crossed pair before it reads the chain.
{
"scheme": "exact",
"network": "eip155:5042",
"asset": "0x3600000000000000000000000000000000000000",
"amount": "10000",
"payTo": "0xYourReceivingAddress",
"maxTimeoutSeconds": 60,
"extra": { "name": "USDC", "version": "2" }
}To charge in euros instead, change those two fields and nothing else. EURC has its own domain name, so a EURC address with USDC domain parameters is rejected.
"asset": "0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1",
"extra": { "name": "EURC", "version": "2" }Seller in two lines
Any x402 v2 server SDK works. Set the facilitator URL and the network; the SDK builds the requirements and calls verify and settle for you.
import { HTTPFacilitatorClient } from '@x402/core/server'
import { ExactEvmScheme } from '@x402/evm/exact/server'
import { paymentMiddlewareFromConfig } from '@x402/hono'
const facilitator = new HTTPFacilitatorClient({ url: 'https://x402.arclight.finance' })
app.use('/api/answer', paymentMiddlewareFromConfig(
{
'GET /api/answer': {
accepts: {
scheme: 'exact',
network: 'eip155:5042',
payTo: '0xYourReceivingAddress',
// 10000 = 0.01 USDC, 6 decimals. For euros: asset 0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1
// and extra { name: 'EURC', version: '2' } — the domain must match the asset.
price: { amount: '10000', asset: '0x3600000000000000000000000000000000000000' },
maxTimeoutSeconds: 60,
extra: { name: 'USDC', version: '2' },
},
description: 'One answer',
mimeType: 'application/json',
},
},
facilitator,
[{ network: 'eip155:5042', server: new ExactEvmScheme() }],
))Buyer: an agent paying for a call
An agent never talks to the facilitator. It reads the seller's 402, signs one authorization for the asset the seller asked for, and asks again with the signature. The live demo does exactly this from a wallet.
- An EVM key holding USDC, or EURC, on Arc (chain id 5042, RPC https://rpc.mainnet.arc.io). Nothing else: no native gas, no approval, no deposit.
- A seller whose 402 lists network eip155:5042. The Arclight API demo above is one, priced at 0.001 USDC per call.
- Nothing from us. A buyer needs no key, no signup and no allowlist; the seller points at the facilitator, you never call it.
import { wrapFetchWithPayment, x402Client } from '@x402/fetch'
import { ExactEvmScheme } from '@x402/evm/exact/client'
import { privateKeyToAccount } from 'viem/accounts'
// A key that holds USDC on Arc (chain 5042). No gas needed: the facilitator pays it.
const signer = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`)
const client = new x402Client().register('eip155:*', new ExactEvmScheme(signer))
// Paying in EURC? Client spend controls recognise USD-pegged assets only, so opt in once:
// client.setSpendControls({ allowedAssets: [{ network: 'eip155:5042', asset: '0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1' }] })
const fetchWithPayment = wrapFetchWithPayment(fetch, client)
// Any x402 seller whose 402 lists network eip155:5042. This one costs 0.001 USDC.
const res = await fetchWithPayment('https://api.arclight.finance/v1/x402/demo')
console.log(res.status, await res.json())
console.log(res.headers.get('payment-response')) // base64 { success, transaction, network, payer }# 1. Ask. The seller answers 402 with the price in a header.
curl -si https://api.arclight.finance/v1/x402/demo | grep -i payment-required
# PAYMENT-REQUIRED: base64 JSON { x402Version: 2, accepts: [ { scheme, network, asset, amount, payTo, maxTimeoutSeconds, extra } ] }
# 2. Pick the entry with network "eip155:5042" and sign it as EIP-712 (no transaction, no gas).
# domain { name: <extra.name>, version: "2", chainId: 5042, verifyingContract: <asset> }
# Take the name from the entry's own extra block: "USDC" for 0x3600…0000,
# "EURC" for 0xbef5…21c1. A crossed pair is refused.
# primaryType TransferWithAuthorization
# types { TransferWithAuthorization: [ from address, to address, value uint256,
# validAfter uint256, validBefore uint256, nonce bytes32 ] }
# message { from: <you>, to: <payTo>, value: <amount>, validAfter: 0,
# validBefore: now + maxTimeoutSeconds, nonce: 32 random bytes }
# 3. Ask again with the signature. Decimal strings for the numbers.
# PAYMENT-SIGNATURE: base64 JSON { x402Version: 2, accepted: <that entry>,
# payload: { signature: "0x…", authorization: { from, to, value, validAfter, validBefore, nonce } } }
# 4. 200 with the content. The receipt is in a header.
# PAYMENT-RESPONSE: base64 JSON { success: true, transaction: "0x…", network: "eip155:5042", payer: "0x…" }Buyer: a NEAR account, with no Arc key
NEAR's MPC network controls one root key and signs for a derived key only when the call comes from the NEAR account that derives it. A NEAR account can therefore spend USDC at an Arc address that no private key anywhere corresponds to. The facilitator sees an ordinary secp256k1 signature, so nothing on this side changes.
- A NEAR account with a little NEAR for the signing call. Mainnet or testnet both work; only the derived address differs.
- USDC on Arc at the derived address. No Arc private key is involved, and no native gas: Arclight still pays it.
- Nothing registered anywhere. The address is derived offline, so you can show a user where to deposit before the first NEAR call.
import { Account, JsonRpcProvider, KeyPairSigner } from 'near-api-js'
import { secp256k1 } from '@noble/curves/secp256k1'
import { sha3_256 } from '@noble/hashes/sha3'
import { hashTypedData, keccak256 } from 'viem'
// 1. Which Arc address this NEAR account controls. Pure maths, no network call.
// Note SHA3-256, not the keccak256 used elsewhere in Ethereum.
const ROOT = 'secp256k1:3tFRbMqmoa6AAALMrEFAYCEoHcqKxeW38YptwowBVBtXK1vo36HDbUWuR6EZmoK4JcH6HDkNMGGqP1ouV7VZUWya'
const path = 'arclight-x402-1'
const epsilon = BigInt('0x' + Buffer.from(
sha3_256(`near-mpc-recovery v0.1.0 epsilon derivation:${accountId},${path}`),
).toString('hex'))
const root = secp256k1.ProjectivePoint.fromHex(base58ToHex(ROOT)) // 04 || x || y
const pub = secp256k1.ProjectivePoint.BASE.multiply(epsilon).add(root)
const from = `0x${keccak256(`0x${pub.toHex(false).slice(2)}`).slice(-40)}`
// 2. The usual EIP-3009 authorization, and the digest the seller's 402 asks you to sign.
const authorization = { from, to: payTo, value: '1000', validAfter: '0',
validBefore: String(Math.floor(Date.now() / 1000) + 300), nonce: randomNonce() }
const digest = hashTypedData({
domain: { name: 'USDC', version: '2', chainId: 5042, verifyingContract: USDC },
types: { TransferWithAuthorization: [
{ name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' },
] },
primaryType: 'TransferWithAuthorization',
message: { ...authorization, value: 1000n, validAfter: 0n, validBefore: BigInt(authorization.validBefore) },
})
// 3. Ask the MPC network to sign it. Costs 1 yoctoNEAR plus gas; takes a couple of seconds.
const account = new Account(accountId, new JsonRpcProvider({ url: 'https://rpc.mainnet.near.org' }),
KeyPairSigner.fromSecretKey(nearPrivateKey))
const res = await account.callFunction({
contractId: 'v1.signer', methodName: 'sign',
args: { request: { path, payload_v2: { Ecdsa: digest.slice(2) }, domain_id: 0 } },
gas: 250000000000000n, deposit: 1n, waitUntil: 'EXECUTED',
})
// 4. NEAR returns big_r/s/recovery_id. Ethereum wants r|s|v, with s in the low half.
const signature = toEthSignature(res) // see apps/facilitator/src/tools/near-signature.ts
// Send it as PAYMENT-SIGNATURE exactly as any other buyer would.Limits
- Verify: 10 a second per IP.
- Settle: 2 a second per IP, 60 a minute per receiving address, 20 a minute per payer.
- Body: 32 KB.
- One settle at a time per authorization; a duplicate while the first is in flight answers invalid_transaction_state.
Pricing
- One credit is $0.001, so 1 USDC buys 1,000 credits.
- A settle costs the gas it actually used, times 1.30. The seller pays it; the buyer pays only the seller’s price.
- Every receiving wallet gets 1,000 free credits, once, not monthly. No key is needed while they last.
- After that, a settle draws on prepaid credits held by an API key, sent as Authorization: Bearer <key>. A key is only drawn on when it is presented, so nobody else can spend it by settling to your address.
- Credits are bought over x402 itself at POST /credits/buy?usdc=N, from 1 to 1,000 USDC a purchase. Minimum $1, non-refundable. Without a key it returns a new one; with Authorization: Bearer <key> it tops that key up.
- Out of credits, /settle answers 403 with errorReason free_tier_exhausted (no key, or an unknown one) or credits_exhausted (a key at zero).
- Free and staked settles share a global daily gas budget; past it they answer 503 until midnight UTC. Key-billed settles skip that budget.
# 1. Ask for credits. The answer is a 402, like any seller's.
POST https://x402.arclight.finance/credits/buy?usdc=5
# 402 · PAYMENT-REQUIRED: exact, eip155:5042, amount "5000000", payTo = the Arclight treasury
# 2. Sign that requirement and ask again, as any x402 buyer does.
POST https://x402.arclight.finance/credits/buy?usdc=5
PAYMENT-SIGNATURE: <base64 payment payload>
# 200 · { "key": "ak_…", "credits": 5000 } the key is shown once
# 3. Send the key on settle once the wallet's free credits are spent.
POST https://x402.arclight.finance/settle
Authorization: Bearer ak_…Staked sellers settle free
Locking ARCLIGHT gives the address being paid a daily allowance of settlements that spend no credits. Past the allowance, a staked seller falls through to its free credits, then to its key. Buyers are never asked for the token, and no tier is needed to be paid.
- Nothing staked: no daily allowance, only the 1,000 free credits.
- 250,000 ARCLIGHT: 500 a day.
- 1,000,000 ARCLIGHT: 1,500 a day.
- 5,000,000 ARCLIGHT: no per-seller cap.
Stake on the staking page, or call ArclightStaking yourself. It holds the tokens and answers dailyAllowanceOf; the facilitator reads that view and obeys it, so the rule lives on chain rather than on our server. Unstaking returns everything after a 7-day cooldown and drops the tier the moment it starts. Nothing is minted, no fee is taken, and the stake is never ours. The daily gas budget in /health still binds first: past it, free and staked settles answer 503 until midnight UTC.
What the facilitator never does
- Hold your funds. The transfer goes from the payer to the seller in one call; our wallet fronts the gas and bills the seller in credits.
- Retry a reverted settle. A revert, including one from Arc’s protocol blocklist, is reported once as a failure.
- Ask for a signup. Point your server at the URL and start; a key is needed only once a wallet’s free credits are spent.
- Charge the buyer. Settlement is billed to the seller; the buyer pays only the seller’s price.
Status: x402.arclight.finance/health. EURC and the upto scheme are live.