<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Hyperliquid Journey]]></title><description><![CDATA[I write about my journey on Hyperliquid, building and hosting communities.]]></description><link>https://realjojo.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ab22629e2ebedb9e891b94a/66b24260-1e38-4b88-baa7-0c30e038db2a.jpg</url><title>Hyperliquid Journey</title><link>https://realjojo.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 12:12:30 GMT</lastBuildDate><atom:link href="https://realjojo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Moving a Hyperliquid Bot from Testnet to Mainnet: What Breaks]]></title><description><![CDATA[What breaks when a Hyperliquid bot moves from testnet to mainnet: asset IDs, prices, order errors, and a checklist for the switch.]]></description><link>https://realjojo.hashnode.dev/moving-a-hyperliquid-bot-from-testnet-to-mainnet-what-breaks</link><guid isPermaLink="true">https://realjojo.hashnode.dev/moving-a-hyperliquid-bot-from-testnet-to-mainnet-what-breaks</guid><category><![CDATA[hyperliquid]]></category><category><![CDATA[Python]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Joshua Yap]]></dc:creator><pubDate>Fri, 23 Oct 2026 07:50:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ab22629e2ebedb9e891b94a/48a9e427-7251-4fe0-b498-1d05c7b3705c.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Adapted from our <a href="https://hydromancer.xyz/blog/hyperliquid-testnet">full Hyperliquid testnet guide</a> on the Hydromancer blog, which also covers test funds, the API addresses and HyperEVM setup.</em></p>
<p>Your bot works on Hyperliquid's testnet. Before you point it at mainnet, check these, because each one can break your code or send the wrong order.</p>
<h2>Market IDs change between networks</h2>
<p>Orders refer to markets by number, and the numbers don't match:</p>
<pre><code class="language-python">from hyperliquid.info import Info
from hyperliquid.utils import constants

def asset_ids(url):
    universe = Info(url, skip_ws=True).meta()["universe"]
    return {asset["name"]: i for i, asset in enumerate(universe)}

test, main = asset_ids(constants.TESTNET_API_URL), asset_ids(constants.MAINNET_API_URL)
for coin in ["BTC", "ETH", "SOL", "HYPE"]:
    print(f"{coin}: mainnet asset {main[coin]}, testnet asset {test[coin]}")
</code></pre>
<pre><code class="language-text">BTC: mainnet asset 0, testnet asset 3
ETH: mainnet asset 1, testnet asset 4
SOL: mainnet asset 5, testnet asset 0
HYPE: mainnet asset 159, testnet asset 135
</code></pre>
<p>Of the 207 perps on both networks, only one had the same ID. Spot pairs differ too: HYPE/USDC is named <code>@107</code> on mainnet and <code>@1035</code> on testnet. Read IDs from <code>meta</code> and <code>spotMeta</code> on the network you trade on, every time.</p>
<h2>Testnet prices aren't real prices</h2>
<p>Testnet has its own order books and traders:</p>
<pre><code class="language-python">from hyperliquid.info import Info
from hyperliquid.utils import constants

testnet = Info(constants.TESTNET_API_URL, skip_ws=True)
mainnet = Info(constants.MAINNET_API_URL, skip_ws=True)

for coin in ["BTC", "ETH", "HYPE"]:
    print(coin, "testnet:", testnet.all_mids()[coin], "| mainnet:", mainnet.all_mids()[coin])
</code></pre>
<pre><code class="language-text">BTC testnet: 81679.5 | mainnet: 81332.5
ETH testnet: 2379.2 | mainnet: 2383.95
HYPE testnet: 86.4825 | mainnet: 77.3055
</code></pre>
<p>Use testnet to check that your code works, not to judge fills, slippage or a strategy. For backtests, use mainnet history such as the free <a href="https://hydromancer.xyz/blog/hyperliquid-historical-s3-archive">Reservoir archive</a>.</p>
<h2>The errors you'll hit, and what they mean</h2>
<table>
<thead>
<tr>
<th>Error</th>
<th>Cause</th>
<th>Fix</th>
</tr>
</thead>
<tbody><tr>
<td><code>Order must have minimum value of $10.</code></td>
<td>Size times price is under $10</td>
<td>Raise the size</td>
</tr>
<tr>
<td><code>Order has invalid price.</code> / <code>Price must be divisible by tick size.</code></td>
<td>The price breaks the tick rules (at most 5 significant figures; limited decimals)</td>
<td>Round the price</td>
</tr>
<tr>
<td><code>Insufficient margin to place order.</code></td>
<td>Not enough USDC for that size</td>
<td>Add funds or cut the size</td>
</tr>
<tr>
<td><code>Post only order would have immediately matched…</code></td>
<td>An <code>Alo</code> order priced through the book</td>
<td>Price it behind the best bid or ask</td>
</tr>
<tr>
<td><code>User or API Wallet 0x… does not exist.</code></td>
<td>The key isn't approved on this network, its approval expired, or you signed for the other network</td>
<td>Approve the API wallet on this network</td>
</tr>
<tr>
<td><code>Cannot set scheduled cancel time until enough volume traded.</code></td>
<td>The dead man's switch needs $1,000,000 of traded volume</td>
<td>Test it on mainnet, with care</td>
</tr>
</tbody></table>
<p>The order rejections come back inside a top-level <code>"status": "ok"</code>, so check each order's own status, not just the response.</p>
<h2>Catch rounding errors before you send</h2>
<p>This helper rounds a perp order to the market's rules and checks the $10 minimum:</p>
<pre><code class="language-python">from hyperliquid.info import Info
from hyperliquid.utils import constants

info = Info(constants.TESTNET_API_URL, skip_ws=True)
sz_decimals = {asset["name"]: asset["szDecimals"] for asset in info.meta()["universe"]}

def prepare_order(coin, size, price):
    """Round a perp order to Hyperliquid's rules and check the $10 minimum."""
    decimals = sz_decimals[coin]                      # KeyError here means the coin isn't on this network
    size = round(size, decimals)
    price = float(f"{price:.5g}")                     # at most 5 significant figures
    if price != int(price):
        price = round(price, 6 - decimals)            # and at most 6 - szDecimals decimals on perps
    if size * price &lt; 10:
        raise ValueError(f"Order value ${size * price:.2f} is under the $10 minimum")
    return size, price

print(prepare_order("BTC", 0.0012345678, 40887.123456))
print(prepare_order("ETH", 0.0123456, 1234.56789))
try:
    prepare_order("BTC", 0.0001, 40887)
except ValueError as e:
    print("Rejected before sending:", e)
</code></pre>
<pre><code class="language-text">(0.00123, 40887.0)
(0.0123, 1234.6)
Rejected before sending: Order value $4.09 is under the $10 minimum
</code></pre>
<h2>Before you switch to mainnet</h2>
<ul>
<li>Swap the API and WebSocket URLs, and for HyperEVM the RPC URL and chain ID (999).</li>
<li>Approve an API wallet on mainnet.</li>
<li>Remove hardcoded asset IDs, prices, leverage and size rules.</li>
<li>Keep the per-order status checks.</li>
<li>Re-check <a href="https://hydromancer.xyz/blog/hyperliquid-rate-limits">rate limits</a> under real traffic.</li>
</ul>
<p>The <a href="https://hydromancer.xyz/blog/hyperliquid-testnet">full guide</a> covers getting test USDC and HYPE, streaming testnet trades and the rest of the error list.</p>
]]></content:encoded></item></channel></rss>