Getting started

MNX is a derivatives exchange running on the MegaETH blockchain. It lists three kinds of markets: perpetual swaps (contracts that track an asset's price and never expire), binary futures (YES/NO prediction markets priced from 0 to 1 as a probability), and numeric futures (markets that settle to a number, such as a company valuation, under each market's published specification). Everything a trading client needs — market data, order placement, balances, positions — is exposed over a REST API plus a WebSocket feed.

Base URLs

  • Production: https://api.app.mnx.fi
  • Testnet: https://api.testnet.mnx.fi

Production runs on MegaETH mainnet with real funds. Testnet runs on the MegaETH testnet and is the right place to develop and test an integration. Both environments serve the same API, except testnet-only tools such as the faucet. Testnet can also run a newer release, so an endpoint in the testnet reference may not exist on production yet.

The /v0/ prefix

Every public endpoint is served under the /v0/ path prefix. The API reference writes paths with the prefix included, e.g. /v0/markets or /v0/orders. GET endpoints take parameters as query parameters (and path segments); POST endpoints take a JSON body with Content-Type: application/json.

Your first call

Most market-data endpoints require no authentication. List markets on testnet:

curl "https://api.testnet.mnx.fi/v0/markets?type=perpetual&limit=3"

Each market object includes a stable numeric market_id (the canonical identifier — use it for orders and WebSocket topics), a human-readable symbol, current prices, and the market contract addresses. From there you can drill into public data:

  • GET /v0/markets/:market_id — one market's full detail
  • GET /v0/markets/stats — 24h volume, open interest, mark price, and funding rate per market
  • GET /v0/markets/:market_id/book — the public order book: aggregated price levels the matcher can trade right now
  • GET /v0/markets/:market_id/candlesticks — OHLC (open/high/low/close) candles by interval

Public order data

For public order book data, use GET /v0/markets/:market_id/book, naming the market by id or by symbol (/v0/markets/NVDA/book). It returns aggregated price levels inside the market's take band in long_levels and short_levels, each with a tradeable size beside its quantity, plus the best bid and ask computed from those levels. Tradeable size is the part of a level that counts toward prices: it leaves out orders that have not rested yet (live 400 ms, or longer while the market's previous batch settles). Reduce-only orders count only what they can close: an account's reduce-only orders on a side share its position, best price first, after its ordinary orders on that side at the same or a better price. The older GET /v0/orderbook/by-market/:market_id and GET /v0/orderbook?symbol=... routes still answer with a gross book, which keeps matched size until settlement, and will be removed.

Each level contains only side, price, price_e18_raw, quantity, quantity_e18_raw, and order_count, plus tradeable and tradeable_e18_raw on the book route. Quantities are totals at that price and side. Pending trigger orders are excluded, along with account identities, signatures, margin details, and individual order metadata.

To read your own orders, use the authenticated GET /v0/orders/by-user/:user_id endpoint with an optional market_id query parameter, or fetch one order with GET /v0/orders/:order_id.

Public wallet portfolios

MNX wallet portfolio pages are designed to be shareable. The following read-only endpoints intentionally do not require authentication and may be queried for any wallet-linked account:

  • GET /v0/users/by-eoa/:eoa_address — resolve a wallet to its public profile and numeric user ID.
  • GET /v0/balance/:user_id/margin — current total, available, and reserved margin.
  • GET /v0/positions/by-user/:user_id/enriched — current positions and their derived value, margin, price, funding, equity, and profit-and-loss fields. A binary short's value counts one minus the mark price for each contract; see Binary positions.
  • GET /v0/portfolio/:user_id/realized-pnl — cumulative realized profit and loss, fees, funding, and net realized profit and loss.
  • GET /v0/portfolio/:user_id/realized-pnl-periods — the same realized profit-and-loss components for the portfolio's today, 24-hour, 7-day, and 30-day windows.
  • GET /v0/portfolio/:user_id/pnl-history — historical portfolio-value, margin, account-value, and profit-and-loss snapshots.

:user_id is the numeric user ID, not the wallet address. A value that is not a positive whole number gets a 400.

Cumulative fee and funding totals include only amounts recorded on or after September 19, 2026 (UTC); earlier values were not backfilled. The period endpoint uses an event ledger that begins on the same date, so windows reaching further back contain only subsequently recorded activity.

Anyone who knows a wallet address or numeric user ID can read these documented fields. Do not treat them as private. Per-user orders and fills, withdrawals, private WebSocket account topics, and other endpoints marked with a lock in the API reference still require authentication, and routes for a specific account enforce account ownership.

Placing orders requires request authentication and an order signature. Reading authenticated account data requires request authentication but not an order signature. See Authentication & signing and Placing orders.

Response conventions

CaseWhat you get
SuccessHTTP 200 with the documented response object as the JSON body — there is no wrapper envelope. Endpoints with no payload return { "success": true }.
ErrorAn HTTP error status (400, 401, 403, 404, 408, 409, 413, 415, 429, 500, 503) with a JSON body of { "message": "...", "details": ... }, where details is optional structured context. Validation errors list each problem as { field, error }. A JSON body over 100 KB gets 413, and one in an unsupported encoding or character set gets 415. Only some errors carry a machine-readable code; see error codes. A request to an unknown path gets a plain 404 page rather than JSON, and a request over the per-address request limit a plain 429 page.

Error codes

Branch on the HTTP status, and treat any machine-readable code as optional: the message is meant for people, and many errors carry no code. For read-after errors and orders, codes appear as follows:

  • Read-after errors carry details.code (see read-after-write bookmarks below).
  • Order placement: a refusal from the order book has details.reason, for example reduce_only_order_limit_reached. A market or limit order refused because its market cannot trade answers 409 with details.reason trading_halted_on_chain (a guardian halt) or bankruptcy_hold (a market order while the market resolves a bankruptcy). POST /v0/orders makes its margin, position, and trigger checks before the order reaches the order book, and those refusals have no reason. POST /v0/orders/place/batch leaves the margin and position checks to the order book, so in atomic mode they fail with a reason such as insufficient_margin or reduce_only_close_capacity_exceeded. On both routes, a conditional order whose trigger the mark price has already crossed normally gets a 400 with no reason; when the order book's own check catches it instead, the 400 has details.reason conditional_trigger_already_met.
  • During a planned maintenance pause, placement answers 409 with details.code EXCHANGE_MAINTENANCE and details.expected_resume_at_ms (Unix milliseconds, or null; a best_effort bulk item gets only error.code 409); retry after that time. In best_effort mode the batch result, rejected items included, is kept under its client_request_id for at least 10 minutes, so resending the same ID returns the same 409s, even after trading resumes. Send the rejected items with a new client_request_id. Reuse the original ID only within 10 minutes, to learn the outcome of a request whose response you did not receive; after that it may return the old result or be handled as a new request, so check your orders instead. Any other trading pause, and a market in cancel-only mode, answer 409 with no code.
  • A placement or cancel over your write rate limit answers 429 with details.error rate_limited and details.retry_after_ms, plus a Retry-After header.
  • POST /v0/orders/cancel/all with orders to cancel also draws on a budget every account shares. When it is spent the request gets the same 429, with details.subject_kind global; cancel by hash or in batches in the meantime.
  • POST /v0/account/invalidate-margin-nonces has a budget of 10 invalidations per user, refilled at 10 a day and shared by all of your API keys and sessions. Each call covers one market. A request over the budget gets the same 429, with details.op_class margin_nonce_invalidation.
  • Margin and leverage changes have a budget of 50 per user, refilled at 200 a day and shared by all of your API keys and sessions, across POST /v0/account/adjust-margin, POST /v0/account/adjust-leverage and the margin top-up of POST /v0/orders/place-with-top-up. A margin change counts when it is about to be signed, so one refused before then does not; a leverage change counts just before it cancels your orders on the market, and still counts if it is refused after that. A change over the budget gets a 429 with details.error rate_limited, details.retry_after_ms and details.op_class account_adjustment_daily; a leverage change refused for its budget cancels none of your orders.
  • POST /v0/orders/estimate and POST /v0/orders/calculate-quantity share a budget of 60 requests per user, refilled at 10 a second and shared by all of your API keys and sessions. POST /v0/orders/place-with-top-up draws 2 from the same budget, besides its placement limit. A request over the budget gets the same 429, with details.op_class order_sizing.
  • POST /v0/orders/cancel/by-hash/:order_hash answers 403 with details.error_code forced_order_not_cancellable when the order is a liquidation order MNX placed. Its other refusals have no error_code: 403 when the order belongs to another account, 404 when the order is no longer open.
  • POST /v0/orders/cancel/batch, and bulk placement in best_effort mode, report a refused order inside items[] instead of failing the request. A cancel item with status failed has an error_code (order_not_owned or forced_order_not_cancellable); a rejected placement item's error has only a code, which is an HTTP status, and a message.

Request limits per address

Requests are counted per client address before they reach the API, in addition to the write rate limit above. On production, keep each address under 5,000 requests a minute, and under 120 a minute in total to these market-data routes: GET /v0/market-bootstrap/:slug, GET /v0/markets/:market_id/candlesticks, GET /v0/fills/by-market/:market_id, and GET /v0/markets/stats. Keep requests to the order book, GET /v0/markets/:market_id/book, to at most 100 in any 10 seconds: an address that sends it more than 1,200 in a minute is refused the book until 5 minutes after it slows down. To follow a book live, keep it in sync over WebSockets rather than polling it. On testnet the overall limit is 10,000 a minute, the market-data limit is the same, and the order book has no limit of its own.

A request over any of these limits gets a 429 whose body is a short HTML page instead of the JSON error above. It has no Retry-After header and no cross-origin headers, so a browser reports it as a failed request rather than a 429. Slow down, and retry after a minute, or 5 minutes after slowing down once the order book has refused your address.

Raw integer amounts

Fields whose names end in _e18_raw are exact integers scaled by 1018, serialized as strings to avoid floating point rounding (they match the on-chain representation). For example, a balance of 1.5 USDM appears as "1500000000000000000". Many responses include both the raw field and a human-readable companion; when doing math on raw values, parse them as arbitrary-precision integers, never as floats.

Read-after-write bookmarks

The exchange's read endpoints are served from a read model that trails writes by a short interval. Responses to order placement, cancels, margin and leverage adjustments, and withdrawals can include a read_after object (shard_id, shard_seq). Pass its values back as read_after_shard_id and read_after_shard_seq query parameters on GET endpoints that accept them; the API reference lists those parameters per endpoint, and GET /v0/orderbook?symbol=... does not accept them. The read then waits until the read model reflects your write, giving read-your-own-writes consistency. Omitting the parameters returns the latest available data immediately.

The server decides how long to wait — up to 5 seconds — so there is no client-supplied timeout. If the wait does not finish in time you get an answer describing what to do next, and a client that treats these as fatal will stop on a read that was only going to need one more try. The code is in the error body's details.code field:

  • 409 with details.code HTV1_READ_AFTER_STALE_BOOKMARK — the bookmark cannot be placed at all (it names an owner this API does not serve, its sequence is not a whole number, or, on GET /v0/markets/:market_id/book, the exchange has not reached that sequence). Discard it and read again without it.
  • 503 with details.code HTV1_READ_AFTER_PENDING and a Retry-After header — the write has not landed in the read model yet. Keep the bookmark and ask again after the requested delay.
  • 503 with details.code HTV1_READ_AFTER_UNAVAILABLE — this process cannot establish projection freshness. Keep the bookmark and retry within your client's bounded read budget after the Retry-After delay (currently 2 seconds).
  • 429 with details.code HTV1_READ_AFTER_CAPACITY — the API is already holding as many waiting reads as it allows. Back off, then retry with the same bookmark.

Next steps