рдореБрдЦреНрдп рд╕рд╛рдордЧреНрд░реА рдкрд░ рдЬрд╛рдПрдВ

ЁЯОе How to Create a Secure NFT Gated Website

рдХреЛ рдЕрдкрдбреЗрдЯ
26 рдирд╡рдВрдмрд░, 2025

15 min read

Web3.js Deprecation

Web3.js, the popular JavaScript library for Ethereum, is no longer being actively maintained. For all new development, we recommend using either Ethers.js or Viem instead. Both options offer modern tooling and active community support.

If you have any questions or need guidance in transitioning to alternative solutions, please reach out to us.

рдЕрд╡рд▓реЛрдХрдитАЛ

Now that NFTs are being used as membership cards, event tickets, and status symbols, sites are looking for ways to gate their content based on whether a user owns an NFT from a given collection. We've seen Spotify, Shopify, and more roll out this "token-gating" functionality and countless tutorials on how to implement it yourself. Almost all of these implementations lack protections against a simple hack in which, once a user passes the check, they send the NFT to another user who can then access the gated content.

In the video below, we demonstrate how to create a secure NFT Gated website that prevents double spends and token sharing by alerting in real time of any token transfers using QuickAlerts.

рд╡реАрдбрд┐рдпреЛтАЛ

An end-to-end video course explaining how to Build a secure NFT-gated website using JavaScript
рдФрд░ рднреА рд╡реАрдбрд┐рдпреЛ рджреЗрдЦрдиреЗ рдХреЗ рд▓рд┐рдП рд╣рдорд╛рд░реЗ рдпреВрдЯреНрдпреВрдм рдЪреИрдирд▓ рдХреЛ рд╕рдмреНрд╕рдХреНрд░рд╛рдЗрдм рдХрд░реЗрдВ!

CodeтАЛ

Following are the code examples shown in the video. Alternatively, you can access the full repo on GitHub or clone it to your local with:

git clone https://github.com/kshitijofficial/nftGating.git

api/destination.jsтАЛ

const axios = require('axios');

const headers = {
'accept': 'application/json',
'x-api-key': 'YOUR API KEY'
};

const data = {
name: 'My Destination',
to_url: 'https://7de9-103-171-168-142.ngrok-free.app/webhook',
webhook_type: 'POST',
service: 'webhook',
payload_type: 5
};

axios.post('https://api.quicknode.com/quickalerts/rest/v1/destinations', data, { headers })
.then(response => console.log("Response Data",response.data))
.catch(error => console.log('error', error));

api/notification.jsтАЛ

const axios = require('axios');

const headers = {
'accept': 'application/json',
'x-api-key': 'YOUR API KEY'
};

const data = {
name: 'NFT Transfer',
expression: 'KHR4X2xvZ3NfdG9waWMxID1+ICdhMmM4MzQ1YjMwQjRjMjZlMTE4RjMwNzI2OWRFOTY5MTNEYmRCNGJDJykgJiYgDQoodHhfbG9nc19hZGRyZXNzID09ICcweEQ2MTg1ODE0MDIyMjZjOTJiMTRDOWY0ODcwNzk5YjMwMDBBQzRDNzcnKSAmJiANCih0eF9sb2dzX3RvcGljMCA9PSAnMHhkZGYyNTJhZDFiZTJjODliNjljMmIwNjhmYzM3OGRhYTk1MmJhN2YxNjNjNGExMTYyOGY1NWE0ZGY1MjNiM2VmJyk=',
network: 'ethereum-sepolia',
destinationIds: ['fa375e89-8c4c-4260-8c0b-91cb20cd4da9']
};

axios.post('https://api.quicknode.com/quickalerts/rest/v1/notifications', data, { headers })
.then(response => console.log(response.data))
.catch(error => console.log('error', error));

api/server.jsтАЛ

const express = require('express');
const cors = require('cors')
const {Web3} = require('web3');
const ABI =require('./ABI.json')
const socketIO = require('socket.io')
const app = express();
app.use(cors())
app.use(express.json());

const web3 =new Web3('YOUR API KEY')
const contractAddress = 'YOUR CONTRACT ADDRESS';
const contract = new web3.eth.Contract(ABI,contractAddress);
// console.log(contract)

const fetchNFTs = async(account)=>{
try{
const nftBalance = await contract.methods.balanceOf(account).call();
return {userNFTs:Number(nftBalance)}
}catch(error){
console.log('Error fetching NFTs',error);
}
}

app.post('/members',async(req,res)=>{
try{
const account = req.body.from;
console.log(account)
const numNFTs = await fetchNFTs(account)

if(numNFTs.userNFTs>0){
res.status(200).json({status:200,numNFTs})
}else{
res.status(404).json({status:404,message:"You don't own any nft",numNFTs});
}
}catch(error){
res.status(500).json({status:500,message:"Internal Server Error"});
}
})

app.post('/webhook',async(req,res)=>{
try{
const account = req.body[0].from;
const numNFTs = await fetchNFTs(account);
io.emit('nftsUpdated',{userNFTs:numNFTs.userNFTs})
res.status(200).json({status:200,message:"Webhook Triggered"})
}catch(error){
console.error(error)
}
})


const PORT=3000;
const server = app.listen(PORT,()=>{
console.log(`Sever running at ${PORT}`)
})
const io = socketIO(server);
io.on('connection',()=>{
console.log("Connected")
})

api/ABI.jsonтАЛ

[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_fromTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_toTokenId",
"type": "uint256"
}
],
"name": "BatchMetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "MetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "player",
"type": "address"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
}
],
"name": "awardItem",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

client/.eslintrc.cjsтАЛ

/* eslint-env node */

module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

client/index.htmlтАЛ

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

client/src/components/Home.cssтАЛ

.beautiful-sentence {
font-family: "Arial", sans-serif;
font-size: 18px;
color: #ff4081;
text-align: center;
animation: rainbow 5s infinite;
}
@keyframes rainbow {
0% { color: #ff4081; }
14% { color: #ff7f00; }
28% { color: #ffff00; }
42% { color: #00ff00; }
57% { color: #0000ff; }
71% { color: #4b0082; }
85% { color: #9400d3; }
100% { color: #ff4081; }
}

client/src/components/Home.jsxтАЛ

import {useNavigate,useLocation} from "react-router-dom"
import './Home.css'
const Home=()=>{
const location = useLocation()
const navigateTo=useNavigate()
const revealMsg=async()=>{
try{
const account = location.state.address;
const res = await fetch(`http://localhost:3000/members`,{
method:"POST",
headers:{
"content-type":"application/json"
},
body:JSON.stringify({from:account})
})
const data = await res.json();
if(data.status===200){
navigateTo("/members")
}else{
window.alert("You currently do not hold any NFTs in collection w/ address 0xd618581402226c92b14c9f4870799b3000ac4c77")
}
}catch(error){
console.error(error)
}
}
return(
<>
<span className="beautiful-sentence">I have a secret message for holders of my NFT collection with <br></br>address 0xd618581402226c92b14c9f4870799b3000ac4c77</span>
<br></br>
<br></br>
<button onClick={revealMsg}>Reveal Message</button>
</>
)
}
export default Home;

client/src/components/Members.jsxтАЛ

import {useState,useEffect} from "react";
import {useNavigate} from "react-router-dom"
import io from 'socket.io-client'
import welcomeImg from '../images/GM.png'
const Members=()=>{
const [socket,setSocket]=useState(null);
const navigateTo = useNavigate();

useEffect(()=>{
const socketInstance = io('http://localhost:3000');
setSocket(socketInstance);

return()=>{
socketInstance.disconnect()
}
},[])

useEffect(()=>{
if(socket){
socket.on('nftsUpdated',(data)=>{
if(data.userNFTs<1){
navigateTo('/')
alert("You've been logged out because you no longer hold any NFTs in collection with address 0xd618581402226c92b14c9f4870799b3000ac4c77")
}
})
}
},[socket])

return<>
<p>Thank you for being a holder of my NFT collection, here's your message:</p>
<img src={welcomeImg}></img>
</>
}
export default Members;

client/src/components/Wallet.jsxтАЛ

import { useNavigate } from "react-router-dom";
const Wallet=()=>{
const navigateTo =useNavigate()
const connectWallet =async()=>{
try{
if(window.ethereum){
const accounts = await window.ethereum.request({method:'eth_requestAccounts'});
navigateTo("/home",{state:{address:accounts[0]}})
}else{
alert("Install Metamask")
}
}catch(error){
console.error(error)
}
}
return<><button onClick={connectWallet}>Connect Wallet</button></>
}
export default Wallet;

client/src/images/GM.pngтАЛ

рд╣рдореЗрдВ рдЖрдкрдХреА рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдмрд╣реБрдд рдкрд╕рдВрдж рд╣реИ!

рдЕрдЧрд░ рдЖрдкрдХреЗ рдкрд╛рд╕ рдХреЛрдИ рд╕реБрдЭрд╛рд╡ рдпрд╛ рдирдП рд╡рд┐рд╖рдпреЛрдВ рдХреЗ рд▓рд┐рдП рдХреЛрдИ рдЕрдиреБрд░реЛрдз рд╣реЛ рддреЛ рд╣рдореЗрдВ рдЬрд░реВрд░ рдмрддрд╛рдПрдВ ред рд╣рдореЗрдВ рдЖрдкрд╕реЗ рд╕реБрдирдирд╛ рдЕрдЪреНрдЫрд╛ рд▓рдЧреЗрдЧрд╛ред

рдЗрд╕ рдЧрд╛рдЗрдб рдХреЛ рд╕рд╛рдЭрд╛ рдХрд░реЗрдВ