API reference

A JSON API for cohort positioning.

Base URL /v1. Authenticate with your key in the X-API-Key header. Every data response carries the age of the facts inside it.

The response envelope

Every data endpoint returns the same wrapper. Read stale before you read data.

{
  "data": [ ... ],
  "as_of": 1785626383.0,     // newest fact in the payload
  "age_s": 42.1,             // how old that is, right now
  "stale": false,            // computed, never asserted
  "stale_after_s": 600,
  "source": "live",
  "served_at": 1785626425.1
}

age_s is measured from the data, not from when we answered. If as_of is null, stale is true — an unknown age is treated as a bad one, because a feed that stops reporting its own recency is exactly the failure this guards against. Free-tier responses add delayed_s; those numbers are real, just behind.

Endpoints

GET /v1/flow

Cohort flow per coin over an arbitrary window.

curl -H "X-API-Key: $KEY" \
  "https://your-host/v1/flow?window=4h&coins=ETH,BTC"

window accepts 90m, 4h, 3d — anything up to 8 days. Arbitrary windows are possible because every event carries its own timestamp; sources built on pre-bucketed aggregates can only offer fixed 24h/7d/30d.

{
  "data": [{
    "coin": "ETH",
    "long_usd": 4821904.11,   // opened/added long, still held
    "short_usd": 1204551.02,
    "net_usd": 3617353.09,
    "wallets": 213,           // distinct cohort wallets
    "bias": 0.8002            // long / (long + short)
  }],
  "totals": { ... }, "by_segment": { ... }
}

GET /v1/flow/series

Rolling bias and net flow over time. This is the endpoint the product exists for. ?window=24h&coin=ETH&resolution=20s.

{
  "data": [{"t": 1785626380, "bias": 0.71, "net_usd": 812004.2, "wallets": 41}],
  "resolution_s": 20,          // what you actually got
  "native_resolution_s": 20,
  "rolling_recency_s": 300,   // how far back each point looks
  "points": 4320
}

resolution is how often you get a reading: 20s on Desk, 60s on Build, 900s on Free. Ask for finer than your plan allows and the response is clamped, with resolution_clamped, a reason, and the plans that would serve it. A clamped bucket is a genuine aggregate over the wider interval, never a smoothed one.

recency is how far back each point looks, and defaults to a rolling window of window/48. Every point is recalculated as of its own timestamp using only the data that existed then — so a wallet that closed at 14:00 does not retroactively vanish from the 09:00 point. This is also why the series cannot be rebuilt from periodic snapshots: a wallet that opened and closed between two snapshots was never observed at all.

GET /v1/wallet/{address}

Positions across both dexes, PnL by period, leverage, distance to liquidation, and both cohort badges. Size and profitability are independent classifications; a wallet carries one of each. An address with no perp account returns exists: false — a fact, not an error. Also /v1/wallet/{address}/fills.

GET /v1/leaderboard

?kind=flow ranks by dollars of position change in the last four hours, at full resolution. kind=pnl and kind=equity are served from the nightly archive.

GET /v1/network

Exchange population rather than prices: wallets in existence and how many were created in the last 24 hours, active perp traders, open positions, total equity with its spot/perp/staking/vault split, and the equity distribution by band. Every headline carries its own 24h change, and the rolling 24h creation figure is the sum of the hourly series returned beside it, so the number and the chart cannot disagree.

Fields that need an index of every address — which no public endpoint provides — are null with the reason in note and their names in unavailable, never estimated.

GET /v1/udf/*

A TradingView UDF datafeed: /config, /symbols, /search, /history, /time. The dashboard chart uses TradingView's open-source Lightweight Charts, which needs none of this — these exist so their full Advanced Charts library can be dropped in without touching the back end. static/udf-datafeed.js is the adapter. The key goes in the X-API-Key header, never a query string: a datafeed URL ends up in history, referrers and logs.

GET /v1/funding · /v1/stats · /v1/liquidation-risk

Live funding, open interest and volume per pair; exchange-wide aggregates and leverage distribution; and the share of each cohort sitting within 25% of liquidation. That last one discriminates: proven-profitable cohorts consistently run less leverage, so convergence between cohorts is the thing to watch.

GET|POST|PATCH|DELETE /v1/watchlist

Track addresses and get them back with live equity, bias, unrealised PnL, both cohort badges and liquidation proximity in one call. PATCH /v1/watchlist/{addr} sets label and pinned; pinned rows sort first. Both are your own annotations on a public address, which is the only part of a watchlist worth storing for you.

POST /v1/webhooks Build

Fired alerts POST to your endpoint, signed. Every delivery carries X-HC-Timestamp and X-HC-Signature, an HMAC-SHA256 of <timestamp>.<raw body> keyed by the secret shown once at creation. The timestamp is inside the signature, not merely alongside it, so an old delivery cannot be replayed with a fresh one — reject anything older than ~5 minutes. Ten consecutive failures disables an endpoint.

GET /v1/zones Pro

Support and resistance ranked by profit-cohort dollars resting there. ?coin=BTC&lookback_days=7. Each zone reports cohort_usd, cohort_pct and a conviction label, and the response includes a coverage block stating how much of the book was read. Add &segments= to choose which cohorts count — ids, names like smart-money, or groups (profit, loss, whales, retail, all). elite_usd and elite_pct are kept as aliases so existing clients keep working.

GET /v1/signal Pro

A weighted scorecard from −100 to +100 with every input broken out. It is a summary of measured quantities, not a backtested predictor, and the response says so in a caveat field.

&segments= chooses whose positioning is scored, using the same ids, names and groups as /v1/zones. It defaults to profit. The selection changes what the number means, not how it is computed, so the response states its own basis: segments, segment_names, basis (profit, loss or mixed) and a basis_note when the reading needs inverting — +60 across the loss-making cohorts is the side that has historically been wrong leaning long, which is not the same finding as +60 across the profitable ones.

GET /v1/archive/history Desk

Daily cohort bias per coin for every archived day. ?coin=ETH. Also /v1/archive for depth and /v1/archive/day?day=YYYY-MM-DD for one night. Requests before the archive begins return before_archive_start rather than an empty list — an empty result would read as "nothing happened" when the truth is "we were not watching yet".

GET /v1/export Desk

curl -H "X-API-Key: $KEY" -o flow.csv \
  "https://your-host/v1/export?dataset=flow&from=2026-08-01&to=2026-08-02"

Bulk download as csv or jsonl. dataset=flow is the per-coin cohort aggregate over the window (the same rows /v1/flow serves), events is the raw position-change events behind it, zones the current zone table for one coin, and archive the nightly position snapshots. Freshness travels in headers — X-HC-As-Of, X-HC-Age-S — because a CSV has nowhere to put the envelope. Row counts are capped per request; truncation is stated in X-HC-Truncated and a trailing comment row, and every complete CSV ends with # rows: N so a cut-off download is detectable.

GET /v1/stream Stream

curl -N -H "X-API-Key: $KEY" \
  "https://your-host/v1/stream?coins=ETH&segments=profit"

The raw event firehose as Server-Sent Events: every cohort position change as it lands, in the same shape /v1/flow consumes — t, addr, seg, size_seg, coin, d_szi, szi, px, usd. Direction is the sign of d_szi, measured from the position diff, never inferred from trade-side ordering. Each event's SSE id is its timestamp, so reconnecting with Last-Event-ID (or ?since=) replays the missed tail, bounded to ten minutes. Comment heartbeats every 15s keep proxies from closing the pipe. Concurrent streams per account are capped; the limit is in X-HC-Stream-Limit on every response rather than discovered by hitting it.

GET|POST|DELETE /v1/alerts Pro

curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"kind":"flow_bias","coin":"ETH","window_h":4,
       "threshold":0.7,"direction":"above"}' \
  https://your-host/v1/alerts

Alerts are edge-triggered with hysteresis and are never evaluated against stale data — if the feed is behind, the pass is skipped rather than firing on a frozen number.

Kinds. Marketwide: flow_bias, flow_usd, zone_touch, price, large_position (any single cohort-wallet position change over a dollar size, on any coin unless you name one — it watches the classified cohorts the feed follows, not every wallet on the exchange) and oi_move (percent change in open interest since that alert last looked). Per wallet: wallet_activity (open, close, add, reduce) and wallet_transfer (deposits and withdrawals over an optional floor) — both take addr instead of coin. A wallet alert never fires on creation: the first pass records what the wallet holds, and only a change after that is news.

Digests. Add interval_s — 300, 900, 3600, 21600 or 86400 — and the alert reports on that clock instead of on a crossing. No hysteresis, because there is no edge to debounce. Leave it out for the default behaviour, one message when it happens.

Limits. Each plan holds a fixed number of alerts: Build 25, Desk 100, Stream uncapped. GET /v1/alerts returns used, limit and remaining; going over returns 402 naming the cap and the plans that hold more.

channel is telegram (the default), webhook or all. Telegram is bound per account by a one-time code: the account requests a link, opens the bot with it, and the bot reports which chat sent it — a chat id is never typed into a form, so a typo cannot deliver your positions to a stranger. Creating an alert with no chat connected still returns 201, with a warning saying it has nowhere to go; finding that out when it fails to arrive would be too late.

GET /v1/health

No key required. Per-upstream freshness, measured from the newest fact in each feed. Publicly checkable on purpose.

Plans and limits

Every plan gets every endpoint except webhooks, the archive and the firehose, and every plan gets every coin with no delay. What you buy is resolution.

PlanPriceResolutionRequests/hourAdds
Free $0 900s 250
Build $99 60s 6,000 alerts, webhooks
Desk $349 20s 30,000 archive, export
Stream $1200 20s 250,000 firehose

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, so you can back off before being refused. 401 means a missing or revoked key, 402 means your plan excludes the endpoint (the body lists which plans include it), 429 means the hourly limit with a Retry-After. Asking for finer resolution than your plan allows is not an error — you get a 200 with the coarser data and resolution_clamped set, because a chart that renders beats a refusal.

How the numbers are made

Cohort membership is the set of wallets with a proven profit record, split into three segments. Flow comes from diffing each wallet's actual position between polls: d_szi > 0 means that wallet got longer, and szi is the resulting position, so a wallet still shows up only while it is still in the trade.

Direction is never inferred from which side of a trade a wallet appears on. That ordering is undocumented, and reading it wrong would invert every bias while still producing numbers that look entirely reasonable.