
{"id":223284,"date":"2026-09-05T15:35:34","date_gmt":"2026-09-05T15:35:34","guid":{"rendered":"https:\/\/mycryptomania.com\/?p=223284"},"modified":"2026-09-05T15:35:34","modified_gmt":"2026-09-05T15:35:34","slug":"building-an-ai-crypto-trading-bot-on-hyperliquid","status":"publish","type":"post","link":"https:\/\/mycryptomania.com\/?p=223284","title":{"rendered":"Building an AI Crypto Trading Bot on Hyperliquid"},"content":{"rendered":"<h4>Claude decides, the Hyperliquid SDK executes, an indexed feed supplies the market. Plus the three ways the data will quietly lie to your agent, all of which I\u00a0hit.<\/h4>\n<p>An agent that trades your own account is easy. Fifty lines, one SDK,\u00a0done.<\/p>\n<p>An agent that trades your account <em>based on what the rest of the market is doing<\/em> is a different build, and on most exchanges it is impossible, because the exchange never tells you what the rest of the market is doing at the grain you would\u00a0need.<\/p>\n<p>Hyperliquid is the exception, and it is the reason to build this here rather than on Binance or on any of <a href=\"https:\/\/medium.com\/coinmonks\/hyperliquid-alternatives-9d3e05e76726\">the perp venues competing with it<\/a>. The order book runs on its own L1. Every placement, cancel, modify and fill is a signed action sitting in a block, with the wallet attached. Your agent can see who is quoting, who just got liquidated, and how much of the book is real, because all of it is on\u00a0chain.<\/p>\n<p>Getting at it takes more work than a websocket subscribe message. Here is the whole\u00a0build.<\/p>\n<p>Upfront: I work on developer content at Bitquery, and Bitquery sells the indexed Hyperliquid feed used for the read path below. The write path is Hyperliquid\u2019s own free SDK, and I will be specific about where the free native API is the better\u00a0choice.<\/p>\n<h3>The architecture: three paths, three\u00a0tools<\/h3>\n<p>The instinct is to use one API for everything. That is the first mistake, because reading the market and writing to your account are different problems with different best\u00a0answers.<\/p>\n<p>PathWhat it doesWhat serves it bestWritePlace, cancel and modify your own ordersHyperliquid\u2019s native SDK. Closest to the matching engine, free, canonical.Read (own account)Your positions, fills, marginHyperliquid\u2019s native Info API. Same reason.Read (the market)Who else is positioned, quoting, blowing upAn indexed feed. The native API cannot serve this.DecideTurn the above into an order or a decision to sit stillClaude, with the other three wired in as\u00a0tools<\/p>\n<p>That third row is the one people get wrong, so it is worth being precise about\u00a0why.<\/p>\n<p><a href=\"https:\/\/hyperliquid.gitbook.io\/hyperliquid-docs\/for-developers\/api\/websocket\">Hyperliquid\u2019s public websocket<\/a> gives you l2Book, which is size totalled per price level, up to 20 levels a side. Forty BTC rests at $95,000 and the feed cannot tell you whether that is one order or twenty, whose it is, or whether it got pulled rather than filled. Order-level detail does exist in the native API through orderUpdates and userFills, but only for <strong>your own account<\/strong>. Liquidations are the same story: userEvents reports them for one address you already know about, and there is no exchange-wide liquidation feed at\u00a0all.<\/p>\n<p>So if your agent\u2019s job is \u201creact to what other people are doing\u201d, the native API cannot feed it. You need someone to have indexed the chain. That is the read path\u00a0below.<\/p>\n<h3>The write\u00a0path<\/h3>\n<p>Start here. It is the part that can lose money, and the part to get familiar with\u00a0first.<\/p>\n<p>The write path is the <a href=\"https:\/\/github.com\/hyperliquid-dex\/hyperliquid-python-sdk\">hyperliquid-python-sdk<\/a>, Hyperliquid\u2019s own\u00a0client.<\/p>\n<p>pip install hyperliquid-python-sdk anthropic eth-account requestsimport os, time<br \/>from eth_account import Account<br \/>from hyperliquid.exchange import Exchange<br \/>from hyperliquid.info import Info<br \/>from hyperliquid.utils import constants<br \/>from hyperliquid.utils.types import CloidBASE_URL = constants.TESTNET_API_URL   # change this last, and deliberatelywallet = Account.from_key(os.environ[&#8220;HL_SECRET_KEY&#8221;])<br \/>address = os.environ[&#8220;HL_ACCOUNT_ADDRESS&#8221;]exchange = Exchange(wallet, BASE_URL, account_address=address)<br \/>info = Info(BASE_URL, skip_ws=True)<\/p>\n<p>The order call is positional and easy to get backwards, so here it is spelled\u00a0out:<\/p>\n<p># exchange.order(name, is_buy, sz, limit_px, order_type, reduce_only=False, cloid=None)<br \/>result = exchange.order(<br \/>    &#8220;ETH&#8221;, True, 0.2, 1100.0,<br \/>    {&#8220;limit&#8221;: {&#8220;tif&#8221;: &#8220;Alo&#8221;}},<br \/>    cloid=Cloid.from_int(1734029481),<br \/>)<\/p>\n<p>Three things in that call matter more than they\u00a0look.<\/p>\n<p>{&#8220;limit&#8221;: {&#8220;tif&#8221;: &#8220;Alo&#8221;}} is post-only. The order is rejected outright if it would cross the spread and take liquidity. For an agent this is the safest default you have, because the worst case of a mispriced quote becomes a rejection instead of a fill at a price you did not intend. Use Gtc when you actually want to rest and cross, Ioc when you want fill-or-kill behaviour.<\/p>\n<p>cloid is your idempotency key, and it is what stops a retry after a network timeout from double-submitting. Derive it from the decision\u00a0itself:<\/p>\n<p>import hashlib<br \/>from hyperliquid.utils.types import Cloiddef decision_cloid(*parts) -&gt; Cloid:<br \/>    &#8220;&#8221;&#8221;Stable 16-byte client order id derived from the decision.&#8221;&#8221;&#8221;<br \/>    key = &#8220;|&#8221;.join(str(p) for p in parts).encode()<br \/>    return Cloid.from_str(&#8220;0x&#8221; + hashlib.sha256(key).hexdigest()[:32])<\/p>\n<p>Do not reach for Python\u2019s built-in hash() for this, which is the mistake I made first. It is salted per process, so the same decision hashes to a different id after every restart, which is the one property an idempotency key cannot have. An agent loop without a stable cloid will place the same order twice sooner or later, and you will find out during a fast\u00a0market.<\/p>\n<p>And reduce_only=True is worth wiring into any tool whose job is to close rather than open. It is a cheap way to stop &#8220;flatten the position&#8221; from opening a new one the other\u00a0way.<\/p>\n<p>Cancels come in both flavours, which is why the cloid pays\u00a0off:<\/p>\n<p>exchange.cancel(&#8220;ETH&#8221;, oid)                 # by exchange order id<br \/>exchange.cancel_by_cloid(&#8220;ETH&#8221;, cloid)      # by your own id<\/p>\n<h3>The read\u00a0path<\/h3>\n<p>The read tools hit an indexed copy of the chain over GraphQL. The technique is the same one I used to <a href=\"https:\/\/medium.com\/coinmonks\/pump-fun-api-how-to-track-bonding-curves-graduations-and-pumpswap-on-chain-879b689fbedb\">track bonding curves and graduations on Pump.fun<\/a>, just pointed at a different chain. The useful property is that the same document works as a query and as a live stream: change query to subscription, drop limit and orderBy, point it at the websocket endpoint, and it\u00a0pushes.<\/p>\n<p>Here is the whole exchange\u2019s fill flow (<a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\/hyperliquid-trades-api\">Trades cube reference<\/a>), which is the feed you would run in a separate process to keep a market picture\u00a0warm:<\/p>\n<p>subscription {<br \/>  Hyperliquid {<br \/>    Trades {<br \/>      Block { Time }<br \/>      Trade {<br \/>        Market { Symbol CoinRaw Kind }<br \/>        Execution { Price Size Side Direction IsAggressor Oid }<br \/>        Fees { Fee FeeToken }<br \/>        Position { Leverage IsCross SizeBefore }<br \/>        Trader { Address }<br \/>      }<br \/>    }<br \/>  }<br \/>}<\/p>\n<p>No coin filter, so one subscription carries every market. A message looks like\u00a0this:<\/p>\n<p>{<br \/>  &#8220;Block&#8221;: { &#8220;Time&#8221;: &#8220;2026-09-04T11:19:51.137023Z&#8221; },<br \/>  &#8220;Trade&#8221;: {<br \/>    &#8220;Market&#8221;: { &#8220;Symbol&#8221;: &#8220;ASTER&#8221;, &#8220;CoinRaw&#8221;: &#8220;ASTER&#8221;, &#8220;Kind&#8221;: &#8220;perp&#8221; },<br \/>    &#8220;Execution&#8221;: {<br \/>      &#8220;Price&#8221;: &#8220;0.75677&#8221;, &#8220;Size&#8221;: &#8220;175.0&#8221;, &#8220;Side&#8221;: &#8220;Sell&#8221;,<br \/>      &#8220;Direction&#8221;: &#8220;Open Short&#8221;, &#8220;IsAggressor&#8221;: true, &#8220;Oid&#8221;: &#8220;535941127746&#8221;<br \/>    },<br \/>    &#8220;Fees&#8221;: { &#8220;Fee&#8221;: &#8220;0.01907&#8221;, &#8220;FeeToken&#8221;: &#8220;USDC&#8221; },<br \/>    &#8220;Position&#8221;: { &#8220;Leverage&#8221;: 5, &#8220;IsCross&#8221;: true, &#8220;SizeBefore&#8221;: &#8220;-175858.0&#8221; },<br \/>    &#8220;Trader&#8221;: { &#8220;Address&#8221;: &#8220;0xa33a4a057334c7811ad5f45f3c4f0dfa3d081ff8&#8221; }<br \/>  }<br \/>}<\/p>\n<p>Two fields there are worth handing to a model. Direction arrives resolved to Open Short, so the agent is not inferring intent from side plus position state. SizeBefore says the wallet was already short 175,858 ASTER before this fill, which is the difference between &#8220;someone sold&#8221; and &#8220;a large short added&#8221;. A negative Fees.Fee is a maker rebate, which is a cheap way to separate passive flow from aggressive.<\/p>\n<p>For book data the cube to know about is <a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\/hyperliquid-orders-api\">BookUpdates<\/a>, which is market-by-order rather than aggregated. One message is one order, carrying its Oid and the Trader.Address that placed it. Oid joins across the schema: the same id appears on Orders as the lifecycle and on Trade.Execution.Oid when it fills, so a single order can be followed end to end. Filter it to one address and you are watching a specific market maker quote and pull in real time (<a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\/hyperliquid-order-flow-by-wallet\">worked examples<\/a>), which is not something a centralised venue will sell you at any\u00a0price.<\/p>\n<h3>Wiring the\u00a0tools<\/h3>\n<p>Claude gets read tools that hit the feed and exactly one write tool that touches the exchange.<\/p>\n<p>import requests<br \/>from anthropic import Anthropic, beta_toolclient = Anthropic()<br \/>BQ_URL = &#8220;https:\/\/streaming.bitquery.io\/graphql&#8221;<br \/>BQ_AUTH = {&#8220;Authorization&#8221;: f&#8221;Bearer {os.environ[&#8216;BITQUERY_TOKEN&#8217;]}&#8221;}<br \/>ALLOWED_MARKETS = {&#8220;BTC&#8221;, &#8220;ETH&#8221;}def bq(query: str, variables: dict) -&gt; dict:<br \/>    r = requests.post(BQ_URL, headers=BQ_AUTH,<br \/>                      json={&#8220;query&#8221;: query, &#8220;variables&#8221;: variables}, timeout=30)<br \/>    r.raise_for_status()<br \/>    payload = r.json()<br \/>    if &#8220;errors&#8221; in payload:<br \/>        raise RuntimeError(payload[&#8220;errors&#8221;][0][&#8220;message&#8221;])<br \/>    return payload[&#8220;data&#8221;][&#8220;Hyperliquid&#8221;]<\/p>\n<p>The liquidation read\u00a0tool:<\/p>\n<p>@beta_tool<br \/>def recent_liquidations(symbol: str, minutes: int = 60) -&gt; str:<br \/>    &#8220;&#8221;&#8221;Count Hyperliquid liquidations on one market over a recent window.    Returns distinct liquidation events, the wallets hit, and the raw fill<br \/>    count. Prefer the liquidation count over the fill count.    Args:<br \/>        symbol: Market symbol. Must be BTC or ETH.<br \/>        minutes: Lookback in minutes, 1 to 60.<br \/>    &#8220;&#8221;&#8221;<br \/>    if symbol not in ALLOWED_MARKETS:<br \/>        return f&#8221;refused: {symbol} is not in the allowlist&#8221;<br \/>    minutes = max(1, min(int(minutes), 60))    query = &#8220;&#8221;&#8221;<br \/>      query ($sym: String!, $mins: Int!) {<br \/>        Hyperliquid {<br \/>          PerpLiquidations(where: {<br \/>            Liquidation: {Market: {Symbol: {is: $sym}}}<br \/>            Block: {Time: {since_relative: {minutes_ago: $mins}}}<br \/>          }) {<br \/>            fills: count<br \/>            liquidations: count(distinct: Liquidation_Execution_Hash)<br \/>            wallets: count(distinct: Liquidation_LiquidatedUser)<br \/>          }<br \/>        }<br \/>      }<br \/>    &#8220;&#8221;&#8221;<br \/>    rows = bq(query, {&#8220;sym&#8221;: symbol, &#8220;mins&#8221;: minutes})[&#8220;PerpLiquidations&#8221;]<br \/>    if not rows:<br \/>        return f&#8221;{symbol}: 0 liquidations in the last {minutes}m&#8221;<br \/>    r = rows[0]<br \/>    return (f&#8221;{symbol}: {r[&#8216;liquidations&#8217;]} liquidations hitting &#8220;<br \/>            f&#8221;{r[&#8216;wallets&#8217;]} wallets in the last {minutes}m &#8220;<br \/>            f&#8221;({r[&#8216;fills&#8217;]} individual fills)&#8221;)<\/p>\n<p>Note the return value is a sentence, not a JSON dump. Tool results are input tokens on every subsequent turn of the loop, and a compact string the model reads correctly beats a nested object it has to parse and might\u00a0misread.<\/p>\n<p>The write tool is where the care\u00a0goes:<\/p>\n<p>MAX_NOTIONAL_USD = 250.0@beta_tool<br \/>def place_post_only_order(symbol: str, is_buy: bool, size: float,<br \/>                          limit_price: float, reason: str) -&gt; str:<br \/>    &#8220;&#8221;&#8221;Place one post-only limit order on Hyperliquid.    Post-only means the exchange rejects the order outright if it would<br \/>    cross the spread. Rejection is normal and expected, not an error.    Args:<br \/>        symbol: Market symbol. Must be BTC or ETH.<br \/>        is_buy: True to bid, False to offer.<br \/>        size: Contracts. Notional is capped server-side by this tool.<br \/>        limit_price: Limit price in USD.<br \/>        reason: One sentence on why, recorded in the audit log.<br \/>    &#8220;&#8221;&#8221;<br \/>    if symbol not in ALLOWED_MARKETS:<br \/>        return f&#8221;refused: {symbol} is not in the allowlist&#8221;<br \/>    notional = size * limit_price<br \/>    if notional &gt; MAX_NOTIONAL_USD:<br \/>        return (f&#8221;refused: ${notional:,.0f} notional exceeds &#8220;<br \/>                f&#8221;the ${MAX_NOTIONAL_USD:,.0f} cap&#8221;)    cloid = decision_cloid(symbol, is_buy, round(limit_price, 2),<br \/>                           int(time.time() \/\/ 60))<br \/>    audit.write(symbol, is_buy, size, limit_price, reason, str(cloid))    result = exchange.order(symbol, is_buy, size, limit_price,<br \/>                            {&#8220;limit&#8221;: {&#8220;tif&#8221;: &#8220;Alo&#8221;}}, cloid=cloid)<br \/>    if result.get(&#8220;status&#8221;) != &#8220;ok&#8221;:<br \/>        return f&#8221;exchange rejected the request: {result}&#8221;    status = result[&#8220;response&#8221;][&#8220;data&#8221;][&#8220;statuses&#8221;][0]<br \/>    if &#8220;resting&#8221; in status:<br \/>        return f&#8221;resting on the book, oid {status[&#8216;resting&#8217;][&#8216;oid&#8217;]}&#8221;<br \/>    if &#8220;filled&#8221; in status:<br \/>        return f&#8221;filled immediately: {status[&#8216;filled&#8217;]}&#8221;<br \/>    return f&#8221;not resting, no fill: {status}&#8221;<\/p>\n<p>Two decisions in there carry the\u00a0weight.<\/p>\n<p>The allowlist and the notional cap are Python, not prompt text. A model asked politely to stay under a cap will stay under it nearly every time, and nearly every time is not a risk control. Anything you would be unhappy to see violated once belongs in an if that runs before the order\u00a0does.<\/p>\n<p>And the tool reports back which of three things happened: resting, filled, or neither. That distinction is not cosmetic, for a reason the next section gets\u00a0to.<\/p>\n<p>The reason argument is doing quiet work too. Requiring the model to state why, in the same call that places the order, gives you an audit log that explains itself six weeks later, and it costs one extra\u00a0field.<\/p>\n<h3>The loop<\/h3>\n<p>You do not have to write the agent loop. The SDK\u2019s <a href=\"https:\/\/docs.claude.com\/en\/docs\/agents-and-tools\/tool-use\/overview\">tool runner<\/a> drives the call, execute and continue\u00a0cycle:<\/p>\n<p>DESK_RULES = &#8220;&#8221;&#8221;You watch two Hyperliquid perp markets and quote passively.Doing nothing is a valid and common answer, and most runs should end that way.<br \/>Never chase price. Place at most one order per run.<br \/>A post-only rejection means your price crossed the spread. Do not resubmit it<br \/>at a crossing price; either move the price passive or stand down.<br \/>Liquidation counts are events, not fills. Do not treat a fill count as activity.&#8221;&#8221;&#8221;runner = client.beta.messages.tool_runner(<br \/>    model=&#8221;claude-opus-5&#8243;,<br \/>    max_tokens=16000,<br \/>    thinking={&#8220;type&#8221;: &#8220;adaptive&#8221;},<br \/>    output_config={&#8220;effort&#8221;: &#8220;high&#8221;},<br \/>    system=[{<br \/>        &#8220;type&#8221;: &#8220;text&#8221;,<br \/>        &#8220;text&#8221;: DESK_RULES,<br \/>        &#8220;cache_control&#8221;: {&#8220;type&#8221;: &#8220;ephemeral&#8221;},<br \/>    }],<br \/>    tools=[recent_liquidations, open_position, place_post_only_order],<br \/>    messages=[{&#8220;role&#8221;: &#8220;user&#8221;, &#8220;content&#8221;:<br \/>        &#8220;Check BTC. If liquidations are elevated versus a normal hour, consider &#8220;<br \/>        &#8220;quoting passively on the side that just got run over. Otherwise do nothing.&#8221;<br \/>    }],<br \/>)for message in runner:<br \/>    log(message)<\/p>\n<p>thinking={&#8220;type&#8221;: &#8220;adaptive&#8221;} lets the model decide how much reasoning a given run deserves, which matters when most runs should end in &#8220;nothing to do here&#8221;. The cache_control block matters because the rules and tool schemas get resent every turn, and cached reads bill at roughly a tenth of the input\u00a0rate.<\/p>\n<p>Rough cost. Claude Opus 5 is $5 per million input tokens and $25 per million output. A run that reads about 2,000 input tokens and writes about 1,500 comes to roughly five cents. On a five-minute cadence that is 288 runs a day and roughly thirteen dollars, before caching brings the input side down. That number is worth computing for your own cadence before you leave anything running, because the cost of an agent that thinks every minute is not obvious until the invoice\u00a0arrives.<\/p>\n<h3>Three ways the data will lie to your\u00a0agent<\/h3>\n<p>Every one of these cost me a wrong number before I caught it, and each one produces a <em>plausible<\/em> wrong answer rather than an error, which is the dangerous kind.<\/p>\n<h4>It thinks one liquidation is\u00a0sixteen<\/h4>\n<p>Counting rows on the <a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\/hyperliquid-perpetuals-api\">liquidation feed<\/a> overstates activity, badly. In one recent\u00a0hour:<\/p>\n<p>fills:        127<br \/>liquidations:  33<br \/>wallets:       33<br \/>markets:       11<\/p>\n<p>A single XPL position unwind produced 16 rows, all in one block, all sharing one execution hash:<\/p>\n<p>11:27:28.537  Buy  size=  5010.0  px=0.10143<br \/>11:27:28.537  Buy  size=   490.0  px=0.10142<br \/>11:27:28.537  Buy  size= 11059.0  px=0.10149<br \/>11:27:28.537  Buy  size= 28173.0  px=0.10160<br \/>&#8230;  (12 more)<\/p>\n<p>One forced unwind ate 16 resting orders at 16 prices, and the feed gives you one row per fill because that is what happened on chain. An agent told \u201c127 liquidations\u201d when the real number is 33 will read a calm hour as a cascade and quote into\u00a0it.<\/p>\n<p>Count distinct execution hashes:<\/p>\n<p>fills:        count<br \/>liquidations: count(distinct: Liquidation_Execution_Hash)<br \/>wallets:      count(distinct: Liquidation_LiquidatedUser)<\/p>\n<p>Fix it at the tool boundary where you can see it. A model handed a number labelled count will reason confidently about the wrong quantity and will not flag that it is confused.<\/p>\n<h4>It thinks its quote is resting when it was\u00a0rejected<\/h4>\n<p>Count BTC order events by status over ten minutes and the shape is startling:<\/p>\n<p>badAloPxRejected           1,848,618   83.6%<br \/>open                         150,206    6.8%<br \/>canceled                     131,269    5.9%<br \/>perpMarginRejected            43,063    1.9%<br \/>iocCancelRejected             20,579    0.9%<br \/>tooManyOpenOrdersRejected     14,775    0.7%<br \/>filled                         1,608    0.1%<br \/>TOTAL                      2,210,732<\/p>\n<p>Eighty-four percent of everything that happens to a BTC order is badAloPxRejected, and one tenth of one percent is a fill. Checking what those rejected orders were, every one is a post-only limit order, split near evenly between buys and\u00a0sells:<\/p>\n<p>Limit  Buy   Tif=Alo   478,047<br \/>Limit  Sell  Tif=Alo   431,349<\/p>\n<p>That is the quoting race on the most liquid market on the exchange: market makers trying to post at the touch, losing, and getting bounced. Two million of those in ten minutes. ETH is the same shape, 78.6% rejected and 0.06%\u00a0filled.<\/p>\n<p>Your agent is posting Alo orders into exactly that. Rejection is the <em>normal<\/em> outcome, not the exception, which is why the write tool above distinguishes resting from filled from neither. An agent that assumes its quote is live when the matching engine bounced it will keep reasoning about a position it does not have, and will hedge or size against a\u00a0phantom.<\/p>\n<p>It also breaks any activity metric you build. If you compute a cancel-to-fill ratio from a bare event count, 84% of your denominator on BTC never reached the\u00a0book.<\/p>\n<h3>It trades the wrong\u00a0BTC<\/h3>\n<p>HIP-3 lets outside builders deploy their own perp markets on Hyperliquid, under a namespace prefix, trading in the same infrastructure. A lot of them are tokenized equities, which is the same land grab <a href=\"https:\/\/medium.com\/coinmonks\/arcus-review-dydx-stock-token-perps-dex-0675570be68b\">Arcus is running at the dYdX team<\/a>. There are currently 279 live across 10 deployers, the largest being xyz with 119 markets, then para with 33 and hyna with\u00a025.<\/p>\n<p>Query <a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\/hyperliquid-prices-api\">mark prices<\/a> filtered to the symbol\u00a0BTC:<\/p>\n<p>flx:BTC     91470.2<br \/>hyna:BTC    76888.0<br \/>cash:BTC    70000.0<\/p>\n<p>Three builders, three markets called BTC, three prices more than twenty thousand dollars apart, each on its own oracle. If your ingestion keys on Symbol, an agent can read a price from one market and send an order to another. Key on CoinRaw, which carries the full namespace:symbol identifier.<\/p>\n<p>No data provider invented this. It falls out of permissionless market listing, and it will bite anyone who assumes symbols are\u00a0unique.<\/p>\n<h3>State between\u00a0runs<\/h3>\n<p>An agent that only reads the market and never reads itself will drift. Two things need reconciling at the top of every\u00a0run.<\/p>\n<p>The real position, from the native API rather than from\u00a0memory:<\/p>\n<p>@beta_tool<br \/>def open_position(symbol: str) -&gt; str:<br \/>    &#8220;&#8221;&#8221;Report the agent&#8217;s actual open position on one market.    Args:<br \/>        symbol: Market symbol. Must be BTC or ETH.<br \/>    &#8220;&#8221;&#8221;<br \/>    state = info.user_state(address)<br \/>    for entry in state[&#8220;assetPositions&#8221;]:<br \/>        p = entry[&#8220;position&#8221;]<br \/>        if p[&#8220;coin&#8221;] == symbol:<br \/>            return (f&#8221;{symbol}: size {p[&#8216;szi&#8217;]}, entry {p.get(&#8216;entryPx&#8217;)}, &#8220;<br \/>                    f&#8221;unrealized {p[&#8216;unrealizedPnl&#8217;]}&#8221;)<br \/>    return f&#8221;{symbol}: flat&#8221;<\/p>\n<p>And the resting orders, so the agent does not stack five quotes across five runs because each run forgot the last. info.open_orders(address) covers this, and a cheap policy that works well is to cancel everything the agent placed at the start of a run and requote from a clean\u00a0book.<\/p>\n<p>Feed both in as tools rather than as prompt text. The model then reads current state at the moment it needs it, instead of trusting a snapshot you pasted in at the top of the turn that may already be\u00a0stale.<\/p>\n<h3>Running it without losing\u00a0money<\/h3>\n<p>constants.TESTNET_API_URL is not decoration. Moving off it should be a separate, deliberate commit made after the thing has run for a couple of weeks and surprised you at least\u00a0once.<\/p>\n<p>Some specifics that are worth more than a paragraph of general\u00a0caution.<\/p>\n<p><strong>Expect it to do nothing.<\/strong> Exchange-wide, Hyperliquid liquidates in the low tens of positions an hour, and BTC alone can go four hours without a single one. An agent gated on BTC liquidations will correctly sit still on most runs. That is the right way round to test it: watch it decline to act on a quiet market before you point it at a busy\u00a0one.<\/p>\n<p><strong>Keep the kill switch outside the process.<\/strong> A supervisor you can kill -9, or an exchange-side cancel-all you can fire by hand, beats any instruction in a system prompt. The system prompt is guidance. The process boundary is a guarantee.<\/p>\n<p><strong>Log the tool calls, not just the outcome.<\/strong> An agent that placed a strange order is only debuggable if you can replay what it saw when it decided. Arguments and results, every call, including the refusals from your own guardrails, since a spike in refusals is the earliest signal that the reasoning has gone somewhere odd.<\/p>\n<p><strong>Cap what one run can do, not just one order.<\/strong> The notional cap above limits a single order. A run that places one order twenty times is still within that cap and nowhere near\u00a0safe.<\/p>\n<p>Where this approach is weaker than the alternatives, plainly. The indexed feed sits behind the matching engine by an indexing step, so anything reacting in single-digit milliseconds belongs on the native websocket instead. The GraphQL window is a rolling thirty days or so, which covers live trading and recent-history checks but not a multi-year backtest. And the highest-volume cubes, Orders and BookUpdates, run to hundreds of millions of rows a day on a busy market, so filtered scans over long windows time out; keep interactive windows to an hour and accumulate anything longer in your own\u00a0store.<\/p>\n<h3>What this is and is\u00a0not<\/h3>\n<p>This is plumbing. Nothing above tells you what to trade or suggests you should, and a language model wired to a market data feed is not an edge. It is a way to act on one you already have, and equally a way to act on a bad idea faster than you could by\u00a0hand.<\/p>\n<p>What Hyperliquid genuinely changes is the input. On a centralised venue your agent reasons about price and its own fills, because that is all the exchange will sell you. Here it can reason about who is positioned where, which quotes are real, and who just got carried out, because the book is on a public chain and the wallet is attached to every\u00a0order.<\/p>\n<p>The reasoning layer is the easy part now. Getting clean, correctly counted market state into it is the work, and three of the traps are\u00a0above.<\/p>\n<p><em>Docs for the read-path queries: <\/em><a href=\"https:\/\/docs.bitquery.io\/docs\/perpetuals\/hyperliquid\"><em>Hyperliquid API on Bitquery<\/em><\/a><em>. The native API and SDK: <\/em><a href=\"https:\/\/hyperliquid.gitbook.io\/hyperliquid-docs\/for-developers\/api\"><em>hyperliquid.gitbook.io<\/em><\/a><em>. Every figure was pulled live on 4 September 2026 and will have moved by the time you read\u00a0this.<\/em><\/p>\n<p><em>Disclosure: I work on developer content at Bitquery, which sells the indexed feed used for the read path. The write path is Hyperliquid\u2019s own free SDK, and the sections on latency, history depth and query limits are there because they are real constraints.<\/em><\/p>\n<p><a href=\"https:\/\/medium.com\/coinmonks\/building-an-ai-crypto-trading-bot-on-hyperliquid-39a91550b640\">Building an AI Crypto Trading Bot on Hyperliquid<\/a> was originally published in <a href=\"https:\/\/medium.com\/coinmonks\">Coinmonks<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>","protected":false},"excerpt":{"rendered":"<p>Claude decides, the Hyperliquid SDK executes, an indexed feed supplies the market. Plus the three ways the data will quietly lie to your agent, all of which I\u00a0hit. An agent that trades your own account is easy. Fifty lines, one SDK,\u00a0done. An agent that trades your account based on what the rest of the market [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":223285,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-223284","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-interesting"],"_links":{"self":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/223284"}],"collection":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=223284"}],"version-history":[{"count":0,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/223284\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/media\/223285"}],"wp:attachment":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=223284"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=223284"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=223284"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}