Hyperliquid Desk for Hermes
A live Hyperliquid market dashboard inside Hermes, on Hyperliquid's public data:
- A dashboard tab with header stats, the order-book ladder, the tape, aggressive flow, observed depth, the Flow Deck (per-market regime cards plus an events list with burst alerts), the wallets behind the volume with drill-down, and a price chart drawn with TradingView Lightweight Charts, with the node's stop clusters, resting depth and net taker flow per candle (on every interval, from the node's fills) as layers.
- Twelve agent tools: nine for the market,
hl_market,hl_flow,hl_context,hl_status,hl_alerts,hl_wallets,hl_candles(the venue's candle history per interval, with the in-progress candle unverified),hl_structure(a deterministic multi-timeframe read of those candles, days down to minutes: ranges, changes, streaks, bars and volumes as numbers, never labels),hl_watch(the swing-context block per market: where the price sits in the 24h and 7d ranges, taker flow imbalance over 15m, 1h and 4h, the 4h wave, taker notional per hour, the hour's top wallet and a verdict by a fixed rule; descriptions, not a forecast), and three for the node data,node_status,node_bookandnode_stops(see Node mode). - A commentary skill,
hyperliquid-desk:hyperliquid-commentary, that explains only what the tools returned, cites the snapshot and its freshness, and describes rather than recommends. - A
/hlcommand for status and for starting, stopping and restarting the collector.
Hyperliquid Desk is an independent, open-source plugin. It is not affiliated with, endorsed by or connected to Hyperliquid.
It's read-only. There is no order code, no exchange endpoint, no signing and no trading or wallet key; the only credential is the optional node feed key described under Node mode. Markets: BTC, ETH, SOL, HYPE, xyz:SP500, xyz:XYZ100; each must pass a metadata check against Hyperliquid's info endpoint, and unknown markets are unsupported.
Requires Hermes >= 0.23.0 (tested on v0.23.0), Linux or macOS.
Quickstart (two minutes)
- Install (below) and enable the plugin; it asks once for consent to install its single dependency.
- Open
hermes dashboard, choose the Hyperliquid Desk tab and press Start. Within a few seconds the header shows the mid price, spread and top of book for the selected market, from Hyperliquid's public API; the tabs switch between BTC, ETH, SOL, HYPE, xyz:SP500 and xyz:XYZ100. The regime, net flow and top wallet fill once the first 90-second window is complete, about two minutes after Start. - Within about a minute the node panels fill from the hosted node-data relay, with no key and no configuration: the node's full order book, resting walls, the stop and liquidity map with the whole-position size estimates, and "What the node sees". The header's data feed line names the relay.
- In a chat (with a model provider configured in Hermes), ask "What is BTC doing? Use the hyperliquid commentary skill". The agent answers only from the tools' snapshot and cites its freshness.
If the relay is unreachable, full or switched off, the node panels say so in plain words and the public data keeps working. Nothing here trades or signs; see Disclosures.
Install
Copy or clone the plugin directory into your Hermes plugins folder, enable it, then restart Hermes:
git clone https://github.com/KevinM0001/hermes-hyperliquid-plugin ~/.hermes/plugins/hyperliquid-desk
hermes plugins enable hyperliquid-desk # asks for consent to install its one dependency: websockets>=13.0,<16
Then, in a Hermes chat:
/hl start # starts the live collector (nothing runs until you do this)
/hl # collector state, provenance and freshness per market
Open the dashboard (hermes dashboard) and choose the Hyperliquid Desk tab. The tab has its own Start, Stop and Restart buttons. Ask the agent things like "What is BTC doing? Use the hyperliquid commentary skill", or paste the tab's snapshot ID so the agent cites the same numbers you see.
How the numbers are kept honest
- One state owner per Hermes profile. One collector builds an immutable snapshot every second. The tools and the tab read the same snapshot by its ID (
hd-<run>-<seq>), from whichever Hermes process they run in (other processes ask the owner over a local socket), so chat and screen can't disagree. A "latest" snapshot older than 5 s, or one from a stopped collector, is never served as current. A snapshot's age is measured on the owner's monotonic clock, so a wall-clock step can't make it look younger; a latest snapshot whose age can't be established that way isn't served. A snapshot asked for by ID is labeled with its age, and every tool result built from it is labeled historical, "as of" its build time. A tool reading the latest snapshot re-judges each public freshness status at the snapshot's served age: a field that has lapsed by then reads stale or delayed, with the reason, and every value built on it is withheld, as for node results. The tab ages the snapshot it shows the same way, counting from when it asked for it, so delivery delay and time on screen count: lapsed values turn unavailable in place, with the reason, and the delay bar says when the snapshot is past 5 s or a status has lapsed. The tab never blanks the page for a delay; the empty view is only for having no current snapshot at all. - Provenance on everything. Every result carries its mode (
LIVE,REPLAYorSYNTHETIC), its source, the market, the snapshot ID and its coverage (since when trades were observed, and the gaps). - Freshness is separate from provenance. A value is fresh only when its feed is still advancing (its progress signal advanced within a receipt-age limit, on the monotonic clock) and its source timestamp is recent, judged against the local clock with a known error bound. That bound is the kernel's maximum clock error when the system clock is NTP-synchronized (Linux), or the
clock_error_bound_mssetting. A feed that advances but runs behind is delayed. With no known clock bound, or no source timestamp, a value is unverified, never fresh. Statuses are re-checked every second even when nothing arrives, and a connected socket or a ping never makes data fresh. The thresholds are inschema/metrics.v1.jsonand are tested. - Null with a reason, never zero. Missing, partial, stale, unverified or warming-up values are
nullwith the reason shown next to them. The one exception is the context family (mark, oracle, funding, open interest, day volume), shown with its value marked unverified and the receipt age (below). - Definitions live in one versioned schema,
schema/metrics.v1.json(v1.3.0). For every metric and alert rule it gives the formula, units, inputs, side convention, window, threshold, reset rule and completeness rule.
What is sampled, unverified or unavailable
- Book levels are aggregated. Hyperliquid returns at most 20 price levels per side, never individual orders. The level count is shown as API count: Hyperliquid's docs describe it both as "number of levels" (info l2Book) and as "number of orders" (websocket).
- Depth. Exact depth within ±0.25%, ±0.5% and ±1% comes only from the full-precision returned levels, and only where they reach that band on that side; otherwise it is unavailable and the panel shows the observed depth within the returned levels with its span. The aggregated
nSigFigs=4andnSigFigs=3books (polled from the info endpoint) give a separate figure, labeled approximate. Each bucket counts at its representative price; the bucket alignment isn't documented, so the figure comes with a range covering the boundary buckets. An approximate imbalance appears only when both sides come from one aggregated book. ThenSigFigs=4,3and2books are also published as grouped ladders (book.ladders, the tab's grouping dropdown), levels as Hyperliquid aggregated them; a grouped ladder whose book isn't fresh is withheld whole, with the reason. - Flow, the Flow Deck and wallets cover only what was observed since the collector started or the last gap; nothing is backfilled. Each trade is counted once, by (coin, time, tid). A trade missing any essential field (price, size, time, tid, a B/A side, two 0x addresses, or its coin) is never filled in: it ends the coverage segment as a gap, trades freshness restarts, and counting resumes after it. A trades message that can't be attributed to a market ends every market's trade coverage the same way. The first trades message after each (re)subscription is Hyperliquid's snapshot of recent trades: it is shown on the tape but not counted.
- bbo is always "freshness unverified": it's sent only when it changes, so silence proves nothing. Its prices are never shown; best bid and ask come from the fast book.
- activeAssetCtx (mark, oracle, funding, open interest, day volume) carries no source timestamp, so its source age is unknown. It is therefore always unverified, never fresh: a deliberate consequence of the freshness rule, since no other public channel supplies these fields with a timestamp. Since schema 1.3.0 those values are shown anyway, marked unverified with the receipt age of the last changed context: each is
{value, unit, status: "unverified", reason, received_age_s}, in the tab, inhl_contextand in the snapshot, and the receipt age keeps growing while a snapshot is served or on screen. They arenullwith the reason when the collector is stopped, disconnected or warming up, or when no changed context arrived within the max age (20 s for BTC and HYPE, 45 s for the xyz markets), so a dead stream never shows old numbers. - Trades in a quiet market: after 30 s (BTC), 60 s (HYPE) or 120 s (xyz) with no trade, flow values are unverified, because silence can't be told apart from a stalled feed. A late trade (one at or before the watermark) is counted but never makes the feed fresh again.
- Funding is shown per 1 hour (Hyperliquid's funding page: "paid every hour"; the API field's own period isn't stated). Open interest is in coin, and day volume is in USD; the API pages don't state those units, so the plugin follows the docs' notation conventions.
- Flow Deck. Regimes, thresholds and card numbers are ported from the author's terminal Flow Deck (
hl_deck.py, a private tool, not distributed).- PRESSURE is aggressive buy notional over aggressive sell notional, every execution included (flips too), and is available when selling is at least $25K. It is the same in public and node mode, so TRENDING-UP/DOWN can fire on public data.
- hl_deck's flip-free ratio survives only as a named legacy comparison in the schema, and isn't computed.
- The regime explanations are plain descriptions of the window's measurements; they never claim a cause, an intent or a forecast.
- Each card shows the confirmed (latched) regime, the live classification of the current window and any pending change separately.
- IMPACT is the absolute observed trade-price change per $1M of net aggressive flow: a descriptive ratio.
- A late trade, one whose time falls in a tick already evaluated, re-evaluates the affected ticks, so the card's numbers, the latch and the regime-change events always match the trades counted. A change the late trade no longer supports is reported as withdrawn.
- The top wallet's share keeps hl_deck's population, and the card adds its net aggressive notional (aggressive bought minus aggressive sold).
- The opening and closing activity buckets (new long, cover, new short, exit) need node mode. The schema defines them per position effect, flips split.
- hl_deck's whale alerts (GONE, BACK, FLIPPED, CONVICTION, SELLING DONE) aren't ported in v1.
- Burst alerts. Aggressive buying or selling over a threshold within 60 s. They name the aggressive wallets behind the burst. No regime, alert or window ever spans a gap.
- Public mode (handoff definition): from public trades; at least $10M for BTC, ETH $5M, SOL $2M, HYPE $2M, xyz markets $1M by default, set per market in
burst_min_usd. - Node mode (a
node_feedis set): from node fills, evaluated once per node block, with the rule the thresholds were measured with. A window containing a rejected or unreadable record is incomplete: it never fires, re-arms or disarms. Public-trades bursts aren't shown in node mode. With a relay (the hosted default), a market's bursts come from node fills while they are flowing for it and from public trades otherwise (no panel on screen withoutnode_alerts, or the relay paused, unreachable, full or stopped). One known exception, to be fixed after the release: a relay that stays connected while it has no feed from the node keeps node bursts selected, with no new fills, until the flow window is lost. Each market's threshold is automatic by default: a dated, versioned calibration measured on node fills from Nov 3 to 7, targeting about 10 bursts a day. BTC is $30M and xyz:XYZ100 $5M ("automatic: about 10 a day, measured Nov 3 to 7"). HYPE and xyz:SP500 are $5M, and their labels state the achieved rate, 14.6 and 7.2 a day, because no threshold came within 25% of 10. Each burst is ranked against that week's minutes: "a top x% minute, Nov 3 to 7", or unavailable when the reference file isn't loaded. See "Burst thresholds in node mode" below to set a market's threshold by hand.
- Public mode (handoff definition): from public trades; at least $10M for BTC, ETH $5M, SOL $2M, HYPE $2M, xyz markets $1M by default, set per market in
- Watch (the swing-context block:
deck.<market>.watch,hl_watch). Per market: the price (the fast book's mid when fresh, else the last trade) and where it sits in the 24h range (the last 24 closed 1h candles; the middle 35 to 65% is the band the rule marks as mid-range) and in the 7d range (7 closed days, or 42 closed 4h candles when fewer exist); taker flow imbalance over 15m, 1h and 4h from minute bars of the counted public trades; the 4h wave (the second half of the four hours against the first); taker notional per hour over the covered minutes; the hour's top wallet; and a verdict by a fixed rule (MIDDLE, LOW or HIGH, + FADING or + BUILDING). A minute counts only when it lies wholly inside one coverage segment, every window says how many minutes it covers and whether it spans a gap, the ranges use closed candles only (never the candle in progress), and the parts built on trades or on the fast book are withheld with the reason when those aren't fresh, at build time and while a snapshot is served or on screen. Descriptions by that rule, not a forecast. - Wallets. Addresses are public on-chain identifiers; the plugin never guesses who owns one. Bought plus sold is participation, not volume, and net bought is not a position change or PnL. An opened wallet's account state is fetched from the info endpoint for the market's dex only and is shown only when the answer's own time is recent when served (within 120 s, 2 minutes, under the clock bound, counting the time both requests took); otherwise every value is unavailable with the reason. Open orders have no response timestamp, so they're never shown as current. The tab keeps aging the account state from the moment it asked for it, so delivery time and every second the window stays open count. Once it passes 120 s, including on arrival, it is marked expired and its values are withheld until Fetch account again gets a new answer.
- Node mode (on by default through the hosted relay; setting
node_feed): the node ladder, waiting stops, wall watch, heatmap history, node tape with flags, burst alerts with their rank, Flow Deck activity buckets, wallet activity splits, liquidation bursts, the liquidity-thinning panel, the order flow map and the stop and liquidity map come from the engine's own feed; see "Node mode" below. Withnode_feedset tooffthose panels say "needs node mode".
Node mode
The tab's node panels and the node_status, node_book and node_stops tools show the engine's own book, the order flow map, waiting triggers and fills (the l4-runner live feed, protocol v2). By default they come from the hosted relay (below), with no setup. node_feed can instead name the engine's local Unix socket or a relay wss:// URL of your own; node_feed: off turns node data off, and everything above works as before with the node panels saying "needs node mode". Settings are read when the collector starts (/hl restart after a change).
The hosted relay (the default)
-
A third-party network call. With
node_feedat its default,hosted, the plugin connects to a relay run by the plugin's author (KevinM0001). The relay receives the engine's live feed from the author's own node and serves it to every plugin user over TLS (wss://, certificate and host name checked by your system's trust store, no proxy taken from the environment, no redirect ever followed). Its address,wss://relay.perpdepth.com/v1/feed, is written in one place,HOSTED_RELAY_URLinhl_desk/config.py. -
You trust the relay operator for data integrity. The relay passes the engine's lines through unchanged, but the plugin can't check them against the chain. It checks what the protocol allows: sequence continuity, snapshots against their deltas, and the watermark's age. A relay that sent wrong data with consistent sequences would not be caught.
-
Free for now. No key and no sign-up. The
node_keysetting stays optional and unused by the hosted relay. -
What the plugin sends: one handshake naming the market it has open and the markets whose executions it wants, then a switch message when you change markets. Nothing else, and no key unless you set
node_key. -
What the server logs (the relay's own statement): the relay process never logs IP addresses, ports, headers, paths or anything you send; its journal lines are counts, reason codes and sequence numbers, kept at most 14 days and 200 MB. Subscriber addresses are held in memory only, for the per-address limits, and forgotten within minutes of disconnecting. There is no proxy in front of the relay. The server's operating system logs SSH logins and failed login attempts with their IP addresses: about 5 weeks (the auth log) to 2 months (failed logins) under Ubuntu's defaults, and the operator's own login records can stay for many months. These steps turn on no AWS flow logs; AWS keeps its own records of the operator's account activity.
-
Data flows only while it's used. While a node panel is on screen (the tab is visible), the plugin receives the open market in full plus the executions of every followed market, so the alerts cover all of them. Switching markets switches the subscription, and the new market starts from a fresh snapshot. 60 seconds after the tab's last poll while visible (it polls once a second; hidden or closed, it stops), the subscription ends and the connection closes. Coming back starts again from a fresh snapshot. A
node_book,node_stopsornode_statuscall for a market the panels don't have open subscribes to that market for the call and releases it once it has answered. Withnode_alerts: true, every followed market's executions keep flowing while no panel is on screen (no full market), so node-mode alerts keep running. -
About how much data: measured with four markets on a recorded window, the open market plus every market's executions was 13 to 28 KB/s (0.05 to 0.10 GB an hour, BTC the largest) and executions only 9 KB/s (0.03 GB an hour); each further market adds its executions, and busy days are larger.
-
When the relay can't serve: the node panels and
node_statussay which: it can't be reached, it's full (at capacity), it's switched off by its operator, or too many connections from your network, or that the relay has no feed from the node right now. Public mode keeps working (burst alerts fall back to public trades, except in the one case under Bursts), and a replay is never used in its place. -
The protocol is followed as written.
- State is kept per source, mode, producer session, replay session and market, and each family (book, triggers, depth, bands, node fills) has its own validity.
- A new producer session, a missing sequence number, a dropped connection or an incomplete snapshot invalidates the affected families at once. They recover only from their own snapshot, assembled off to the side and swapped in whole, never rolling state back.
- A retired session's late lines are ignored.
-
Freshness comes only from the engine's watermarks, once the client has applied everything before them. Heartbeats and connection activity never count.
- The node feed's delay is the age of the market's latest watermark (now minus its block time), averaged over the last 5 s. It is shown as a counter at the top of the tab, about once a second, and carried in the snapshot (
node_statusand every family'sdelay_ms), so answers report it. - Each family reads LIVE, DELAYED (
BEHINDin the data: the averaged delay is above 10 s), shown as CATCHING UP, with the delay falling, while a fresh connection replays what the feed buffered (loading, not lagging), STALE (above 30 s; heartbeats don't count, so a silent feed's delay keeps growing), UNKNOWN (this machine's clock is behind the producer's, so ages can't be bounded) or INVALID, and every node result shows it. The limits sit well above the normal delay, so the labels don't toggle. - A delay never blanks the tab: values and labels update in place. The empty "No snapshot" view is only for having no data at all (the collector stopped, the feed down, the first start). A snapshot that has aged on screen past a limit stays on screen, and the status row says why until the next one replaces it. The screen has a 30 s allowance: the page refreshes about once a second, so the tab shows a held snapshot as built and re-judges it only after 30 s on screen (a status whose age has passed its limit within that window is labelled "as built", not fresh); the agent's tools re-judge at serving time regardless, so for up to 30 s chat may say stale while the screen still shows the number. A "no current snapshot" answer while a snapshot is on screen changes nothing: the screen freezes until the next snapshot.
- The node feed's delay is the age of the market's latest watermark (now minus its block time), averaged over the last 5 s. It is shown as a counter at the top of the tab, about once a second, and carried in the snapshot (
-
One stream per metric. Public panels count public trades; node metrics (activity buckets, wallet splits, liquidations, the node tape) count node fills. The two are never added together.
- Whether a public trade and a node fill with the same
tidare the same execution hasn't been proven on recorded data, so neither stream adds flags or quantity to the other. - A change of input stream starts a new, labelled window.
- Whether a public trade and a node fill with the same
-
Activity buckets (new long and cover for aggressive buying, new short and exit for aggressive selling) come from each participant's own position effect as the engine states it.
- A flip fills both buckets, and each converts to notional at the execution's price.
- Flow Deck cards and burst alerts use the aggressive side's effects over the same window, each percentage over that side's aggressive notional.
- Wallet splits show all fills and the aggressive (taker) fills separately, each over the wallet's own bought or sold notional.
- A missing effect shows the bucket as unavailable, with the unclassified notional; nothing is scaled to 100%.
- None of this is a change in open interest.
-
Book panels:
- Level statistics. "largest order size at this level" and "oldest resting order at this level" are separate statistics.
- Changes. Changes in the last 10 s are labelled only from lifecycle records: absorbed (filled as maker), pulled (canceled with nothing filled) or unknown. Nothing is called "moved".
- Crossed book. If the best bid is at or above the best ask, the book and every panel built on it read INVALID until a fresh snapshot clears it.
- Liquidity thinning. Depth per 0.5% band out to 10% from the price, in dollars and dollars per $1 of price, with the sharpest drop called out. What's resting right now. Orders can be added or pulled at any moment; this isn't a forecast.
- Heatmap. The 15-minute history starts when the book became valid and is marked partial until it covers 15 minutes.
-
Waiting stops are conditional interest: orders that do nothing unless the price reaches them.
- Checked against the venue only at restart. The engine compares its waiting-stop set with the venue's only when it restarts (its rebase check). During a run nothing compares them, so a stop the venue has already ended can stay shown until the next restart. The stops panel and
node_stops(venue_check) say so. - Whole-position TP/SL orders have no stated size (the venue gives 0). Each band counts them (
unknown_size_orders), and its stated size is then a lower bound; a band made only of them shows its size as unknown, never 0. - Each band shows its order count beside its size, and says so when one order makes up most of it.
- Every waiting trigger is accounted for, in a band or outside the range. The engine leaves out trailing stops (their current trigger price isn't established) and reports how many.
- The stop map, in the stops panel and
node_stops, is defined under "Stop and liquidity map" below: the bands, the lower bounds, the walk share, the exclusions and the census.
- Checked against the venue only at restart. The engine compares its waiting-stop set with the venue's only when it restarts (its rebase check). During a run nothing compares them, so a stop the venue has already ended can stay shown until the next restart. The stops panel and
-
Late node data is shown, labelled. The node feed can run behind the chain, especially in busy sessions, and in production it reaches users through a relay. A family whose delay passes its limits reads BEHIND or STALE, and everything built on it is still served, carrying the family's state, reason and delay and an "as of the node's book at , behind" note where a panel would otherwise read as the present; the tab shows a quiet "delayed" with the delay counting up. Only no data, INVALID (a gap, a crossed book, a sequence hole) or UNKNOWN (clock skew) withhold a block. The Flow Deck card's node buckets and the wallet panels' node split measure the present window, which a node behind the chain hasn't reached: they then show the last window of the same length the node's fills do cover, labelled with its own times and the gap ("the node's last 90 s, 19:02:36 to 19:04:06 UTC, 72 s behind the card's window"), never a partial present window; a burst's own window is never shifted.
-
Liquidations count once each, for the liquidated trader, whose own position effect says long or short. A burst of them (by default 2 within 60 s, or $1M) raises its own alert.
-
Order flow map (
market.node.live.flow_map, schemanode.flow_map). Aggressive (taker) flow from the node's fills over the last two hours in eight 15-minute buckets aligned to the clock, each fill once, the taker side only, at price times size. The direction is never read from price: each fill is placed by what it did to the taker's own position, read from the position just before the fill. A buy is open long (risk added) or close short (risk removed); a sell is close long (risk removed) or open short (risk added); a fill that carried a position through zero is a flip and is counted apart, so buy + sell = risk added + risk removed + flips in every bucket. A fill whose side can't be told is counted as unclassified and enters none of the numbers. Every bucket says how many of its seconds the node fills covered; a bucket not fully covered is marked and never scaled up, and the last bucket is in progress until its quarter hour ends. The four readings (net, composition, risk, concentration) are sentences the plugin writes from these numbers; the tab quotes them unchanged. They describe the window; they do not predict. -
Stop and liquidity map (
market.node.live.stops.map,.clusters,.exclusions,.census; schemanode.stop_walk_ratio,node.stop_clusters,node.stop_exclusions,node.stop_census). Per 0.5% band out to 10% on each side of the node book mid (the same bands as the liquidity-thinning panel, so the two never disagree on an edge): the resting depth in the band beside the waiting trigger orders (stops and take-profits) that would fire into it, sell-side triggers below the price and buy-side triggers above. Nothing here is a liquidation level: these are resting orders the engine holds, and the plugin never estimates where anyone would be liquidated.- Lower bounds, never zeros. Whole-position stops and take-profits rest with no stated size, so a band's trigger notional and its walk share are lower bounds whenever any of its orders is unsized, and they say so ("at least", the count of unsized orders); a band whose orders are all unsized shows no number, with the reason, never a 0. A census of the waiting set can be complete in count and incomplete in notional, and a warm start's snapshot can hold many such orders.
- The walk share is the band's stated trigger notional divided by the resting depth at and beyond the band, out to 10% on the side the triggers fire into: the bids from a sell-side band down to -10%, the asks from a buy-side band up to +10%. That is the liquidity those orders would consume once they fire and push the price away from where it is. It is not the depth between the current price and the band, which is served beside it as
walk.depth_between_usdfor anyone who wants that reading. The two largest bands per side by stated notional are the clusters, written as sentences the plugin produces and the tab quotes unchanged. - Exclusions. Each waiting trigger is counted once, first match in this order: an implausible trigger price (outside one tenth to ten times the reference price), a trigger already true at the price, a trigger beyond 10%, and a size outlier: a single trigger whose stated notional at its own trigger price exceeds everything resting at and beyond its band out to 10% on the side it fires into, which could not be walked through the book. Whole-position orders never qualify as outliers, and a band with nothing resting at and beyond it makes every sized trigger in it an outlier by this test, which is what the rule says and reads oddly beside the word. This is an absolute test against the book, not a multiple of a typical order size: on a deep market a multiple of the median stated size left out ordinary orders by the hundreds, more than the clusters it was meant to protect. The rule is served in words beside its counts (
exclusions.size_outlier), so a reader sees what was left out and why. Trailing stops are counted from the engine's own count only, because their prices and sizes never reach the feed. Every exclusion shows its count, its stated notional where any exists, and its rule, and the panel states that the bands plus the exclusions account for every waiting trigger. - Estimate (needs a feed with position streams, PROTOCOL.md revision 2.3). Whole-position TP/SL orders have no stated size, so beside each band's stated figures the map carries an estimate: each such order at its owner's current position when its side would close it, less the owner's stated orders that fire first, one position counted once per side and kind at its nearest trigger; a flat or wrong-side owner stays unknown with the reason. The estimate is a second figure, labelled and timed ("at least $17.94M stated, about $31.2M with whole-position orders at their owners' positions as of 19:04:06Z"; the time is the block of the data it is computed from, the older of the owners' positions and the waiting triggers, each at its last applied change or its last watermark, whichever is later, and a change applied before its watermark arrives is marked not yet confirmed), never inside the stated ones: the ranking, the size-outlier test, the exclusions and the census ratios use stated sizes only, and when the estimate's largest band differs the map says so in one sentence per side. The positions themselves stay in the collector's memory; nothing per account is served. Without the stream (an older feed) the map reads as before.
- Census. The waiting triggers the feed holds for the market, how many report no size, how many sit within ±10% of the price by the engine's own bands view, how many resting executable orders sit in the same range, and the executable-to-trigger ratio, with the snapshots' stated totals and their block. These are sanity checks of the live census against the engine's other views; each is a count at the book time, not a history scan.
-
Replay runs only when you start it:
/hl replay start [file], thenpause,resume,seek <time>,speed <x>,restartandstop(or the tab's replay controls).- Each start, seek and restart is a new replay session.
- Its view shows the recording's date and time, the position, the speed and whether it is playing, paused or ended.
- It is labelled REPLAY throughout, its end stays REPLAY, and it is never drawn over live prices or mixed into current metrics or commentary.
Disclosures (catalog rule 13)
| What | Details |
| --- | --- |
| Network destinations | https://api.hyperliquid.xyz/info (POST: perpDexs, meta, l2Book, candleSnapshot, clearinghouseState, frontendOpenOrders) and wss://api.hyperliquid.xyz/ws (subscriptions l2Book, trades, bbo, activeAssetCtx, candle, plus ping). Nothing else. Each endpoint and its doc link are in docs/ENDPOINTS.md. No telemetry. Node data: by default the hosted relay wss://relay.perpdepth.com/v1/feed (node_feed: hosted, the URL in HOSTED_RELAY_URL), only while a node panel is on screen and 60 s after, while a node tool answers, or with node_alerts on; or the local Unix socket or relay URL you set instead; none with node_feed: off. The plugin sends a handshake and, to a relay, view switches, and otherwise only reads. |
| Background worker | One worker thread, hyperliquid-desk-collector, created with Hermes's documented spawn_context_thread so it keeps its profile's context, running its own asyncio loop: one Hyperliquid websocket connection (plus one node feed connection: to a local socket while the collector runs, or to a relay only while node data is used, as above, and a short second one while a node tool answers for a market the panels don't have open), an info poll every 5 s per market for depth books, and a snapshot every second. Blocking HTTP requests run in that loop's short-lived executor threads, never on the gateway's event loop. Snapshots stay in memory; nothing is written per snapshot. It starts only on /hl start or the tab's Start button, never on install, import, registration, plugins doctor or plugins validate. Stop it with /hl stop; /hl restart restarts it. Reconnects back off from 1 s to 30 s, and the disconnected state is shown. Bounded memory: 16 minutes of trades and 4 hours of minute bars per market (each minute's aggressor notional per wallet for the latest hour), and the last 180 snapshots. With a node feed (or a replay), per market and source: the current order book and waiting trigger orders (sized by the market itself), 15 minutes of book changes and heatmap samples, 16 minutes of node fills (and of execution identities for the burst rule), the burst reference (about 230,000 sorted 60 s sums across the four markets, loaded once), and a bounded buffer of events during a snapshot (buffer_messages, 50,000); node views are kept in the newest 60 snapshots only. |
| Storage | <HERMES_HOME>/plugin-data/hyperliquid-desk/: collector.lock (marks the profile's single collector owner) and, while the collector runs, owner.sock, a Unix socket with mode 0600 (removed on stop). If that path is too long for a Unix socket, the socket goes in a private per-user directory under the system temp folder instead (hl-desk-<uid>/, mode 0700). Snapshots stay in the owner's memory: the last 180, each with the exact trades its panels used. Nothing else is written, and nothing is written outside it. Inside Hermes this profile directory is always used; the HL_DESK_DATA_DIR environment variable, used by the tests, is honored only where Hermes's plugin storage isn't available (outside Hermes). |
| Local IPC | The process that owns the collector answers the profile's other Hermes processes (for example the dashboard process and a plugin host) on owner.sock. It serves snapshots by ID, a wallet's executions from a retained snapshot, status, the chart's candle history, replay commands, the node data's on-screen and lease signals (hosted mode), stop and restart. Only the same OS user can connect, and there is no network listener. |
| Chat socket (Ask Hermes) | The tab's Ask Hermes card opens the dashboard's own JSON-RPC WebSocket (/api/ws, its URL from the plugin SDK's buildWsUrl), creates one chat session (session.create) and submits one prompt (prompt.submit) to the model the user picks from the dashboard's own list (SDK.api.getModelOptions / getModelInfo); the answer streams back into the card. It talks only to the local Hermes process the dashboard already talks to; no other destination. |
| Chat page strip | window.__HERMES_PLUGINS__.registerSlot("chat:top", ...): a one-line strip on the Chat page with the followed markets' mid prices and freshness, from the same snapshot route. No input, no actions. |
| Commands | /hl [status|start|stop|restart|help]; /hl replay start [file]|pause|resume|seek <time>|speed <x>|restart|stop |
| Tools | hl_market, hl_flow, hl_context, hl_status, hl_alerts, hl_wallets, hl_candles, hl_structure, hl_watch: all read-only; hl_wallets with an address makes two info requests; hl_candles and hl_structure (and the tab's chart intervals) make a candleSnapshot request per market and interval on first use (hl_structure reads six intervals by default), then at most one per interval and never more than one per 30 s; the watch block (deck.<market>.watch, read by hl_watch) requests the 1h and 1d candles of every followed market when the collector starts (4h too when fewer than 7 days exist), under the same policy. node_status, node_book, node_stops: read-only, from the same snapshot as the tab's node panels. |
| Dashboard routes | /api/plugins/hyperliquid-desk/: snapshot (the tab adds node=visible while it is visible, the signal that keeps relay data flowing), status, wallets, tool/<name>, candles (GET; candles serves the venue's closed candles per interval for the price chart, with net taker flow per candle from the node's fills beside them, taker_flow); collector/start|stop|restart (POST); node/replay/<start|pause|resume|seek|speed|restart|stop> (POST). |
| Credentials | One optional setting: node_key, a Hermes secret (HL_DESK_NODE_KEY), read only through Hermes's secret scope and sent only to a relay WebSocket URL as an Authorization: Bearer header. Not needed for a local socket or a relay without keys; never in requires_env, so the plugin loads and public mode works without it. No other secret is read or stored. |
| Shell commands, reads outside its data | None. The clock error bound is read with a read-only adjtimex system call through the C library already loaded in the process; no external command is run to find it. The one file it reads outside its data is a recording you name in node_replay_file (or pass to /hl replay start), and only when you start a replay. Inside the plugin it also reads its packaged burst calibration (hl_desk/node/calibration, about 1.7 MB compressed) once, when a node source first starts; public mode never reads it. |
| Bundled JavaScript | TradingView Lightweight Charts 5.2.1, readable development build, Apache-2.0. License, notice and on-chart attribution are kept; see THIRD_PARTY_NOTICES.md. No remote code or fonts are loaded. |
Settings
The default markets are BTC, ETH, SOL, HYPE, xyz:SP500 and xyz:XYZ100; any perp the venue lists can be followed (node mode shows the markets the feed publishes). Set these in config.yaml under plugins.entries.hyperliquid-desk.settings, or in the Desktop plugin settings form: node_feed (hosted by default, off, or a socket path or relay URL of your own), node_alerts (node-mode alerts in the background, off by default), markets, burst_min_usd (per market, public mode), node_burst_min_usd (per market, node mode, manual override), large_print_usd (per market), wallet_target_share_pct, and clock_error_bound_ms. That last one is needed only where the OS can't report its clock error bound (for example macOS, or an unsynchronized clock); without a bound, nothing source-timestamped can be fresh. Formulas never change at runtime.
Burst thresholds in node mode
Each market starts on automatic: the threshold from the shipped calibration (hl_desk/node/calibration/burst-calibration-2026-11-08.json, version 2026-11-08.1, measured Nov 3 to 7). To set one market's threshold by hand, in USD per 60 s:
hermes config set plugins.entries.hyperliquid-desk.settings.node_burst_min_usd.BTC 25000000
Then run /hl restart, since settings are read when the collector starts. The Alerts card's burst rule line (in its tooltip) and node_status show "manual: $25,000,000 (setting node_burst_min_usd)". To switch that market back to automatic, remove it and restart:
hermes config unset plugins.entries.hyperliquid-desk.settings.node_burst_min_usd.BTC
Setting it to auto instead (hermes config set ….node_burst_min_usd.BTC auto) does the same. Other markets are unaffected; xyz markets use their full name, for example node_burst_min_usd.xyz:SP500. In config.yaml it reads:
plugins:
entries:
hyperliquid-desk:
settings:
node_burst_min_usd:
BTC: 25000000
A manual threshold has no calibrated rate, so its label says manual. The rank is still against the Nov 3 to 7 reference. In public mode node_burst_min_usd isn't used; burst_min_usd is.
Development
python -m pytest -c tests/pytest.ini tests # network off; recorded public data in tests/fixtures/recorded
python scripts/build_dashboard.py . # rebuilds dashboard/dist/index.js from src + the vendored chart library
python scripts/record_fixtures.py --out <dir> # re-records public fixtures (needs network)
The tests cover every tool's JSON shape; each data rule (a band the levels don't reach, an empty side, a duplicate trade, stale data, a frozen source behind a live connection, a quiet market, a reconnect gap, clock skew); every Flow Deck regime at its thresholds; bursts below, at and above threshold and across a gap; node-mode bursts on PLAN.md's rule cases (warm-up, same-timestamp blocks, gaps, incomplete windows and every rejection) and on a recorded window of node fills against an independent computation, with ranks counted from the reference; wallet ranks and shares against independent calculations; the metadata check per market; and the real collector against a local stand-in server, including its reconnect.
The hosted relay mode (tests/test_node_hosted_relay.py) feeds the client the relay's executions view of recorded node fills (with the relay's filtered ranges between them) and runs the transport against a local relay stand-in: filtered against lost lines, a switch to full data, reconnects that change the view, the handshake's resume rules, the 60 s pause and the fresh start after it, background alerts, a tool's own subscription, each refusal shown with the relay's reason, a redirect never followed, the stop switch, a relay without its publisher, burst alerts falling back to public trades whenever relay data isn't flowing, and a tool's subscription that ends on its own if never released. The node-mode tests (tests/test_node_*.py) feed recorded node feed lines (tests/fixtures/node, provenance in its MANIFEST.json) through the plugin and read every intermediate tool and tab result. Two optional checks need local reference files and are skipped without them: HL_DECK_SOURCE (the terminal Flow Deck's hl_deck.py, for the bucket definitions) and HL_DECK_LIVE_LOG (its frames, a vocabulary check only).
See docs/DEMO.md for the 90-second public-data demo, and docs/SNAPSHOT.md for the snapshot contract.
License
MIT for this plugin (see LICENSE). Lightweight Charts keeps its Apache-2.0 license and NOTICE.
README from the reviewed commit abc8167; it updates when the author re-pins.

KevinM0001