Разработка NFT-gated контента
Типичная ошибка при реализации NFT-gated доступа — проверять ownership только на фронтенде. Пользователь подключает кошелёк, JS делает ownerOf(tokenId), получает адрес, сравнивает с account из кошелька — всё, доступ открыт. Проблема: эту проверку тривиально обойти из DevTools. Весь gating должен происходить на бэкенде, фронтенд только инициирует flow.
Как правильно верифицировать ownership
Схема с подписью сообщения (Sign-In With Ethereum)
Стандарт EIP-4361 (SIWE) — правильный путь. Пользователь подписывает стандартизированное сообщение своим приватным ключом, бэкенд верифицирует подпись и проверяет ownership контракта.
Схема:
- Фронтенд запрашивает у бэкенда nonce для адреса (защита от replay-атак)
- Формирует SIWE message — стандартный текст с доменом, адресом, nonce, timestamp, expiry
- Пользователь подписывает через кошелёк (
personal_sign) - Бэкенд верифицирует подпись: восстанавливает адрес из подписи через
ecrecover, проверяет nonce, проверяет timestamp, проверяетownerOf/balanceOfна контракте
// Backend verification (Node.js)
import { SiweMessage } from "siwe"
import { createPublicClient, http } from "viem"
async function verifyNFTAccess(message: string, signature: string, contractAddress: string) {
const siweMessage = new SiweMessage(message)
const { success, data } = await siweMessage.verify({ signature })
if (!success) throw new Error("Invalid signature")
if (data.nonce !== await getNonce(data.address)) throw new Error("Invalid nonce")
if (new Date(data.expirationTime!) < new Date()) throw new Error("Expired")
// Check NFT ownership on-chain
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) })
const balance = await client.readContract({
address: contractAddress,
abi: ERC721_ABI,
functionName: "balanceOf",
args: [data.address as `0x${string}`]
})
if (balance === 0n) throw new Error("No NFT found")
// Issue JWT session token
return issueJWT(data.address)
}
После успешной верификации — JWT токен с коротким TTL (например, 24 часа). Повторная верификация ownership при каждом запросе не нужна — проверяем JWT, а ownership перепроверяем при обновлении токена.
Гранулярный доступ: конкретный токен vs. любой из коллекции
Два режима:
Collection-level gating: любой держатель токена коллекции получает доступ. Проверяем balanceOf(address) > 0. Быстро, дёшево по RPC вызовам.
Token-specific gating: доступ только для держателя конкретного tokenId (например, NFT-тикет на ивент). Проверяем ownerOf(tokenId) == address. Нужно хранить маппинг tokenId → ресурс.
Trait-based gating: доступ только для NFT с определёнными атрибутами (редкие traits, определённый уровень). Здесь нужна либо on-chain запись атрибутов, либо верифицируемый маппинг с IPFS метаданных. Самый сложный вариант — нужно заранее проиндексировать metadata коллекции.
ERC-1155: multi-token gating
ERC-1155 открывает более гибкие модели. balanceOf(address, tokenId) возвращает количество токенов конкретного ID. Можно строить tier-based доступ:
- tokenId 1 = базовый доступ
- tokenId 2 = премиум доступ
- tokenId 3 = VIP доступ
Или: require 10 токенов tokenId 1 для определённого действия (gamification). Логика сложнее, но всё так же верифицируется одним balanceOf вызовом.
Инфраструктура для масштабируемого gating
Кеширование ownership данных
При активной аудитории 10k+ пользователей проверять ownerOf на каждый запрос — это нагрузка на RPC. Решение: кеш с TTL.
User authenticates → check ownership → cache result (TTL: 5 min) → issue JWT (TTL: 24h)
На каждый API запрос: проверяем JWT (локально, быстро)
При refresh JWT: проверяем кеш, если промах — идём к RPC
При transfer NFT кеш устаревает за 5 минут — для большинства сценариев приемлемо. Если нужна немедленная реакция на transfer — подписываемся на Transfer events через WebSocket и инвалидируем кеш.
Мониторинг transfer events для отзыва доступа
Продажа NFT должна немедленно отзывать доступ у продавца — критично для платных сообществ. Подписка на Transfer event через Alchemy/Infura WebSocket:
const filter = {
address: NFT_CONTRACT,
topics: [
ethers.id("Transfer(address,address,uint256)"),
null, // from: any
null // to: any
]
}
provider.on(filter, (log) => {
const [from, to, tokenId] = parseTransferEvent(log)
revokeAccess(from) // invalidate session for previous owner
grantAccess(to) // pre-cache for new owner
})
Мультичейн gating
Коллекция может быть на Ethereum, а пользователи хотят платить gas на Polygon — частый кейс для бриджнутых коллекций. Мультичейн gating: проверяем ownership на нескольких чейнах, достаточно одного совпадения.
Библиотека Moralis или Alchemy NFT API упрощают это — single API call с указанием нескольких чейнов:
const nfts = await alchemy.nft.getNftsForOwner(address, {
contractAddresses: [CONTRACT_ETH, CONTRACT_POLYGON],
})
const hasAccess = nfts.ownedNfts.length > 0
Контентная часть: что и как защищать
Статический контент (PDF, видео, изображения): файлы хранятся в S3/R2 с приватным доступом. Бэкенд генерирует signed URL с коротким TTL (15-60 минут) только для верифицированных держателей. Прямые ссылки на файлы не утекают.
Динамический контент (страницы, API данные): middleware на бэкенде проверяет JWT токен перед каждым ответом. Без валидного JWT — 401.
Стриминговый контент (видео прямые эфиры): аутентификация через токен в query param или заголовке при подключении к стриму. После подключения — периодическая ревалидация каждые N минут.
Сроки разработки
SIWE интеграция с базовым collection-level gating и JWT сессиями — 2 дня. Добавление Transfer event watcher и автоматического отзыва доступа — ещё день. Trait-based gating с индексацией metadata — 1-2 дня в зависимости от размера коллекции. Мультичейн поддержка — ещё 1-2 дня.
Полная система с мониторингом, кешированием и мультичейн верификацией — 4-5 рабочих дней.







