CCXT-Compatible API
Use o2 through familiar CCXT methods and normalized response shapes in TypeScript or Python. The adapter is maintained by o2 and is intended for trading bots and existing CCXT integrations.
Public alpha
The adapter is not part of the upstream CCXT package, so o2 is not available as ccxt.o2. Instantiate O2CCXT directly, validate your integration on testnet, and begin with capped balances and independent risk limits.
Runtime support
| Runtime | Support |
|---|---|
| TypeScript | Node.js 22.4+ |
| Python | Python 3.10–3.13, async only |
| Browser | Use the core o2 SDK instead of the CCXT adapter |
| CCXT | >=4.5.0,<5 |
The TypeScript CCXT entry point is server-side only for the alpha. The core @o2exchange/sdk package continues to support modern browsers.
Install
npm install @o2exchange/sdk ccxtpip install "o2-sdk[ccxt]"CCXT is optional. Importing the core o2 SDK does not load it.
Read public market data
Public methods do not require a wallet, trading account, or session.
import { O2CCXT } from "@o2exchange/sdk/ccxt";
const exchange = new O2CCXT({ network: "testnet" });
try {
await exchange.loadMarkets();
const symbol = "fFUEL/fUSDC";
const [ticker, book, trades, candles] = await Promise.all([
exchange.fetchTicker(symbol),
exchange.fetchOrderBook(symbol, 20),
exchange.fetchTrades(symbol, undefined, 50),
exchange.fetchOHLCV(symbol, "1m", undefined, 100),
]);
console.log("last", ticker.last);
console.log("best bid", book.bids[0]);
console.log("latest trade", trades.at(-1));
console.log("latest candle", candles.at(-1));
} finally {
await exchange.close();
}import asyncio
from o2_sdk.ccxt import O2CCXT
async def main():
exchange = O2CCXT({"network": "testnet"})
try:
await exchange.load_markets()
symbol = "fFUEL/fUSDC"
ticker, book, trades, candles = await asyncio.gather(
exchange.fetch_ticker(symbol),
exchange.fetch_order_book(symbol, 20),
exchange.fetch_trades(symbol, limit=50),
exchange.fetch_ohlcv(symbol, "1m", limit=100),
)
print("last", ticker["last"])
print("best bid", book["bids"][0])
print("latest trade", trades[-1])
print("latest candle", candles[-1])
finally:
await exchange.close()
asyncio.run(main())Set up trading
Private methods require an o2 trading account and an active session. These steps are explicit: constructing O2CCXT never creates an account or signs an on-chain action.
Store owner keys in a secret manager or use an external signer in production. Do not hard-code private keys.
import { O2CCXT } from "@o2exchange/sdk/ccxt";
const exchange = new O2CCXT({
network: "testnet",
privateKey: process.env.O2_PRIVATE_KEY!,
});
try {
const account = await exchange.setupAccount();
const session = await exchange.createSession(["fFUEL/fUSDC"], 30);
console.log("trade account", account.tradeAccountId);
console.log("session", session.sessionAddress);
await exchange.loadMarkets();
console.log(await exchange.fetchBalance());
} finally {
await exchange.close();
}import asyncio
import os
from o2_sdk.ccxt import O2CCXT
async def main():
exchange = O2CCXT({
"network": "testnet",
"privateKey": os.environ["O2_PRIVATE_KEY"],
})
try:
account = await exchange.setup_account()
session = await exchange.create_session(["fFUEL/fUSDC"], 30)
print("trade account", account.trade_account_id)
print("session expires", session.session_expiry)
await exchange.load_markets()
print(await exchange.fetch_balance())
finally:
await exchange.close()
asyncio.run(main())setupAccount / setup_account is idempotent. On testnet it also attempts to fund the trading account through the faucet. Mainnet has no faucet; deposit funds before trading.
For normal bot restarts, securely persist the session returned by createSession / create_session and restore it instead of creating a new one:
const exchange = new O2CCXT({
network: "mainnet",
session: savedSession,
tradeAccountId: savedSession.tradeAccountId,
});exchange = O2CCXT({
"network": "mainnet",
"session": saved_session,
"tradeAccountId": saved_session.trade_account_id,
})Place and manage limit orders
Order amounts are base-asset quantities. Prices and amounts use standard CCXT human-readable units; the adapter applies the market precision before sending the native o2 action.
const symbol = "fFUEL/fUSDC";
const order = await exchange.createOrder(
symbol,
"limit",
"buy",
50,
0.02,
{
orderType: "PostOnly",
settleFirst: true,
},
);
console.log(order.id, order.status);
const indexed = await exchange.fetchOrder(order.id, symbol);
console.log(indexed.filled, indexed.remaining);
const canceled = await exchange.cancelOrder(order.id, symbol);
console.log(canceled.status);symbol = "fFUEL/fUSDC"
order = await exchange.create_order(
symbol,
"limit",
"buy",
50,
0.02,
{
"orderType": "PostOnly",
"settleFirst": True,
},
)
print(order["id"], order["status"])
indexed = await exchange.fetch_order(order["id"], symbol)
print(indexed["filled"], indexed["remaining"])
canceled = await exchange.cancel_order(order["id"], symbol)
print(canceled["status"])Supported native order types in params.orderType are Spot, PostOnly, and FillOrKill. A normal CCXT limit order defaults to Spot.
Order status may take a moment to update
After you create, fill, or cancel an order, its latest status may not appear in the API immediately. Call fetchOrder / fetch_order again after a short delay, using a limited number of retries, before treating the reported status as final.
Place a price-protected market order
o2 does not expose unbounded market orders. The adapter maps a CCXT market order to a native FillOrKill order with explicit price protection.
Both maxPrice and minPrice are required. A buy executes at or below maxPrice; a sell executes at or above minPrice. The full amount fills within the bounds or the order fails, and no remainder rests on the book.
const order = await exchange.createOrder(
"fFUEL/fUSDC",
"market",
"buy",
50,
undefined,
{
minPrice: 0.019,
maxPrice: 0.021,
},
);order = await exchange.create_order(
"fFUEL/fUSDC",
"market",
"buy",
50,
None,
{
"minPrice": 0.019,
"maxPrice": 0.021,
},
)The immediate create response reports type: "market". A later indexed fetch currently exposes the native representation as type: "limit" with timeInForce: "FOK".
Method coverage
| TypeScript | Python | Notes |
|---|---|---|
loadMarkets, fetchMarkets | load_markets, fetch_markets | Spot markets only |
fetchTicker | fetch_ticker | Unavailable statistics are null / None |
fetchOrderBook, fetchL2OrderBook | fetch_order_book, fetch_l2_order_book | params.precision accepts levels 1–18 |
fetchTrades | fetch_trades | Maximum 50 results per request |
fetchOHLCV | fetch_ohlcv | 1m, 5m, 15m, 30m, 1h, 4h, 1d |
fetchBalance | fetch_balance | Requires a trade account or session |
createOrder | create_order | Limit and bounded FOK market orders |
fetchOrder, fetchOrders | fetch_order, fetch_orders | Single-order lookup requires symbol |
fetchOpenOrders, fetchClosedOrders | fetch_open_orders, fetch_closed_orders | Omit symbol to query loaded markets |
fetchMyTrades | fetch_my_trades | Self-trades return once with side: null / None |
cancelOrder, cancelAllOrders | cancel_order, cancel_all_orders | cancelOrder requires symbol |
withdraw | withdraw | Requires the owner signer |
Use exchange.has to feature-detect methods at runtime.
o2 extension methods
The adapters also expose explicit o2 lifecycle operations:
| TypeScript | Python | Purpose |
|---|---|---|
setupAccount() | setup_account() | Create or resolve the trading account |
createSession() | create_session() | Authorize an ephemeral trading session |
restoreSession() | restore_session() | Reuse a persisted session |
settleBalance() | settle_balance() | Settle filled proceeds |
batchActions() | batch_actions() | Submit a native o2 action batch |
Signing, encoding, session state, and nonce management remain owned by the native o2 SDK.
Errors and safe retries
The adapter raises official CCXT error categories such as AuthenticationError, InsufficientFunds, InvalidOrder, BadSymbol, RateLimitExceeded, OrderNotFound, and NetworkError.
Private submissions are never automatically retried. A timeout or lost response can happen after o2 accepted the action.
import { O2AmbiguousSubmission } from "@o2exchange/sdk/ccxt";
try {
await exchange.createOrder("fFUEL/fUSDC", "limit", "buy", 50, 0.02);
} catch (error) {
if (error instanceof O2AmbiguousSubmission) {
// Do not immediately resubmit. Reconcile orders and the account nonce.
const open = await exchange.fetchOpenOrders("fFUEL/fUSDC");
console.error("Submission outcome unknown", open, error.originalError);
}
throw error;
}from o2_sdk.ccxt import O2AmbiguousSubmission
try:
await exchange.create_order("fFUEL/fUSDC", "limit", "buy", 50, 0.02)
except O2AmbiguousSubmission as error:
# Do not immediately resubmit. Reconcile orders and the account nonce.
open_orders = await exchange.fetch_open_orders("fFUEL/fUSDC")
print("Submission outcome unknown", open_orders, error.original_error)
raiseNever blindly retry an ambiguous private action
If the response was lost after acceptance, retrying can create a duplicate order or consume another nonce. Reconcile open and closed orders and refresh the account state before deciding what to do next.
Known limitations
- This integration is maintained by o2 and is not registered upstream as
ccxt.o2. - TypeScript supports Node.js only for the alpha; use the core SDK in browsers.
- Python is async-only and extends
ccxt.async_support.Exchange. - CCXT Pro
watch*methods are not implemented. Use the native o2stream*/stream_*methods. - Historical methods accept CCXT's
sinceandlimitarguments but do not automatically paginate unlimited history. - Complete ticker statistics, fee schedules, and per-result fee data are not yet available. Unsupported fields are
null/None. - CCXT returns JavaScript
numberor Pythonfloatvalues. Use the nativeinfopayload when exact scaled-integer accounting is required. - Always call or await
close()when finished to release client resources.
For native SDK usage, WebSocket streams, external signers, and lower-level account operations, see the developer quick starts.