A real-time Hyperliquid wallet tracker / whale watcher as a multi-tenant Telegram bot,
written in Rust: anyone can message it, follow any wallet trading perps on
Hyperliquid, and get a DM the moment that wallet opens, scales into,
reduces, or closes a position — with entry, size, notional, leverage, realized/unrealized PnL
and ROI. Useful for copy trading, whale watching, and monitoring your own accounts, all over the
public trades firehose.
- No auth, no scraping. Uses only Hyperliquid's sanctioned public API
(
wss://api.hyperliquid.xyz/ws+POST /info). See docs/hyperliquid-api-map.md. - Scales to 1k+ wallets on one connection — the
tradesfeed carries both counterparties, so one market-data subscription serves every subscriber by filtering. - Multi-tenant. Each subscriber keeps their own labelled watchlist; a wallet's activity fans out to every subscriber following it, in their own chat with their own label.
- Stores nothing but the watchlists. Positions live in memory and are rebuilt from the chain
(
clearinghouseState) on every start; the SQLitetracker.dbholds only(chat_id, address, label).
Design + rationale: docs/DESIGN.md.
trades firehose ─► resolve_deltas (users:[buyer,seller] filter) ─► apply_fill state machine
└─► open/add/reduce/close ─► Telegram push
clearinghouseState ─► silent seed (cold-start correctness) + leverage ┘
The cold-start guarantee: before a wallet is admitted to the live filter, its current on-chain positions are seeded silently, so an add on a position opened before the tracker started is correctly reported as "Added to position", never a false "Started trade".
The bot never sends a message per fill. Each (wallet, coin) position gets one message — a
"TAPE" card — that is edited in place through the whole lifecycle: open → add → reduce →
close. Telegram edits are silent, so you get one ping when the trade opens and the card then
quietly morphs until it ends as the completed round-trip. No spam — and scrolling back shows
each position's final state, not a trail of stale fills.
Cards are terminal-style grids: the same fixed slots across every event, in a monospace block, so the numbers a copy-trader acts on always sit in the same place. The trader name in the header is a link to the wallet's explorer page (the address is one tap away — no noisy raw-address line), and the meta line carries a TX explorer link and the fill time. A fresh open:
🟢 Whale-1 · BTC LONG 10x
ENTRY $63,120
SIZE 2.5 BTC
NOTIONAL $157.8k
🔗 TX · 🕒 12:00 UTC
…becomes, via silent edits, the same message once the wallet closes:
🔴 Whale-1 · BTC LONG 10x
SIZE 2 BTC
ENTRY $63,120
EXIT $64,500
NET P/L +$2,760
ROI +21.9%
HELD 3h12m
🔗 TX · 🕒 15:12 UTC
In between, an add shows the fill, the % it grew the position, the new blended entry and the
live uPnL (ADDED +1 BTC +50% / ENTRY $62,800 / uPNL +$960); a reduce shows the booked
PnL, the ROI on the reduced slice, and what's left (REDUCED -0.5 BTC -20% /
REALIZED +$440 / ROI +14.7% / SIZE 2 left). ROI is the leveraged return on margin,
shown when leverage is known.
Every live card carries a 🔄 Update P&L button: tapping it re-reads the wallet's on-chain
state and edits the card into a live snapshot — current MARK, fresh uPNL + ROI, and the
liquidation price with its distance from the mark (LIQ $57,180 -10.7%).
Set TRACKER_NOTIFY_REDUCE_CLOSE=false to suppress new reduce/close pings (relevant after a
restart, when there's no live card left to edit) — an existing card is always edited silently,
so it never goes stale.
One credential: a Telegram bot token. It cannot be generated by code — Telegram issues it via @BotFather:
- Open Telegram, message @BotFather, send
/newbot. - Choose a display name, then a username ending in
bot. - BotFather replies with a token like
123456789:AA...— copy it.
Put it in a .env at the repo root (loaded via load_dotenv()), or export it:
# .env
TELEGRAM_BOT_TOKEN=123456789:AA...Verify the token works (and optionally see chat ids that have messaged the bot):
cargo run --bin hl-tracker-telegram-setup # reads TELEGRAM_BOT_TOKEN from .envThat's it — there is no chat id to configure. This is a public bot: each subscriber's chat is captured automatically when they message it. (On-chain data needs no key — Hyperliquid's public API is unauthenticated.)
Because the bot is public by default, anyone who finds it can use it. If ADMIN_CHAT_ID is
present, the bot is only accessible to you — no one else. Every command (/start, /help,
/add, …) and button tap from any other chat is silently ignored, and persisted watchlists
from other chats stop receiving notifications (their rows stay in the DB — removing the
variable makes the bot public again). Find your chat id by messaging the bot and running
cargo run --bin hl-tracker-telegram-setup, then:
# .env
ADMIN_CHAT_ID=123456789To allow a few extra chats besides the admin, add them to TRACKER_ALLOWED_CHAT_IDS
(comma-separated); the two combine.
The tracker is a Rust binary; docker is the intended deployment (the original Python
implementation still lives side by side — uv run hl-tracker — but the Rust port is what ships
and carries the current notification format):
make up # docker: rebuild + start in the background
make logs # follow (notifications land here in log-only mode)
# or natively:
cargo run --release --bin hl-tracker # a.k.a. make run-rsThen, in Telegram, anyone can DM the bot and manage their own watchlist:
/add 0x6bea…597f Whale-1 follow a wallet (labelled)
/remove 0x6bea…597f stop following
/rename 0x6bea…597f Whale relabel
/list show the wallets you follow
/positions live open positions of a wallet you follow (tap to pick one)
/positions renders a terminal watchlist — largest position first, abbreviated money, and a
TOT footer with the wallet's net notional and net unrealized PnL:
📋 Whale-1 · 3 open
COIN SIDE VALUE uPNL
BTC L 10x $128.2k +$3.1k
ETH S 5x $64.4k -$820
DOGE L 3x $9.6k +$95
----------------------------
TOT 3 pos $202.2k +$2.4k
| Variable | Default | Meaning |
|---|---|---|
TELEGRAM_BOT_TOKEN |
— | Bot token from @BotFather; without it, notifications are logged only |
ADMIN_CHAT_ID |
(open to all) | If present, the bot is only accessible to you — commands, buttons, and notifications from/to any other chat are ignored |
TRACKER_ALLOWED_CHAT_IDS |
(open to all) | Comma-separated chat-id allowlist to make the bot private (combines with ADMIN_CHAT_ID) |
TRACKER_NOTIFY_REDUCE_CLOSE |
true |
Notify on reduce/close (false = open+add only) |
TRACKER_RECONCILE_INTERVAL_S |
45 |
Leverage/drift reconcile cadence (0 disables) |
TRACKER_RECONCILE_BATCH |
20 |
Wallets re-seeded per reconcile cycle |
TRACKER_SEED_CONCURRENCY |
8 |
Concurrency of the startup seed sweep |
TRACKER_LIVE_COINS |
(all perps) | Comma-separated coins to restrict the feed |
TRACKER_DB_PATH |
tracker.db |
SQLite watchlist path (repo root) |
make check # everything: ruff + ty + pytest, plus clippy -D warnings + cargo test + fmt
make check-rs # Rust side only