Skip to content
🌐Network: Mainnetβ–Ό

Developer Quick Start - Python ​

Build your first o2 trade using the official Python SDK.

Prerequisites ​

  • Python 3.10–3.13 (3.14+ not yet supported due to dependency constraints)
  • pip install o2-sdk

1. Set Up Your Project ​

bash
mkdir o2-quickstart && cd o2-quickstart
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

pip install o2-sdk

Create your trading script:

bash
touch main.py

2. Get Test Funds ​

ℹ️

Faucet is not available on mainnet. Deposit funds from your Fuel wallet to your trade account contract via the web-interface or via the Deposit flow.

3. Complete Trading Flow ​

Create main.py. The SDK handles network configuration, signing, encoding, and nonce management automatically.

Select your network (the SDK has built-in endpoints for each):

Network SelectionMainnet
from o2_sdk import Network
network = Network.TESTNET  # https://api.o2.app
python
import asyncio
from o2_sdk import Network, O2Client, OrderSide, OrderType

# Configuration
PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"  # Use env vars in production!


async def main():
    # 1. Initialize client
    client = O2Client(network=Network.TESTNET)

    # 2. Load wallet
    # Use load_wallet() for an existing key, or generate_wallet() for a new one
    wallet = client.load_wallet(PRIVATE_KEY)
    print(f"Owner address: {wallet.b256_address}")

    # 3. Set up trading account
    # Idempotent: creates account, mints test tokens (testnet/devnet),
    # and whitelists β€” safe to call on every startup
    account = await client.setup_account(wallet)
    print(f"Trade account: {account.trade_account_id}")

    # Wait for faucet funds to settle
    await asyncio.sleep(5)

    # 4. Fetch markets
    markets = await client.get_markets()
    market = markets[0]
    pair = market.pair
    print(f"Trading on: {pair}")

    # 5. Check balances
    balances = await client.get_balances(account.trade_account_id)
    for symbol, bal in balances.items():
        print(f"{symbol}: {bal.trading_account_balance}")

    # 6. Create session
    # Delegates trading authority to an ephemeral keypair.
    # The session is stored on the client and used implicitly by trading methods.
    # o2 sponsors gas for all session-based actions.
    session = await client.create_session(
        owner=wallet,
        markets=[pair],
        expiry_days=30,
    )
    print(f"Session created, expires: {session.session_expiry}")

    # 7. Place an order
    # Prices and quantities are human-readable β€” the SDK auto-scales them.
    # SettleBalance is auto-prepended by default (settle_first=True).
    # Use a low price to avoid immediate fill; notional must be >= ~5 USDC.
    result = await client.create_order(
        market=pair,
        side=OrderSide.BUY,
        price="1",
        quantity="5",
        order_type=OrderType.SPOT,
    )
    print(f"Order tx: {result.tx_id}")

    if result.success and result.orders:
        order = result.orders[0]
        print(f"Order ID: {order.order_id}")

        # 8. Check open orders
        await asyncio.sleep(3)
        open_orders = await client.get_orders(
            account.trade_account_id,
            pair,
            is_open=True,
        )
        print(f"Open orders: {len(open_orders)}")

        # 9. Cancel order
        if open_orders:
            await client.cancel_order(
                order_id=open_orders[0].order_id,
                market=pair,
            )
            print("Order cancelled")

    # 10. Check final balance
    final_balances = await client.get_balances(account.trade_account_id)
    for symbol, bal in final_balances.items():
        print(f"{symbol}: {bal.trading_account_balance}")

    await client.close()
    print("Done!")


if __name__ == "__main__":
    asyncio.run(main())

Run the script:

bash
python main.py

4. Batch Orders ​

Submit up to 5 actions per request using the fluent builder.

Create a new session scoped to the batch market, refresh the nonce, then submit:

python
from o2_sdk import OrderSide, OrderType

await client.create_session(owner=wallet, markets=["fFUEL/fUSDC"], expiry_days=30)
await client.refresh_nonce()

batch = (
    client.actions_for("fFUEL/fUSDC")
    .settle_balance()
    .create_order(OrderSide.BUY, "0.1", "50")
    .create_order(OrderSide.BUY, "1", "5")
    .create_order(OrderSide.SELL, "2", "5", OrderType.POST_ONLY)
    .build()
)

result = await client.batch_actions([batch], collect_orders=True)
print(f"Batch tx: {result.tx_id}")
if result.orders:
    for order in result.orders:
        print(f"{order.side} order: {order.order_id}")

WARNING

create_order automatically prepends a SettleBalance call (controlled by settle_first, default True). When using batch_actions, add .settle_balance() explicitly if needed. The total action count (including settle) must not exceed 5.

5. Deposit & Withdraw ​

The easiest way to deposit and withdraw funds is through the o2 trading UI. The UI supports deposits from and withdrawals to EVM chains (Base, Ethereum) and Fuel wallets, and handles all bridging, wrapping, and unwrapping automatically.

Programmatic deposits & withdrawals

o2-sdk v0.5.0 and newer includes FastBridgeClient for EVM-to-Fuel deposits and Fuel-to-EVM withdrawals. Continue below for the complete prepare, inspect, sign, submit, and status workflow.

Optional AI-agent guidance: use the installable Fast Bridge deposit and withdrawal skills alongside this canonical SDK quick start.

Programmatic Fast Bridge ​

FastBridgeClient is separate from O2Client and does not use an o2 trading session. It prepares a transaction for your wallet to inspect and sign, then submits the exact prepared transaction through the Fast Bridge proxy. The proxy is stateless: the preparation_proof authenticates the prepared transaction and expires after a short time.

NetworkProxy URL
Mainnethttps://bridge.o2.app
Testnethttps://bridge.testnet.o2.app

Initialize the client and discover the supported chains, assets, and routes:

python
from o2_sdk import (
    FAST_BRIDGE_MAINNET_URL,
    FAST_BRIDGE_TESTNET_URL,
    FastBridgeClient,
    Network,
)

network = Network.MAINNET
bridge_url = {
    Network.MAINNET: FAST_BRIDGE_MAINNET_URL,
    Network.TESTNET: FAST_BRIDGE_TESTNET_URL,
}.get(network)
if bridge_url is None:
    raise ValueError("Fast Bridge is not available on this network")

async with FastBridgeClient(bridge_url) as bridge:
    info = await bridge.get_info()
    assets = (await bridge.get_assets()).assets
    print(info.chains, assets)

asset_id is the full Fuel AssetId returned by get_assets(), not a symbol, EVM token address, or asset sub-ID. API amounts use that Fuel asset's base units and must be integer strings, never floats.

To bridge directly into an o2 trading account, use its trade_account_id as the Fuel recipient and set to_type to "contract". Use "address" when the recipient is a Fuel wallet.

Deposit: EVM to Fuel ​

The deposit flow prepares an unsigned EIP-1559 transaction. Parse it locally, verify every field against the transfer the user approved and independently trusted contract configuration, and only then sign the locally calculated digest. The following code runs inside the FastBridgeClient context above:

python
import os
import time

from o2_sdk import (
    O2Client,
    fuel_compact_sign,
    parse_evm_unsigned_transaction,
    parse_preparation_proof,
)
from o2_sdk.bridge.models import DepositPrepareRequest, SubmitRequest

source_chain_id = 8453  # Base mainnet; select a chain returned by get_info()
asset_id = "0xFULL_FUEL_ASSET_ID"
amount = "1000000"  # Fuel asset base units, never a float
fuel_recipient = "0xFUEL_ADDRESS"
evm_wallet = O2Client.load_evm_wallet(os.environ["EVM_PRIVATE_KEY"])

route = await bridge.get_deposit_info(source_chain_id, asset_id, amount)
if not route.route_enabled or route.paused:
    raise ValueError("Deposit route unavailable")

prepared = await bridge.prepare_deposit(
    DepositPrepareRequest(
        source_chain_id=source_chain_id,
        from_address=evm_wallet.evm_address,
        to=fuel_recipient,
        to_type="address",  # Use "contract" for a Fuel contract recipient.
        asset_id=asset_id,
        amount=amount,
    )
)

proof = parse_preparation_proof(prepared.preparation_proof)
if proof.expires_at <= time.time():
    raise ValueError("Prepare again")

inspected = parse_evm_unsigned_transaction(prepared.unsigned_transaction)
if (
    inspected.chain_id != source_chain_id
    or inspected.recipient.lower() != fuel_recipient.lower()
):
    raise ValueError("Prepared deposit does not match the approved transfer")

# Also verify messenger_address, token_address, amount, method, gas limit, and
# fee caps. Parsed amount uses EVM token units (or wei for native ETH), which
# can differ from the Fuel-unit amount sent to prepare_deposit().
signature = bytearray(
    fuel_compact_sign(
        evm_wallet.private_key,
        bytes.fromhex(inspected.signing_digest.removeprefix("0x")),
    )
)
signature.append(27 + (signature[32] >> 7))
signature[32] &= 0x7F

submitted = await bridge.submit_deposit(
    SubmitRequest(
        unsigned_transaction=prepared.unsigned_transaction,
        preparation_proof=prepared.preparation_proof,
        signature="0x" + signature.hex(),
    )
)
status = await bridge.get_deposit_status(
    submitted.source_chain_id,
    submitted.evm_tx_hash,
)
print(status)

Native ETH routes automatically prepare depositETH; ERC-20 routes prepare deposit and may require an allowance. When the token supports EIP-2612, provide a separately signed DepositPermit to prepare depositWithPermit.

Withdraw: Fuel to EVM ​

The withdrawal flow spends a funded Fuel wallet, not an o2 trading account. If funds are still in the trading account, call O2Client.withdraw() to move them to the owner wallet first. Then parse the prepared bridge transaction with a trusted Fuel chain ID and the network's trusted consensus maxInputs, verify the full transaction, and sign the locally calculated transaction_id with the Fuel owner key.

python
from o2_sdk import parse_fuel_unsigned_transaction
from o2_sdk.bridge.models import SubmitRequest, WithdrawPrepareRequest

destination_chain_id = 8453
asset_id = "0xFULL_FUEL_ASSET_ID"
amount = "1000000"  # Gross amount in Fuel asset base units
evm_recipient = "0xEVM_ADDRESS"
fuel_wallet = O2Client.load_wallet(os.environ["FUEL_PRIVATE_KEY"])

# Set these from trusted Fuel network/consensus configuration, not from the
# prepared response. maxInputs affects FuelVM pointer calculation.
trusted_fuel_chain_id = int(os.environ["FUEL_CHAIN_ID"])
trusted_fuel_max_inputs = int(os.environ["FUEL_MAX_INPUTS"])

route = await bridge.get_withdraw_info(destination_chain_id, asset_id, amount)
if not route.route_enabled or route.paused:
    raise ValueError("Withdrawal unavailable")

prepared = await bridge.prepare_withdraw(
    WithdrawPrepareRequest(
        destination_chain_id=destination_chain_id,
        from_address=fuel_wallet.b256_address,
        to=evm_recipient,
        asset_id=asset_id,
        amount=amount,
    )
)
if int(prepared.fuel_chain_id) != trusted_fuel_chain_id:
    raise ValueError("Unexpected Fuel chain")

proof = parse_preparation_proof(prepared.preparation_proof)
if proof.expires_at <= time.time():
    raise ValueError("Prepare again")

inspected = parse_fuel_unsigned_transaction(
    prepared.unsigned_transaction,
    trusted_fuel_chain_id,
    trusted_fuel_max_inputs,
)
if (
    inspected.destination_chain_id != destination_chain_id
    or inspected.recipient.lower() != evm_recipient.lower()
    or inspected.asset_id.lower() != asset_id.lower()
    or inspected.gross_amount != int(amount)
):
    raise ValueError("Prepared withdrawal does not match the approved transfer")

# Also verify every input, contract, output, bridge fee, network-fee cap,
# expiration block, and expected net amount before signing.
signature = fuel_compact_sign(
    fuel_wallet.private_key,
    bytes.fromhex(inspected.transaction_id.removeprefix("0x")),
)
submitted = await bridge.submit_withdraw(
    SubmitRequest(
        unsigned_transaction=prepared.unsigned_transaction,
        preparation_proof=prepared.preparation_proof,
        signature="0x" + signature.hex(),
    )
)
print(await bridge.get_withdraw_status(submitted.fuel_tx_id))

parse_preparation_proof() only displays the proof's version, key ID, expiry, and claimed signer. It does not verify the proofβ€”the proxy authenticates the proof and binds it to the exact unsigned transaction during submission. Always submit the unchanged transaction and proof returned by the matching prepare call.

Status endpoints can return 404 while a transaction is unknown; treat that as β€œnot found,” not fabricated pending state. Submission requests are single-attempt. If a request times out, check status before submitting again.

For every endpoint, native ETH and ERC-20 permit examples, complete inspection checks, and error handling, see the o2-sdk Fast Bridge example. For protocol architecture, see Fuel Fast Bridge, and for deployed contracts and asset IDs, see Network Identifiers.

Key Concepts ​

ConceptDescription
Trading AccountOn-chain contract holding your balances and order state
SessionEphemeral keypair authorized to trade. Enables fast trading without wallet popups
NonceCounter for replay protection. Auto-managed by the SDK
Gas Sponsorshipo2 pays gas fees for all session-based actions
setup_account()Idempotent β€” creates account, funds via faucet, whitelists. Safe on every startup
SettleBalanceMoves filled order proceeds into your available balance. Auto-prepended by create_order

Nonce management

The SDK auto-manages nonces. If you encounter errors after a failed transaction, call refresh_nonce() to re-sync:

python
await client.refresh_nonce(session)

SDK Reference ​

For the full API reference, advanced patterns (market making, WebSocket streams, external signers), and more examples, see the SDK documentation on GitHub.