Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.o1.exchange/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks you through a complete swap from a TypeScript client. We’ll trade 1,000 USDC for WETH on Base, then receive native ETH back instead of WETH.
Production base URL is currently https://quiet-bloodhound-531.convex.site. If you’re integrating against a vanity domain (e.g. https://api.o1.exchange/dex), substitute that throughout.

Prerequisites

API key

See Authentication to request one.

Base RPC URL

Any Base mainnet RPC works. We use viem defaults below.

Wallet with USDC

Plus a small ETH balance to pay gas (and approve the router if this is your first swap).

1. Set up your client

import { createPublicClient, createWalletClient, http, erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const API_URL = "https://quiet-bloodhound-531.convex.site";
const API_KEY = process.env.O1_API_KEY!;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: base, transport: http() });
const walletClient = createWalletClient({ chain: base, account, transport: http() });

const USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const WETH = "0x4200000000000000000000000000000000000006";

2. Get a quote

Endpoint: POST {API_URL}/quote
const quote = await fetch(`${API_URL}/quote`, {
  method: "POST",
  headers: {
    "x-api-key": API_KEY,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    chainId: 8453,
    tokenIn: USDC,
    tokenOut: WETH,
    amountIn: "1000000000",   // 1,000 USDC (6 decimals)
    slippageBps: 100,         // 1% slippage tolerance
  }),
}).then((r) => r.json());
The fields you’ll display to the user:
  • routePlan.expectedAmountOut — the price you quote
  • routePlan.minAmountOut — the worst-case fill after slippage
  • routePlan.routes[].legs[].dex — which venues are involved
  • expiresAt — quote validity window (about 10 seconds; see Quote freshness)

3. Approve the router (first swap only)

Skip this step entirely if tokenIn is native ETH, or if you’re using an EIP-2612 permit (see POST /submitpermit).
const ROUTER = "0xe56e22354DDdc07cF2dfbCFb53a90fB0a56E50D5";

const allowance = await publicClient.readContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "allowance",
  args: [account.address, ROUTER],
});

if (allowance < BigInt(quote.routePlan.amountIn)) {
  await walletClient.writeContract({
    address: USDC,
    abi: erc20Abi,
    functionName: "approve",
    args: [ROUTER, BigInt(quote.routePlan.amountIn)],
  });
}

4. Build the transaction

Endpoint: POST {API_URL}/submit
const submit = await fetch(`${API_URL}/submit`, {
  method: "POST",
  headers: {
    "x-api-key": API_KEY,
    "content-type": "application/json",
  },
  body: JSON.stringify({
    quoteId: quote.quoteId,
    user: account.address,
    unwrapNativeOut: true,   // receive native ETH instead of WETH
  }),
}).then((r) => r.json());

5. Sign and send

const txHash = await walletClient.sendTransaction({
  to: submit.to as `0x${string}`,
  data: submit.data as `0x${string}`,
  value: BigInt(submit.value),
});

console.log(`Submitted: https://basescan.org/tx/${txHash}`);
That’s it. Wait for the receipt with publicClient.waitForTransactionReceipt({ hash: txHash }) and the swap is complete.

Full example, end to end

import { createPublicClient, createWalletClient, http, erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const API_URL = "https://quiet-bloodhound-531.convex.site";
const API_KEY = process.env.O1_API_KEY!;
const ROUTER = "0xe56e22354DDdc07cF2dfbCFb53a90fB0a56E50D5";

const USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
const WETH = "0x4200000000000000000000000000000000000006";

async function main() {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
  const publicClient = createPublicClient({ chain: base, transport: http() });
  const walletClient = createWalletClient({ chain: base, account, transport: http() });

  // 1. Quote
  const quote = await fetch(`${API_URL}/quote`, {
    method: "POST",
    headers: { "x-api-key": API_KEY, "content-type": "application/json" },
    body: JSON.stringify({
      chainId: 8453,
      tokenIn: USDC,
      tokenOut: WETH,
      amountIn: "1000000000",
      slippageBps: 100,
    }),
  }).then((r) => r.json());

  console.log(`Expected out: ${quote.routePlan.expectedAmountOut} WETH (wei)`);

  // 2. Approve if needed
  const allowance = await publicClient.readContract({
    address: USDC,
    abi: erc20Abi,
    functionName: "allowance",
    args: [account.address, ROUTER],
  });
  if (allowance < BigInt(quote.routePlan.amountIn)) {
    const approveHash = await walletClient.writeContract({
      address: USDC,
      abi: erc20Abi,
      functionName: "approve",
      args: [ROUTER, BigInt(quote.routePlan.amountIn)],
    });
    await publicClient.waitForTransactionReceipt({ hash: approveHash });
  }

  // 3. Submit
  const submit = await fetch(`${API_URL}/submit`, {
    method: "POST",
    headers: { "x-api-key": API_KEY, "content-type": "application/json" },
    body: JSON.stringify({
      quoteId: quote.quoteId,
      user: account.address,
      unwrapNativeOut: true,
    }),
  }).then((r) => r.json());

  // 4. Send
  const txHash = await walletClient.sendTransaction({
    to: submit.to as `0x${string}`,
    data: submit.data as `0x${string}`,
    value: BigInt(submit.value),
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
  console.log(`Confirmed in block ${receipt.blockNumber}`);
}

main().catch(console.error);

What’s next

Two-step vs one-step

When to use /quote + /submit vs /execute.

ERC-20 approvals

Approve patterns, including one-tx permit-enabled swaps.

Native ETH

Sell ETH directly without wrapping, or receive ETH instead of WETH.

Quote freshness

How to keep the displayed price live in your UI.