Running on Base Sepolia · not on mainnet

Recurring USDC billing that never holds the money.

Retainer charges on schedule against Base spend permissions. Funds move customer → router → merchant in one atomic transaction, and the router's balance is zero after every charge. A billing engine, not a wallet.

20.00CustomerBase Account1.000.00Retainer routerone atomic transaction0.00Merchant treasury0x47dB…6A671.00charge 1 · fixed · router balance after: 0
Six real charges from the test drills, totalling 7.05 USDC, each settled to the merchant treasury with the router holding nothing afterwards.
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,                 // 0x3370…cC79, verified on Basescan
  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.

How a charge works

One signature from the customer. Everything after that is on-chain-enforced.

The customer signs a bounded permission

A Base spend permission names the spender, the token, an allowance per period, a period length, a start and an end. The cap resets on-chain at every period boundary and unused allowance does not carry forward. The customer can revoke at any time, and revocation takes effect the block it's mined. None of this is enforced by Retainer — the contract does it.

spenderRetainer router
tokenUSDC
allowance20 USDC / period
period30 days, resets on-chain
revocableyes, instantly

Retainer registers it and pays the gas

approveWithSignature is permissionless, so our executor submits it. The customer signs, never transacts.

Funds never rest with Retainer

A charge is one transaction: the router pulls from the customer and forwards to the merchant treasury atomically. If the forward fails, the whole thing reverts. The router's USDC balance is zero after every charge — asserted by a fuzz test that fails the build, and observed on-chain after every charge we've run.

customer account
↓ spend(permission, value)
router · balance after: 0
↓ same transaction
merchant treasury

The amount is decided at charge time

Fixed or metered. A usage charge is enqueued with no amount and computed from recorded usage when it runs — anything up to the cap.

What the engine guarantees

Each of these has been triggered deliberately on testnet, with the transaction hashes in the repo.

1 spend per period, proven

Exactly-once charging

The signed transaction is persisted with its hash before it is broadcast. If the process dies after the send, recovery finds the receipt. If another transaction took the nonce, the attempt is marked superseded and retried fresh. Both branches were forced deliberately; each produced exactly one on-chain spend.

6 modes · 0 wasted transactions

Failures classified before gas is spent

Revoked, expired, insufficient balance, allowance exhausted, not started, not approved — read from chain state before broadcasting, each with its own disposition. Revoked and expired are terminal; the rest know when to try again.

never on broadcast

Paid means reconciled

A charge is confirmed only when both the manager's SpendPermissionUsedand the router's SpendRouted events are indexed for that transaction, and the router paid the recipient we intended. Every confirmed row maps to one transaction hash.

A routed charge is roughly 135–150k gas. At the Base fees we measured, that is about half a cent per charge — under 0.4% of a $3 subscription, where card rails take more than ten percent.

The contracts

Nothing here is ours to audit. We deploy an unmodified instance of Coinbase's audited router.

SpendRouter · Retainer's instance
0x337099eE403C090388A66cc9370F7b0Fe4CDcC79

Verified source on Basescan. Deployed by us, logic untouched.

SpendPermissionManager · canonical
0xf85210B21cC50302F477BA56686d2019dC9b67Ad

Not redeployed. Its on-chain EIP-712 typehash matches the source we vendor.

Source
github.com/coinbase/spend-permissions

coinbase/spend-permissions @ e0004e6, vendored verbatim. A script re-checks it against upstream by sha256.

Network
testnet only

Base Sepolia, chain 84532. There is no mainnet deployment.

Questions worth asking

Answers are only as good as what the repo can back.

Status

Custody

Charging

Read it, run it, or sign a permission yourself.

Below are the six confirmed charges from the test drills and the crash-recovery runs — real Base Sepolia transactions. Every hash links to Basescan. Requested and settled amounts are shown side by side so the match is checkable, not asserted.

ChargeAmountRequestedSettled on-chainBlockTransaction
#1fixed1.0000001.000000 USDC465656050x91a4980a…8a1e7c
#8fixed1.5000001.500000 USDC465656950x35043fd1…8aaa93
#9usage · computed at charge time1.2500001.250000 USDC465657600x55c0e2b5…9bea3d
#10crash Afixed1.1000001.100000 USDC465657850x8e4ce873…68e2df
#11crash Bfixed1.3000001.300000 USDC465658380xe0ac2a1e…b5244d
#12fixed0.9000000.900000 USDC465666400xa9043281…545103
Sum of settled amounts7.050000 USDCequals the treasury balance; router balance 0 — checked 2026-09-09

Crash recovery A · killed after the send, before the row updated

Recovery found the receipt on-chain. Exactly one spend.

the one transaction (charge #10, nonce 22)
0x8e4ce873…68e2df

Crash recovery B · killed after signing, before the send; nonce then taken by another transaction

Recovery marked the attempt superseded and did not re-broadcast. Charged once on a fresh nonce.

superseded attempt — never landed, by design
0x0a078798…9131a3not on Basescan — it was never mined
competing transaction that consumed nonce 24
0x21cd8eb3…22c32d
the one spend (charge #11, nonce 25)
0xe0ac2a1e…b5244d
Retainer

Recurring and usage-based USDC billing on Base spend permissions. Base Sepolia only.

Retainer