Cabbge is a production Telegram trading bot for Hyperliquid: perpetual futures, HIP-3 tokenised equities and commodities, HIP-4 prediction markets, spot, deposits, withdrawals, and an MCP server that lets an AI client place the same orders. 35,432 lines of TypeScript, deployed on Fly, typecheck-clean.
This page is the engineering record. What the architecture is, which problems were genuinely hard and how they were solved, the numbers you can count yourself, and an honest account of how it went after launch. Every claim below points at a file in the repository. There are no user counts, revenue figures or testimonials on this page, because there are none to report.
src/tsc --noEmitCounts measured from the working tree on 2026-08-23. The commit history runs to 2026-07-21 (196 commits, first commit 2026-05-22); the difference is the white-label refactor, which is written but not yet committed. Reproduce with find src -name '*.ts' | xargs wc -l and bun run typecheck. If those commands disagree with the numbers above, trust the commands: the code moved and this page did not.
Two front doors, one execution core. A Telegram user and an AI client both end up in the same order builder, the same signing layer, and the same Hyperliquid endpoint. Nothing about custody or signing changes based on which door you came through.
agent key that removes the MPC round trip from the hot trade path.
approveBuilderFee, spotSend, usdClassTransfer, tokenDelegate, withdraw3). The exchange client wraps that with nonce management, retry, error classification and a self-healing path for revoked agents.
xyz:AAPL), HIP-4 prediction markets by a computed asset id of 100_000_000 + (10 × outcomeIndex + sideIndex), and spot by token pair. Everything past that point is shared.
setTimeout tick rather than setInterval, specifically so a stalled database round trip cannot cause two cycles to run concurrently and race each other's cooldown checks. Each tick is wrapped so a single user's failure never kills the cycle.
CREATE TABLE statements, 31 tables live (a partner-payouts pair was added and then dropped when that product direction was cut). A typed query module per domain (users, agents, trailing stops, copy follows, referrals, settings, HIP-4 positions). A hot-path cache layer sits in front with an in-flight map so a hundred simultaneous requests for the same market produce one upstream call.
Not the CRUD. These are the parts where getting it slightly wrong means a signature the exchange rejects, a double fill the user did not ask for, or a position sitting on the exchange with no stop attached.
Hyperliquid does not verify a signature against bytes you send it. It reconstructs the action bytes server-side and checks the signature against its own reconstruction. So your msgpack output has to match Python's, byte for byte, or every order you sign is rejected with an unhelpful error and a garbage recovered address.
The action hash is keccak256(msgpack(action) ‖ nonce_be8 ‖ vault_flag ‖ optional_vault ‖ optional_expires), then that hash goes in as connectionId inside a fixed Agent { source, connectionId } struct on domain { name: "Exchange", chainId: 1337 }. Map keys have to be emitted in insertion order, not sorted, which means the action objects have to be constructed in the exact field order the Python reference uses.
One example of how unforgiving this is: withdraw3 takes destination as an EIP-712 string, not an address. Using address is more natural and produces a valid signature over the wrong hash, so Hyperliquid recovers a nonsense account and answers "must deposit before performing actions". That was found end-to-end, on a real withdrawal that kept failing, and the fix is annotated in the source so nobody undoes it.
Hyperliquid requires strictly increasing nonces per wallet. Date.now() is not good enough: a worker tick and a user callback can land in the same millisecond and produce a duplicate. The nonce generator clamps forward instead, taking the next tick when the clock has not moved, which burns a few milliseconds of nonce space and never collides.
The larger problem is human. Impatient users double-tap "Confirm" before the bot has edited the message, and each tap is a fresh callback query producing a fresh order with a fresh nonce. Hyperliquid accepts both, and the user wakes up with two positions they did not want. The defence is a per-user, per-asset in-flight lock, keyed on u:userId:assetId, falling back to w:walletId:assetId for the prediction-market path where the user id is deliberately null. The fallback exists because without it that path bypassed the lock entirely.
Underneath, separate read and write token buckets: 30 reads and 10 writes per user per minute. Money-moving actions get the stricter one.
Every user gets their own secp256k1 keypair, approved on Hyperliquid via approveAgent. The protocol scopes that key to placing and cancelling orders and adjusting leverage. It cannot withdraw or move collateral. That is the ceiling on the blast radius: a total server compromise lets an attacker churn a balance through forced trades, not drain it.
The private key is stored encrypted with AES-256-GCM under a per-user key derived by HKDF-SHA256, where the input keying material is an environment secret that never touches the database and the salt is the user id. A database dump on its own decrypts nothing. Signing is pure CPU, roughly a millisecond, with no network hop.
Third leg: an anomaly watcher polls per-agent trade counters every 60 seconds against an in-memory baseline. Thirty trades in a minute DMs the user with a revoke button. A hundred in five minutes pages the operator, for the case where the user is asleep. And when Hyperliquid deregisters an agent (which it does when an account is emptied to zero), the exchange client detects that exact rejection, revokes the dead agent locally, re-signs the identical action with the master wallet under a fresh nonce, resubmits, and re-provisions a new agent in the background. The user sees a trade that worked.
Hyperliquid has stop orders. It does not have a stop that follows price up and never comes back down. Building that server-side means holding state the exchange does not hold for you, and being correct about it every 60 seconds, forever.
The cycle: fetch clearinghouse state once per user (one call covers all their coins), walk the extreme (high for a long, low for a short), compute the new trigger at extreme × (1 ∓ trail_bps), and only act if the new trigger is strictly tighter than what is already armed by at least 5 basis points. Below that threshold it just persists the walked extreme, because churning cancel-and-replace on 0.01% moves is how you burn API quota for nothing.
The correctness work is in the edge cases. If the user flipped from long to short on the same coin between ticks, position size is non-zero but on the wrong side; walking that extreme would cancel the old stop and leave the new position naked, so the trail is disabled and the user is told to re-arm. If the stop we placed is no longer resting (the user cancelled it in the Hyperliquid UI), we re-arm rather than assume. Each trail is isolated in its own try/catch so one failure costs one trail one minute, not the whole cycle. And in the one genuinely dangerous window, cancel succeeded but the replacement was rejected, the user gets an immediate DM saying the position is unprotected right now, with re-arm and fixed-stop buttons attached. Discovering that after a liquidation is not acceptable.
A new user with USDC on Arbitrum and no ETH cannot deposit to Hyperliquid. They need gas, and telling someone to go buy ETH to move the money they already have is where onboarding dies.
The bridge signs an EIP-2612 permit off-chain with the user's wallet, authorising the Hyperliquid bridge contract to pull their USDC, then submits batchedDepositWithPermit from a treasury relayer that pays the gas. No approve transaction, no ETH balance, no second signature. If no relayer is configured the same code path falls back to a user-signed transaction and surfaces a specific error about the ETH shortfall rather than a generic revert.
Two pollers watch for arrival on both the Arbitrum and native L1 sides and notify the user when funds land, because a deposit that has been credited but not announced is indistinguishable from a deposit that was lost.
Claude's custom connectors authenticate one way: OAuth. A static bearer token will not work, a token in the URL will not work. To be a first-class connector you have to be an authorization server, not just consume one.
So the bot serves the whole surface: protected-resource metadata (RFC 9728), authorization-server metadata (RFC 8414) with an OpenID-configuration alias for clients that probe it, dynamic client registration (RFC 7591), an authorize endpoint, and a token endpoint. PKCE with S256 is required, not optional; anything else is rejected at validation. Discovery metadata is derived from the request host so it stays self-consistent on any domain without a redeploy.
The identity problem is separate and more interesting: an AI client has no idea who the Telegram user is. That gap is closed with Sign-In With Ethereum on chain 42161, where the server builds the message, stores a single-use nonce, and verifies the signature, plus a short-lived code minted inside the bot. Eighteen tools sit behind it.
Every row below is a measurement of the repository or the deployment config, with the command or file that produces it. Nothing here is an estimate.
| Measurement | Value | How it is produced |
|---|---|---|
| Application code | 35,432 lines | 155 TypeScript files under src/. Excludes tests, landing pages, scripts and generated types. |
| Type safety | 0 errors | bun run typecheck runs tsc --noEmit and exits clean. No @ts-ignore escape hatch holding it together. |
| Background workers | 17 modules | 14 boot at startup. Of the three that do not: the funding-alert worker is held back until an opt-in setting ships, because as written it DMs every user; the yield router was cut on 2026-06-07 on unit economics and is kept only so an old position can be unwound; the third is a shared keyboard builder used by the deposit pollers, not a worker at all. Source is kept rather than deleted, and the reasons are commented at the import site in src/index.ts. |
| Database | 34 migrations | Forward-only SQL in supabase/migrations/. 33 CREATE TABLE statements, 31 tables live: migration 00022 deliberately drops the two-table partner-payouts pair added by 00021 when that direction was cut. Numbered 00001 to 00035 with one unused slot. |
| Telegram surface | 47 + 192 | 47 bot.command() registrations and 192 callback-query routes, across 44 handler modules in src/bot/handlers/. |
| MCP surface | 18 tools | 18 server.tool() registrations in src/mcp/server.ts, behind the OAuth flow in src/mcp/oauth.ts. |
| Tests | 5 files | Bun test suites covering L1 signing primitives, agent-key encryption round-trip, the rate limiter, SIWE verification and the Hyperliquid WebSocket handler. This is targeted coverage of the parts that silently corrupt money, not broad coverage. |
| Deployment | Fly.io, sin | 2 shared vCPU, 1024 MB, Singapore region for the shortest hop to Hyperliquid in Tokyo. TCP and HTTP health checks, restart policy always, SIGTERM with a 20 second kill timeout. |
| Deployment constraint | min = max = 1 | Pinned to a single machine on purpose. Telegram long polling is single-consumer, so a second machine would fight over getUpdates and crash-loop its health check. Documented in fly.toml so nobody "helpfully" scales it. |
| Capacity provisioning | ~1,500 to 2,000 | The memory budget was sized for that many concurrent users from a written breakdown of cache tuples, WebSocket subscriptions, worker overhead and Bun baseline, with the next bump documented in SCALING.md. This is a provisioning target, not a load figure. The bot never had that many users. |
| Operational safety | 4 watched limits | A scaling monitor ticks every 60 seconds and alerts the operator on memory above 80%, Telegram queue depth above 500 sustained across two ticks, Hyperliquid 429s above 30 per five minutes, and cache hit rate below 80%. |
| History | 196 commits | First commit 2026-05-22, last 2026-07-21. Roughly two months, solo. |
On the signing test specifically, an honest note: the suite asserts hash determinism, nonce and vault sensitivity, and the correct EIP-712 domain. It does not carry a golden vector copied out of the Python SDK. The real verification is that Hyperliquid rebuilds the action bytes itself and rejects any divergence, and orders signed by this code fill on mainnet.
The system went live and worked. What it never got was distribution. I am one person in India with no audience, no marketing budget and no network in crypto trading, and I spent two months on engineering and almost none on the much harder problem of getting a stranger to trust a new bot with their money. The failure was distribution, not the system. That is not a comfortable thing to put on a page, but a technical buyer can tell the difference between a build problem and a go-to-market problem, and pretending otherwise would waste both our time.
Which is exactly why the work is for sale rather than sitting idle. If you already have the audience, the community or the client, the engineering above is the part you would otherwise spend months and a lot of money reproducing. That part is done, it is documented, and I know every line of it.
Fixed price, fixed scope, delivered as source you own. The system above is the reference implementation for all three, so none of it starts from a blank file.
Signing, order building, nonce and concurrency handling, error classification, market resolution across perps, HIP-3, HIP-4 and spot. The part that takes weeks to get byte-correct and then never needs touching again.
Wallet provisioning, deposits and withdrawals, confirm-card UX, stops and trailing stops, portfolio, alerts, referrals. Deployed and running on your infrastructure, with the operational monitoring already wired.
An MCP server your users can add to a Claude client by pasting one URL. Full OAuth 2.1 with PKCE, dynamic client registration and discovery metadata, so it behaves like a first-party connector instead of a developer toy.
Two things I will not tell you. I will not tell you a bot makes money, because what is described on this page is infrastructure that places orders correctly and nothing on top of that is a promise about trading outcomes. And I will not show you traction I do not have. What I will show you is the code, and I will walk you through any part of it before you pay anything.
The demo runs the real interaction flow in your browser, no wallet and no deposit. If what you see is close to what you need, send me the scope and I will send back a fixed price.