Quickstart — 5-minute end-to-end
Status. stable wire surface. Testnet endpoints, no mainnet warranty.
Deposit, place an order, cancel, withdraw. By the end of this page your TypeScript / Python / curl session has done a complete round-trip against testnet.
Prerequisites
- An EVM private key (any 32-byte hex; generate a fresh one — never reuse a mainnet key)
- USDC on a MetaBridge source chain (Base or Arbitrum) — on testnet the faucet replaces this
curlor any HTTP client
Endpoints
The gateway is the single public front door, serving the MTF-native surface.
| Service | URL (testnet) |
|---|---|
| Gateway front door | https://api.testnet.mtf.exchange |
| MTF-native | POST /info · POST /exchange · GET /ws |
| EVM JSON-RPC | POST /evm |
| Faucet | POST /faucet |
| Explorer | https://app.mtf.exchange/explorer |
The faucet is not a separate service — it's the
POST /faucetroute on the gateway front door. Running the node yourself? The same native surface (/info·/exchange·/ws·/faucet) is served directly athttp://localhost:8080. SeePOST /faucet.
See networks for the full list including testnet and (post-launch) mainnet.
Step 1 — Get testnet USDC
curl -X POST https://api.testnet.mtf.exchange/faucet \
-H 'content-type: application/json' \
-d '{"address":"0x<YOUR_ADDRESS>"}'
# -> {"address":"0x…","usdc":3000,"mtf":10,"status":"queued"}
One claim grants 3000 USDC cross-collateral and 10 MTF spot tokens —
once ever per address (a second claim returns 429 address already funded),
rate-limited per source IP (one claim per minute) and crossed with the
per-address rule. The optional amount only caps the USDC grant downward
(≤ 3000); MTF is fixed — see Limits. The grant is
"queued" — it lands ~1 block later, so wait a moment before confirming the
balance:
"queued" means staged, not credited. The faucet transfers out of a reserve
account rather than creating tokens, so the grant lands about one block later.
The reserve is checked before the response, so a 200 means it could pay at that
moment. Confirm with account_state below before you trade — see
the reserve.
The faucet is a test-network convenience only. To fund a real account with bridged USDC, deposit through the MetaBridge custody bridge — call the source chain's
deposit(mtfDest, amount)(never a plain transfer to the custody address). See bridge → deposit.
The raw curls below speak MTF-native on the gateway (snake_case types like
account_state / open_orders). The @metaflux-dex/client examples speak the
same native surface — the SDK just builds the signed envelope for you.
curl -X POST https://api.testnet.mtf.exchange/info \
-H 'content-type: application/json' \
-d '{"type":"account_state","address":"0x<YOUR_ADDRESS>"}'
You should see data.account_value: "3000".
Step 2 — Place a limit order
Going further than this quickstart? Placing orders is the canonical order guide — the raw wire request and response, the two number planes, and a tiered map of every order action.
The full signing flow is in signing. For this quickstart use the official TypeScript SDK (@metaflux-dex/client — ships before mainnet; see TypeScript SDK).
import { Client } from '@metaflux-dex/client';
const client = new Client({
baseUrl: 'https://api.testnet.mtf.exchange', // MTF-native is the gateway default path
privateKey: Buffer.from(process.env.PRIVATE_KEY!.replace(/^0x/, ''), 'hex'), // 32 bytes
});
const owner = '0x<YOUR_ADDRESS>';
// `markets()` keys by `coin` (the symbol); the numeric id a signed action
// needs is `signing_id` on `markets_meta`, the STATIC read. There is no
// `asset_id` field — reading one gives you `undefined`.
const meta = await client.info.marketsMeta();
const btc = meta.perp.find((m) => m.coin === 'BTC')!;
const result = await client.placeOrder({
venue: 'perp',
owner,
market: btc.signing_id,
side: 'bid', // 'bid' = buy, 'ask' = sell
kind: 'limit',
size: 1_000, // raw lots, scaled by the market's sz_decimals
limit_px: 5_000_000_000_000, // 1e8 fixed-point plane
tif: 'gtc',
stp_mode: 'cancel_newest',
reduce_only: false,
});
if (result.route === 'batch_order') {
console.log('order status:', result.legs[0]?.status);
}
Raw curl (MTF-native shape — you build the signature yourself; see signing):
curl -X POST https://api.testnet.mtf.exchange/exchange \
-H 'content-type: application/json' \
-d @order.json
where order.json is the signed MTF-native envelope you assembled.
Spot trading example
Spot is a token-for-token CLOB, separate from
perps — no leverage, no positions. Place a spot order with the native
spot_order action: it takes a spot pair
id (not a perp market), a side, a limit_px, a size, and a tif. A
resting gtc/alo order locks reserved-balance escrow; ioc never rests.
// the `action` you sign and POST to /exchange (sender-authorized; owner is optional)
{
"type": "spot_order",
"order": {
"pair": 200, // spot pair id from /info, not a perp market id
"side": "bid", // bid = buy base (pays quote); ask = sell base
"size": 100000000,
"limit_px": 200000000, // 1e8 plane; 0 places a market order (must use tif "ioc")
"tif": "gtc",
"stp_mode": "cancel_oldest"
}
}
The synchronous response carries the assigned oid with a resting or filled
entry (the same status union as a perp order). Read your spot balances and open
spot orders back via POST /info; cancel with
spot_cancel, which refunds the escrow.
Step 3 — Check the order is on the book
curl -X POST https://api.testnet.mtf.exchange/info \
-H 'content-type: application/json' \
-d '{"type":"open_orders","address":"0x<YOUR_ADDRESS>"}'
You should see your order with the oid from step 2.
Or, subscribe to live updates (preferred for any non-trivial usage):
const ws = await client.connectWs();
ws.onMessage((f) => {
if (f.channel === 'order_updates') console.log('event:', f.data);
});
await ws.subscribe({ type: 'order_updates', user: owner });
Step 4 — Cancel
if (result.route === 'batch_order') {
const status = result.legs[0]?.status;
const oid = status && 'resting' in status ? status.resting.oid : undefined;
if (oid !== undefined) await client.cancelOrderNative({ owner, market: btc.signing_id, oid });
}
# raw curl
curl -X POST https://api.testnet.mtf.exchange/exchange \
-d @cancel.json
Step 5 — Withdraw
await client.mbWithdraw({
chain: 'Arbitrum',
asset: 0, // 0 = USDC cross-collateral
amount: 100_000_000, // 100 USDC, base units
dst_addr: '0x<DESTINATION>',
});
This queues a MetaBridge withdrawal. After the MetaFlux validator set co-signs it to a ⅔ stake-weighted quorum and the dispute window elapses (a few minutes), you can claim on the destination chain (see bridge).
What just happened
Next steps
- Placing orders — the canonical order guide: batches, spot, cancels, number planes
- Signing — what's inside the SDK's signing
- Agent wallets in practice — production hot-key pattern
- Order types — beyond plain limit orders
- Error handling — admission vs commit vs network
- WS subscriptions — push for live data
- Migrating from HL — already have a Hyperliquid bot? this page first
Troubleshooting
Show troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 signer is not the sender | Wrong EIP-712 domain chain id | The SDK signs against MTF_CHAIN_ID (testnet 114514, mainnet 8964) by default — don't override chainId on a call unless you mean to target a different network |
400 action: <parse error> | Wrong field name, wrong type, or a missing required field | Check the action's entry in the catalog |
404 unknown user on info | Address has no on-chain state yet | Deposit first (faucet) |
429 rate limit | Too many requests | See rate limits; back off |
| Withdrawal stuck on destination | MetaBridge withdrawal pending (dispute window) | Wait for the ⅔ co-signature + dispute window; then claim on the destination chain (see bridge) |
See also
- Networks — testnet and mainnet endpoints + chainIds
- Signing — the full envelope spec
POST /exchangePOST /info- WS