> ## 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.

# Quote freshness

> How to keep the displayed price live in your UI and gracefully handle expired quotes.

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

<CardGroup cols={2}>
  <Card title="TTL is ~10 seconds" icon="clock">
    Each `/quote` response includes `expiresAt` (Unix milliseconds). The default TTL is 10 seconds. After that, the cache entry is gone.
  </Card>

  <Card title="/submit returns 404 on expired quotes" icon="circle-xmark">
    Once `expiresAt` has passed, `POST /submit` with that `quoteId` returns `{ "error": "quote not found or expired" }`. Re-quote and retry.
  </Card>
</CardGroup>

## Recommended UI pattern

<Steps>
  <Step title="Fetch a fresh quote when the swap modal opens">
    On modal open, call `/quote` and store the result. Display the price.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Submit using the latest quoteId">
    Always submit using the `quoteId` from the most recent successful `/quote`. Don't reuse an older one.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Reference implementation

```ts theme={null} theme={null}
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:

```ts theme={null} theme={null}
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 type                              | Recommended `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-cap                   | `100` to `300` (1% to 3%)   |
| Long tail / memecoins                  | `300` 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.

<Tip>
  Cache the user's preferred slippage per pair-type. Forcing the user to set slippage manually on every swap is a common UX pitfall.
</Tip>
