Skip to content

Building a gated group with NFT verification

In this tutorial, you’ll learn how to create and manage a private group where membership is gated by NFT ownership. We’ll build an Express server that verifies a wallet before adding it to the group, making it easy to manage NFT-based access to a conversation.

Start the XMTP agent

Start your XMTP client and start listening to messages from the bot.

import { XMTP } from "xmtp";
 
const xmtp = new XMTP(onMessage, {
  encryptionKey: ENCRYPTION_KEY,
});
 
await xmtp.init();

Create a gated group

Use the createGroup function to create a new group conversation and set both the user and the bot as super admins. Once created, you’ll have a groupId you can use to add others:

export async function createGroup(
  client: Client | undefined,
  members: string[],
  senderAddress: string | undefined,
  clientAddress: string | undefined,
) {
  try {
    await client?.conversations.sync();
    const group = await client?.conversations.newGroup([members]);
    console.log("Group created", group?.id);
 
    // Grab members and find the sender
    const members = await group?.members();
    const senderMember = members?.find((member) =>
      member.accountAddresses.includes(senderAddress?.toLowerCase() ?? ""),
    );
 
    if (!senderMember) {
      console.log("Sender not found in members list");
      return undefined;
    }
 
    // Grant superAdmin privileges
    const senderInboxId = senderMember.inboxId;
    await group?.addSuperAdmin(senderInboxId);
    console.log(
      "Sender is superAdmin",
      await group?.isSuperAdmin(senderInboxId),
    );
 
    // Send welcome messages
    await group?.send(`Welcome to the new group!`);
    await group?.send(`You are now the admin of this group as well as the bot`);
 
    return group;
  } catch (error) {
    console.log("Error creating group", error);
    return undefined;
  }
}
What this does:
  • Creates a new group conversation.
  • Syncs conversations and grabs the newly created group’s ID.
  • Finds the senderAddress in the group’s member list.
  • Promotes the sender to superAdmin.
  • Sends a couple of welcome messages.

Spin up the verification server

The server provides a single endpoint to add a wallet address to a group—but only if the wallet holds the right NFT.

src/index.ts
export function startGatedGroupServer(client: Client) {
  async function addWalletToGroup(
    walletAddress: string,
    groupId: string,
  ): Promise<string> {
    // Check if wallet holds the "XMTPeople" NFT
    const verified = await checkNft(walletAddress, "XMTPeople");
    if (!verified) {
      console.log("User can't be added to the group");
      return "not verified";
    } else {
      try {
        // If verified, add the wallet to the group
        await addToGroup(groupId, client, walletAddress);
        return "success";
      } catch (error: any) {
        console.log(error.message);
        return "error";
      }
    }
  }
 
  const app = express();
  app.use(express.json());
 
  // POST endpoint that takes a wallet address and groupId
  app.post("/add-wallet", async (req, res) => {
    try {
      const { walletAddress, groupId } = req.body;
      const result = await addWalletToGroup(walletAddress, groupId);
      res.status(200).send(result);
    } catch (error: any) {
      res.status(400).send(error.message);
    }
  });
 
  // Start the server
  const PORT = process.env.PORT || 3000;
  const url = process.env.URL || `http://localhost:${PORT}`;
  app.listen(PORT, () => {
    console.warn(
      `Use this endpoint to add a wallet to a group using the groupId\n${url}/add-wallet <body: {walletAddress, groupId}>`,
    );
  });
}
Key points:
  • checkNft(walletAddress, "XMTPeople"): a function (not shown here) that verifies if a wallet holds the “XMTPeople” NFT.
  • Only verified addresses are added to the group with addToGroup(...).
  • The server logs important messages to keep you informed.

Check the NFT with alchemy

import { Alchemy, Network } from "alchemy-sdk";
 
const settings = {
  apiKey: process.env.ALCHEMY_API_KEY, // Replace with your Alchemy API key
  network: Network.BASE_MAINNET, // Use the appropriate network
};
 
export async function checkNft(
  walletAddress: string,
  collectionSlug: string,
): Promise<boolean> {
  const alchemy = new Alchemy(settings);
  try {
    const nfts = await alchemy.nft.getNftsForOwner(walletAddress);
 
    const ownsNft = nfts.ownedNfts.some(
      (nft: any) =>
        nft.contract.name.toLowerCase() === collectionSlug.toLowerCase(),
    );
    console.log("is the nft owned: ", ownsNft);
    return ownsNft as boolean;
  } catch (error) {
    console.error("Error fetching NFTs from Alchemy:", error);
  }
 
  return false;
}

Test the Endpoint

Once your server is running (by default on port 3000), test the add-wallet endpoint with your chosen wallet and the groupId you received from createGroup:

curl -X POST http://localhost:3000/add-wallet \
 -H "Content-Type: application/json" \
 -d '{"walletAddress": "0x93E2fc3e99dFb1238eB9e0eF2580EFC5809C7204", "groupId": "b9ab876c87ef3cf63b81c8d45c824fae"}'

If the wallet is verified for your NFT, you should get a "success" response, and the user will be added to the group. Otherwise, you’ll see "not verified" or "error" in the response.

That’s It!

You now have a gated group chat that only NFT holders can access. Combine these steps with any front-end application to create exclusive communities, coordinate DAO discussions, or manage membership-based chat groups. Happy building!