reclaimExpired is permissionless: anyone may call it, and the money always goes
back to the original sender. But permissionless is not automatic. Until something
actually calls it, an unclaimed transfer just sits there, and the refund the
product promises never arrives on its own.
The keeper (apps/keeper) is that something. It is also the clearest working
example of an agent wallet: it is not handed a funded key and trusted to be
careful, it is paid the way this product pays any recurring payee, from a PULL
box whose policy is on chain.
What it does each tick
1
Scan
Read
Sent events forward from where it last looked, and read back the current
status of each transfer. Bounded by KEEPER_MAX_READS and KEEPER_SCAN_SPAN_BLOCKS,
so a failed tick does not make the next request larger.2
Decide
A pure function over the snapshot. No network, so every rule is testable without a
chain, and the reasons it logs are the reasons it acted.
3
Act
Call
reclaimExpired on what survived, highest amount first, until the tick limit
or the budget runs out.4
Get paid
If its balance has fallen below
KEEPER_LOW_WATER, pull its salary back up to
KEEPER_TARGET_BALANCE from its own box.The economics, which are the interesting part
The keeper pays the gas and the money goes to somebody else, so it never profits. The question is not “is this profitable” but “does this leave the system better off”:not-reclaimable, not-expired,
not-worth-the-gas, over-tick-limit, out-of-budget. Only PENDING and
LOCKED are reclaimable; the contract reverts on the rest.
On Arc gas is USDC, so the keeper’s operating cost and the money it rescues are the
same unit and the comparison above is a straight subtraction. On a chain with a
separate gas token this rule needs a price oracle, which is one more reason the
design fits here.
How it pays for itself
The keeper’s salary box is aSpendPolicyAccount in PULL mode whose target is the
keeper’s own address. The keeper proves control of that address to the co-signer,
the co-signer re-reads the policy from chain and signs or vetoes, and the contract
enforces the caps regardless of what either of them decided.
sweepToVault from the operator’s wallet, a call
the keeper cannot make, because that one is gated on msg.sender == vault.
Without a salary box the keeper still reclaims; it just cannot refill itself and
stops at its reserve.
Running it
apps/keeper/.env.example:
Next
- The box that pays it: Subscriptions.
- The refund it automates: Protected transfer.
- The co-signer it asks: The co-signer.