(Yes, you could implement sliding allowances or just approve a huge number. But you're already 200 lines deeper into the contract than traditional billing was.)

Implement a retry queue with exponential backoff. First retry in 24 hours. Second in 3 days. Third in 7 days. Then you escalate to communication.

Usage-based billing is where I stayed up nights thinking about implementation. With subscription, you charge a flat fee. Simple. But what if you want to charge based on usage?

Use a relayer to submit transactions on behalf of customers, but only after you've validated the allowance and wallet balance off-chain. This shields customers from needing to maintain gas fees.

And honestly? For most SaaS companies? You're better off accepting cryptocurrency as a payment method through a traditional payment processor. Stripe accepts USDC. Coinbase Commerce exists. Let someone else solve dunning.

The blockchain part is genuinely elegant for the happy path. But the real complexity in billing isn't the payment transmission. It's all the edge cases. And blockchain makes those worse, not better.

You could aggregate usage and batch the submissions. But now you're delaying the charge. The customer's account is technically in arrears for hours. What if they cancel during that gap? What if the batch submission fails?

On blockchain, there is no "on file." There's no card details being stored. What you have is an allowance - an ERC-20 approval that says "this smart contract can spend up to X tokens from my wallet."

When a charge fails, check the allowance automatically. If it's zero, trigger a notification asking them to re-approve. If it's non-zero but insufficient, notify them to top up. If the transaction failed for other reasons (gas, network), just retry.

Store the intent to charge off-chain. Your database tracks when the next charge is due, how many times you've retried, what the last error was. The smart contract is lightweight - it just executes the transfer if conditions are met.

Most implementations I've seen just... don't do usage-based billing on-chain. They charge a flat rate and bill usage separately in traditional rails. Which means you've got two payment systems running in parallel. One for the base subscription. One for overages. Which creates reconciliation problems.

When a pull fails, you need to check the allowance. If it's zero, you notify the customer and ask them to re-approve. You can make this frictionless with a wallet integration - they click a button, approve the full subscription amount, and you're back in business.

But here's the thing nobody implements. You need to track approval state in your contract. When did the customer last approve? What amount? Is it still valid? What happens if they've approved multiple times (maybe they panic-approved, then forgot they had a subscription, then approved again)?

Here's what happens in a traditional billing system. Customer's card declines. Your processor stores the intent to retry. It retries automatically on a schedule. The card details stay on file. If the card works later, you charge it. If it fails three times, you escalate to communication.

I've built crypto payment systems for three different companies now. And every single time, we get to month four and someone realizes we shipped the happy path. Subscription works. Recurring charges work. But what happens when the subscriber's wallet is empty and we need to retry the payment three times?

Customer uses 10,000 API calls. You need to charge them for that. But how does the blockchain know about API calls? You need an oracle. You submit API usage data to the smart contract. The contract calculates the bill. But that submission is a transaction. It costs gas. It has latency.

Third scenario. The allowance was revoked. The customer did this intentionally. Maybe they connected the wallet to something malicious and clicked approve on the wrong contract. Maybe they're trying to cancel and thought revoking would do it. Whatever the reason. The allowance is zero. The subscription still exists. You try to charge. It fails. Now what?

The difference between crypto and traditional billing is this. With traditional payments, you have dunning. That's the retry logic. Customer's Visa fails. Your processor retries tomorrow. Fails again. Retries in three days. Fails a third time. You send an email. Account gets paused. That whole system (payment processor, email service, logic around escalating failures) is invisible to most SaaS founders. Stripe handles it. You don't think about it.

First scenario. The customer's wallet is empty. No tokens. The smart contract tries to pull the subscription payment. The transaction fails immediately. OK, so you retry later. But when you retry, you need the wallet to have tokens. The customer has to manually deposit. You can't retry a pull from an empty account. You can queue the intent, sure. But you're waiting for the customer to action something on their end.

Second scenario. The allowance expired. Some implementations build expiration into the approve() call. This is actually rare in subscriptions (usually approval is unlimited), but I've seen it. Customer approved you to spend 500 USDC. You've spent 120 over three months. There's still 380 in approval. But the approval was set to expire after 90 days. Now you're on day 91. The transaction fails because the approval is dead, even though the wallet has tokens.

Fourth scenario. Slippage and gas failures. The customer has tokens. The allowance is active. You submit the transaction. But the network is congested. Gas prices spike between the time you sign the transaction and the time it gets mined. Some blockchain implementations require gas to come from the smart contract itself (which creates its own can of worms). The transaction sits in the mempool and expires. Now you've got a partial pull that nobody knows about.

Pull versus push. There are two ways to implement recurring payments. Pull means your smart contract calls transferFrom() on the customer's wallet at a schedule. This is more common because it feels familiar. Push means the customer calls a function to say "please charge me." It's cleaner for security (customer in control) but requires participation. You could build a bot that watches due dates, but then the customer maintains that wallet, and if the bot fails or gets hacked, your system fails. Pull is worse but at least it's deterministic.