Retainer
Welcome

What Retainer is

Recurring and usage-based USDC billing on Base, where the biller never holds the money.

Retainer is a charge engine. A customer signs one spend permission; Retainer registers it on-chain, charges against it on a schedule for whatever the period actually cost, routes each charge from the customer to the merchant's treasury in a single atomic transaction, and marks the charge paid only after the on-chain events confirm it.apps/worker/src/charger.js

The system, and its two fulfilment paths

An expected payment is the top-level object: an amount, a due date, and a state. It is satisfied one of two ways, and both are first-class. Pull draws on a spend permission through Retainer's router. Watch detects an incoming transfer the payer sent themselves and matches it to the obligation. The second exists because a Safe multisigcannot be the account of a spend permission, so DAO and treasury payers are reachable no other way.packages/db/migrations/003_expected_payments.sql:12

Retainer system architectureAn expected payment is satisfied by one of two paths. In the pull path, a customer's Base Account, or a smart account their wallet owns, signs a spend permission once. Retainer's worker calls Retainer's SpendRouter, the router calls the SpendPermissionManager, and the manager moves USDC from the customer's wallet to the router, which forwards the full value to the merchant treasury in the same transaction. A confirmed charge settles the obligation. In the watch path, any payer, including a Safe multisig, sends USDC directly to the treasury; the worker indexes the transfer and a matched transfer settles the obligation. Retainer never holds the money on either path.PULL — SPEND PERMISSIONSpendPermissionManagerexecutes on the customer's walletCustomerBase Account, or wallet-ownedSpendRouterRetainer's own instancesigns a spend permission, oncespend()USDC, one atomic transactionforwards the full valueMerchant treasuryreceives on both pathsExpected paymentamount, due date, stateoverdue if neither path settles itsettled by a confirmed chargeor by a matched transferWATCH — INCOMING TRANSFERPayerSafe multisig, DAO, any walletplain USDC transferno permission exists, and none canRetainer workerholds no funds on either path — it signs transactions and reads the chain, never custodiescharger → classifies before spending gas, then calls spendAndRoute() · reconciler → sole writer of confirmedwatcher → indexes transfers at 3 confirmations · matcher → attributes, or defers to review · sweep → overdue + alertsspendAndRoute()watches
Both paths end at the merchant treasury, and Retainer holds the money on neither.

The consequence worth noticing is that overdue detection has one path, not two: a pull whose charge failed terminally and a watch payment that never arrived both leave the same obligation unsatisfied past its due date, and both raise the same alert. apps/worker/src/sweep.js:11

The problem it solves

Card rails give merchants recurring billing by letting a processor hold funds and reverse them. Onchain, the usual substitutes are worse in one of two ways: either the merchant is handed an unbounded ERC-20 allowance and asked to be trusted with it, or a third party sits in the flow and custodies money on the way through. Base spend permissions are a different primitive — a capped, self-resetting, revocable authorisation enforced by a contract — and Retainer is the billing layer that turns that primitive into something a merchant can run a business on: schedules, retries, failure classification, and reconciliation.

What it does, and what backs each claim

The integration, in three lines

merchant/billing.ts
// 1. The customer signs once. Retainer registers it on-chain and pays the gas.
const permission = await requestSpendPermission({
  account, spender: RETAINER_ROUTER, token: USDC,
  allowance: 20_000_000n,        // 20 USDC per period — a ceiling, not a price
  periodInDays: 30,              // resets on-chain; nothing carries over
  extraData: encodeExtraData(executor, merchantTreasury),
  provider,
});

// 2. Charge whatever this period actually cost, up to the cap, when it's due.
await enqueueCharge({ permission, amount: usageThisPeriod });

// 3. "Paid" means the on-chain events say so — never that a transaction was sent.

The first call is Coinbase's Base Account SDK; the second is Retainer's queue. A customer's billing link — /pay/<token>, where the merchant's exact terms come from the link — and the API route behind it are the real implementation of step 1. apps/web/app/api/permissions/route.js

Where it runs
Base Sepolia only, chain 84532. There is no mainnet deployment and no production claim. The limitations page lists what else is not true yet.