Token Gates
Token gates let you lock down content, features, or whole pages based on what a user holds in their wallet. The user signs a message to prove they own the wallet, and Tribe checks their on-chain balance. Nothing gets transferred. It’s just a read: do you hold enough tokens or not?
How it works
Section titled “How it works”The flow is based on the x402 “Payment Required” protocol. Everything goes through one GET endpoint.
-
Create a gate in the Tribe dashboard under your site’s settings. Set the token mint, symbol, decimals, and the minimum amount a user needs to hold.
-
Hit the endpoint with no wallet. You’ll get back a
402with the gate requirements and the message format the user needs to sign. -
Have the user sign the message, then call the same endpoint again with their wallet address, the signature, and a timestamp as query params.
-
Tribe checks it all. It verifies the Ed25519 signature, makes sure the timestamp is fresh (within 5 minutes), looks up the wallet’s token balance on Solana, and returns
200if they’re good or403if they’re short.
API reference
Section titled “API reference”One endpoint handles everything:
GET /gate/:gateIdStep 1: Get the gate requirements
Section titled “Step 1: Get the gate requirements”Hit the endpoint with no query params:
const res = await fetch( `https://api.tribe.utopian.build/gate/${gateId}`);// HTTP 402const data = await res.json();The 402 response tells you what the gate needs:
{ "type": "token-gate", "version": "1", "gate": { "id": "cuid_abc123", "name": "Premium Access" }, "requirement": { "chain": "solana", "tokenMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "tokenSymbol": "USDC", "tokenDecimals": 6, "minAmount": "100" }, "auth": { "method": "ed25519", "message": "Access gate <gateId>\\nWallet: <address>\\nTimestamp: <unix_seconds>", "params": "?wallet=<address>&signature=<base58_sig>×tamp=<unix_seconds>" }, "message": "This resource requires holding at least 100 USDC."}Step 2: Sign the message and resubmit
Section titled “Step 2: Sign the message and resubmit”Build the message string, sign it with the wallet, and call the same endpoint with wallet, signature, and timestamp as query params:
import nacl from "tweetnacl";import { Keypair } from "@solana/web3.js";
const wallet = keypair.publicKey.toBase58();const timestamp = Math.floor(Date.now() / 1000).toString();
// Build the exact message the server expectsconst message = `Access gate ${gateId}\nWallet: ${wallet}\nTimestamp: ${timestamp}`;const messageBytes = new TextEncoder().encode(message);
// Sign with Ed25519const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);const signature = encodeBase58(signatureBytes); // base58-encoded
const res = await fetch( `https://api.tribe.utopian.build/gate/${gateId}` + `?wallet=${wallet}&signature=${signature}×tamp=${timestamp}`);Response codes
Section titled “Response codes”| Status | Meaning | When |
|---|---|---|
402 | Payment Required | No wallet provided, returns gate requirements |
401 | Unauthorized | Missing, invalid, or expired signature |
200 | Allowed | Signature checks out and balance is sufficient |
403 | Forbidden | Signature is valid but the wallet doesn’t hold enough |
200 response (they’re in):
{ "allowed": true, "gate": { "id": "cuid_abc123", "name": "Premium Access" }, "wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "balance": "250.5", "required": "100", "tokenSymbol": "USDC"}403 response (not enough tokens):
{ "allowed": false, "gate": { "id": "cuid_abc123", "name": "Premium Access" }, "wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "balance": "42", "required": "100", "deficit": "58", "tokenSymbol": "USDC", "message": "Wallet holds 42 USDC but 100 is required."}React example
Section titled “React example”A full component that fetches gate requirements, asks the user to sign, and checks access:
import { useWallet } from "@solana/wallet-adapter-react";import nacl from "tweetnacl";import { useState, useEffect } from "react";
function TokenGatedContent({ gateId, children }) { const { publicKey, signMessage } = useWallet(); const [state, setState] = useState< "loading" | "needs-wallet" | "allowed" | "denied" >("loading"); const [requirement, setRequirement] = useState(null); const [deficit, setDeficit] = useState(null);
useEffect(() => { checkGate(); }, [publicKey]);
const checkGate = async () => { setState("loading");
if (!publicKey || !signMessage) { // No wallet connected yet, just grab the requirements const res = await fetch( `https://api.tribe.utopian.build/gate/${gateId}` ); const data = await res.json(); setRequirement(data.requirement); setState("needs-wallet"); return; }
// Wallet connected: sign and verify const wallet = publicKey.toBase58(); const timestamp = Math.floor(Date.now() / 1000).toString(); const message = `Access gate ${gateId}\nWallet: ${wallet}\nTimestamp: ${timestamp}`; const messageBytes = new TextEncoder().encode(message); const signatureBytes = await signMessage(messageBytes); const signature = encodeBase58(signatureBytes);
const res = await fetch( `https://api.tribe.utopian.build/gate/${gateId}` + `?wallet=${wallet}&signature=${signature}×tamp=${timestamp}` ); const data = await res.json();
if (res.status === 200) { setState("allowed"); } else { setDeficit(data.deficit); setState("denied"); } };
if (state === "loading") return <p>Checking access...</p>;
if (state === "needs-wallet") { return ( <div> <p> Connect a wallet holding at least {requirement.minAmount}{" "} {requirement.tokenSymbol} to access this content. </p> </div> ); }
if (state === "denied") { return ( <div> <p> Your wallet needs {deficit} more {requirement?.tokenSymbol} to unlock this content. </p> </div> ); }
return children;}Use cases
Section titled “Use cases”- Premium content like articles, videos, or downloads gated by token holdings
- Feature unlocks for users who hold a specific token
- NFT-gated access by setting a minimum balance of 1 for a given mint
- DAO-gated features that only governance token holders can use