Developer Documentation

REST + WebSocket API for the OutcomeX prediction market. Auto-generated reference at /swagger; OpenAPI spec for codegen at /swagger/v1/swagger.json.

Market model

Each market is a collateralized binary CLOB. There is one order book — the YES book — and the NO book is its exact mirror: a price is in cents (1–99) and NO price = 100 − YES price. YES + NO always cost $1 (100¢) by construction, so there is no cross-book arbitrage. One YES share pays 1 OXC if the market resolves YES, 0 if NO (and vice-versa for NO shares). OXC is play-money.

Order side values, and how they normalize onto the single YES book:

side means normalizes to
BuyYesbuy YES at priceBuyYes @ price
SellYessell YES at priceSellYes @ price
BuyNobuy NO at priceSellYes @ (100 − price)
SellNosell NO at priceBuyYes @ (100 − price)

You always quote in your outcome's price; the engine mirrors NO onto the YES book for you. Markets can be grouped (a slate of related binaries); group children resolve independently, so a "by date / threshold" slate can settle several outcomes YES.

Authentication

All trading endpoints require an authenticated user. Two schemes are supported:

  • Bearer tokens — recommended for scripts and market makers. Issued by POST /login.
  • Cookies — used by the browser surface (Razor pages, Identity).

Get a bearer token:

POST /register
Content-Type: application/json

{ "email": "you@example.com", "password": "..." }

POST /login?useCookies=false&useSessionCookies=false
Content-Type: application/json

{ "email": "you@example.com", "password": "..." }

// Response carries { accessToken: "..." }
// Send subsequent requests with: Authorization: Bearer <token>

Admin endpoints additionally require the Admin role; a non-admin user gets 403. Banned users get 403 on every authenticated request from the ban-enforcement middleware.

Rate limits

Fixed 1-minute window, partitioned per-IP (or per-user when authenticated):

Policy Per minute Endpoints
global 300 default for every route not below
auth 10 register, login, account-settings (credential brute-force defense)
trading 180 orders, comments, watchlist (mutations + per-user accounting)

Exceeding the limit returns 429 Too Many Requests with a retryAfterSeconds field (and a Retry-After header). The trading policy partitions per signed-in user (or per-IP if anonymous) so a single user can't drain another's quota.

Note: one logical action can cost several requests — a buy is the order POST plus a balance re-sync — so budget your loop accordingly. WebSocket frames are not rate-limited; prefer the live feeds over polling REST.

Pagination

List endpoints accept ?page=N&pageSize=K. The hard upper cap on pageSize is 100 across every public list — a larger value is silently clamped. Per-endpoint defaults vary; see /swagger.

Market data (public, no auth)

GET /api/v1/markets?page=1&pageSize=20      // list open markets
GET /api/v1/markets/search?q=starmer&limit=8 // type-ahead search
GET /api/v1/markets/{id}                       // detail: top-of-book + last 20 trades
GET /api/v1/markets/{id}/trades?range=1h       // trade history for charts
//   range: 1h / 4h / 1d (default) / all  ->  { range, points: [{ t, p }] }
//   t = unix seconds, p = YES price in cents

Top-of-book here is computed from the DB (best resting YES bid/ask), so it's stateless and reproducible from any node. For a live book, use the WebSocket feed below.

Orders & positions (auth required)

Place a limit order

POST /api/v1/orders
Authorization: Bearer <token>
Content-Type: application/json

{
  "marketId": 42,
  "side": "BuyYes",      // BuyYes | SellYes | BuyNo | SellNo
  "type": "Limit",
  "price": 35,           // cents, 1..99 (in YOUR outcome's terms)
  "size": 4,             // shares (decimal)
  "expiresAt": null      // optional UTC ISO-8601; omit/null = good-till-cancelled
}

Place a market order

A market order sweeps the book across price levels. price is still required — it's the reference used to reserve your cash budget (price × size of per-share collateral). The engine crosses any level, caps the spend at that budget, and refunds the unspent remainder; any unfilled remainder is cancelled (it never rests).

{
  "marketId": 42,
  "side": "BuyYes",
  "type": "Market",
  "price": 60,           // budget reference (≈ how far you'll pay up), still 1..99
  "size": 10
}

Cancel, close, list

DELETE /api/v1/orders/{id}                  // cancel one resting order (refunds reserve)
POST   /api/v1/orders/cancel-mine           // cancel all your open orders
POST   /api/v1/orders/close-position/{marketId}  // sell your whole position at market
GET    /api/v1/orders/mine?status=open&page=1    // your orders (status: all|open|filled|cancelled)
GET    /api/v1/orders/{id}                  // one of your orders
GET    /api/v1/account/balance              // { balance } in OXC

close-position sizes a market order against your net holding (YES → SellYes, NO → SellNo) and returns 409 if there's no opposing liquidity to lift.

WebSocket feeds

Push, not poll. On connect you get one snapshot, then bookDelta / trade frames as the engine emits them. Both YES and NO views are sent (NO is mirrored server-side). Every frame carries marketId.

// One market:
GET wss://<host>/ws/markets/{marketId}

// Many markets over ONE socket (slate / portfolio). Up to 50 ids:
GET wss://<host>/ws/markets?ids=1,2,3
// -> a snapshot per open market, then that market's deltas, each tagged with marketId.

// Frame shapes (camelCase JSON):
{ "type": "snapshot", "marketId": 42,
  "yesBook": { "bids": [{ "price": 48, "size": 10 }], "asks": [{ "price": 53, "size": 10 }] },
  "noBook":  { "bids": [...], "asks": [...] },
  "lastPrice": { "yes": 50, "no": 50 }, "spreadCents": 5,
  "recentTrades": [{ "yesPrice": 50, "noPrice": 50, "size": 2, "createdAt": "..." }] }

{ "type": "bookDelta", "marketId": 42,
  "yes": { "side": "bid", "price": 48, "sizeDelta": -5 },
  "no":  { "side": "ask", "price": 52, "sizeDelta": -5 } }   // side: "bid" | "ask"

{ "type": "trade", "marketId": 42,
  "yesPrice": 50, "noPrice": 50, "size": 2, "createdAt": "..." }

Apply bookDelta.sizeDelta to your local book (negative = removed by a fill/cancel). Close codes are application-defined: 4404 (market not found / no open markets in an ids list) and 4409 (market not Open) — distinct from the 1000-series so a client can tell "gone" from "stop reconnecting".

Settlement

  • On resolution, winning-side shares pay 1 OXC each; losing-side shares decay to 0.
  • A win-side settlement fee of Features:SettlementFeeBps basis points (default 4%) is deducted from the gross payout.
  • Resting orders on the resolved market are cancelled and their reserve refunded.
  • Selling a complete set (you hold both YES + NO) redeems 1 OXC per matched pair (cash back on sell).
  • Group children resolve independently — a slate may have several YES outcomes.

Error shape

Most errors return a JSON body with an error string. Specific cases use additional fields:

  • 400 Bad Request on /api/v1/orders — invalid side/type, price out of 1–99, size ≤ 0, market not open, or a self-cross (your order would cross your own resting order).
  • 451 Unavailable For Legal Reasons — your jurisdiction is blocked. text/plain body.
  • 503 Service Unavailable on /api/v1/account/deposit / /withdraw — carries reason (provider_not_configured / kyc_required) and paymentMode so a client can render a precise CTA.
  • 429 Too Many Requests — carries retryAfterSeconds; on /api/v1/account/demo-deposit also nextAvailableAt for cooldown rendering.

Full endpoint reference at /swagger. Spec for codegen at /swagger/v1/swagger.json.