Token-Gated Access System Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Token-Gated Access System Development
Medium
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Token-Gated Access System Development

Token-gating—restricting access to content or features based on owning specific tokens. Technically solvable in 20 minutes with ready solutions like Lit Protocol or Guild.xyz. But in 9 of 10 real cases, you need custom implementation: non-standard conditions, integration with existing auth, multi-chain checks, performance requirements.

Architectural Variants

Client-side (bad)—check balance in browser, show/hide content. Content is already in DOM; anyone can see it via DevTools. Only works for soft gates (nice-to-have, non-critical content).

Server-side (correct)—server checks token ownership before serving content. Client only gets what they have access to.

JWT with blockchain claims (optimal)—on SIWE login, check tokens, write permissions to JWT. Subsequent requests only need JWT verification, no blockchain calls.

SIWE + Token Check at Login

// After successful SIWE verification
async function createSessionToken(address: string): Promise<string> {
  const [nftBalance, tokenBalance, ensName] = await Promise.all([
    checkNFTOwnership(address),
    checkTokenBalance(address),
    resolveENS(address),
  ]);
  
  const roles: string[] = [];
  if (nftBalance > 0) roles.push("nft_holder");
  if (tokenBalance >= parseEther("100")) roles.push("token_holder");
  if (ensName) roles.push("ens_user");
  
  return jwt.sign(
    {
      sub: address,
      roles,
      // Expires in 24 hours—balance could change
      exp: Math.floor(Date.now() / 1000) + 86400,
    },
    process.env.JWT_SECRET!
  );
}

Important: JWT expiry. Tokens are bought and sold. A week-long session means someone who sold their NFT keeps access for 7 days. For high stakes—short expiry (1-4 hours) + refresh via re-verification.

On-Chain Checks via viem/wagmi

import { createPublicClient, http, erc721Abi, erc20Abi } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
  chain: mainnet,
  transport: http(process.env.ETH_RPC),
});

// Check ERC-721 ownership
async function checkNFTOwnership(
  address: `0x${string}`,
  collection: `0x${string}`
): Promise<number> {
  const balance = await client.readContract({
    address: collection,
    abi: erc721Abi,
    functionName: "balanceOf",
    args: [address],
  });
  return Number(balance);
}

// Check specific tokenId
async function checkSpecificToken(
  address: `0x${string}`,
  collection: `0x${string}`,
  tokenId: bigint
): Promise<boolean> {
  const owner = await client.readContract({
    address: collection,
    abi: erc721Abi,
    functionName: "ownerOf",
    args: [tokenId],
  });
  return owner.toLowerCase() === address.toLowerCase();
}

Multi-Chain Gates

User might hold NFT on Ethereum, tokens on Polygon. Check in parallel:

const [ethBalance, polyBalance, arbBalance] = await Promise.all([
  checkBalanceOnChain(address, "ethereum"),
  checkBalanceOnChain(address, "polygon"),
  checkBalanceOnChain(address, "arbitrum"),
]);

const hasAccess = ethBalance > 0 || polyBalance > 0 || arbBalance > 0;

Cache results in Redis with 5-15 minute TTL—constant node calls are expensive and slow.

Complex Access Conditions

Real-world gates are rarely simple. Typical cases:

// AND: need both NFT and sufficient token balance
const hasAccess = nftBalance > 0 && tokenBalance >= minTokenThreshold;

// OR: any condition sufficient
const hasPremium = nftBalance > 0 || tokenBalance >= premiumThreshold || hasStaked;

// Trait-based: specific NFT attributes
// Requires off-chain metadata or on-chain traits
const isGoldMember = await checkTraitOwnership(address, "tier", "gold");

// Snapshot-based: balance at specific block (for airdrop eligibility)
const snapshotBalance = await client.readContract({
  ...tokenContract,
  functionName: "balanceOfAt",
  args: [address, snapshotBlockNumber],
  blockNumber: snapshotBlockNumber,
});

Middleware for API Routes

// Express/Fastify middleware
async function tokenGateMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  
  if (!token) return res.status(401).json({ error: "Unauthorized" });
  
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET) as JWTPayload;
    
    // Check roles from JWT (no blockchain call)
    if (!payload.roles.includes("nft_holder")) {
      return res.status(403).json({ error: "NFT required" });
    }
    
    req.user = payload;
    next();
  } catch {
    return res.status(401).json({ error: "Invalid token" });
  }
}

// Apply to protected routes
app.get("/api/premium/content", tokenGateMiddleware, getPremiumContent);

Frontend: UX for Access Denial

Nothing worse than user not understanding why they're blocked or what to do:

function GatedContent({ requiredNFT, children }) {
  const { address } = useAccount();
  const { data: balance } = useReadContract({
    address: requiredNFT,
    abi: erc721Abi,
    functionName: "balanceOf",
    args: [address],
    query: { enabled: !!address },
  });
  
  if (!address) {
    return <ConnectWalletPrompt />;
  }
  
  if (!balance || balance === 0n) {
    return (
      <AccessDenied
        message="NFT from XYZ collection required to access"
        ctaText="Buy on OpenSea"
        ctaUrl={`https://opensea.io/collection/xyz`}
        currentFloorPrice={useFloorPrice(requiredNFT)}
      />
    );
  }
  
  return children;
}

Delegation via delegate.cash

User doesn't want to connect their cold wallet with valuable NFTs. delegate.cash (EIP-5639) lets the owner delegate rights to a hot wallet:

// Check not just direct ownership, but delegation
const { data: isDelegated } = useReadContract({
  address: DELEGATE_REGISTRY,
  abi: delegateRegistryAbi,
  functionName: "checkDelegateForContract",
  args: [hotWallet, coldWallet, nftCollection],
});

const hasAccess = directBalance > 0 || isDelegated;

For production gates with valuable content—delegate.cash support is mandatory.