Розробка системи відслідкування floor price NFT
Floor price одна з найманипульованіших метрик у NFT. Wash trader виставляє листинг на 0.001 ETH, щоб штучно занизити floor та скупити по паніці. Або навпаки — знімає дешеві листинги перед крупною продажею. Система відслідкування, яка просто опитує OpenSea API раз на хвилину, не дасть вам актуальну картину. Потрібен real-time моніторинг з кількох джерел одночасно.
Джерела даних та їхні обмеження
Marketplace API
| Маркетплейс | Endpoint | Затримка | Rate limit |
|---|---|---|---|
| OpenSea v2 | GET /api/v2/collections/{slug}/stats |
5-15 хв | 4 req/s (free) |
| Blur | Неофіційний / зворотна інженерія | ~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 — агрегатор, який нормалізує дані з усіх маркетплейсів. Для більшості задач це найкраще єдине джерело істини, особливо на їх 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 │
│ (обчислити true floor, │
│ виявити аномалії) │
└────────────┬──────────────┘
│
┌────────────┴──────────────┐
│ 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);
Continuous aggregates для 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 дні усього.







