How to Build Token-Gated Access with an ERC-721 Smart Contract
Review the parts of ERC-721 token-gated access: wallet authentication, ownership checks, session expiry, and revocation after transfers.
Token-gated access uses token ownership as a condition for accessing content or features. The application must verify control of the wallet, check ownership, and decide when to repeat that check after access is granted.
How Token-Gated Access Works
Token gating is a permission check: does this wallet hold the required token? The basic flow is:
- User connects their wallet to your application.
- Your backend requests a signed message from the wallet to prove ownership.
- Your backend verifies the signature and checks the relevant smart contract for token ownership.
- Access is granted or denied based on the result.
The NFT contract itself does not gate anything. Your application does. The contract is the source of truth; your backend enforces the rule.
Step 1: Wallet Connection and Signature Verification
Never trust a wallet address the client sends you. A client can send any string. You need a cryptographic proof that the user controls the wallet.
Use a Sign-In with Ethereum library that validates the complete message, including the expected domain, URI, chain, nonce, and time constraints. Contract wallets need the appropriate signature-validation path. The abbreviated sequence and signature-only example below are not a complete SIWE verifier. ERC-4361 requirements.
- Generate a nonce (random value) server-side and associate it with the session.
- Ask the user to sign a message that includes the nonce.
- Recover the signing address from the signature using
ecrecover(or a library likeethers.json the server). - Compare the recovered address against what the client claimed.
// Pseudo-code — server-side verification
const recovered = ethers.verifyMessage(message, signature);
if (recovered.toLowerCase() !== claimedAddress.toLowerCase()) {
throw new Error("Signature mismatch");
}
Bind the nonce to the intended session, expire it, and consume it once verification succeeds. Signature recovery alone does not perform those checks.
Step 2: Checking Token Ownership On-Chain
Once you have a verified address, query the contract. For ERC-721:
// Using ethers.js
const contract = new ethers.Contract(NFT_ADDRESS, ERC721_ABI, provider);
const balance = await contract.balanceOf(verifiedAddress);
const hasAccess = balance > 0;
For ERC-1155 with a specific token type:
const balance = await contract.balanceOf(verifiedAddress, TOKEN_ID);
const hasAccess = balance > 0;
This call happens server-side against an RPC provider you control. Never trust the client to report its own balance.
Step 3: Session Management
Checking the contract on every request is expensive and slow. Issue a short-lived session token (JWT or signed cookie) after a successful gate check. Set a reasonable expiry — hours, not days — and re-verify on expiry.
Include the wallet address and the contract address checked in the session payload. This makes debugging and auditing tractable when users report access issues.
Step 4: Handling Transfers and Revocations
Define what happens when a user sells or transfers the NFT after gaining access. Your session token may still be valid while the user no longer holds the required asset.
Options:
- Short session lifetimes. Force re-verification frequently. Annoying for users but simple to implement.
- Webhook invalidation. Listen to
Transferevents from the contract and invalidate sessions when a token leaves a verified address. Requires an event indexer or webhook service. - On-demand re-check. Verify ownership at the start of each meaningful action rather than at session creation. Adds latency but is accurate.
Choose session lifetimes and re-check points based on how quickly access must be revoked after a transfer, especially for sensitive actions such as downloading or transacting.
Common Implementation Mistakes
Trusting the Client-Reported Address
Without signature verification, a user can submit an address they do not control.
Checking Ownership Once at Registration
Gating only at signup creates a permanent credential from a temporary ownership state. The user sells the NFT, your gate stays open.
Not Accounting for Contract Proxies
Some NFT collections use proxy patterns. Confirm the chain, deployed address, and ABI; an incorrect address or interface can cause failed or incorrect reads.
Ignoring RPC Rate Limits
If your application has concurrent users, naive per-request RPC calls will hit rate limits fast. Cache ownership lookups with a short TTL (30–60 seconds) or use a dedicated indexing service.
On-Chain vs Off-Chain Enforcement
The check described above is off-chain enforcement — your server decides who gets in. This is fine for content, community access, and application features.
On-chain enforcement — where the smart contract itself restricts a function call — is appropriate when the action being gated is itself a blockchain transaction. For example, a staking contract that only allows NFT holders to participate can check ownerOf(tokenId) == msg.sender directly in Solidity. No backend needed.
Use on-chain enforcement for on-chain actions. Use off-chain enforcement for off-chain resources.
Choosing What to Gate
Common use cases:
- Content access: gated articles, videos, downloads
- Community access: Discord roles via bots, private forums
- Application features: advanced analytics, API quota tiers
- Event access: ticketed livestreams, IRL check-in
The engineering implementation is the same across all of them. The policy — which contract, which token IDs, minimum balance — varies by product.