NFT Floor Price Tracking System Development
Floor price is one of the most manipulated metrics in NFT. A wash trader posts a listing for 0.001 ETH to artificially lower the floor and scoop up panicked sales. Or the opposite — removes cheap listings before a large sale. A tracking system that just polls OpenSea API once a minute won't give you an accurate picture. You need real-time monitoring from multiple sources simultaneously.
Data Sources and Their Limitations
Marketplace API
| Marketplace | Endpoint | Delay | Rate limit |
|---|---|---|---|
| OpenSea v2 | GET /api/v2/collections/{slug}/stats |
5-15 min | 4 req/s (free) |
| Blur | Unofficial / reverse-engineered | ~1 min | No public |
| LooksRare | GET /api/v1/collections/stats |
~1 min | 5 req/s |
| Reservoir | GET /collections/v7 |
~30 sec | 10 req/s (free) |
OpenSea floor is the minimum price among active listings on their platform. Blur and LooksRare calculate independently. The true "market" floor is the minimum across all sources simultaneously.
Reservoir — an aggregator that normalizes data from all marketplaces. For most tasks it's the best single source of truth, especially on their free tier (10 req/s enough for monitoring dozens of collections).
On-chain events (for real-time)
True real-time floor can only be calculated from listing events:
-
Seaport:
OrderValidated(new listing),OrderCancelled,OrderFulfilled(sale) -
Blur Pool:
NewPool,DepositERC721— Blur uses AMM model for floor bids
WebSocket subscription to Seaport contract events gives latency ~100-500ms from on-chain event to database update. Orders of magnitude faster than any REST polling.
System Architecture
┌─────────────────────────────────────────────┐
│ 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 │
│ (client alerts) │
└───────────────────────────┘
Aggregation worker: computing 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); // only data < 2 min
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 storage
TimescaleDB (PostgreSQL extension) — optimal choice if already using Postgres in the stack. Create hypertable with time partitioning:
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 for candle data (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;
Alerts and Anomalies
Two signals really useful to traders:
Floor drop alert: floor decrease >X% in last Y minutes. Trigger to buy the dip or exit position.
Sweep alert: in short period (1-5 blocks) N tokens sold at floor price. This is "sweep floor" — someone scooping cheap listings. Often precedes price increase.
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% from floor
return floorSales.length >= SWEEP_THRESHOLD; // e.g., 5 sales in 3 blocks
}
WebSocket push for frontend
Clients subscribe to collections via WebSocket. Server-side: Node.js + ws library, or Socket.IO for automatic reconnect. When floor changes >1% — broadcast to all subscribers on collection:
interface FloorUpdate {
collection: string;
floorEth: number;
changePct: number;
timestamp: number;
}
Timeline Estimates
Basic tracker with Reservoir API polling + TimescaleDB + REST endpoint — 1 day. Real-time WebSocket listener on Seaport events + alert system + WebSocket push API — 2-3 days total.







