Разработка системы отслеживания floor price NFT
Floor price — одна из самых манипулируемых метрик в NFT. Wash trader выставляет листинг на 0.001 ETH, чтобы искусственно занизить floor и скупить по панике. Или наоборот — снимает дешёвые листинги перед крупной продажей. Система отслеживания, которая просто опрашивает OpenSea API раз в минуту, не даст вам актуальную картину. Нужен real-time мониторинг с нескольких источников одновременно.
Источники данных и их ограничения
Маркетплейс API
| Маркетплейс | Endpoint | Задержка | Rate limit |
|---|---|---|---|
| OpenSea v2 | GET /api/v2/collections/{slug}/stats |
5-15 мин | 4 req/s (free) |
| Blur | Unofficial / reverse-engineered | ~1 мин | Нет публичного |
| LooksRare | GET /api/v1/collections/stats |
~1 мин | 5 req/s |
| Reservoir | GET /collections/v7 |
~30 сек | 10 req/s (free) |
OpenSea floor — это минимальная цена среди активных листингов на их платформе. Blur и LooksRare считают независимо. Реальный «рыночный» floor — минимум из всех источников одновременно.
Reservoir — агрегатор, который нормализует данные со всех маркетплейсов. Для большинства задач это лучший single source of truth, особенно на их free tier (10 req/s достаточно для мониторинга нескольких десятков коллекций).
On-chain события (для real-time)
Истинный real-time floor можно вычислять только через события листинга:
-
Seaport:
OrderValidated(новый листинг),OrderCancelled,OrderFulfilled(продажа) -
Blur Pool:
NewPool,DepositERC721— Blur использует AMM модель для floor bids
Подписка через WebSocket на события Seaport контракта даёт latency ~100-500ms от момента on-chain события до обновления вашей базы. Это порядки быстрее любого REST polling.
Архитектура системы
┌─────────────────────────────────────────────┐
│ Data Ingestion Layer │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ OpenSea │ │Reservoir │ │ WebSocket │ │
│ │ Poller │ │ Poller │ │ Listener │ │
│ └────┬─────┘ └────┬─────┘ └──────┬──────┘ │
└───────┼────────────┼──────────────┼──────────┘
│ │ │
└────────────┴──────────────┘
│
Redis Streams
│
┌────────────┴──────────────┐
│ Aggregation Worker │
│ (compute true floor, │
│ detect anomalies) │
└────────────┬──────────────┘
│
┌────────────┴──────────────┐
│ TimescaleDB / ClickHouse │
│ (time-series storage) │
└────────────┬──────────────┘
│
┌────────────┴──────────────┐
│ WebSocket Push API │
│ (клиентские алёрты) │
└───────────────────────────┘
Aggregation worker: вычисление floor
interface FloorSnapshot {
collectionAddress: string;
floorPriceWei: bigint;
floorPriceEth: number;
source: 'opensea' | 'blur' | 'looksrare' | 'reservoir' | 'onchain';
timestamp: number;
listingsCount: number;
}
function computeTrueFloor(snapshots: FloorSnapshot[]): bigint {
const fresh = snapshots.filter(s => Date.now() - s.timestamp < 120_000); // только данные < 2 мин
if (fresh.length === 0) throw new Error('No fresh data');
return fresh.reduce((min, s) => s.floorPriceWei < min ? s.floorPriceWei : min, fresh[0].floorPriceWei);
}
Хранение time-series
TimescaleDB (расширение PostgreSQL) — оптимальный выбор если уже используете Postgres в стеке. Создаём hypertable с партиционированием по времени:
CREATE TABLE floor_snapshots (
time TIMESTAMPTZ NOT NULL,
collection TEXT NOT NULL,
floor_eth DOUBLE PRECISION,
volume_24h DOUBLE PRECISION,
source TEXT
);
SELECT create_hypertable('floor_snapshots', 'time');
CREATE INDEX ON floor_snapshots (collection, time DESC);
Непрерывные агрегаты для candle-данных (1m, 5m, 1h OHLC floor price):
CREATE MATERIALIZED VIEW floor_1h
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket,
collection,
first(floor_eth, time) AS open,
max(floor_eth) AS high,
min(floor_eth) AS low,
last(floor_eth, time) AS close
FROM floor_snapshots
GROUP BY bucket, collection;
Алёрты и аномалии
Два сигнала реально полезны трейдерам:
Floor drop alert: снижение floor >X% за последние Y минут. Триггер для покупки на падении или выхода из позиции.
Sweep alert: за короткий период (1-5 блоков) продалось N токенов по floor цене. Это «sweep floor» — кто-то скупает дешёвые листинги. Часто предшествует росту цены.
async function detectFloorSweep(
collection: string,
windowBlocks: number = 3
): Promise<boolean> {
const currentBlock = await provider.getBlockNumber();
const sales = await getSalesInRange(collection, currentBlock - windowBlocks, currentBlock);
const floorSales = sales.filter(s => s.priceEth <= currentFloor * 1.02); // ±2% от floor
return floorSales.length >= SWEEP_THRESHOLD; // например, 5 продаж за 3 блока
}
WebSocket push для фронтенда
Клиенты подписываются на коллекции через WebSocket. Server-side: Node.js + ws библиотека, или Socket.IO для автоматического reconnect. При изменении floor >1% — broadcast всем подписчикам на коллекцию:
interface FloorUpdate {
collection: string;
floorEth: number;
changePct: number;
timestamp: number;
}
Ориентиры по срокам
Базовый трекер с polling Reservoir API + TimescaleDB + REST endpoint — 1 день. Real-time WebSocket listener на Seaport события + алёрт система + WebSocket push API — 2-3 дня суммарно.







