Authentication & signing

MNX uses two separate mechanisms, and a full trading integration needs both:

  1. Request authentication — an Authorization header on HTTP requests, proving who is calling. Endpoints that require it are marked with a lock (bearer auth) in the API reference.
  2. 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:

HeaderCredential
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:

  1. 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 (the personal_sign flow, which prefixes "\x19Ethereum Signed Message:" before signing).
  2. Send the signature to POST /v0/auth/authorize:
    POST /v0/auth/authorize
    {
      "signature": "0x...",
      "userAddress": "0xYourWalletAddress",
      "isTermAccepted": true,
      "accepted_risk_disclaimer": true
    }
  3. The response is { "token": "..." } — a session token to send as Authorization: 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:

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.mjs

Save 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:

  1. Generate a new keypair locally and keep the private key.
  2. POST /v0/session-keys/prepare with 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 the chain_id and the AccountManager / MinimalForwarder contract addresses involved.
  3. Sign that forward request with your main wallet (an EIP-712 signature).
  4. POST /v0/session-keys/relay with the forward-request fields, your signature, the session public address, approved: true, an expiration_seconds, and the permissions object. The exchange relayer submits the registration on-chain and the response reports the resulting status and session_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.