Real-time Hyperliquid liquidation alerts to Discord, via Quicknode's gRPC TRADES stream.
You write rules in TypeScript, run one process, and get pinged the moment a liquidation matches.
**Liquidation > $1M (any market)** — Liquidation $1,243,891 | BTC Close Long 13.5 @ 92118 | user 0x9aa2…4698 | market
- Connects to Quicknode's Hyperliquid gRPC endpoint and subscribes to the
TRADESstream, filtered server-side to liquidation fills only. - Normalizes each fill into a clean shape (
user,coin,px,sz,notionalUsd,dir,liquidation, ...). - Runs every fill against every rule in
alerts.config.ts. - For each match, posts a message to a Discord webhook.
- Auto-reconnects on disconnect (exponential backoff) and detects silently-stalled streams via HTTP/2 keepalive plus an application-level ping/pong watchdog.
Hyperliquid liquidations come through the TRADES stream (not EVENTS — that one carries capital movements like transfers and validator rewards, not fills). We apply a server-side filter filters: { liquidation: ["*"] } so Quicknode drops non-liquidation blocks before they reach us — purely an efficiency optimization, since liquidations are roughly 1-in-100,000 of all trades. The liquidation field itself is present in the default unfiltered stream too; the filter doesn't unlock it, it just thins the firehose.
You need Node 20+ and an npm.
- Sign up at quicknode.com, create a Hyperliquid Mainnet endpoint.
- From the endpoint detail page, grab two values:
- The gRPC address — looks like
your-endpoint-slug.hype-mainnet.quiknode.pro:10000. Do not includehttps://, do not include any path, do not include the token. - Your auth token from the Endpoint Security tab (40 chars).
- The gRPC address — looks like
In any Discord server you can manage:
- Channel → Edit Channel → Integrations → Webhooks → New Webhook
- Name it (e.g. "HL Alert"), pick the target channel, click Copy Webhook URL.
git clone <this repo>
cd hl-alert
cp .env.example .env
# edit .env and fill in the three values:
# QUICKNODE_GRPC_ENDPOINT
# QUICKNODE_TOKEN
# DISCORD_WEBHOOK_URL
npm install
npm startYou should see:
[hl-alert] connecting to [your-endpoint-slug.hype-mainnet.quiknode.pro:10000] (token length=40) ...
[hl-alert] loaded 2 rule(s):
- Liquidation > $1M (any market)
- Liquidation > $500K (BTC)
[hl-alert] subscription filtered to liquidation trades only
[hl-alert] subscribed (stream alive, waiting for first liquidation block)
Then, once an hour or so during quiet periods, a liveness log:
[hl-alert] alive (180s since last block, 30s since last pong)
When the first liquidation block actually arrives:
[hl-alert] connected, receiving blocks
Real liquidations of $1M+ are rare — you may wait hours for one. To verify the pipeline immediately, see Testing the pipeline below.
For the default rules, the only thing you need to change is one number in alerts.config.ts. The rule name in the startup banner, the dedup logic, and the Discord message format all update automatically — there is nothing else to edit.
export const rules: AlertRule[] = [
liquidationOver(1_000_000), // ← change this number
liquidationOverOnCoin(500_000, "BTC"), // ← or this one (and/or swap "BTC" for any coin)
];Save, npm start, and the banner reflects whatever you wrote:
[hl-alert] loaded 2 rule(s):
- Liquidation > $127K (any market) ← auto-generated from liquidationOver(127_000)
- Liquidation > $55K (BTC) ← auto-generated from liquidationOverOnCoin(55_000, "BTC")
The threshold formatter handles K/M/B suffixes automatically — liquidationOver(2_500) → "$2.5K", liquidationOver(1_500_000_000) → "$1.5B". See src/alerts/helpers.ts if you're curious.
If you need fancier matching than "size threshold on a market" — direction filter, liquidation method, multi-condition — see How to add a rule below.
Liquidations on Hyperliquid are infrequent. During volatile periods the whole exchange might produce ~1 per few seconds; in calm markets the stream can be silent for several minutes, sometimes longer on weekends. In low-volatility windows you may wait a while before seeing your first alert. This is normal, not a bug.
Three log lines tell you the system is healthy even when no alerts are firing:
subscribed (stream alive, waiting for first liquidation block)— handshake worked, Quicknode is responding to our pings.alive (Xs since last block, Ys since last pong)every 60s — heartbeat is positive; the stream is connected and just quiet.connected, receiving blocks— first real liquidation arrived; data is flowing.
If you want to confirm the pipeline is wired up in any market condition without waiting for a $1M liquidation, use the DEBUG rule described in Testing the pipeline below.
Open alerts.config.ts.
Use a helper. One line per rule, auto-generated name, dedup baked in:
liquidationOver(2_500_000), // any market, ≥ $2.5M
liquidationOverOnCoin(750_000, "ETH"), // ETH only, ≥ $750K
liquidationOverOnCoin(100_000, "SOL"), // SOL only, ≥ $100KThat covers most needs. If you want to change a threshold later, edit the number — that's it.
For anything beyond "size on a market" — only Close Long liquidations, only backstop-method liquidations, leverage filters, custom message text — drop down to the manual rule shape:
{
name: "Backstop long liquidation > $250K",
match: (e) =>
e.liquidation !== null &&
e.user === e.liquidation.liquidatedUser &&
e.liquidation.method === "backstop" &&
e.dir === "Close Long" &&
e.notionalUsd >= 250_000,
message: (e) =>
`Backstop long liquidation ${fmtUsd(e.notionalUsd)} | ${e.coin} ${e.sz} @ ${e.px} | user ${shortAddr(e.user)}`,
},Manual rules need the formatters imported: import { fmtUsd, shortAddr } from "./src/alerts/helpers.js".
- Dedup against the two sides of a liquidation. Every liquidation produces two fills — the liquidated user's fill and the counterparty's fill — both carrying the same
liquidationobject. Always adde.user === e.liquidation.liquidatedUserto yourmatchif you want one alert per liquidation, not two. - Multiple rules fire independently. If a single fill matches three rules, you'll get three Discord messages. There's no "first match wins" — see
src/alerts/engine.ts.
The fields on each event are documented in the header comment of alerts.config.ts. notionalUsd is px * sz precomputed.
To prove end-to-end delivery without waiting for a $1M+ liquidation, uncomment the DEBUG rule at the bottom of alerts.config.ts:
{
name: "DEBUG: any trade",
match: () => true,
message: (e) =>
`${e.coin} ${e.side} ${e.sz} @ ${e.px} (${fmtUsd(e.notionalUsd)}) liq=${e.liquidation ? "yes" : "no"}`,
},Because the subscription is filtered to liquidations only, the rule will fire on every small liquidation (typically $20–$2,000 each across builder-DEX markets). Re-comment the DEBUG rule before walking away or your channel will get noisy.
The client supervises its own connection (src/grpc/client.ts):
- On
errororend: exponential backoff (1s → 2s → 4s → 8s → 16s → cap 30s), resets on first data frame. - HTTP/2 keepalive:
grpc.keepalive_time_ms=30000,grpc.keepalive_timeout_ms=10000. Detects TCP/NAT-level deadness. - Application-level ping: a
SubscribeRequest.pingevery 30s. If no data and no pong in 90s, declare the stream stalled and reconnect. - Liveness log every 60s during quiet periods (
alive (Xs since last block, Ys since last pong)) so you can distinguish "stream is quiet" from "stream is dead".
Verified by toggling Wi-Fi mid-stream and watching stream error → reconnecting in 1s → connected, receiving blocks.
src/
index.ts # entry point
config.ts # env validation
grpc/client.ts # gRPC connection, reconnect, heartbeat
alerts/engine.ts # event normalization + rule evaluation
alerts/helpers.ts # rule-builder helpers (liquidationOver, etc.)
notifiers/discord.ts # webhook sender with 429 backoff
alerts.config.ts # YOUR rules live here
proto/streaming.proto # vendored proto (Quicknode does not publish a download URL)
.env.example
About 500 lines of TypeScript total. The whole src/ tree should read end-to-end in 15 minutes.
- Only the TRADES stream, only Discord, no Telegram/Slack/email.
- No web UI, no dashboard, no database. The process is the system.
- No multi-user, no auth, no API to add rules at runtime — edit the file and restart.
- No tests. Verification is manual (run it, look at Discord).
MIT.