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/wallet/{address}/trades

Closed round trips for one wallet: coin, side, average entry and exit, how long it was held, fees, and profit net of them. Plus a summary — win rate, median hold, long/short split — and the still-open positions with their age. ?days=30&limit=100.

The exchange publishes fills and positions, never trades, so these are reconstructed from the fill tape. The pivot is each fill's startPosition, not its buy/sell side: a sell closes a long and opens a short, so side alone cannot say what happened. Crossing zero is reported as two trades, because a position that ends on the opposite side is not the trade that started. A trade already open when the window begins is marked partial — its entry and duration are understated, and it is excluded from the hold-time averages rather than quietly dragging them.

GET /v1/leaderboard

?kind=flow ranks by dollars of position change in the last four hours, at full resolution. kind=pnl_day, pnl_week and pnl_month rank by profit over the last 24 hours, 7 days and 30 days — who is making money now, which a lifetime total cannot tell you, since it is dominated by wallets that were right years ago. Every row carries its cohort badge, so a wallet up millions today on a losing lifetime record is visible as exactly that. kind=pnl and kind=equity are served from our own nightly archive.

GET /v1/network

Exchange population rather than prices: how many wallets are tracked, how many hold an open perp position, how many positions are open, the equity behind them and its distribution by band, and the share of positions currently in profit. Counted from the nightly sweep, so the envelope's as_of is the sweep's capture time, not the moment you asked.

population_basis states plainly what the population is: every wallet this product can enumerate — the exchange's own leaderboard, every address seen trading in the live feed, and every wallet looked up here. That is not every address that has ever existed on the exchange, and the field that would claim otherwise (total_wallets) stays null. Anything else needing a chain index — hourly wallet creation, the spot/staking/vault split — is null too, with its name in unavailable and the reason in note. 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. Free reads BTC as a teaser; every other coin needs a paid plan, and the refusal names the coin, the plans that include it, and the price. The zones export honours the same gate. ?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.

Level width. &zone_width_pct= sets how wide a level is, in percent, between 0.01 and 5. It defaults to 1, which answers “where is the wall” and is far too coarse to place an order against: at 1% every price returned is a multiple of 1% off the mid and never a level off the book. Ask for 0.1 or 0.05 to get levels at the resolution you are trading. A width outside the range is refused rather than clamped — silently widening a level somebody asked to narrow answers a different question than the one they asked. Narrow buckets produce many more zones, so limit defaults to whatever covers the near book at the width you chose; zones_found and zones_truncated say when the list was cropped.

The money next to spot. The bucket containing the mid spans it, so it holds bids and asks together. It used to be returned with side: "at_mid" — neither support nor resistance, because calling it one would have been a coin flip. That made the largest cluster on most books the one thing the API would not take a position on. It is now split at the mid: below spot is support, above spot is resistance, and both halves carry at_mid: true. side is therefore always support or resistance — if you were testing for "at_mid", read the flag instead. The summary block still skips these two, because together they are the biggest cluster almost by construction and would otherwise be the answer to every query; zone_approach alerts skip them too, since price is inside them by definition and could never “approach” one.

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":"cohort_turns","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. Five, each a question rather than a mechanism. cohort_turns — a cohort turns bullish or bearish on a coin; add "measure":"net_usd" to set the level in dollars of net buying instead of the default bias in [0, 1]. big_money — 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. price — a level you set is crossed. zone_approach — price comes within threshold percent of a conviction zone we compute from resting cohort orders, so you set a distance and never a price. wallet_watch — one addr trades or moves money; narrow it with "watch":"positions" or "watch":"transfers" (default is both, and threshold is an optional dollar floor on transfers). A wallet alert never fires on creation: the first pass records what the wallet holds, and only a change after that is news.

Renamed, not broken. These used to be eight kinds named after their plumbing. The old names — flow_bias, flow_usd, large_position, wallet_activity, wallet_transfer and zone_touch — are still accepted on POST and still work on alerts you already saved; nothing needs migrating. One kind was dropped rather than renamed: oi_move compared open interest against whatever the alert last saw, so its percentage depended on when it happened to run. Alerts you already have keep firing — killing a live subscription to tidy a menu is not an upgrade — but new ones cannot be created. Ask for it back and it returns with a fixed window.

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.