Reading view

There are new articles available, click to refresh the page.

Can an AI Agent Actually Move Bitcoin?

An agent can plan the move, price it, and build the transaction, but on almost every tool available today, it cannot sign it, and the two largest agent-facing bridge servers do not carry Bitcoin at all. The limit is not routing. Routing is solved. The limit is that moving Bitcoin requires a key, and handing a key to an autonomous process is a decision nobody has found a safe default for.

This piece covers why the key is the constraint, what breaks on Bitcoin specifically, the working pattern people actually use, and the three providers whose tools an agent can call today.

Why can’t an AI agent just hold Bitcoin?

An agent has no way to hold Bitcoin that is meaningfully different from a script holding a private key, and that is the whole problem. A wallet is a keypair. Whatever holds the key controls the coins. There is no permission layer inside Bitcoin that lets you grant an agent the right to move 0.01 BTC on Tuesdays and nothing else.

Compare that with what an agent gets on an EVM chain. There, a token approval is a bounded grant — a spending cap on one contract for one token, revocable. Account abstraction goes further: session keys, spending limits, allowlisted destinations, a co-signer. None of that exists on Bitcoin. The key signs everything or it signs nothing.

So “agent holds Bitcoin” collapses into “some process on a machine has the seed.” That is a custody decision dressed up as an architecture decision, and it is why the serious tools decline to make it for you.

What breaks when an agent tries to move Bitcoin specifically?

Four things break, and they are all properties of Bitcoin rather than shortcomings of the agent.

There is no contract to approve against. Bitcoin Script can check a signature, enforce a timelock, and verify a hash preimage. It cannot hold the persistent state an allowance needs. The entire approve-then-spend pattern that agent tooling is built around has no Bitcoin equivalent.

Balances are UTXOs, not numbers. Spending requires selecting inputs, calculating change, and building a transaction. Send 0.05 BTC from a 0.4 BTC input, and the remainder comes back as change to an address you must control. Get that wrong, and the difference goes to miners.

Blocks are ten minutes. An agent loop expects a result in seconds. A Bitcoin confirmation is minutes, and most destinations want several. Anything holding a quote across that window is exposed to the price moving.

There is no native dollar. The reason to move Bitcoin at all is usually to reach something priced in dollars — a lending market, a perp venue, a payment. Bitcoin has no stablecoin, so the move and the conversion are the same action.

So what does the working pattern actually look like?

One supervised conversion, then the agent operates on the other side. Rather than giving an agent Bitcoin, you move the Bitcoin once — with a human signing — into an asset on a chain where bounded permissions exist. USDC on Base, USDC on Arbitrum, a wrapped BTC token if you want to keep the price exposure. From that point, the agent works inside an approval you set and can revoke.

This is a narrower claim than “agentic Bitcoin,” and it is the one that survives contact with the tooling. The agent’s job is research, routing, quoting, monitoring and execution on the destination side. The Bitcoin leg stays manual, once, at the start.

It is also where the demand is. Spark’s BTCFi research, published 29 May 2026, put Bitcoin’s entire DeFi footprint at 91,332 BTC — about 0.46% of circulating supply. The gap is a plumbing problem, and the plumbing is exactly what an agent is good at, right up to the signature.

Which providers let an AI agent move Bitcoin?

Three are worth knowing. Two of them are bigger than the third and neither touches native Bitcoin.

LI.FI - the widest coverage, explicitly read-only

LI.FI’s MCP server is the broadest route surface an agent can query, and it is deliberately incapable of moving anything. It exposes around fifteen tools across token and chain metadata, quotes, routing, gas, allowances, and transaction status, covering 58 chains, 27-plus bridges and 31-plus DEXes, and works with Claude, Cursor, Windsurf and VS Code Copilot.

The docs are blunt about the boundary: the server “provides read-only tools” and “does not sign or broadcast transactions.” A quote comes back with an unsigned transactionRequest that you sign and submit with your own wallet. Rate limits are 200 requests per two hours without an API key, 200 per minute with one.

The honest part: Bitcoin is not in the chain list. LI.FI is an EVM-and-Solana aggregator, and for an agent that already holds tokens on those chains it is the best research tool of the three. It is not a Bitcoin tool.

deBridge - execution-shaped, with the signature handed back to you

deBridge shipped its MCP server in February 2026 and designed it around handing the final step to a browser. The Block covered the launch on 16 February 2026. The server exposes six tools — get_instructions, search_tokens, get_supported_chains, create_tx, estimate_same_chain_swap and get_trade_dapp_url — across 24 to 28 networks and more than 40,000 tokens, with a hosted endpoint at agents.debridge.com so an agent can connect without installing anything.

The last tool is the interesting one. The agent produces a shareable deBridge App link, and the documentation describes the boundary plainly: the server “never touches private keys or signs transactions. The user always completes the swap themselves: open the generated link, connect their wallet, review, and sign.”

The honest part: same gap. Bitcoin is not a supported native chain — the coverage is EVM plus Solana. deBridge is the better of the two if you want an agent that gets you to a one-click confirmation, and it still starts from tokens you already hold off Bitcoin.

Garden - the one with the Bitcoin chain in it

Garden Finance is a Bitcoin-first cross-chain protocol, which makes it the only one of the three whose route set starts where Bitcoin actually is. Its catalogue on 8 September 2026 listed 26 assets across 15 chains, including Lightning, Solana, Starknet and Hyperliquid, 13 of those entries a form of Bitcoin across seven tickers. Settlement is by atomic swap: the swap completes on both sides or refunds on both, so there is no pooled balance for an agent to be wrong about.

It reaches agents through two formats, and the difference decides where the tool is reachable rather than what it can do. MCP is portable. The protocol is spoken by Claude Code, Cursor, Codex, Windsurf and VS Code alike, so one server covers every environment an agent might run in. A skill file is not. A .md skill is native to Claude and does nothing in Cursor or Copilot. Garden ships both: an MCP server for swaps, a Claude skill that drives the same flow conversationally, and a second Claude skill for developers embedding Garden into their own application.

Underneath either one the API is the same, and it is already agent-shaped: quote, create order, execute HTLC action, instant refund hash, available liquidity. A refund that is a script condition rather than a support ticket matters more when a machine is driving, and quotes are whole — Garden states it charges no fee of its own and that the quoted figure includes network costs, so an agent does not have to model fee layers separately to know what it will receive. In a nine-swap cost snapshot on 20 August 2026, Garden quoted lowest on all nine against Chainflip and Relay, the gap widest on $100 swaps.

How do the three compare?

Who actually signs, and why does every tool stop there?

All three stop at the signature, and they stop at the same place for the same reason: an agent that can sign is an agent that can lose everything on a bad inference. A hallucinated address, a decimal misread, a prompt-injected instruction inside a token name or a web page the agent read on the way — each of those is survivable when a human confirms and unrecoverable when nothing does.

This is not a Bitcoin-specific caution. Base shipped its MCP in May 2026 behind OAuth with user approval on every transaction. Bybit and BitGo both shipped agent servers in 2026 with the same shape. The pattern across the whole category is identical: agents propose, humans dispose.

On Bitcoin, the stakes of crossing that line are higher, because there is no revoke. An EVM approval can be pulled after the fact. A Bitcoin transaction cannot.

What should you check before letting an agent near your Bitcoin?

Keep the key out of the agent’s context. A key pasted into a chat is in the transcript, the logs and possibly a provider’s retention window. Anything holding a key should read it from an OS keystore at the moment of use.

Confirm the destination address out of band. The single highest-value target for prompt injection against a swap agent is the destination field.

Have gas on the destination chain. Arriving with a Bitcoin-denominated token and no ETH, SOL or STRK is the most common way a first attempt stalls, agent or not.

Size to the route, not to your balance. Caps exist on every route, and inside them the far side’s depth sets your slippage.

Assume the conversion is taxable. In most jurisdictions giving up BTC for a token is a disposal, and coming back is a second one. An agent running conversions on a schedule generates a lot of those.

FAQ

Can AI agents hold crypto?
They can hold a key, which is not the same as being trusted with one. On EVM chains bounded permissions exist — token approvals, session keys, spending caps — so an agent can be given limited authority. Bitcoin has no equivalent, so authority there is all or nothing.

Can Claude execute a crypto swap?
Claude can call an MCP server to search tokens, price a route and build an unsigned transaction. On LI.FI and deBridge the signature is handed back to a wallet you control. That handoff is deliberate, not a missing feature.

Does deBridge support Bitcoin?
Not as a native chain. Its MCP covers EVM networks and Solana. You can move a wrapped Bitcoin token across those chains; you cannot start from BTC on the Bitcoin chain.

Does LI.FI support Bitcoin?
Bitcoin is not in its 58-chain list either. LI.FI aggregates across EVM chains and Solana.

What is the safest way to give an agent access to funds?
Move a fixed amount to a dedicated wallet, on a chain where approvals are bounded and revocable, and grant nothing from your main holdings. Treat it as a float rather than an account.

Can an agent move Bitcoin without any human step at all?
Only if something in the loop holds the key and signs unattended, which every provider named here declines to do. If a tool offers it, that is the question to ask about it first.

Is a bridge cheaper than an exchange for this?
Often not, for common pairs. An exchange usually wins on BTC to USDC. Bridges win when the destination is a rollup that exchanges do not support as a withdrawal network, which is most of them.


Can an AI Agent Actually Move Bitcoin? was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

Building an AI Crypto Trading Bot on Hyperliquid

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 hit.

An agent that trades your own account is easy. Fifty lines, one SDK, done.

An agent that trades your account based on what the rest of the market is doing 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 need.

Hyperliquid is the exception, and it is the reason to build this here rather than on Binance or on any of the perp venues competing with it. 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 chain.

Getting at it takes more work than a websocket subscribe message. Here is the whole build.

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’s own free SDK, and I will be specific about where the free native API is the better choice.

The architecture: three paths, three tools

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 answers.

PathWhat it doesWhat serves it bestWritePlace, cancel and modify your own ordersHyperliquid’s native SDK. Closest to the matching engine, free, canonical.Read (own account)Your positions, fills, marginHyperliquid’s 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 tools

That third row is the one people get wrong, so it is worth being precise about why.

Hyperliquid’s public websocket 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 your own account. Liquidations are the same story: userEvents reports them for one address you already know about, and there is no exchange-wide liquidation feed at all.

So if your agent’s job is “react to what other people are doing”, the native API cannot feed it. You need someone to have indexed the chain. That is the read path below.

The write path

Start here. It is the part that can lose money, and the part to get familiar with first.

The write path is the hyperliquid-python-sdk, Hyperliquid’s own client.

pip install hyperliquid-python-sdk anthropic eth-account requests
import os, time
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
from hyperliquid.utils.types import Cloid
BASE_URL = constants.TESTNET_API_URL   # change this last, and deliberately
wallet = Account.from_key(os.environ["HL_SECRET_KEY"])
address = os.environ["HL_ACCOUNT_ADDRESS"]
exchange = Exchange(wallet, BASE_URL, account_address=address)
info = Info(BASE_URL, skip_ws=True)

The order call is positional and easy to get backwards, so here it is spelled out:

# exchange.order(name, is_buy, sz, limit_px, order_type, reduce_only=False, cloid=None)
result = exchange.order(
"ETH", True, 0.2, 1100.0,
{"limit": {"tif": "Alo"}},
cloid=Cloid.from_int(1734029481),
)

Three things in that call matter more than they look.

{"limit": {"tif": "Alo"}} 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.

cloid is your idempotency key, and it is what stops a retry after a network timeout from double-submitting. Derive it from the decision itself:

import hashlib
from hyperliquid.utils.types import Cloid
def decision_cloid(*parts) -> Cloid:
"""Stable 16-byte client order id derived from the decision."""
key = "|".join(str(p) for p in parts).encode()
return Cloid.from_str("0x" + hashlib.sha256(key).hexdigest()[:32])

Do not reach for Python’s 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 market.

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 "flatten the position" from opening a new one the other way.

Cancels come in both flavours, which is why the cloid pays off:

exchange.cancel("ETH", oid)                 # by exchange order id
exchange.cancel_by_cloid("ETH", cloid) # by your own id

The read path

The read tools hit an indexed copy of the chain over GraphQL. The technique is the same one I used to track bonding curves and graduations on Pump.fun, 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 pushes.

Here is the whole exchange’s fill flow (Trades cube reference), which is the feed you would run in a separate process to keep a market picture warm:

subscription {
Hyperliquid {
Trades {
Block { Time }
Trade {
Market { Symbol CoinRaw Kind }
Execution { Price Size Side Direction IsAggressor Oid }
Fees { Fee FeeToken }
Position { Leverage IsCross SizeBefore }
Trader { Address }
}
}
}
}

No coin filter, so one subscription carries every market. A message looks like this:

{
"Block": { "Time": "2026-09-04T11:19:51.137023Z" },
"Trade": {
"Market": { "Symbol": "ASTER", "CoinRaw": "ASTER", "Kind": "perp" },
"Execution": {
"Price": "0.75677", "Size": "175.0", "Side": "Sell",
"Direction": "Open Short", "IsAggressor": true, "Oid": "535941127746"
},
"Fees": { "Fee": "0.01907", "FeeToken": "USDC" },
"Position": { "Leverage": 5, "IsCross": true, "SizeBefore": "-175858.0" },
"Trader": { "Address": "0xa33a4a057334c7811ad5f45f3c4f0dfa3d081ff8" }
}
}

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 "someone sold" and "a large short added". A negative Fees.Fee is a maker rebate, which is a cheap way to separate passive flow from aggressive.

For book data the cube to know about is BookUpdates, 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 (worked examples), which is not something a centralised venue will sell you at any price.

Wiring the tools

Claude gets read tools that hit the feed and exactly one write tool that touches the exchange.

import requests
from anthropic import Anthropic, beta_tool
client = Anthropic()
BQ_URL = "https://streaming.bitquery.io/graphql"
BQ_AUTH = {"Authorization": f"Bearer {os.environ['BITQUERY_TOKEN']}"}
ALLOWED_MARKETS = {"BTC", "ETH"}
def bq(query: str, variables: dict) -> dict:
r = requests.post(BQ_URL, headers=BQ_AUTH,
json={"query": query, "variables": variables}, timeout=30)
r.raise_for_status()
payload = r.json()
if "errors" in payload:
raise RuntimeError(payload["errors"][0]["message"])
return payload["data"]["Hyperliquid"]

The liquidation read tool:

@beta_tool
def recent_liquidations(symbol: str, minutes: int = 60) -> str:
"""Count Hyperliquid liquidations on one market over a recent window.
    Returns distinct liquidation events, the wallets hit, and the raw fill
count. Prefer the liquidation count over the fill count.
    Args:
symbol: Market symbol. Must be BTC or ETH.
minutes: Lookback in minutes, 1 to 60.
"""
if symbol not in ALLOWED_MARKETS:
return f"refused: {symbol} is not in the allowlist"
minutes = max(1, min(int(minutes), 60))
    query = """
query ($sym: String!, $mins: Int!) {
Hyperliquid {
PerpLiquidations(where: {
Liquidation: {Market: {Symbol: {is: $sym}}}
Block: {Time: {since_relative: {minutes_ago: $mins}}}
}) {
fills: count
liquidations: count(distinct: Liquidation_Execution_Hash)
wallets: count(distinct: Liquidation_LiquidatedUser)
}
}
}
"""
rows = bq(query, {"sym": symbol, "mins": minutes})["PerpLiquidations"]
if not rows:
return f"{symbol}: 0 liquidations in the last {minutes}m"
r = rows[0]
return (f"{symbol}: {r['liquidations']} liquidations hitting "
f"{r['wallets']} wallets in the last {minutes}m "
f"({r['fills']} individual fills)")

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 misread.

The write tool is where the care goes:

MAX_NOTIONAL_USD = 250.0
@beta_tool
def place_post_only_order(symbol: str, is_buy: bool, size: float,
limit_price: float, reason: str) -> str:
"""Place one post-only limit order on Hyperliquid.
    Post-only means the exchange rejects the order outright if it would
cross the spread. Rejection is normal and expected, not an error.
    Args:
symbol: Market symbol. Must be BTC or ETH.
is_buy: True to bid, False to offer.
size: Contracts. Notional is capped server-side by this tool.
limit_price: Limit price in USD.
reason: One sentence on why, recorded in the audit log.
"""
if symbol not in ALLOWED_MARKETS:
return f"refused: {symbol} is not in the allowlist"
notional = size * limit_price
if notional > MAX_NOTIONAL_USD:
return (f"refused: ${notional:,.0f} notional exceeds "
f"the ${MAX_NOTIONAL_USD:,.0f} cap")
    cloid = decision_cloid(symbol, is_buy, round(limit_price, 2),
int(time.time() // 60))
audit.write(symbol, is_buy, size, limit_price, reason, str(cloid))
    result = exchange.order(symbol, is_buy, size, limit_price,
{"limit": {"tif": "Alo"}}, cloid=cloid)
if result.get("status") != "ok":
return f"exchange rejected the request: {result}"
    status = result["response"]["data"]["statuses"][0]
if "resting" in status:
return f"resting on the book, oid {status['resting']['oid']}"
if "filled" in status:
return f"filled immediately: {status['filled']}"
return f"not resting, no fill: {status}"

Two decisions in there carry the weight.

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 does.

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 to.

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 field.

The loop

You do not have to write the agent loop. The SDK’s tool runner drives the call, execute and continue cycle:

DESK_RULES = """You watch two Hyperliquid perp markets and quote passively.
Doing nothing is a valid and common answer, and most runs should end that way.
Never chase price. Place at most one order per run.
A post-only rejection means your price crossed the spread. Do not resubmit it
at a crossing price; either move the price passive or stand down.
Liquidation counts are events, not fills. Do not treat a fill count as activity."""
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "adaptive"},
output_config={"effort": "high"},
system=[{
"type": "text",
"text": DESK_RULES,
"cache_control": {"type": "ephemeral"},
}],
tools=[recent_liquidations, open_position, place_post_only_order],
messages=[{"role": "user", "content":
"Check BTC. If liquidations are elevated versus a normal hour, consider "
"quoting passively on the side that just got run over. Otherwise do nothing."
}],
)
for message in runner:
log(message)

thinking={"type": "adaptive"} lets the model decide how much reasoning a given run deserves, which matters when most runs should end in "nothing to do here". 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 rate.

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 arrives.

Three ways the data will lie to your agent

Every one of these cost me a wrong number before I caught it, and each one produces a plausible wrong answer rather than an error, which is the dangerous kind.

It thinks one liquidation is sixteen

Counting rows on the liquidation feed overstates activity, badly. In one recent hour:

fills:        127
liquidations: 33
wallets: 33
markets: 11

A single XPL position unwind produced 16 rows, all in one block, all sharing one execution hash:

11:27:28.537  Buy  size=  5010.0  px=0.10143
11:27:28.537 Buy size= 490.0 px=0.10142
11:27:28.537 Buy size= 11059.0 px=0.10149
11:27:28.537 Buy size= 28173.0 px=0.10160
... (12 more)

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 “127 liquidations” when the real number is 33 will read a calm hour as a cascade and quote into it.

Count distinct execution hashes:

fills:        count
liquidations: count(distinct: Liquidation_Execution_Hash)
wallets: count(distinct: Liquidation_LiquidatedUser)

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.

It thinks its quote is resting when it was rejected

Count BTC order events by status over ten minutes and the shape is startling:

badAloPxRejected           1,848,618   83.6%
open 150,206 6.8%
canceled 131,269 5.9%
perpMarginRejected 43,063 1.9%
iocCancelRejected 20,579 0.9%
tooManyOpenOrdersRejected 14,775 0.7%
filled 1,608 0.1%
TOTAL 2,210,732

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 sells:

Limit  Buy   Tif=Alo   478,047
Limit Sell Tif=Alo 431,349

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% filled.

Your agent is posting Alo orders into exactly that. Rejection is the normal 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 phantom.

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 book.

It trades the wrong BTC

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 Arcus is running at the dYdX team. There are currently 279 live across 10 deployers, the largest being xyz with 119 markets, then para with 33 and hyna with 25.

Query mark prices filtered to the symbol BTC:

flx:BTC     91470.2
hyna:BTC 76888.0
cash:BTC 70000.0

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.

No data provider invented this. It falls out of permissionless market listing, and it will bite anyone who assumes symbols are unique.

State between runs

An agent that only reads the market and never reads itself will drift. Two things need reconciling at the top of every run.

The real position, from the native API rather than from memory:

@beta_tool
def open_position(symbol: str) -> str:
"""Report the agent's actual open position on one market.
    Args:
symbol: Market symbol. Must be BTC or ETH.
"""
state = info.user_state(address)
for entry in state["assetPositions"]:
p = entry["position"]
if p["coin"] == symbol:
return (f"{symbol}: size {p['szi']}, entry {p.get('entryPx')}, "
f"unrealized {p['unrealizedPnl']}")
return f"{symbol}: flat"

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 book.

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 stale.

Running it without losing money

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 once.

Some specifics that are worth more than a paragraph of general caution.

Expect it to do nothing. 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 one.

Keep the kill switch outside the process. 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.

Log the tool calls, not just the outcome. 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.

Cap what one run can do, not just one order. The notional cap above limits a single order. A run that places one order twenty times is still within that cap and nowhere near safe.

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 store.

What this is and is not

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 hand.

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 order.

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 above.

Docs for the read-path queries: Hyperliquid API on Bitquery. The native API and SDK: hyperliquid.gitbook.io. Every figure was pulled live on 4 September 2026 and will have moved by the time you read this.

Disclosure: I work on developer content at Bitquery, which sells the indexed feed used for the read path. The write path is Hyperliquid’s own free SDK, and the sections on latency, history depth and query limits are there because they are real constraints.


Building an AI Crypto Trading Bot on Hyperliquid was originally published in Coinmonks on Medium, where people are continuing the conversation by highlighting and responding to this story.

❌