Base URL /v1. Authenticate with your key in the
X-API-Key header. Every data response carries the age of the
facts inside it.
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.
GET /v1/flowCohort 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-riskLive 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 Deskcurl -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 Streamcurl -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 Procurl -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/healthNo key required. Per-upstream freshness, measured from the newest fact in each feed. Publicly checkable on purpose.
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.
| Plan | Price | Resolution | Requests/hour | Adds |
|---|---|---|---|---|
| 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.
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.