How to Build Recurring Crypto Payments (Subscriptions On-Chain)
Reconciliation is where most of these controls get tested first
See how Formance Reconciliation compares ledger postings against on-chain and provider data.
Reconciliation is where most of these controls get tested first
See how Formance Reconciliation compares ledger postings against on-chain and provider data.
Recurring crypto payments settle on-chain instead of through card networks. Fintech teams building subscriptions, streaming payroll, or usage-based billing on this rail inherit a problem card networks normally solve for free: authorizing repeat charges, retrying failures without duplicating them, and reconciling billing state against what the chain actually recorded.
Your keeper service called transferFrom for last night's renewals. One call timed out before the receipt came back. The billing database marked the charge failed. The chain settled it anyway. An hour later, the retry job charged the subscriber again.
On a card rail, that duplicate becomes a refund ticket. On-chain, it's a settled transfer your system has to compensate for, not reverse. That's the failure mode recurring crypto payments need to design out from the start.
Most of the design work happens off-chain with the authorization model, the renewal state machine, and the reconciliation loop between billing records and on-chain state.
Recurring crypto payments are subscription charges settled over a blockchain payment rail, assembled from a one-time authorization plus an off-chain scheduler.
On-chain subscriptions substitute the mandate (which becomes an allowance, permit, or spend permission granted to a contract or agent), and the batch job becomes a keeper service you run. The mandate is executable policy with enforceable scope. Authorization rules become programmable behavior in a billing system, which is what is programmable money in practice.
Two properties separate recurring crypto payments from fiat subscriptions, and every design decision below traces back to them: irreversibility of settled transfers and open-ended authorization lifetime.
Settled on-chain transfers and ERC-20 authorizations invert the default assumptions of a card-based billing stack. A card processor absorbs disputes, expires mandates, and gives you retries against a reversible rail, while an on-chain rail settles finally against a standing grant that keeps working until someone changes it.
Five components define every on-chain subscription system: an account model with per-cycle approval, an authorization primitive that scopes, an execution mode, an off-chain scheduler that tracks billing, and a ledger that immutably records each collection and settlement.
Whether the subscriber holds an externally owned account (EOA) or a smart account decides your charging model. For EOA subscribers, design around token allowances or signed messages, and either fund gas from the EOA or add a relayer.
A smart account has programmable validation under ERC-4337, which makes scoped session keys possible. ERC-7715 session keys, still a Draft, define a wallet method through which applications can request scoped permissions; the supported constraints depend on the wallet and permission implementation.
Smart accounts can also route gas through paymasters instead of the subscriber's balance.
Three options govern how a subscriber authorizes recurring pulls, including standing allowances (ERC-20), signed off-chain approvals (ERC-2612 permits), and one-shot signed transfers (EIP-3009).
A classic ERC-20 allowance works when the subscriber approves a spending cap once, and your keeper pulls from it each cycle. The token spec treats each pull as a withdraw workflow that the account holder has explicitly authorized, and an unlimited approval, once granted, generally remains in effect until the subscriber changes it.
An ERC-2612 permit lets the subscriber set the same allowance with a signed message instead of an on-chain transaction, which saves them gas but still leaves a standing allowance in place. The permit's deadline only limits when the signature can be submitted, and does not make the resulting allowance expire.
EIP-3009 goes a step further and removes the standing allowance entirely. Each charge carries its own signature with a unique ID and a validity window, so it cannot be replayed. The trade-off is that every renewal requires a fresh signature from the subscriber.
Two draft standards target the subscription: ERC-5827 proposes an allowance that automatically refills at a set rate, and other proposals add time-bound approvals that expire on their own.
Execution mode is either streaming (continuous flow) or discrete scheduled pulls, and the choice determines both your keeper design and how you record charges in the ledger.
Streaming: A streaming design models the subscription as a continuous flow rather than a sequence of discrete charges, with the merchant withdrawing value from the stream. The trade-off is reflected in your billing records. For reconciliation, synthesize a per-period boundary and snapshot accrued value at each period close rather than relying on a discrete charge event.
Scheduled pulls: Discrete scheduled pulls use a keeper, whether your own service, a decentralized automation network, or an ERC-4337 bundler path, to fire transferFrom or the smart-account equivalent at each billing epoch against the scoped authorization. Intervals demand two things streams don't: a scheduler with correct wall-clock semantics per rail, and idempotency on retry.
With smart accounts, a scoped session permission can enforce expiration while restricting total spend and the number of callable contracts. For recurring USDC payments, that puts the spending boundary in smart-account validation while your application remains responsible for period boundaries, execution, and reconciliation.
The scheduler owns wall-clock correctness for every renewal, because the chain does not know when a billing period opens or closes.
It tracks billing-period epochs, triggers keeper calls at the correct time for each rail, manages retries with backoff, and passes the deterministic renewal ID to both the on-chain call and the ledger posting so the two can be reconciled later.
The ledger is the system of record for what was collected, settled, and owed, and it runs off-chain because the chain records only the transfer, not the subscription's business state.
Every renewal produces immutable postings keyed to a deterministic renewal ID, which is what prevents duplicate charges, missed renewals, and reconciliation drift.
Building recurring crypto payments takes four steps: define subscription plans in your ledger, collect scoped on-chain authorization, automate renewals with an idempotent state machine, and handle reconciliation and reorganization failures. Each step below assumes the architecture decisions from the previous section are already made.
Model plans as first-class ledger objects before you touch a contract, because every downstream artifact, from the renewal ID to the posting to the reconciliation key, derives from the plan schema.
The fields you need include a plan ID, price and denomination (USDC, USDT, or another stablecoin), billing period, grace window, and the authorization model the plan requires (allowance pull, permit, or spend permission). Choose the pricing denomination deliberately. If the existing authorization does not cover a new price, collect an updated authorization before renewal.
Formance is an open-source, programmable core ledger where you encode the plan's posting shape in Numscript so every renewal produces the same atomic set of postings.
Take a $48,000 annual plan. The Base blockchain boundary is @external:blockchain:base, and Acme's pending subscription funds are @merchants:acme:subscriptions:pending.
Upon submission, the collection moves from the Base blockchain boundary to the merchant's pending account. On settlement, a second posting moves it from pending to payable. The renewal ID and transaction hash ride along as metadata:
// SUBSCRIPTION_COLLECTION
// Event: collect 48,000 USDC for Acme's subscription renewal
send [USDC/6 48000000000] (
source = @external:blockchain:base allowing unbounded overdraft
destination = @merchants:acme:subscriptions:pending
)
set_tx_meta("event_type", "subscription_collection")
set_tx_meta("renewal_id", "sub_20481:2026-02")
set_tx_meta("tx_hash", "0x9c41...e2af")
Settlement posts a second, separate transaction from @merchants:acme:subscriptions:pending to @merchants:acme:payable for the same amount. The transaction carries the same renewal ID. Every transaction balances, with every debit matched by an equal credit. This invariant is central to understanding ledger balance, so value never appears or vanishes between accounts.
Postings are immutable once written. Store the deterministic renewal ID as transaction metadata, enforce exactly one settled transaction per renewal ID in a concurrency-safe application or persistence layer, and then use the metadata and queries for audit and on-chain/off-chain reconciliation.
Ask the subscriber for the smallest authorization that still covers the plan. An exact allowance sized to the term (never unlimited), a per-period spend cap, or a fresh signature for each charge. Give it an expiry wherever the primitive supports one.
An over-scoped or never-expiring authorization stays live against every subscriber on that plan until they revoke it, so the scope and expiry you choose here shape both the revocation UX and the size of any future security incident.
Where the smart account, spender contract, or authorization itself supports expiration, set it there. For a plain ERC-20 allowance, an expiry in your own system can stop your scheduler from charging, but it does not clear the allowance on-chain. Give subscribers a revoke button in your own UI instead of pointing them at third-party revocation tools.
The tighter you scope the amount, the smaller the loss if something goes wrong later. The shorter you scope the lifetime, the sooner that risk expires on its own.
Model every renewal as a state machine and give each one a unique ID built from the subscription and the billing period (this is what the idempotency key is for). A renewal starts as scheduled when its billing period opens, moves to authorization-valid once you confirm the allowance or permission still covers the amount, and moves to submitted when the keeper sends the on-chain call.
It becomes provisionally confirmed at the first block confirmation and settled once you hit your confirmation threshold for that chain. A definitive on-chain failure (including running out of gas) moves it to failed.
A timeout, missing gas, or a pre-broadcast error makes it retryable once you fix the underlying issue. It becomes revoked if the subscriber pulls their authorization, and canceled if they end the subscription.
Duplicate prevention has two sides, and both key off the same renewal ID. On the ledger side, ensure only one settled transaction exists per renewal ID, enforced as close to the ledger write as possible so a retry can't sneak past.
On the on-chain side, ensure the same charge executes only once. For EIP-3009, check whether the signed transfer has already been used before submitting a new one; for ERC-20 allowance pulls, check the previous transaction's receipt and on-chain state before calling transferFrom again.
Both halves matter because the ledger is your source of truth for what you collected, and the chain is where the money actually moved.
Three failure surfaces need explicit handling before launch: sorting failures by cause (balance, allowance, gas), running a reconciliation loop that doesn't trust webhooks, and handling revocation races when a charge is already in flight.
For failure recovery, define a retry schedule with growing gaps between attempts and a grace period before suspending access. Add a terminal lapsed state that cuts the subscriber off, re-check the remaining allowance before every retry, and keep the dunning steps on the plan object rather than in keeper code.
Assume a block reorganization can move a renewal from provisionally confirmed back to unconfirmed, and pick a confirmation depth per chain you're comfortable calling final.
Before you launch recurring crypto payments, six controls have to be in place, and five of them are off-chain:
If any of these six aren't in place, don't ship. On-chain subscriptions succeed or fail on the off-chain plumbing, and the rail won't forgive a duplicate charge the way a card network will.