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.

Before the router can pull tokenIn from a user, the user must approve the router as a spender. This is standard ERC-20 behavior. The aggregator supports two patterns:
  1. Standard approve + swap — two transactions, but works for any ERC-20.
  2. EIP-2612 permit + swap — one transaction, but only works for tokens that implement the permit extension.
No approval is needed when tokenIn is native ETH (using the sentinel 0xEeee...EEeE or 0x0). In that case, the router pulls value via msg.value. See Native ETH.

Pattern 1: standard approve

This is the universal fallback. Two transactions per first swap, then no approvals on subsequent swaps if the previous allowance is large enough.
import { erc20Abi } from "viem";

const ROUTER = "0xe56e22354DDdc07cF2dfbCFb53a90fB0a56E50D5";

// 1. Read current allowance
const allowance = await publicClient.readContract({
  address: tokenIn,
  abi: erc20Abi,
  functionName: "allowance",
  args: [walletAddress, ROUTER],
});

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

// 3. Now safe to call /submit and broadcast the swap

Approve amount strategies

Pattern 2: EIP-2612 permit (one transaction)

For tokens that implement EIP-2612 (USDC and a growing number of others on Base), the user can sign an off-chain permit that the router consumes inside the same transaction as the swap. Net effect: one signature, one transaction, no approve step.

How it works

1

Build the permit typed data

Read the token’s name, EIP712_VERSION, and the user’s nonces(user). Construct the EIP-712 domain and message.
2

User signs the permit off-chain

Use signTypedData (eth_signTypedData_v4). No gas, no on-chain state change.
3

Send the permit on /submit

Pass { value, deadline, v, r, s } in the permit field on POST /submit.
4

Router consumes it atomically

O1Router.swapExactIn(...) calls IERC20Permit(token).permit(...) then pulls funds, all in one tx.

Example

import { signTypedData } from "viem/actions";

const PERMIT_TYPES = {
  Permit: [
    { name: "owner", type: "address" },
    { name: "spender", type: "address" },
    { name: "value", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
};

async function buildPermit(token: `0x${string}`, owner: `0x${string}`, value: bigint) {
  const [name, nonce, chainId] = await Promise.all([
    publicClient.readContract({ address: token, abi: erc20PermitAbi, functionName: "name" }),
    publicClient.readContract({ address: token, abi: erc20PermitAbi, functionName: "nonces", args: [owner] }),
    publicClient.getChainId(),
  ]);

  const deadline = Math.floor(Date.now() / 1000) + 600; // 10 minutes

  const signature = await walletClient.signTypedData({
    account: owner,
    domain: { name, version: "1", chainId, verifyingContract: token },
    types: PERMIT_TYPES,
    primaryType: "Permit",
    message: {
      owner,
      spender: ROUTER,
      value,
      nonce,
      deadline: BigInt(deadline),
    },
  });

  // Split into v / r / s
  const r = `0x${signature.slice(2, 66)}` as const;
  const s = `0x${signature.slice(66, 130)}` as const;
  const v = parseInt(signature.slice(130, 132), 16);

  return { value: value.toString(), deadline, v, r, s };
}
Then on /submit:
const permit = await buildPermit(tokenIn, walletAddress, BigInt(amountIn));

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: walletAddress,
    permit,
  }),
}).then((r) => r.json());

const txHash = await walletClient.sendTransaction({
  to: submit.to as `0x${string}`,
  data: submit.data as `0x${string}`,
  value: BigInt(submit.value),
});
Cache name and the EIP-712 domain per token to avoid an RPC call on every swap. Don’t cache nonce though; it changes after every permit consumption.

Detecting permit support

Not every token implements EIP-2612. The cheapest way to detect support is to read nonces(user) on the token and treat a successful read as a positive signal. Tokens without permit will revert.
async function supportsPermit(token: `0x${string}`, user: `0x${string}`): Promise<boolean> {
  try {
    await publicClient.readContract({
      address: token,
      abi: [{ name: "nonces", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] }],
      functionName: "nonces",
      args: [user],
    });
    return true;
  } catch {
    return false;
  }
}
For better UX, fall back to standard approve automatically when permit support is missing.

Security checklist

Always approve the router only

The approval target is always the canonical O1Router address: 0xe56e22354DDdc07cF2dfbCFb53a90fB0a56E50D5. Reject any UI that asks you to approve a different address while using this API.

Verify the spender on the response

submit.to should equal the router address. If it doesn’t, abort and report.

Respect deadlines on permits

Use a short deadline (5 to 15 minutes). Don’t sign permits with deadlines hours in the future — they’re a long-lived approval risk if the signature leaks.

Don't reuse permits

Each permit signature consumes a nonce. Don’t try to bundle multiple swaps under a single permit.