Skip to main content
The same disposable account that makes a one-off private payment expresses a subscription when you put it in PULL mode. The merchant draws from it on an interval; the contract enforces how much and how often, and the co-signer can refuse any individual pull. This is what replaces an infinite ERC-20 approval. An approval is a standing permission with no cap, no schedule and no revocation short of another transaction. A PULL box is a permission with all three written into the contract. Creating a subscription

The four guarantees

Every one of these is enforced on chain, not by the co-signer:

Per-pull cap

perPullMax bounds a single pull. This is the real subscription guarantee: even a misbehaving co-signer cannot drain the cumulative cap in one call.

Interval

interval seconds must pass between pulls. A merchant that tries to charge twice in a month reverts with TooSoon.

Cumulative cap

maxAmount is the most the box will ever pay out in its lifetime, across all pulls. It cannot be raised.

Expiry

After expiry no outbound payment is possible at all, whatever the co-signer signs.
Set perPullMax to the per-period amount, not to the total. A 5 USDC monthly subscription over a year is maxAmount: 60 USDC, perPullMax: 5 USDC, interval: 30 days. Leaving perPullMax at 0 means “no tighter cap than maxAmount”, which is legal and gives away the guarantee that matters most.

Creating one

A PULL box must have interval > 0. init reverts with ZeroInterval otherwise, because a zero interval makes the rate limit always pass and lets several pulls land in one block, which defeats the only thing making it a subscription rather than an allowance. PUSH ignores interval, so this constrains PULL alone.

Pulling

The merchant (or a relayer on their behalf) asks the co-signer to authorize, then submits. The payer signs nothing, and is not required to be online.
action is ACTION_PULL (1), not ACTION_PAY (0), and the two are separate fields in the EIP-712 struct. That is deliberate: a pay authorization can never be replayed as a pull, or the reverse. The subscription list

Reading its state

Everything a UI needs is a getter, so nothing depends on replaying events:
remaining is what is left of the cumulative cap, spent is what has gone out, and nonce counts settled spends. There is deliberately no owner and no vault getter, so a merchant reading the box they are paid from learns nothing about who funded it. A subscription's detail

Cancelling

Sweep it. There is no separate revoke, because a box with no balance cannot pay anyone.
The money goes home immediately and the co-signer stops signing for the box. This is the difference from an infinite approval most worth stating plainly: cancelling is one transaction the payer can always make, needs no cooperation from the merchant, and cannot be front-run into a last charge, because a sweep and a pull cannot both spend the same balance.
Both sweeps require msg.sender == vault. Bind the box to an address you can transact from. For a stealth subscription that address is the stealth address, so you sweep by deriving its key: see Stealth boxes.

Agent wallets

The same shape, pointed at software instead of a merchant. Hand an agent a tightly-scoped PULL box and it transacts autonomously inside a policy it cannot exceed, because the caps are on chain and the co-signer vetoes anything outside them. The keeper in this repository is exactly that, and it pays its own gas from a PULL box whose target is itself. See The keeper.

Next