Status: IN DEVELOPMENT. Integration code and tests only. No payment component is deployed and no facilitator
runs. The ARL token is deployed on Base Sepolia (0x244312b619127B6458154F3467eFD7c87CD28500);
it is not on Base Mainnet, and Base Mainnet is refused by the code.
ARL payments let an AI or compute service charge per use in ARL. They use the upto scheme of
the x402 protocol: the payer signs a ceiling, the service meters actual usage, and the
facilitator settles the metered amount, never more than the ceiling.
Path
payer wallet ── signs Permit2 witness (ceiling, payee, facilitator, nonce, deadline)
│ and, first time only, an ARL EIP-2612 permit approving Permit2
▼
facilitator ── x402UptoPermit2Proxy.settle / settleWithPermit(amount ≤ ceiling)
│
▼
Permit2.permitWitnessTransferFrom ── ARL.transferFrom(payer → payee, amount)
ARL's EIP-2612 permit is only used to approve Permit2 (x402's eip2612GasSponsoring); the payment
itself is always the Permit2 witness transfer.
External contracts and pins
| Item | Value |
|---|---|
| Network | Base Sepolia, CAIP-2 eip155:84532. Base Mainnet (8453) refused by paymentNetwork |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3, Base Sepolia code hash 0xdcde65555316946c298e4c60c6213eb5c3aeab4354d1f3fac5427236bcbb9ebe |
| x402UptoPermit2Proxy | 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002, code hash 0x4662dc27323421a3698be49ac95f7b0dba141c238d31ef543248d1a11f8d8eec (same on Base Sepolia and Base Mainnet; identical to a build of the source below) |
| x402 source | x402-foundation/x402 @ 71eb9a55e081e7b81ba3046d0bd17c3eb9c7bf81 (npm @x402/core and @x402/evm 2.27.0), Apache-2.0; contracts MIT |
| Reviews of the proxies | Cantina, Feb 2026 (commit c0a80b76: 3 low, 4 informational, all fixed) and Mar 2026 (commit 79136b97: 1 informational on the exact proxy, acknowledged). The May 2026 report covers only the separate batch-settlement contract. Reports are published in the x402 repository |
| Review coverage check | 79136b97 is the first commit of x402 PR #1322 (refs/pull/1322/head), squash-merged as 713907fc. The proxy sources and ISignatureTransfer.sol have the same git blob hashes at 79136b97 and at 71eb9a55 (base 46c6c886, upto 236e9e85), and the library pins match. foundry.toml changed only in metadata settings (cbor_metadata, bytecode_hash), a pinned evm_version = "cancun" and RPC entries; the deployed bytecode matches a build at 71eb9a55 |
| Fork block | Base Sepolia 47,419,967 |
assertPinnedCode checks both code hashes before use.
Guarantees
Enforced by the proxy and Permit2 (tested on the fork, contracts/test-fork/):
- settlement ≤ signed ceiling (
AmountExceedsPermitted); zero settlement reverts (InvalidAmount); - payee, facilitator, token, ceiling, nonce and deadline are covered by the payer's signature;
- only the named facilitator can settle (
UnauthorizedFacilitator); - each Permit2 nonce is usable once (
InvalidNonce); the payer can revoke withinvalidateUnorderedNonces; - the Permit2 domain includes the chain id, so a signature for another chain fails.
Added by @arl/payments (packages/payments):
- Network gate: Base Sepolia only; Base Mainnet refused, no override.
- Asset and spender pin: only ARL, only through the pinned upto proxy.
- One settlement per authorization: a shared
AuthorizationStorerecords every authorization (payer + nonce) assettling,settled,retiredorfailed, claimed before anything is sent, so concurrent or repeated requests settle at most once. - Zero settlement: the SDK returns success without a transaction, which leaves the Permit2
nonce unused and the ceiling spendable until its deadline. ARL records it as
retiredand never settles it afterwards. - Short windows: requirements allow 30–600 seconds; authorizations expiring more than 600 seconds ahead, or already expired, are refused.
- Failures are not retried with the same authorization; the payer signs a new one.
Usage receipts
The ceiling bounds what a payer can lose; the amount settled under it is the service's metering. A
usage receipt (src/receipt.ts) makes that metering a signed claim the payer can check.
For every call, the service signs an EIP-712 UsageReceipt (domain ARL Usage Receipt, version
1, chain id) with:
| Field | Meaning |
|---|---|
payer, payTo, token |
From the Permit2 authorization charged |
nonce, ceiling, deadline |
The authorization's Permit2 nonce, signed ceiling and deadline |
resource |
keccak256 of the resource identifier (job?seconds=n) |
units, unitPrice |
What the service counted and its price per unit |
amount |
min(units × unitPrice, ceiling): the amount settled |
response |
keccak256 of the response bytes returned to the payer |
- Advertised signer. The service names its receipt signer in the 402 requirements
(
extra.receiptSigner), before the payer signs anything. - Facilitator gate.
ArlUptoFacilitatortakesreceiptSigners(payee → signer). For a listed payee it settles only whenrequirements.extra.usageReceiptis signed by that signer, matches the authorization field for field, states exactly the amount being settled, and that amount is min(units × unitPrice, ceiling). Payees without a signer settle as before. - Payer check.
checkUsageReceiptchecks the same against the authorization the payer signed, the amount moved on chain and the response bytes received. The receipt is the payer's evidence.
What a receipt does not do: it does not prove the units were really used. It makes the service's claim attributable, bound to one authorization and one response, and reproducible from units × price, so a wrong charge is signed evidence against the service.
Facilitator service
createFacilitatorServer (src/service.ts) serves the facilitator over the x402 facilitator API.
That is the API the SDK's HTTPFacilitatorClient calls, so any x402 resource server can use
it, including @arl/provider:
| Endpoint | Does |
|---|---|
GET /supported |
The one kind ARL settles (x402 v2, upto, Base Sepolia) and the settling address |
POST /verify |
{ x402Version, paymentPayload, paymentRequirements } → VerifyResponse |
POST /settle |
The same body → SettleResponse |
- Every request goes through
ArlUptoFacilitator. - Bodies are shape-checked with the SDK's own schemas.
- Settling spends the facilitator's gas, so
/verifyand/settlecan require a bearer token that only the operator's resource servers hold. The token is compared in constant time. - Error details are not echoed to the caller.
FileAuthorizationStore (src/store.ts) keeps the settlement record on disk:
- It appends every state change to a file and flushes it to disk before acknowledging.
- It replays the file at start, so a restarted facilitator never settles an authorization twice.
- A settlement that was in progress when the process stopped stays recorded as
settlingand is never sent again. - A last record cut short by a crash is dropped (it was never acknowledged). A damaged record anywhere else stops the start-up.
- It is for one process; several instances need a shared store with compare-and-set.
The service is tested, but no instance runs, and the settlement account is the operator's (see below).
Known limitations
- Facilitator trust. The facilitator chooses the settled amount (up to the ceiling). Payers trust the service's metering for that amount; the ceiling bounds the loss. With usage receipts, that amount is a signed, checkable claim, but still the service's own measurement.
- Residual allowance. An EIP-2612 permit approves Permit2 for the whole ceiling; after a partial settlement the rest stays approved to Permit2 (usable only with a new valid Permit2 signature from the payer).
- Shared store.
InMemoryAuthorizationStoreis for tests.FileAuthorizationStoresurvives restarts of one process. Several facilitator instances must share a durable store with compare-and-set semantics, for example a database row with a unique key. - Facilitator key and gas. Running a facilitator needs a signing key with ETH for gas on Base Sepolia. Not created; an owner decision.
arlTokenmust come from the deployment manifest (Base Sepolia:0x244312b619127B6458154F3467eFD7c87CD28500).
Tests
| Suite | Command | Covers |
|---|---|---|
| Unit | npm test -w @arl/payments |
network gate, usage receipts (signing, recovery, every mismatch, malformed receipts, the facilitator gate), pinned-code check, requirements, metering, settlement policy, facilitator wrapper (with a stand-in SDK scheme), file store across restarts and crashes, the HTTP service through the SDK's HTTPFacilitatorClient (bearer token, malformed bodies) |
| Contract fork | FOUNDRY_PROFILE=fork ARL_FORK_RPC=<Base Sepolia RPC> forge test in contracts/ |
the 15 agreed cases against the real Permit2 and proxy, fuzzing both sides of the ceiling, EIP-2612 → Permit2 (gasless, front-run, value mismatch), pinned code |
| SDK end-to-end fork | ARL_FORK_RPC=<Base Sepolia RPC> npm run test:fork -w @arl/payments (needs forge build and anvil) |
real @x402/evm client and facilitator through ArlUptoFacilitator on a local Anvil fork: verify, metered settlement, replay refusal, ceiling, zero retirement, tampered payee |
| Provider fork | ARL_FORK_RPC=<Base Sepolia RPC> npm run test:fork -w @arl/provider (needs forge build and anvil) |
the reference compute provider over HTTP, paid by the real SDK client and settled through the facilitator service (bearer token, file store): a job billed for seconds used and settled on-chain, a job killed at the paid time and settled at the ceiling, a replayed payment refused before it runs, and a signed usage receipt checked by the payer against the on-chain settlement |
Fork tests read chain state only; every transaction stays in the local fork.