Documents

AI payments

Paying for AI services per use in ARL with x402 upto.

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 with invalidateUnorderedNonces;
  • 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 AuthorizationStore records every authorization (payer + nonce) as settling, settled, retired or failed, 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 retired and 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. ArlUptoFacilitator takes receiptSigners (payee → signer). For a listed payee it settles only when requirements.extra.usageReceipt is 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. checkUsageReceipt checks 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 /verify and /settle can 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 settling and 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. InMemoryAuthorizationStore is for tests. FileAuthorizationStore survives 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.
  • arlToken must 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.

Partnership