A client-side only Ethereum paywall demonstration using the Ethereum Base network
Paywalls are fundamental tools in the industry that help creators monetize their content, specific features, and games. They're essential for sustainable content creation and service provision.
Recently, I considered implementing a paywall for a special feature in a web app I developed. However, I didn't want to use any backend technology - not due to lack of skill, but because I didn't want to maintain any backend infrastructure at the moment.
I analyzed several options like Cardano (which I'm somewhat familiar with), Coinbase, BitPay, and Stripe. However, all these familiar options required some backend to verify important information.
That's when I realized that with Ethereum, it's possible to interact with smart contracts without needing a backend.
So I opened Cursor AI and started sketching what would become my paywall proof of concept with Ethereum.
You can view all contract information at: https://basescan.org/address/0xd7827bb72b456c64c7a18617e6510b7a9c95558a
The demo is available at https://tbbc.app/ethereum-paywall/
There's a video recording demonstrating the purchase flow, which I'm embedding below:
Contract read methods are usually free - you typically pay more for writing to a contract, not so much for reading from one.
This means users pay a small gas fee when purchasing their access, but when the application reloads and checks whether the user has access, the operation is free.
Let's start by understanding the fundamental requirements for creating a smart contract on Ethereum. First, you'll need to access Remix IDE at https://remix.ethereum.org/.
Remix is a powerful web-based IDE where you can build your smart contract, validate it, and deploy it to the production network. It provides everything you need to get started with Ethereum development without installing any local tools.
I won't get into the details of writing a contract here because it's quite simple. The contract I built for my Ethereum paywall can be viewed here: https://basescan.org/address/0xd7827bb72b456c64c7a18617e6510b7a9c95558a
Once I had my contract address and ABIs, I asked Cursor to help me with the code. Here are the most relevant pieces of code it generated:
// Contract details
const contractAddress = '0xd7827bb72b456c64c7a18617e6510b7a9c95558a'; // Your contract address here
// Your contract ABI here
const contractABI = []
The ABI is easy to find inside the Remix IDE within a JSON file. It essentially describes each method defined in your contract, including their arguments and outputs.
Contract instance: Then we just combine these two pieces of data to create the contract instance:
const web3 = new Web3(window.ethereum);
contract = new web3.eth.Contract(contractABI, contractAddress);
Check for access: Check whether a wallet has bought access or not:
const hasAccess = await contract.methods.hasAccess(account).call();
Purchase: When calling this method, it triggers the wallet signing process with all the information so the user can make an informed decision to buy it or not:
const web3 = new Web3(window.ethereum);
const priceWei = web3.utils.toWei(ACCESS_PRICE_ETH.toString(), 'ether');
const transaction = await contract.methods.purchaseAccess().send({
from: account,
value: priceWei
});
The method above will trigger this dialog:
This is basically ALL the code that is currently interacting with the contract - this is amazing!
We need to get the user's account from their installed wallet. Here's what we do:
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
account = accounts[0];
Before checking with contract.hasAccess(account), we must ask the user to sign a piece of text to verify they are truly the account owner:
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const address = accounts[0];
const message = `Verify wallet ownership for ${address} at ${Date.now()}`;
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address]
});
This will trigger a dialog like this one:
The method above returns a signature that is stored client-side, and on every reload, we check whether the signature was signed by the current wallet:
const web3 = new Web3(window.ethereum);
const recoveredAddress = await web3.eth.personal.ecRecover(message, signature);
const isValid = recoveredAddress.toLowerCase() === address.toLowerCase();
This provides some level of security that the wallet provided to the contract.hasAccess method is indeed owned by the user.
The main security issue with frontend-only solutions is that any information can be modified, and the biggest benefit of having a backend is that only you, as the application owner, can modify certain information, providing a high level of control and security.
You could argue that if a user who has bought access shares their signature and wallet address with someone else, they could be impersonated, but that's also true with many website cookies today.
Since I wanted something that's just easy to verify access on subsequent page reloads, this solution is pretty reliable - and it's not like I'm solving a huge industry problem.
I just want to have fun building small utility tools and help individuals pay with less friction while maintaining less code and bureaucracy.
Thank you for reading! I'm on 𝕏 as @imfelquis Check out my other web experiments on my website at https://tbbc.app/
Also, shoutout to @MarcoWorms for funding my original wallet and recommending the Ethereum L2 Base network