Skip to main content

Migrating from HL

This page explains how to move a Hyperliquid bot to the MetaFlux API.

MetaFlux uses its own MTF-native protocol

There is no Hyperliquid-compatible shim. Your bot keeps its strategy and trading logic. The client and wire layer changes. The fastest path is the official TypeScript or Rust SDK. The SDK builds the native envelope and the EIP-712 signature for you. For other languages, implement typed-data signing directly.

If your bot trades on a Hyperliquid-style perps DEX, the move to MetaFlux rewrites the client layer. It does not rewrite the strategy. The concepts you depend on all exist on MTF: limit orders, fills, funding, cross and isolated margin, agent wallets, sub-accounts and vaults. You replace the wire shape, the action and query names, the chain ID and the asset IDs.

Scope of the change​

  • Wire shape. MTF-native is snake_case JSON over POST /exchange (write), POST /info (read) and GET /ws (stream), each EIP-712-signed where required. Use the SDK, or implement the native signing scheme.
  • Strategy and risk logic. Unchanged. Your quoting, sizing and hedging code carries over.
  • Names and some semantics. Action types and query types have new names (table below). Some behaviours differ: asset IDs, the T0 liquidation tier and agent-approval latency.

Unchanged features​

  • Limit, IOC and ALO orders, reduce-only, and client order ids (cloid).
  • EIP-712 signing: the same signature primitive, with a different domain and chain ID.
  • Cross and isolated margin, funding payments, fills and order-status reads.
  • Agent wallets (hot keys with no withdrawal authority), sub-accounts and vaults.

Changes​

Protocol surface​

There is one MTF-native surface. You call it through the SDK, or you build the envelope yourself. The names map one to one:

You used on HLMTF-native equivalent
POST /exchange ordersubmit_order / batch_order
POST /exchange cancelcancel_order / cancel_by_cloid
POST /exchange modify / batchModifymodify / batch_modify
POST /info metamarkets
POST /info clearinghouseStateTwo reads, not one. account_state for the collateral and margin health, clearinghouse_state for the position rows. HL keeps positions inside the account read. MetaFlux does not. See below
POST /info spotClearinghouseStateaccount_state: the spot.balances array. There is no separate spot read
POST /info openOrders / frontendOpenOrdersopen_orders, one kind for both. There is no separate "frontend" variant. Every open_orders row includes the time-in-force, cloid and trigger detail.
POST /info userFillsuser_fills
POST /info candleSnapshotcandle_snapshot. The standalone candle type is removed. candle_type selects one of THREE series: mark (the default), oracle or trade. A mark or oracle bar is a price series, not executions: its v and q read "0" and its n reads 0. v, q and n can also be ABSENT on any series, because durable history holds no volume for the bucket. A trade bar served from history drops q in every case. Test that the key is present before you read it. Absent means "no data", and "0" means "no trades". See the volume rule
WS userEvents, l2Book, candlefills / order_updates / ledger_updates (there is no combined events channel), l2_book, candles. See WS subscriptions

The account read splits differently from HL's. HL splits per product, into clearinghouseState and spotClearinghouseState, and keeps position rows inside the perp read. MetaFlux splits per lane in the same way, but it also moves position detail to its own read. account_state therefore gives you one consistent set of money figures for the whole account. clearinghouse_state gives you the rows. Do not rebuild HL's single object by a join of the two frames. The two frames can be rendered a commit apart, and the joined result was true at no single block. If you must combine them, compare the height that both frames carry.

The full catalogs are POST /exchange and POST /info.

Chain ID​

MetaFlux is its own L1. It is not an HL deployment. Sign against the MetaFlux chain ID, not HL's:

NetworkMTF chainId
Mainnet8964 (0x2304)
Testnet114514 (0x1bf52)
Devnet / local31337 (0x7a69)

The MTF EIP-712 domain uses name = "MetaFlux", version = "1", verifyingContract = 0x0. See networks and signing.

Base URL​

MTF: https://api.<net>.mtf.exchange/{info,exchange,ws}

The gateway is the one entry point for the MTF-native surface. If you run the node yourself, it serves the same surface at http://localhost:8080.

Asset IDs​

HL and MTF both use integer asset IDs, but the integers are different. 0 on HL is the BTC perp. 0 on MTF can be ETH or any other asset, depending on the deployment. Always look up your asset IDs through POST /info { "type": "markets" } at startup. Never hard-code them.

Numeric precision​

Price and size fields are scaled integers sent as JSON strings, because IEEE-754 loses precision above 2^53. If your bot parses with the default JS JSON.parse, use a parser that supports big integers for these fields.

Liquidation behaviour​

MetaFlux adds a T0 yellow-card grace tier that HL does not have. At health [1.0, 1.1), the chain force-cancels your account's resting ALO orders and emits a warning event. It does not touch positions. T1, T2 and T3 then behave like HL's Partial, Market and Backstop tiers.

If your bot listens for liquidation events to add margin, add a handler for the new T0 event. That event is the early warning that HL does not give you. It gives you one block of grace to act.

Agent wallet semantics​

An agent is a key with no withdrawal authority. HL uses the same model (see agent wallets). The action is approve_agent. The one mechanical difference: an MTF agent approval takes effect one block after commit. HL typically takes two blocks. MTF is a little faster, and the warm-up steps are the same.

Vaults​

HL vaults and MetaFlux vaults are different products. The vault_state read returns MTF's own vault types (MFlux Vault, user vaults). HL vault addresses do not resolve. Expect MTF entities, not HL ones.

Migration steps​

Day 0​

Adopt the native client.

  1. Install the TypeScript or Rust SDK, or implement typed-data signing for your language.
  2. Point baseUrl at the MTF gateway, and set chainId for your target network.
  3. Implement the asset lookup again, against POST /info { "type": "markets" }.

Day 1​

Map your actions. Translate each action your bot sends to its MTF-native equivalent. See the table in protocol surface. order becomes submit_order, cancel becomes cancel_order, and leverage and margin changes become update_leverage / update_isolated_margin. The SDK builds the EIP-712 envelope. Only the action variant name and the field casing differ.

Day 2​

Connect the new signals.

  • If you operate sub-accounts, read account_state with detail: "overview". The sub-account list is one of its facets. MTF allows up to 32 sub-accounts per master.
  • Add a handler for T0 yellow-card events on the notifications WS channel (kind yellow_card).
  • If you depend on portfolio margin, enroll on MTF with user_portfolio_margin. The threshold and the scenario set are network parameters. See portfolio margin.

Day 3 and later​

Adopt the features that only MTF has. This step is optional:

  • RFQ. Request-for-quote primitives, for size that you do not want to show on the book.
  • FBA. Frequent batch auction matching for designated markets. It reduces MEV.
  • Cross-chain primitives. Bridge primitives that EVM contracts can call natively.

These are MTF-native actions on POST /exchange. See the API overview.

Common HL bot patterns​

Simple limit-order market maker​

This is the most common pattern.

import { Client } from '@metaflux-dex/client';

const client = new Client({
baseUrl: 'https://api.testnet.mtf.exchange',
privateKey: Buffer.from(process.env.PRIVATE_KEY!.replace(/^0x/, ''), 'hex'),
});
const owner = '0x<YOUR_ADDRESS>';

// asset lookup: HL `meta.universe` → MTF `marketsMeta` (`signing_id` is the
// numeric id a signed action needs; may not be 0)
const meta = await client.info.marketsMeta();
const BTC = meta.perp.find((m) => m.coin === 'BTC')!.signing_id;

// order / cancel — your strategy logic, native action names
await client.submitOrderNative({
owner, market: BTC, side: 'bid', kind: 'limit',
size: 1_000, limit_px: 1_000_000_000_000,
tif: 'gtc', stp_mode: 'cancel_newest', reduce_only: false,
});

The strategy stays. The client layer becomes the SDK call.

Liquidation watcher​

This bot adds margin when the account nears liquidation. HL emits liquidation events at the partial and market tiers. MTF adds a yellow_card notification as the earliest signal, on the dedicated notifications channel.

import { isChannelFrame } from '@metaflux-dex/client';

const ws = await client.connectWs();
ws.onMessage((f) => {
if (!isChannelFrame(f, 'notifications')) return;
for (const record of f.data) {
switch (record.kind) {
case 'yellow_card':
// T0 — one block to act; ALO orders already cancelled
deposit(YELLOW_CARD_DEPOSIT);
break;
case 'forced_close_tier':
// T1 partial OR T2 full — too late for prevention
emergencyUnwind();
break;
}
}
});
await ws.subscribe({ type: 'notifications', user: owner });

See risk-watcher for the full pattern.

Funding-rate arbitrage bot​

The funding cadence is similar: hourly by default, and configurable per market on MTF. The formula structure is identical. The read is the native funding query.

const funding = await client.info.fundingHistory('BTC');
// values may differ from HL because oracle composition differs
const rate = funding.samples.at(-1)?.funding_rate;

Governance sets the MTF oracle composition per market, through committed SetOracleWeights. If your arbitrage depends on specific oracle providers, verify the weighted source list. See mark prices.

Multi-account and institutional setup​

HL uses a master and one agent per host. MTF uses the same setup, and adds multi-sig accounts.

// existing: master + agents (each host is its own Client with its own key;
// `owner` on each action routes it to the master, not a client option)
await master.approveAgent({ agent: host1AgentAddr });
await master.approveAgent({ agent: host2AgentAddr });

// new on MTF: convert master to multi-sig for cold custody
await master.convertToMultiSigUser({
threshold: 2,
signers: [signer1, signer2, signer3],
});
// every subsequent master-level action then requires 2 sigs;
// agents still work as before for trading actions

See multi-sig.

Sub-account portfolio manager​

HL allows up to 8 sub-accounts. MTF allows up to 32.

// MTF: create one of up to 32 subs
await master.createSubAccount({ name: 'desk-A', shared_stp_group: false });
await master.subAccountTransfer({ sub_index: 0, deposit: true, amount: '10000' });

MTF supports agent management, PM enrollment and margin modes per sub-account.

Reference table​

Action you used on HLMTF-native action
order (place limit / IOC / ALO)submit_order / batch_order
cancel (by OID)cancel_order
cancelByCloidcancel_by_cloid
modify / batchModifymodify / batch_modify
usdSend / spot transfersnative spot transfer actions
withdraw3bridge_withdraw
sendToEvmWithDatasend_to_evm_with_data (same field names), or core_evm_transfer. Both are active. Read the note below.
approveAgentapprove_agent
updateLeverage / updateIsolatedMarginupdate_leverage / update_isolated_margin
convertToMultiSigUserconvert_to_multi_sig_user
setReferrer / createReferralset_referrer_by_code / register_referral_code. set_referrer binds by address, but only to an address that holds a code. A code needs trailing 30-day volume, not lifetime volume. The referee discount and the referrer share each stop at a cap on the referee's volume. See the referral program

Copied sendToEvmWithData payloads​

The chain refuses a payload copied from HL without changes. send_to_evm_with_data keeps the HL field names, so a direct copy is tempting. Do not do it. HL accepts and ignores three fields that MTF refuses. You will hit the first one:

  • source_dex must be 0. An HL payload often carries source_dex: 1. MTF debits one ledger, the spot ledger. It refuses any other value, so that it never debits a ledger you did not name.
  • to_perp must be false. The EVM side has no perp account to credit.
  • destination_chain_id must be 0 or the local EVM chain id. The chain refuses any other value. This field is not a cross-chain lane. To leave the chain, use bridge_withdraw.

Check two more things before you port it:

  • The action is active. An earlier version of this page said that the network refused it, and told you to port to core_evm_transfer. That is no longer true.
  • It debits the spot ledger only. It cannot move USDC held as perp collateral. core_evm_transfer can, and it is active now. It is therefore the better target for most ports.

Full rules: send_to_evm_with_data.

Help​