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 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
- Non-custodial by construction. Funds move customer → router → merchant treasury in one transaction; if the forward fails, the whole charge reverts. A fuzz test asserts the router's balance is zero after every charge and fails the build otherwise. contracts/test/retainer/RouterCustodyInvariant.t.sol:24
- Built on Base spend permissions, which are bounded per period, reset on-chain, and can be revoked by the customer at any time. None of that is enforced by Retainer. contracts/src/SpendPermissionManager.sol:38
- Variable and usage-based amounts, computed at charge time up to the cap — not fixed when the charge is enqueued. resolveAmount
- Exactly-once charging, proven across both crash-recovery branches on Base Sepolia. apps/worker/src/recovery.js:9
- Six failure modes classified from chain state before any gas is spent, each with its own disposition. packages/chain/src/classify.js:25
- Paid means reconciled — only the reconciler writes
confirmed, and only after both on-chain events are indexed. apps/worker/src/reconciler.js:76 - About half a cent of gas per charge, measured on real transactions. phase0-addendum.md:111
The integration, in three lines
// 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. The signing page at /sign and the API route behind it are the real implementation of step 1. apps/web/app/api/permissions/route.js