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 detailGET /v0/markets/stats— 24h volume, open interest, mark price, and funding rate per marketGET /v0/markets/:market_id/book— the public order book: aggregated price levels the matcher can trade right nowGET /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
| Case | What you get |
|---|---|
| Success | HTTP 200 with the documented response object as the JSON body — there is no wrapper envelope. Endpoints with no payload return { "success": true }. |
| Error | An 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 examplereduce_only_order_limit_reached. A market or limit order refused because its market cannot trade answers 409 withdetails.reasontrading_halted_on_chain(a guardian halt) orbankruptcy_hold(a market order while the market resolves a bankruptcy).POST /v0/ordersmakes its margin, position, and trigger checks before the order reaches the order book, and those refusals have no reason.POST /v0/orders/place/batchleaves the margin and position checks to the order book, so inatomicmode they fail with a reason such asinsufficient_marginorreduce_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 hasdetails.reasonconditional_trigger_already_met. - During a planned maintenance pause, placement answers 409 with
details.codeEXCHANGE_MAINTENANCEanddetails.expected_resume_at_ms(Unix milliseconds, or null; abest_effortbulk item gets onlyerror.code409); retry after that time. Inbest_effortmode the batch result, rejected items included, is kept under itsclient_request_idfor at least 10 minutes, so resending the same ID returns the same 409s, even after trading resumes. Send the rejected items with a newclient_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.errorrate_limitedanddetails.retry_after_ms, plus aRetry-Afterheader. POST /v0/orders/cancel/allwith orders to cancel also draws on a budget every account shares. When it is spent the request gets the same 429, withdetails.subject_kindglobal; cancel by hash or in batches in the meantime.POST /v0/account/invalidate-margin-nonceshas 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, withdetails.op_classmargin_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-leverageand the margin top-up ofPOST /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 withdetails.errorrate_limited,details.retry_after_msanddetails.op_classaccount_adjustment_daily; a leverage change refused for its budget cancels none of your orders. POST /v0/orders/estimateandPOST /v0/orders/calculate-quantityshare 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-updraws 2 from the same budget, besides its placement limit. A request over the budget gets the same 429, withdetails.op_classorder_sizing.POST /v0/orders/cancel/by-hash/:order_hashanswers 403 withdetails.error_codeforced_order_not_cancellablewhen the order is a liquidation order MNX placed. Its other refusals have noerror_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 inbest_effortmode, report a refused order insideitems[]instead of failing the request. A cancel item with statusfailedhas anerror_code(order_not_ownedorforced_order_not_cancellable); a rejected placement item'serrorhas only acode, which is an HTTP status, and amessage.
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.codeHTV1_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, onGET /v0/markets/:market_id/book, the exchange has not reached that sequence). Discard it and read again without it. - 503 with
details.codeHTV1_READ_AFTER_PENDINGand aRetry-Afterheader — the write has not landed in the read model yet. Keep the bookmark and ask again after the requested delay. - 503 with
details.codeHTV1_READ_AFTER_UNAVAILABLE— this process cannot establish projection freshness. Keep the bookmark and retry within your client's bounded read budget after theRetry-Afterdelay (currently 2 seconds). - 429 with
details.codeHTV1_READ_AFTER_CAPACITY— the API is already holding as many waiting reads as it allows. Back off, then retry with the same bookmark.