O2 Exchange Trading Bot Integration Guide
O2 is a fully on-chain order book exchange on Fuel. Every order placement, fill, and cancellation executes on-chain. This reference preserves the full integration context (workflow, signatures, byte layout rules, endpoints, errors, bot patterns) in a shorter operational format.
Primary docs:
- o2 API Overview
- Developer Quick Start (TypeScript)
- Developer Quick Start (Python)
- MCP Server
- Detailed reference file
1. Quick Start (TL;DR)
- Generate owner secp256k1 keypair and derive owner address (B256).
- Fund owner wallet (testnet/devnet/sandbox faucet; no mainnet faucet).
- Create trading account:
POST /v1/accounts-> persisttrade_account_id. - Deposit funds (faucet mint on non-mainnet or on-chain transfer on mainnet).
- Fetch market metadata:
GET /v1/markets-> market IDs, contract IDs, assets, decimals. - Whitelist account:
POST /analytics/v1/whitelist(required before trading in test/dev environments). - Generate session keypair; create session via
PUT /v1/sessionsigned by owner wallet. - Fetch account nonce:
GET /v1/accounts?trade_account_id=.... - Place orders via
POST /v1/session/actionssigned by session wallet. - Monitor and manage orders/balances (
/v1/orders,/v1/order,/v1/balance). - Cancel, settle, withdraw as needed (
session/actions,/v1/accounts/withdraw).
Critical rules:
- Session creation signing: owner key +
personalSigndigest flow. - Session actions signing: session key + raw
sha256(message)(no personalSign prefix). - Nonce increments on all session action tx attempts, including reverts.
- Respect max 5 actions per request.
- Include
SettleBalancein balance-sensitive cycles before new order placements.
2. Environment Setup
Devnet URLs
| Component | URL |
|---|---|
| API | https://api.devnet.o2.app |
| WS | wss://api.devnet.o2.app/v1/ws |
| Fuel RPC | https://devnet.fuel.network/v1/graphql |
| Faucet | https://fuel-o2-faucet.vercel.app/api/devnet/mint-v2 |
Testnet URLs
| Component | URL |
|---|---|
| API | https://api.testnet.o2.app |
| WS | wss://api.testnet.o2.app/v1/ws |
| Fuel RPC | https://testnet.fuel.network/v1/graphql |
| Faucet | https://fuel-o2-faucet.vercel.app/api/testnet/mint-v2 |
Mainnet API: https://api.o2.app (no faucet).
Sandbox faucet path uses /api/sandbox/mint-v2.
All REST endpoints are under /v1 and use JSON.
Required Dependencies
Required capabilities:
- secp256k1 signing and compact signature handling
- SHA-256
- UTF-8 encoding
- u64 big-endian byte encoding
- BigInt/u64-safe arithmetic
Common library options:
- Python:
coincurve/ecdsa,hashlib - Rust:
secp256k1,sha2 - JS/TS:
@noble/secp256k1,@noble/hashes/sha2.js - Go:
btcec,crypto/sha256
If owner wallet is EVM-based, include keccak256 support.
JS/TS caveat:
@noble/secp256k1v3 defaults toprehash: true. In O2 flows passprehash: falseto avoid double hashing and recovered-address mismatch (4000).
3. Authentication Architecture
O2 identity model:
- Owner Wallet: controls account and withdrawal capability.
- Trading Account (
trade_account_id): on-chain contract that holds funds and executes trading calls. - Session Wallet: delegated short-lived key for trading actions.
Owner responsibilities:
- account creation
- session creation/rotation
- privileged account operations (withdraw/upgrade/actions)
Session capabilities:
- create/cancel orders
- settle balances
- cannot withdraw or transfer ownership funds
Address support:
- Fuel-native owner addresses (B256)
- EVM owner addresses represented in B256 form (zero-padded to 32 bytes)
4. Step-by-Step Walkthrough
4.1 Generate a Wallet
Generate owner secp256k1 keypair and derive B256 identity.
Persist:
owner_private_keyowner_b256_address
4.2 Fund Your Wallet via Faucet (Testnet/Devnet/Sandbox Only)
Mint endpoint:
POST https://fuel-o2-faucet.vercel.app/api/<network>/mint-v2
{"address":"0x<target_b256_or_trade_account>"}Use owner address first; later you can mint directly to trade_account_id.
Mint to a wallet address
Send owner B256 in address.
Mint directly to a trading account contract
After account creation, fund trade_account_id directly in test/dev/sandbox.
4.3 Create a Trading Account
POST /v1/accounts
{"identity":{"Address":"0x<owner_b256>"}}Persist:
trade_account_id- deploy tx metadata if returned
4.4 Deposit Funds
Option A: Mint Directly to the Trading Account (Devnet/Testnet)
Use faucet and set address = trade_account_id.
Option B: On-Chain Transfer (Mainnet / Production)
Transfer asset to account contract (trade_account_id) on Fuel.
4.5 Fetch Market Info
GET /v1/markets
For each market cache:
market_id- order book
contract_id base_asset/quote_asset- decimal precision configuration
- top-level registry IDs when needed by advanced actions (for example referer registration)
4.5b Whitelist Your Trading Account (Required Before Trading)
POST /analytics/v1/whitelist
{"tradeAccount":"0x<trade_account_id>"}Whitelist missing/misconfigured state can block order placement.
4.6 Create a Trading Session
Session request binds a session wallet to selected contract IDs until expiry.
Request skeleton:
PUT /v1/session
{
"session": {"Address":"0x<session_b256>"},
"expiry": "1737504000",
"contract_ids": ["0x<market_contract_id>"],
"signature": {"Secp256k1":"0x<owner_signature>"}
}Required header:
O2-Owner-Id: 0x<owner_b256>
Critical details:
- owner key signs session creation
- use personalSign-style digest prefixing
- session scope is restricted to allowlisted contracts
- unsupported signature schemes return
4000
Step 1: Generate a Session Wallet
Persist:
session_private_keysession_b256_address
Step 2: Get the Current Nonce
GET /v1/accounts?trade_account_id=0x... -> read nonce.
Step 3: Construct Signing Bytes
Session signing bytes must deterministically include:
- function selector for
set_session - owner identity
- session identity
- expiry
- allowed contract IDs
- nonce
Any byte mismatch causes signature recovery mismatch.
Step 4: Sign with Owner Wallet (personalSign)
Use digest form:
sha256("\x19Fuel Signed Message:\n" + len(message) + message)
Step 4b: EVM Owner Signing (Alternative)
For EVM owners:
- keep EVM signing/recovery behavior consistent
- still transmit identity as B256 format
Step 5: Send the Session Request
Submit request with owner header + owner signature.
4.7 Place an Order
Endpoint:
POST /v1/session/actions
Limits:
- max 5 actions per request
- max 5 market groups per request
Canonical action ordering:
- stale cancels (optional)
SettleBalance- new creates
Signing Process for Session Actions
Session action signing rules:
- digest: raw
sha256(message)(no personalSign prefix) - signer: session key, not owner key
- signature must cover exact serialized payload bytes and nonce
Minimal payload shape:
{
"nonce": "123",
"actions": [
{
"market_id": "0x<market_id>",
"actions": [
{"SettleBalance":{"to":{"ContractId":"0x<trade_account_id>"}}},
{"CreateOrder":{"side":"Buy","price":"1000000","quantity":"1000000","order_type":"Spot"}}
]
}
],
"collect_orders": true,
"signature": {"Secp256k1":"0x<session_signature>"}
}Action mapping:
CreateOrder->OrderBook.create_orderCancelOrder->OrderBook.cancel_orderSettleBalance->OrderBook.settle_balanceRegisterReferer->TradeAccountRegistry.register_referer
Byte/encoding rules:
- Fuel function selector is not keccak; use
u64_be(len(name)) + utf8(name) - all u64 numeric fields are 8-byte big-endian
- gas field typically set to
u64::MAXclient-side; server can override OrderTypeenum bytes must match on-chain discriminant+payload layout- side determines forwarded asset/amount for create flows
OrderType variants used in OrderArgs:
Limit: variant 0 with payloadprice+timestampSpot: variant 1FillOrKill: variant 2PostOnly: variant 3Market: variant 4BoundedMarket: variant 5 withmax_price+min_price
Complete Request Example
Production requests must have:
- fresh nonce
- correctly scaled integer amounts
- exact market/contract IDs
- signature generated over exact message bytes
collect_orders: truewhen you need returned order IDs for lifecycle tracking
4.8 Check Order Status
Get All Orders
GET /v1/orders?market_id=...&contract=...&direction=desc&count=20
Supports:
is_open=true- pagination via
start_timestamp+start_order_id
Get a Single Order
GET /v1/order?market_id=...&order_id=...
4.9 Cancel an Order
Use CancelOrder action via POST /v1/session/actions.
Batch revert risk:
- canceling an already-filled order may revert the full batch.
- mitigate via live order state tracking and stale-order filtering.
4.10 Check Balances
GET /v1/balance?asset_id=...&contract=...
Endpoint selection rules:
- provide either
addressorcontractas supported by query contract.
4.11 Withdraw Funds
POST /v1/accounts/withdraw (owner-authenticated) withdraws from the trading account to a Fuel Ignition destination only. It does not bridge funds to Base, Ethereum, or another EVM chain.
For EVM-chain withdrawals, use the Fast Bridge withdrawal flow instead. See the fast-bridge/withdrawals skill, its withdrawal reference flows, and the Fuel Fast Bridge withdrawal flow.
Typical fields:
asset_idamount- Fuel-side destination
toidentity
Session key cannot withdraw.
5. WebSocket Real-Time Data
Connection
- Devnet:
wss://api.devnet.o2.app/v1/ws - Testnet:
wss://api.testnet.o2.app/v1/ws
Subscribe to Order Book Depth
Topic: depth updates keyed by market_id and precision settings.
Subscribe to Your Orders
Topic: order lifecycle events keyed by account identity (contract/trade account).
Subscribe to Trades
Topic: market trade stream keyed by market_id.
Subscribe to Balances
Topic: balance updates keyed by address/contract identity.
Subscribe to Nonce Updates
Topic: account nonce changes for replay protection tracking.
Unsubscribe
Use matching unsubscribe message with same topic and identity.
Identity Format
Common identity keys:
market_idcontract/trade_account_id- optional stream-specific cursor/window fields
Example:
{"action":"subscribe","topic":"orders","identity":{"market_id":"0x...","contract":"0x..."}}Operational guidance:
- Use WS to avoid stale cancels and nonce drift.
- Keep REST polling as fallback for reconciliation.
6. Order Types Reference
Supported order types:
"Spot": resting order, partial fills allowed."Market": immediate execution at available prices.{"Limit":["<price>","<timestamp>"]}: explicit price + timestamped limit variant."FillOrKill": full fill immediately or cancel."PostOnly": maker-only placement.{"BoundedMarket":{"max_price":"...","min_price":"..."}}: market behavior with bound constraints.
JSON Format for Each Order Type
"Spot"
"Market"
"FillOrKill"
"PostOnly"
{"Limit":["1000000","1730000000"]}
{"BoundedMarket":{"max_price":"1100000","min_price":"900000"}}7. Complete REST API Reference
All endpoints are JSON and rooted under /v1 unless otherwise noted.
Market Data
GET /v1/marketsGET /v1/markets/summary?market_id=...GET /v1/markets/ticker?market_id=...
Order Book & Depth
GET /v1/depth?market_id=...&precision=...- Valid precision values include powers of ten from
10up to1000000000.
Trading Data
GET /v1/trades?market_id=...&direction=...&count=...GET /v1/trades_by_account?market_id=...&contract=...&direction=...&count=...GET /v1/bars?market_id=...&from=...&to=...&resolution=...
Constraints:
- trades count max 50
directionandcountrequired for trades- bars max 5000
- bar resolutions:
1s,1m,5m,15m,30m,1h,4h,1d,1w,1M
Account & Balance
POST /v1/accountsGET /v1/accounts?owner=...GET /v1/accounts?owner_contract=...GET /v1/accounts?trade_account_id=...GET /v1/balance?asset_id=...&address=...GET /v1/balance?asset_id=...&contract=...
Lookup rules:
- account lookup expects exactly one of
owner,owner_contract,trade_account_id - balance lookup uses either
addressorcontractper endpoint contract
Order Management
GET /v1/orders?market_id=...&contract=...&direction=...&count=...GET /v1/order?market_id=...&order_id=...
Order query constraints:
- max 200 per orders request
- optional
is_open=true - pagination by
start_timestamp+start_order_id
Session Management
PUT /v1/sessionPOST /v1/session/actions
Account Operations
POST /v1/accounts/actionsPOST /v1/accounts/upgradePOST /v1/accounts/withdraw
Analytics Endpoints
POST /analytics/v1/whitelistGET /analytics/v1/referral/code-info?code=...
Aggregator Endpoints
Uses market_pair notation (for example FUEL_USDC) instead of hex market IDs.
GET /v1/aggregated/assetsGET /v1/aggregated/orderbook?market_pair=...&depth=...&level=...GET /v1/aggregated/summaryGET /v1/aggregated/tickerGET /v1/aggregated/trades?market_pair=...
System
GET /v1/ws(upgrade target for WebSocket connections)
Protected endpoint header:
O2-Owner-Id: 0x<owner_b256>required for session/account operation routes.
8. Error Handling
Error Response Format
Common structured response:
{"code":2000,"message":"..."}POST /v1/session/actions can also return on-chain revert style:
{"message":"Revert(...)","reason":"...","receipts":[...]}Success/failure check:
- success when
tx_idexists - failure when
messageexists andtx_idis absent
Complete Error Code Table
General (1xxx):
1000 InternalError1001 InvalidRequest1002 ParseError1003 RateLimitExceeded1004 GeoRestricted
Market (2xxx):
2000 MarketNotFound2001 MarketPaused2002 MarketAlreadyExists
Order (3xxx):
3000 OrderNotFound3001 OrderNotActive3002 InvalidOrderParams
Account/Session (4xxx):
4000 InvalidSignature4001 InvalidSession4002 AccountNotFound4003 WhitelistNotConfigured
Trade (5xxx):
5000 TradeNotFound5001 InvalidTradeCount
Subscription/WebSocket (6xxx):
6000 AlreadySubscribed6001 TooManySubscriptions6002 SubscriptionError
Validation (7xxx):
7000 InvalidAmount7001 InvalidTimeRange7002 InvalidPagination7003 NoActionsProvided7004 TooManyActions
Block/Events (8xxx):
8000 BlockNotFound8001 EventsNotFound
Common Error Scenarios and Solutions
4000 on session creation:
- wrong signing mode (must be personalSign flow)
- wrong key (must be owner key)
- double hashing from signer misconfiguration
4000 on session actions:
- wrong signing mode (must be raw sha256 signing)
- wrong key (must be session key)
- byte mismatch from nonce/action/ordering differences
7004:
- request exceeds 5 actions; split batches
4001:
- session missing/expired; rotate via
PUT /v1/session
1003:
- apply backoff + jitter and request pacing
9. Common Bot Patterns
Testnet Market Conditions
Test/dev books can be thin or one-sided. Check depth before placing crossing orders to avoid unexpected fills.
Market Maker Skeleton
Loop:
- fetch external reference
- compute buy/sell quotes from spread policy
- scale to chain integers with precision limits
- fetch latest nonce
- build atomic batch (cancel stale, settle, create new orders)
- sign with session key and submit
- record returned order IDs for next cycle
Simple Taker / Sniper
Loop:
- subscribe to WS depth/trades
- wait for trigger condition
- submit bounded market or targeted spot order
- verify outcome and refresh state
Order Management Best Practices
- Settle before creating orders in capital-constrained loops.
- Track nonce locally and increment after each action tx attempt.
- Use WS order updates to reduce stale-cancel reverts.
- Keep fallback reconciliation via REST.
- Proactively rotate sessions before expiry.
10. Appendix
Fuel ABI Encoding Rules
Core encoding rules:
- selector bytes:
u64_be(len(name)) + utf8(name) - integers: u64 big-endian
- identities: discriminant + value bytes
- enums: discriminant + active variant payload
For order/session action bytes, the encoded layout must exactly match on-chain ABI expectations.
Decimal Handling Rules
Use market metadata for:
- token decimals
- max precision constraints
Conversion pattern:
scaled = floor(human * 10^decimals)- apply precision truncation according to market limits
- ensure scaled outputs fit expected u64 ranges
Fee Structure
Fee schedules vary by market/program and can differ by environment. Read current live data and avoid hardcoded assumptions.
Rate Limit Summary
Implement:
- exponential backoff
- jitter
- burst throttling
- safe retry policy for idempotent paths
Session Expiry Guidelines
- Track expiry when session is created.
- Renew proactively for long-running bots.
- Keep owner signing path available for immediate recovery.