Authentication & signing
MNX uses two separate mechanisms, and a full trading integration needs both:
- Request authentication — an
Authorizationheader on HTTP requests, proving who is calling. Endpoints that require it are marked with a lock (bearer auth) in the API reference. - Order signing — an EIP-712 wallet signature attached to each order, proving the account holder authorized that exact trade. Signatures are verified against the market's on-chain contracts, so the exchange cannot forge or alter your orders.
Request authentication
The API accepts two Authorization header schemes, Key and Bearer:
| Header | Credential |
|---|---|
Authorization: Key <api-key> | A personal API key tied to your exchange account. Rotate it (and obtain the new value) with POST /v0/me/api-key/rotate, which itself must be called with a valid credential — an existing key or a wallet session token. |
Authorization: Bearer <token> | A wallet session token (see below), or — in the official web app — an identity token issued by the Privy embedded-wallet provider. External API clients should use a session token or an API key. |
On authenticated endpoints whose path contains a :user_id parameter, the ID must be your own — you can only read your own orders, balances, and positions.
Wallet session tokens
If you control an Ethereum wallet, you can create a 30-day API session without any prior setup:
- Hash the environment's API base URL (e.g.
https://api.testnet.mnx.fi) with keccak-256 and sign the hash as a standard Ethereum personal message (thepersonal_signflow, which prefixes"\x19Ethereum Signed Message:"before signing). - Send the signature to
POST /v0/auth/authorize:POST /v0/auth/authorize { "signature": "0x...", "userAddress": "0xYourWalletAddress", "isTermAccepted": true, "accepted_risk_disclaimer": true } - The response is
{ "token": "..." }— a session token to send asAuthorization: Bearer <token>on subsequent requests. If the wallet has never traded on MNX, this call also creates the exchange account.
Check a session with GET /v0/me/client/session and end it with POST /v0/me/client/logout.
To create a disposable wallet, fund it, deposit collateral, place a signed order, verify it, and clean it up in one runnable example, see Build a testnet order bot.
EIP-712 order signing
EIP-712 is an Ethereum standard for signing structured data instead of an opaque string of bytes. The data is a typed struct (named fields with types), and the signature also covers a domain — the contract name, version, chain ID, and contract address it is intended for. That binding means a signature for one order cannot be replayed as a different order, on a different market, or on a different chain, and wallets can display the individual fields for review before signing.
Orders are signed over this struct:
Order(
bytes8 flags, // packed salt + isBuy + reduceOnly bits
uint128 quantity, // base quantity, scaled by 1e18
uint128 price, // limit price scaled by 1e18 (0 for market orders)
uint128 triggerPrice, // trigger price scaled by 1e18 (0 if not conditional)
uint128 leverage, // whole-number leverage, scaled by 1e18
address maker, // the account the order trades for
uint128 expiration // Unix seconds, 0 = good-til-canceled
)with the EIP-712 domain:
{
name: "IsolatedTrader",
version: "1.0",
chainId: <chain id>, // MegaETH mainnet 4326, testnet 6343
verifyingContract: <address> // the market's trader_address
}Notes on the fields:
verifyingContractis per-market: use thetrader_addressreturned byGET /v0/marketsfor the market you are trading. Shared contract addresses are available fromGET /v0/contracts/shared.flagsis exactly(salt << 4) | buyBit | reduceOnlyBit, encoded as 8 bytes.buyBitis 1 forLONGand 0 forSHORT;reduceOnlyBitis 2 when enabled and 0 otherwise. Use a nonnegative salt below 253, send the same integer as the request's decimalsaltstring, and leave the other two low bits clear.- The struct amounts are the 1e18-scaled integer versions of the human-readable numbers you send in the JSON request body. The two must describe the same order or the signature check fails.
- Sign market orders with
price: 0. Conditional market orders also sign a zero price, while theirtriggerPriceremains the 1e18-scaled trigger. - Take-profit and stop-loss order types are always reduce-only. Set both the signed reduce-only bit and the request's
reduce_onlyfield to true for those orders. - Ethers returns a 65-byte signature from
signTypedData. Append00to select the EIP-712 no-prepend recovery mode. That embedded byte is separate from the JSON request'ssignature_type: 1. The request'ssaltandexpiration_secondsmust also match the signed values.
Sign and place a testnet order with ethers v6
The MNX client packages are not currently distributed publicly. This example uses only ethers and Node's built-in fetch. Use the same funded testnet wallet that created your bearer token, keep its private key out of source control, and inspect GET /v0/markets if you want to select a market instead of using the first enabled one.
npm install ethers@6
export MNX_TOKEN="your-wallet-session-token"
export MNX_PRIVATE_KEY="your-funded-testnet-wallet-private-key"
node place-order.mjsSave the following as place-order.mjs. It signs and submits a minimum-size LONG market order; change the side, quantity, or reduce-only value in both the signed message and request together.
import { Wallet, parseUnits, toBeHex } from 'ethers'
const API = 'https://api.testnet.mnx.fi'
const { MNX_TOKEN, MNX_PRIVATE_KEY } = process.env
if (!MNX_TOKEN || !MNX_PRIVATE_KEY) {
throw new Error('Set MNX_TOKEN and MNX_PRIVATE_KEY')
}
const wallet = new Wallet(MNX_PRIVATE_KEY)
const marketsResponse = await fetch(API + '/v0/markets')
if (!marketsResponse.ok) throw new Error(await marketsResponse.text())
const markets = await marketsResponse.json()
const market = markets.find((item) => item.trading_enabled)
if (!market) throw new Error('No trading-enabled market is available')
const side = 'LONG'
const reduceOnly = false
const leverage = 1
const quantity = market.min_order_size
const salt = BigInt(Date.now()) // below 2^53; use once
const expiration = Math.floor(Date.now() / 1000) + 60
const buyBit = side === 'LONG' ? 1n : 0n
const reduceOnlyBit = reduceOnly ? 2n : 0n
const flags = toBeHex((salt << 4n) | buyBit | reduceOnlyBit, 8)
const domain = {
name: 'IsolatedTrader',
version: '1.0',
chainId: 6343,
verifyingContract: market.trader_address,
}
const types = {
Order: [
{ name: 'flags', type: 'bytes8' },
{ name: 'quantity', type: 'uint128' },
{ name: 'price', type: 'uint128' },
{ name: 'triggerPrice', type: 'uint128' },
{ name: 'leverage', type: 'uint128' },
{ name: 'maker', type: 'address' },
{ name: 'expiration', type: 'uint128' },
],
}
const message = {
flags,
quantity: BigInt(market.min_order_size_e18_raw),
price: 0n,
triggerPrice: 0n,
leverage: parseUnits(String(leverage), 18),
maker: wallet.address,
expiration: BigInt(expiration),
}
const rawSignature = await wallet.signTypedData(domain, types, message)
const signature = rawSignature + '00'
const orderResponse = await fetch(API + '/v0/orders', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + MNX_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({
market_id: market.market_id,
side,
order_type: 'MARKET',
price: 0,
quantity,
leverage,
reduce_only: reduceOnly,
expiration_seconds: expiration,
salt: salt.toString(),
signature,
signature_type: 1,
}),
})
const result = await orderResponse.json()
if (!orderResponse.ok) throw new Error(JSON.stringify(result))
console.log(result)See Placing orders for limit and conditional request fields.
Session keys
Signing every order with your main wallet is slow for interactive or automated trading — hardware wallets and browser wallets prompt on every signature. A session key is a fresh, locally generated keypair that you authorize to sign orders on your account's behalf, with an expiration time and limited permissions (can_place_orders, can_cancel_orders). The session key can sign trades; it cannot withdraw funds.
Registration is gasless, relayed on-chain by the exchange:
- Generate a new keypair locally and keep the private key.
POST /v0/session-keys/preparewith the new public address. The response contains the fields of a forward request (the meta-transaction pattern where a relayer submits your signed call and pays the gas):from,to,gas,nonce,data, plus thechain_idand the AccountManager / MinimalForwarder contract addresses involved.- Sign that forward request with your main wallet (an EIP-712 signature).
POST /v0/session-keys/relaywith the forward-request fields, your signature, the session public address,approved: true, anexpiration_seconds, and the permissions object. The exchange relayer submits the registration on-chain and the response reports the resulting status andsession_key_id.
From then on, sign orders with the session key exactly as described above — the maker field stays your account address; only the signer changes. The exchange accepts the signature if the recovered signer is one of your registered session keys that is active, unexpired, and has the required permission.
List your keys with GET /v0/session-keys (filterable by ACTIVE / REVOKED / EXPIRED). Revoke one via the same prepare/relay flow with approved: false and the session_key_id.