Skip to main content

Quickstart — 5-minute end-to-end

info

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
  • curl or any HTTP client

Endpoints

The gateway is the single public front door, serving the MTF-native surface.

ServiceURL (testnet)
Gateway front doorhttps://api.testnet.mtf.exchange
MTF-nativePOST /info · POST /exchange · GET /ws
EVM JSON-RPCPOST /evm
FaucetPOST /faucet
Explorerhttps://app.mtf.exchange/explorer

The faucet is not a separate service — it's the POST /faucet route on the gateway front door. Running the node yourself? The same native surface (/info · /exchange · /ws · /faucet) is served directly at http://localhost:8080. See POST /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:

info

"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

tip

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

Troubleshooting

Show troubleshooting
SymptomLikely causeFix
401 signer is not the senderWrong EIP-712 domain chain idThe 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 fieldCheck the action's entry in the catalog
404 unknown user on infoAddress has no on-chain state yetDeposit first (faucet)
429 rate limitToo many requestsSee rate limits; back off
Withdrawal stuck on destinationMetaBridge withdrawal pending (dispute window)Wait for the ⅔ co-signature + dispute window; then claim on the destination chain (see bridge)

See also