TypeScript SDK
The @metaflux-dex/client package is the TypeScript client for the MetaFlux API.
Preview. The @metaflux-dex/client package ships before mainnet. The API shape below is committed.
Summary
npm install @metaflux-dex/client
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; omit for a read-only client
});
const meta = await client.info.marketsMeta();
const btc = meta.perp.find((m) => m.coin === 'BTC')!;
await client.placeOrder({
venue: 'perp',
owner: '0x17c5185167401ed00cf5f5b2fc97d9bbfdb7d025',
market: btc.signing_id, // numeric market id; only `markets_meta` carries it
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,
});
The package exports the class as Client (not MetaFluxClient). It has no .exchange or
.info sub-object for writes. Trading and account actions are flat methods on Client
(client.placeOrder, client.submitOrderNative, client.approveAgent, …). Reads are under
client.info (client.info.markets(), client.info.accountState(), …). client.connectWs()
opens the WebSocket feed.
Constructor
new Client(opts: ClientOpts)
| Field | Type | Required | Description |
|---|---|---|---|
baseUrl | string | yes | Gateway entry point (https://api.<net>.mtf.exchange). The SDK uses the MTF-native surface, which the gateway serves at /info · /exchange · /ws. If you run the node yourself, point it at http://localhost:8080. See networks. |
privateKey | Uint8Array (32 bytes) | optional | secp256k1 private key. Required for any signing method (every /exchange write). Without it, client.info.* reads still work, and client.canSign reads false. |
chainId | number | optional | Legacy constructor field, kept for backward compatibility. It does not drive signing. Every typed action signs against MTF_CHAIN_ID (testnet 114514; mainnet 8964). To override it per call, pass opts.chainId on the method itself. |
expiresAfterMs | bigint | optional | Default action expiry (unix-ms), added to every typed action this client signs. 0n / absent means it never expires. The network accepts it only when it activates the field. Leave it unset until then. |
There is no signer / senderAddress field. External signing and the agent-wallet pattern work
differently. See Signing externally and
Agent-signing pattern below.
Reads with client.info
Every POST /info query is a method on client.info. It needs no private key.
await client.info.nodeInfo();
await client.info.markets(); // { perp: MarketDynamic[], spot: SpotMeta }
await client.info.marketsMeta(); // { perp: MarketStatic[], spot: SpotMeta } — precision grids, leverage ladders
await client.info.l2Book('BTC', { nLevels: 20 });
await client.info.accountState(address); // collateral + margin health, four lane summaries
await client.info.clearinghouseState(address); // perp position rows, keyed by dex
await client.info.optionState(address); // open option legs
await client.info.openOrders(address);
await client.info.userFills(address, 1000);
await client.info.fundingHistory('BTC');
await client.info.feeSchedule();
await client.info.vaultState(vaultAddress);
await client.info.subAccounts(address);
await client.info.agents(address); // approved agents for `address`
await client.info.spotClearinghouseState(address);
Market reads key by coin (the symbol, for example "BTC"). Account reads key by 0x address.
For an /info query with no dedicated wrapper, use the typed escape hatch
client.info.raw({ type, ...params }).
Writes with flat methods on Client
Every POST /exchange action is a method directly on Client, not under a sub-object. A
representative set:
// Orders
await client.submitOrderNative(order); // one order, POST /exchange
await client.placeOrder(legs, opts); // convenience: routes to batch_order (perp) or spot_order (spot)
await client.batchOrder(batch);
await client.cancelOrderNative(cancel);
await client.cancelByCloid({ asset, cloid });
await client.batchCancel(batch);
await client.modify(params);
await client.batchModify(batch);
await client.cancelAllOrders();
await client.twapOrder(params);
await client.twapCancel(params);
// Leverage & margin (perp only — spot has no leverage; it uses reserved-balance escrow)
await client.updateLeverage({ asset, leverage, is_isolated });
await client.updateIsolatedMargin({ asset, delta }); // delta is a signed decimal STRING
await client.userPortfolioMargin({ enroll: true });
// Account & agents
await client.setDisplayName({ display_name });
await client.approveAgent({ agent, name, expires_at_ms });
await client.approveBrokerFee({ builder, max_bps }); // old name: approveBuilderFee
await client.convertToMultiSigUser({ signers, threshold });
// Staking
await client.tokenDelegate({ validator, amount, is_undelegate });
await client.claimRewards();
// Vaults
await client.createVault({ name, lock_period_secs });
await client.vaultWithdraw({ vault_id, shares });
// MetaBridge
await client.mbWithdraw({ chain: 'Base', asset: 0, amount, dst_addr });
Each method takes an optional { nonce?, chainId? }. Where the action supports agent
authorization, it takes { nonce?, chainId?, owner? }. Each method returns a
NativeExchangeAck ({ statuses?, action_hash?, error? }). POST /exchange
lists the full surface: every buildNative*Action builder, spot orders, TWAP, RFQ/FBA and
spot-margin/Earn. The package exports the builders for out-of-band signing.
placeOrder
placeOrder is one entry point for orders. It tags each order with a venue and picks the wire
action for you. Any number of venue: 'perp' legs become one batch_order. Each
venue: 'spot' leg becomes its own spot_order, because the wire cannot batch spot. The SDK
rejects a call that mixes venues.
const result = await client.placeOrder([
{ venue: 'perp', owner, market: 0, side: 'bid', kind: 'limit',
size: 1_000, limit_px: 5_000_000_000_000, tif: 'gtc',
stp_mode: 'cancel_newest', reduce_only: false },
]);
if (result.route === 'batch_order') {
for (const leg of result.legs) console.log(leg.index, leg.status);
}
WebSocket: client.connectWs()
import { isChannelFrame } from '@metaflux-dex/client';
const ws = await client.connectWs();
ws.onMessage((frame) => {
if (isChannelFrame(frame, 'l2_book')) {
console.log(frame.data.levels[0].length, frame.data.levels[1].length); // bids, asks
}
if (isChannelFrame(frame, 'order_updates')) {
for (const rec of frame.data) {
switch (rec.status) {
case 'filled': /* ... */ break;
case 'canceled': /* ... */ break;
}
}
}
});
await ws.subscribeTrades('BTC');
await ws.subscribe({ type: 'order_updates', user: '0x17c5185167401ed00cf5f5b2fc97d9bbfdb7d025' });
connectWs() derives the ws(s):// URL from baseUrl and connects. If this Client holds a
private key, it gives the returned WsClient a signer. The WsClient can then also POST signed
actions over the socket (ws.submitOrder / ws.cancelOrder / ws.postAction). The WsClient of
a read-only client can still subscribe and call ws.postInfo.
isChannelFrame(frame, channel) narrows an inbound frame and types its data. Use it, and do not
trust frame.channel by hand. The WS client reconnects automatically and replays active
subscriptions. There is no separate .on('open'|'close', …) event API. Inspect ws.isOpen, or
handle a subscriptionResponse frame.
Error handling
The SDK throws one error class, MetaFluxApiError, for any non-2xx /exchange or /info
response. There is no split into RateLimitError / AuthError / CommitError /
NetworkError. Branch on .status yourself:
import { MetaFluxApiError } from '@metaflux-dex/client';
try {
await client.submitOrderNative(order);
} catch (e) {
if (e instanceof MetaFluxApiError) {
if (e.status === 429) { /* back off and retry */ }
else if (e.status === 401) { /* signing / agent-approval problem — do not retry blindly */ }
else if (e.status >= 500) { /* retry with backoff */ }
else { /* 4xx logical error — e.bodyText carries the server's message */ }
} else {
// fetch threw before any response landed (DNS, TCP reset, timeout) —
// unknown outcome; reconcile via cloid / open_orders instead of retrying blind
}
}
MetaFluxApiError carries status (HTTP status), bodyText (raw response body) and message.
A network drop (no response at all) is a plain fetch failure, not a wrapped SDK type. Catch it
in the else branch above.
See error handling for the full decision tree for admission, commit and network errors.
Signing externally
There is no pluggable Signer interface. For an HSM or a wallet popup, build the typed payload
without a signature, give it to the signer, then POST the result:
const built = client.typedData('approve_agent', {
agent: '0x0000000000000000000000000000000000dead',
name: 'mm-host-3',
expires_at_ms: Date.now() + 30 * 24 * 60 * 60 * 1000,
});
// Hand `built.payload` (the eth_signTypedData_v4 payload) to the external
// signer; it returns a 65-byte 0x-hex signature.
const signature = await signWithHsm(JSON.stringify(built.payload));
await client.postTyped({
actionJson: built.actionJson,
nonce: built.nonce,
signature,
expiresAfter: built.expiresAfter,
});
client.typedData(actionType, payload, opts?) builds the EIP-712 struct and the canonical action
JSON without a key. client.postTyped(signed) posts an envelope that is already signed. A wallet
integration (eth_signTypedData_v4) uses the same pair.
Agent-signing pattern
For the agent-wallets pattern, there is no senderAddress /
signerAddress constructor option. Construct a separate Client with the agent's own key, and
set the action's owner field to the master's address:
const master = new Client({ baseUrl, privateKey: masterKey });
const agent = new Client({ baseUrl, privateKey: agentKey });
await master.approveAgent({
agent: agentAddress,
expires_at_ms: Date.now() + 30 * 24 * 60 * 60 * 1000,
});
// Signed by the agent's key; `owner` routes it to the master's account.
await agent.submitOrderNative({
owner: masterAddress,
market: 0, side: 'bid', kind: 'limit',
size: 1_000, limit_px: 5_000_000_000_000,
tif: 'gtc', stp_mode: 'cancel_newest', reduce_only: false,
});
Before the request leaves the process, the agent Client recovers its own signer. The signer
differs from owner, so the client reads the owner's approved agents from /info. It rejects an
unrelated address locally, before the request reaches the chain.
Common patterns
Place and confirm
const cloid = '0x' + randomBytes(16).toString('hex');
await client.submitOrderNative({
owner, market: 0, side: 'bid', kind: 'limit',
size: 1_000, limit_px: 5_000_000_000_000,
tif: 'gtc', stp_mode: 'cancel_newest', reduce_only: false,
cloid,
});
const ws = await client.connectWs();
const filled = new Promise((resolve) => {
const unsubscribe = ws.onMessage((f) => {
if (f.channel !== 'order_updates') return;
for (const rec of f.data as { order: { cloid?: string }; status: string }[]) {
if (rec.order.cloid === cloid && rec.status === 'open') {
unsubscribe();
resolve(rec);
}
}
});
});
await ws.subscribe({ type: 'order_updates', user: owner });
await filled;
Cancel-all
await client.cancelAllOrders();
Subscribe and persist
import { isChannelFrame } from '@metaflux-dex/client';
const fills: unknown[] = [];
const ws = await client.connectWs();
ws.onMessage((f) => {
if (isChannelFrame(f, 'fills')) fills.push(...f.data);
});
await ws.subscribe({ type: 'fills', user: owner });
Numeric handling
/info reads answer in canonical decimal strings (for example account_value: "10000"). They
are exact, with no f64 precision loss. /exchange writes take plain integers on fixed-point
planes: limit_px on the 1e8 book plane, and size scaled by the market's sz_decimals. The
package exports conversion helpers, so you do not write the scaling yourself:
import { pxToWire, szToWire, wireToPx, wireToSz } from '@metaflux-dex/client';
const limitPx = pxToWire('100.50'); // -> 10050000000n, the 1e8 wire plane
const size = szToWire('0.5', 6); // -> 500000n at sz_decimals = 6
console.log(wireToPx(limitPx), wireToSz(size, 6)); // round-trip for display
snapPxToWire / snapSizeToWire / roundOrderToGrid also snap a human price or size onto a
market's tick or lot grid before you build an order. The node rejects an off-grid value.
See also
- Quickstart: a 5-minute run, end to end
- Agent wallets howto
POST /exchange: the full action surface- WS subscriptions: the channel catalog
- Rust SDK
FAQ
Show FAQ
Q: Does the SDK support browsers?
A: The crypto layer is a wasm-pack --target web build, so it is meant to run directly in a browser and in Node (≥ 20). There is no separate @metaflux-dex/client/browser entry point today. A bundler that handles WASM and ESM targets both.
Q: What is the dependency tree?
A: There are no runtime dependencies. Signing (secp256k1, keccak256, EIP-712 hashing, msgpack encoding) runs in the bundled WASM module. HTTP and WebSocket use the platform fetch / WebSocket.
Q: Can I plug in my own HTTP transport (axios, undici)?
A: No. The SDK calls the platform fetch directly and has no transport override hook. To send the signed envelope through your own HTTP stack, use signing externally.