Парсинг данных NFT-коллекций (floor price, volume, holders)
OpenSea API возвращает floor price с задержкой 5-15 минут и агрегирует данные по своей методологии. Для торговых ботов, аналитических платформ и minting dApps, которым нужен реальный floor — это неприемлемо. Единственный путь к точным данным: читать события прямо из блокчейна.
Источники данных: что откуда брать
On-chain события
Для ERC-721/ERC-1155 коллекций все продажи видны через события маркетплейсов. Каждый маркетплейс эмитирует собственное событие:
-
OpenSea Seaport:
OrderFulfilled(bytes32 orderHash, address offerer, address zone, address recipient, SpentItem[] offer, ReceivedItem[] consideration)— контракт0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC -
Blur:
TakerAsk/TakerBidна0x000000000000Ad05Ccc4F10045630fb830B95127 -
LooksRare v2:
TakerAsk/TakerBid -
X2Y2:
EvInventory
Floor price нельзя получить из событий напрямую — события показывают исполненные ордера, а не активные листинги. Для актуального floor нужно либо индексировать active listings через маркетплейс API, либо использовать агрегаторы.
Holders и transfers
Transfer(address indexed from, address indexed to, uint256 indexed tokenId) — стандарт ERC-721. Полный граф владения строится через replay всех Transfer событий от блока деплоя. Unique holders = уникальные адреса to за вычетом адресов, которые затем перевели токены на другой адрес.
Для ERC-1155: TransferSingle и TransferBatch. Здесь владение — это баланс, не бинарное состояние: balanceOf(address, tokenId).
Архитектура парсера
Стек
ethereum-node (Alchemy/Infura/Quicknode)
→ ethers.js / viem (event filtering)
→ message queue (Redis Streams / BullMQ)
→ PostgreSQL / ClickHouse (storage)
→ REST/WebSocket API (выдача данных)
Для исторических данных — getLogs с фильтром по address и topics[0]. Блоки батчим по 2000 (ограничение большинства RPC провайдеров на eth_getLogs):
async function fetchTransferEvents(
contract: string,
fromBlock: number,
toBlock: number,
provider: JsonRpcProvider
) {
const iface = new Interface(['event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)']);
const filter = {
address: contract,
topics: [iface.getEventTopic('Transfer')],
fromBlock,
toBlock,
};
const logs = await provider.getLogs(filter);
return logs.map(log => iface.parseLog(log));
}
Для real-time: WebSocket подписка через provider.on(filter, callback) или Alchemy eth_subscribe newLogs.
Вычисление floor price
Два подхода:
1. Маркетплейс API агрегация — запрашиваем floor у OpenSea, Blur, LooksRare, берём минимум. Проблема: rate limits и кэширование на стороне API.
2. Orderbook индексирование — подписываемся на события создания/отмены ордеров. Seaport: OrderValidated (создание), OrderCancelled, OrderFulfilled (исполнение). Строим локальный orderbook, вычисляем floor самостоятельно. Точнее, но сложнее в поддержке при обновлениях контрактов маркетплейса.
Для большинства задач достаточен первый подход с кэшем на 60 секунд.
Хранение и запросы
ClickHouse эффективнее PostgreSQL для time-series NFT данных — аналитические запросы на миллионах строк в 10-50x быстрее. Схема:
| Колонка | Тип | Описание |
|---|---|---|
block_number |
UInt64 | Блок события |
tx_hash |
FixedString(66) | Хэш транзакции |
contract |
FixedString(42) | Адрес коллекции |
token_id |
UInt256 | ID токена |
from |
FixedString(42) | Продавец/отправитель |
to |
FixedString(42) | Покупатель/получатель |
price_wei |
UInt256 | Цена в wei |
marketplace |
LowCardinality(String) | Маркетплейс |
timestamp |
DateTime | Время блока |
Партиционирование по месяцам (toYYYYMM(timestamp)), сортировочный ключ (contract, timestamp).
Решение типовых проблем
Rate limits: Alchemy Free — 330 CUPS, Growth — 660 CUPS. При историческом парсинге крупной коллекции (BAYC: 500k+ Transfer событий) без throttling получим 429. Реализуем exponential backoff + queue с concurrency control.
Реорганизации блокчейна: события из последних 12 блоков нужно помечать как «pending» и подтверждать только после finality. Для Ethereum PoS — 2 эпохи (64 блока) для экономического finality.
Wash trading: объём по адресам с циклическими переводами искажает статистику. Базовая эвристика: сделки где from и to — связанные адреса (получали ETH из одного источника) помечаются флагом.
Ориентиры по срокам
Парсер Transfer событий + holders tracker — 1 день. Добавление floor price через маркетплейс API + кэш — ещё полдня. Исторический бэкфилл для крупной коллекции + дашборд — 2-3 дня суммарно.







