> ## Documentation Index
> Fetch the complete documentation index at: https://docs.payai.network/llms.txt
> Use this file to discover all available pages before exploring further.

# EVM Batch Settlement on Base

> Payment channels for metered x402 APIs on Base: one USDC deposit, off-chain vouchers per request, batched claims and a single sweep. Public, no API key, gas paid by PayAI; or let PayAI hold the receiver authorizer with an API key.

Batch settlement lets a customer fund a reusable on-chain channel once and pay for many requests with signed vouchers, while your server claims and sweeps the accumulated charges in batches. It is built for metered APIs, inference, and any service where the final price of a request is known only after the work is done. On EVM the scheme is implemented by the `x402BatchSettlement` contract, and PayAI relays every on-chain leg and pays the gas.

<Note>
  PayAI serves `batch-settlement` on **Base mainnet** (`eip155:8453`) and **Base Sepolia** (`eip155:84532`) at `https://facilitator.payai.network`. Access is public: no PayAI account or API key is needed when you run your own receiver authorizer. With a PayAI API key you can instead let PayAI hold the authorizer key (see section 2), and pay for settlement legs with credits after the free allowance.
</Note>

## How it works

1. **Deposit.** The customer signs a USDC authorization (EIP-3009, or Permit2 for other ERC-20s) that funds a channel identified by its immutable config: payer, receiver, receiver authorizer, token, withdraw delay and a salt. PayAI submits the deposit; the customer pays no gas.
2. **Vouchers.** Each request carries a voucher with a cumulative ceiling: everything charged so far plus this request's maximum. Your server verifies the signature locally in a few milliseconds and charges the actual amount, from zero up to the ceiling. Nothing touches the chain.
3. **Claim.** On your schedule, your server sends the latest voucher per channel to PayAI, which submits one `claimWithSignature` transaction covering up to 25 channels.
4. **Sweep.** A `settle` transaction transfers everything claimed for your receiver and token to your wallet in one transfer.
5. **Refund or withdraw.** Your server can cooperatively refund a customer's unused balance at any time; the customer can always start a timed withdrawal (one to 24 hours under PayAI's policy) as a unilateral fallback.

Partial charges are first class: the ceiling is authorized by the customer, the charge is decided by your server. Use the HTTP adapter's settlement override to bill less than the ceiling:

```typescript theme={null}
import { setSettlementOverrides } from "@x402/express";

app.get("/api/generate", (req, res) => {
  const actualUsage = computeCost(); // atomic units, "50%", or "$0.001"
  setSettlementOverrides(res, { amount: String(actualUsage) });
  res.json({ result: "..." });
});
```

## 1. Install the upstream SDK

PayAI runs the merged x402 Foundation implementation. Use the published packages; no PayAI-specific build is required:

```bash theme={null}
npm install @x402/core@^2.27.0 @x402/evm@^2.27.0 @x402/express@^2.27.0 viem
```

Use `BatchSettlementEvmScheme` from `@x402/evm/batch-settlement/server` on your server and the same-named class from `@x402/evm/batch-settlement/client` on the customer side. The reference server and client live in the x402 repository under `examples/typescript/servers/batch-settlement` and `examples/typescript/clients/batch-settlement`.

## 2. Choose a receiver authorizer

The receiver authorizer is the key that signs claims and refunds for your channels. You have two options. The address is part of every channel's immutable config, so pick one before you open channels.

### Option A: your own key (default, no account needed)

```typescript theme={null}
import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server";
import { BatchSettlementEvmScheme } from "@x402/evm/batch-settlement/server";
import { RedisChannelStorage } from "@x402/evm/batch-settlement/server/redis-storage";

const facilitator = new HTTPFacilitatorClient({
  url: "https://facilitator.payai.network",
  timeoutMs: 110_000, // PayAI answers within its 100 s budget; keep the client above it
});

const scheme = new BatchSettlementEvmScheme(receiverAddress, {
  receiverAuthorizerSigner, // an EOA you control; no ETH or tokens needed
  withdrawDelay: 3600,      // seconds; PayAI accepts 3600–86400
  storage: new RedisChannelStorage({ client: redisClient }),
});

const server = new x402ResourceServer(facilitator).register("eip155:8453", scheme);
```

This is upstream's recommended production mode. Your channels survive a facilitator change, because any facilitator can relay your signed claims and refunds. Keep the key until every channel it authorizes has drained; rotating it means opening new channels.

### Option B: PayAI holds the authorizer (API key required)

If you would rather not operate a signing key, PayAI can sign claims and refunds for you. PayAI advertises its authorizer address in `/supported` **only to authenticated callers**, so every facilitator call from your server must carry a PayAI JWT ([how to mint one](/x402/facilitators/authentication)), including the `/supported` call the SDK makes at startup. Each JWT must carry a unique `jti`: PayAI consumes it on `/verify` and `/settle`, so mint a fresh token per call.

```typescript theme={null}
const facilitator = new HTTPFacilitatorClient({
  url: "https://facilitator.payai.network",
  timeoutMs: 110_000,
  createAuthHeaders: async () => {
    const headers = { Authorization: `Bearer ${await generatePayAIJwt(apiKeyId, apiKeySecret)}` };
    return { verify: headers, settle: headers, supported: headers };
  },
});

const scheme = new BatchSettlementEvmScheme(receiverAddress, {
  // no receiverAuthorizerSigner: the SDK takes extra.receiverAuthorizer from /supported
  withdrawDelay: 3600,
  storage: new RedisChannelStorage({ client: redisClient }),
});
```

How PayAI binds channels to your account:

* The first authenticated deposit `/verify` for a channel reserves it for your account; the deposit `/settle` from the same account binds it. First writer wins.
* Claims, refunds and voucher or refund verifies on that channel must come from your account. Requests from any other account, or without a JWT, are refused. Sweeps stay public because they only pay the channel's receiver.
* Delegated legs bill like any authenticated leg: the free allowance first, then credits, at the same per-leg prices.

What to know before choosing this option:

* **Lock-in.** PayAI's authorizer address is hashed into every channel id. To move those channels to another facilitator, claim outstanding vouchers and refund the remaining balances first.
* **Fallback.** If PayAI is unavailable, the channel's receiver can still call `claim` and `refund` on the contract directly, but only if the receiver is an EOA or a contract able to make those calls. Use Option A if your `payTo` is a routing contract.
* **Rotation.** If PayAI rotates its advertised address, channels on the old address keep working, but one claim batch cannot mix channels from two authorizers; drain old channels before your channel manager mixes them. Restart your server after a rotation so it reads the new address.

## 3. Run the channel manager

Claims and sweeps happen when your server asks for them. Run the exported channel manager against the same durable storage:

```typescript theme={null}
const manager = scheme.createChannelManager(facilitator, "eip155:8453");
manager.start({
  claimIntervalSecs: 60,
  settleIntervalSecs: 300,
  refundIntervalSecs: 3600,
  selectRefundChannels: channels =>
    channels.filter(c => Date.now() - c.lastRequestTimestamp >= 3_600_000),
});
```

Claim well inside the withdraw delay of your channels. Vouchers you have not claimed when a customer's withdrawal finalizes are lost. Use Redis or Valkey storage for anything beyond a single local process; the file and memory stores are for tests.

## 4. Policy and limits

Read `batchPolicy` from `GET /supported` for the live values; these are the defaults:

| Limit | Value |
| - | - |
| Asset | USDC on Base mainnet (`0x8335…2913`); USDC on Base Sepolia (`0x036C…CF7e`) |
| New channel deposit | 0.10–100 USDC (`100000`–`100000000` atomic units) |
| Withdraw delay | 3,600–86,400 seconds |
| Deposit attempts | 5 per minute per receiver, 10 per minute per client IP |
| Channels per claim or refund request | 25 |

Top-ups to an existing channel are not bound by the initial-deposit range. The upstream server announces a `minDeposit` hint of ten times the route price by default, which for a `$0.01` route is exactly PayAI's 0.10 USDC floor; raise the route's `extra.minDeposit` if your customers should fund more per channel.

## 5. What PayAI bills

Every on-chain leg PayAI relays for you is priced at the observed gas plus 30 percent: deposit, claim, sweep and refund. Vouchers are free; they never reach PayAI. Without an API key, legs draw on the public free-tier allowance for your `payTo` address and are refused when it is exhausted. With a [merchant API key](/x402/facilitators/authentication) they are billed as credits. There are no privileged legs: a refused claim or sweep never strands funds, because `claimWithSignature`, `settle` and `refundWithSignature` are permissionless on the contract and you can relay them yourself.

Observed on Base mainnet: a deposit costs about 140,000 gas, a single-channel claim about 77,000, a sweep about 57,000 and a refund about 95,000. At Base fees today that is well under a cent per leg.

## Recovery

Retry the exact payload after a timeout, a `settlement_pending` answer, or a lost response. Deposits, claims and refunds are content-addressed: an identical retry returns the recorded outcome, and if the original transaction is still unconfirmed PayAI reconciles it from its receipt rather than broadcasting again. A claim that is no longer strictly increasing is refused with `batch_claim_not_increasing`, which means the earlier claim landed; reconcile against the channel's on-chain `totalClaimed`.

Sweeps are different: a `settle` request sweeps whatever is owed now, so it has no durable identity. A second sweep while one is in flight returns `409 duplicate_settlement`. A sweep with nothing left to move returns `nothing_to_settle`, and when a recent sweep for that receiver and token is known the response carries `extra.lastSweep` with its transaction and amount. A `settlement_pending` sweep answered with an empty transaction hash means PayAI was still waiting for the receipt; retry after a minute.

| Response | Meaning and action |
| - | - |
| `400 batch_target_mismatch` | The payload's receiver or token differs from `payTo`/`asset`; fix the requirements. |
| `400 batch_claim_authorizer_signature_required` | Option A channel without a receiver-authorizer signature; include it. |
| `400 batch_claim_signature_not_allowed` or `batch_claim_mixed_authorizers` | Option B channels must be unsigned, and one batch cannot mix authorizers. |
| `401 missing_api_key_token`, `jwt_jti_required`, `jwt_replayed` | Option B calls need a fresh PayAI JWT with a unique `jti` on every request. |
| `403 batch_channel_owner_mismatch`, `batch_channel_unreserved`, `batch_channel_unbound` | The channel belongs to another account, or was never reserved and bound through PayAI; see Option B. |
| `403 batch_delegated_disabled` or `batch_delegated_channel_cap` | Delegation is paused, or your account has reached its open-channel cap. |
| `409 batch_refund_nonce_conflict` | The channel's refund nonce moved; re-read it and resubmit. |
| `429 batch_delegated_rate_limited` | Respect `Retry-After`. |
| `400 batch_claim_simulation_failed` | The claim would revert; check voucher signatures and channel state. |
| `400 batch_claim_not_increasing` | Already claimed on-chain; reconcile, do not retry. |
| `400 batch_claim_too_many` or `batch_claim_duplicate_channel` | At most 25 distinct channels per request. |
| `400 batch_deposit_below_minimum`, `batch_deposit_above_maximum`, `batch_withdraw_delay_out_of_range`, `batch_deposit_asset_not_supported` | Adjust the channel terms and obtain a new customer signature. |
| `403 batch_new_channels_disabled` | New channels are paused; existing channels keep working. |
| `403 invalid_scheme` | The network is not enabled for batch settlement. |
| `429 batch_deposit_rate_limited` or `batch_claim_rate_limited` | Respect `Retry-After`. |
| `free_tier_exhausted` | Add an API key to pay with credits, or relay the transaction yourself. |
| `409 duplicate_settlement` | The same leg is in flight; wait and retry. |
| `settlement_pending` | Non-terminal; retry the identical payload to reconcile. |

## Test before taking customer traffic

Start on Base Sepolia with USDC from the Circle faucet, using the same facilitator URL. Exercise partial charges, a claim and sweep cycle, a lost response, a process restart, and a cooperative refund, and confirm your receiving wallet's USDC balance moves by the charged total. Then repeat with a small channel on Base mainnet.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.