Concept Overview
This section introduces the core business concepts you need to understand before integrating the Outcomes API, including xp (the base asset), events, markets, outcomes, prices, Split / Merge, the mirror order book, the order book, positions, settlement, and the key IDs.
Once you understand these concepts, you will be able to reason more clearly about:
- How to find a tradable market from an event
- How to choose the YES / NO outcome
- Why placing orders, cancelling orders, and querying market data generally use
assetId - The relationship between Split / Merge and the mirror order book
- The difference between a Binary Market and a NegRisk Market
1. Prediction Market
A prediction market is a market for trading on the outcomes of future events.
Each market usually corresponds to a clear question, for example:
Will Germany win a given match?
Users trade the different outcomes of that question based on their own judgment. The most common outcomes are YES and NO.
Example:
| Object | Example |
|---|---|
| Market | Will Germany win? |
| YES | Germany wins |
| NO | Germany does not win |
In simple terms:
| Direction | Meaning |
|---|---|
| Buy YES | The user believes the outcome will happen |
| Buy NO | The user believes the outcome will not happen |
2. Core Object Model
The core object relationships in the Outcomes API are as follows:
- Event
- Market
- YES outcome
- NO outcome
In simple terms:
| Object | Meaning |
|---|---|
| Event | A real-world event |
| Market | A specific tradable question under an Event |
| Outcome | A tradable result under a Market, e.g. YES / NO |
| assetId | The tradable asset ID of a specific outcome |
When integrating, you typically follow this path:
- Query an
Event - Get the
Markets under thatEvent - Choose the
YESorNOoutcome under aMarket - Trade using the
assetIdof that outcome
3. Event and Market
An Event is the upper-level organizational unit of the prediction market, representing a real-world event.
Example:
| Object | Example |
|---|---|
| Event | Germany vs. Curacao |
An Event can contain multiple Markets:
| Market | Question |
|---|---|
| Market 1 | Will Germany win? |
| Market 2 | Will the two teams draw? |
| Market 3 | Will Curacao win? |
What users actually trade is not the Event itself, but the YES or NO outcome under a specific Market.
4. Outcome
An Outcome is the tradable object under a Market.
In a binary market, each Market usually has two outcomes:
| Outcome | Meaning |
|---|---|
| YES | The Market's result will happen |
| NO | The Market's result will not happen |
Each outcome has its own assetId. When placing an order, what you ultimately use is the assetId of the target outcome.
Example:
marketId:100001question: Will Germany win?yesOutcome:assetId=100049000,price=0.65noOutcome:assetId=100049001,price=0.35
To buy YES, use yesOutcome.assetId.
To buy NO, use noOutcome.assetId.
5. Price and Probability
A price in the prediction market is a decimal between 0 and 1. It can be understood as the market's real-time estimate of the probability that an outcome happens.
For example:
| Outcome | Current price | Interpretation |
|---|---|---|
| YES | 0.65 |
The market currently estimates the probability of YES at about 65% |
| NO | 0.35 |
The market currently estimates the probability of NO at about 35% |
Why price β probability: a winning outcome settles at 1 xp and a losing one at 0 xp, so the expected value of holding an outcome = 1 xp Γ the probability that it occurs. In an efficient market the price converges to this expected value, so the price can be read as the market's estimate of that outcome's probability.
The price is formed by market trading and does not represent the final result. The final result is determined by market settlement.
6. Complementary Relationship of YES / NO
YES and NO are the two sides of the same Binary Market. When one outcome happens, the other does not.
Ideally, the sum of their prices is close to 1.
| Outcome | Price |
|---|---|
| YES | 0.65 |
| NO | 0.35 |
| YES + NO | 1.00 |
This relationship can be summarized as:
YES + NO β 1
You do not need to handle the YES / NO price conversion manually. When placing an order, simply choose the target outcome and use its assetId.
7. Split / Merge Mechanism
Split / Merge is the underlying mechanism behind the YES / NO complementary relationship.
For a Binary Market, YES and NO can be seen as a pair of conditional outcomes. The system can convert between xp (the base asset of this prediction market) and the YES / NO outcomes via Split and Merge.
7.1 Split
Split splits one unit of xp into equal amounts of YES and NO outcomes.
1 xp β 1 YES + 1 NO
This can be understood as: the user splits one complete market entitlement into the market's two complementary outcomes.
| Before | After |
|---|---|
| 1 xp | 1 YES + 1 NO |
7.2 Merge
Merge is the inverse of Split. The user can merge equal amounts of YES and NO outcomes back into xp.
1 YES + 1 NO β 1 xp
Merge is only possible when the YES and NO amounts are equal.
| Before | After |
|---|---|
| 1 YES + 1 NO | 1 xp |
This mechanism guarantees the complementary relationship of YES and NO:
YES + NO β 1
In other words, within the same Binary Market, YES and NO are not fully independent assets, but the two sides of the same set of conditional outcomes.
8. Mirror Order Book
Based on the YES / NO complementary relationship, the system can use a mirror order book to handle YES and NO trading in a unified way.
For the same Binary Market:
| User action | Economically equivalent to |
|---|---|
Buy YES @ 0.60 |
Sell NO @ 0.40 |
Buy NO @ 0.30 |
Sell YES @ 0.70 |
The reason is:
YES price + NO price β 1
Therefore, the system can support both YES and NO trading on a single unified order book at the underlying level.
You do not need to split or convert mirror orders manually. When placing an order, simply choose the outcome the user wants to trade and use its assetId. The system handles the YES / NO equivalence at the order book level.
9. Order Book
Outcomes uses a central limit order book (CLOB).
CLOB stands for Central Limit Order Book. Market prices are not set directly by the platform, but are formed by users' orders and trades.
The order book has two sides:
| Side | Meaning |
|---|---|
| Bid | The price a buyer is willing to buy at |
| Ask | The price a seller is willing to sell at |
Example:
| Side | Price | Size |
|---|---|---|
| Ask | 0.67 |
100 |
| Ask | 0.66 |
80 |
| Bid | 0.65 |
120 |
| Bid | 0.64 |
200 |
When the bid and ask prices match, the orders are matched and traded.
10. Maker and Taker
When an order is filled, the user may be a Maker or a Taker.
| Role | Meaning |
|---|---|
| Maker | Posts an order to the order book, providing liquidity |
| Taker | Actively fills an existing order in the book, consuming liquidity |
Example:
The current best ask is 0.66.
| User action | Result | Role |
|---|---|---|
Buy at 0.66 |
Fills immediately | Taker |
Post a buy at 0.60 |
Cannot fill immediately; enters the book and waits | Maker |
Order matching generally follows:
| Priority | Description |
|---|---|
| Price priority | Better prices are matched first |
| Time priority | At the same price, earlier orders in the book are matched first |
11. Position
Open orders that are not yet filled occupy the available balance. After an order is filled, the user obtains a position in the corresponding outcome.
Example:
| Action | Result |
|---|---|
| User buys 10 YES | After the fill, the user holds 10 YES outcomes |
Positions change with the following operations:
| Operation | Effect on position |
|---|---|
| Fill | Increases or decreases the corresponding outcome position |
| Close | Decreases an existing outcome position |
| Split | Generates YES / NO outcomes |
| Merge | Merges YES / NO outcomes |
| Settlement | Updates positions and balances based on the final result |
12. Closing a Position
A user does not have to wait for market settlement to exit a position. If the market is still tradable, the user can close a position by selling an existing outcome.
Example:
| Step | Action |
|---|---|
| Step 1 | User buys YES at 0.40 |
| Step 2 | YES price later rises to 0.70 |
| Step 3 | User sells YES to close the position |
| Step 4 | The user's position and balance are updated based on the fill |
Closing a position is essentially exiting an existing position via a reverse trade.
13. Settlement
Once an event's result is determined, the Market enters the settlement process.
After settlement:
| Outcome | Settlement result |
|---|---|
| Winning outcome | Settled at 1 xp |
| Losing outcome | Settled at 0 xp |
Example:
Market: Will Germany win?
If Germany ultimately wins:
| Outcome | Result |
|---|---|
| YES | Wins |
| NO | Loses |
If Germany does not win:
| Outcome | Result |
|---|---|
| YES | Loses |
| NO | Wins |
After the market settles, open orders are closed, and the user's positions and balances are updated based on the final result.
14. Binary Market
A Binary Market is the most common market structure, representing a yes / no question.
Example:
| Market | Will Germany win? |
|---|---|
| YES | Germany wins |
| NO | Germany does not win |
A Binary Market usually has only two outcomes:
YESNO
You only need to choose the assetId of YES or NO to trade.
15. NegRisk Market
A NegRisk Market is used for scenarios with multiple mutually exclusive outcomes. An Event can contain multiple related Markets, but usually only one outcome ultimately wins.
Example:
Event: Who will win the championship?
| Market | Question |
|---|---|
| Market 1 | Will Germany win the title? |
| Market 2 | Will France win the title? |
| Market 3 | Will Brazil win the title? |
| Market 4 | Will another team win the title? |
If Germany ultimately wins the title:
| Market | YES result |
|---|---|
| Will Germany win the title? | Wins |
| Will France win the title? | Loses |
| Will Brazil win the title? | Loses |
| Will another team win the title? | Loses |
For developers, a NegRisk Market does not change the basic trading method. You still choose the YES or NO outcome under a specific Market and trade with its assetId.
| Market type | Characteristics | Developer trading method |
|---|---|---|
| Binary Market | A single yes / no question | Choose the assetId of YES or NO |
| NegRisk Market | Multiple mutually exclusive outcomes | Still choose the assetId of YES or NO under a specific Market |
16. Market Status
A Market goes through different states during its lifecycle.
Common states:
| Status | Meaning | Generally tradable |
|---|---|---|
active |
Market is trading normally | Yes |
paused |
Market trading is paused | No |
settling |
Market is settling | No |
resolved |
Market has been settled | No |
Check the Market status before placing an order. If the Market is not active, the order may be rejected.
17. Key IDs
When integrating the API, the various IDs are the most easily confused.
Keep these in mind:
| ID | Meaning | Common use |
|---|---|---|
eventId |
Event ID | Query Event details and its Markets |
marketId |
Market ID | Identify a specific tradable question |
assetId |
An outcome's asset ID | Place order, cancel, market data, order book, positions, and other trading operations |
orderId |
Order ID | Query orders, cancel, track order status |
The most important rule is:
Order placement, cancellation, market data, and order book operations generally use assetId.
If the API returns assetId as null, the outcome is temporarily not tradable. Wait until assetId returns a valid value before performing trading operations.
18. Summary
You can understand the core model of Outcomes with the following chain:
Event β Market β Outcome β assetId β Order / Position
The mapping is:
| Stage | Description |
|---|---|
| Event | Find a real-world event |
| Market | Find a specific question under that event |
| Outcome | Choose YES or NO |
| assetId | Get the tradable asset ID |
| Order | Place an order using assetId |
| Position | A position is formed after the fill |
For API consumers, the most critical points are:
| Key point | Description |
|---|---|
| Do not trade the Event | The Event is only an organizational unit |
| Do not trade the Market directly | The Market is a specific question |
| Actually trade the Outcome | The Outcome is represented by assetId |
| YES / NO are complementary | YES + NO β 1 |
| The mirror order book is handled by the system | No manual conversion needed |
| Check status before ordering | The Market must be tradable |
| assetId must not be null | assetId = null means temporarily not tradable |
Quick Start
This guide walks you through the minimal integration flow of the Outcomes API and submitting your first order.
Prerequisites
Before you start, make sure you have the following:
| Requirement | Description |
|---|---|
| OKX account | Prediction market capability enabled |
| Available balance | For submitting orders |
| OKX App | For creating an API Key and authorizing an Agent |
| Dev environment | Examples in this doc use the Rust SDK |
| Agent private key | For signing write operations such as place / cancel order |
Step 1: Set Up an API Key
Before signing any request, you must first create an API Key via the OKX API management page or the App. After creation, you receive the following 3 credentials, which must be kept safe:
| Credential | Description |
|---|---|
| APIKey | Public identifier |
| SecretKey | Used to sign requests |
| Passphrase | An extra security credential you set yourself |
APIKey and SecretKey are randomly generated and provided by the platform; the Passphrase is set by you to strengthen the security of API access.
APIKey Permissions
For the prediction market, select all of the following permissions:
| Permission | Description |
|---|---|
| Read | Read-only operations such as querying bills and history |
| Trade | Write operations such as place order, cancel, transfer, and config changes |
Creation Steps
| Step 1: Tap "View" on the App prediction market page | Step 2: Tap the top-right menu |
|---|---|
![]() |
![]() |
| Step 3: Open the API Key generation page | Step 4: Generate the API Key |
|---|---|
![]() |
![]() |
| Step 5: View details after generation | Step 6: Details page |
|---|---|
![]() |
![]() |
All REST endpoints require OKX API credentials. Construct the client with with_credentials (see Step 5).
Step 2: Set Up an Agent
Keep your main key safe β create an Agent key that signs trades on your behalf.
- Generate a new Ethereum key pair (any key-generation tool). Save the Agent private key and the Agent address.
- Authorize the Agent in the OKX App β Agent authorization must be done via the App, not the API.
App Agent Authorization Steps
| Step 1: Enter the Agent address to authorize | Step 2: Authorization succeeded |
|---|---|
![]() |
![]() |
After authorization, all subsequent write API calls use the Agent private key; read API calls do not require the private key.
Step 3: Add the SDK
Installation
Add this SDK and the Tokio runtime to your Cargo.toml:
Cargo.toml dependencies
[dependencies]
okx-outcomes-sdk = { git = "https://github.com/okx/outcomes-sdk.git", features = ["signing", "websocket"] }
tokio = { version = "1", features = ["full"] }
That is the complete dependency list. tokio-tungstenite, k256, sha3, futures-util, etc. are pulled in transitively via the SDK's features. Do not add these dependencies yourself, or you may end up with versions inconsistent with what the SDK was built against.
Feature Flags
| Feature | What it enables | When to enable |
|---|---|---|
| default | REST client only (events, markets, orders, positions, balance, trades, prices, sports data). | Read-only integration. |
signing |
EIP-712 + ECDSA action signing helpers. | Any write operation: place_order, cancel_order, cancel_all, split, merge, redeem, heartbeat. |
websocket |
The tokio-tungstenite-based PredictionsWsClient and the typed WsMessage parser. |
Real-time prices, trades, order book, and user order / position / balance streams. |
Step 4: Configure Environment Variables
We recommend storing credentials in environment variables:
Environment variable configuration
export PREDICTIONS_API_KEY="your-api-key"
export PREDICTIONS_API_SECRET="your-secret-key"
export PREDICTIONS_API_PASSPHRASE="your-passphrase"
export PREDICTIONS_AGENT_PRIVATE_KEY="0x..."
Step 5: Initialize the Client
Initialize the SDK Client with the API Key, Secret Key, and Passphrase:
Client initialization example
use okx_outcomes_sdk::{ApiCredentials, OutcomesSdkClient};
let creds = ApiCredentials {
api_key: std::env::var("PREDICTIONS_API_KEY")?,
secret_key: std::env::var("PREDICTIONS_API_SECRET")?,
passphrase: std::env::var("PREDICTIONS_API_PASSPHRASE")?,
};
let client = OutcomesSdkClient::with_credentials(creds);
The SDK signs REST requests locally and automatically writes the required authentication headers.
Step 6: Query Tradable Markets
get_events returns a paginated list of events. All filter parameters are optional:
Query events example
let page = client
.get_events(
Some("active"), // status: "active" | "resolved"
None, // tag
None, // league_id
Some("volume_24h"), // sort: "volume" | "volume_24h" | "ending_soon" | "newest"
None, // cursor (from the previous page)
Some(20), // page_size (max 50)
)
.await?;
for event in &page.events {
println!("{} - {} markets ({} points volume)",
event.event_title, event.total_markets_count, event.volume);
}
// Pagination: pass `page.pagination.next_cursor` back as `cursor` on the next call.
Other read methods
| Method | Purpose |
|---|---|
client.search(keyword, cursor, page_size) |
Search events and markets by keyword. |
client.get_event(event_id) |
A single event and its full market list. |
client.get_event_markets(event_id) |
All markets under a single event (no pagination). |
client.get_market(market_id) |
Get a single market by market ID. |
client.get_ticker(inst_id) |
Latest ticker for a single market instrument. |
client.get_candles(...) |
Candlestick history. |
client.get_trades(...) |
Recent public trade history. |
Find a market with status: "active" and record:
yesOutcome.assetId(e.g."100049000") β used for placing ordersmarketId(e.g."1") β used for position queries
Step 7: Place a Limit Order
Buy 10 YES tokens at 0.55 xp on market 100049000. This requires two layers of authentication: the API Key headers + the SDK-signed request body.
Full limit order example
use okx_outcomes_sdk::{ApiCredentials, OutcomesSdkClient};
use okx_outcomes_sdk::models::order::{
LimitOrderType, OrderItem, PlaceOrderAction, PlaceOrderRequest, SigningOrderSide, SizeType,
};
use okx_outcomes_sdk::signing::{
action_place_order, generate_client_order_id_default, now_millis, parse_private_key,
sign_to_wrapper, LimitTif, OrderRequest, OrderType,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let creds = ApiCredentials {
api_key: std::env::var("PREDICTIONS_API_KEY")?,
secret_key: std::env::var("PREDICTIONS_API_SECRET")?,
passphrase: std::env::var("PREDICTIONS_API_PASSPHRASE")?,
};
let client = OutcomesSdkClient::with_credentials(creds);
// 1. Load the on-chain signing key (hex, with or without 0x prefix).
let key = parse_private_key(&std::env::var("PREDICTIONS_AGENT_PRIVATE_KEY")?)?;
// 2. Build the typed order. `asset_id` is the outcome asset ID from Step 6.
let order_request = OrderRequest {
asset_id: "100049000".into(),
side: SigningOrderSide::Buy,
market_type: "prediction".into(),
client_order_id: Some(generate_client_order_id_default()?),
price: "0.55".into(),
reduce_only: false,
size: "10".into(),
size_type: SizeType::Base, // omitted on the wire (default)
order_type: OrderType::Limit(LimitOrderType { tif: LimitTif::Gtc }),
};
// 3. Derive the wire-format OrderItem from the same OrderRequest so the
// JSON body cannot drift from the signed msgpack bytes.
let order_item = OrderItem::try_from(&order_request)?;
let action = action_place_order(vec![order_request]);
// 4. Sign and assemble the SignatureWrapper expected by the wire format.
let nonce = now_millis();
let signature = sign_to_wrapper(&action, nonce, None, &key)?;
// 5. Submit.
let req = PlaceOrderRequest {
action: PlaceOrderAction {
action_type: "placeOrder".into(),
grouping: "na".into(),
orders: vec![order_item],
},
nonce: nonce as i64,
signature,
};
let resp = client.place_order(&req).await?;
println!("tx_hash: {}", resp.tx_hash);
Ok(())
}
Place order response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0x7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}
}
Step 8: View Orders
When status = Some("open"), list_orders returns all non-terminal orders on the account, page by page. Pass the next_cursor from the previous response back via the cursor parameter until has_next is false:
Query order list example
use okx_outcomes_sdk::models::order::OrderRecord;
let mut cursor: Option<String> = None;
let mut all_open: Vec<OrderRecord> = Vec::new();
loop {
let page = client
.list_orders(
None, // market_id: None = all markets, Some(123) = a specific market
Some("open"), // "open" = PENDING_PLACE / ACTIVE / PENDING_CANCEL; "closed" = FILLED / PARTIALLY_FILLED / CANCELLED / EXPIRED / FAILED
cursor.as_deref(),
Some(50), // max items per page
)
.await?;
all_open.extend(page.list);
if !page.has_next {
break;
}
cursor = page.next_cursor;
}
println!("{} open orders", all_open.len());
for o in &all_open {
println!(
"#{} [{}] {} {}/{} @ {} market={} asset={} cloid={:?}",
o.id, o.status, o.side, o.filled_size, o.size, o.price,
o.market_id, o.asset_id, o.client_order_id,
);
}
Each OrderRecord exposes the order parameters (side, size, price, order_type, size_type, expiration), order metadata (market_id, asset_id, client_order_id, tx_hash), and the current lifecycle status (status, filled_size, filled_amount).
Tips:
market_idis ani64in the SDK, not a string; pass it asSome(123_456_789). ThemarketIdreturned by the REST API is a string, which the SDK converts internally.client_order_id(the cloid passed when placing the order) can be passed tocancel_orderto cancel by client ID instead of server ID.- Record
id(e.g.578840) β you need it when cancelling.
Step 9: Cancel an Order
The cancel flow is the same as place_order, also in three steps: build a typed CancelRequest, derive the corresponding wire-format CancelItem from it, sign the action, and finally submit the wire request. You can locate the target order by the order's server oid (the OrderRecord.id value) or by cloid (the client_order_id passed when placing the order).
The snippet below assumes client and key are already constructed as in the order placement section.
Cancel order example
use okx_outcomes_sdk::models::order::{CancelItem, CancelOrderAction, CancelOrderRequest};
use okx_outcomes_sdk::signing::{
action_cancel, now_millis, sign_to_wrapper, CancelRequest, CancelTarget,
};
// 1. CancelTarget must be one of:
// Oid = server-assigned order ID (OrderRecord.id), a decimal string.
// Cloid = client-assigned order ID, a hex string with the 0x prefix.
let cancel_request = CancelRequest {
asset_id: "100049000".into(),
market_type: "prediction".into(),
target: CancelTarget::Oid("578840".into()),
// target: CancelTarget::Cloid("0xabc...".into()),
};
// 2. Derive the wire item from the same request so the signed bytes do not diverge from the JSON body.
let cancel_item = CancelItem::from(&cancel_request);
let action = action_cancel(vec![cancel_request]);
// 3. Sign and submit.
let nonce = now_millis();
let signature = sign_to_wrapper(&action, nonce, None, &key)?;
let resp = client
.cancel_order(&CancelOrderRequest {
action: CancelOrderAction {
action_type: "cancel".into(),
cancels: vec![cancel_item],
},
nonce: nonce as i64,
signature,
})
.await?;
println!("cancel tx_hash: {}", resp.tx_hash);
Step 10: View Balance, Orders, and Positions
Query balance and positions example
// Balance
let entries = client.get_balance().await?; // Vec<BalanceEntry>
for entry in &entries {
println!("[{}] available={} total={}", entry.odds_type, entry.available, entry.balance);
}
// Positions
let positions = client.get_positions(
Some("open"), // "open" | "closed" | "settled"
None, // market_id
None, // cursor
Some(50), // limit
).await?;
println!("{} positions", positions.list.len());
For order queries, see Step 8. get_positions is also paginated via cursor + limit.
Full Flow Recap
The minimal integration flow is:
- Create an API Key in the App / web
- Authorize an Agent in the App
- Install the SDK
- Configure environment variables
- Initialize the Client
- Query Events and Markets
- Get the assetId of the YES / NO outcome and submit an order with it
- Query order status
- Cancel the order if needed
- Query positions and balance after the fill
Notes
| Topic | Description |
|---|---|
| Heartbeat | If you run a bot, set up a heartbeat to auto-cancel all orders when the bot disconnects. Send a pre-signed cancel-all every < 5 minutes; see the SDK docs for details. |
| Price tick | See the table below. |
| Nonce | Use the current millisecond timestamp. Each nonce can be used only once. |
| Timestamp skew | OK-ACCESS-TIMESTAMP must be within 30 seconds of the server time, otherwise error code 50102 is returned. |
Price Tick Rules
| Price range | Min precision | Example |
|---|---|---|
| 0.04 - 0.96 | 0.01 | 0.04, 0.55, 0.96 |
| < 0.04 or > 0.96 | 0.001 | 0.039, 0.962 |
FAQ
| Issue | Possible cause | Resolution |
|---|---|---|
assetId is null |
The market is not yet registered | Wait until assetId returns a valid value before placing orders |
| Order fails | Market not tradable, insufficient balance, signature error, or invalid parameters | Check the error code and the order failure reason |
| Signature fails | The Agent private key is incorrect, or the signed content does not match the request body | Confirm the Agent is authorized and use the SDK to build the signature |
| Order not found | Wrong order ID, or the order has already ended | Use the order list endpoint to confirm the order status |
| Order still not cancelled after cancel | The cancellation is being processed asynchronously | Wait for the status to update from PENDING_CANCEL to CANCELLED |
REST API
The REST API lets you query events, markets, tickers, order books, orders, trades, positions, and balances, and submit write operations such as place order, cancel order, Split, Merge, and Redeem. Developers can complete the full trading loop through the REST API: discover markets, obtain the assetId, submit orders, query order status, manage positions, and handle settlement-related operations. The base URL for all endpoints is https://www.okx.com.
1. Events & Markets API
Data Structures
OutcomeResp β Outcome option
| Field | Type | Description |
|---|---|---|
| tokenId | string / null | Conditional token contract address; null before deployment |
| assetId | string / null | TradeZone asset ID (used when placing orders); null before deployment |
| name | string | Outcome name, e.g. "Yes", "No" |
| price | string | Current price (string in [0, 1], e.g. "0.82") |
| bgColor | string | Button background color (Hex); for a sports moneyline Yes outcome this is the home-team theme color; otherwise null |
MarketResp β Market object
| Field | Type | Description |
|---|---|---|
| id | string | Globally unique market ID |
| marketId | string | Market's unique ID in TradeZone (used when placing orders) |
| negRisk | boolean | Whether this is a mutually exclusive (negRisk) market |
| status | string | Market status: active / paused / settling / resolved |
| settleStage | int | Settlement stage: 0=not started, 1=first-round publication, 2=first-round dispute, 3=second-round publication, 4=second-round dispute, 5=settled |
| question | string | Full market question |
| shortQuestion | string / null | Short market question |
| description | string | Market description |
| marketIcon | string / null | Market icon URL |
| bestBid | string / null | Best bid price (0β1); null if there are no bids |
| bestAsk | string / null | Best ask price (0β1); null if there are no asks |
| lastTradePrice | string / null | Last trade price (0β1); null if never traded |
| volume | string | Total volume (xp) |
| probability | string / null | Market Yes probability (decimal in [0, 1]); null before deployment |
| resolutionSources | string[] | List of resolution data source URLs |
| yesOutcome | OutcomeObject | Yes outcome option |
| noOutcome | OutcomeObject | No outcome option |
| startTime | string | Expected market start time (Unix ms) |
| endTime | string | Expected market end time (Unix ms) |
| resolveStartAt | string | First time the market entered the start_resolve state (Unix ms) |
| resolveAt | string | First time the market entered the resolved state (Unix ms) |
EventResp β Event object
| Field | Type | Description |
|---|---|---|
| id | string | Globally unique event ID |
| eventId | string | Event's unique ID in TradeZone |
| negRisk | boolean | Whether this is a negRisk event |
| status | string | Event status: active / paused / resolved |
| eventTitle | string | Event title |
| description | string | Event description |
| eventIcon | string / null | Event icon URL |
| volume | string | Sum of volume across all markets in the event (xp) |
| startTime | string / null | Trading start timestamp (ms) |
| endTime | string / null | Trading stop timestamp (ms) |
| createdAt | string | Event creation timestamp (ms) |
| totalMarketsCount | int | Total number of markets under the event |
| finalOutcomesMarketId | string / null | Winning market ID after settlement; null before settlement |
| markets | array<MarketResp> | Market list (the list endpoint returns at most the first 2; get the full list via GET /api/v5/predictions/events/{eventId}/markets) |
PaginationResp β Cursor pagination
| Field | Type | Description |
|---|---|---|
| nextCursor | string / null | Next-page cursor; null means this is the last page |
| hasMore | boolean | Whether more data is available |
| pageSize | int | Number of items returned this time |
1.1 Get events
Get the prediction market event list, with multi-dimensional filtering and sorting by status, etc.
HTTP Request
GET /api/v5/predictions/events
Request Parameters (Query)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| status | string | No | active |
Event status filter: active / resolved |
| sort | string | No | volume_24h |
Sort order: volume / volume_24h / ending_soon / newest |
| tag | string | No | - | Sports tag ID filter (from GET /api/v5/predictions/sports/tags) |
| leagueId | string | No | - | League ID filter (from GET /api/v5/predictions/sports/tags/{tagId}/leagues) |
| cursor | string | No | - | Pagination cursor; omit on the first request |
| pageSize | int | No | 10 | Items per page (max 50) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"events": [
{
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "Will BTC exceed $100k by end of 2026?",
"description": "This event tracks whether Bitcoin will cross $100,000.",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "1250000.00",
"startTime": 1700000000000,
"endTime": 1798761600000,
"createdAt": 1710000000000,
"totalMarketsCount": 1,
"finalOutcomesMarketId": null,
"markets": [
{
"id": "1",
"marketId": "1",
"oddsType": "points",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
],
"pagination": {
"nextCursor": "eyJpZCI6MTAwfQ",
"hasMore": true,
"pageSize": 10
}
}
}
| Field | Type | Description |
|---|---|---|
| events | array<EventResp> | Event list; each event's markets returns at most the first 2 markets |
| pagination | PaginationResp | Cursor pagination info |
1.2 Get a single event
Get the full information for a specified event. The markets field returns all markets under the event (not truncated).
HTTP Request
GET /api/v5/predictions/events/{eventId}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string | Yes | Event ID |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "NBA Finals 2026 - Lakers vs Celtics",
"description": "Who will win the 2026 NBA Championship?",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "3200000.00",
"startTime": 1748000000000,
"endTime": 1750000000000,
"createdAt": 1710000000000,
"totalMarketsCount": 3,
"finalOutcomesMarketId": null,
"markets": [ ]
}
}
Returns the full EventResp, with markets containing all markets under the event.
1.3 Get markets
Get all markets under a specified event, without pagination.
HTTP Request
GET /api/v5/predictions/events/{eventId}/markets
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| eventId | string | Yes | Event ID |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"markets": [
{
"id": "1",
"marketId": "1",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
}
| Field | Type | Description |
|---|---|---|
| markets | array<MarketResp> | All markets under the event |
1.4 Get a single market
Get the detailed information for a specified market.
HTTP Request
GET /api/v5/predictions/markets/{marketId}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| marketId | string | Yes | Market TradeZone ID |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"id": "1",
"marketId": "1",
"negRisk": false,
"status": "resolved",
"settleStage": 5,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": null,
"bestAsk": null,
"lastTradePrice": "0.98",
"volume": "1250000.00",
"probability": null,
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "1.00",
"finalResult": true
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.00",
"finalResult": false
}
}
}
Returns the full MarketResp.
1.5 Search events
Full-text search of events by keyword. Returns the matching event list, with cursor pagination.
HTTP Request
GET /api/v5/predictions/events/search
Request Parameters (Query)
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| keyword | string | Yes | - | Search keyword, matched against event title and description |
| cursor | string | No | - | Pagination cursor; omit on the first request |
| pageSize | int | No | 10 | Items per page (max 50) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"events": [
{
"id": "100",
"eventId": "100",
"negRisk": false,
"status": "active",
"eventTitle": "Will BTC exceed $100k by end of 2026?",
"description": "This event tracks whether Bitcoin will cross $100,000.",
"eventIcon": "https://cdn.example.com/events/100.png",
"volume": "1250000.00",
"startTime": 1700000000000,
"endTime": 1798761600000,
"createdAt": 1710000000000,
"totalMarketsCount": 1,
"finalOutcomesMarketId": null,
"markets": [
{
"id": "1",
"marketId": "1",
"oddsType": "points",
"negRisk": false,
"status": "active",
"settleStage": 0,
"question": "Will BTC exceed $100k by end of 2026?",
"shortQuestion": "BTC > $100k?",
"description": "Resolves YES if BTC price exceeds $100,000 before Jan 1, 2027.",
"marketIcon": null,
"bestBid": "0.64",
"bestAsk": "0.66",
"lastTradePrice": "0.65",
"volume": "1250000.00",
"probability": "0.65",
"resolutionSources": ["https://coinmarketcap.com"],
"yesOutcome": {
"tokenId": "0xabc...001",
"assetId": "1",
"name": "Yes",
"price": "0.65",
"finalResult": null
},
"noOutcome": {
"tokenId": "0xabc...002",
"assetId": "2",
"name": "No",
"price": "0.35",
"finalResult": null
}
}
]
}
],
"pagination": {
"nextCursor": "eyJpZCI6MTAwfQ",
"hasMore": true,
"pageSize": 10
}
}
}
| Field | Type | Description |
|---|---|---|
| events | array<EventResp> | Event list |
| pagination | PaginationResp | Cursor pagination info |
2. Price API
2.1 Get ticker
Get the latest ticker for a single prediction market instrument, used to display the last trade price in the order book.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
instId |
String | Yes | Prediction market yesAssetId |
HTTP Request
GET /api/v5/market/ticker?instId={yesAssetId}
Response Parameters
Response example
{
"code": "0",
"msg": "",
"data": [{
"instType": "SPOT",
"instId": "{yesAssetId}",
"last": "0.65",
"lastSz": "100",
"askPx": "0.66",
"askSz": "500",
"bidPx": "0.64",
"bidSz": "300",
"open24h": "0.60",
"high24h": "0.70",
"low24h": "0.55",
"vol24h": "10000",
"volCcy24h": "6500",
"sodUtc0": "0.62",
"sodUtc8": "0.61",
"ts": "1711900800000"
}]
}
| Field | Type | Description |
|---|---|---|
instType |
String | Instrument type |
instId |
String | Instrument ID |
last |
String | Last trade price |
lastSz |
String | Last trade size |
askPx |
String | Best ask price |
askSz |
String | Size at the best ask |
bidPx |
String | Best bid price |
bidSz |
String | Size at the best bid |
open24h |
String | 24h open price |
high24h |
String | 24h high price |
low24h |
String | 24h low price |
vol24h |
String | 24h volume (in contracts) |
volCcy24h |
String | 24h volume (in quote currency) |
sodUtc0 |
String | Open price at UTC 0 |
sodUtc8 |
String | Open price at UTC+8 |
ts |
String | Data update time (Unix ms timestamp) |
2.2 Get candlesticks
Query historical candlestick data for a prediction market instrument. Candlesticks are grouped and returned by the requested bar size; at most 1,440 candles are available per bar size.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
instId |
String | Yes | Prediction market yesAssetId |
bar |
String | No | Bar size, default 1m. Allowed values: 1m 3m 5m 15m 30m 1H 2H 4H 6H 12H 1D 1W 1M |
after |
String | No | Pagination cursor; return data before this timestamp (Unix ms) |
before |
String | No | Pagination cursor; return data after this timestamp (Unix ms) |
limit |
String | No | Items per page, max 100, default 100 |
HTTP Request
GET /api/v5/market/candles?instId={yesAssetId}&bar=1m&limit=100
Response Data
(data is a 2D array; each record's field order is as follows)
Response example
{
"code": "0",
"msg": "",
"data": [
["1711900800000", "0.65", "0.67", "0.63", "0.66", "2000", "1300", "1300", "1"],
["1711900740000", "0.63", "0.66", "0.62", "0.65", "1800", "1170", "1170", "1"]
]
}
| Index | Field | Type | Description |
|---|---|---|---|
| 0 | ts |
String | Candle open time (Unix ms timestamp) |
| 1 | o |
String | Open price |
| 2 | h |
String | High price |
| 3 | l |
String | Low price |
| 4 | c |
String | Close price |
| 5 | vol |
String | Volume (in contracts) |
| 6 | volCcy |
String | Volume (in quote currency) |
| 7 | volCcyQuote |
String | Volume (in quote currency) |
| 8 | confirm |
String | Candle state: 0 not confirmed, 1 confirmed |
2.3 Get order book
Query a bid/ask depth snapshot for a prediction market instrument.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
instId |
String | Yes | Prediction market yesAssetId |
sz |
String | No | Number of depth levels; max 400 per side, i.e. up to 800 across both sides. If omitted, defaults to 1 level. |
HTTP Request
GET /api/v5/market/pm-books?instId={yesAssetId}&sz=400
Response Parameters
Response example
{
"code": "0",
"msg": "",
"data": [
{
"asks": [
["67364.1","0.45478048","5"]
],
"bids": [
["67364","1.72315936", "17"]
],
"ts": "1774943488756",
"seqId": 74487243135
}
]
}
| Field | Type | Description |
|---|---|---|
asks |
Array | Ask levels, sorted by price low to high |
bids |
Array | Bid levels, sorted by price high to low |
ts |
String | Depth snapshot time (Unix ms timestamp) |
seqId |
Number | Order book version number; not relevant for prediction markets |
Each record in the asks / bids arrays has the format: [price, size, order count]
3. Orders API
3.1 Place order
Submit a signed order request. The developer constructs the calldata and signs it with their own wallet private key (ECDSA); after the server validates it, the order is written to the database and forwarded directly to TradeZone (it does not go through the AA Wallet).
HTTP Request
POST /api/v5/predictions/orders
Request Body
Request example
{
"action": {
"type": "placeOrder",
"grouping": "na",
"orders": [{
"assetId": "1",
"marketType": "prediction",
"side": "buy",
"price": "0.65",
"size": "100",
"reduceOnly": false,
"clientOrderId": "0x0197a98c91312671ca83f15ccbd5186f",
"orderType": { "limit": { "tif": "gtc" } }
}]
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Order action |
| action.type | string | Yes | Fixed "placeOrder" |
| action.grouping | string | Yes | Fixed "na" |
| action.orders | array | Yes | Order list (one order per request) |
| action.orders[].assetId | string | Yes | TradeZone asset ID (e.g. "1") |
| action.orders[].marketType | string | Yes | Fixed "prediction" |
| action.orders[].side | string | Yes | "buy" / "sell" |
| action.orders[].price | string | Yes | Order price (e.g. "0.65") |
| action.orders[].size | string | Yes | Order size (e.g. "100") |
| action.orders[].clientOrderId | string | Yes | Client order ID; used to distinguish region |
| action.orders[].reduceOnly | boolean | No | Whether reduce-only, default false |
| action.orders[].sizeType | string | No | "base" (default) / "quote" |
| action.orders[].orderType | object | Yes | Order type |
| action.orders[].orderType.limit.tif | string | Yes | "gtc" / "gtd" / "ioc" / "fok" / "alo" |
| nonce | long | Yes | Request timestamp (ms), anti-replay |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
TIF (Time In Force)
| JSON value | Trade structure | Description |
|---|---|---|
"gtc" |
"tif": "gtc" | Good-Til-Cancel; the order stays until filled or cancelled |
"gtd" |
"tif": { "gtd": { "expiresAfter": 1700000005000 } }, | Good-Til-Date; the order stays until it expires at expiresAfter, or is filled/cancelled |
"ioc" |
"tif": "ioc" | Immediate-Or-Cancel (i.e. FAK); fills as much as possible immediately, then cancels the remainder. For simulated market orders only; must be paired with a price protection upper/lower bound (price) |
"fok" |
"tif": "fok" | Fill-Or-Kill; fully filled or fully cancelled. For simulated market orders only; must be paired with a price protection upper/lower bound (price) |
"alo" |
"tif": "alo" | Add-Liquidity-Only (Post-Only); maker-only order, rejected if it would fill immediately |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...789"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
3.2 Cancel a single order
Cancel an active order. The developer constructs the cancel calldata and signs it; after validating the order's ownership and status, the server submits it to TradeZone.
HTTP Request
POST /api/v5/predictions/orders/cancel
Request Body
Request example
{
"action": {
"type": "cancel",
"cancels": [{
"assetId": "1",
"marketType": "prediction",
"oid": "12345",
"clientOrderId": "0x"
}]
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Cancel action |
| action.type | string | Yes | Fixed "cancel" |
| action.cancels | array | Yes | Cancel list (one per request) |
| action.cancels[].assetId | string | Yes | TradeZone asset ID (e.g. "1") |
| action.cancels[].marketType | string | Yes | Fixed "prediction" |
| action.cancels[].oid | string | No | Order ID; exactly one of oid and clientOrderId is required |
| action.cancels[].clientOrderId | string | No | Client order ID; exactly one of oid and clientOrderId is required |
| nonce | long | Yes | Request timestamp (ms) |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
3.3 Get a single order
Get the full details of a specified order (active or historical).
HTTP Request
GET /api/v5/predictions/orders/{orderId}
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| orderId | string | Yes | Order ID |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"id": "1",
"oid": "1",
"clientOrderId": "1",
"marketId": "1",
"assetId": "1",
"side": "BUY",
"orderType": "GTC",
"sizeType": "BASE",
"size": "100",
"price": "0.65",
"expiration": null,
"txHash": "0xdef...789",
"status": "ACTIVE",
"filledSize": "40",
"filledAmount": "26",
"failReason": null,
"cancelReason": null,
"oddsType": "points",
"createdAt": "1710000000000",
"updatedAt": "1710000000000"
}
}
| Field | Type | Description |
|---|---|---|
| id | string | Order ID |
| oid | string | Order oid |
| clientOrderId | string | Order clientOrderId |
| marketId | string | Market ID |
| tokenId | string | YES/NO token address |
| assetId | string | TradeZone asset ID |
| side | string | BUY / SELL |
| orderType | string | GTC / GTD / FOK / IOC / POST_ONLY |
| sizeType | string | BASE / QUOTE |
| size | string | Original order size |
| price | string | Order price |
| expiration | string | GTD expiry timestamp (ms); null for non-GTD |
| txHash | string | Transaction hash for the current stage |
| status | string | Order status (see table below) |
| filledSize | string | Filled token amount |
| filledAmount | string | Filled xp amount |
| failReason | string | Failure reason (present only when status = FAILED) |
| cancelReason | string | Cancel reason (system-initiated cancellations) |
| oddsType | string | Odds type: points = points odds |
| createdAt | string | Order creation timestamp (ms) |
| updatedAt | string | Last update timestamp (ms) |
Order Status
| Status | Description |
|---|---|
| PENDING_PLACE | Submitted, waiting for on-chain confirmation |
| ACTIVE | Active open order |
| PENDING_CANCEL | Cancellation submitted, waiting for on-chain confirmation |
| FILLED | Fully filled |
| PARTIALLY_FILLED | Partially filled then cancelled or expired |
| FAILED | On-chain transaction failed |
| CANCELLED | Cancelled by user or system |
| EXPIRED | GTD order expired |
3.4 Get user orders
Query the current authenticated user's order list, with filtering and pagination.
HTTP Request
GET /api/v5/predictions/orders
Request Parameters (Query)
| Parameter | Type | Required | Description |
|---|---|---|---|
| marketId | string | No | Filter by market ID |
| status | string | No | open (PENDING_PLACE / ACTIVE / PENDING_CANCEL), closed (FILLED / PARTIALLY_FILLED / CANCELLED / EXPIRED / FAILED); defaults to open |
| cursor | string | No | Pagination cursor |
| limit | int | No | Items per page (default 20, max 50) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"id": "1",
"oid": "1",
"clientOrderId": "1",
"marketId": "string",
"assetId": "1",
"side": "BUY",
"orderType": "GTC",
"sizeType": "BASE",
"size": "100",
"price": "0.65",
"expiration": null,
"txHash": "0xdef...789",
"status": "ACTIVE",
"filledSize": "40",
"filledAmount": "26",
"failReason": null,
"cancelReason": null,
"oddsType": "points",
"createdAt": "1710000000000",
"updatedAt": "1710000000000"
}
],
"nextCursor": "eyJpZCI6MTIzfQ",
"hasNext": false
}
}
| Field | Type | Description |
|---|---|---|
| list[].id | string | Order ID |
| list[].marketId | string | Market ID |
| list[].oid | string | Order oid |
| list[].clientOrderId | string | Order clientOrderId |
| list[].tokenId | string | YES/NO token address |
| list[].assetId | string | TradeZone asset ID |
| list[].side | string | BUY / SELL |
| list[].orderType | string | GTC / GTD / FOK / IOC / POST_ONLY |
| list[].sizeType | string | BASE / QUOTE |
| list[].size | string | Original order size |
| list[].price | string | Order price |
| list[].expiration | string | GTD expiry timestamp (ms); null for non-GTD |
| list[].txHash | string | Transaction hash for the current stage |
| list[].status | string | Order status: PENDING_PLACE / ACTIVE / PENDING_CANCEL / FILLED / PARTIALLY_FILLED / FAILED / CANCELLED / EXPIRED |
| list[].filledSize | string | Filled token amount |
| list[].filledAmount | string | Filled xp amount |
| list[].failReason | string | Failure reason (present only when status = FAILED) |
| list[].cancelReason | string | Cancel reason (system-initiated cancellations) |
| list[].oddsType | string | Odds type: points = points odds |
| list[].createdAt | String | Order creation timestamp (ms) |
| list[].updatedAt | string | Last update timestamp (ms) |
| nextCursor | string | Next-page cursor |
| hasNext | boolean | Whether more data is available |
3.5 Cancel all / market orders
Cancel the current authenticated user's active orders. When assetIds is an empty list, cancel orders across all markets; when specific values are passed, cancel only the orders in the markets of the corresponding assets.
HTTP Request
POST /api/v5/predictions/orders/cancel-all
Request Body
Request example (cancel by market)
{
"action": {
"type": "cancelAll",
"assetIds": [1, 2, 5],
"marketType": "prediction"
},
"nonce": 1708929600000,
"expiresAfter": 1708929660000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
Request example (cancel all)
{
"action": {
"type": "cancelAll",
"assetIds": [],
"marketType": "prediction"
},
"nonce": 1708929600000,
"expiresAfter": 1708929660000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Cancel action |
| action.type | string | Yes | Fixed "cancelAll" |
| action.assetIds | array | Yes | Asset ID list; pass an empty list to cancel all orders, or specific values to cancel by the corresponding markets |
| action.marketType | string | Yes | Market type, fixed "prediction" |
| nonce | long | Yes | Request timestamp (ms) |
| expiresAfter | long | Yes | Expiry timestamp (ms) |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
3.6 Send heartbeat
A heartbeat mechanism that protects the developer's active orders. The request body carries a pre-signed cancelAll action with nonce set to the current time + 5 minutes. The server stores this signature; if the developer does not renew the heartbeat within 5 minutes, the system automatically uses this signature to cancel all orders.
The developer should keep calling this endpoint (recommended interval < 5 minutes), each time overwriting the previous pre-signature with a new nonce (current time + 5 minutes).
HTTP Request
POST /api/v5/predictions/heartbeat
Request Body
Request example
{
"action": {
"type": "cancelAll",
"assetIds": [],
"marketType": "prediction"
},
"nonce": 1708929900000,
"expiresAfter": 1708929960000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Cancel action; same structure as POST /api/v5/predictions/orders/cancel-all |
| action.type | string | Yes | Fixed "cancelAll" |
| action.assetIds | array | Yes | Always pass an empty list []; cancels all orders when the heartbeat fires |
| action.marketType | string | Yes | Market type, fixed "prediction" |
| nonce | long | Yes | Timestamp of current time + 5 minutes (ms) |
| expiresAfter | long | Yes | Expiry timestamp (ms) |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"serverTimestamp": 1708929600000,
"expireAt": 1708929900000
}
}
| Field | Type | Description |
|---|---|---|
| serverTimestamp | long | Server current timestamp (ms) |
| expireAt | long | This heartbeat's expiry timestamp (ms) |
Order Semantics
Order Mode Support Matrix
Support matrix
| Scenario | sizeType | tif | size | price semantics | Supported |
|---|---|---|---|---|---|
| Limit buy | base |
gtc / gtd / ioc / fok / alo |
user-entered shares | user-entered limit price | β |
| Limit sell | base |
gtc / gtd / ioc / fok / alo |
user-entered shares | user-entered limit price | β |
| Market buy (by size) | base |
ioc / fok |
user-entered shares | system-simulated worst fill price (protection price) from the book | β |
| Market buy (by amount) | quote |
ioc |
user-entered notional amount (xp) | system-simulated worst fill price (protection price) from the book | β |
| Market sell (by size) | base |
ioc / fok |
user-entered shares | system-simulated worst fill price (protection price) from the book | β |
| Market buy (by amount) + FOK | quote |
fok |
β | β | β Not supported |
Key rules
In the prediction market,
iocis equivalent to the commonly usedfak: it fills as much as possible, cancels the remainder, and never leaves a resting order.fokis only supported when ordering by shares: the combination ofsizeType=quoteandtif=fokis rejected by the server.alo(Post Only) applies only to limit orders: combining it withsizeType=quoteor a market order is rejected.A market order is not an "unprotected order": at order time the system simulates a
worstPricefrom the book and submits it to TradeZone as theprice. The actual fill price will never be worse thanworstPrice.tif=gtdmust includeexpiration: the field path isaction.orders[].orderType.limit.expiration, of typeString(Unix ms timestamp, 13 digits), an absolute time. If missing or invalid, it currently throws10001 PARAM_ERROR.
Error returns for unsupported combinations
| Trigger | Current error code |
|---|---|
sizeType=quote and tif=fok |
10001 PARAM_ERROR |
sizeType=quote and tif β {gtc, gtd, alo} |
10001 PARAM_ERROR |
tif=gtd without expiration |
10001 PARAM_ERROR |
Minimum Order Amount & Liquidation Exception
Minimum order amount rule
- The prediction market enforces a minimum notional amount per order: 1 xp.
Sell-All Exception
After a user sells a position in one direction, if the remaining total position for that assetId (= available + frozen) is 0, it is treated as a sell-all (liquidation).
In the sell-all scenario, the order is allowed even if the notional fill amount is below 1 xp.
Price Unit & Precision Rules
Valid range
| Condition | Allowed |
|---|---|
price β€ 0 |
Rejected |
price β₯ 1 |
Rejected |
0 < price < 1 |
Allowed, proceeds to precision validation |
Segmented precision rules
Precision is split into two segments by [0.04, 0.96] and outside it:
| Range | Max decimal places | Equivalent cent range | Example (allowed) | Example (rejected) |
|---|---|---|---|---|
0 < price < 0.04 |
3 places | (0, 4) cents |
0.001 / 0.025 / 0.039 |
0.0125 (4 places) |
0.04 β€ price β€ 0.96 |
2 places (tick=0.01) | [4, 96] cents |
0.04 / 0.25 / 0.96 |
0.045 / 0.123 / 0.961 |
0.96 < price < 1 |
3 places | (96, 100) cents |
0.962 / 0.9875 / 0.999 |
0.99875 (4 places) |
ClientOrderId Generation Specification
clientOrderId (cloid for short) is the order's unique identifier on chain. On-chain events are broadcast globally, and consumers in each region use the region/env prefix in the cloid to filter out the orders that belong to them. Any cloid generated by any integrator must follow this specification, otherwise orders will be misattributed.
Format
The format is 0x{region}{env}{random}:
| Segment | Length (chars) | Content | Description |
|---|---|---|---|
| prefix | 2 | 0x |
Fixed literal |
| region | 1 | 1 hex char | Region code |
| env | 1 | 1 hex char | Environment code |
| random | 30 | 30 hex chars | Random number (lowercase) |
The total length is fixed at 34 characters, all lowercase hex (0-9, a-f).
region / env code table
| region | Meaning |
|---|---|
0 |
HK |
1 |
US |
2 |
EU |
| env | Meaning |
|---|---|
1 |
Production |
Each value is a single hex char; currently 0/1/2 are used, and future expansion will not exceed f (15).
random generation requirements
- Entropy: β₯ 120 bits (30 hex = 120 bits)
- Source: must use a cryptographically secure random source or equivalent strength (e.g. UUIDv4,
crypto.randomBytes,secrets, etc.) - Encoding: lowercase hex, zero-padded on the high bits if short
Reference implementations
Java (UUID-derived)
UUID u = UUID.randomUUID();
long hi = u.getMostSignificantBits();
long lo = u.getLeastSignificantBits();
StringBuilder sb = new StringBuilder("0x");
sb.append(Integer.toHexString(region & 0xF));
sb.append(Integer.toHexString(env & 0xF));
for (int i = 14; i >= 0; i--) {
sb.append(Character.forDigit((int)((hi >>> (i * 4)) & 0xF), 16));
}
for (int i = 14; i >= 0; i--) {
sb.append(Character.forDigit((int)((lo >>> (i * 4)) & 0xF), 16));
}
return sb.toString();
Node.js
const crypto = require('crypto');
function generate(region, env) {
const rand = crypto.randomBytes(15).toString('hex'); // 30 hex
return `0x${region.toString(16)}${env.toString(16)}${rand}`;
}
import secrets
def generate(region: int, env: int) -> str:
return f"0x{region:x}{env:x}{secrets.token_hex(15)}"
Go
import (
"crypto/rand"
"encoding/hex"
"fmt"
)
func Generate(region, env int) (string, error) {
b := make([]byte, 15)
if _, err := rand.Read(b); err != nil {
return "", err
}
return fmt.Sprintf("0x%x%x%s", region, env, hex.EncodeToString(b)), nil
}
Server-side parsing and attribution
The receiver decides whether a cloid belongs to the current environment using the following rules:
if cloid is null/empty
or length < 4
or does not start with "0x"
or cloid[2], cloid[3] are not valid hex chars:
treat as HK-staging (region=0, env=0) <- fallback rule
else:
region = parse_hex(cloid[2])
env = parse_hex(cloid[3])
check whether it equals the current environment's (region, env)
FAQ
Heartbeat: if you run a bot, set up a heartbeat to automatically cancel all orders when the bot disconnects. Send a pre-signed cancel-all every < 5 minutes; see the API docs for details.
Nonce: use the current millisecond timestamp. Each nonce can be used only once.
Timestamp drift: OK-ACCESS-TIMESTAMP must not differ from server time by more than 30 seconds, otherwise error code 50102 is returned.
4. Positions Operations API
4.1 Split (xp β YES + NO)
Split xp into an equal amount of YES and NO conditional token pairs. The developer constructs and signs the calldata; after validation, the server submits it directly to TradeZone.
HTTP Request
POST /api/v5/predictions/positions/split
Request Body
Request example
{
"action": {
"type": "predictionSplit",
"marketId": "1",
"size": "100000000"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Split action |
| action.type | string | Yes | Fixed "predictionSplit" |
| action.marketId | string | Yes | Market ID (e.g. "1") |
| action.size | string | Yes | xp amount (minimal-unit string, e.g. "100000000") |
| nonce | long | Yes | Request timestamp (ms), anti-replay |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
4.2 Merge (YES + NO β xp)
Merge equal amounts of YES and NO conditional tokens back into xp (the inverse of Split). The developer constructs and signs the calldata; after validation, the server submits it directly to TradeZone.
HTTP Request
POST /api/v5/predictions/positions/merge
Request Body
Request example
{
"action": {
"type": "predictionMerge",
"marketId": "1",
"size": "100000000"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Merge action |
| action.type | string | Yes | Fixed "predictionMerge" |
| action.marketId | string | Yes | Market ID (e.g. "1") |
| action.size | string | Yes | Merge amount (minimal-unit string, e.g. "100000000") |
| nonce | long | Yes | Request timestamp (ms), anti-replay |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
4.3 Redeem (redeem xp after settlement)
After a market settles, redeem xp 1:1 with the winning conditional tokens. The redeem amount is not passed in; by default it redeems all of the user's winning tokens.
HTTP Request
POST /api/v5/predictions/positions/redeem
Request Body
Request example
{
"action": {
"type": "predictionRedeem",
"marketId": "1"
},
"nonce": 1708929600000,
"signature": {
"Ecdsa": {
"r": "0x...",
"s": "0x...",
"v": 1
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
| action | object | Yes | Redeem action |
| action.type | string | Yes | Fixed "predictionRedeem" |
| action.marketId | string | Yes | Market ID (e.g. "1") |
| nonce | long | Yes | Request timestamp (ms), anti-replay |
| signature | object | Yes | Signature object |
| signature.Ecdsa | object | Yes | ECDSA signature components |
| signature.Ecdsa.r | string | Yes | Signature r value ("0x...") |
| signature.Ecdsa.s | string | Yes | Signature s value ("0x...") |
| signature.Ecdsa.v | int | Yes | Signature v value (0 or 1) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"txHash": "0xdef...abc"
}
}
| Field | Type | Description |
|---|---|---|
| txHash | string | TradeZone transaction hash |
5. Trade History API
5.1 Query trades
Query the current authenticated user's trade (fill) records. The server resolves the userId from the login session and internally converts it to an address for the query.
HTTP Request
GET /api/v5/predictions/trades
Request Parameters (Query)
| Parameter | Type | Required | Description |
|---|---|---|---|
| marketId | string | No | Filter by market ID |
| side | string | No | Filter by side: BUY / SELL |
| startTime | long | No | Start timestamp (ms), inclusive |
| endTime | long | No | End timestamp (ms), exclusive |
| cursor | string | No | Pagination cursor |
| limit | int | No | Items per page (default 20, max 100) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"tradeId": "1",
"orderId": "1",
"marketId": "1",
"tokenId": "0xabc...001",
"side": "BUY",
"size": "50",
"amount": "32.5",
"price": "0.65",
"fee": "0.065",
"role": "TAKER",
"txHash": "0xdef...789",
"createdAt": 1710000030000
}
],
"nextCursor": "eyJpZCI6MTAwMDF9",
"hasNext": false
}
}
| Field | Type | Description |
|---|---|---|
| list[].tradeId | string | Trade record ID |
| list[].orderId | string | Associated order ID |
| list[].marketId | string | Market ID |
| list[].tokenId | string | Token address (YES or NO) |
| list[].side | string | BUY / SELL |
| list[].size | string | Filled token amount |
| list[].amount | string | Filled xp amount |
| list[].price | string | Fill price |
| list[].fee | string | Fee (xp) |
| list[].role | string | MAKER / TAKER |
| list[].txHash | string | On-chain transaction hash |
| list[].createdAt | string | Trade timestamp (ms) |
| nextCursor | string | Next-page cursor |
| hasNext | boolean | Whether more data is available |
6. Positions Query API
6.1 Query current positions
Query the current authenticated user's active positions.
HTTP Request
GET /api/v5/predictions/positions?status=open
6.2 Query closed positions
Query the current authenticated user's exited or settled positions.
HTTP Request
GET /api/v5/predictions/positions?status=closed
6.3 Query positions in a specified market
Query the current authenticated user's positions in a specified market.
HTTP Request
GET /api/v5/predictions/positions?marketId={marketId}
6.4 Unified endpoint
All three scenarios above are implemented through the same endpoint + query parameter combinations:
HTTP Request
GET /api/v5/predictions/positions
Request Parameters (Query)
| Parameter | Type | Required | Description |
|---|---|---|---|
| status | string | No | open (active positions), closed (liquidated); returns all if omitted |
| marketId | long | No | Filter by market ID |
| cursor | string | No | Pagination cursor |
| limit | int | No | Items per page (default 20, max 100) |
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": {
"list": [
{
"id": "100001",
"tokenId": "0xabc...001",
"marketId": "1",
"tokenIndex": "1",
"tokenName": "Yes",
"size": "500",
"availableSize": "480",
"value": "325",
"avgPrice": "0.62",
"unRealizedPnl": "15",
"unRealizedPnlPercentage": "0.048",
"title": "Will BTC exceed $100k by end of 2026?",
"icon": "https://cdn.example.com/markets/1.png",
"eventId": "100",
"winningToken": null,
"positionStatus": 1,
"oddsType": "points",
"curPrice": "0.65",
"realizedPnl": "12.5",
"realizedPnlPercentage": "0.04"
}
],
"nextCursor": "eyJpZCI6MTAwMDAxfQ",
"hasNext": false
}
}
| Field | Type | Description |
|---|---|---|
| list[].id | string | Position ID |
| list[].tokenId | string | Token ID |
| list[].marketId | string | Market ID |
| list[].tokenIndex | string | Token direction ("1" = YES, "2" = NO) |
| list[].tokenName | string | Token name ("Yes" / "No") |
| list[].size | string | Position size (= remain) |
| list[].availableSize | string | Available position size (= remain β frozen, the amount not locked by resting SELL orders) |
| list[].value | string | Current value (curPrice Γ size) |
| list[].avgPrice | string | Weighted average entry cost |
| list[].unRealizedPnl | string | Unrealized PnL |
| list[].unRealizedPnlPercentage | string | Unrealized PnL percentage |
| list[].title | string | Market question text |
| list[].icon | string | Market icon URL |
| list[].eventId | string | Parent event ID |
| list[].winningToken | string | Winning token ID after settlement; null before settlement |
| list[].positionStatus | integer | Position status code (see PositionStatusEnum) |
| list[].oddsType | string | Odds type: points = points odds |
| list[].curPrice | string | Current real-time token price |
| list[].realizedPnl | string | Realized PnL |
| list[].realizedPnlPercentage | string | Realized PnL percentage |
| nextCursor | string | Next-page cursor |
| hasNext | boolean | Whether more data is available |
7. Account Balance API
7.1 Query account balance
Query the current authenticated user's xp balance.
HTTP Request
GET /api/v5/predictions/balance
Request parameters: none
Response (data):
Response example
{
"code": 0,
"message": "OK",
"data": [
{ "oddsType": "points", "balance": "2.2", "available": "2.2" }
]
}
data is an array; each element corresponds to the balance of one odds type:
| Field | Type | Description |
|---|---|---|
| oddsType | string | Odds type: points = points odds |
| balance | string | Total balance |
| available | string | Available balance (total balance β frozen amount) |
8. Rate Limits
Events & Markets API
| Endpoint | Limit |
|---|---|
GET /api/v5/predictions/events |
20 times / 1s |
GET /api/v5/predictions/events/{eventId} |
20 times / 1s |
GET /api/v5/predictions/events/{eventId}/markets |
20 times / 1s |
GET /api/v5/predictions/markets/{marketId} |
20 times / 1s |
GET /api/v5/predictions/events/search |
10 times / 1s |
Price API (market data)
| Endpoint | Limit |
|---|---|
GET /api/v5/market/ticker |
10 times / 1s |
GET /api/v5/market/candles |
20 times / 1s |
GET /api/v5/market/pm-books |
20 times / 1s |
Orders API
Query
| Endpoint | Limit |
|---|---|
GET /api/v5/predictions/orders/{orderId} |
20 times / 1s |
GET /api/v5/predictions/orders |
20 times / 1s |
Write
| Endpoint | Limit |
|---|---|
POST /api/v5/predictions/orders |
50 times / 1s |
POST /api/v5/predictions/orders/cancel |
20 times / 1s |
POST /api/v5/predictions/orders/cancel-all |
1 time / 1h |
POST /api/v5/predictions/heartbeat |
1 time / 1s |
Positions Operations API
| Endpoint | Limit |
|---|---|
POST /api/v5/predictions/positions/split |
5 times / 1s |
POST /api/v5/predictions/positions/merge |
5 times / 1s |
POST /api/v5/predictions/positions/redeem |
5 times / 1s |
Trade History / Positions Query / Balance
| Endpoint | Limit |
|---|---|
GET /api/v5/predictions/trades |
10 times / 1s |
GET /api/v5/predictions/positions |
10 times / 1s |
GET /api/v5/predictions/balance |
10 times / 1s |
Rate-limit response
- HTTP Status:
429 Too Many Requests - Business error code:
50011 RATE_LIMIT_EXCEEDED - Clients are advised to retry with exponential backoff: 1s first, doubling each time, up to a max of 30s; if 5 consecutive retries still return 429, stop and alert.
SDK API Reference
A complete reference for every public method, request body, response struct, error variant, and WebSocket channel exposed by the okx-outcomes-sdk Rust crate. The README is a quick start; this document is the detailed version.
Conventions used throughout:
- All Rust types are
pubtypes re-exported fromokx_outcomes_sdk::*(the specific module path is given at the top of each section). - "Authentication" for each REST call refers to the OKX REST
OK-ACCESS-*headers written after signing locally with HMAC-SHA256 viaApiCredentials. The SDK signs locally; the secret never leaves the process. - "Write" operations (place / cancel order, split / merge / redeem / heartbeat) additionally require an EIP-712 ECDSA signature over the typed action. See the Signing section below.
- All decimal values (price, size, balance, PnL) are exchanged as decimal strings to avoid floating-point precision loss.
- All timestamps are Unix milliseconds.
- Wire field names use camelCase; Rust struct fields use snake_case, converted via
serde(rename_all = "camelCase").
1. Installation
Add the dependency via Git in your Cargo.toml:
Cargo.toml dependencies
[dependencies]
okx-outcomes-sdk = { git = "https://github.com/okx/outcomes-sdk.git", features = ["signing", "websocket"] }
2. Client Construction
Module: okx_outcomes_sdk::{OutcomesSdkClient, ApiCredentials}.
pub struct ApiCredentials {
pub api_key: String, // value of the OK-ACCESS-KEY header
pub secret_key: String, // HMAC-SHA256 signing secret; never transmitted
pub passphrase: String, // value of the OK-ACCESS-PASSPHRASE header
}
impl OutcomesSdkClient {
pub fn with_credentials(creds: ApiCredentials) -> Self;
pub fn with_credentials_and_url(creds: ApiCredentials, base_url: impl Into<String>) -> Self;
}
Base URL resolution order: the explicit argument passed to with_credentials_and_url > the PREDICTIONS_API_BASE environment variable > https://www.okx.com. Endpoint constants are full absolute paths (/api/v5/predictions/..., /api/v5/market/...) concatenated with the base URL, so a single host configuration covers both prediction and market-data calls.
3. Errors
Module: okx_outcomes_sdk::SdkError.
Every fallible call returns Result<T, SdkError>. The enum has seven variants:
pub enum SdkError {
/// Network failure: connection refused, DNS, timeout, TLS handshake failure.
/// Transport layer; retrying is usually safe.
Http(reqwest::Error),
/// The server returned a non-zero business error code in the response envelope.
/// `code` is the upstream OKX business code; decide retry / backoff / abort based on it.
Api { code: i64, message: String },
/// The response body could not be deserialized to the expected schema.
/// Usually means the SDK and server versions are mismatched.
Deserialize(serde_json::Error),
/// WS connect, send, login, or close failed.
/// Includes login rejection (`60xxx` error codes) and timeout during login.
WebSocket { message: String },
/// Reserved for callers that bypass the public constructors and end up without credentials.
/// Clients constructed via `with_credentials` / `with_credentials_and_url` never
/// return this variant β they always carry credentials.
NotAuthenticated { hint: String },
/// Invalid URL, missing state, or an unexpected internal precondition.
/// Almost always a programming error by the caller.
Internal { message: String },
/// Serializing the request body or WS payload failed.
/// Rare in practice (only triggered by, e.g., non-finite floats).
Serialization { message: String },
}
Each variant implements Display via thiserror, so format!("{e}") produces a readable one-line log.
4. Events and Markets
Module: okx_outcomes_sdk::models::event::*. The API is implemented in okx_outcomes_sdk::api::events.
4.1 get_events
Get a paginated list of prediction market events.
- Endpoint:
GET /api/v5/predictions/events - Auth: Required
pub async fn get_events(
&self,
status: Option<&str>, // "active" (default) | "resolved"
tag: Option<&str>, // sports tag ID
league_id: Option<&str>, // sports league ID
sort: Option<&str>, // "volume" | "volume_24h" (default) | "ending_soon" | "newest"
cursor: Option<&str>, // pagination cursor from the previous `EventsResponse.pagination.next_cursor`
page_size: Option<i32>, // items per page, max 50 (default 10)
) -> Result<EventsResponse, SdkError>;
pub struct EventsResponse {
events: Vec<EventObject>,
pagination: Pagination, // see Common Types
}
pub struct EventObject {
id: String, // globally unique event ID
event_id: String, // event ID
neg_risk: bool, // mutually exclusive (negRisk) event
status: EventStatus, // Active / Paused / Resolved / Unknown
event_title: String, // display title
description: String, // long description
event_icon: Option<String>, // icon URL
volume: String, // total volume across all markets
start_time: Option<i64>, // trading start time (ms)
end_time: Option<i64>, // trading end time (ms)
created_at: i64, // creation timestamp (ms)
total_markets_count: i32, // number of markets under this event
final_outcomes_market_id: Option<String>, // winning market ID after settlement
markets: Vec<MarketObject>, // the list endpoint returns at most 2 markets;
// call `get_event_markets` for the full list
}
4.2 search
Search events and markets by keyword.
- Endpoint:
GET /api/v5/predictions/events/search - Auth: Required
pub async fn search(
&self,
keyword: &str, // free-text query (required)
cursor: Option<&str>, // pagination cursor
page_size: Option<i32>, // default 10
) -> Result<EventsResponse, SdkError>;
Response: EventsResponse (same shape as get_events).
4.3 get_event
Get a single event, with its full market list inlined.
- Endpoint:
GET /api/v5/predictions/events/{eventId} - Auth: Required
pub async fn get_event(&self, event_id: &str) -> Result<EventObject, SdkError>;
Returns Api { code: 40404, ... } if the event ID does not exist.
4.4 get_event_markets
Get all markets of an event (no pagination, no list cap).
- Endpoint:
GET /api/v5/predictions/events/{eventId}/markets - Auth: Required
pub async fn get_event_markets(&self, event_id: &str) -> Result<MarketsResponse, SdkError>;
pub struct MarketsResponse {
markets: Vec<MarketObject>,
}
pub struct MarketObject {
id: String, // globally unique market ID
market_id: String, // market ID
neg_risk: bool, // negRisk market flag
status: MarketStatus, // Active / Paused / Settling / Resolved / Unknown
settle_stage: i32, // 0 = not started, 5 = settled
question: String, // full market question
short_question: Option<String>, // short question
description: String, // long description
market_icon: Option<String>, // icon URL
start_time: String, // trading start time (ms, string)
end_time: String, // trading end time (ms, string)
resolve_start_at: String, // settlement window start time (ms, string)
resolve_at: String, // settlement time (ms, string)
best_bid: Option<String>, // decimal in [0, 1]; None if no bids
best_ask: Option<String>, // decimal in [0, 1]; None if no asks
last_trade_price: Option<String>, // None before the first trade
volume: String, // market volume
probability: Option<String>, // YES outcome probability in [0, 1]
resolution_sources: Vec<String>, // source URLs used for resolution
yes_outcome: OutcomeObject,
no_outcome: OutcomeObject,
}
pub struct OutcomeObject {
token_id: Option<String>, // conditional token address; None before deployment
asset_id: Option<String>, // asset ID; used as `inst_id` in order placement / market data
name: String, // "Yes" or "No"
price: String, // decimal in [0, 1]
final_result: Option<bool>, // Some(true) = won, Some(false) = lost, None = not settled
}
4.5 get_market
Get a single market.
- Endpoint:
GET /api/v5/predictions/markets/{marketId} - Auth: Required
pub async fn get_market(&self, market_id: &str) -> Result<MarketObject, SdkError>;
5. Account: Balance
Module: okx_outcomes_sdk::models::balance::*. The API is in okx_outcomes_sdk::api::balance.
5.1 get_balance
Returns the authenticated user's available balance grouped by odds type.
- Endpoint:
GET /api/v5/predictions/balance - Auth: Required
pub async fn get_balance(&self) -> Result<BalanceResponse, SdkError>;
pub type BalanceResponse = Vec<BalanceEntry>;
pub struct BalanceEntry {
odds_type: OddsType, // Spots / Points / Unknown (spots = real, points = points)
balance: String, // total balance (unit determined by odds_type)
available: String, // available balance (total balance - amount frozen by open orders)
}
6. Account: Orders
Module: okx_outcomes_sdk::models::order::*. The API is in okx_outcomes_sdk::api::orders.
6.1 place_order
Submit a signed limit (or trigger) order.
- Endpoint:
POST /api/v5/predictions/orders - Auth: Required (REST credentials) + EIP-712 signature
pub async fn place_order(&self, req: &PlaceOrderRequest) -> Result<TxHashResponse, SdkError>;
struct PlaceOrderRequest {
action: PlaceOrderAction,
nonce: i64, // millisecond timestamp, anti-replay
signature: SignatureWrapper, // { Ecdsa: { r, s, v } }
}
struct PlaceOrderAction {
action_type: String, // always "placeOrder"
grouping: String, // always "na"
orders: Vec<OrderItem>,
}
struct OrderItem {
asset_id: String, // outcome assetId
side: SigningOrderSide, // Buy / Sell (wire side lowercase; bytes used for the EIP-712 hash)
market_type: String, // always "prediction"
client_order_id: Option<String>, // 34-char client order ID; see Signing > Client Order ID
price: String, // decimal in [0, 1]
reduce_only: bool,
size: String, // decimal
size_type: SizeType, // Base (default, omitted on wire) / Quote
order_type: OrderTypeSpec, // { limit: { tif } }
}
struct OrderTypeSpec { limit: LimitOrderType }
struct LimitOrderType { tif: LimitTif /* Gtc | Gtd { expires_after } | Ioc | Fok | Alo */ }
Response: TxHashResponse { tx_hash: String }.
Build a typed signing::types::OrderRequest, sign it with signing::sign_to_wrapper, then derive the wire-side OrderItem via OrderItem::from(&OrderRequest) so the signed bytes and the JSON body cannot drift. See Signing.
6.2 cancel_order
Cancel an active order (by server ID or client order ID).
- Endpoint:
POST /api/v5/predictions/orders/cancel - Auth: Required + EIP-712 signature
pub async fn cancel_order(&self, req: &CancelOrderRequest) -> Result<TxHashResponse, SdkError>;
struct CancelOrderRequest {
action: CancelOrderAction,
nonce: i64,
signature: SignatureWrapper,
}
struct CancelOrderAction {
action_type: String, // always "cancel"
cancels: Vec<CancelItem>,
}
struct CancelItem {
asset_id: String,
market_type: String, // "prediction"
// exactly one of the two:
by: CancelBy, // flattened-serialized as { "oid": ... } or { "clientOrderId": ... }
}
enum CancelBy {
Oid { oid: String }, // server-assigned, decimal string
ClientOrderId { client_order_id: String }, // 34-char client order ID, hex with 0x prefix
}
Response: TxHashResponse { tx_hash: String }.
6.3 cancel_all
Cancel all active orders, or all active orders under a given set of asset IDs.
- Endpoint:
POST /api/v5/predictions/orders/cancel-all - Auth: Required + EIP-712 signature
pub async fn cancel_all(&self, req: &CancelAllRequest) -> Result<TxHashResponse, SdkError>;
struct CancelAllRequest {
action: CancelAllAction,
nonce: i64,
expires_after: i64, // expiry timestamp (ms), required
signature: SignatureWrapper,
}
struct CancelAllAction {
action_type: String, // always "cancelAll"
asset_ids: Vec<String>, // empty = all markets; non-empty = filter
market_type: String, // "prediction"
}
Response: TxHashResponse.
6.4 heartbeat
Refresh the dead-man's switch that protects active orders (auto-cancels on disconnect).
- Endpoint:
POST /api/v5/predictions/heartbeat - Auth: Required + EIP-712 signature
pub async fn heartbeat(&self, req: &CancelAllRequest) -> Result<HeartbeatResponse, SdkError>;
struct HeartbeatResponse {
server_timestamp: i64, // current server time (ms)
expire_at: i64, // time this heartbeat expires (ms)
}
The request body uses the same CancelAllRequest shape: the signed payload is a pre-authorized cancel-all β the server executes it on your behalf after the heartbeat times out. Set nonce to now_ms and expires_after to now_ms + 300_000 (5 minutes). Heartbeats should be sent more frequently than once every 5 minutes.
6.5 get_order
Query a single order by its server-assigned ID.
- Endpoint:
GET /api/v5/predictions/orders/{orderId} - Auth: Required
pub async fn get_order(&self, order_id: &str) -> Result<OrderRecord, SdkError>;
pub struct OrderRecord {
id: String, // server-assigned order ID
oid: String, // order oid (distinct from `id`)
market_id: String,
token_id: String, // YES/NO token contract address
asset_id: String, // YES or NO outcome asset ID
client_order_id: Option<String>, // client order ID provided at placement (if any)
side: OrderSide, // Buy / Sell / Unknown
order_type: TimeInForce, // Gtc / Gtd / Ioc / Fok / PostOnly / Unknown
size_type: OrderSizeType, // Base / Quote / Unknown
size: String, // decimal
price: String, // decimal
expiration: Option<String>, // GTD expiry (ms, string); None for non-GTD
tx_hash: String, // submission transaction hash
status: RestOrderStatus, // PendingPlace / Active / PendingCancel / Filled /
// PartiallyFilled / Failed / Cancelled / Expired / Unknown
filled_size: String, // decimal
filled_amount: String, // decimal
fail_reason: Option<String>, // present only when status == RestOrderStatus::Failed
cancel_reason: Option<String>, // set when the server initiates the cancel (heartbeat timeout, market settlement, etc.)
odds_type: OddsType, // Spots / Points / Unknown
created_at: String, // Unix ms (string)
updated_at: String, // Unix ms (string)
}
6.6 list_orders
List the authenticated user's orders.
- Endpoint:
GET /api/v5/predictions/orders - Auth: Required
pub async fn list_orders(
&self,
market_id: Option<&str>, // filter by market ID
status: Option<&str>, // "open" (pending + active) | "closed" (filled / cancelled / expired / failed)
cursor: Option<&str>, // pagination cursor
limit: Option<i32>, // max 50, default 20
) -> Result<OrdersResponse, SdkError>;
// Type alias over the shared paginated-list response wrapper.
pub type OrdersResponse = PagedListResponse<OrderRecord>;
// pub struct PagedListResponse<T> { list: Vec<T>, next_cursor: Option<String>, has_next: bool }
7. Account: Positions
Module: okx_outcomes_sdk::models::position::*. The API is in okx_outcomes_sdk::api::positions.
7.1 get_positions
Query the authenticated user's positions.
- Endpoint:
GET /api/v5/predictions/positions - Auth: Required
pub async fn get_positions(
&self,
status: Option<&str>, // "open" | "closed"; all if omitted
market_id: Option<&str>,
cursor: Option<&str>, // pagination cursor
limit: Option<i32>, // max 100, default 20
) -> Result<PositionsResponse, SdkError>;
pub type PositionsResponse = PagedListResponse<PositionRecord>;
pub struct PositionRecord {
id: String, // identifier
token_id: String,
market_id: String,
token_index: String, // "1" = YES, "2" = NO
token_name: String, // "Yes" or "No"
size: String, // current remaining size
available_size: String, // available size (= size β the portion frozen by sell orders)
value: String, // cur_price * size
avg_price: String, // weighted average entry cost
un_realized_pnl: String, // unrealized PnL
un_realized_pnl_percentage: String,
title: String, // display string
icon: String, // display string
event_id: String,
winning_token: Option<String>, // winning token ID after settlement; None before settlement
position_status: i32, // position status code (full enum in the API reference)
cur_price: String, // current token price
realized_pnl: String, // realized PnL
realized_pnl_percentage: String,
odds_type: OddsType, // Spots / Points / Unknown
// (verified in production: wire value is "points" or "spots", not "real")
}
8. Account: Trades
Module: okx_outcomes_sdk::models::trade::*. The API is in okx_outcomes_sdk::api::trades.
8.1 get_trades
Query the authenticated user's trade history.
- Endpoint:
GET /api/v5/predictions/trades - Auth: Required
pub async fn get_trades(
&self,
market_id: Option<&str>, // filter by market ID
side: Option<&str>, // "BUY" | "SELL"
start_time: Option<i64>, // start time (inclusive, ms)
end_time: Option<i64>, // end time (exclusive, ms)
cursor: Option<&str>, // pagination cursor
limit: Option<i32>, // max 100, default 20
) -> Result<TradesResponse, SdkError>;
type TradesResponse = PagedListResponse<TradeRecord>;
struct TradeRecord {
trade_id: String, // empty string for TAKER rows and MAKER rows before on-chain assignment
order_id: String,
market_id: String,
token_id: String,
side: OrderSide, // Buy / Sell / Unknown
size: String, // filled token amount
amount: String, // filled amount
price: String,
fee: String,
role: Role, // Maker / Taker / Unknown
tx_hash: String,
created_at: String, // Unix ms (string)
}
trade_id is None for TAKER rows, and for historical MAKER rows from before the on-chain trade-id assignment mechanism.
9. Conditional Tokens
Module: okx_outcomes_sdk::models::position::* (shared with positions). The API is in okx_outcomes_sdk::api::positions.
All three are write operations that require an EIP-712 signature. Each request body has the same outer shape: { action, nonce, signature }, differing only in action. Each returns TxHashResponse { tx_hash: String }.
9.1 split
Split an equal amount of xp in a market into equal amounts of YES + NO tokens (the inverse of merge).
- Endpoint:
POST /api/v5/predictions/positions/split - Auth: Required + EIP-712 signature
pub async fn split(&self, req: &SplitRequest) -> Result<TxHashResponse, SdkError>;
struct SplitRequest { action: SplitAction, nonce: i64, signature: SignatureWrapper }
struct SplitAction {
action_type: String, // "predictionSplit"
market_id: String,
size: String, // amount in minimal units
}
9.2 merge
Merge equal amounts of YES + NO tokens (back into xp).
- Endpoint:
POST /api/v5/predictions/positions/merge - Auth: Required + EIP-712 signature
pub async fn merge(&self, req: &MergeRequest) -> Result<TxHashResponse, SdkError>;
struct MergeRequest { action: MergeAction, nonce: i64, signature: SignatureWrapper }
struct MergeAction {
action_type: String, // "predictionMerge"
market_id: String,
size: String,
}
9.3 redeem
After a market settles, redeem the caller's entire winning token balance. There is no size field; the server redeems the full amount the caller holds.
- Endpoint:
POST /api/v5/predictions/positions/redeem - Auth: Required + EIP-712 signature
pub async fn redeem(&self, req: &RedeemRequest) -> Result<TxHashResponse, SdkError>;
struct RedeemRequest { action: RedeemAction, nonce: i64, signature: SignatureWrapper }
struct RedeemAction {
action_type: String, // "predictionRedeem"
market_id: String,
}
Returns Api { code: 51020, ... } if the market has not been resolved yet.
10. Market Data
Module: okx_outcomes_sdk::models::price::*. The API is in okx_outcomes_sdk::api::prices.
These calls hit OKX's market data API at https://www.okx.com/api/v5/market/* β the same host as the prediction market API but with a different path prefix and response envelope. The market data envelope wraps code as a JSON string, so the code in SdkError::Api { code } is the parsed integer value.
10.1 get_ticker
Latest ticker for a single instrument. inst_id is the market's yes_outcome.asset_id.
- Endpoint:
GET /api/v5/market/ticker - Auth: Required
pub async fn get_ticker(&self, inst_id: &str) -> Result<Ticker, SdkError>;
pub struct Ticker {
inst_type: String,
inst_id: String,
last: String, // latest trade price
last_sz: String, // latest trade size
ask_px: String, // best ask price
ask_sz: String, // size at the best ask
bid_px: String, // best bid price
bid_sz: String, // size at the best bid
open24h: String, // 24h open price
high24h: String, // 24h high price
low24h: String, // 24h low price
vol24h: String, // 24h volume (base currency)
vol_ccy24h: String, // 24h volume (quote currency)
sod_utc0: String, // open price at UTC 0
sod_utc8: String, // open price at UTC+8
ts: String, // update timestamp (Unix ms decimal string)
}
The server returns a 1-element array; the SDK unwraps it. Returns Api { code: -1, message: "ticker not found" } when the inst ID is unknown.
10.2 get_candles
Candlestick history.
- Endpoint:
GET /api/v5/market/candles - Auth: Required
pub async fn get_candles(
&self,
inst_id: &str,
bar: Option<&str>, // "1m" / "5m" / "15m" / "30m" / "1H" / "4H" / "1D" / ... ; default "1m"
after: Option<&str>, // return candles with timestamp **before** this value (ms)
before: Option<&str>, // return candles with timestamp **after** this value (ms)
limit: Option<i32>, // max 300, default 100
) -> Result<Vec<Candle>, SdkError>;
pub struct Candle(pub Vec<String>);
impl Candle {
pub fn ts(&self) -> &str; // index 0: open time (Unix ms string)
pub fn open(&self) -> &str; // index 1: open price
pub fn high(&self) -> &str; // index 2: high price
pub fn low(&self) -> &str; // index 3: low price
pub fn close(&self) -> &str; // index 4: close price
pub fn vol(&self) -> &str; // index 5: volume (contracts)
// index 6: volume in the base currency (no helper)
// index 7: volume in the quote currency (no helper)
pub fn confirmed(&self) -> bool; // index 8: returns true when "1" (candle closed)
}
10.3 get_pm_books
Prediction market order book depth snapshot.
- Endpoint:
GET /api/v5/market/pm-books - Auth: Required
- Rate limit: 40 times / 2s
pub async fn get_pm_books(
&self,
inst_id: &str, // YES outcome asset ID
sz: Option<i32>, // number of depth levels per side; max 400 (up to 800 across both sides).
// Defaults to 1 (BBO only) when omitted.
) -> Result<PmBookDepth, SdkError>;
pub struct PmBookDepth {
asks: Vec<Vec<String>>, // ask levels, ascending by price. Each entry is [price, size, order_count].
bids: Vec<Vec<String>>, // bid levels, descending by price. Each entry is [price, size, order_count].
ts: String, // snapshot timestamp (Unix ms decimal string)
seq_id: i64, // order book version sequence; opaque to most callers,
// exposed only to align with the API response
}
The server returns a 1-element data array; the SDK unwraps it. Returns Api { code: -1, message: "pm-books snapshot not found" } when the response is empty.
11. WebSocket
Module: okx_outcomes_sdk::ws::*. Requires the websocket Cargo feature.
Connection model
The Open API uses the same endpoint for public and private channels: wss://<host>/ws/v5/business. Public channels can be used anonymously. Private channels require a one-time op: "login" after the WS handshake.
Hosts:
pub mod ws::endpoints {
pub const DEFAULT_WS_HOST: &str = "wss://ws.okx.com:8443";
pub const EU_WS_HOST: &str = "wss://wseea.okx.com";
pub const US_WS_HOST: &str = "wss://wsus.okx.com";
pub const BUSINESS_PATH: &str = "/ws/v5/business";
}
PredictionsWsClient uses DEFAULT_WS_HOST by default; override it with PredictionsWsClient::with_host(...) or the PREDICTIONS_WS_HOST environment variable.
Lifecycle and resilience:
- 25-second heartbeat keepalive (OKX requires < 30 seconds).
- Automatic reconnect with exponential backoff (3s -> 6s -> 12s -> capped at 30s).
- On reconnect, if credentials are stored, the client replays login and re-subscribes to every channel that was active when the connection dropped.
- Every state change triggers
connection_state_callback("public" | "private", connected: bool).
Public API
pub struct PredictionsWsClient { /* ... */ }
impl PredictionsWsClient {
pub fn new() -> Self;
pub fn with_host(host: &str) -> Self;
pub async fn connect(&self, path: &str) -> Result<(), SdkError>;
pub async fn login(&self, creds: &ApiCredentials) -> Result<(), SdkError>;
pub async fn subscribe(&self, channel: &str, params: Vec<HashMap<String, String>>) -> Result<(), SdkError>;
pub async fn unsubscribe(&self, channel: &str, params: Vec<HashMap<String, String>>) -> Result<(), SdkError>;
pub async fn disconnect(&self);
pub fn set_on_data(&self, callback: WsDataCallback);
pub fn set_on_connection_state(&self, callback: WsConnectionStateCallback);
}
pub type WsDataCallback = Arc<dyn Fn(&WsMessage) + Send + Sync>;
pub type WsConnectionStateCallback = Arc<dyn Fn(&str, bool) + Send + Sync>;
subscribe is idempotent: calling it twice with the same (channel, params) pair does not create a duplicate subscription, nor a duplicate entry in the replay list.
Login signature (handled internally by login): the SDK computes sign = Base64(HMAC-SHA256(secret_key, timestamp + "GET" + "/users/self/verify")) and sends:
{"op": "login", "args": [{"apiKey": "...", "passphrase": "...", "timestamp": "...", "sign": "..."}]}
The future returned by login resolves only after the server responds:
{"event":"login","code":"0"}->Ok(()).{"event":"error","code":"600xx",...}->Err(SdkError::WebSocket { message: "Login rejected: [60xxx] ..." }).- No response within 30 seconds ->
Err(SdkError::WebSocket { message: "Login timed out (30s)" }).
Message dispatch
Each incoming JSON frame is parsed once into the WsMessage enum and handed to on_data. Consumers never see the raw JSON.
pub enum WsMessage {
Event {
event: String,
channel: Option<String>,
inst_id: Option<String>,
msg: Option<String>,
},
Prices(Vec<WsPriceTick>),
Books { data: Vec<WsPmBookData>, action: String }, // action = "snapshot" | "update"
Trades(Vec<WsPmTrade>),
Tickers(Vec<WsPmTicker>),
Game(Vec<WsGameStatus>),
EventStatus(Vec<WsEventStatus>),
Candle(Vec<Candle>), // typed wrapper, 9-column OHLCV array
Orders(Vec<WsOrder>),
Positions(Vec<WsPosition>),
UserTrades(Vec<WsUserTrade>),
Balance(Vec<WsBalance>),
Pnl(Vec<WsPnl>),
Unknown { channel: String, raw: serde_json::Value },
}
WsMessage::Event carries acknowledgement messages such as event:"subscribe"|"unsubscribe"|"login"|"error", unrelated to any data channel.
Public channels
11.1 prediction-market-prices
Per-market price ticks.
- Subscribe params:
[{"instId": "<asset_id>"}](one per market). - Message variant:
WsMessage::Prices(Vec<WsPriceTick>).
struct WsPriceTick {
yes_asset_id: String,
last_trade_price: Option<String>, // None before the first trade
best_bid: Option<String>, // None if no bids
best_ask: Option<String>, // None if no asks
timestamp: String, // Unix ms decimal string
probability: String, // basis points * 100, e.g. "6500" = 65.00%
market_volume: String,
event_volume: String,
event_id: String,
}
11.2 pm-books
Order book snapshots and incremental updates.
- Subscribe params:
[{"instId": "<asset_id>"}]. - Message variant:
WsMessage::Books { data, action }, whereactionis"snapshot"(full snapshot on subscribe / reconnect) or"update"(incremental delta).
struct WsPmBookData {
asks: Vec<Vec<String>>, // [[price, size, ...], ...]
bids: Vec<Vec<String>>, // [[price, size, ...], ...]
ts: String,
checksum: Option<i64>, // CRC32 integrity check over the normalized book
seq_id: Option<i64>, // monotonic sequence; a gap = loss, reset required
prev_seq_id: Option<i64>, // -1 for the first snapshot
}
When prev_seq_id does not match the previous frame's seq_id, discard the local book and wait for the next snapshot.
11.3 pm-trades
Public trade feed.
- Subscribe params:
[{"instId": "<asset_id>"}]. - Message variant:
WsMessage::Trades(Vec<WsPmTrade>).
struct WsPmTrade {
inst_id: String,
trade_id: Option<String>, // single push: Some; aggregated push: None
f_id: Option<String>, // aggregated push: first trade id; single: None
l_id: Option<String>, // aggregated push: last trade id; single: None
px: String, // price
sz: String, // size
side: String, // "buy" / "sell" (taker side)
ts: String,
}
Distinguish single pushes from windowed aggregated pushes by which of trade_id and (f_id, l_id) is Some.
11.4 pm-tickers
OKX-style per-instrument ticker push.
- Subscribe params:
[{"instId": "<asset_id>"}]. - Message variant:
WsMessage::Tickers(Vec<WsPmTicker>).
struct WsPmTicker {
inst_type: String, inst_id: String,
last: String, last_sz: String,
ask_px: String, ask_sz: String,
bid_px: String, bid_sz: String,
open24h: String, high24h: String, low24h: String,
vol24h: String, vol_ccy24h: String,
sod_utc0: String, sod_utc8: String,
ts: String,
}
11.5 pm-event-status
Event settlement push.
- Subscribe params:
[{"instId": "event-<event_id>"}]. The SDK does not automatically add theevent-prefix on the WS path; pass it explicitly. - Message variant:
WsMessage::EventStatus(Vec<WsEventStatus>).
struct WsEventStatus {
event_id: String,
status: String, // e.g. "resolved"
market_id: String, // winning market ID
outcome_option: String, // "yes" / "no" / "others" / team name / "draw"
timestamp: String,
}
11.6 pm-candle*
Candlestick stream. The channel name encodes the bar: pm-candle1m, pm-candle5m, pm-candle1H, pm-candle1D, etc.
- Subscribe params:
[{"instId": "<asset_id>"}]. - Message variant:
WsMessage::Candle(Vec<Candle>), where eachCandleis a typed wrapper over a 9-column OHLCV array. Accessors:ts(),open(),high(),low(),close(),vol(),vol_ccy(),vol_ccy_quote(),confirmed().
Private channels (require login)
Subscribe with empty params (the server scopes the subscription to the logged-in account).
11.7 pm-order
Order status changes.
- Subscribe params:
[]. - Message variant:
WsMessage::Orders(Vec<WsOrder>).
struct WsOrder {
order_id: String,
market_id: String,
status: OrderStatus, // Active / Filled / PartiallyFilled / PlaceFailed /
// CancelFailed / Cancelled / Expired / Unknown
side: OrderSide, // Buy / Sell / Unknown
// All fields below depend on `status` β see the status -> required-fields table in the spec.
// Modeled as Option; a missing key deserializes to None.
client_order_id: Option<String>,
asset_id: Option<String>, // assetId of YES or NO
direction: Option<Direction>, // Yes / No / Unknown β the outcome side this order takes
filled_size: Option<String>,
order_size: Option<String>, // serde alias = "size"
avg_price: Option<String>,
amount: Option<String>, // BUY = spent, SELL = received (xp)
limit_price: Option<String>, // serde alias = "price"
fail_message: Option<String>, // present only for PLACE_FAILED / CANCEL_FAILED
odds_type: Option<OddsType>,
tx_hash: Option<String>, // serde rename = "txHash"
trade_id: Option<String>,
}
11.8 pm-position
Position updates.
- Subscribe params:
[]. - Message variant:
WsMessage::Positions(Vec<WsPosition>).
This channel has two payload variants; WsPosition represents both with a single flat struct, with the variant-specific fields wrapped in Option. Branch on status (via PositionStatus::is_position_snapshot() / is_failed()) to decide which fields are meaningful.
struct WsPosition {
// Common to both variants
market_id: String,
status: PositionStatus, // Fill / FillFailed / Redeem / RedeemFailed /
// Split / SplitFailed / Merge / MergeFailed /
// Deposit / DepositFailed / Withdraw / WithdrawFailed / Unknown
amount: String, // variant 1: position `remain` ("0" for REDEEM)
// variant 2: split/merge/deposit/withdraw amount
odds_type: Option<OddsType>,
// Variant 1 (FILL / REDEEM / *_FAILED) β full position snapshot
id: Option<String>,
token_id: Option<String>,
asset_id: Option<String>,
timestamp: Option<String>,
un_realized_pnl: Option<String>,
un_realized_pnl_percentage: Option<String>,
value: Option<String>,
avg_price: Option<String>,
trade_id: Option<String>,
// Variant 2 (SPLIT / MERGE / DEPOSIT / WITHDRAW / *_FAILED)
tx_hash: Option<String>, // serde rename = "txHash"
ext: Option<WsPositionExt>, // populated only for DEPOSIT
}
struct WsPositionExt {
to_tx_hash: String, // serde rename = "toTxHash"
}
11.9 pm-user-trade
The user's own trade stream.
- Subscribe params:
[]. - Message variant:
WsMessage::UserTrades(Vec<WsUserTrade>).
struct WsUserTrade {
order_id: String,
client_order_id: String, // defaults to empty string for resilience; usually present
market_id: String,
token_id: String,
asset_id: String, // yesAssetId or noAssetId
side: OrderSide, // Buy / Sell / Unknown
size: String,
price: String,
txhash: String,
timestamp: String,
trade_id: String, // trade ID
}
11.10 pm-balance
Balance changes.
- Subscribe params:
[]. - Message variant:
WsMessage::Balance(Vec<WsBalance>).
struct WsBalance {
wallet_address: String,
available: String,
total: String,
frozen: String,
token_id: String, // on-chain Point token id
change_type: BalanceChangeType, // Place / Cancel / Fill / Split / Merge /
// Redeem / Deposit / Withdraw / Unknown
change_amount: Option<String>, // spec: may be null
update_time: String,
odds_type: Option<OddsType>,
}
11.11 pm-pnl
Unrealized PnL stream β pushes two payloads; modeled with a serde untagged enum that picks the right variant based on the fields present.
- Subscribe params:
[]. - Message variant:
WsMessage::Pnl(Vec<WsPnl>).
enum WsPnl {
Overview(WsPnlOverview), // portfolioValue + per-period summaries
Timeseries(WsPnlTimeseries), // chart points with high/low/current
}
struct WsPnlOverview {
portfolio_value: String, // xp balance + position market value
periods: Vec<WsPnlPeriodSummary>,
}
struct WsPnlPeriodSummary {
period: String, // "1D" / "1W" / "1M" / "6M" / "1Y"
period_pnl: String,
pnl_percent: String,
}
struct WsPnlTimeseries {
period: String, // "0"=1D / "1"=1W / "2"=1M / "3"=6M / "4"=1Y
interval: String, // ms: 600000 / 1800000 / 3600000 / 86400000
points: Vec<WsPnlPoint>,
current_pnl: String,
high: String,
low: String,
}
WS Error Codes
WS-layer errors are exposed via WsMessage::Event { event: "error", msg, .. }; login-related errors are exposed via the Err returned by login(). Common error codes:
| Error code | Meaning |
|---|---|
60004 |
Invalid login timestamp (clock drift, expired). |
60005 |
Invalid API key. |
60006 |
Timestamp expired (30-second window). |
60007 |
Invalid signature. |
60009 |
Login failed (generic). |
60011 |
This private channel requires login. |
60012 |
Invalid op value. |
60018 |
Subscription failed (wrong channel name or params). |
12. Signing
Module: okx_outcomes_sdk::signing::*. Requires the signing Cargo feature.
The full pipeline for any write operation: build a typed Action, sign it with your k256::ecdsa::SigningKey via sign_to_wrapper, then put the resulting SignatureWrapper into the request body.
pub fn parse_private_key(hex_key: &str) -> Result<SigningKey, String>;
pub fn now_millis() -> u64;
pub fn sign_to_wrapper(
action: &Action,
nonce: u64,
expires_after: Option<u64>,
key: &SigningKey,
) -> Result<SignatureWrapper, String>;
Action constructors:
pub fn action_place_order(orders: Vec<OrderRequest>) -> Action;
pub fn action_cancel(cancels: Vec<CancelRequest>) -> Action;
pub fn action_cancel_all(asset_ids: Vec<String>, market_type: &str) -> Action;
pub fn action_prediction_split (market_id: &str, size: &str) -> Action;
pub fn action_prediction_merge (market_id: &str, size: &str) -> Action;
pub fn action_prediction_redeem(market_id: &str) -> Action;
Typed inputs:
struct OrderRequest {
asset_id: String,
side: SigningOrderSide, // Buy / Sell (wire side lowercase)
market_type: String, // "prediction"
client_order_id: Option<String>, // 34-char client order ID
price: String,
reduce_only: bool,
size: String,
size_type: SizeType, // Base (default) / Quote
order_type: OrderType,
}
enum OrderType {
Limit(LimitTif),
}
enum LimitTif {
Plain(String), // "gtc" | "ioc" | "fok" | "alo"
Gtd { expiry_time: u64 }, // good-til-date, ms
}
struct CancelRequest {
asset_id: String,
market_type: String, // "prediction"
target: CancelTarget,
}
enum CancelTarget { Oid(String), ClientOrderId(String) }
The wire-side counterparts (OrderItem, CancelItem) implement TryFrom<&OrderRequest> and From<&CancelRequest> respectively, so the JSON body and the signed bytes are both constructed from the same source struct.
Client order ID:
pub fn generate_client_order_id_default() -> Result<String, String>;
pub fn generate_client_order_id(region: Region, env: Env) -> Result<String, String>;
pub fn validate_client_order_id(s: &str) -> bool;
pub fn parse_client_order_id_prefix(client_order_id: Option<&str>) -> ClientOrderIdPrefix;
pub fn register_client_order_id_context(region: Region, env: Env);
The client order ID is a 34-char hex string of the form 0x{region}{env}{30 hex random chars}. generate_client_order_id_default() reads the registered global context (HK / PROD by default). To use a different context, register it once at startup to override.
Low-level helpers:
pub fn signer_address(key: &SigningKey) -> String;
pub fn ecrecover(signing_hash: &str, signature: &str) -> Result<String, String>;
pub fn sign_action(...) -> Result<String, String>; // returns "0x..." hex
pub fn sign_action_full(...) -> Result<(String, String, String, u8), String>; // (txhash, r, s, v)
pub fn sign_action_debug(...) -> Result<SigningDebug, String>; // returns all intermediate hashes
For the normal flow, use sign_to_wrapper. The low-level functions are only for debugging, or for callers that need access to the txhash (e.g. to display a "view in explorer" link).
13. Common Types
Module: okx_outcomes_sdk::models::common::*.
struct Pagination {
next_cursor: Option<String>, // None on the last page
has_more: bool,
page_size: i32, // item count on the current page
}
struct EcdsaSignature {
r: String, // hex, with 0x prefix
s: String, // hex, with 0x prefix
v: u8, // recovery id: 0 or 1
}
struct SignatureWrapper {
// serialized as { "Ecdsa": { r, s, v } }
ecdsa: EcdsaSignature,
}
The SDK transparently wraps two API envelopes:
- Prediction market REST:
{ "code": <int>, "message": "...", "data": <T> }, wherecode == 0means success. - OKX market data:
{ "code": "<int>", "msg": "...", "data": <T> }(notecodeis a string).code == "0"means success.
WebSocket
1. WebSocket Login Authentication
WebSocket login authentication is required only before subscribing to private channels. Public channels do not require login.
See WebSocket login documentation.
Generic subscribe format
{
"op": "subscribe",
"args": [
{ "channel": "<channel1>", "instId": "<yesAssetId1>" },
{ "channel": "<channel2>", "instId": "<yesAssetId2>" }
]
}
Generic unsubscribe format
{
"op": "unsubscribe",
"args": [
{ "channel": "<channel1>", "instId": "<yesAssetId1>" },
{ "channel": "<channel2>", "instId": "<yesAssetId2>" }
]
}
2. WebSocket Private Channels
| Channel | Subscribe params | Auth required | Description |
|---|---|---|---|
| pm-order | channelName | Yes | User order data push |
| pm-position | channelName | Yes | Position changes |
| pm-user-trade | channelName | Yes | User trade history push |
| pm-balance | channelName | Yes | Balance changes |
| pm-pnl | channelName | Yes | Unrealized PnL of current positions |
2.1 Order Status Push
Push frequency: Event-driven, pushed when an order status changes
Subscribe example
{
"op": "subscribe",
"args": [{ "channel": "pm-order" }]
}
Push data format
{
"arg": { "channel": "pm-order", "uid": "{cexUserId}" },
"data": [{
"orderId": "307173036051017730",
"clientOrderId": "cli-abc-123",
"marketId": "100001",
"status": "FILLED",
"assetId": "71",
"side": "BUY",
"direction": "YES",
"filledSize": "10",
"orderSize": "10",
"avgPrice": "0.57",
"amount": "5.7",
"limitPrice": "0.45",
"failMessage": null,
"oddsType": "points",
"txHash": "0xdef...",
"tradeId": "9876543210"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
orderId |
string | Order ID |
clientOrderId |
string / null | Client order ID (cloid); null if not provided by the client |
marketId |
string | Market ID |
status |
string | Push event type (see the enum table below) |
assetId |
string | Outcome asset ID (yesAssetId or noAssetId) |
side |
string | Trade direction: BUY / SELL |
direction |
string | Position direction: YES / NO |
filledSize |
string / null | Cumulative filled size; null if no fill |
orderSize |
string | Order size |
avgPrice |
string / null | Cumulative average fill price (= amount / filledSize); null if no fill |
amount |
string / null | Cumulative filled amount (xp), BUY = spent / SELL = received; null if no fill |
limitPrice |
string / null | Limit order price (limit scenario only); null for market orders |
failMessage |
string / null | Failure message; only populated for PLACE_FAILED / CANCEL_FAILED |
oddsType |
string | Odds type: points |
txHash |
string / null | On-chain transaction hash; null for events not yet on-chain (e.g. PLACE_FAILED) |
tradeId |
string / null | TradeZone trade ID; only populated for limit order partial fills (status=ACTIVE) |
status Enum
| code | Description | Required fields |
|---|---|---|
ACTIVE |
Active (limit order on-chain / partially filled with remainder valid) | orderSize, limitPrice (partial fills also include filledSize, avgPrice, amount, tradeId) |
FILLED |
Fully filled | filledSize, avgPrice, amount, txHash |
PARTIALLY_FILLED |
Cancelled after partial fill | filledSize, orderSize, avgPrice, amount, txHash |
PLACE_FAILED |
Order placement failed | orderSize, failMessage |
CANCEL_FAILED |
Cancellation failed (unexpected) | orderId, failMessage |
CANCELLED |
Cancelled by user / batch-cancelled by system | orderSize, limitPrice |
EXPIRED |
Order expired | orderSize, limitPrice |
2.2 Position Change Push
Push frequency: Event-driven
Subscribe example
{
"op": "subscribe",
"args": [{ "channel": "pm-position" }]
}
2.3 Position
(status = FILL / REDEEM / FILL_FAILED / REDEEM_FAILED)
Push data format
{
"arg": { "channel": "pm-position", "uid": "{cexUserId}" },
"data": [{
"id": "307173036051017730",
"marketId": "100001",
"tokenId": "20000",
"assetId": "71",
"amount": "10",
"timestamp": "1712736000000",
"unRealizedPnl": "5.20",
"unRealizedPnlPercentage": "1.05",
"value": "100.00",
"avgPrice": "0.57",
"status": "FILL",
"tradeId": "9876543210",
"oddsType": "points"
}]
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
id |
String | Position ID |
marketId |
String | Market ID |
tokenId |
String | YES/NO token on-chain ID |
assetId |
String | Outcome asset ID (yesAssetId or noAssetId) |
amount |
String | Current position size (remain snapshot); "0" for REDEEM |
timestamp |
String | Event timestamp (ms) |
unRealizedPnl |
String | Unrealized PnL = (currentPrice β avgCost) Γ remain; "0" for REDEEM |
unRealizedPnlPercentage |
String | Unrealized PnL percentage (8-digit precision); "0" for REDEEM |
value |
String | Position market value = currentPrice Γ remain; "0" for REDEEM |
avgPrice |
String | Weighted average cost basis; "0" for REDEEM |
status |
String | FILL / REDEEM |
tradeId |
String / null | TradeZone trade ID; only populated for FILL (null for REDEEM) |
oddsType |
String | Odds type: points |
2.4 status Scenarios
| code | Description | Pushes per event |
|---|---|---|
FILL |
Position change caused by an order fill | 1 (single tokenId position) |
REDEEM |
Settlement redemption; the market's position is cleared | N (one per tokenId the market originally had, with amount=0) |
2.5 Position
(status = SPLIT / MERGE / DEPOSIT / WITHDRAW / SPLIT_FAILED / MERGE_FAILED / DEPOSIT_FAILED / WITHDRAW_FAILED)
Push data format
{
"arg": { "channel": "pm-position", "uid": "{cexUserId}" },
"data": [{
"marketId": "100001",
"status": "DEPOSIT",
"amount": "100",
"txHash": "0xdef...",
"oddsType": "points",
"ext": {
"toTxHash": "0xabc..."
}
}]
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
marketId |
String | Market ID |
status |
String | SPLIT / MERGE / DEPOSIT / WITHDRAW |
amount |
String | Amount |
txHash |
String | On-chain transaction hash. For DEPOSIT, the original XLayer transaction hash; otherwise the on-chain hash of the corresponding operation |
oddsType |
String | Odds type: points |
ext |
Object / null | Extended info; only populated for DEPOSIT, null otherwise |
ext.toTxHash |
String / null | The TZ-side crediting transaction hash; only populated for DEPOSIT |
status Enum
| code | Description |
|---|---|
FILL |
Position change caused by an order fill |
SPLIT |
Split succeeded |
MERGE |
Merge succeeded |
REDEEM |
Settlement redemption succeeded |
DEPOSIT |
Deposit succeeded |
WITHDRAW |
Withdrawal succeeded |
FILL_FAILED |
Fill failed |
SPLIT_FAILED |
Split failed |
MERGE_FAILED |
Merge failed |
REDEEM_FAILED |
Redemption failed |
DEPOSIT_FAILED |
Deposit failed |
WITHDRAW_FAILED |
Withdrawal failed |
2.6 Trade Fill Push
Each FILL event pushes one trade record; the front end uses it to display the trade list in real time.
Push frequency: Event-driven, one per fill
Subscribe example
{
"op": "subscribe",
"args": [{ "channel": "pm-user-trade" }]
}
Push data format
{
"arg": { "channel": "pm-user-trade", "uid": "{cexUserId}" },
"data": [{
"orderId": "307173036051017730",
"clientOrderId": "cli-abc-123",
"marketId": "100001",
"tokenId": "20000",
"assetId": "71",
"side": "BUY",
"size": "10",
"price": "0.57",
"txhash": "0xdef...",
"timestamp": "1712736000000",
"tradeId": "9876543210"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
orderId |
string | TradeZone order ID |
clientOrderId |
string / null | Client order ID; null if not provided by the client |
marketId |
string | Market ID |
tokenId |
string | YES/NO token on-chain ID |
assetId |
string | Outcome asset ID (yesAssetId or noAssetId) |
side |
string | Fill direction: BUY / SELL |
size |
string | Fill size |
price |
string | Fill price |
txhash |
string | On-chain transaction hash |
timestamp |
string | Event timestamp (ms) |
tradeId |
string | TradeZone trade ID |
2.7 Balance Change Push
Pushed after a balance sync succeeds; the front end uses it to update the balance display in real time.
Push frequency: Event-driven, pushed on balance changes (place / cancel / fill / Split / Merge / redeem / deposit / withdraw)
Subscribe example
{
"op": "subscribe",
"args": [{ "channel": "pm-balance" }]
}
Push data format
{
"arg": { "channel": "pm-balance", "uid": "{cexUserId}" },
"data": [{
"walletAddress": "0x1234abcd5678ef901234abcd5678ef901234abcd",
"available": "950.5",
"total": "1000",
"frozen": "49.5",
"tokenId": "0",
"changeType": "FILL",
"changeAmount": "100",
"updateTime": "1712736000000",
"oddsType": "points"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
walletAddress |
String | User's AA wallet address |
available |
String | Available balance |
total |
String | Total balance (including frozen) |
frozen |
String | Frozen amount (= total β available) |
tokenId |
String | xp token on-chain ID (corresponds to oddsType) |
changeType |
String | Trigger reason (see the enum table below) |
changeAmount |
String / null | Amount changed this time; null for non-applicable scenarios |
updateTime |
String | Event timestamp (ms) |
oddsType |
String | Odds type: points |
2.8 changeType Enum
| code | Trigger scenario |
|---|---|
PLACE |
Funds frozen on order placement |
CANCEL |
Funds unfrozen on cancellation |
FILL |
Balance change caused by a fill |
SPLIT |
Split deducts xp |
MERGE |
Merge adds xp |
REDEEM |
Settlement redemption |
DEPOSIT |
Deposit |
WITHDRAW |
Withdrawal |
2.9 PnL Change Push
Pushes both the PnL curve and the overview.
Push data format
{
"arg": { "channel": "pm-pnl", "uid": "{cexUserId}" },
"data": [{
"portfolioValue": "1234.56",
"periods": [
{ "period": "1D", "periodPnl": "12.34", "pnlPercent": "1.01" },
{ "period": "1W", "periodPnl": "56.78", "pnlPercent": "4.83" },
{ "period": "1M", "periodPnl": "120.45", "pnlPercent": "10.81" },
{ "period": "6M", "periodPnl": "320.10", "pnlPercent": "35.02" },
{ "period": "1Y", "periodPnl": "500.00", "pnlPercent": "67.93" }
]
}]
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
portfolioValue |
String | Current total portfolio value (xp available balance + position market value) |
periods |
Array | Multi-period PnL summary array |
periods[].period |
String | Period: 1D / 1W / 1M / 6M / 1Y |
periods[].periodPnl |
String | Absolute PnL within the period |
periods[].pnlPercent |
String | PnL percentage within the period |
Push data format
{
"arg": { "channel": "pm-pnl", "uid": "{cexUserId}" },
"data": [{
"period": "0",
"interval": "600000",
"points": [
{ "time": "1712707200000", "pnl": "1000.00" },
{ "time": "1712728800000", "pnl": "1010.50" },
{ "time": "1712750400000", "pnl": "1023.75" }
],
"currentPnl": "1023.75",
"high": "1030.00",
"low": "995.00"
}]
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
period |
String | Period code: 0=1D / 1=1W / 2=1M / 3=6M / 4=1Y |
interval |
String | Data point interval (ms), corresponding to the period (see the table below) |
points |
Array | Time-series data points |
points[].time |
String | Data point timestamp (ms) |
points[].pnl |
String | Total assets at that moment (= xp balance + position market value) |
currentPnl |
String | Current total assets (compatibility field) |
high |
String | Highest total assets within the period |
low |
String | Lowest total assets within the period |
2.10 Period Code and interval Mapping
| period code | Period | interval (ms) | Meaning |
|---|---|---|---|
0 |
1D | 600000 |
10 minutes |
1 |
1W | 1800000 |
30 minutes |
2 |
1M | 3600000 |
1 hour |
3 |
6M | 86400000 |
1 day |
4 |
1Y | 86400000 |
1 day |
3. WebSocket Public Channels
3.1 Order Book Push
3.2 Push Frequency
Minimum push interval 100ms β the shortest interval between two incremental pushes
Maximum push interval 60,000ms (60s) β the longest interval when the order book does not change
Subscribe example
{
"op": "subscribe",
"args": [{
"channel": "pm-books",
"instId": "{yesAssetId}"
}]
}
Subscribe success response
{
"event": "subscribe",
"arg": { "channel": "pm-books", "instId": "{yesAssetId}" }
}
Push data format
{
"arg": {"channel": "pm-books", "instId": "{yesAssetId}"},
"action": "snapshot",
"data": [
{
"asks": [
["67300.1", "201.18", "25"],
["67300.2", "421.45", "5"]
],
"bids": [
["67300", "525.41", "34"],
["67299.9", "0.17", "7"]
],
"ts": "1774944028506",
"checksum": -702280706,
"seqId": 308306650401,
"prevSeqId": -1
}
]
}
3.3 Trade Push
Pushes each trade record in real time.
Push frequency: push interval 200ms, up to 20 trades per push, encoded as aggregated trades
Subscribe example
{
"op": "subscribe",
"args": [{
"channel": "pm-trades",
"instId": "{yesAssetId}"
}]
}
Push data format
{
"arg": { "channel": "pm-trades", "instId": "{yesAssetId}" },
"data": [{
"instId": "{yesAssetId}",
"tradeId": "123456789",
"px": "0.65",
"sz": "100",
"side": "buy",
"ts": "1711900800123"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
instId |
String | yesAssetId |
fId |
String | First trade ID within the aggregation window |
lId |
String | Last trade ID within the aggregation window |
px |
String | Trade price |
sz |
String | Trade size |
side |
String | Trade direction: buy / sell |
ts |
String | Trade time (Unix ms timestamp) |
3.4 Candlestick Push
Pushes candlestick data in real time; the current candle keeps updating until it closes. Clients can draw a price trend chart based on the candle close price; the subscription granularity is determined by the client.
Channel naming rule: pm-candle + period, e.g. pm-candle1m, pm-candle15m
Subscribe example (1-minute and 5-minute as examples)
{
"op": "subscribe",
"args": [
{ "channel": "pm-candle1m", "instId": "{yesAssetId}" },
{ "channel": "pm-candle15m", "instId": "{yesAssetId}" }
]
}
Push data format
{
"arg": { "channel": "pm-candle15m", "instId": "{yesAssetId}" },
"data": [
["1711900800000", "0.65", "0.67", "0.63", "0.66", "2000", "1300", "1300", "0"]
]
}
3.5 Push Field Descriptions
(each record in the data array is in index order)
| Index | Field | Description |
|---|---|---|
| 0 | ts |
Candle start time (Unix ms timestamp) |
| 1 | o |
Open price |
| 2 | h |
High price |
| 3 | l |
Low price |
| 4 | c |
Close price |
| 5 | vol |
Volume (in contracts) |
| 6 | volCcy |
Volume (in the base currency) |
| 7 | volCcyQuote |
Volume (in the quote currency) |
| 8 | confirm |
Candle status: 0 unconfirmed (current candle), 1 confirmed (closed) |
3.6 Ticker Push
Pushes the latest trade price, best bid, best ask, 24h volume, and other info in real time.
Push frequency: up to once every 100ms, triggered by trades or best-bid/best-ask changes; nothing is pushed without a trigger event
Subscribe example
{
"op": "subscribe",
"args": [{ "channel": "pm-tickers", "instId": "{yesAssetId}" }]
}
Subscribe success response
{
"event": "subscribe",
"arg": { "channel": "pm-tickers", "instId": "{yesAssetId}" },
"connId": "accb8e21"
}
Push data format
{
"arg": { "channel": "pm-tickers", "instId": "{yesAssetId}" },
"data": [{
"instType": "PREDICTIONS",
"instId": "{yesAssetId}",
"last": "0.4999",
"lastSz": "1",
"askPx": "0.6",
"askSz": "30",
"bidPx": "0.4",
"bidSz": "30",
"open24h": "0.5001",
"high24h": "0.5001",
"low24h": "0.4999",
"vol24h": "372",
"volCcy24h": "186",
"sodUtc0": "0.4999",
"sodUtc8": "0.4999",
"ts": "1774948808119"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
| instType | String | Instrument type |
| instId | String | yesAssetId |
| last | String | Latest trade price |
| lastSz | String | Latest trade size |
| askPx | String | Best ask price |
| askSz | String | Size at the best ask |
| bidPx | String | Best bid price |
| bidSz | String | Size at the best bid |
| open24h | String | 24h open price |
| high24h | String | 24h high price |
| low24h | String | 24h low price |
| vol24h | String | 24h volume (in contracts) |
| volCcy24h | String | 24h volume (in the base currency) |
| sodUtc0 | String | Open price at UTC 0 |
| sodUtc8 | String | Open price at UTC+8 |
| ts | String | Data push time (Unix ms timestamp) |
3.7 Probability Price Push
Pushes prediction market probability prices, including market probability, cumulative volume, best bid/ask, latest trade price, etc.
Push frequency: pushed every 3s on a timer
Subscribe example
{
"op": "subscribe",
"args": [{
"channel": "prediction-market-prices",
"instId": "{yesAssetId}"
}]
}
Push data format
{
"arg": {
"channel": "prediction-market-prices",
"instId": "{yesAssetId}"
},
"data": [
{
"yesAssetId": "71",
"eventId": "1774875348987717503",
"bestBid": "0.4896",
"bestAsk": "0.6134",
"lastTradePrice": "0.5848",
"probability": "5515",
"marketVolume": "17.1463",
"eventVolume": "773.6524",
"timestamp": "1775036647300"
}
]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
| yesAssetId | String | Prediction market yesAssetId |
| eventId | String | The event the market belongs to |
| bestBid | String | Best bid price |
| bestAsk | String | Best ask price |
| lastTradePrice | String | Latest trade price |
| probability | String | Market probability for the Yes side, pushed as a basis-point integer (e.g. 0.6500 β 6500) |
| marketVolume | String | Current cumulative market volume |
| eventVolume | String | Current cumulative event volume |
| timestamp | String | Event timestamp (ms) |
3.8 Event Status Push
Event status changes, used to display an event's final settlement result.
Push frequency: event-driven, pushed when an event reaches its final result. During the World Cup there are 2-3 matches per day; pushed when a NegRisk or single binary event reaches its final result.
Subscribe example
Note: When subscribing to this channel,
instIdisevent-{eventId}.
{
"op": "subscribe",
"args": [{
"channel": "pm-event-status",
"instId": "event-{eventId}"
}]
}
Push data format
{
"arg": {
"channel": "pm-event-status",
"instId": "event-{eventId}"
},
"data": [{
"eventId": "{eventId}",
"status": "resolved",
"marketId":"marketId",
"outcomeOption":"yes | no | others | team name | draw",
"timestamp": "1672290687"
}]
}
Push Field Descriptions
| Field | Type | Description |
|---|---|---|
| eventId | string | Event ID |
| status | string | Event status |
| marketId | string | The marketId of the winning market |
| outcomeOption | string | Display of the final winning outcome |
| timestamp | string | Event timestamp (ms) |
Error Codes
1. Response Format
All errors are returned in the following unified JSON structure:
{
"code": 100015,
"msg": "Invalid calldata or malformed fields"
}
1.1 Common
May be returned by any OpenAPI v5 endpoint. For the full list of common error codes, see: OKX public error codes
| Code | Meaning |
|---|---|
| 10000 | User not logged in |
| 10001 | Parameter validation failed |
| 10002 | Authentication failed |
| 206004 | Request oddsType does not match the account |
1.2 Event / Market
| Code | Meaning |
|---|---|
| 201001 | Event does not exist |
| 201002 | Market does not exist |
Applicable endpoints:
GET /api/v5/predictions/events
GET /api/v5/predictions/events/{eventId}
GET /api/v5/predictions/events/{eventId}/markets
GET /api/v5/predictions/markets/{marketId}
GET /api/v5/predictions/events/search
2. Order / Write Operations
Covers: place order, cancel order, cancel all, heartbeat, Split, Merge, Redeem.
| Code | Meaning |
|---|---|
| 100001 | Market is not tradable |
| 100002 | Insufficient balance |
| 100006 | Account is frozen |
| 100010 | Insufficient token balance |
| 100011 | Order does not exist |
| 100012 | Order status does not allow cancellation |
| 100013 | Request address does not match the user address |
| 100015 | Invalid calldata or malformed fields |
| 100016 | Nonce already used |
| 100017 | Nonce expired |
| 100018 | Order amount is below the minimum notional |
| 100101 | TradeZone SDK signature exception |
| 120007 | User does not exist |
| 120022 | Account is in escape (exit) process |
| 201002 | Market does not exist |
| 213003 | Signature verified but TradeZone submission failed |
Applicable endpoints:
POST /api/v5/predictions/ordersPlace orderPOST /api/v5/predictions/orders/cancelCancel a single orderPOST /api/v5/predictions/orders/cancel-allCancel all / market ordersPOST /api/v5/predictions/heartbeatHeartbeatPOST /api/v5/predictions/positions/splitSplitPOST /api/v5/predictions/positions/mergeMergePOST /api/v5/predictions/positions/redeemRedeem
3. Order / Position Queries
Covers: query single order, order list, trade history, position queries.
| Code | Meaning |
|---|---|
| 100011 | Order does not exist |
| 400 | Path parameter parsing failed |
Applicable endpoints:
GET /api/v5/predictions/orders/{orderId}Query a single orderGET /api/v5/predictions/ordersQuery order listGET /api/v5/predictions/tradesQuery trade historyGET /api/v5/predictions/positionsQuery positions







