Разработка Discord-бота раздачи ролей по NFT
Token gating — стандартная практика для NFT-сообществ. Держишь NFT из коллекции — получаешь доступ к приватным каналам, early access, приватным drop-ам. Технически это верификация: кошелёк с нужным NFT → Discord аккаунт → роль. Задача выглядит простой, но между «верификацией» и «реальным постоянным доступом» — целый слой инфраструктуры, который ломается предсказуемо.
Архитектура системы
Связка wallet ↔ Discord
Пользователь кликает «Verify» → редирект на verification page → подключает кошелёк через WalletConnect v2 → подписывает сообщение (sign message, без gas) → сервер проверяет подпись → сохраняет {discordId: walletAddress}.
Sign message — не транзакция, пользователь ничего не платит. Стандартное сообщение для верификации:
Verify Discord: username#1234
Nonce: a3f8b2c1
Timestamp: 1711234567
Nonce — случайная строка, уникальная для каждой сессии, с TTL 5 минут. Без nonce — возможен replay attack: скопированная подпись может быть переиспользована.
Верификация подписи на бэкенде:
import { verifyMessage } from 'viem';
const isValid = await verifyMessage({
address: claimedAddress,
message: expectedMessage,
signature: userSignature
});
Проверка NFT ownership: три подхода
Прямой RPC вызов. balanceOf(wallet, tokenId) через ethers.js или viem. Простой, работает для маленьких коллекций. Проблема: при 10,000 пользователях — 10,000 RPC вызовов при каждой проверке.
Alchemy/Moralis NFT API. getNFTsForOwner(wallet, contractAddress) — один запрос, возвращает все токены. Быстро, но зависимость от внешнего сервиса. При даунтайме Alchemy — бот не работает.
The Graph subgraph. Индексируем Transfer события коллекции, строим {owner: [tokenIds]} маппинг. Быстрый GraphQL запрос. Задержка индексации ~1-5 минут — при быстрой продаже токена пользователь ещё 5 минут сохраняет роль.
Для продакшн ботов используем Alchemy NFT API как primary с The Graph как backup и прямым RPC как последний fallback.
Discord bot: slash commands и event handling
Бот реализован на discord.js v14. Ключевые slash commands:
-
/verify— начало верификации, бот отправляет ephemeral message со ссылкой -
/check— принудительная проверка владения (для пользователей, которые продали токен) -
/roles— показать все роли и требования к ним
Роли назначаются через guild.members.cache.get(userId)?.roles.add(roleId). Требует permission MANAGE_ROLES и чтобы роль бота была выше назначаемых ролей в hierarchy — частая ошибка при настройке.
async function syncUserRoles(userId: string, wallet: string): Promise<void> {
const member = await guild.members.fetch(userId);
const ownedTokens = await getNFTsForOwner(wallet, CONTRACT_ADDRESS);
for (const [roleId, requirement] of ROLE_REQUIREMENTS) {
const qualifies = checkQualification(ownedTokens, requirement);
if (qualifies && !member.roles.cache.has(roleId)) {
await member.roles.add(roleId);
} else if (!qualifies && member.roles.cache.has(roleId)) {
await member.roles.remove(roleId);
}
}
}
Периодическая ресинхронизация
Критический момент: пользователь продал NFT, должен потерять роль. Бот не получает событие от Discord — он должен сам периодически проверять.
Cron job каждые 10-30 минут: для каждого верифицированного пользователя проверяем текущий баланс, обновляем роли. При 1000 пользователях и 30-минутном интервале — ~33 API запроса в минуту. Укладывается в лимиты Alchemy.
Оптимизация: слушаем Transfer события контракта через WebSocket (Alchemy WebSocket API). При любом Transfer проверяем, задействован ли верифицированный кошелёк, и немедленно обновляем роль. Это снижает latency до секунд.
const provider = new WebSocketProvider(ALCHEMY_WS_URL);
const contract = new Contract(NFT_ADDRESS, erc721Abi, provider);
contract.on('Transfer', async (from, to, tokenId) => {
const affectedWallets = [from, to].filter(w => w !== ethers.ZeroAddress);
for (const wallet of affectedWallets) {
await syncRolesForWallet(wallet);
}
});
Поддержка нескольких коллекций и trait-based роли
Реальные проекты требуют сложных условий:
- Держишь ≥3 токена из коллекции A → VIP роль
- Держишь токен с trait «Legendary» → Legendary роль
- Держишь токен из коллекции A и коллекции B → Collab роль
Для trait-based ролей нужен доступ к метаданным. Alchemy getNFTsForOwner возвращает tokenMetadata включая attributes. Конфигурация ролей в JSON:
{
"LEGENDARY_ROLE_ID": {
"contract": "0x...",
"minBalance": 1,
"requiredTrait": {"trait_type": "Rarity", "value": "Legendary"}
}
}
Стек
| Компонент | Технология |
|---|---|
| Бот | discord.js v14, TypeScript |
| Wallet connect | WalletConnect v2 (web app для верификации) |
| NFT data | Alchemy NFT API + WebSocket |
| База данных | PostgreSQL (userId ↔ wallet маппинг) |
| Хостинг | Railway или Render (persistent process) |
| Верификация подписей | viem verifyMessage |
Ориентиры по срокам
Базовый бот с одной коллекцией и одной ролью — 3-4 дня. Расширенный с несколькими коллекциями, trait-based ролями и real-time sync через WebSocket — 4-5 дней.







