Retainer/DocsBASE SEPOLIA

Spend permissions

The primitive Retainer is built on. Most readers won't have met it, so this page explains it properly.

A spend permission is a signed message from a Base Account that lets a named spender pull a named token from the account, up to an allowance per period, between a start and an end. Coinbase'sSpendPermissionManager contract enforces every one of those bounds; the spender cannot exceed them, and Retainer does not need to be trusted to respect them. contracts/src/SpendPermissionManager.sol:28

What the customer signs

SpendPermissionManager.sol — the struct, verbatim
struct SpendPermission {
    address account;    // the customer's Base Account
    address spender;    // who may pull: Retainer's router
    address token;      // USDC
    uint160 allowance;  // maximum per period
    uint48  period;     // seconds; the allowance resets on this cadence
    uint48  start;      // valid from (inclusive)
    uint48  end;        // valid until (exclusive)
    uint256 salt;       // makes otherwise-identical permissions distinct
    bytes   extraData;  // consumed by the spender: for the router, (executor, recipient)
}

The five properties that matter

PropertyWhat it meansWhere the contract does it
Bounded per periodThe spender can pull any amount, in any number of transactions, until the period's total reaches allowance. The next pull reverts with ExceededSpendPermission._useSpendPermission
Self-resettingPeriods are fixed intervals from start. When time enters a new period the used amount resets to zero. Unused allowance does not carry forward.getCurrentPeriod
RevocableThe customer calls revoke; the spender can also call revokeAsSpender. It takes effect in the block it is mined, and it is permanent — there is no un-revoke.contracts/src/SpendPermissionManager.sol:397
Expiringend is a hard stop. Pulls after it revert with AfterSpendPermissionEnd; pulls before start revert with BeforeSpendPermissionStart.contracts/src/SpendPermissionManager.sol:516
Enforced on-chainAll of the above is contract logic. Retainer holds a key that can ask; the manager decides.spend

How the reset actually works

From Coinbase's own accounting note, at the commit Retainer vendors: “As apps spend user assets, the recurring logic automatically increments and enforces the allowance for the current period. Once enough time passes to enter the next period, the allowance usage is reset to zero and the app can keep spending up to the same allowance.”The start time and period “set a deterministic schedule infinitely into the future”; the end time “does not have to correlate with a clean period boundary.”upstream docs/SpendPermissionAccounting.md @ e0004e6

Registration is permissionless — so the customer never pays gas

Signing is off-chain. The permission becomes usable when someone submits approveWithSignature(permission, signature), and that function has no caller restriction: anyone holding a valid signature can submit it, and it is ERC-6492-compatible, so it works even if the customer's account is not yet deployed. Retainer's executor submits it and pays.contracts/src/SpendPermissionManager.sol:293

What the cap does to billing

This is the constraint the rest of Retainer is shaped around. The allowance is a ceiling the customer sets before the charge amount is known. A metered bill above the cap cannot be collected, and there is no increaseAllowance: raising the cap means a new permission and a new signature. A charge that fails because the cap is exhausted is not retried until the next period, because retrying sooner cannot succeed. packages/chain/src/classify.js:53

What a failed pull costs
A reverted spend rolls back the accounting in the same transaction, so a failed charge consumes no allowance. It does consume gas. Retainer therefore checks chain state before broadcasting and almost never sends a transaction it expects to revert.contracts/test/retainer/RouterCustodyInvariant.t.sol:72

The account must be a Base Account

The manager moves funds by calling execute()on the customer's account, so the account must be a smart account that lists the manager as an owner. A plain externally-owned wallet cannot be the payer. Coinbase's wallet adds the manager as an owner inside its approval flow; a scripted account has to do it explicitly, and if it doesn't, every charge reverts.README — the non-obvious prerequisite

  • Manager, canonical on Base and Base Sepolia: 0xf85210B21cC50302F477BA56686d2019dC9b67Ad
  • Its on-chain EIP-712 typehash equals keccak256 of the type string in the vendored source, so the code Retainer reads is the code it transacts against. test_typehash_matchesDeployedManager