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 |
|---|---|---|
BuyYes | buy YES at price | BuyYes @ price |
SellYes | sell YES at price | SellYes @ price |
BuyNo | buy NO at price | SellYes @ (100 − price) |
SellNo | sell NO at price | BuyYes @ (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:SettlementFeeBpsbasis 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/plainbody. - 503 Service Unavailable on
/api/v1/account/deposit//withdraw— carriesreason(provider_not_configured/kyc_required) andpaymentModeso a client can render a precise CTA. - 429 Too Many Requests — carries
retryAfterSeconds; on/api/v1/account/demo-depositalsonextAvailableAtfor cooldown rendering.
Full endpoint reference at /swagger. Spec for codegen at /swagger/v1/swagger.json.