Matcher HTTP and WebSocket API
In one paragraph. The matcher exposes a small JSON API: public GET endpoints for configuration, markets, book, trades, fills, candles, orders and account risk; two mutation endpoints (POST an EIP-712 signed order, DELETE a soft cancel with a personal-sign message); one bearer-token endpoint that serves an authenticated depth snapshot to the oracle; and one WebSocket stream. There is no API key, no authentication header and no rate limiting on the public endpoints; CORS is open (*). Authorization is cryptographic: a mutation is accepted only if the trader's signature verifies. Everything below is read from overdrive/exchange/matcher/src/server.ts and ws.ts.
Conventions
- The production base URL is
https://api.trion.market(the matcher serving the desk on Robinhood Chain 4663). A local matcher listens onOD_MATCHER_PORT(default8787; the local scripts use8790). All paths are under/v1. - Every
bigintis serialized as a decimal string ("2500000000000000000"), never a JSON number. Exception: candle OHLC values are floats in human units. - Prices are 18-decimal WAD USD per underlying unit. Quantities are integer lots; in V2 one lot is one contract (
lotWad = 1e18). Cash and margin fields are collateral atoms (6 decimals for USDG on 4663, and for the local mock token). See Signed orders. - Errors are
{ "error": { "code": string, "message": string } }with an HTTP status. Unknown paths return404 NOT_FOUND; unhandled exceptions return500 INTERNAL_ERROR. - The matcher only serves markets whose canonical deployment and registry identity resolve at boot; others return
404 MARKET_NOT_FOUND. Every order admission and batch proposal re-reads the registry (marketStateLoader); when the market is disabled the order is rejected with aFaultand any provisional match is cancelled rather than settled. - The Options V3 order book is a separate service with its own base path,
https://api.trion.market/v3/options, and its own JSON shapes; it is documented on the Options order-book API page. This page covers the perp matcher's/v1API and the read-only oracle proxy only.
Public GET endpoints
| Endpoint | Query | Returns |
|---|---|---|
GET /v1/config | — | chainId, rpcUrl, v2 (bool), lastProcessedBlock, deployments, v2Deployments, markets, priceVerifier, registry, fundingEpochs, riskEngine, matcher, quote, takerFeeBps, makerRebateBps, tickSizes, lotSizes (human strings per symbol). deployments.options.markets is one row per option series from the finalized options manifest (seriesId, pool, sleeve, kind, strike, cap, expiry, tradingEnabled); it is empty when no series has been seeded. |
GET /v1/markets | — | Array, one row per listed market: symbol, status (verifier market status), prices (source-tagged, below), indexPrice, markPrice, fundingRatePerSecond, fundingRate8h, fundingStatus, nextFundingTs, observedAt, lastTrade, volume24h, openInterest, takerFeeBps, makerRebateBps, tickSize, lotSize. Returns 503 as a whole if any market's canonical price is unavailable (below). |
GET /v1/book/:symbol | depth 1–100 (default 25) | { bids: [{price, quantity}], asks: [...], ts } from EngineV2.getBookView. Levels are resting, funded, uncancelled orders; not a fill guarantee. |
GET /v1/trades/:symbol | limit 1–500 (default 100) | Confirmed trades {price, quantity, side, ts, txHash, fee?, rebate?}. Only receipt-confirmed fills appear. |
GET /v1/fills or /v1/fills/:symbol | limit, orderHash | Fill records from the SQLite store, including makerHash, takerHash, txHash. |
GET /v1/candles/:symbol | tf = 1m,5m,15m,1h,4h,1d,1w; limit ≤ 1000; to (ms); source = trades | index | mark | OHLC candles. source=index daily/weekly candles come from the historical composite in data/index/, not live V2 snapshots. Candles are history and keep being served while the current price is 503-unavailable; never derive a live quote from the last bar. |
GET /v1/orders | symbol, trader, status=all | Open orders for the market engine; with status=all, every order record the engine remembers (open, matched, confirmed, cancelled, rejected). |
GET /v1/orders/:orderHash | — | One order record: orderHash, order, status, filledQuantity, remainingQuantity, txHash, createdAt. |
GET /v1/account/:symbol/:address | — | Canonical risk view read from PerpetualV2.getAccount and preview: ledger {cash, position, lastFundingEpoch, flags}, basis {entryPrice, openedSeq, revision}, risk {equity, pendingFunding, notional, initialMargin, maintenanceMargin, reserved, withdrawable, freeCash, leverage, liquidationPrice, state, priceSequence}, prices, openOrders. liquidationPrice is null when flat or not liquidatable at any positive price. |
GET /v1/fixing/:symbol | — | Fixing state for the market's listed expiries, proxied from the oracle. |
GET /v1/fixing/:symbol/:expiry | account (optional address) | { symbol, marketId, expiry, oracle, onChain, dispute }: the oracle's fixing record, the on-chain verifier fixing and council case (account-specific fields when account is given), and the public dispute text when the case is challenged. |
GET /v1/fixing/:symbol/:expiry/estimate | — | Running settlement estimate from the oracle (eligible and elapsed five-minute sample counts); null while the oracle cannot produce one. |
GET/POST /v1/fixing/:symbol/:expiry/dispute | — | Read the public dispute for a challenged fixing, or submit one (signed) alongside the on-chain challenge bond. |
GET /v1/oracle/* | passthrough | Read-only proxy to the index oracle so the browser talks to one public host. Useful paths: /v1/oracle/v1/quote/CMPT (live composite with per-venue components and evidence hash), /v1/oracle/v1/quotes, /v1/oracle/v1/daily/CMPT (daily index bars with per-bar provenance), /v1/oracle/v1/evidence/:hash (raw observation blob). Returns 503 ORACLE_NOT_CONFIGURED when no oracle URL is configured. |
Source-tagged prices
prices carries four tags so a client never confuses them. In V2 mode I and M are copied field for field from the snapshot the TrionPriceVerifierV2 contract has accepted (canonical-prices.ts canonicalRiskPrices); the matcher does not compute, clamp or substitute either of them. Hypothetical local shape:
"prices": {
"I": { "price": "2500000000000000000", "source": "I", "sequence": "7", "epoch": 1, "timestamp": 1789748410,
"minSourceAt": 1789748400, "maxSourceAt": 1789748405, "validUntil": 1789748710, "status": "NORMAL", "evidenceRoot": "0x…" },
"M": { "price": "2500000000000000000", "source": "M", "sequence": "7", "epoch": 1, "timestamp": 1789748410,
"minSourceAt": 1789748400, "maxSourceAt": 1789748405, "validUntil": 1789748710, "status": "NORMAL", "evidenceRoot": "0x…", "basis": "VERIFIER_ACCEPTED" },
"L": { "price": "2501000000000000000", "source": "L", "confirmed": true, "txHash": "0x…" },
"E": { "price": "2501000000000000000", "source": "E", "makerHash": "0x…", "takerHash": "0x…", "quantity": "3" }
}
L and E are null until a fill has been confirmed. M.basis is always VERIFIER_ACCEPTED for a V2 market: it tells you M is the on-chain accepted mark, not that any live mark sampling happened. On the local chain the seeded snapshot has M equal to I. The ±0.5 % bound between M and I is enforced by the verifier contract when it accepts the snapshot, not by the API.
When the canonical data is unavailable
The V2 endpoints never substitute a placeholder for a missing price or account. readCanonicalSnapshot rejects a snapshot that is absent, unreadable, bound to the wrong marketId, zero-priced, expired (validUntil in the past), future-dated (sampleAt after now) or otherwise malformed, and the request fails as a whole:
| Condition | Response |
|---|---|
| Verifier snapshot missing, unreadable, expired, future-dated or malformed for any listed market | 503 { "error": { "code": "V2_PRICE_UNAVAILABLE", "message": … } } — the entire /v1/markets array is replaced by the error envelope. There is no per-market partial answer. |
getAccount/preview RPC failure, or the preview's priceSequence/configEpoch not matching the accepted snapshot | 503 { "error": { "code": "V2_ACCOUNT_UNAVAILABLE", "message": … } } on /v1/account/:symbol/:address. |
WebSocket ticker while the price is unavailable | { "symbol", "status": "UNAVAILABLE", "error": { "code": "V2_PRICE_UNAVAILABLE", "message" } } with no prices object. |
A 200 therefore always carries non-null I and M with the verifier's own sequence, epoch (configEpoch) and status. The seed-price constant that still exists in server.ts is only reachable on the V1 code path.
Fields that are placeholders, not measurements
openInterestis always the string"0". The matcher does not aggregate V2 open interest; treat the field as unavailable, not as an empty market.fundingRatePerSecondandfundingRate8hare"0"and the row carriesfundingStatus: "UNAVAILABLE". The matcher does not readFundingEpochsV2; zero means "not reported", not "no funding is accruing". Your on-chainpendingFundingin/v1/accountis the real number. Whether epochs are being finalized at all depends on an oracle publisher running against that chain (see Data & methodology).nextFundingTsis simply the next UTC hour boundary; it is a clock, not a funding-service schedule. The contract's epochs run from a governed per-market origin, not from Unix hours.
Mutation endpoints
POST /v1/orders?symbol=CMPT
Body: { "order": Order, "signature": Hex } where Order is the 14-field V2 struct with bigint fields as decimal strings and enums as numbers (side, tif, flags). A V1-shaped body (isBuy present, no marketId) is rejected with 400 INVALID_ORDER_VERSION. Missing marketId/methodologyHash are filled from the manifest; do not rely on this—sign what you send.
{
"order": {
"marketId": "0xaee1f7724c3ed0bd1db9034995237b35f87b9250b1a4a0abedacf59530c161de",
"methodologyHash": "0x…",
"trader": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"strategyId": "0x0000000000000000000000000000000000000000000000000000000000000000",
"limitPrice": "2500000000000000000",
"quantity": "2",
"nonce": "1789748410123",
"expiry": "1789834810",
"accountEpoch": 0,
"strategyEpoch": 0,
"maxFeeBps": 50,
"side": 0,
"tif": 0,
"flags": 2
},
"signature": "0x…65 bytes…"
}
Responses:
201 { orderHash, status, filledQuantity, remainingQuantity }withstatus∈open(resting),matched(filled in memory, awaiting on-chain settlement),cancelled(IOC with no fill), or the lowercase engine status.400 { error: { code, message } }wherecodeis aFaultcode:BAD_SIGNATURE,EXPIRED,EPOCH,MARGIN,BAND,CAP,STATE,TRANSPORT;messageis"<stage>: <code>"with stageAdmission,Fill,RiskorChain.
A matched order can still fail on chain (revert, epoch change, price band). Poll GET /v1/orders/:orderHash for confirmed and a txHash, or subscribe to trades.
DELETE /v1/orders/:orderHash
Body: { "signature": Hex } — an EIP-191 personal_sign of the string trion-cancel:<orderHash>.
The route finds the resting V2 order across the market engines (EngineV2.getOpenOrders), verifies the signature against the actual trader of that resting order (not an address supplied by the caller), and removes it with EngineV2.cancelOrder. Responses:
| Status | Meaning |
|---|---|
200 { "ok": true } | The order was actually removed from the book. Not returned before removal. |
403 CANCEL_REJECTED | Signature does not verify for the resting order's trader. |
409 CANCEL_REJECTED | The order stopped resting between lookup and removal (matched, batched or cancelled concurrently). |
404 NOT_FOUND | No resting V2 order has that hash: unknown, already cancelled, or already filled. Repeating a successful cancel therefore returns 404. |
400 CANCEL_REJECTED / MISSING_SIGNATURE / INVALID_JSON | Signature verification threw, or the body is malformed. |
This is a local cancel (LOCAL_ORDER mode): it removes the order from the matcher's book only. It does not revoke the signature on chain and cannot recall a fill already proposed in a batch; a copy of the signed order could still be settled if it reaches the contract by another route. The on-chain paths remain authoritative: OrdersV2.cancel(order) (single order, msg.sender must be the trader) or OrdersV2.advanceAccountEpoch(newEpoch) (revokes every order signed under the old epoch; the matcher removes them after ingesting the event).
There is no REST endpoint for deposits, withdrawals, strategy-policy submission or epoch changes; those are wallet transactions. See Contracts.
Oracle-only: GET /v2/mark-evidence
Requires Authorization: Bearer <OD_MARK_EVIDENCE_TOKEN> (constant-time compared; the token must be at least 32 characters, otherwise the endpoint answers 503 EVIDENCE_UNAVAILABLE). With ?marketId= it returns the canonical depth snapshot for the current 5-second slot: { chainId, marketId, observedAtMs, sequence, root, bids, asks } (up to 100 levels per side, root = keccak of the ABI-encoded levels). The first capture in a slot is stored and every later request in that slot returns the same bytes, so two independent signers fetching the book see the same evidence. With ?hash= it returns a previously captured snapshot by its hash (404 if unknown). This is how the V2 mark M gets a bookRoot; it is not a trading endpoint and carries no user data.
WebSocket /v1/ws
Client → server messages:
{ "op": "subscribe", "channel": "book", "symbol": "CMPT" }
{ "op": "subscribe", "channel": "trades", "symbol": "CMPT" }
{ "op": "subscribe", "channel": "candles", "symbol": "CMPT", "tf": "1m", "source": "mark" }
{ "op": "subscribe", "channel": "ticker", "symbol": "CMPT" }
{ "op": "unsubscribe", "channel": "book", "symbol": "CMPT" }
Server → client: { "channel", "symbol", "data" } (candles add tf and source; data.source is also set), plus { "channel": "ping" } every 15 s. Malformed client messages are ignored silently; there is no acknowledgement of a subscription. Trade broadcasts fire only after receipt reconciliation.
What this API does not do
- No pagination cursors; use
limit/to. - No SDK; the browser client in
overdrive/exchange/web/src/api.tsis the reference consumer. - No authentication on public reads: anyone can read anyone's
/v1/account/:symbol/:address; that data is public on chain anyway. - No idempotency keys: re-posting the same signed order returns the same
orderHash; the contract prevents double-fill viaOrdersV2.orderState.
All examples on this page are hypothetical shapes; none is a production quotation.
Source trail: overdrive/exchange/matcher/src/server.ts (/v2/mark-evidence, /v1/config, /v1/oracle/*), fixing-routes.ts (handleFixingRoute), ws.ts, mark-evidence.ts (MarkEvidenceStore, authorizedEvidenceRequest), config.ts (options.markets), main.ts (marketStateLoader), canonical-prices.ts (readCanonicalSnapshot, requireCanonicalSnapshot, canonicalRiskPrices, CanonicalDataUnavailable), index-feed.ts (pollV2Snapshot), v2-engine/engine.ts (submitOrder, getOpenOrders, cancelOrder, getBookView, getTrades, getOrder), v2-relay/reconciliation.ts, store.ts (getCandles); overdrive/exchange/oracle/src/server.ts (/v1/quote/, /v1/quotes, /v1/daily/, /v1/evidence/); overdrive/exchange/core/src/orders.ts (cancelMessage).
Repository-owned documentation · September 2026 · Educational material, not investment advice and not an audit.
Documentation
V2 · Live on Robinhood Chain