vike
API Endpoints

Live stream (WebSocket)

One authenticated WebSocket that pushes closed Hyperliquid OHLCV bars as they complete. Lives on vike.io, not on the data.vike.io base URL, and appears in no OpenAPI spec.

One endpoint: a credential-gated WebSocket that pushes closed OHLCV bars for the subscriptions you hold. Those bars are Hyperliquid's, read from HL's own fill stream, and they are the only series this endpoint carries. It runs in the vike.io application, not the one that serves every /v1 namespace on data.vike.io.

It is spelled /v1/stream but it is not on the /v1 base URL

The REST base URL is https://data.vike.io. This endpoint is wss://vike.io/v1/stream. A client that follows the documented base and connects to wss://data.vike.io/v1/stream cannot reach it: that host's /v1/ prefix is served by an application with no such route. The path segment is the same word; the host is not.

This is a known inconsistency, and it now costs you two hosts rather than one: the REST endpoint you backfill a gap from — /v1/hyperliquid/ohlcv, the same series this stream polls — is on data.vike.io. Moving the stream onto data.vike.io is worth doing and has not been done; until it is, point the socket at vike.io and the backfill at data.vike.io.

This page describes behaviour, not captured traffic

This endpoint is invisible to the OpenAPI spec — OpenAPI has no WebSocket operation to emit — and it cannot be confirmed by probing (see Why you cannot probe it). Everything below describes how the running service behaves, including the refused intervals, the four-field subscription key and the closed-window boundary, all of which are covered by tests. No frame on this page was captured from the live endpoint, and where the behaviour is not settled, this page says so rather than guessing.

Endpoint

wss://vike.io/v1/stream

WEBSOCKET wss://vike.io/v1/stream

Subscribe to one or more Hyperliquid series and receive a bar frame each time a candle closes. Use it when you want to be told a bar has completed without polling GET https://data.vike.io/v1/hyperliquid/ohlcv on a timer; use REST for history, for warm-up, and for backfilling a gap.

The handshake takes one optional query parameter. Everything else is negotiated over the socket after it opens.

tokenqueryoptional
API key, for clients that cannot set request headers. Ignored when an X-API-KEY header is present, or when an Authorization bearer validates. An Authorization header that does not validate — malformed, revoked, expired, or not a bearer at all — falls through, and the token is read and honoured. See Authenticating.

The edge allows the WebSocket upgrade and drops a connection that goes 300 seconds without data. The server's ~15-second heartbeat keeps traffic well inside that window.

What it streams: Hyperliquid, from our own node

The feed is driven by the same engine behind /v1/hyperliquid/ohlcv, reading one-minute and one-hour rollups built from Hyperliquid fills captured by a node Vike runs itself. That is the whole source: one venue, our own node, and the same series the REST endpoint serves. The one-minute rollup trails wall clock by 18 seconds, measured 2026-09-03, which is the lag the 15-second poll cadence is sized for.

Exchange-pair symbols are not on this stream, and they fail silently

BTCUSDT, ETHUSDT, a _PERP suffix, anything shaped like an exchange pair: a subscribe carrying one is accepted — nothing validates the symbol against HL's asset list — and then streams nothing, forever, because Hyperliquid has no such asset. You cannot tell that apart from a quiet market.

There is no second series behind this endpoint to fall back to. Subscribe with the HL asset — a perp ticker (BTC), a spot token (HYPE) or a raw spot index (@107) — and resolve it over REST first if you are not sure it trades.

Which markets and DEXs are reachable

An HL bar is not identified by symbol alone, so the subscription carries two more fields:

FieldValuesDefault
marketperp or spot. Lower-cased before use, so "PERP" is accepted.perp
dexThe core venue HL, or a HIP-3 builder-deployed perp DEX. 1–16 alphanumeric characters, upper-cased before use, so "hl" is the default.HL

Symbols follow the REST endpoint: a perp ticker (BTC), a spot token (HYPE) or a raw spot index (@107). Whatever you send is stripped and upper-cased before use.

Do not hard-code the dex list

Builder DEXs deploy permissionlessly. The live set is read from the data and cached for 10 minutes, falling back to a fixed nine-name list only when that read fails — and an earlier three-hour sample of that fallback saw only five of the nine actually trading. Subscribe with the dex you want and read the error message, which names the known set. Note also that spot lives on the core venue only (HL is perp and spot; the rest are perp DEXs), so market: "spot" with a builder dex is a well-formed subscription that will never produce a bar.

What changed for a client, and what did not

The wire protocol is unchanged: subscribe/unsubscribe/pong, the bar frame shape, the monotonic seq, the closed flag, the ~15-second ping, the bounded outbound queue and the seq-gap-then-backfill recovery path are all exactly as they were. A client written against the published spec still connects, still subscribes with {symbol, interval}, and still parses every frame. Since 2026-09-03 the data behind those frames is Hyperliquid's, so a client written before that date sees three differences:

  • Symbols are HL assets (see the callout above). The venue is pinned by construction — it is Hyperliquid — and market/dex select within it.
  • Six intervals are gone. 3m, 2h, 6h, 8h, 12h and 1w are not in HL's set. A subscription to one is now an error frame, not silence.
  • funding is still always null, for a new reason: HL candle rows carry volume_usd and n_trades, never a funding rate.

Authenticating the handshake

The handshake tries three credential sources in this order and stops at the first that produces an identity:

OrderSourceForm
1Authorization headerBearer vk_oat_... — an OAuth 2.1 access token. Requires the vk_oat_ prefix; revoked and expired tokens are rejected.
2X-API-KEY headervk_... — matched against an active API key.
3token query parameterThe same API key, in the URL: wss://vike.io/v1/stream?token=vk_...

Both header forms reach the same account — the same rule as the REST surface, see Authentication.

The query parameter exists because browsers force it

The browser WebSocket constructor cannot set request headers, so ?token= is the only way a page can authenticate. It puts a live credential in a URL. Prefer a header from any client that can send one, and treat a key used this way as more exposed than one that is not.

What a refusal looks like

If no source yields an identity the connection is closed with code 4401, and it is closed before the handshake completes. Most clients surface that as an HTTP 403 on the upgrade rather than as close code 4401.

403 does not tell you what was wrong

The server answers 403 identically for a connection refused before the handshake completes and for a WebSocket path that matches no route. A 403 therefore cannot distinguish your key was refused from your URL was wrong — /v1/stream, /v1/stream-bogus and /v1/nope all look the same from outside. Check the URL by hand before concluding the credential failed.

Subscribing

After the socket opens, send JSON text frames. Exactly three values of action are accepted:

FieldRequiredNotes
actionYessubscribe, unsubscribe or pong. Anything else is an error frame.
symbolFor subscribe/unsubscribeHL perp ticker (BTC), spot token (HYPE) or raw index (@107). Stripped and upper-cased by the server before use.
intervalFor subscribe/unsubscribeOne of the seven below.
marketNoperp or spot. Default perp.
dexNoHL or a builder DEX. Default HL.
channelNoAccepted and never read — nothing on the server inspects it, so "channel": "bars" is decoration. Bars are the only thing this endpoint streams.
Subscribe frames
{"action": "subscribe",   "channel": "bars", "symbol": "BTC",  "interval": "1m"}
{"action": "subscribe",   "channel": "bars", "symbol": "HYPE", "interval": "5m", "market": "spot"}
{"action": "subscribe",   "channel": "bars", "symbol": "BTC",  "interval": "1m", "dex": "XYZ"}
{"action": "unsubscribe", "channel": "bars", "symbol": "BTC",  "interval": "1m"}
{"action": "pong"}

Intervals are the HL engine's time intervals:

1m  5m  15m  30m  1h  4h  1d

Tick intervals are refused, on purpose

The REST endpoint accepts Nt bars (15t = 15 trades per bar). This one does not, for two independent reasons, either sufficient:

  • A tick bar has no wall-clock bucket, so which bars have closed — the only question this poller is built to answer — has no tick analogue.
  • Tick bars are not stable across polls. Trades are counted off from the start of the requested window, so the bar boundaries are anchored to that window. The poller advances its window every cycle, which re-partitions the same trades into different bars: the same ts would come back with different OHLC, and a monotonic seq over contradictory bars is worse than no bars at all.

A tick stream needs trade arrival to drive it, not a wider validator. Ask for tick bars over REST.

The subscription key

A subscription is (symbol, interval, market, dex) — all four fields, and everything the server does with a subscription (registering it, attaching, detaching, tearing it down) keys on all four. Consequences:

  • BTC/1m perp and BTC/1m spot are two independent subscriptions with two independent feeds and two independent seq counters. They never collide, and neither do two builder DEXs.
  • Unsubscribe with the same four fields you subscribed with. The key is re-derived from the unsubscribe message, so unsubscribing {symbol, interval} does not cancel a spot subscription — the keys differ, and the call is a silent no-op.
  • Subscribing to a key you already hold on that connection is a no-op; so is unsubscribing from one you do not hold.

A successful subscribe is answered with silence

There is no subscribed acknowledgement frame. The server replies to a valid subscribe with nothing at all, and the next thing you receive is either a heartbeat ping or the first bar — which for a 1d subscription is up to a day away. Do not wait for an ack; treat the absence of an error frame as acceptance.

The error frame

Every client-side rejection uses the same shape and, today, the same code:

The error frame
{"type": "error", "code": "bad_subscription", "message": "unknown action 'frobnicate'"}

message is the only field that varies, and it now diagnoses rather than gestures — each fault has its own message, checked in this order:

What you got wrongmessage
No symbol, or a blank onesymbol is required
An interval outside the sevenunknown interval '12h'; allowed: 1m, 5m, 15m, 30m, 1h, 4h, 1d
A market that is neithermarket must be one of perp, spot; got 'futures'
A malformed dexdex must be 1-16 alphanumeric characters; got 'not a dex'
A well-formed dex that is not deployedunknown dex 'ZZZ'; known: ... — the live set, named

Malformed JSON, a non-object payload and an unknown action are caught earlier still, before the subscription is built, and arrive with the same code. The socket stays open in every case; a rejected message is discarded and the connection continues.

Only the last row costs a round trip, and it is the one that can be skipped

The format checks are free. The membership check — is this dex actually deployed — needs a database round trip, and it runs only when dex is not HL: the core venue is always known and can never be the unknown one, so an ordinary subscription never pays for it. That check is also deliberately permissive: if the lookup itself fails, the subscription proceeds. It is there to catch typos, not to be a gate.

Server frames

Two frame types reach the client in normal operation — bar and ping — plus the error frame above.

bar

The frame shape is fixed, and a test holds it that way:

FieldMeaning
typeAlways "bar".
symbolThe symbol as you subscribed it, stripped and upper-cased. Not the resolved HL asset — a spot subscription for HYPE reports HYPE, not @107.
intervalThe subscribed interval.
tsCandle open time, epoch milliseconds, UTC, left-aligned to the interval — the same convention as the REST candle. A bar covers [ts, ts+interval).
open, high, low, closeThe candle.
volumeBase-asset, taker-side volume. Hyperliquid reports two rows per trade, one per side; the taker side is the figure venues publish, and counting both legs doubles it (24h BTC measured at $3.34B unfiltered against a per-trade-exact $1.67B, 2026-08-18).
fundingAlways null, see below.
closedtrue. Always, today — see below.
market"spot". Present only when the subscription is not the default perp.
dexThe builder DEX name. Present only when the subscription is not the default HL.
seqMonotonic counter, per subscription key, incremented once per emitted frame. See Sequence numbers and gaps.

The shape — the numbers are illustrative, and BTCUSDT here is a placeholder the server never inspects, not a symbol that would return bars from HL:

The bar frame
{
  "type": "bar", "symbol": "BTCUSDT", "interval": "1m",
  "ts": 1738411260000, "open": 1.0, "high": 2.0, "low": 0.5,
  "close": 1.5, "volume": 10.0, "funding": null, "closed": true, "seq": 5
}

market and dex appear only when they are not the default

A subscription is keyed by four fields, but a default (perp/HL) frame names two — so it stays byte-identical to the published spec and an existing client is unaffected. The cost is real and worth planning for: two subscriptions on one connection differing only by market or dex produce frames you cannot tell apart unless the non-default one is present, and seq cannot demultiplex them either — it is per-subscription, and both start at 1. If you hold BTC-perp and BTC-spot on one socket, route on the presence of the market key; do not route on symbol alone.

funding is always null, and closed is always true

An HL candle carries ts, open, high, low, close, volume, volume_usd, n_trades and no funding rate, so funding is present and null on every frame. The two extra HL columns are not in the frame at all: volume_usd and n_trades exist on the REST response and are dropped here, because adding fields would change a published shape. Fetch them over REST if you need them.

closed is likewise fixed: every frame is built with closed: true, and the poll only ever asks for a window ending at the last completed bucket. Both fields exist for a live-forming-bar upgrade — a live trade feed driving the same wire protocol and emitting closed: false frames — that is not shipped, so do not branch on either field expecting variation today.

ping

The ping frame
{"type": "ping"}

Heartbeat

The server sends {"type": "ping"} every 15 seconds. The client must answer with a pong message, which resets the miss counter to zero.

Reply with a JSON pong, not a protocol pong

This is an application-level heartbeat carried in a JSON text frame. A WebSocket protocol-level pong frame — what most libraries send automatically in response to a protocol ping — never reaches the application and does not reset the counter. You must send the literal pong action as a text message.

The counter is incremented immediately before each ping is sent, and the connection is closed with code 1011 once it exceeds 3 missed pongs. A client that never ponged is therefore dropped on the fourth heartbeat tick, roughly 60 seconds after the socket opened.

Note also that the server reads text frames only. Binary messages are not part of this protocol.

Sequence numbers and gaps

seq is what tells you a frame went missing, and its scope is the part that surprises people: it belongs to the (symbol, interval, market, dex) feed, not to your connection.

There is one shared feed per active key on the server, with one poller serving every subscriber. Its counter starts at 0 when the feed is created — that is, when the first client anywhere subscribes to that key — and the feed is torn down when its last subscriber leaves.

The consequences worth planning for:

  • If you subscribe to a key somebody else is already watching, your first frame's seq is not 1. It is wherever that feed had got to.
  • If you are the only subscriber and you unsubscribe and resubscribe, the feed is destroyed and rebuilt, and seq restarts at 0. A seq you saw before is not comparable with a seq you see after.
  • seq does not survive a server restart, and it is per-process.

Use seq for what it is designed for — detecting a gap within one continuous subscription — and not as a durable cursor.

Backpressure

Each connection has a bounded outbound queue of 1000 frames. When a slow consumer's queue is full the server counts the drop and moves on: the feed never blocks on one slow client. You observe the loss as a jump in seq, and the documented recovery is to backfill the missing window over REST — GET https://data.vike.io/v1/hyperliquid/ohlcv with the same symbol, interval, market and dex, over the ts range you missed (Hyperliquid OHLCV). That is the same series the feed polls, so the backfilled bars are the same bars.

It drops the newest frame, not the oldest

A full queue is not drained to make room: the server discards the frame it could not enqueue. Either way you see a seq gap, but the bars you lose are the most recent ones, and the queue you are draining is stale. If your client falls 1000 frames behind, backfill from the last seq you received rather than assuming what you are still reading is current.

What closes the connection

CauseCodeNotes
No valid credential4401Sent before the handshake completes, so it fails; clients usually see HTTP 403 instead.
Four heartbeat ticks with no pong1011~60 s after the last pong.
The client disconnects—Handled quietly.
Any unhandled server-side error on the connectiondefaultThe socket is closed.

On every one of these, all of that connection's subscriptions are detached, which tears down any feed left with no subscribers.

Freshness, and what the bars actually are

Each feed polls the HL series once every 15 seconds and broadcasts whatever closed bars are new since its last poll. The one-minute rollup behind it trails wall clock by 18 seconds, measured 2026-09-03.

These are closed bars only. There is no live forming candle, and the 18 seconds is not delivery latency at the bucket boundary — it is how long after the boundary the bar becomes readable at all. Stack the poll cadence on top and a closed bar reaches you somewhere in the region of 18 to 33 seconds after it closed, depending on where the boundary falls between two polls. That range is arithmetic over two constants, not an end-to-end measurement; nobody has timed a frame on the wire (see Known gaps).

Two mechanics behind that, both visible from outside:

  • The poller floors its own window. It asks only for buckets that have already closed, explicitly ending the window at the open of the forming bucket — otherwise it would ship a partial bar stamped closed: true that nothing would ever correct. So a subscription never emits the bar that was forming when you subscribed until it closes, and never emits a partial bar at all.
  • A fresh subscription is quiet until the next boundary. A new feed starts just before the forming bucket's open, so its closed window is empty and it queries nothing until a bucket closes. On 1m bars at 15-second polls that is up to three silent cycles; on 1d it is up to a day. Warm-up history comes from REST, not from the stream.

Every data-plane fault on this endpoint looks like silence

A subscribe is validated for shape, not for existence. Everything past that point degrades to nothing-arrives:

  • A symbol HL does not trade (an exchange pair, a typo) returns an empty candle list, forever.
  • A spot symbol the venue does not list makes the poll itself fail; the server retries next cycle and the client is told nothing.
  • A well-formed but undeployed dex is normally caught at subscribe time, but that check is permissive when the database is unreachable, so it can reach the poller and land in the same place.

No error frame is emitted from the feed — only from subscribe/unsubscribe handling. Emitting one is a deliberate non-change, not an oversight. Resolve a symbol over REST before you subscribe to it, or you cannot tell a typo from a quiet market.

Load, and what a connection costs

Fan-out is per subscription key, not per connection: 1,000 clients watching BTC/1m on the core perp produce one engine query per 15-second cycle, delivered 1,000 ways. Cost scales with the number of distinct keys subscribed across the whole process, not with the number of connections.

A spot subscription costs an extra query per poll

A market: "spot" poll resolves the spot-token map on every call, to turn HYPE into @107. That is one additional query per feed per 15-second cycle, for as long as the subscription is held. It is pre-existing behaviour of the underlying query rather than something the stream added, but the stream is what turns it into a standing cost. The dex list is not in that class — it caches for 10 minutes.

On metering: today this endpoint runs no rate-limit check and writes no usage record — authenticating the handshake is the entire gate, and there is no second gate at the edge. That is current behaviour, not a published allowance, so do not design around a stream being free. The hourly budget and its documented home are on Rate limits.

Why you cannot probe it

This endpoint cannot be confirmed or refuted by curling it, which is a large part of why it went undocumented for so long:

  • The server answers 403 identically for an unmatched WebSocket path and for a connection refused before the handshake completes. /v1/stream and /v1/nope are indistinguishable from outside.
  • The edge behaves differently by protocol. Over HTTP/2, Connection: Upgrade is rejected outright — measured as a 400. Over --http1.1 the request reaches the application and returns 403. A probe run over h2 will look like proof the endpoint does not exist.

The only positive confirmation is a completed connection: open the socket over HTTP/1.1 with a valid credential, subscribe to a symbol you know trades on HL, and wait for a bar.

Known gaps in this page

  • No observed traffic. No bar on this page was captured from the live endpoint, and the 18-to-33-second delivery window above is arithmetic over the measured data lag and the poll cadence, not a timing anyone has taken on the wire.
  • Multi-worker behaviour is not specified. A feed is per-process, and single-process operation is what is assumed today. What seq would do across several processes is not settled, so this page does not claim it.

On this page