Trion Documentation V2 · Live on Robinhood Chain
Browse documentation

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

Public GET endpoints

EndpointQueryReturns
GET /v1/configchainId, 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/marketsArray, 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/:symboldepth 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/:symbollimit 1–500 (default 100)Confirmed trades {price, quantity, side, ts, txHash, fee?, rebate?}. Only receipt-confirmed fills appear.
GET /v1/fills or /v1/fills/:symbollimit, orderHashFill records from the SQLite store, including makerHash, takerHash, txHash.
GET /v1/candles/:symboltf = 1m,5m,15m,1h,4h,1d,1w; limit ≤ 1000; to (ms); source = trades | index | markOHLC 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/orderssymbol, trader, status=allOpen orders for the market engine; with status=all, every order record the engine remembers (open, matched, confirmed, cancelled, rejected).
GET /v1/orders/:orderHashOne order record: orderHash, order, status, filledQuantity, remainingQuantity, txHash, createdAt.
GET /v1/account/:symbol/:addressCanonical 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/:symbolFixing state for the market's listed expiries, proxied from the oracle.
GET /v1/fixing/:symbol/:expiryaccount (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/estimateRunning 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/disputeRead the public dispute for a challenged fixing, or submit one (signed) alongside the on-chain challenge bond.
GET /v1/oracle/*passthroughRead-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:

ConditionResponse
Verifier snapshot missing, unreadable, expired, future-dated or malformed for any listed market503 { "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 snapshot503 { "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

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:

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:

StatusMeaning
200 { "ok": true }The order was actually removed from the book. Not returned before removal.
403 CANCEL_REJECTEDSignature does not verify for the resting order's trader.
409 CANCEL_REJECTEDThe order stopped resting between lookup and removal (matched, batched or cancelled concurrently).
404 NOT_FOUNDNo 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_JSONSignature 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

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.