Skip to content
🌐Network: Mainnet▼

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 ​

RuntimeSupport
TypeScriptNode.js 22.4+
PythonPython 3.10–3.13, async only
BrowserUse 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 ​

bash
npm install @o2exchange/sdk ccxt
bash
pip 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.

typescript
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();
}
python
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.

typescript
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();
}
python
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:

typescript
const exchange = new O2CCXT({
  network: "mainnet",
  session: savedSession,
  tradeAccountId: savedSession.tradeAccountId,
});
python
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.

typescript
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);
python
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.

typescript
const order = await exchange.createOrder(
  "fFUEL/fUSDC",
  "market",
  "buy",
  50,
  undefined,
  {
    minPrice: 0.019,
    maxPrice: 0.021,
  },
);
python
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 ​

TypeScriptPythonNotes
loadMarkets, fetchMarketsload_markets, fetch_marketsSpot markets only
fetchTickerfetch_tickerUnavailable statistics are null / None
fetchOrderBook, fetchL2OrderBookfetch_order_book, fetch_l2_order_bookparams.precision accepts levels 1–18
fetchTradesfetch_tradesMaximum 50 results per request
fetchOHLCVfetch_ohlcv1m, 5m, 15m, 30m, 1h, 4h, 1d
fetchBalancefetch_balanceRequires a trade account or session
createOrdercreate_orderLimit and bounded FOK market orders
fetchOrder, fetchOrdersfetch_order, fetch_ordersSingle-order lookup requires symbol
fetchOpenOrders, fetchClosedOrdersfetch_open_orders, fetch_closed_ordersOmit symbol to query loaded markets
fetchMyTradesfetch_my_tradesSelf-trades return once with side: null / None
cancelOrder, cancelAllOrderscancel_order, cancel_all_orderscancelOrder requires symbol
withdrawwithdrawRequires the owner signer

Use exchange.has to feature-detect methods at runtime.

o2 extension methods ​

The adapters also expose explicit o2 lifecycle operations:

TypeScriptPythonPurpose
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.

typescript
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;
}
python
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)
    raise

Never 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 o2 stream* / stream_* methods.
  • Historical methods accept CCXT's since and limit arguments 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 number or Python float values. Use the native info payload 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.