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.
Base URL:

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.
bash
curl -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-Limit
    • X-RateLimit-Remaining
    • X-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 text
Order( 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:
  • price must be on the 0.01 tick grid (a multiple of 1e16); an off-tick price is rejected with 400 (see Price Tick Size).
  • GTC orders must have expiration = 0.
  • GTD orders must have expiration > 0.
  • PO orders accept any expiration0 behaves GTC-style, >0 behaves GTD-style.
  • accept_by, when non-zero, must not already be in the past — a submission whose receive window has already passed is rejected with 400 (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 (where notional = amount * price / 1e18). For NegRisk markets the check is performed against the underlying collateral token (the network's configured collateral), 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 hash0x + 64 hex chars; or
  • a raw client_order_id — the value you passed to POST /orders (e.g. 17256719159), without the user prefix. The server reconstructs the namespaced id from the authenticated user, so cancelling by client_order_id requires 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 text
CancelAll( 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 to Yes and all others resolve to No (e.g. "Who will win the election?").
  • Non-NegRisk events (negRisk: false) — sibling markets resolve independently, so zero, one, or multiple siblings can resolve to Yes (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 previous ethEventId field. negRiskId is 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 as GET /markets items, including their own outcomes and externalSources), ordered by outcomeIndex.
  • 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)

javascript
import { 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:
  1. Connect — after the socket opens, send a connect command. Anonymous connections send empty params: { "connect": {}, "id": 1 }.
  1. Subscribe — once the connect reply arrives, send one subscribe command per channel: { "subscribe": { "channel": "…" }, "id": N }. Each command needs a unique incrementing id.
  1. Ping/pong — the server periodically sends an empty frame ({}); reply with an empty frame ({}) to keep the connection alive.
  1. Publications — channel data arrives as an async push with no id: { "push": { "channel": "…", "pub": { "data": { … } } } }. The payload you care about is push.pub.data.
A single WebSocket frame may pack several commands/replies joined by newlines (\n), so split on \n before parsing.
javascript
import 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:
javascript
client.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.
  • ts is 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=56
Every 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) or 1 (Order Book)
  • tradingModel: "amm" or "ob"
  • eventId: parent event UUID, or null for standalone markets
  • outcomeIndex: index within the parent event, or null
  • 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=true

GET /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=ob
Each 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=56
group_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 }, ...] }markets is sorted by market.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 text
Trader 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" } | |
  1. Place order — trader signs an EIP-712 order and sends it to POST /orders.
  1. Validation — the API verifies the signature, checks on-chain balance/allowance, and stores the order.
  1. Matching — the matcher service loads open orders, finds compatible pairs/sets, and calls the exchange contract.
  1. Settlement — the exchange contract atomically transfers shares and collateral on-chain.
  1. Sync — the API listens for OrdersMatched / OrderCancelled events 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.