Automated Recurring Crypto Payments System
Blockchain is inherently a push system. Nobody can withdraw funds from your wallet without your transaction signature. Recurring payments in crypto aren't "a subscription that pulls money monthly" — it's an architectural problem: how to organize automatic payouts without constant user presence while maintaining security.
Implementation Models
Three fundamentally different approaches, each solving different tasks.
Pull model with approval. Recipient (or protocol) can withdraw themselves, but only within approved allowance. Standard ERC-20 scheme: user once does approve(spender, amount), then spender calls transferFrom on schedule.
Problem — unlimited approve. User approves type(uint256).max, and if the contract is compromised, all funds are at risk. Correct: approve specific amount + allowance resets after each payout.
Escrow with schedule. User deposits funds into storage contract, contract pays on schedule. User retains control via pause/cancel function. More secure architecture — funds are locked, but user knows exactly what and when will be spent.
Streaming payment (streaming). Superfluid and Sablier protocols implement continuous token flow: funds flow per second, recipient can withdraw accumulated any time. Especially good for salaries, vesting, rent payments.
Contract Architecture
For a custom recurring payment system, build around several key elements:
struct PaymentSchedule {
address payer;
address payee;
address token; // address(0) for ETH
uint256 amount; // amount per period
uint256 period; // in seconds
uint256 nextPaymentAt; // timestamp of next payment
uint256 maxPayments; // 0 = infinite
uint256 completedPayments;
bool active;
}
mapping(bytes32 => PaymentSchedule) public schedules;
Key functions:
-
createSchedule()— user creates subscription, first payment optionally immediately -
processPayment(bytes32 scheduleId)— executes next payout (called by keeper) -
cancelSchedule()— user cancels subscription -
pauseSchedule()/resumeSchedule()— temporary pause
Protection from double-spending. nextPaymentAt updates before fund transfer (Check-Effects-Interactions). Add paymentNonce — unique counter per payment, protection from replay in multisig scenarios.
Payment Automation: Who Calls the Contract
The contract won't call itself. Need an external trigger.
Chainlink Automation (formerly Keepers). Decentralized network of nodes monitoring condition and calling contract:
import "@chainlink/contracts/src/v0.8/automation/AutomationCompatible.sol";
contract RecurringPayments is AutomationCompatibleInterface {
function checkUpkeep(bytes calldata)
external view override
returns (bool upkeepNeeded, bytes memory performData)
{
bytes32[] memory dueSchedules = getDueSchedules(); // schedules with nextPaymentAt <= block.timestamp
upkeepNeeded = dueSchedules.length > 0;
performData = abi.encode(dueSchedules);
}
function performUpkeep(bytes calldata performData) external override {
bytes32[] memory scheduleIds = abi.decode(performData, (bytes32[]));
for (uint i = 0; i < scheduleIds.length; i++) {
_processPayment(scheduleIds[i]);
}
}
}
Chainlink Automation — reliable choice for mainnet. Cost — pay LINK per upkeep call plus gas. Registration takes minutes via web interface or programmatically.
Gelato Network. Chainlink Automation alternative with more flexible triggers. Supports time-based and event-based triggers. Can pay in ETH instead of native token.
Custom backend keeper. For B2B solutions or when full customization needed: backend monitors contract, calls processPayment. Centralized but simpler to debug. Often preferred for enterprise clients.
Limit Management and Security
Per-period amount limit. User sets maximum single payout size when creating subscription. Keeper calling payout with higher amount — revert.
Time window. Payment is overdue if not executed within graceWindow after nextPaymentAt. If keeper doesn't call within window — payment skipped (or accumulates, depends on business logic).
Pause on insufficient funds. If escrow account lacks tokens — instead of revert, contract emits InsufficientFunds event and deactivates schedule. Keeper reads event and notifies user (via backend + email/push).
Native Currency vs Tokens
ETH payments simpler to implement but harder to manage: user must keep ETH in contract. Tokens (ERC-20) more convenient for stablecoin payments (USDC, DAI) — user approves contract to spend tokens from their wallet, keeps them themselves.
For B2C recurring payments (subscriptions) — recommend USDC on Polygon or Arbitrum. Low gas, stable cost, wide support.
| Criteria | ETH/MATIC native | ERC-20 (USDC) |
|---|---|---|
| Complexity | Simpler | Slightly harder |
| Fund storage | In contract (escrow) | With user (approve) |
| Amount predictability | Depends on rate | Stable (stablecoin) |
| User UX | Worse (must top up contract) | Better |
Development Process
Design (1-2 days). Determine model (pull/escrow/streaming), choose keeper, draw state machine for schedule (active → paused → cancelled → completed).
Contract development (4-6 days). Write in Solidity 0.8+, OpenZeppelin ReentrancyGuard, Pausable. Tests via Foundry — fuzzing time boundary conditions especially important.
Keeper integration (1-2 days). Chainlink Automation registration or Gelato task deployment.
Backend and notifications (2-3 days). Monitor contract events via ethers.js or viem, send user notifications.
Total timeline — 1-2 weeks depending on complexity and number of integrations.







