Skip to content
🌐Network: Mainnetβ–Ό

Developer Quick Start - TypeScript ​

Build your first o2 trade in minutes using the official SDK.

Prerequisites ​

  • Node.js v22.4+ (or Bun)
  • A private key (we'll generate one below, or use an existing Fuel wallet key)

1. Set Up Your Project ​

bash
mkdir o2-quickstart && cd o2-quickstart
bun init -y && bun install @o2exchange/sdk
bash
mkdir o2-quickstart && cd o2-quickstart
npm init -y && npm install @o2exchange/sdk

Create your trading script:

bash
touch index.ts

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 index.ts. The SDK handles network configuration, signing, encoding, and nonce management automatically.

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

Network SelectionMainnet
import { Network } from '@o2exchange/sdk';
const network = Network.MAINNET;  // https://api.o2.app
typescript
import { Network, O2Client } from "@o2exchange/sdk";

// Configuration
const PRIVATE_KEY = "0xYOUR_PRIVATE_KEY"; // Use env vars in production!

async function main() {
  // 1. Initialize client (use Network.MAINNET for production)
  const client = new O2Client({ network: Network.TESTNET });

  // 2. Load wallet
  // Use loadWallet() for an existing key, or generateWallet() for a new one
  const wallet = O2Client.loadWallet(PRIVATE_KEY);
  console.log("Owner address:", wallet.b256Address);

  // 3. Set up trading account
  // Idempotent: creates account, mints test tokens (testnet/devnet),
  // and whitelists β€” safe to call on every startup
  const { tradeAccountId } = await client.setupAccount(wallet);
  console.log("Trade account:", tradeAccountId);

  // Wait for faucet funds to settle
  await new Promise((r) => setTimeout(r, 5000));

  // 4. Fetch markets
  const markets = await client.getMarkets();
  const market = markets[0];
  const pair = `${market.base.symbol}/${market.quote.symbol}`;
  console.log("Trading on:", pair);

  // 5. Check balances
  const balances = await client.getBalances(tradeAccountId);
  for (const [symbol, bal] of Object.entries(balances)) {
    console.log(`${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.
  await client.createSession(wallet, [pair], 30);
  console.log("Session created:", client.session!.sessionAddress);

  // 7. Place an order
  // Prices and quantities are human-readable strings β€” the SDK auto-scales them.
  // SettleBalance is auto-prepended by default.
  // Use a low price to avoid immediate fill; notional must be >= ~5 USDC.
  const orderRes = await client.createOrder(pair, "buy", "1", "5");
  console.log("Order tx:", orderRes.txId);

  if (orderRes.success && orderRes.orders?.length) {
    const order = orderRes.orders[0];
    console.log("Order ID:", order.order_id);

    // 8. Check open orders
    await new Promise((r) => setTimeout(r, 3000));
    const openOrders = await client.getOrders(tradeAccountId, pair, true);
    console.log("Open orders:", openOrders.length);

    // 9. Cancel order
    if (openOrders.length > 0) {
      await client.cancelOrder(openOrders[0].order_id, pair);
      console.log("Order cancelled");
    }
  }

  // 10. Check final balance
  const finalBalances = await client.getBalances(tradeAccountId);
  for (const [symbol, bal] of Object.entries(finalBalances)) {
    console.log(`${symbol}: ${bal.trading_account_balance}`);
  }

  client.close();
}

main().catch(console.error);

Run the script:

bash
bun index.ts
bash
npx tsx index.ts

4. Batch Orders ​

You can submit up to 5 actions per request using batchActions with type-safe action factories.

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

typescript
import {
  createOrderAction,
  settleBalanceAction,
} from "@o2exchange/sdk";

await client.createSession(wallet, ["fFUEL/fUSDC"], 30);
await client.refreshNonce();

const response = await client.batchActions([
  {
    market: "fFUEL/fUSDC",
    actions: [
      settleBalanceAction(),
      createOrderAction("buy", "0.1", "50"),
      createOrderAction("buy", "1", "5"),
      createOrderAction("sell", "2", "5", "PostOnly"),
    ],
  },
], true);

console.log("Batch tx:", response.txId);
if (response.orders) {
  for (const order of response.orders) {
    console.log(`${order.side} order: ${order.order_id}`);
  }
}

WARNING

createOrder automatically prepends a SettleBalance call (controlled by settleFirst, default true). When using batchActions, add settleBalanceAction() 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

@o2exchange/sdk v0.4.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 preparationProof 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:

typescript
import {
  FAST_BRIDGE_MAINNET_URL,
  FAST_BRIDGE_TESTNET_URL,
  FastBridgeClient,
  Network,
} from "@o2exchange/sdk";

const network = Network.MAINNET;
const bridge = new FastBridgeClient({
  baseUrl:
    network === Network.MAINNET
      ? FAST_BRIDGE_MAINNET_URL
      : FAST_BRIDGE_TESTNET_URL,
});

const info = await bridge.getInfo();
const { assets } = await bridge.getAssets();

// assetId is the full Fuel AssetId returned by getAssets(), not a symbol,
// EVM token address, or asset sub-ID. Amounts use that Fuel asset's base units.
console.log(info.chains, assets);

To bridge directly into an o2 trading account, use its tradeAccountId as the Fuel recipient and set toType 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 ask the EVM wallet to sign it.

The example below uses ethers v6 for signing (npm install ethers):

typescript
import {
  parseEvmUnsignedTransaction,
  parsePreparationProof,
} from "@o2exchange/sdk";
import { Transaction, Wallet as EvmWallet } from "ethers";

const sourceChainId = 8453; // Base mainnet; select a chain returned by getInfo()
const assetId = "0xFULL_FUEL_ASSET_ID";
const amount = "1000000"; // Fuel asset base units, never a float
const fuelRecipient = "0xFUEL_ADDRESS";
const evmWallet = new EvmWallet(process.env.EVM_PRIVATE_KEY!);

const route = await bridge.getDepositInfo(sourceChainId, assetId, amount);
if (!route.routeEnabled || route.paused) throw new Error("Deposit route unavailable");

const prepared = await bridge.prepareDeposit({
  sourceChainId,
  from: evmWallet.address,
  to: fuelRecipient,
  toType: "address", // Use "contract" when the Fuel recipient is a contract.
  assetId,
  amount,
});

const proof = parsePreparationProof(prepared.preparationProof);
if (proof.expiresAt <= Date.now() / 1000) throw new Error("Prepare again");

const inspected = parseEvmUnsignedTransaction(prepared.unsignedTransaction);
if (
  inspected.chainId !== BigInt(sourceChainId) ||
  inspected.recipient.toLowerCase() !== fuelRecipient.toLowerCase()
) {
  throw new Error("Prepared deposit does not match the approved transfer");
}

// Also verify messengerAddress, tokenAddress, amount, method, gas limit, and
// fee caps. The parsed amount is in EVM token units (or wei for native ETH),
// which can differ from the Fuel-unit amount sent to prepareDeposit().
const signed = await evmWallet.signTransaction(
  Transaction.from(prepared.unsignedTransaction),
);
const signature = Transaction.from(signed).signature?.serialized;
if (!signature) throw new Error("EVM wallet did not produce a signature");

const submitted = await bridge.submitDeposit({
  unsignedTransaction: prepared.unsignedTransaction,
  preparationProof: prepared.preparationProof,
  signature,
});

console.log(
  await bridge.getDepositStatus(submitted.sourceChainId, submitted.evmTxHash),
);

Native ETH routes automatically prepare depositETH; ERC-20 routes prepare deposit and may require an allowance. When the token supports EIP-2612, supply a separately signed permit to prepareDeposit() 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 transactionId with the Fuel owner key.

typescript
import {
  O2Client,
  parseFuelUnsignedTransaction,
  parsePreparationProof,
} from "@o2exchange/sdk";
import {
  bytesToHex,
  fuelCompactSign,
  hexToBytes,
} from "@o2exchange/sdk/internals";

const destinationChainId = 8453;
const assetId = "0xFULL_FUEL_ASSET_ID";
const amount = "1000000"; // Gross amount in Fuel asset base units
const evmRecipient = "0xEVM_ADDRESS";
const fuelWallet = O2Client.loadWallet(process.env.FUEL_PRIVATE_KEY!);

// Set these from trusted Fuel network/consensus configuration, not from the
// prepared response. maxInputs affects FuelVM pointer calculation.
const trustedFuelChainId = BigInt(process.env.FUEL_CHAIN_ID!);
const trustedFuelMaxInputs = Number.parseInt(
  process.env.FUEL_MAX_INPUTS!,
  10,
);
if (
  !Number.isSafeInteger(trustedFuelMaxInputs) ||
  trustedFuelMaxInputs <= 0
) {
  throw new Error("Invalid FUEL_MAX_INPUTS");
}

const route = await bridge.getWithdrawInfo(
  destinationChainId,
  assetId,
  amount,
);
if (!route.routeEnabled || route.paused) throw new Error("Withdrawal unavailable");

const prepared = await bridge.prepareWithdraw({
  destinationChainId,
  from: fuelWallet.b256Address,
  to: evmRecipient,
  assetId,
  amount,
});

if (BigInt(prepared.fuelChainId) !== trustedFuelChainId) {
  throw new Error("Unexpected Fuel chain");
}
const proof = parsePreparationProof(prepared.preparationProof);
if (proof.expiresAt <= Date.now() / 1000) throw new Error("Prepare again");

const inspected = parseFuelUnsignedTransaction(
  prepared.unsignedTransaction,
  trustedFuelChainId,
  trustedFuelMaxInputs,
);
if (
  inspected.destinationChainId !== destinationChainId ||
  inspected.recipient.toLowerCase() !== evmRecipient.toLowerCase() ||
  inspected.assetId.toLowerCase() !== assetId.toLowerCase() ||
  inspected.grossAmount !== BigInt(amount)
) {
  throw new Error("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.
const signature = bytesToHex(
  fuelCompactSign(
    fuelWallet.privateKey,
    hexToBytes(inspected.transactionId),
  ),
);

const submitted = await bridge.submitWithdraw({
  unsignedTransaction: prepared.unsignedTransaction,
  preparationProof: prepared.preparationProof,
  signature,
});

console.log(await bridge.getWithdrawStatus(submitted.fuelTxId));

parsePreparationProof() 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 unsignedTransaction and preparationProof 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 @o2exchange/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
setupAccount()Idempotent β€” creates account, funds via faucet, whitelists. Safe on every startup
SettleBalanceMoves filled order proceeds into your available balance. Auto-prepended by createOrder

Nonce management

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

typescript
await client.refreshNonce();

Turbo Accounts ​

Trade o2's capital instead of your own. client.turbo opens an account and trades it, composing the funding leg into the same signed batch as the order, so long() and short() are all you call.

typescript
await client.createSession(wallet, ["ETH/USDC"], { turbo: true });
await client.turbo.long("ETH/USDC", { notional: "2000" });

Requires @o2exchange/sdk 0.4.0 or later, and the session must be created with turbo: true β€” that scope is signed and cannot be widened afterwards. See Turbo Accounts for the product and the Turbo Accounts API reference for the integration.

SDK Reference ​

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