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.

Quotes are price snapshots at a specific block. Pool reserves, tick state, and competing trades can move the market between the moment you quote and the moment your transaction lands. The aggregator handles this with a short server-side cache TTL and explicit expiry semantics.

The contract

TTL is ~10 seconds

Each /quote response includes expiresAt (Unix milliseconds). The default TTL is 10 seconds. After that, the cache entry is gone.

/submit returns 404 on expired quotes

Once expiresAt has passed, POST /submit with that quoteId returns { "error": "quote not found or expired" }. Re-quote and retry.
1

Fetch a fresh quote when the swap modal opens

On modal open, call /quote and store the result. Display the price.
2

Re-quote on a 5 to 8 second interval

While the modal is open, re-fetch the quote periodically. Compare expectedAmountOut against the previous value to decide whether to flash an “updated” indicator.
3

Stop polling on user action

When the user clicks “Swap” (or your equivalent), pause polling so the price doesn’t change between their decision and the wallet prompt.
4

Submit using the latest quoteId

Always submit using the quoteId from the most recent successful /quote. Don’t reuse an older one.
5

Handle 404 gracefully

If /submit returns 404, automatically re-fetch /quote and retry once. Only surface a user-facing error if the second attempt also fails.

Reference implementation

import { useEffect, useRef, useState } from "react";

interface QuoteState {
  quoteId: string;
  expectedOut: bigint;
  minOut: bigint;
  expiresAt: number;
}

function useLiveQuote(params: QuoteParams, isOpen: boolean) {
  const [quote, setQuote] = useState<QuoteState | null>(null);
  const isFreezingRef = useRef(false);

  useEffect(() => {
    if (!isOpen) return;

    let cancelled = false;
    const tick = async () => {
      if (cancelled || isFreezingRef.current) return;
      try {
        const q = await fetch(`${API_URL}/quote`, {
          method: "POST",
          headers: { "x-api-key": API_KEY, "content-type": "application/json" },
          body: JSON.stringify(params),
        }).then((r) => r.json());
        if (!cancelled && q.quoteId) {
          setQuote({
            quoteId: q.quoteId,
            expectedOut: BigInt(q.routePlan.expectedAmountOut),
            minOut: BigInt(q.routePlan.minAmountOut),
            expiresAt: q.expiresAt,
          });
        }
      } catch (e) {
        // ignore transient failures; next tick will retry
      }
    };

    tick();
    const id = setInterval(tick, 6000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, [isOpen, JSON.stringify(params)]);

  const freeze = () => { isFreezingRef.current = true; };
  const unfreeze = () => { isFreezingRef.current = false; };

  return { quote, freeze, unfreeze };
}
Then in the swap submit handler:
async function onSwap() {
  if (!quote) return;
  freeze(); // stop background re-quoting

  try {
    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 }),
    }).then(async (r) => {
      if (r.status === 404) {
        // Quote expired between the freeze and the submit. Re-quote and retry.
        const fresh = await refreshQuote(params);
        return fetch(`${API_URL}/submit`, {
          method: "POST",
          headers: { "x-api-key": API_KEY, "content-type": "application/json" },
          body: JSON.stringify({ quoteId: fresh.quoteId, user: walletAddress }),
        }).then((r2) => r2.json());
      }
      return r.json();
    });

    const txHash = await walletClient.sendTransaction({
      to: submit.to,
      data: submit.data,
      value: BigInt(submit.value),
    });
    onSuccess(txHash);
  } finally {
    unfreeze();
  }
}

Why not use /execute for this?

/execute always returns a fresh quote, so it sidesteps the 404 problem. But:
  • You lose the ability to display the quoted price before the user signs.
  • Every /execute call burns rate-limit budget and re-runs the routing engine, so it’s more expensive than alternating /quote and /submit.
Use /execute for one-click flows where preview doesn’t matter. Use the two-step pattern with the freshness loop above for any UI that shows a price.

Slippage as a freshness backstop

Even with aggressive re-quoting, your submitted transaction may land 1-2 blocks after /quote ran. Use slippageBps as the safety margin:
Pair typeRecommended slippageBps
Stablecoin to stablecoin (USDC → USDT)10 to 30 (0.1% to 0.3%)
Blue-chip to blue-chip (WETH → cbBTC)30 to 100 (0.3% to 1%)
Blue-chip to mid-cap100 to 300 (1% to 3%)
Long tail / memecoins300 to 1000 (3% to 10%)
The user’s minAmountOut is set on the router, so the swap reverts cleanly if real output falls below the slippage threshold — your transaction never lands in a worse-than-expected state.
Cache the user’s preferred slippage per pair-type. Forcing the user to set slippage manually on every swap is a common UX pitfall.