How to create a subscription-based dApp using ERC-20 tokens and smart contracts

 A few years ago, the idea of a decentralized subscription model seemed overly ambitious. After all, most subscription-based platforms like Netflix or Spotify depend heavily on centralized servers and payment systems. But as I became more immersed in blockchain development, I realized we could replicate—and even improve—subscription models using smart contracts and ERC-20 tokens. With Web3, control shifts from the middlemen to the users and developers, removing payment processors and recurring billing headaches.

In a traditional system, you subscribe using fiat money, your card is charged monthly, and the company controls everything—your access, your data, your payment details. But what if the subscription logic was coded into a smart contract, and payments were made automatically using ERC-20 tokens? That’s the heart of a decentralized subscription dApp.

This blog post is a detailed walk-through of how I built a working subscription dApp from scratch. I’ll break down how the contracts were structured, how the payment flow works, and how users interact with it. And I’ll do it in a simple, non-technical style—based on real experience, not theory.

Getting the Architecture Right

The first step was defining the flow of the dApp. I needed three core pieces:

  1. An ERC-20 token that users could use to pay for the subscription.

  2. A smart contract that checks if a user has paid and grants or denies access accordingly.

  3. A frontend (built in React) that lets users interact with the system—subscribe, check their status, and cancel.

The smart contract would be the gatekeeper. It would keep track of who subscribed, when they subscribed, and for how long their access remains valid. It would only accept the defined ERC-20 token as payment.

To keep things manageable, I created a token called SubToken (SUB) using the OpenZeppelin ERC-20 standard. This token was used for subscription payments. You could use any existing ERC-20 token like USDC, but building a demo token helped me understand the mechanics better.

Writing the ERC-20 Token Contract

This part was fairly straightforward. I used OpenZeppelin’s standard implementation and modified it slightly to allow minting for demo purposes.

solidity

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract SubToken is ERC20, Ownable { constructor() ERC20("Subscription Token", "SUB") {} function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }

In production, minting might be disabled or controlled via staking/farming logic. For the sake of development and demo, I allowed minting so I could test easily.

Designing the Subscription Smart Contract

The subscription contract is where most of the logic lives. It keeps a mapping of subscribers and their expiry dates, and only accepts tokens as payments. I also included a subscribe function that deducts tokens and updates the user’s access time.

Here’s a simplified version of how it worked:

solidity

pragma solidity ^0.8.0;
interface IERC20 { function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); } contract SubscriptionService { IERC20 public paymentToken; uint public subscriptionPrice; uint public duration; // e.g. 30 days mapping(address => uint256) public subscriptionExpiry; constructor(address _token, uint _price, uint _duration) { paymentToken = IERC20(_token); subscriptionPrice = _price; duration = _duration; } function subscribe() external { require(paymentToken.transferFrom(msg.sender, address(this), subscriptionPrice), "Payment failed"); if (block.timestamp > subscriptionExpiry[msg.sender]) { subscriptionExpiry[msg.sender] = block.timestamp + duration; } else { subscriptionExpiry[msg.sender] += duration; } } function isSubscribed(address user) external view returns (bool) { return subscriptionExpiry[user] >= block.timestamp; } }

This contract is clean and avoids complexity. The user must approve token spending beforehand, and once they call subscribe(), their subscription is extended.

One lesson I learned the hard way: always guard against reentrancy. Even with simple contracts, best practices like using OpenZeppelin’s ReentrancyGuard can save you from future headaches.

Integrating the Frontend and Wallets

The frontend was built with React and used Ethers.js to interact with both the token and the subscription contract. Users needed to:

  • Connect their MetaMask wallet
  • Approve the subscription contract to spend their SUB tokens
  • Subscribe
  • Check their subscription status

Here’s a function that checks if the user is still subscribed:

javascript

async function checkSubscription(address user) {
const result = await subscriptionContract.isSubscribed(user); setIsSubscribed(result); }

And to subscribe:

javascript

async function subscribe() {
const tx = await tokenContract.approve(subscriptionAddress, price); await tx.wait(); const subTx = await subscriptionContract.subscribe(); await subTx.wait(); alert("Subscribed successfully!"); }

The experience is smooth once it’s set up. No third-party payment gateways. No recurring billing. Just crypto and code.

Managing Recurring Payments Without Automation?

This is one area where many developers trip up. On Ethereum, smart contracts can’t trigger actions on their own—they’re passive. That means there’s no automatic billing unless someone (or something) calls the function.

My workaround was simple: the contract doesn’t auto-renew subscriptions. Instead, it grants access for 30 days (or whatever duration you set), and users must manually renew.

To improve the UX, I created a frontend reminder and even considered integrating Chainlink Keepers (now known as Automation) to ping the contract on behalf of users. That’s something I plan to experiment with more deeply, especially for enterprise-level use cases.

Gas Fees and Optimization

Every interaction on-chain costs gas. For testnets and dev environments, that’s fine. But in production, users don’t want to spend $10 just to subscribe.

To reduce friction, I deployed everything on Polygon, which is fast and affordable. Another option is BNB Chain or Optimism. Stick to EVM-compatible chains, and your Solidity contracts will work with little to no modification.

Also, avoid unnecessary writes in your contract. Only update state variables when you absolutely need to. Gas efficiency will improve the overall adoption of your dApp.

Real-World Use Cases I Explored

One idea I tried was a gated content dApp. Think of it as a decentralized Patreon. Creators could upload exclusive files or videos, and only wallet addresses with active subscriptions could unlock the content.

Another idea was premium Discord access. I wrote a simple bot that checked wallet addresses against the smart contract and assigned Discord roles accordingly. It created a seamless experience—users pay in crypto, get verified, and access the private group without human intervention.

This opens up massive potential for educators, artists, and even SaaS products looking to tokenize access without giving up revenue to platforms.

Common Pitfalls You Should Avoid

During this build, I faced a few issues that might save you hours of stress if you avoid them early:

  • Token approvals: Many users don't understand that ERC-20 tokens require approval before spending. Make sure your frontend guides them properly.
  • Time-based logic: Always compare against block.timestamp and handle edge cases. What happens if a user subscribes at the exact expiry second?
  • Gas estimation: Test on multiple wallets with varying balances to understand how much gas each function needs.
  • Hardcoding prices: Let your contracts support price updates through admin functions, but always use access control.
  • Testing without simulation: Use Hardhat or Foundry to write proper test cases. Don't rely on deploying to testnets alone.

Adding Optional NFT Integration for Identity

As I scaled the concept, I added NFT-based identity for subscribers. When users subscribed, they could optionally mint a “Subscriber Badge”—an NFT that represents their membership. This added a cool layer of ownership and community branding.

NFTs also made it easier to offer tiered access. For example, a gold-tier NFT gave access to premium content, while a silver-tier had limitations.

This structure creates real-world value: users can resell or even transfer their access (depending on how the contract is designed), something that isn’t possible with Web2 platforms.

Future Improvements and Onboarding UX

Onboarding remains a barrier. Most users aren’t crypto-savvy. Wallet setup, token transfers, and approvals feel foreign.

I plan to solve this by integrating on-ramps like MoonPay or Transak, so users can buy SUB tokens directly with a credit card. Also, using WalletConnect and supporting mobile-friendly dApps will make the experience smoother.

Eventually, I want to support fiat-to-crypto abstractions where users subscribe using local currency, but behind the scenes, smart contracts manage everything transparently on-chain.

Conclusion

Building a subscription-based dApp using ERC-20 tokens and smart contracts isn’t just technically achievable—it’s an opportunity to reimagine digital services with true ownership, decentralization, and transparency.

From the ERC-20 token to the subscription contract, and finally to the frontend and user experience, every piece of this system can be customized to your vision. The key is to build a system that’s fair, transparent, and valuable for users—not just technically impressive.

As blockchain continues to mature, this model will likely become a blueprint for future digital services, content platforms, and creator economies. So if you're thinking about building one, now is the perfect time to dive in

Post a Comment

0 Comments