> ## Documentation Index
> Fetch the complete documentation index at: https://tetrafi.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# How search works

> One quote request asks every venue your workspace can use. You get comparable quotes, and every venue's outcome.

You search by sending one quote request: the asset you sell, the asset you buy, and one exact amount, either the amount you sell or the amount you buy. TetraFi asks every venue your workspace can use at the same time and answers with two lists:

* `quotes`: the quotes that came back, best first, each one held for you under its `id`;
* `sources`: every venue that was considered, with its outcome, so a missing quote always has a reason.

## The request

`POST /api/v1/quotes`

| Field | Required | Value |
| - | - | - |
| `sell.asset`, `buy.asset` | Yes | An Asset id from [`GET /api/v1/assets`](#use-values-from-discovery), such as `USDC`. Assets carry no chain. |
| `sell.chain`, `buy.chain` | No | A CAIP-2 chain id, such as `eip155:8453`, to pin that side to one chain. See [Chains and routes](https://tetrafi.mintlify.site/search/chains-and-routes.md). |
| `sellAmount` or `buyAmount` | Exactly one | The exact amount you sell (`sellAmount`) or buy (`buyAmount`), a positive decimal string in Asset units, such as `"10"` or `"0.001"`. Never base units. Both or neither is refused with `400 INVALID_REQUEST`. See [Sell exact or buy exact](https://tetrafi.mintlify.site/search/sell-or-buy-exact.md). |
| `filters` | No | Narrows the search. Omit a filter to keep everything. |
| `filters.locations` | No | Any of `singleChain`, `crossChain`, `offChain`. `mixed` is not a filter value. |
| `filters.modalities` | No | Any of `amm`, `solver`, `bridge`, `rfq`, `orderBook`, `fiat`. |
| `filters.venues` | No | `{"include": [...], "exclude": [...]}` with venue ids from `sources[].venue.id`. See [Venue types and venues](https://tetrafi.mintlify.site/search/venues.md#specific-venues). |
| `filters.variants` | No | `{"sell": "any" or "nativeOnly", "buy": "any" or "nativeOnly"}`. |
| `filters.firmOnly` | No | `true` keeps only firm quotes (`firm: true`), leaving out executable estimates and indicative quotes; `false`, the default, keeps them all. |
| `sort` | No | `netReceived` (default) or `eta`. |
| `priceProtectionBps` | No | 0 to 1000, 30 when omitted. How far a non-firm executable quote's delivery may fall below its quoted amount, Basic or Advanced. See [Sort and price protection](https://tetrafi.mintlify.site/search/sort-and-protection.md#price-protection). |

```bash cURL theme={null}
# Sell exactly `amount` of the sell asset. Each quote states what you would buy.
request=$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" '{
  sell: {asset: $sell},
  buy: {asset: $buy},
  sellAmount: $amount
}')
sell_exact=$(quote_request "$request")
jq -c '.quotes[] | {venue: .legs[0].venue.id, location: .location.kind,
  firm, sell: .sell.amount, buy: .buy.amount, expiresAt}' <<<"$sell_exact" >&2
```

```typescript TypeScript SDK theme={null}
// Sell exactly 10 USDC. Each quote says how much ETH you would receive for it.
const sellExact = await client.quote({
  sell: { asset: usdc.assetId },
  buy: { asset: eth.assetId },
  sellAmount: '10',
  filters: { locations: ['offChain'] },
});
// An order book trades in whole lot sizes, so it may spend a little less than you asked. It never spends
// more, and `unspent` says how much stays in your account.
const overspent = sellExact.quotes.filter((quote) => quote.sell.amount !== null && Number(quote.sell.amount) > 10);
```

## The response

```json theme={null}
{
  "quoteRequestId": "rq_...",
  "venueAsked": true,
  "referenceMid": { "sellUsd": "0.99965", "buyUsd": "2323.71", "buyPerSell": "0.00043019" },
  "quotes": [
    {
      "id": "q_...",
      "location": { "kind": "offChain" },
      "firm": false,
      "lastLook": false,
      "sell": { "asset": "USDC", "chain": null, "variant": { "kind": "native", "name": "USDC" }, "amount": "99.99994" },
      "buy": { "asset": "ETH", "chain": null, "variant": { "kind": "native", "name": "ETH" }, "amount": "0.043" },
      "minOut": "0.042",
      "expiresAt": "2026-10-27T00:33:18.432Z",
      "next": { "kind": "order" }
    }
  ],
  "sources": [
    { "status": "answered", "venue": { "id": "...", "modality": "orderBook" }, "asked": true, "detail": null },
    { "status": "unsupported", "venue": { "id": "...", "modality": "bridge" }, "asked": false, "reason": "pairNotServed", "detail": null }
  ]
}
```

Quotes are trimmed here: each one also carries its `legs`, `fees`, `netReceived`, `eta` and more.

Each quote tells you:

* **where it executes**, in `location.kind`: `singleChain`, `crossChain` or `offChain`, or `mixed` for a route whose legs differ. `mixed` describes a quote only: you cannot filter on it, and `filters.locations: ["mixed"]` is refused with `400 INVALID_REQUEST`;
* **whether the venue is bound to its price**, in `firm` with `expiresAt` ([below](#executable-and-indicative-quotes)), and `lastLook`, always `false`: an accepted price is never re-checked before it fills;
* **what you do next**, in `next.kind`: `order`, `fund` or `requote` (`preflight`, `sign` and `transact` appear only on Advanced quotes, which are not open yet).

`venueAsked` is `false` when every quote came from cached prices and no venue was contacted.

## Executable and indicative quotes

\| `firm` and `expiresAt` | What it means | `minOut` | `staleAfter` |
\| --- | --- | --- | --- | --- |
\| `firm: true`, `expiresAt` set | Executable: the venue delivers exactly this amount until expiry. | `null` | `null` |
\| `firm: false`, `expiresAt` set | Executable: delivery may move, but never below `minOut`. | set | `null` |
\| `firm: false`, `expiresAt` `null` | Indicative: the venue's price, not a commitment. | `null` | set |

`expiresAt` is when an executable quote stops being orderable. Order before then, or requote it: see [Requotes and expiry](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md). An indicative quote has no expiry; `staleAfter` says when its price is old.

An indicative quote has no `minOut`, because it commits to nothing. You can still order one when its `next` is `order`: you send `minReceive`, the least you accept, the venue is asked for its price when you order, and the trade happens only if it delivers at least that ([Orders](https://tetrafi.mintlify.site/trade/orders.md#venue-quotes-that-execute-within-your-limit)). When its `next` is `requote`, ask the venue again first.

## Sources: why a venue did or did not quote

Every venue considered appears once in `sources`, whether or not it was asked (`asked`). `detail` is a sentence you can show to a person; `reason`, where present, is a token your code can match.

| `status` | What happened |
| - | - |
| `answered` | The venue quoted. Its quotes are in `quotes`. |
| `filteredOut` | Your filters excluded it. It was not asked. |
| `unsupported` | It does not serve this request, for example this pair (`pairNotServed`) or a buy-exact amount (`buyAmountUnsupported`). |
| `unavailable` | It could not quote now. `reason` and `detail` say why, for example `notConnected` (no account at that venue), `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable` (no organisation wallet the key may use is granted to it) or `advancedModeDisabled` (Advanced execution is not open yet). |
| `restricted` | A permission keeps it from you. `reason` names it, for example `venueTradeScopeRequired`. |
| `needsWallet` | It quotes only for a wallet you have not bound. |
| `walletRefused` | The wallet it would use cannot trade there, for example because it was revoked. `wallet` names it ([When a quote needs a wallet](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#when-a-quote-needs-a-wallet)). |
| `timedOut` | It did not answer before the collection deadline. |
| `refused` | The venue declined. |

More statuses exist and new ones can appear. Treat any status you do not recognise as "other" and show its `detail`.

> [!NOTE]
>
> What an API key sees depends on what it may do. Quotes from your connected venue accounts need a key with venue trading enabled that signs its requests; any other key gets those venues as `restricted` with reason `venueTradeScopeRequired`. On-chain venues that TetraFi trades for you from an organisation wallet need that wallet granted to the key; otherwise they read `unavailable` with the reason `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable`. See [Authentication](https://tetrafi.mintlify.site/authentication.md#trading-on-your-venue-accounts).

## JSON or a stream

By default you get one JSON reply when collection ends (3 seconds at most). Send `Accept: text/event-stream` to get server-sent events as venues answer:

| Event | Data |
| - | - |
| `quote` | One quote, as it arrives. |
| `source` | One venue's outcome. |
| `complete` | The whole quote set, the same object as the JSON reply. |
| `error` | The error body. The stream ends. |

Event ids increase, and the stream sends keepalive comments between events.

```bash cURL theme={null}
# The same request as a stream: each quote and each venue's outcome arrives as its own event,
# then `complete` carries the whole set. One live quote request every 3 seconds, so wait first.
sleep 3
request=$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" '{
  sell: {asset: $sell},
  buy: {asset: $buy},
  sellAmount: $amount
}')
event="" complete="" quote_events=0 source_events=0 other=""
while IFS= read -r line; do
  line=${line%$'\r'}
  case $line in
    "event: "*) event=${line#event: } ;;
    "data: "*)
      data=${line#data: }
      case $event in
        quote) quote_events=$((quote_events + 1)) ;;   # one PairQuote, best shown as it lands
        source) source_events=$((source_events + 1)) ;; # one venue's outcome
        complete) complete=$data ;;                     # the full quote set, as the JSON reply
        error) printf '%s\n' "$data" >&2; exit 1 ;;
      esac ;;
    ""|":"*|"id: "*) ;;                                 # event separators, keepalives, event ids
    *) other+=$line ;;                                  # not an event: an error body
  esac
done < <(stream_request "$request")
if [[ -z $complete ]]; then
  if [[ -z $other ]]; then other='{"error":"STREAM_ENDED_WITHOUT_COMPLETE"}'; fi
  printf '%s\n' "$other" >&2
  exit 1
fi
echo "stream: $quote_events quote events, $source_events source events, then complete" >&2
```

## Rate limit

You may send one live quote request every 3 seconds. A faster one is refused with `429 QUOTE_RATE_LIMITED`, a `Retry-After` header in seconds, and the same wait in `details.retryAfter` (for example `PT1S`). Wait that long and send it again. A request answered entirely from cached prices is not counted.

For prices without that limit, read [`GET /api/v1/prices`](https://tetrafi.mintlify.site/search/prices) first.

## Use values from discovery

Never hardcode Asset ids, chains or venue lists. `GET /api/v1/assets` returns what your workspace can trade:

```json theme={null}
{
  "eligibility": { "complete": true },
  "assets": [
    {
      "assetId": "USDC",
      "symbol": "USDC",
      "kind": "stablecoin",
      "variants": [
        {
          "name": "USDC",
          "kind": "native",
          "deployments": [{ "chain": "eip155:8453", "decimals": 6 }],
          "offChainVenues": ["..."]
        }
      ]
    }
  ]
}
```

Use `assetId` in `sell.asset` and `buy.asset`, and a deployment's `chain` when you pin a side. `offChainVenues` lists the venue accounts that hold that variant. When `eligibility.complete` is `false`, `reason` says which part of the list could not be read. An API key needs the `prices_read` scope to read it, which the **Read quotes** permission grants ([Permissions](https://tetrafi.mintlify.site/authentication.md#permissions)).

## Refusals

A refused request answers one error body, `{"error", "message", "requestId", "timestamp", "next"?, "details"?}`, the same at every status. `error` is the code to match on; `requestId` equals the `X-Request-Id` header; `details.value`, where present, is the value that was refused. [Errors and refusals](https://tetrafi.mintlify.site/trade/errors.md) lists every code.

| Code | Why | What to do |
| - | - | - |
| `INVALID_REQUEST` | A field is unknown, missing or of the wrong type; `message` names it. | Fix that field. |
| `UNKNOWN_ASSET`, `UNKNOWN_CHAIN`, `UNKNOWN_VENUE` | The value is not in the catalogue. | Use values from `GET /api/v1/assets` and from `sources[].venue.id`. |
| `INVALID_AMOUNT` | `sellAmount` or `buyAmount` is not a positive decimal string. | Send it as a string, such as `"10"`. |
| `QUOTE_RATE_LIMITED` | More than one live quote request in 3 seconds. | Wait `Retry-After` seconds, then retry. |

Chain refusals are on [Chains and routes](https://tetrafi.mintlify.site/search/chains-and-routes.md).

## In the app

The Trade page sends this same request when you choose a pair and type an amount, and lists the quotes it returns.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.