This document describes the Order Book REST API endpoints exposed by the Myriad Protocol API. The Order Book replaces the AMM for order matching — traders sign EIP-712 orders off-chain, the API validates and stores them, and an on-chain matcher settles fills through the
MyriadCTFExchange contract.AuthenticationRate LimitingConceptsTrading ModelOrder Signing (EIP-712)Price and Amount ScalePrice Tick SizeSidesOutcomesTime-in-ForceOrder PriorityMatch TypesOrdersPOST /ordersGET /ordersGET /orders/:orderHashDELETE /orders/:orderHashPOST /orders/batchPOST /orders/batch-modifyPOST /orders/cancel-batchPOST /orders/cancel-allMarket DataGET /markets/:id/orderbookGET /markets/:id/tradesPositionsPOST /positions/splitPOST /positions/mergePOST /positions/redeemPOST /positions/redeem-voidedPOST /positions/neg-risk/splitPOST /positions/neg-risk/mergeEventsGET /eventsGET /events/:idGET /events/:id/orderbookGET /events/:id/actionsReal-time Updates (WebSockets)ConnectionUsing the client library (recommended)Raw WebSocket (no client library)ChannelsPayload conventionsOrder book deltaTradeSettlementPrice updateOrder updatePosition updateMarket eventExisting Endpoints with Order Book SupportGET /marketsGET /markets/:idGET /markets/:id/eventsGET /users/:address/portfolioGET /users/:address/marketsGET /users/:address/eventsOrder LifecycleErrorsNetworks
Base URL:
- Production:
https://api-v2.myriadprotocol.com/
Authentication
API access is public, with higher rate limits if an API key is provided.
Some endpoints require authentication and whitelisting, such as
/markets/quote_with_fee .How to authenticate your request:
- Header:
x-api-key: <your_api_key>
- Or Query:
?api_key=<your_api_key>
To obtain an API key, please reach out to the Myriad team.
bashcurl -H "x-api-key: YOUR_API_KEY" <https://api-v2.myriadprotocol.com/markets>
Rate Limiting
- Requests with API key - 200 requests/second per IP and/or API key.
- Requests w/o API key - 30 requests/10 seconds per IP
- Order placement: 200 orders per 10-second window per trader address (shared across single and bulk order endpoints).
- Headers included on responses:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
Concepts
Trading Model
By default, all endpoints return AMM-only data. To access Order Book data, you must explicitly pass the
trading_model query parameter.Value | Description |
amm | AMM markets only (default when parameter is omitted) |
ob | Order Book markets only |
all | Both AMM and Order Book markets |
Order Signing (EIP-712)
Orders are signed off-chain using the EIP-712 typed data standard. The signing domain is:
json{ "name": "MyriadCTFExchange", "version": "1", "chainId": "<chain_id>", "verifyingContract": "<exchange_contract_address>" }
The
Order struct:plain textOrder( address trader, uint256 marketId, uint8 outcomeId, uint8 side, uint256 amount, uint256 price, uint256 minFillAmount, uint256 nonce, uint256 expiration )
Price and Amount Scale
- Price: integer in
[1, 1e18]representing a fraction of 1 collateral token per share.0.50=500000000000000000.
- Amount: integer in the token's smallest unit (e.g. for 18-decimal tokens,
1e18= 1 share).
Price Tick Size
New orders must be priced on a 0.01 (1-cent) tick grid:
price must be a whole number of cents, i.e. a multiple of the tick size 1e16 (10000000000000000). Equivalently, price % 1e16 == 0.- ✅ Valid:
10000000000000000(0.01),570000000000000000(0.57),990000000000000000(0.99),1000000000000000000(1.00).
- ❌ Rejected:
575000000000000000(0.575 — finer than a cent),569999999999999936(a floating-point artifact for 0.57),1(1 wei).
An off-tick price is rejected with
400 and a details[] entry whose path is ["order","price"] (or ["orders",i,"order","price"] / ["place",i,"order","price"] in the batch endpoints).Sides
Value | Meaning |
0 | Buy — buying outcome shares |
1 | Sell — selling outcome shares |
Outcomes
Value | Meaning |
0 | Yes |
1 | No |
Time-in-Force
TIF | Behaviour |
GTC | Good-til-cancelled. Remains on the book until filled, cancelled, or the market closes. expiration must be 0. |
GTD | Good-til-date. Expires at the unix timestamp in expiration. expiration must be non-zero. |
FOK | Fill-or-kill. Must be fully filled in a single matcher run or it is cancelled. |
FAK | Fill-and-kill. Partial fill is allowed; the unfilled remainder is cancelled after the matcher run. |
PO | Post-only. Never fills as a taker — if the order would cross an older counterparty, the matcher cancels the entire order (no partial rest). expiration=0 rests indefinitely (GTC-style); expiration>0 rests until the timestamp (GTD-style). |
Order Priority
Makers are filled in price-time priority: best price first, and on price ties the oldest
createdAt fills first (strict FIFO).Match Types
The on-chain matcher supports three settlement modes:
Type | Description |
Direct | A BUY order is matched against a SELL order on the same outcome. Bid price >= ask price. |
Mint | Two BUY orders on opposite outcomes (YES + NO) whose prices sum to 1. New shares are minted from collateral. |
Merge | Two SELL orders on opposite outcomes whose prices sum to 1. Shares are burned and collateral is returned. |
Cross-market | (NegRisk only) N BUY-YES orders across all outcomes of an event whose prices sum to 1. |
Orders
POST /orders
Place a new order. The order is validated, the trader's signature is verified, on-chain balance/allowance is checked, and the order is persisted and pushed to the matcher.
Request body:
json{ "order": { "trader": "0x1234...abcd", "marketId": "42", "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "signature": "0x<130 hex chars>", "network_id": 56, "time_in_force": "GTC", "accept_by": "0", "client_order_id": "17256719159" }
Field | Type | Required | Description |
order.trader | address | yes | The signer's wallet address (40 hex chars, 0x-prefixed) |
order.marketId | uint string | yes | On-chain market ID |
order.outcomeId | 0 or 1 | yes | Outcome to trade |
order.side | 0 or 1 | yes | 0 = buy, 1 = sell |
order.amount | uint string | yes | Maximum number of shares (in wei). Must be > 0 |
order.price | uint string | yes | Price per share in [1, 1e18]. Must be on the 0.01 tick grid — a multiple of 1e16 (see Price Tick Size) |
order.minFillAmount | uint string | no | Minimum fill size (default "0") |
order.nonce | uint string | yes | Unique nonce for the order |
order.expiration | uint string | yes | Unix timestamp for GTD; "0" for GTC/FOK/FAK. PO accepts any value. |
signature | hex string | yes | EIP-712 signature ( 0x + 130 hex chars = 65 bytes) |
network_id | number | no | Network ID (defaults to server config) |
time_in_force | string | no | GTC (default), GTD, FOK, FAK, PO |
accept_by | uint string | no | API receive window: Unix-milliseconds timestamp by which this submission must reach the API to be accepted onto the book. If the request arrives after accept_by, it is rejected with 400 and the order is never placed. "0" (default) = no window. Values are interpreted as milliseconds; a seconds-scale value (below 1e12) is auto-detected and scaled up, so either unit works. This is an ingestion-time guard only (similar to Binance recvWindow) — it protects against acting on a stale request, not the order's lifetime once resting. To bound how long a resting order stays matchable, sign a GTD expiration instead. Not part of the signed order and not persisted. |
client_order_id | string | no | Client-supplied order identifier (1-64 chars: letters, digits, -, _). The server namespaces it by prepending the authenticated user's id first 4 chars + - (e.g. you send 17256719159, it is stored as 1ccd-17256719159). Globally unique; requires an API key (x-api-key). Use it to cancel via DELETE /orders/:orderHash. |
Validation rules:
pricemust be on the 0.01 tick grid (a multiple of1e16); an off-tick price is rejected with400(see Price Tick Size).
- GTC orders must have
expiration = 0.
- GTD orders must have
expiration > 0.
- PO orders accept any
expiration—0behaves GTC-style,>0behaves GTD-style.
accept_by, when non-zero, must not already be in the past — a submission whose receive window has already passed is rejected with400(the order is not placed).
- For buy orders (non-PO/FOK/FAK): the trader must have sufficient collateral balance and allowance on the exchange contract for
notional + fee(wherenotional = amount * price / 1e18). For NegRisk markets the check is performed against the underlying collateral token (the network's configuredcollateral), not a wrapped collateral — so traders only need to approve the underlying token.
- For sell orders (non-PO/FOK/FAK): the trader must hold enough outcome shares in the ConditionalTokens contract and have approved the exchange via
setApprovalForAll.
- FOK, FAK, and PO skip the upfront collateral check — the on-chain
matchOrders*call enforces allowance/balance at fill time.
Success response (
200):json{ "orderHash": "0x...", "status": "open", "timeInForce": "GTC", "clientOrderId": "1ccd-17256719159" }
clientOrderId is only present when a client_order_id was supplied.Error responses:
Status | Condition |
400 | Invalid payload, off-tick price (not a multiple of 1e16), market not open, insufficient balance/allowance, invalid signature, client_order_id supplied without an API key |
404 | Market not found |
409 | Order already exists (duplicate hash, or duplicate client_order_id) |
429 | Per-trader rate limit exceeded (200 orders / 10 seconds) |
500 | Server error or RPC failure |
GET /orders
List orders with optional filters.
Query parameters:
Param | Type | Description |
trader | address | Filter by trader wallet |
network_id | number | Filter by network |
market_id | number | Filter by on-chain market ID |
status | string | open, filled, cancelled, expired |
time_in_force | string | GTC, GTD, FOK, FAK, PO. Comma-separated for multiple (e.g. GTC,GTD) |
keyword | string | Case-insensitive substring match against the order's market title or its outcome title |
sort | string | Sort field: created_at (default), total, filled, market, resolving_soonest (see below) |
order | string | asc or desc (default desc) |
page | number | Page number (default 1) |
limit | number | 1-5000 (default 5000) |
offset | number | Legacy. Raw row offset. When supplied it takes precedence over page, and page in the response is derived from it. Prefer page. |
Sort fields:
Value | Sorts by |
created_at | Order creation time (default) |
total | Order notional ( price × amount) |
filled | Fill ratio ( filledAmount / amount) |
market | Market title (alphabetical) |
resolving_soonest | Market expiry date (markets without one sort last) |
Ties on the primary sort key are broken by
orderHash ascending, so pagination is deterministic. An invalid sort value returns 400.Response (
200):json{ "data": [ { "orderHash": "0x...", "clientOrderId": "1ccd-17256719159", "order": { "trader": "0x...", "marketId": 42, "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "status": "open", "filledAmount": "0", "timeInForce": "GTC", "createdAt": "2025-07-01T12:00:00.000Z" } ], "pagination": { "page": 1, "limit": 5000, "total": 137, "totalPages": 1, "hasNext": false, "hasPrev": false } }
The
data array items are unchanged; the pagination object is returned alongside them. With the default limit=5000 a trader's full position set is typically returned in a single page.GET /orders/:orderHash
Get a single order by its hash.
Response (
200):json{ "orderHash": "0x...", "clientOrderId": "1ccd-17256719159", "order": { "trader": "0x...", "marketId": 42, "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "status": "open", "filledAmount": "0", "timeInForce": "GTC", "networkId": 56, "createdAt": "2025-07-01T12:00:00.000Z", "updatedAt": "2025-07-01T12:00:00.000Z", "cancelledAt": null, "filledAt": null }
clientOrderId is null for orders placed without a client_order_id.Errors:
404 if order not found.DELETE /orders/:orderHash
Cancel an open order. Requires the original order + signature in the request body for ownership verification.
The path segment accepts either:
- an order hash —
0x+ 64 hex chars; or
- a raw
client_order_id— the value you passed toPOST /orders(e.g.17256719159), without the user prefix. The server reconstructs the namespaced id from the authenticated user, so cancelling byclient_order_idrequires an API key (x-api-key). The signed order in the body must still match the stored order.
The path type is auto-detected: anything matching
0x + 64 hex is treated as a hash; everything else as a client_order_id. The orderHash in the response is always the resolved order hash.Request body:
json{ "order": { "trader": "0x...", "marketId": "42", "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "signature": "0x<130 hex chars>", "network_id": 56 }
Success response (
200):json{ "orderHash": "0x...", "status": "cancelled" }
Error responses:
Status | Condition |
400 | Missing body, invalid payload, hash mismatch, invalid signature, order already filled/expired, client_order_id cancellation without an API key |
404 | Order not found |
POST /orders/batch
Place up to 200 signed orders in a single atomic request. Each entry is validated independently (signature, market lookup, duplicate check) and the valid entries are inserted via a single bulk SQL statement so the matcher is notified only once.
The on-chain collateral check is skipped for bulk placement — the exchange contract enforces allowance/balance at match time.
Request body:
json{ "orders": [ { "order": { "trader": "0x...", "marketId": "42", "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "signature": "0x<130 hex chars>", "time_in_force": "GTC" } ], "network_id": 56, "allow_partial": true }
Field | Type | Required | Description |
orders | array | yes | 1–200 {order, signature, time_in_force?, accept_by?} entries |
orders[].time_in_force | string | no | Per-entry TIF ( GTC, GTD, FOK, FAK, PO) |
orders[].accept_by | uint string | no | Per-entry API receive window (see POST /orders). "0" (default) = no window. |
network_id | number | no | Network ID (defaults to server config) |
allow_partial | boolean | no | true (default) accepts valid entries and reports per-entry errors; false rejects the whole batch with 400 if any entry fails |
Success response (
200):json{ "placed": ["0xabc...", "0xdef..."], "errors": [ { "index": 3, "orderHash": null, "reason": "Invalid order signature" } ] }
Error responses:
Status | Condition |
400 | Invalid payload (including any off-tick price — rejects the whole batch regardless of allow_partial), missing CLOB config, or allow_partial=false and any entry failed |
429 | Rate limit exceeded (per-trader budget is 200 orders / 10s) |
500 | Server error |
POST /orders/batch-modify
Cancel existing orders and place new orders in a single atomic request. The endpoint runs the cancel phase before the place phase (same DB round-trip order as the matcher's view), so the matcher can never observe both the old and the new orders simultaneously.
Request body:
json{ "cancel": [ { "order": { /* ... */ }, "signature": "0x..." } ], "place": [ { "order": { /* ... */ }, "signature": "0x...", "time_in_force": "GTC" } ], "network_id": 56, "allow_partial": true }
Field | Type | Required | Description |
place | array | no | Up to 200 orders to place ( {order, signature, time_in_force?, accept_by?} — same entry shape as POST /orders/batch) |
cancel | array | no | Up to 200 orders to cancel ( {order, signature} — no TIF) |
network_id | number | no | Network ID (defaults to server config) |
allow_partial | boolean | no | true (default) executes the valid subset; false rejects the entire call if any entry fails |
At least one of
place or cancel must be non-empty.Success response (
200):json{ "placed": ["0xabc..."], "cancelled": ["0xdef..."], "errors": [ { "phase": "place", "index": 2, "orderHash": null, "reason": "Market not found" } ] }
Error responses:
Status | Condition |
400 | Invalid payload (including any off-tick place price — rejects the whole request regardless of allow_partial; cancel entries are exempt), missing CLOB config, or allow_partial=false and any entry failed |
429 | Rate limit exceeded |
500 | Server error |
POST /orders/cancel-batch
Cancel multiple orders in a single request. Each order requires its original order data and signature for ownership verification.
Request body:
json{ "orders": [ { "order": { "trader": "0x...", "marketId": "42", "outcomeId": 0, "side": 0, "amount": "1000000000000000000", "price": "500000000000000000", "minFillAmount": "0", "nonce": "1", "expiration": "0" }, "signature": "0x<130 hex chars>" } ], "network_id": 56, "allow_partial": true }
Field | Type | Required | Description |
orders | array | yes | Array of {order, signature} objects (1-200 items) |
orders[].order | object | yes | Full order struct (same as POST /orders) |
orders[].signature | hex string | yes | EIP-712 signature for this order |
network_id | number | no | Network ID (defaults to server config) |
allow_partial | boolean | no | true (default) cancels valid entries and reports per-entry errors; false rejects the whole batch with 400 if any entry fails |
Success response (
200):json{ "cancelled": ["0xabc...", "0xdef..."], "errors": [ { "index": 2, "orderHash": "0x123...", "reason": "Order not found" } ] }
Error responses:
Status | Condition |
400 | Invalid payload, missing CLOB config, or allow_partial=false and any entry failed |
500 | Server error |
POST /orders/cancel-all
Cancel all open orders for a trader, optionally filtered by market. Requires an EIP-712
CancelAll signature to prove wallet ownership.EIP-712
CancelAll struct:plain textCancelAll( address trader, uint256 marketId, uint256 timestamp )
Set
marketId to 0 to cancel across all markets. The signing domain is the same as for Order (see Order Signing).Request body:
json{ "trader": "0x1234...abcd", "market_id": 42, "timestamp": "1719835200", "signature": "0x<130 hex chars>", "network_id": 56 }
Field | Type | Required | Description |
trader | address | yes | Trader wallet address |
market_id | number | no | On-chain market ID. Omit to cancel across all markets |
timestamp | uint string | yes | Current unix timestamp (must be within 5 minutes of server time) |
signature | hex string | yes | EIP-712 CancelAll signature |
network_id | number | no | Network ID (defaults to server config) |
Success response (
200):json{ "cancelled_count": 12, "market_ids_affected": ["uuid-...", "uuid-..."] }
Error responses:
Status | Condition |
400 | Invalid payload, bad signature, timestamp too old, missing CLOB config |
500 | Server error |
Market Data
GET /markets/:id/orderbook
Aggregated orderbook for an Order Book market outcome. Returns open, non-expired orders with remaining size, grouped by price level. Accepts either a numeric market ID (with
network_id) or a market slug.Query parameters:
Param | Type | Description |
network_id | number | Network ID (required when using numeric ID) |
outcome | 0 or 1 | Outcome to query (default 0) |
Response (
200):json{ "bids": [ ["500000000000000000", "3000000000000000000"], ["490000000000000000", "1500000000000000000"] ], "asks": [ ["510000000000000000", "2000000000000000000"], ["520000000000000000", "5000000000000000000"] ] }
Each entry is
[price, remaining_amount] as strings. Bids are sorted descending by price, asks ascending.GET /markets/:id/trades
Recent Order Book trades for a market, ordered by timestamp descending. Accepts either a numeric market ID (with
network_id) or a market slug.Query parameters:
Param | Type | Description |
network_id | number | Network ID (required when using numeric ID) |
outcome | 0 or 1 | Filter by outcome (optional) |
limit | number | 1-200 (default 50) |
page | number | Page number (default 1) |
Response (
200):json[ { "price": "0.5", "priceAfterFees": "0.51", "amount": "1000000000000000000", "side": "buy", "outcome": 0, "txHash": "0x...", "timestamp": 1719835200, "fees": { "total": "10000000000000000", "lp": "0", "treasury": "5000000000000000", "distributor": "5000000000000000" } } ]
side reflects the underlying action and may be buy, sell, split, or merge (mint/merge settlements surface as split/merge). price is the pre-fee execution price and priceAfterFees folds in the trade's fees; amount and all fees.* values are uint strings in the token's smallest unit.Positions
Position endpoints return transaction calldata (
{ to, calldata, value }) that the client signs and submits on-chain. All amounts are in the token's smallest unit (uint string, e.g. "1000000000000000000" for 1 token with 18 decimals).POST /positions/split
Split collateral into YES + NO outcome shares.
Request body:
json{ "market_id": 42, "amount": "1000000000000000000", "network_id": 56 }
Field | Type | Required | Description |
market_id | number | yes | On-chain market ID |
amount | uint string | yes | Collateral amount to split |
network_id | number | no | Network ID |
Response (
200):json{ "to": "0x<ConditionalTokens address>", "calldata": "0x...", "value": "0" }
POST /positions/merge
Merge YES + NO outcome shares back into collateral.
Request body: Same as
/positions/split.POST /positions/redeem
Redeem winning outcome shares for collateral after market resolution.
Request body:
json{ "market_id": 42, "network_id": 56 }
Field | Type | Required | Description |
market_id | number | yes | On-chain market ID |
network_id | number | no | Network ID |
POST /positions/redeem-voided
Redeem shares from a voided market at the market's voided payout ratios.
Request body: Same as
/positions/redeem.POST /positions/neg-risk/split
Split collateral into YES + NO shares for a specific outcome within a NegRisk event. The underlying collateral is wrapped into WCOL by the NegRiskAdapter.
Request body:
json{ "event_id": "0x<64 hex chars>", "outcome_index": 0, "amount": "1000000000000000000", "network_id": 56 }
Field | Type | Required | Description |
event_id | bytes32 hex | yes | NegRisk event ID |
outcome_index | number | yes | Index of the outcome within the event |
amount | uint string | yes | Underlying collateral amount |
network_id | number | no | Network ID |
POST /positions/neg-risk/merge
Merge YES + NO shares for a NegRisk outcome back into underlying collateral.
Request body: Same as
/positions/neg-risk/split.Events
Events group multiple binary Order Book markets under a single parent (e.g. "Who will win the election?" with outcomes A, B, C, ...). Each sibling market is its own binary market with
Yes / No outcomes. The negRisk flag determines the resolution semantics:- NegRisk events (
negRisk: true) — sibling markets are mutually exclusive: exactly one resolves toYesand all others resolve toNo(e.g. "Who will win the election?").
- Non-NegRisk events (
negRisk: false) — sibling markets resolve independently, so zero, one, or multiple siblings can resolve toYes(e.g. "Which teams will make the playoffs?").
GET /events
List all published events with nested sibling markets and aggregated metrics.
Query parameters:
Param | Type | Description |
network_id | number | Filter by network (optional) |
state | string | Filter by state (optional) |
Response (
200):Each item is an event row with:
type: "event"— discriminator (matches the shape produced by grouped endpoints elsewhere).
negRisk/negRiskId— replaces the previousethEventIdfield.negRiskIdis the on-chain NegRisk event id (bytes32 hex).
- Aggregated trading metrics (
volume,volume24h,volumeNotional,volumeNotional24h,liquidity,users,featured,featuredAt) summed/aggregated across the event's sibling markets.
markets— array of fully-serialized sibling markets (same shape asGET /marketsitems, including their ownoutcomesandexternalSources), ordered byoutcomeIndex.
externalSources— event-level external sources.
json{ "data": [ { "type": "event", "id": "uuid-...", "networkId": 56, "slug": "2028-election", "title": "Who will win the 2028 election?", "description": "...", "imageUrl": "https://...", "bannerImageUrl": "https://...", "state": "open", "resolvedOutcomeIndex": null, "expiresAt": "2028-11-05T00:00:00.000Z", "publishedAt": "2025-06-01T00:00:00.000Z", "negRisk": true, "negRiskId": "0x...", "volume": 123456.78, "volume24h": 1234.56, "volumeNotional": 234567.89, "volumeNotional24h": 2345.67, "liquidity": 9876.54, "users": 1234, "featured": false, "featuredAt": null, "externalSources": [], "markets": [ { "id": 42, "title": "Candidate A", "outcomeIndex": 0, "state": "open", "outcomes": [ /* ... */ ], "externalSources": [ /* ... */ ] /* ...full market shape... */ } ] } ] }
top-level
ethEventId, rules, createdAt, or the minimal outcomes array
({ marketId, ethMarketId, title, outcomeIndex, state, tokenId }). Use
negRiskId instead of ethEventId, and read sibling market data — including
tokenId — from markets[].outcomes[].GET /events/:id
Get a single published event by UUID or slug. Returns
404 if the event is unpublished or not found.Response (
200): A single event row with the exact same shape as one item in GET /events (the response is the event object directly, not wrapped in data).GET /events/:id/orderbook
Combined orderbook across all outcome markets in a published NegRisk event. Returns the orderbook per outcome market.
Response (
200):json{ "outcomes": [ { "marketId": "uuid-...", "ethMarketId": 42, "outcomeIndex": 0, "title": "Candidate A", "tokenId": "84", "orderbook": { "bids": [["500000000000000000", "1000000000000000000"]], "asks": [["520000000000000000", "2000000000000000000"]] } }, { "marketId": "uuid-...", "ethMarketId": 43, "outcomeIndex": 1, "title": "Candidate B", "tokenId": "86", "orderbook": { "bids": [], "asks": [] } } ] }
tokenId is the ERC1155 token id for the outcome ((ethMarketId << 1) | outcomeIndex).GET /events/:id/actions
Paginated trade actions across all outcome markets in an event, ordered by timestamp descending. Accepts an event UUID or slug.
Query parameters:
Param | Type | Description |
trading_model | amm, ob, all | Filter outcome markets by trading model (default amm) |
since | unix seconds | Only include actions at or after this timestamp |
until | unix seconds | Only include actions at or before this timestamp |
only_relevant | 1/true/yes | Exclude wash-trade actions (where relevant = false) |
page | number | Page number (default 1) |
limit | number | 1-100 (default 20) |
Response (
200):json{ "data": [ { "user": "0x...", "action": "buy", "marketTitle": "Candidate A", "marketSlug": "candidate-a", "marketId": 42, "networkId": 56, "outcomeTitle": "Yes", "outcomeId": 0, "imageUrl": "https://...", "shares": 1.5, "value": 0.75, "timestamp": 1719835200, "blockNumber": 12345678, "token": "0x<collateral address>", "txId": "0x..." } ], "pagination": { "page": 1, "limit": 20, "total": 137, "totalPages": 7, "hasNext": true, "hasPrev": false } }
The
action field is normalized: split is reported as buy and merge as sell. Maker-side actions are excluded (only role = 'taker' or null are returned).Errors:
404 if the event is not found.Real-time Updates (WebSockets)
Live order book, trade, order, position, price, and market updates are delivered over WebSocket. The realtime layer is powered by Centrifugo, which runs a small framing protocol on top of WebSocket (it is not plain "one JSON object per message").
You're not tied to JavaScript. The examples below happen to use JS, but Centrifugo maintains client SDKs for many stacks — JavaScript/TypeScript, Go, Python, Dart/Flutter, Swift, and Java/Android among them (see the full client SDK list). An SDK just handles the framing, reconnects, history recovery, and ping/pong for you.
No SDK is strictly required either: the protocol is a thin framing on top of WebSocket, so any language with a WebSocket client can implement it natively — the raw WebSocket example below does exactly that (in Node's
ws, but the same handshake applies anywhere).Connection
Environment | URL |
Staging | wss://ws.staging.myriadprotocol.com/ws |
Production | wss://ws.myriadprotocol.com/ws |
All channels are publicly subscribable — connect anonymously, no token or API key required. Just open the connection and subscribe to one or more channels.
Using the client library (recommended)
javascriptimport { Centrifuge } from 'centrifuge'; const client = new Centrifuge('wss://ws.myriadprotocol.com/ws'); const sub = client.newSubscription('orderbook:56:42'); sub.on('publication', (ctx) => console.log(ctx.data)); sub.subscribe(); client.connect();
Raw WebSocket (no client library)
If you'd rather use the native
ws library, you implement the Centrifugo bidirectional JSON protocol yourself. It's small:- Connect — after the socket opens, send a
connectcommand. Anonymous connections send empty params:{ "connect": {}, "id": 1 }.
- Subscribe — once the connect reply arrives, send one
subscribecommand per channel:{ "subscribe": { "channel": "…" }, "id": N }. Each command needs a unique incrementingid.
- Ping/pong — the server periodically sends an empty frame (
{}); reply with an empty frame ({}) to keep the connection alive.
- Publications — channel data arrives as an async push with no
id:{ "push": { "channel": "…", "pub": { "data": { … } } } }. The payload you care about ispush.pub.data.
A single WebSocket frame may pack several commands/replies joined by newlines (
\n), so split on \n before parsing.javascriptimport WebSocket from 'ws'; const ws = new WebSocket('wss://ws.myriadprotocol.com/ws'); const channels = ['orderbook:56:42', 'trades:56:42']; let nextId = 1; const send = (cmd) => ws.send(JSON.stringify(cmd)); ws.on('open', () => send({ connect: {}, id: nextId++ })); // 1. authenticate (anonymous) ws.on('message', (frame) => { for (const line of frame.toString().split('\n')) { if (!line) continue; const msg = JSON.parse(line); if (Object.keys(msg).length === 0) { send({}); continue; } // 3. ping → pong if (msg.connect) { // 2. connect reply → subscribe for (const channel of channels) send({ subscribe: { channel }, id: nextId++ }); continue; } if (msg.push?.pub) { // 4. channel data console.log(msg.push.channel, msg.push.pub.data); } } });
Channels
Subscribe to one or more of the channels below. The segment after the namespace is always
{networkId} (the numeric chain id), followed by either the on-chain {marketId} or the {trader} wallet address.Channel | Pattern | Payload | Description |
Order book | orderbook:{networkId}:{marketId} | Order book delta | Incremental price-level changes for a market (both outcomes) |
Trades | trades:{networkId}:{marketId} | Trade | One event per on-chain match transaction |
Trades (per trader) | trades:{networkId}:{marketId}:{trader} | Trade | Trades involving one trader; marketId or trader may be * (not both) |
Settlements | settlements:{networkId}:{marketId} | Settlement | Match broadcast on-chain, pre-confirmation (optimistic) |
Settlements (per trader) | settlements:{networkId}:{marketId}:{trader} | Settlement | One trader's settlements; marketId or trader may be * (not both) |
Prices | prices:{networkId}:{marketId} | Price update | Best bid/ask (and last) per outcome |
Orders | orders:{networkId}:{trader} | Order update | Lifecycle updates for a trader's orders |
Positions | positions:{networkId}:{trader} | Position update | Share-balance changes for a trader |
Markets | markets:{networkId} | Market event | Market created / resolved on a network |
History & recovery. Channels retain a short history buffer. Subscribe with
positioned: true, recoverable: true to receive a streamPosition on subscribed and have the client automatically replay missed publications after a reconnect:javascriptclient.newSubscription('trades:56:42', { positioned: true, recoverable: true });
Payload conventions
- Each Centrifugo publication carries the JSON object documented below as its
data.
- All numeric fields are decimal strings. Prices are
1e18-scaled (see Price and Amount Scale); amounts/shares and fees are in the token's smallest unit.
tsis a unix timestamp in milliseconds.
- Server-side, publishes are coalesced into ~100 ms windows, so a single message may batch several changes.
Order book delta
Incremental changes to aggregated price levels.
amount is the new total remaining at that level; "0" means the level was removed. Each message may include changes for both outcomes (disambiguated by outcome), mirroring the aggregation rules of GET /markets/:id/orderbook.json{ "marketId": 42, "networkId": 56, "ts": 1719835200123, "changes": [ { "outcome": 0, "side": "bid", "price": "500000000000000000", "amount": "3000000000000000000" }, { "outcome": 0, "side": "ask", "price": "510000000000000000", "amount": "0" } ] }
Trade
One event per on-chain match transaction. A single taker can fill against multiple makers in one transaction (
matchMultipleOrdersWithFees), so the maker legs are collapsed into a makers[] array and the taker side is reported as a volume-weighted taker aggregate.json{ "marketId": 42, "networkId": 56, "txHash": "0x...", "blockNumber": 12345678, "ts": 1719835200123, "taker": { "trader": "0x...", "outcome": 0, "side": "buy", "orderHash": "0x...", "totalAmount": "1000000000000000000", "averagePrice": "500000000000000000", "averagePriceAfterFees": "510000000000000000", "totalFees": { "total": "10000000000000000", "lp": "0", "treasury": "5000000000000000", "distributor": "5000000000000000" } }, "makers": [ { "trader": "0x...", "role": "maker", "outcome": 0, "side": "sell", "orderHash": "0x...", "price": "500000000000000000", "priceAfterFees": "500000000000000000", "amount": "1000000000000000000", "fees": { "total": "0", "lp": "0", "treasury": "0", "distributor": "0" } } ] }
side may be buy, sell, split, or merge (mint/merge settlements surface as split/merge).Settlement
Fired the instant the matcher broadcasts the settlement transaction, before it is confirmed on-chain — the optimistic counterpart to Trade, which only lands once the block is mined. Market-makers can act on a fill as soon as it's irreversibly committed rather than waiting out the block time.
Unlike
trades:, settlements carry no fees or prices (those are only known once the on-chain OrdersMatched logs are read) — just the participating orders and their fill amounts in a flat legs[] array (one taker leg + N maker legs for a direct match; all-taker legs for a cross-market neg-risk fill). Reconcile against the matching Trade on the same txHash once it arrives.status is always "submitted" today; it's reserved so a confirmed/failed lifecycle can be added later without breaking subscribers. matchType is "direct" (one taker vs N makers in a single market) or "cross_market" (a neg-risk N-way fill, published once per touched market).json{ "networkId": 56, "marketId": 42, "txHash": "0x...", "matchType": "direct", "status": "submitted", "ts": 1719835200123, "legs": [ { "trader": "0x...", "role": "taker", "orderHash": "0x...", "outcome": 0, "side": "buy", "fillAmount": "1000000000000000000" }, { "trader": "0x...", "role": "maker", "orderHash": "0x...", "outcome": 0, "side": "sell", "fillAmount": "1000000000000000000" } ] }
side reports each order's own side (buy / sell); the split/merge framing and realised fees surface on the confirmed Trade event.Price update
Best bid/ask per outcome (including binary-implied values derived from the opposite outcome). Fields are omitted when there is no resting liquidity on that side.
json{ "networkId": 56, "marketId": 42, "ts": 1719835200123, "outcomes": [ { "outcome": 0, "bestBid": "500000000000000000", "bestAsk": "510000000000000000", "last": "505000000000000000" }, { "outcome": 1, "bestBid": "490000000000000000", "bestAsk": "500000000000000000" } ] }
Order update
Lifecycle transitions for an order, published to the owning trader's channel.
json{ "type": "PARTIALLY_FILLED", "orderHash": "0x...", "networkId": 56, "marketId": 42, "trader": "0x...", "side": "buy", "outcome": 0, "price": "500000000000000000", "amount": "1000000000000000000", "filledAmount": "400000000000000000", "status": "open", "timeInForce": "GTC", "matchTxHash": "0x...", "createdAt": "2025-07-01T12:00:00.000Z", "updatedAt": "2025-07-01T12:00:05.000Z" }
Field | Description |
type | PLACED, PARTIALLY_FILLED, FILLED, CANCELLED, or EXPIRED |
status | Current persisted status: open, filled, cancelled, or expired |
matchTxHash | Settlement tx hash (present on fill events only) |
Position update
Share-balance changes for a trader, emitted per fill leg as well as for redeem/split/merge.
json{ "networkId": 56, "marketId": 42, "trader": "0x...", "outcome": 0, "delta": "+1000000000000000000", "balance": "3000000000000000000", "averageEntryPrice": "500000000000000000", "reason": "fill", "txHash": "0x...", "ts": 1719835200123 }
Field | Description |
delta | Signed share change from this event ( "+…" gained, "-…" lost) |
balance | New on-chain balance after the event (optional; refetch via REST if absent) |
reason | fill, redeem, split, or merge |
Market event
json{ "type": "resolved", "networkId": 56, "marketId": 42 }
type is created or resolved; additional event-specific fields may be present.Existing Endpoints with Order Book Support
The following existing endpoints support Order Book markets via the
trading_model query parameter. By default (no parameter), all endpoints return AMM-only data. You must pass trading_model=ob to access Order Book data, or trading_model=all for both.Param | Values | Default | Description |
trading_model | amm, ob, all | amm | Filter by trading model |
GET /markets
Returns markets filtered by trading model.
Example:
GET /markets?trading_model=ob&network_id=56Every serialized market (here and in
GET /markets/:id, GET /events, GET /questions, and the portfolio endpoints) carries the trading-model / event fields that identify Order Book markets:executionMode:0(AMM) or1(Order Book)
tradingModel:"amm"or"ob"
eventId: parent event UUID, ornullfor standalone markets
outcomeIndex: index within the parent event, ornull
negRisk: whether the market belongs to a NegRisk (mutually-exclusive) event
externalSources: array of{ id, providerName, externalMarketId, externalMarketUrl, externalMarketTitle }(may be empty)
outcomes[].tokenId: ERC1155 token id ((ethMarketId << 1) | outcomeId) used when signing orders and reading on-chain balances
group_by_event (boolean, default false) — when set to true, sibling markets that belong to the same NegRisk event are collapsed into a single event row (same shape as GET /events items, with type: "event"). Standalone markets (no event_id) appear as { type: "market", ...market }. Pagination operates on logical rows. Filters such as state, keyword, topics, in_play, moneyline, ids, min_duration, max_duration, and the fee filters apply at the sibling level — an event row is included if any of its siblings match. Network/token/visibility filters always apply per market.Example:
GET /markets?trading_model=ob&group_by_event=trueGET /markets/:id
Returns a single market. Requires
trading_model=ob or trading_model=all to return Order Book markets.GET /markets/:id/events
Returns market events. Requires
trading_model=ob or trading_model=all to include Order Book market events.GET /users/:address/portfolio
Returns user portfolio positions. Pass
trading_model=ob to get Order Book positions, or trading_model=all for both.Example:
GET /users/0x.../portfolio?trading_model=obEach position now also carries an
eventId field (string | null) so clients can correlate positions to NegRisk events without a follow-up lookup.group_by_event (boolean, default false; accepts true/1) — when set, positions that share an event_id are folded into a single event row. The response items become a discriminated union:{ type: "event", id, networkId, slug, title, description, imageUrl, bannerImageUrl, state, resolvedOutcomeIndex, expiresAt, publishedAt, negRisk, negRiskId, externalSources, totals: { value, profit, positionCount }, markets: [...positions] }
{ type: "market", ...position }for standalone markets.
Pagination is over grouped rows.
GET /users/:address/markets
Returns user markets with position data. Pass
trading_model=ob to get Order Book positions, or trading_model=all for both. Includes on-chain balances from the ConditionalTokens contract.Example:
GET /users/0x.../markets?trading_model=ob&network_id=56group_by_event (boolean, default false) — when set, sibling markets the user has activity in are collapsed into a single event row. Items become a discriminated union of:{ type: "event", id, networkId, slug, title, ..., negRisk, negRiskId, externalSources, totals: { value, profit, positionCount, liquidityValue }, markets: [{ market, portfolio }, ...] }—marketsis sorted bymarket.outcomeIndex.
{ type: "market", market, portfolio }for standalone markets.
Pagination is over grouped rows (computed in SQL via
COUNT(*) OVER ()).GET /users/:address/events
Returns user action events. Pass
trading_model=ob to filter to Order Book actions only.Order Lifecycle
plain textTrader API Matcher (on-chain) | | | | POST /orders (signed) | | | --------------------------► | | | | validate, verify sig, | | | check balance/allowance | | | insert into orders table | | ◄─ { orderHash, status } | | | | NOTIFY orders_changed | | | ────────────────────────────────► | | | | load open orders | | | find matches | | | call exchange contract | | | (matchMultipleOrdersWithFees | | | or matchCrossMarketOrders) | | ◄──── OrdersMatched event ────── | | | update orders + actions | | | | | GET /orders/:hash | | | --------------------------► | | | ◄─ { status: "filled" } | |
- Place order — trader signs an EIP-712 order and sends it to
POST /orders.
- Validation — the API verifies the signature, checks on-chain balance/allowance, and stores the order.
- Matching — the matcher service loads open orders, finds compatible pairs/sets, and calls the exchange contract.
- Settlement — the exchange contract atomically transfers shares and collateral on-chain.
- Sync — the API listens for
OrdersMatched/OrderCancelledevents and updates order statuses.
Errors
Common errors across all Order Book endpoints:
Status | Condition |
400 | Invalid request parameters or validation failure |
401 | Missing or invalid API key |
404 | Resource not found |
409 | Duplicate order |
429 | Rate limit exceeded |
500 | Internal server error |
Networks
Order Book markets are currently deployed on BNB Smart Chain. See the Contract Addresses.