Skip to main content

Quickstart

This page takes a client through one full round trip on testnet: deposit, order, cancel and withdrawal.

info

Status. stable wire surface. Testnet endpoints, no mainnet warranty.

At the end of this page, your TypeScript, Python or curl session has deposited, placed an order, cancelled it and withdrawn on testnet. It takes about 5 minutes.

Prerequisites​

  • An EVM private key: any 32-byte hex value. Generate a new 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 one public entry point. It serves 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 is the POST /faucet route on the gateway. If you run the node yourself, it serves the same native surface (/info · /exchange · /ws · /faucet) at http://localhost:8080. See POST /faucet.

Networks has the full list, with testnet and, after launch, mainnet.

1. Get testnet USDC​

curl -X POST https://api.testnet.mtf.exchange/faucet \
-H 'content-type: application/json' \
-d '{"address":"0x<YOUR_ADDRESS>","expires":<unix seconds>,"signature":"0x<personal_sign result>"}'
# -> {"address":"0x…","usdc":3000,"mtf":10,"status":"queued"}

The recipient signs the claim with EIP-191 personal_sign, over a four-line text that names the address, the chain id and expires. The node refuses an unsigned claim since node 0.9.17. See sign the claim.

One claim grants 3000 USDC cross-collateral and 10 MTF spot tokens. Each address can claim only once. A second claim returns 429 address already funded. The faucet also limits each source IP to one claim per minute, in addition to the rule per address. The grant is fixed. Send no amount. See Limits. The grant is "queued" and lands about 1 block later. Wait a moment before you confirm the balance:

info
"queued" means staged, not credited

The faucet transfers out of a reserve account. It does not create tokens, so the grant lands about one block later. The faucet checks the reserve before it responds, so a 200 means the reserve could pay at that moment. Confirm with account_state below before you trade. See the reserve.

The faucet exists for testnet only. To fund a real account with bridged USDC, deposit through the MetaBridge custody bridge. Call deposit(mtfDest, amount) on the source chain. Never send a plain transfer to the custody address. See bridge deposit.

The raw curls below use the MTF-native surface on the gateway, with snake_case types such as account_state / open_orders. The @metaflux-dex/client examples use the same native surface. The SDK 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>"}'

The response shows data.account_value: "3000".

2. Place a limit order​

tip

Placing orders is the main order guide. It covers the raw wire request and response, the two number planes, and a tiered map of every order action.

Signing has the full signing flow. For this quickstart, use the official TypeScript SDK, @metaflux-dex/client. It 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, in the 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

Here order.json is the signed MTF-native envelope that you assembled.

Spot trading example​

Spot is a token-for-token CLOB, separate from perps. It has no leverage and 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. An ioc order 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. This is the same status union as a perp order. Read your spot balances and open spot orders through POST /info. Cancel with spot_cancel, which refunds the escrow.

3. Check the order 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>"}'

The response shows your order with the oid from step 2.

You can also subscribe to streaming updates. This is the preferred method for any real use:

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 });

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

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 call queues a MetaBridge withdrawal. The MetaFlux validator set co-signs it to a ⅔ stake-weighted quorum. Then the dispute window elapses, which takes a few minutes. Then you can claim on the destination chain. See bridge.

Request flow​

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. Do not 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 infoThe address has no on-chain state yetDeposit first (faucet)
429 rate limitToo many requestsSee rate limits, and back off
Withdrawal stuck on destinationMetaBridge withdrawal pending (dispute window)Wait for the ⅔ co-signature and the dispute window, then claim on the destination chain. See bridge

See also​