vike

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.

AddressProductChannels
wss://stream.vike.io/v1/stream/hlHyperliquidhl.fills, hl.liquidations, hl.positions, hl.liq_risk, hl.order_walls
wss://stream.vike.io/v1/stream/hl-cohortsHL Cohortshl.cohort_flow, hl.indices
wss://stream.vike.io/v1/stream/onchainOn-chainchain.transfers
wss://stream.vike.io/v1/stream/polymarketPolymarketpm.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.fills within about ten seconds; hl.cohort_flow within about 20 seconds plus its settle, so a minute's numbers come about a minute after it ends; hl.order_walls with the next 5-minute slot (it is checked every minute); hl.indices and hl.liq_risk with the next position capture, about 12 minutes; chain.transfers with the next sync, within 5 minutes; pm.trades, pm.price_moves and pm.resolutions within 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 gap frame 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.fills re-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.

  • ts is when the thing happened (UTC, milliseconds), not when the frame was sent.
  • event_id is unique and the same every time the same event arrives. Drop a repeat by it.
  • The cursor inside event is the source's own position. It is not the cursor to resume from: that is the cursor of 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.

KeyValueNotes
walletlist of up to 1,0000x and 40 hex characters, any case.
coinlist of up to 200Upper-cased before matching, so kpepe matches kPEPE.
tokenlist of up to 200A token contract address.
marketlist of up to 200A Polymarket condition id: 0x and 64 hex characters.
token_idlist of up to 200A Polymarket outcome token: 1 to 80 decimal digits.
chainlist of up to 50eth, bsc, base or hyperevm, lower case. Not ethereum.
entity, wallet_typelist of up to 50A label name, 1 to 64 printable characters, matched in lower case.
pnl_cohort, size_cohort, cohort, action, index, pnl_tier, volume_tierlist of up to 50Names from the channel's own list below.
side, direction, event, axis, moveone value, not a list{"side": ["buy"]} is refused; send {"side": "buy"}.
min_usd, min_amount, min_moveone number0 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.

LabelValues
pnl_cohort3x 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>. A trade_id can be larger than 2^53, which JavaScript reads inexactly: take the exact value from the end of the event_id.
  • Filters: wallet, coin, min_usd (the size of the fill), pnl_cohort, size_cohort, action, side.
  • Every fill is a firehose: min_usd: 0 is 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 (same data), with kind position and the id hl.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 by event_id keeps both.
  • Position size: it is not a field. abs(position_after) * price is 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 on hl.fills qualify.
  • 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>. subject is wallet and symbol.
  • data: asset, direction, liq_ratio, enter (0.8), mark_price, entry_price, liquidation_price, size_usd (the position, not a fill), leverage and snapshot_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_usd says.

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: appeared when the order is first seen, disappeared when it is gone. The id is hl.order_walls:<order id>:appeared or :disappeared.
  • data: event, coin, side (buy is a bid wall, sell an ask wall), limit_px, size_usd (the original size), remaining_usd (what was left at the last sighting), ts_placed, first_seen, last_seen and oid. subject is the owner's wallet and symbol.
  • The stream does not say whether a wall was filled or cancelled; remaining_usd is not "filled".
  • Slot times: the book is read in 5-minute slots, so first_seen and last_seen fall on that grid and the event's ts is the slot. Use ts_placed for the placement time. A wall that lives and dies between two slots is in neither event, and a disappeared can come for a wall whose appeared you 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 :r1 added on a revision. subject is the symbol; there is no wallet and labels is empty.
  • data: window_start and window_s (60), axis, cohort (as written, with a space, or unknown), buy_usd, sell_usd, net_usd (buy minus sell), wallets (distinct wallets) and fills, and rev.
  • unknown is 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 rev 1 and the id suffix :r1, carrying the full recomputed totals (not a delta). Keep the highest rev for each window_start, coin, axis and cohort. A fill that arrives later than that is in no number.
  • Filters: coin, axis (one value, pnl or size) and cohort (a list). A cohort belongs to one axis: cohort: ["3x smart"] with axis: "size" matches nothing. unknown is on both axes. There is no wallet or min_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 by axis if 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>. subject is the symbol; labels is empty.
  • data: values and prev (the same five keys each; prev is null the first time a coin is seen), crossed, snapshot_ts, positions (open positions in the coin) and notional_usd.
  • The indices: smi is the long share of the smart cohorts minus that of the rekt cohorts; wsi is the long share of the whale tiers minus that of the shrimp tiers; smart_bias and whale_bias are (long - short) / (long + short) for those groups; smi_wsi_gap is smi - wsi. Ratios, not percentages: -1 to +1, and -2 to +2 for the gap. Any value is null when its group holds no positions.
  • crossed: a list of {index, from_side, to_side} for each index that moved from low (at or below -0.10) to high (at or above +0.10) or back. It compares with the last side the index held outside that band, not with prev, and the first sighting of a coin crosses nothing. It is [] when nothing crossed.
  • Filters: coin and index (a list). An index filter 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.

A chain.transfers event (made-up values)
{
  "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, base or hyperevm. It is eth, not ethereum, here and in the chain filter.
  • subject: from, to, token_address (the zero address for the native coin), token_symbol (null for a token we have not mapped) and tx_hash. A side can be the zero address (a mint, a burn or a contract creation).
  • data: value is the exact raw integer, as a string, in the token's smallest unit; use it with decimals for exact arithmetic. amount is value divided by 10^decimals as a float, which loses precision on large values. decimals and amount are null when the token is not mapped (the native coin always has 18). transfer_type is native or erc20.
  • labels: per side, labels.from and labels.to, each with entity and wallet_type (flagged is a wallet_type). A side without a label has no key, and a transfer with neither side labelled has labels: {}.
  • Filters: wallet, token, chain, min_usd, min_amount, entity, wallet_type.
    • wallet, entity and wallet_type match either side; you cannot ask for "from" only or "to" only.
    • min_amount is in token units (data.amount) and needs token in the same filter: an amount across tokens means nothing. A transfer of an unmapped token has no amount and never matches it.
    • min_usd is 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>. chain is polygon.
  • subject: wallet (the trader's proxy wallet on that row of the tape), condition_id, token_id, slug, title, outcome and outcome_index.
  • data: side (buy or sell), size (shares), price (0 to 1), usd (size * price), role, tx_hash, log_index and block_number. side is 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: name and pseudonym when the tape row has them; and when the wallet has a tier row, pnl_tier, volume_tier and is_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. subject has condition_id and, when we know the market, slug, question and outcomes.
  • data: winning_index (-1 when the top payout is shared, a tie or a void), winning_outcome (null then, or when the outcomes are unknown), payouts (as recorded; a partial split such as [1, 3] passes through), invalid (true when winning_index is -1), resolved_block and traded (true if the market was ever on the tape).
  • Filters: market only. There is no filter on traded; test data.traded yourself.
  • Arrives: about four minutes behind the chain, like pm.trades. labels is 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>. subject has token_id, condition_id, slug and outcome (the last two null for a market that is not open). There is no wallet; labels is empty.
  • data: mid_from and mid_to (0 to 1), move (signed: mid_to - mid_from), direction (up or down), window_s (300), bid and ask of the later quote.
  • Filters: market, token_id, move (one value, up or down) and min_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). move is signed and min_move is 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.

On this page