Event stream channels and filters
Every channel of the four event streams on stream.vike.io (Hyperliquid, HL Cohorts, on-chain and Polymarket), what each event carries, how often events arrive, and the filter keys each channel takes.
Eleven channels, on four addresses, all on stream.vike.io. You connect to an address, then subscribe to one or more
of its channels, each with an optional filter. How a connection starts, ends and resumes is on
Event streams close codes, errors and resuming; this page is what comes through it.
| Address | Product | Channels |
|---|---|---|
wss://stream.vike.io/v1/stream/hl | Hyperliquid | hl.fills, hl.liquidations, hl.positions, hl.liq_risk, hl.order_walls |
wss://stream.vike.io/v1/stream/hl-cohorts | HL Cohorts | hl.cohort_flow, hl.indices |
wss://stream.vike.io/v1/stream/onchain | On-chain | chain.transfers |
wss://stream.vike.io/v1/stream/polymarket | Polymarket | pm.trades, pm.resolutions, pm.price_moves |
One key works on every address. A channel belongs to one address; asking for it on another is an unknown_channel
error that lists what that address serves.
Channels run on demand
One rule holds for all eleven channels: a channel is produced and stored only while someone is subscribed to it, and for 15 minutes after the last subscriber leaves.
- The first events of a channel nobody was watching arrive with its next pass after you subscribe:
hl.fillswithin about ten seconds;hl.cohort_flowwithin about 20 seconds plus its settle, so a minute's numbers come about a minute after it ends;hl.order_wallswith the next 5-minute slot (it is checked every minute);hl.indicesandhl.liq_riskwith the next position capture, about 12 minutes;chain.transferswith the next sync, within 5 minutes;pm.trades,pm.price_movesandpm.resolutionswithin 15, 30 and 60 seconds of their next pass. - An idle window is never filled in afterwards. A client that resumes across one gets a
gapframe for it, not events. - A source's own overlap can bring events dated shortly before you subscribed, since some sources re-read the last
minutes (
hl.fillsre-reads two minutes). - While at least one client is subscribed, the channel keeps running for everyone.
Every event has the same envelope
An event frame carries one event: schema (vike.event.v1), event_id, source (the channel), kind, ts,
cursor, chain, subject, data and labels. It is the same envelope as the events of the
alert feed; what differs is the data of each channel, below.
tsis when the thing happened (UTC, milliseconds), not when the frame was sent.event_idis unique and the same every time the same event arrives. Drop a repeat by it.- The
cursorinsideeventis the source's own position. It is not the cursor to resume from: that is thecursorof the frame. - A number that cannot be represented, such as a division by zero, is
null.
Filters
A filter is an object. Its keys are combined with AND; a list matches any of its values; no filter matches
everything. A key a channel does not take is refused with bad_subscription, as in
filter key 'x' is not supported on hl.fills, and a bad value is refused with the key and the reason, as in
filter.min_usd: must be a number.
| Key | Value | Notes |
|---|---|---|
wallet | list of up to 1,000 | 0x and 40 hex characters, any case. |
coin | list of up to 200 | Upper-cased before matching, so kpepe matches kPEPE. |
token | list of up to 200 | A token contract address. |
market | list of up to 200 | A Polymarket condition id: 0x and 64 hex characters. |
token_id | list of up to 200 | A Polymarket outcome token: 1 to 80 decimal digits. |
chain | list of up to 50 | eth, bsc, base or hyperevm, lower case. Not ethereum. |
entity, wallet_type | list of up to 50 | A label name, 1 to 64 printable characters, matched in lower case. |
pnl_cohort, size_cohort, cohort, action, index, pnl_tier, volume_tier | list of up to 50 | Names from the channel's own list below. |
side, direction, event, axis, move | one value, not a list | {"side": ["buy"]} is refused; send {"side": "buy"}. |
min_usd, min_amount, min_move | one number | 0 or more. The comparison is "at least". hl.fills has a default min_usd of 1000 when you send none; min_usd: 0 removes it. |
In pnl_cohort, size_cohort, cohort, pnl_tier and volume_tier, the name unknown also matches an event that
has no label at all.
Labels
Hyperliquid events about a wallet carry labels with pnl_cohort, pnl_cohort_basis, pnl_cum_usd and
size_cohort. Any of them can be null. They are read when the event is made, not when the fill happened. There is no
rank and there are no tags.
| Label | Values |
|---|---|
pnl_cohort | 3x smart, 2x smart, smart, 3x winner, 2x winner, winner, loser, 2x loser, 3x loser, rekt, 2x rekt, 3x rekt. null for a wallet with fewer than 10 closed trades. |
size_cohort (account value) | shrimp (from $0), 2x shrimp ($1k), fish ($10k), 2x fish ($25k), dolphin ($50k), 2x dolphin ($100k), shark ($250k), 2x shark ($500k), whale ($1M), 2x whale ($2.5M), 3x whale ($5M), 4x whale ($10M). null for a wallet not in the newest account snapshot. |
On-chain and Polymarket events have their own labels, described under their channels.
Hyperliquid: /v1/stream/hl
hl.fills
Every fill of a perp market, as it is recorded. A subscription that sends no min_usd gets min_usd: 1000: fills
of at least $1,000. Send min_usd: 0 for every fill, or a larger number for bigger ones. The default applies on the
socket and in a catch-up alike. The event is the fill event of the
alert feed: subject is wallet and symbol, and
data has action (open, close, flip, increase or reduce; null when the fill could not be classified),
side, size_usd, size_token, price, closed_pnl, start_position, position_after, crossed and trade_id.
labels has all four keys.
- Id:
hl.fills:<wallet>:<fill time in ms>:<trade id>. Atrade_idcan be larger than 2^53, which JavaScript reads inexactly: take the exact value from the end of theevent_id. - Filters:
wallet,coin,min_usd(the size of the fill),pnl_cohort,size_cohort,action,side. - Every fill is a firehose:
min_usd: 0is about 140 to 225 events a second on weekdays (measured 2026-10-28 to 30). A client that takes it has to keep up, and a resume of it from more than a few minutes back is too large to replay over the socket (replay_too_large). - Arrives: within about ten seconds of the fill reaching our store.
hl.liquidations
Liquidations, each as the liquidation event of the alert feed: data has direction (long or short: the side of
the position that was closed), size_usd, size_token, mark_price and inferred (always true). A liquidation is
inferred from changes between position captures, so it is stamped with the capture and arrives in batches,
typically 10 to 18 minutes after the liquidation, not at the instant. There is no size floor.
- Id:
hl.liquidations:..., unique per liquidation. - Filters:
wallet,coin,min_usd,pnl_cohort,size_cohort,direction.
hl.positions
The fills that change a big position, as their own events. A fill is sent here, as well as on hl.fills, when it is
worth at least $1,000 (a floor of this channel that you cannot lower), it could be classified, and either the wallet's size_cohort is shark or bigger (an
account of at least $250k) or the position before or after the fill was worth at least $250k (so a big close
counts).
- Event: the same fill record as
hl.fills(samedata), withkindpositionand the idhl.positions:<wallet>:<fill time in ms>:<trade id>. It is a second event for the same fill: a client on both channels gets both, and deduping byevent_idkeeps both. - Position size: it is not a field.
abs(position_after) * priceis the position after the fill. - Filters:
wallet,coin,min_usd(the size of the fill, not of the position),pnl_cohort,size_cohort,action,side(the fill's side, not the position's direction). - Arrives: with
hl.fills. About 60% of the fills onhl.fillsqualify. - Not in it: leverage, liquidation price and a position snapshot. A position change shows only when a fill happens.
hl.liq_risk
A position that has come close to liquidation, announced once as it enters the zone. It is global, not per rule: every position of at least $100,000 whose liquidation ratio reaches 0.80.
Not the alert rule's liq_risk event
The alert feed also has a liq_risk event, made by a rule you set up, with threshold and band. This channel's events
have a different data and carry none of those.
liq_ratio: how far the mark price has gone from entry toward liquidation, from 0 to 1. For a long it is(entry - mark) / (entry - liquidation), for a short(mark - entry) / (liquidation - entry); 1 means the liquidation price is reached.- Once per entry: the position is announced when it reaches 0.80 and not again until it has fallen below 0.70 or closed. Between 0.70 and 0.80 it is silent. A position already in the zone when the channel starts is not announced.
- Id:
hl.liq_risk:<wallet>:<symbol>:<long|short>:<capture time in seconds>.subjectiswalletandsymbol. data:asset,direction,liq_ratio,enter(0.8),mark_price,entry_price,liquidation_price,size_usd(the position, not a fill),leverageandsnapshot_ts.- Filters:
wallet,coin,min_usd(the position's size),pnl_cohort,size_cohort,direction. - Arrives: once for each new position capture, about every 12 minutes, so an event comes with the capture and not
at the moment the position crossed. A position that crosses and is closed between two captures may never be
announced. A position under $100,000 is never streamed, whatever your
min_usdsays.
hl.order_walls
Large resting perp orders ("walls") appearing and disappearing: a limit order of at least $1,000,000 (price times original size). Spot is not included, and neither are orders waiting on a trigger.
- Two events per wall at most:
appearedwhen the order is first seen,disappearedwhen it is gone. The id ishl.order_walls:<order id>:appearedor:disappeared. data:event,coin,side(buyis a bid wall,sellan ask wall),limit_px,size_usd(the original size),remaining_usd(what was left at the last sighting),ts_placed,first_seen,last_seenandoid.subjectis the owner'swalletandsymbol.- The stream does not say whether a wall was filled or cancelled;
remaining_usdis not "filled". - Slot times: the book is read in 5-minute slots, so
first_seenandlast_seenfall on that grid and the event'stsis the slot. Usets_placedfor the placement time. A wall that lives and dies between two slots is in neither event, and adisappearedcan come for a wall whoseappearedyou never saw (it was placed before you subscribed). - Filters:
wallet,coin,min_usd(the original size),pnl_cohort,size_cohort,side(the order's side),event. - Arrives: in bursts, once per slot (about 50 to 70 of each kind at the $1M floor, measured on 2026-10-31).
HL Cohorts: /v1/stream/hl-cohorts
hl.cohort_flow
What each cohort bought and sold, per closed minute, per coin. For every perp fill of the minute (no size floor), the
numbers are summed per coin, per axis (pnl or size) and per cohort. A fill adds to the buy or sell total by its own
side, so a trade is counted once for each of its two sides.
- Id:
hl.cohort_flow:<coin>:<axis>:<cohort, spaces as _>:<minute in seconds>, with:r1added on a revision.subjectis thesymbol; there is no wallet andlabelsis empty. data:window_startandwindow_s(60),axis,cohort(as written, with a space, orunknown),buy_usd,sell_usd,net_usd(buy minus sell),wallets(distinct wallets) andfills, andrev.unknownis included, for wallets with no label on that axis, so the cohorts of an axis add up to the coin's total. One row per coin, axis and cohort that had a fill; there are no zero rows.- Revisions: about two and a half minutes after the minute ends it is read once more. A coin whose fill count
changed is sent again, every cohort of it, with
rev1 and the id suffix:r1, carrying the full recomputed totals (not a delta). Keep the highestrevfor eachwindow_start, coin, axis and cohort. A fill that arrives later than that is in no number. - Filters:
coin,axis(one value,pnlorsize) andcohort(a list). A cohort belongs to one axis:cohort: ["3x smart"]withaxis: "size"matches nothing.unknownis on both axes. There is nowalletormin_usd. - Arrives: about a minute after each minute closes. Unfiltered it is about 1,000 to 2,000 events a minute, so
filter by
coin, and byaxisif you only need one. - Order: after a catch-up, minutes can arrive out of order. Use
window_start, not arrival order.
hl.indices
The five cohort indices, for the 50 coins with the most open position value, once for every position capture.
- Id:
hl.indices:<coin>:<capture time in seconds>.subjectis thesymbol;labelsis empty. data:valuesandprev(the same five keys each;previsnullthe first time a coin is seen),crossed,snapshot_ts,positions(open positions in the coin) andnotional_usd.- The indices:
smiis the long share of the smart cohorts minus that of the rekt cohorts;wsiis the long share of the whale tiers minus that of the shrimp tiers;smart_biasandwhale_biasare(long - short) / (long + short)for those groups;smi_wsi_gapissmi - wsi. Ratios, not percentages: -1 to +1, and -2 to +2 for the gap. Any value isnullwhen its group holds no positions. crossed: a list of{index, from_side, to_side}for each index that moved fromlow(at or below -0.10) tohigh(at or above +0.10) or back. It compares with the last side the index held outside that band, not withprev, and the first sighting of a coin crosses nothing. It is[]when nothing crossed.- Filters:
coinandindex(a list). Anindexfilter matches only the events where that index crossed; without it you get all 50 coins at every capture. - Arrives: every capture, about every 12 minutes, 50 events at a time.
Not the alert rule's index event
The alert feed's index event, made by a hl_cohort_bias rule, has index, value, threshold and band in its
data. The hl.indices channel is the whole set of indices per coin; it has none of those keys.
On-chain: /v1/stream/onchain
chain.transfers
Every transfer of the four chains, native coin and tokens, with no size floor. A transfer is one event.
{
"schema": "vike.event.v1",
"event_id": "chain.transfers:eth:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:41",
"source": "chain.transfers",
"kind": "transfer",
"ts": "2026-10-31T10:00:35.000Z",
"cursor": "000023095123-000041",
"chain": "eth",
"subject": {
"from": "0x1111111111111111111111111111111111111111",
"to": "0x2222222222222222222222222222222222222222",
"token_address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"token_symbol": "USDC",
"tx_hash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"data": {
"value": "2500000000", "decimals": 6, "amount": 2500.0, "value_usd": null, "priced": false,
"block_number": 23095123, "log_index": 41, "transfer_type": "erc20"
},
"labels": {"from": {"entity": "binance", "wallet_type": "cex"}}
}- Id:
chain.transfers:<chain>:<tx hash>:<log index>; a native transfer ends in:native. chain:eth,bsc,baseorhyperevm. It iseth, notethereum, here and in thechainfilter.subject:from,to,token_address(the zero address for the native coin),token_symbol(nullfor a token we have not mapped) andtx_hash. A side can be the zero address (a mint, a burn or a contract creation).data:valueis the exact raw integer, as a string, in the token's smallest unit; use it withdecimalsfor exact arithmetic.amountisvaluedivided by10^decimalsas a float, which loses precision on large values.decimalsandamountarenullwhen the token is not mapped (the native coin always has 18).transfer_typeisnativeorerc20.labels: per side,labels.fromandlabels.to, each withentityandwallet_type(flaggedis awallet_type). A side without a label has no key, and a transfer with neither side labelled haslabels: {}.- Filters:
wallet,token,chain,min_usd,min_amount,entity,wallet_type.wallet,entityandwallet_typematch either side; you cannot ask for "from" only or "to" only.min_amountis in token units (data.amount) and needstokenin the same filter: an amount across tokens means nothing. A transfer of an unmapped token has noamountand never matches it.min_usdis the priced USD value, and see the next point.
Prices: min_usd matches only transfers that were priced when they were streamed
Pricing runs after the transfer is stored, so at stream time value_usd is null and priced is false for
practically all transfers (a 15-minute sample on 2026-10-31: none of 425,000 was priced). The stream does not send a
transfer again once it is priced, so it stays unpriced in the stream. A min_usd filter, even min_usd: 0, therefore
drops every unpriced transfer. For on-chain, filter on wallet, token, chain, entity or wallet_type, and on
min_amount with a token, and do not rely on a price.
- Arrives in bursts: the chain data is stored about every 5 minutes, and an event arrives about 3 minutes after its block, so roughly 190,000 events come together every 5 minutes (about 625 a second on average with no filter, roughly 0.6 MB a second). A client with no filter has to take that rate; one with a filter takes only its share.
- Never re-sent: a repair of an already streamed block is not sent again, and a transfer that a repair adds to a block already streamed does not come through the stream.
Polymarket: /v1/stream/polymarket
pm.trades
Trades of the on-chain fill tape, one event per fill, about four minutes behind the chain. The order book, trades without a wallet and unusual bets are not streamed.
- Id:
pm.trades:<tx hash>:<log index>.chainispolygon. subject:wallet(the trader's proxy wallet on that row of the tape),condition_id,token_id,slug,title,outcomeandoutcome_index.data:side(buyorsell),size(shares),price(0 to 1),usd(size * price),role,tx_hash,log_indexandblock_number.sideis the tape's maker side for that row, in the convention of that token: do not read it as the other party's direction. The other party's wallet is not in the event.labels:nameandpseudonymwhen the tape row has them; and when the wallet has a tier row,pnl_tier,volume_tierandis_top1. A wallet without a tier row has no tier keys.pnl_tier:top1,top5,top10,median,belowmedian,bot10,bot5,bot1,unranked.volume_tier:whale,dolphin,fish,shrimp.
- Filters:
wallet,market(a condition id),min_usd(data.usd),side(one value),pnl_tier,volume_tier. There is no size floor of ours.
pm.resolutions
Every resolution, as it is recorded on-chain, whether or not anyone traded the market: most are markets nobody
traded, so read data.traded.
- Id:
pm.resolutions:<condition id>; one per market.subjecthascondition_idand, when we know the market,slug,questionandoutcomes. data:winning_index(-1when the top payout is shared, a tie or a void),winning_outcome(nullthen, or when the outcomes are unknown),payouts(as recorded; a partial split such as[1, 3]passes through),invalid(truewhenwinning_indexis-1),resolved_blockandtraded(trueif the market was ever on the tape).- Filters:
marketonly. There is no filter ontraded; testdata.tradedyourself. - Arrives: about four minutes behind the chain, like
pm.trades.labelsis empty.
pm.price_moves
A move of at least 0.05 (five points) in a token's mid price within five minutes.
- Id:
pm.price_moves:<token id>:<5-minute bucket>:<up|down>.subjecthastoken_id,condition_id,slugandoutcome(the last twonullfor a market that is not open). There is no wallet;labelsis empty. data:mid_fromandmid_to(0 to 1),move(signed:mid_to - mid_from),direction(upordown),window_s(300),bidandaskof the later quote.- Filters:
market,token_id,move(one value,upordown) andmin_move(the size of the move, above 0 and at most 1; a value under 0.05 changes nothing, since smaller moves are never made).moveis signed andmin_moveis not. - One real move can give two events: one per outcome token of a market, and a move that straddles a 5-minute boundary can appear in both buckets.
- Arrives: within about a minute of the quote that completed the move.
Event streams close codes, errors and resuming
The four event streams on stream.vike.io put a cursor on every event. This page gives their addresses, every close code and error frame, what a client does about each, and how it resumes without losing or repeating an event.
Alert feed and webhooks
GET /v1/alerts/events returns the events your alert rules produced, oldest first, paged by cursor, and POST /v1/alerts/deliveries/{id}/resend sends one of their deliveries again. This page also gives the webhook body and every event shape. The routes appear in no OpenAPI spec.