Building with an AI Agent or LLM?
The Myriad CLI is the best, fastest, way to get your agent trading on Myriad Markets.
npm install -g @myriadmarkets/cliThis document describes the public REST API exposed by the Myriad Protocol API service — both the AMM endpoints and the Order Book (orders, positions, events, and real-time WebSocket updates).
Base URLAuthenticationLegacy auth sunset — August 15, 2026Getting your first API keySession JWT (Sign-In With Ethereum) — for first-party appsAPI key + secret (HMAC) — for agents / market makersWebSocket (Centrifugo) subscription tokensRate LimitingPaginationMarketsGET /marketsGET /markets/:idGET /markets/:id/eventsGET /markets/:id/referralsGET /markets/:id/holdersGET /markets/:id/orderbookGET /markets/:id/tradesPOST /markets/quotePOST /markets/quote_with_feePOST /markets/claimUsersGET /users/:address/eventsGET /users/:address/referralsGET /users/:address/portfolioGET /users/:address/marketsOrder Book ConceptsTrading ModelOrder Signing (EIP-712)Signature TypesPrice and Amount ScalePrice Tick SizeSidesOutcomesTime-in-ForceOrder PriorityMatch TypesOrder LifecycleOrdersPOST /ordersGET /ordersGET /orders/:orderHashDELETE /orders/:orderHashPOST /orders/batchPOST /orders/batch-modifyPOST /orders/cancel-batchPOST /orders/cancel-allPositionsPOST /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 eventTagsGET /tagsTopicsGET /topicsPrice DataErrorsNetworksBNB Smart ChainOther chainsAbstractLineaCeloAuditsOrder BookAMMChangelogV2.0.0V2.0.1V2.0.2V2.0.3V2.0.4
Base URL
- Production -
https://api-v2.myriadprotocol.com/
Authentication
Most read endpoints remain public, with higher rate limits if an API key is provided. Some endpoints require authentication and whitelisting, such as
/markets/quote_with_fee.Order endpoints, the
/user/* reads, and per-trader WebSocket subscriptions require authentication tied to your account's connected wallet. Unauthenticated requests to those endpoints return 401 on/after the cutover date — August 15, 2026 (see Legacy auth sunset). As part of this change, GET /orders returns only your own orders (the ?trader= parameter is deprecated). The /users/:address/* reads stay open — anyone can read any address; /user/* offers the same reads self-scoped to your connected wallet.An account is identified by a wallet address and/or an email and can hold multiple API keys. The connected wallet is the wallet that authenticated the account via SIWE (see below); wallet-scoped endpoints operate on it. An account that has never completed a wallet sign-in (e.g. created by email) has no connected wallet — wallet-scoped endpoints then return
409 { "code": "NO_WALLET_CONNECTED" }, resolved by completing a SIWE login with the wallet once.Legacy auth sunset — August 15, 2026
The transitional "soft window" ends on August 15, 2026. Until then, legacy and unauthenticated access patterns keep working but every response served through them carries
Deprecation: true and Sunset: <date> headers. From the cutover, they are rejected. What changes, per surface:Surface | Until Aug 15, 2026 (soft window) | From Aug 15, 2026 |
Bare API keys — x-api-key header or ?api_key= with no HMAC signature, on any authenticated endpoint | Accepted, with deprecation headers | Rejected: 401 { "code": "SIGNATURE_REQUIRED" }. Every keyed request must be HMAC-signed (x-api-key + x-api-timestamp + x-api-signature) or use a session JWT |
Order endpoints without a connected wallet — GET /orders, GET /orders/:orderHash, POST /orders, DELETE /orders/:orderHash, POST /orders/batch, POST /orders/batch-modify, POST /orders/cancel-batch, POST /orders/cancel-all | Unscoped (legacy) access allowed, with deprecation headers | A connected wallet is required: no credential → 401; authenticated but no wallet on the account → 409 { "code": "NO_WALLET_CONNECTED" }. Orders whose trader differs from your connected wallet are rejected (403 WALLET_MISMATCH; GET /orders/:orderHash returns 404) |
Not affected by the date: the
/users/:address/* reads (anyone can read any address, before and after), the /user/* reads (already require a connected wallet today), and most market-data reads (/markets, /events, orderbook, trades) remain public.To migrate before the cutover:
- Replace bare-key integrations with HMAC signing (see API key + secret). Keys issued before secrets existed have no secret and cannot be upgraded — mint a new key via
POST /auth/api-keysor Myriad Account settings and retire the old one.
- Connect a wallet to the account by completing one SIWE login (
POST /auth/login) with the wallet your orders trade as — API keys minted from that session act on behalf of that wallet.
- Drop the
?trader=parameter onGET /orders(it must match your connected wallet anyway).
- Watch for
Deprecation/Sunsetresponse headers in the meantime — any request still receiving them will break at the cutover.
Getting your first API key
Every credential starts with a wallet sign-in (SIWE). Logging in creates your account on first use and permanently ties it to your wallet. API keys are then minted from that session and act on behalf of that wallet — there is no separate "link key to wallet" step.
plain text1. GET /auth/nonce?address=0xYourWallet → { nonce, message } 2. Sign `message` with the wallet # personal_sign — any wallet lib / MetaMask 3. POST /auth/login { message, signature } → { accessToken, ... } # account auto-created 4. POST /auth/api-keys { "label": "my agent" } → { key, secret } # Authorization: Bearer <accessToken>
Agents keep the
key + secret from step 4 and HMAC-sign every request from then on — the wallet sign-in was only needed once, to mint the key. First-party apps can skip step 4 entirely and keep using the session token. Keys can also be created without touching the API, from Myriad Account settings.The two credential types in detail:
Session JWT (Sign-In With Ethereum) — for first-party apps
plain text1. GET /auth/nonce?address=0xYourWallet → { nonce, message } 2. Sign `message` with the wallet (personal_sign). 3. POST /auth/login { message, signature } → { accessToken, refreshToken, user } 4. Use it: Authorization: Bearer <accessToken> 5. POST /auth/refresh { refreshToken } → new tokens; POST /auth/logout { refreshToken }
Access tokens are short-lived (default 15m); refresh tokens are long-lived (default 30d), rotated on use, and safe for the frontend to persist.
Nonces expire after 10 minutes and are single-use. A nonce is consumed on successful login, and also by any smart-contract-wallet (EIP-1271) verification attempt — successful or not — so after a failed SCW attempt, request a fresh nonce and re-sign. A signature that fails plain EOA recovery does not consume the nonce.
Smart-contract wallets (SCW):
POST /auth/login accepts EIP-1271 signatures as well as EOA signatures. It first attempts standard ECDSA recovery; if that fails, it verifies the signature via EIP-1271 (isValidSignature) against the address in the SIWE message. This lets a smart-contract wallet sign in with the same flow — no extra parameter is required.API key + secret (HMAC) — for agents / market makers
Long-lived, never needs re-auth mid-session. A key is bound to the account — and therefore the connected wallet — of the session that created it: it reads and trades as that wallet. Create one with a session JWT (see above), or via Myriad Account settings:
plain textPOST /auth/api-keys { "label": "MM agent prod" } → { id, key, secret, ... } # secret shown ONCE GET /auth/api-keys # list (never returns secrets) DELETE /auth/api-keys/:id # revoke
Sign each request:
X-Signature = HMAC-SHA256(secret, "<timestamp>.<METHOD>.<path+query>.<rawBody>").javascriptimport crypto from 'crypto'; const ts = Math.floor(Date.now() / 1000).toString(); const sig = crypto.createHmac('sha256', SECRET) .update(`${ts}.POST./orders.${JSON.stringify(body)}`) .digest('hex'); await fetch('https://api-v2.myriadprotocol.com/orders', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': KEY, 'x-api-timestamp': ts, 'x-api-signature': sig }, body: JSON.stringify(body), });
- The timestamp must be within ±30s of server time (replay protection).
pathincludes the query string;rawBodyis the exact bytes sent (empty for GET).
- A legacy bare
x-api-key(no signature) is accepted until August 15, 2026, then rejected — see Legacy auth sunset.
A key may be configured by the Myriad team with
bypass_signature_verification to skip the
server-side EIP-712 order-signature check on order create/cancel for faster ingestion. The order
signature is still required in every request, is still stored, and is still verified
on-chain at settlement — only the redundant API-side check is skipped, and only for the wallet
connected to that key. Reach out to the Myriad team if your agent needs this.WebSocket (Centrifugo) subscription tokens
Public channels (
orderbook:*, trades:*, settlements:*, prices:*, markets:*) are open. The per-trader orders:* / positions:* channels require a short-lived token, issued only for channels belonging to your connected wallet:plain textPOST /auth/centrifugo/subscription-token { "channel": "orders:97:0xyourwallet" } → { token, expiresIn }
Connect to Centrifugo anonymously and supply the token via the SDK's per-subscription
getToken callback so it auto-refreshes on expiry.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
Pagination
All list endpoints support pagination:
page(default: 1)
limit(default: 20, max: 100)
Response pagination object:
json{ "pagination": { "page": 1, "limit": 20, "total": 123, "totalPages": 7, "hasNext": true, "hasPrev": false } }
Markets
GET /markets
Paginated list of markets with filtering and sorting.
Query params:
page,limit(pagination)
trading_model:amm|ob|all(default:amm). AMM-only by default — passobfor Order Book markets orallfor both (see Order Book Concepts)
sort:volume|volume_24h|volume_notional|volume_notional_24h|liquidity|expires_at|published_at|in_play_starts_at|featured(default:volume)
order:asc|desc(default:desc)
network_id: comma-separated list of network ids (e.g.2741,59144)
state:open|closed|resolved
token_address: comma-separated list of ERC20 token addresses
oracle_address: comma-separated list of oracle contract addresses (matched case-insensitively)
topics: comma-separated list of topics
tags: tag slug(s) to filter by. Repeat the param to AND multiple groups; comma-separate within a value to OR (e.g.tags=world-cup&tags=stage-a,stage-b→world-cup AND (stage-a OR stage-b))
exclude_tags: comma-separated tag slugs to exclude (a market matching ANY is filtered out)
keyword: full-text search acrosstitle,description, and outcome titles
ids: comma-separated list of on-chain market ids
event_id: filter to markets belonging to a single event (UUID)
in_play: boolean (true/false/1/0)
moneyline: boolean (true/false/1/0)
min_duration: minimum market duration in seconds (filters byexpiresAt - publishedAt)
max_duration: maximum market duration in seconds (filters byexpiresAt - publishedAt)
in_play_starts_at_gte: unix seconds (inclusive) — only markets withinPlayStartsAt >=this timestamp; markets withoutinPlayStartsAtare excluded
in_play_starts_at_lte: unix seconds (inclusive) — only markets withinPlayStartsAt <=this timestamp; markets withoutinPlayStartsAtare excluded
group_by_event: boolean (true/false/1/0). Whentrue, sibling markets sharing an event are collapsed into a single event row ({ type: "event", ... }); standalone markets are returned as{ type: "market", ...market }. Pagination then operates on logical rows. Event rows for sports carryscoreboard(the match scoreboard, same shape as the market field above) at the event level; child markets inmarkets[]are ordered canonically (matchingoutcome0/outcome1), so align them positionally. Filters such asstate,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.
Fee filters
Filter markets by protocol fees (decimals between 0 and 1). Fees can be filtered separately for buy and sell, for individual components or by total.
- Components:
lp(liquidity provider fee, fieldfee)dt(distributor fee, fielddistributor_fee)tr(treasury fee, fieldtreasury_fee)total=fee + treasury_fee + distributor_fee
- Actions:
buy,sell
- Operators:
lt,lte,gt,gte,eq
- Parameter patterns:
- Component-specific:
{action}_{component}_fee_{operator}(e.g.,buy_lp_fee_lte=0.01) - Total:
{action}_fee_{operator}(e.g.,sell_fee_lt=0.02)
- Examples:
buy_lp_fee_lte=0.01→ buy LP fee ≤ 1%sell_tr_fee_gte=0.0025→ sell treasury fee ≥ 0.25%buy_dt_fee_eq=0.003→ buy distributor fee = 0.3%sell_fee_lt=0.02→ total sell fee < 2%
- Notes:
- Multiple fee filters are combined with AND.
- All fee values are decimals (e.g., 0.01 = 1%).
Example:
plain textGET /markets?keyword=eth&network_id=2741&page=1&limit=20&sort=volume_24h
Response data (per market):
id: blockchain market id
networkId: number
slug,title,shortName,ctaName,description
publishedAt,expiresAt,resolvesAt
fees,state,voided,resolvedOutcomeId,topics,tags,resolutionSource,resolutionTitle
token:{ address, symbol, name, decimals }
imageUrl,bannerImageUrl,ogImageUrl
liquidity,liquidityPrice,volume,volume24h,volumeNotional,volumeNotional24h,users,shares
featured,featuredAt,inPlay,inPlayStartsAt,perpetual,moneyline
oracle: always-present object{ address, name, url, image_url, data }.address/dataarenullfor markets Myriad resolves itself;name/url/image_urlare always populated (brand metadata).
scoreboard: live-scores scoreboard for standalone sports markets, elsenull. For event-bound markets the scoreboard lives on the parent event (seeGET /events/:id), not the child. Only fields with data are present — a soccer card omitsperiodScores, a pre-game card omitsclock, etc. Always present:type,status,outcome0,outcome1,updatedAt. Shape:type: sport slug, e.g."soccer"/"tennis"— the frontend picks a card layout from thisstatus:"scheduled" | "live" | "finished";statusDetail: provider status text (e.g."1st Half"), present when knownstartsAt: ISO kickoff time, present when knownclock:{ period?, minute?, stopped }— present only while live and carrying time. Animate the minute locally between refreshes.outcome0/outcome1: the two match sides (always exactly two), each{ name, ctaName?, score?, imageUrl? }(optional keys present only when available).outcome0is the home team / first player;name/ctaName/imageUrlprefer the Myriad market/event data (outcome title, CTA label, outcome image) and fall back to the provider's participant data. Betting outcomes are created in canonical order matching these (home/away or player1/player2), so align rows positionally.periodScores: array of{ period, outcome0, outcome1 }(e.g. tennis games per set), present only when the sport has themvenue:{ name, location? }— the match venue, present when knownupdatedAt: ISO timestamp of when the score was last captured (score freshness)
externalSources: array of{ id, providerName, externalMarketId, externalMarketUrl, externalMarketTitle }(may be empty)
topHolders: array (may be empty)
outcomes: array with:id(on-chain outcome id)titleprice,closingPrice,priceChange24hbestBid,bestAsk: best resting order per side as a0–1decimal (binary-implied from the opposite outcome included).nullwhen there is no order on that side, or when the market is not open. UsebestAskto price a buy without waiting on a separate order-book fetch.shares,sharesHeldholdersimageUrltokenId: ERC1155 token id ((ethMarketId << 1) | outcomeId)
Order Book / event fields (present on every market regardless of trading model — see Order Book Concepts):
executionMode:0(AMM) or1(Order Book)
tradingModel:"amm"or"ob"(string form ofexecutionMode)
eventId: parent event UUID, ornullfor standalone markets
outcomeIndex: index within the parent event, ornull
negRisk: boolean — whether the market belongs to a NegRisk (mutually-exclusive) event
Notes:
- Numeric monetary/decimal fields are returned as numbers.
GET /markets/:id
Get a single market by slug or by
marketId + network_id.Modes:
- By slug:
GET /markets/{slug}
- By id + network:
GET /markets/{marketId}?network_id=2741
Requires
trading_model=ob or trading_model=all to return Order Book markets (AMM-only by default).Price charts:
- Field
outcomes[*].price_charts.
- Timeframes and buckets
24h: 5-minute (max 288)7d: 30-minute (max 336)30d: 4-hour (max 180)all: 4-hour
- Series end at
min(now, expiresAt)with backfill from the last known price before the window start.
Example:
plain textGET /markets/164?network_id=2741
GET /markets/:id/events
Paginated actions (trades/liquidity/claims) for a market, ordered by
timestamp desc.Lookup:
- By slug:
GET /markets/{slug}/events
- By id + network:
GET /markets/{marketId}/events?network_id=2741
Query params:
page,limit
trading_model:amm|ob|all(defaultamm) — passoborallto include Order Book market events
since: unix seconds (inclusive)
until: unix seconds (inclusive)
only_relevant:1/true/yesto exclude wash-trade actions (whererelevant = false)
Response items:
user: wallet address
action:buy|sell|add_liquidity|remove_liquidity|claim_winnings|claim_liquidity|claim_fees|claim_voided
marketTitle,marketSlug,marketId,networkId
outcomeTitle,outcomeId
imageUrl
shares,value: numbers
timestamp: unix seconds
blockNumber: number
token: ERC20 token address used for this market
txId: transaction hash
Example:
plain textGET /markets/164/events?network_id=2741&since=1755600000&until=1755800000&page=1&limit=50
GET /markets/:id/referrals
Paginated referrals for a market, ordered by
timestamp desc.Lookup:
- By slug:
GET /markets/{slug}/referrals
- By id + network:
GET /markets/{marketId}/referrals?network_id=2741
Query params:
page,limit
since: unix seconds (optional, inclusive)
until: unix seconds (optional, inclusive)
code: referral code (optional)
Response items (camelCase):
user: wallet address
action:buy|sell(referral trade type)
marketTitle,marketSlug,marketId,networkId
outcomeTitle,outcomeId
imageUrl
value: number (trade value that generated referral fees)
timestamp: unix seconds
blockNumber: number
token: ERC20 token address used for this market
code: referral code used
fees:lp(number): LP fee portion attributed to this referraltreasury(number): treasury fee portiondistributor(number): distributor/referrer fee portion
Example:
plain textGET /markets/164/referrals?network_id=2741&since=1755600000&until=1755800000&page=1&limit=50
GET /markets/:id/holders
Market holders grouped by outcome. Aggregates buy/sell actions to compute net shares per user for each outcome, filters holders with at least 1 share, orders by shares, and applies the
limit per outcome.Lookup:
- By slug:
GET /markets/{slug}/holders
- By id + network:
GET /markets/{marketId}/holders?network_id=2741
Query params:
page,limit(pagination; limit applies per outcome)
network_id(required when using marketId)
Response data (per outcome):
outcomeId: number
outcomeTitle: string | null
totalHolders: total addresses with ≥ 1 share in this outcome
holders: array limited per outcome with:user: addressshares: number
Pagination notes:
totalequals the maximumtotal_holdersacross outcomes for this market.
totalPages = ceil(max(total_holders) / limit);limitis applied per outcome.
Example:
plain textGET /markets/164/holders?network_id=2741&page=1&limit=50
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.POST /markets/quote
Get a trade quote and transaction calldata for a specific market outcome.
- Method: POST
- Path:
/markets/quote
- Body: JSON
Request body:
- Exactly one of the following is required (send only one):
market_id(number): on-chain market id +network_id(number): network idmarket_slug(string)
outcome_id(number, required): on-chain outcome id
action(string, required):buy|sell
- Exactly one of the following is required (send only one):
value(number): amount of tokens to spend (buy) or receive (sell)shares(number): number of shares to buy or sell
slippage(number, optional, default 0.005): between 0 (0%) and 1 (100%)
Validation rules:
- For buy: provide only
value;sharesmust be omitted.
- For sell: provide exactly one of
valueorshares.
- Market must exist and be
open.
- Market must have at least 2 outcomes and sufficient shares/liquidity.
Response body:
value(number): input value used for the calculation
shares(number): expected shares bought/sold
shares_threshold(number): min acceptable based onslippage(for buy, min shares; for sell, min value)
price_average(number): average execution price
price_before(number): price before the trade
price_after(number): price after the trade
calldata(string): hex-encoded calldata for the contract
net_amount(number): value after protocol/treasury/distributor fees
fees:treasury(number)distributor(number)fee(number)
Example request (buy by value):
json{ "market_id": 164, "outcome_id": 0, "network_id": 2741, "action": "buy", "value": 100, "slippage": 0.01 }
Example success response:
json{ "value": 100, "shares": 312.3456, "shares_threshold": 309.2221, "price_average": 0.3201, "price_before": 0.315, "price_after": 0.3252, "calldata": "0x...", "net_amount": 99.3, "fees": { "treasury": 0.2, "distributor": 0.1, "fee": 0.4 } }
Possible errors:
400Invalid request parameters, unsupported network, market not open, insufficient liquidity, or invalid slippage/value/shares combination
404Market or outcome not found
500Unable to resolve token decimals or unexpected server error
POST /markets/quote_with_fee
Select integrators can charge a frontend fee at the time of a trade by using bundled transactions (
wallet_sendCalls) that include a transfer plus the trade (and token approval when needed). The frontend fee is in addition to any of the standard fees that apply to markets, and which are managed by the smart contract.To use this flow, your dapp/wallet integration must support EIP-5792 transactions.
This endpoint requires API authentication and whitelisting. If you’re interested in using the Myriad API to charge frontend fees for your integration, please reach out to the Myriad team.
Get the trade quote plus an ordered set of EIP-5792
calls that can be sent via wallet_sendCalls to:- (Optionally) approve ERC20 collateral spending for the PredictionMarket contract (buy-only, included only when allowance is insufficient)
- Transfer a frontend fee to
to_wallet
- Execute the trade
Request body:
- All fields from
POST /markets/quote
fee(number, required): decimal fee rate, between 0 and 0.05 (e.g.0.01= 1%)
from_wallet(string, required): wallet that will sign/send the bundle (used for allowance+balance checks)
to_wallet(string, required): fee recipient wallet
Response body:
- All fields from
POST /markets/quote
fees.frontend(number): frontend fee amount in token units
approval:required(boolean)currentAllowanceWei(string, optional)requiredAllowanceWei(string, optional)
calldata(object): ready to pass towallet_sendCallsas the first (and only)paramsitem to an EIP-5792 capable walletversion:"2.0.0"from: sender address (same asfrom_wallet)chainId: hex chain id (derived from marketnetwork_id)atomicRequired:truecalls: ordered array of{ to, data, value }(hex quantities)
Example request (buy with 1% frontend fee):
json{ "market_id": 164, "outcome_id": 0, "network_id": 2741, "action": "buy", "value": 100, "slippage": 0.01, "fee": 0.01, "from_wallet": "0x0000000000000000000000000000000000000001", "to_wallet": "0x0000000000000000000000000000000000000002" }
Example success response:
json{ "value": 100, "shares": 107.71301097629218, "shares_threshold": 107.17444592141072, "price_average": 0.9191090203744288, "price_before": 0.89954079, "price_after": 0.9019004096700267, "calldata": { "version": "2.0.0", "from": "0x0000000000000000000000000000000000000001", "chainId": "0xab5", "atomicRequired": true, "calls": [ { "to": "0x55d398326f99059fF775485246999027B3197955", "data": "0x095ea7b300000000000000000000000039e66ee6b2ddaf4defded3038e0162180dbef3400000000000000000000000000000000000000000000000055de6a779bbac0000", "value": "0x0" }, { "to": "0x55d398326f99059fF775485246999027B3197955", "data": "0xa9059cbb00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000de0b6b3a7640000", "value": "0x0" }, { "to": "0x39E66eE6b2ddaf4DEfDEd3038E0162180dbeF340", "data": "0x1281311d00000000000000000000000000000000000000000000000000000000000023270000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000005cf581ebb20ffaa2d0000000000000000000000000000000000000000000000055de6a779bbac0000", "value": "0x0" } ] }, "net_amount": 97.02, "fees": { "treasury": 0, "distributor": 0.99, "fee": 0.99, "frontend": 1 }, "approval": { "required": true, "currentAllowanceWei": "0", "requiredAllowanceWei": "99000000000000000000" } }
Examples with two wallet providers:
Notes:
- Buy fee semantics: for
action="buy", the requestvalueis treated as the total amount charged from the user. The frontend fee is deducted first, and the trade is quoted/executed with the remaining amount. The responsevalueequals the total charged (same as request value).
- Sell fee semantics: for
action="sell", the frontend fee is charged after the trade, and the responsevaluereflects the amount after frontend fee deduction.
- Clients should strongly prefer requiring atomic bundling (i.e.
"atomicRequired": true) so the fee transfer is not executed without the trade.
- Insufficient balance (buy only): for
action="buy", thefrom_walletbalance is checked against the total spend (trade value + frontend fee). If it can't cover the total, the endpoint returns400with the token, current balance, and required amount (in wei).
Possible errors:
400Invalid request parameters; fee too high for the provided value; insufficientfrom_walletbalance (buy); plus anyPOST /markets/quoteerror
401Missing/invalid API key (no authenticated user)
403API key is not whitelisted for frontend fees
404Market or outcome not found
500Unable to resolve token decimals or unexpected server error
POST /markets/claim
Get a trade claim and transaction calldata for a specific market.
- Method: POST
- Path:
/markets/claim
- Body: JSON
Request body:
- Exactly one of the following is required (send only one):
market_id(number): on-chain market id +network_id(number): network idmarket_slug(string)
outcome_id(number, optional): on-chain outcome id (only required when market is voided)
Validation rules:
- Market must exist and be
resolved.
Response body:
action(string):claim_winnings|claim_voided, depending on whether the market is voided (both require the market to beresolved)
outcome_id: winning outcome id, or voided outcome id to be claimed
calldata(string): hex-encoded calldata for the contract
Example request (buy by value):
json{ "market_id": 164, "outcome_id": 0, "network_id": 2741 }
Example success response:
json{ "action": "claim_winnings", "outcome_id": 0, "calldata": "0x..." }
Possible errors:
400Invalid request parameters, unsupported network, market not resolved
404Market or outcome not found
500Unable to resolve token decimals or unexpected server error
Users
GET /users/:address/events
Paginated actions for a user across markets, ordered by
timestamp desc.Query params:
page,limit
trading_model:amm|ob|all(defaultamm) — passobto filter to Order Book actions only
market_id: chain market id (optional)
market_slug: market slug (optional; resolves to id/network for filtering)
network_id: number (optional)
token_address: comma-separated list of ERC20 token addresses (optional)
since: unix seconds (inclusive)
until: unix seconds (inclusive)
only_relevant:1/true/yesto exclude wash-trade actions (optional)
Response items:
user: wallet address
action: action type
marketTitle,marketSlug,marketId,networkId
outcomeTitle,outcomeId
imageUrl
shares,value: numbers
timestamp: unix seconds
blockNumber: number
token: ERC20 token address
txId: transaction hash
Example:
plain textGET /users/0x1234.../events?network_id=2741&market_id=144&page=1&limit=50
GET /users/:address/referrals
Paginated referrals attributed to a user across markets, ordered by
timestamp desc.Query params:
page,limit
market_id: chain market id (optional)
market_slug: market slug (optional; resolves to id/network)
network_id: number (optional)
since: unix seconds (optional, inclusive)
until: unix seconds (optional, inclusive)
code: referral code (optional)
Response items (camelCase):
user: wallet address (referrer or user recorded on referral)
action:buy|sell
marketTitle,marketSlug,marketId,networkId
outcomeTitle,outcomeId
imageUrl
value: number
timestamp: unix seconds
blockNumber: number
token: ERC20 token address
code: referral code used
fees:lp(number)treasury(number)distributor(number)
Example:
plain textGET /users/0x1234.../referrals?network_id=2741&market_id=144&page=1&limit=50
GET /users/:address/portfolio
Aggregated user positions per market/outcome/network, ordered by latest activity.
Query params:
page,limit
trading_model:amm|ob|all(defaultamm) — passobfor Order Book positions orallfor both
min_shares: minimum shares threshold per outcome and for liquidity positions (default0.1)
market_slug: market unique slug (optional)
market_id: chain market id (optional)
network_id: number (optional)
token_address: comma-separated list of ERC20 token addresses (optional)
status: comma-separated subset ofongoing|lost|won|claimed|sold|voided, orallto include every position regardless of size (optional)
exclude_history:trueto return only currently-actionable positions (open, or resolved+unclaimed) (optional)
keyword: full-text search acrosstitle,description, and outcome titles (optional)
sort:asc|desc(defaultdesc)
sort_by:created_at(default) |profit|roi|shares|value|marketTitle|expires_at
group_by_event:true/1to fold positions sharing an event into a single event row (optional)
Notes:
- Positions with net
shares < min_shares(default0.1) are excluded (unlessstatusis provided orstatus=all).
- Pagination and totals are computed after filtering.
priceis the current outcome price;sharesis net buys minus sells; average buy price follows proportional cost-basis when selling.
Response items:
marketId,marketTitle,marketSlug,outcomeId,outcomeTitle,networkId,token,imageUrl
shares: net shares held (number)
price: average buy price (number)
value:shares * currentPrice
profit:shares * (currentPrice - price)
roi:(profit - totalAmount) / totalAmount(null if not computable)
totalProfit,totalRoi: realized + unrealized P/L over the lifetime of the position (null if not computable)
positionFees,totalFees: fees attributed to the open position / to all activity on it
winningsToClaim: true if resolved, holding winning outcome, and noclaim_winnings
winningsClaimed: true if resolved, holding winning outcome, andclaim_winningsexists
voidedWinningsToClaim,voidedWinningsClaimed: same, for voided markets
claimed: true if winnings or voided winnings were claimed
status:ongoing|lost|won|claimed|sold|voided
expiresAt: market expiry (ISO string or null)
eventId: parent event UUID, ornull
executionMode:0(AMM) or1(Order Book);tokenId(ERC1155 id) is set for Order Book positions — see Order Book Concepts
Example:
plain textGET /users/0x1234.../portfolio?network_id=2741&token_address=0x84A71ccD554Cc1b02749b35d22F684CC8ec987e1&page=1&limit=20
GET /users/:address/markets
Portfolio view aggregated by markets (grouped by market, ordered by latest user activity in the market). For Order Book positions, includes on-chain balances from the ConditionalTokens contract.
Query params:
page,limit(pagination; defaultlimit=10, max100)
trading_model:amm|ob|all(defaultamm) — passobfor Order Book positions orallfor both
min_shares: minimum shares threshold per outcome and for liquidity positions (default0.1)
network_id: number (optional)
state:open|closed|resolved(optional)
token_address: ERC20 token address (optional)
topics: comma-separated list of topics (optional)
keyword: full-text search acrosstitle,description, and outcome titles
market_ids: comma-separated list of{networkId}:{marketId}pairs (optional), e.g.2741:164,2741:200
market_slug: comma-separated market slug(s) (optional)
status: comma-separated subset ofongoing|lost|won|claimed|sold|voided, orall(optional)
sort:asc|desc(defaultdesc);sort_by:created_at(default)
group_by_event:true/1to fold sibling markets into a single event row (optional)
- Buy fee filters (optional; decimals between 0 and 1):
buy_lp_fee_lt,buy_lp_fee_lte,buy_lp_fee_gt,buy_lp_fee_gte,buy_lp_fee_eq
Response items:
market: a market object (same shape as/marketsitems)
portfolio:positions: array of outcome positions with:marketId,marketTitle,marketSlug,imageUrl,networkId,tokenoutcomeId,outcomeTitleshares,price,value,profit,roi,totalProfit,totalRoi,positionFees,totalFeeswinningsToClaim,winningsClaimed,voidedWinningsToClaim,voidedWinningsClaimed,claimedstatus:ongoing|lost|won|claimed|sold|voidedliquidity:shares,price,valuetotalAdded,totalRemoved,totalClaimed,totalToClaimfeesClaimed
Order Book Concepts
The Order Book replaces the AMM for order matching on Order Book markets — traders sign EIP-712 orders off-chain, the API validates and stores them, and an on-chain matcher settles fills through the
MyriadCTFExchange contract. This section defines the shared concepts; the Orders, Positions, Events, and Real-time Updates sections document the endpoints.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 )
Signature Types
Order and cancel requests may carry an optional
signatureType alongside signature, declaring which signing scheme produced it. It is transport metadata only — it is not part of the signed EIP-712 struct and does not affect the order hash.Value | Scheme | How the signature is verified |
0 | EOA (default) | ECDSA recovery ( verifyTypedData), with an EIP-1271 fallback |
1–2 | Reserved | Not supported — rejected with 400 |
3 | SCW (smart-contract wallet) | EIP-1271 ( isValidSignature) directly against the trader contract — no ECDSA recovery |
signatureType is optional and defaults to 0, so existing EOA callers can omit it. Only 0 and 3 are accepted; 1–2 are reserved for proxy-wallet schemes the API does not implement, and any other value is likewise rejected with 400. For signatureType=3 the trader must be a deployed contract implementing EIP-1271: the API calls its isValidSignature on-chain and accepts the order only if it returns the ERC-1271 magic value (a trader with no contract code is rejected).Smart-contract wallets can also sign in:
POST /auth/login verifies the SIWE signature via EIP-1271 when standard EOA recovery fails (see Authentication).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. |
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.
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>", "signatureType": 0, "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 for EOA; SCW signatures may differ in length) |
signatureType | number | no | Signing scheme: 0=EOA (default) or 3=SCW (EIP-1271); other values rejected. See Signature Types. |
network_id | number | no | Network ID (defaults to server config) |
time_in_force | string | no | GTC (default), GTD, FOK, FAK, PO |
replaces_order_hash | hex string | no | Hash of an open order this one replaces. Excluded from the reserved-funds check (see below) so a place-then-cancel replacement isn't rejected; the client is expected to cancel it right after. |
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.
- Reserved funds: the balance check subtracts obligations already parked in the trader's open orders on the same market (buys reserve
(amount - filled) * price / 1e18of collateral; sells reserve unfilled shares per outcome). A new order must fit withinbalance - reserved. To replace an existing order without cancelling it first, pass its hash asreplaces_order_hashso it is left out of the reservation.
- 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 | Deprecated. When authenticated with a connected wallet, results are already scoped to your own orders and a differing trader is rejected with 403 TRADER_FORBIDDEN |
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", "signatureType": 0, "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", "signatureType": 0, "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>", "signatureType": 0, "network_id": 56 }
signatureType is optional (default 0); set it to 3 to cancel with a smart-contract-wallet signature. See Signature Types.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, signatureType?, time_in_force?, accept_by?} entries |
orders[].signatureType | number | no | Per-entry signing scheme (default 0). See Signature Types. |
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, signatureType?, time_in_force?, accept_by?} — same entry shape as POST /orders/batch) |
cancel | array | no | Up to 200 orders to cancel ( {order, signature, signatureType?} — 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, signatureType?} 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 |
orders[].signatureType | number | no | Signing scheme (default 0). See Signature Types. |
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>", "signatureType": 0, "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 |
signatureType | number | no | Signing scheme (default 0); 3 for a smart-contract wallet. See Signature Types. |
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 |
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.
scoreboard— for sports events, the live-scores scoreboard (same object documented forGET /marketsitems); omitted for non-sports events. Siblingmarketsare in canonical outcome order, so align their rows positionally with the scoreboard'soutcome0/outcome1.
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 |
Public channels (
orderbook:*, trades:*, settlements:*, prices:*, markets:*) are open — connect anonymously, no token or API key required. The per-trader orders:* / positions:* channels require a short-lived subscription token issued for your connected wallet — see WebSocket subscription tokens.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.Tags
GET /tags
Aggregates tag occurrences across published, open markets, returning one row per distinct tag slug with a
marketCount scoped to currently-tradeable markets. Useful for filter chips on a markets listing page.Tags are denormalized onto markets (synced from the offchain CMS); the matching
tags/exclude_tags filters on GET /markets accept the same slugs.Query params (optional):
type: filter to a single tag namespace (e.g.type=league)
Example:
plain textGET /tags?type=league
Response:
json{ "data": [ { "marketCount": 18, "tag": { "type": "league", "title": "Premier League", "slug": "premier-league", "imageUrl": "https://..." } } ] }
Sorted by
marketCount descending, then tag title ascending.Topics
GET /topics
Per-topic market counts, broken down by
markets.topics. Intended for clients such as sidebar category counters that need counts per topic.Counts are always restricted to published markets that are currently open (
state = open and expires_at in the future) with at least 1 hour between publication and expiry. Sibling markets under the same event always collapse into a single tally — an event is counted once even if several of its siblings carry the topic.Query params (all optional):
network_id: comma-separated list of network ids
Example:
plain textGET /topics?network_id=2741,59144
Response:
json{ "data": [ { "topic": "crypto", "marketCount": 42 }, { "topic": "sports", "marketCount": 31 } ] }
Topics are returned sorted by
marketCount descending, then topic ascending.Price Data
- Historical price data is built from on-chain events (
MarketOutcomeShares) and stored inprices.
- Outcome prices are derived from outcome shares.
- Liquidity price computation follows the contract logic; resolved markets use final shares/liquidity, otherwise
#outcomes / (liquidity * Σ(1/shares)).
Errors
Common errors:
401 Unauthorized– missing/invalid API key
429 Too Many Requests– rate limit exceeded
400 Bad Request– invalid query parameters
404 Not Found– resource not found
409 Conflict– duplicate order (Order Book: duplicate hash orclient_order_id)
500 Internal Server Error
Networks
The Myriad Protocol’s main deployment is on BNB Smart Chain, where most markets will be denominated in USD1 and some markets are still denominated in USDT.
Some markets are also available on other EVM-compatible blockchains.
BNB Smart Chain
Order Book Contracts
Mainnet | Testnet | |
OBExchange | 0xa0b6f8ef8EdB64f395018D1933f2273Ce9f0f16A | |
OBConditionalTokens | 0x6413734f92248D4B29ae35883290BD93212654Dc | |
OBManager | 0xaB5591E280fF9Bf368DB60c3b775b5C7Ba5ea3dB | |
OBFeeModule | 0xc1BB36bb0BA236603b95544E809F2ab1893BBC0C | |
OBNegRiskAdapter | 0xd96F26703Ddbf7d1Cb6858640eca34cF1893d53A | |
OBWrappedCollateral | 0x9F124ce59D8De0274574949400640a2677067ACC |
AMM Contracts
Mainnet | Testnet | |
PredictionMarket | ||
PredictionMarketQuerier |
Tokens
Token | Mainnet | Testnet |
USD1 | ||
USDT |
Other chains
Abstract
AMM Contracts
Mainnet | Testnet | |
PredictionMarket | ||
PredictionMarketQuerier |
Tokens
Token | Mainnet | Testnet |
USDC.e | ||
PENGU | ||
PTS |
Linea
AMM Contracts
Mainnet | Testnet | |
PredictionMarket | ||
PredictionMarketQuerier |
Tokens
Token | Mainnet | Testnet |
USDC |
Celo
AMM Contracts
Mainnet | Testnet | |
PredictionMarket | Coming soon | |
PredictionMarketQuerier | Coming soon |
Tokens
Token | Mainnet | Testnet |
USDT | Coming soon |
Audits
Order Book
The Order Book contracts have been audited by Cyfrin:
- Reports per component and reviews:
- https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2026-03-13-cyfrin-myriad-clob-v2.0.pdf
- https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2026-04-08-cyfrin-myriad-realitio-oracle-v2.0.pdf
- https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2026-04-07-cyfrin-myriad-pr145-v2.0.pdf
AMM
The AMM contracts have been audited by Cyfrin: https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2025-07-25-cyfrin-myriad-v2.0.pdf
Changelog
V2.0.0
- Added API key authentication
- Added rate limiting
- Markets endpoints with keyword search and charts
- Market events and user events endpoints with timestamp filtering
- Historical prices ingestion + charting
V2.0.1
- Added portfolio endpoint
- Added market holders endpoint
V2.0.2
- Added market quote endpoint
V2.0.3
- Added market quote_with_fee endpoint
V2.0.4
- Made most API endpoints public