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

# Chains and routes

> Leave the chain open, pin a side to a chain, or keep only single-chain, cross-chain or off-chain quotes.

An Asset such as `USDC` carries no chain. Left open, a search asks every venue for every place the Asset can be: on any chain it is deployed on, and on your connected venue accounts. You narrow that in two ways:

* **pin a side** to one chain with `sell.chain` or `buy.chain`;
* **choose where quotes execute** with `filters.locations`.

| Field | Value | Effect |
| - | - | - |
| `sell.chain` | A CAIP-2 chain id, such as `eip155:8453` | You sell on that chain. The sell side is on-chain. |
| `buy.chain` | A CAIP-2 chain id | You buy on that chain. The buy side is on-chain. |
| `filters.locations` | Any of `singleChain`, `crossChain`, `offChain` | Keeps only quotes that execute there. Omitted, all three are kept. |
| `filters.variants` | `{"sell": "nativeOnly"}`, `{"buy": "nativeOnly"}` | Keeps only `native` variants on that side, so no `bridged`, `wrapped`, `omnichain` or `test` representation such as `USDC.e`. Default `any`. |

Take chain ids from the deployments that [`GET /api/v1/assets`](https://tetrafi.mintlify.site/search/how-it-works#use-values-from-discovery) lists for each Asset, never from a fixed list.

## Where a quote executes

| `location.kind` | Meaning | Its sides |
| - | - | - |
| `singleChain` | Swapped on one chain. | `sell.chain` and `buy.chain` are the same chain. |
| `crossChain` | Moved from one chain to another, by a bridge or a solver. | `sell.chain` and `buy.chain` differ. |
| `offChain` | Traded on one of your venue accounts, such as an exchange or OTC desk. | `chain` is `null` on a side held on the venue account. |
| `mixed` | A route of several legs that execute in different places. | Each entry in `legs` has its own location. |

`mixed` describes a quote, not a place you can ask for: it is not a `filters.locations` value, and sending it is refused with `400 INVALID_REQUEST`.

## Single-chain

Pin both sides to the same chain and keep `singleChain`:

```bash cURL theme={null}
# Pin both sides to one chain and keep only quotes that execute on it.
# The chain comes from discovery: one both assets are deployed on, preferring one a venue just quoted.
chain=$(jq -r '[.quotes[] | select(.location.kind == "singleChain") | .sell.chain][0] // empty' <<<"$sell_exact")
if [[ -z $chain ]]; then chain=$(jq -r '.[0] // empty' <<<"$shared_chains"); fi
if [[ -z $chain ]]; then
  jq -nc --arg p "$sell" --arg r "$buy" '{error: "NO_SHARED_CHAIN", sell: $p, buy: $r}' >&2
  exit 1
fi
request=$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" --arg chain "$chain" '{
  sell: {asset: $sell, chain: $chain},
  buy: {asset: $buy, chain: $chain},
  sellAmount: $amount,
  filters: {locations: ["singleChain"]}
}')
single_chain=$(quote_request "$request")
jq -c '{chain: $c, quotes: [.quotes[] | {venue: .legs[0].venue.id, buy: .buy.amount}]}' \
  --arg c "$chain" <<<"$single_chain" >&2
```

```typescript TypeScript SDK theme={null}
// Single chain: pay USDC and receive ETH, both on the same chain.
const single = await client.quote({
  sell: { asset: 'USDC', chain: from },
  buy: { asset: 'ETH', chain: from },
  sellAmount: '10',
  filters: { locations: ['singleChain'] },
});
```

## Cross-chain

Pin the two sides to different chains. To move one Asset between chains, the same Asset goes on both sides, and at least one side must be pinned:

```bash cURL theme={null}
# Move the sell asset from one chain to another. The same asset on both sides needs a chain pin,
# or the request is refused with SAME_ASSET_NEEDS_CHAIN.
from_chain=${TETRAFI_FROM_CHAIN:-$(jq -r '.[0] // empty' <<<"$sell_chains")}
to_chain=${TETRAFI_TO_CHAIN:-$(jq -r '.[1] // empty' <<<"$sell_chains")}
if [[ -z $to_chain ]]; then
  jq -nc --arg a "$sell" '{error: "ONE_CHAIN_ONLY", asset: $a}' >&2
  exit 1
fi
request=$(jq -nc --arg asset "$sell" --arg amount "$cross_amount" --arg from "$from_chain" --arg to "$to_chain" '{
  sell: {asset: $asset, chain: $from},
  buy: {asset: $asset, chain: $to},
  sellAmount: $amount,
  filters: {locations: ["crossChain"]}
}')
cross_chain=$(quote_request "$request")
jq -c '{from: $f, to: $t, quotes: [.quotes[] | {venue: .legs[0].venue.id, buy: .buy.amount, eta}]}' \
  --arg f "$from_chain" --arg t "$to_chain" <<<"$cross_chain" >&2
```

```typescript TypeScript SDK theme={null}
// Cross chain: pay USDC on one chain and receive USDC on another. When both sides are the same asset, pin a
// chain on each side; the API refuses the ask otherwise (SAME_ASSET_NEEDS_CHAIN).
const cross = await client.quote({
  sell: { asset: 'USDC', chain: from },
  buy: { asset: 'USDC', chain: to },
  sellAmount: '10',
  filters: { locations: ['crossChain'] },
});
```

A cross-chain quote's `eta` tells you how long delivery is expected to take, as an ISO 8601 duration such as `PT10S`.

## Off-chain

Keep `offChain` and pin no chain. Only your connected venue accounts can answer:

```bash cURL theme={null}
# Keep only quotes that execute on a venue account (an exchange or desk). No chain pins:
# a pinned chain with only offChain allowed is refused with CHAIN_PIN_NEEDS_ON_CHAIN.
request=$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" '{
  sell: {asset: $sell},
  buy: {asset: $buy},
  sellAmount: $amount,
  filters: {locations: ["offChain"]}
}')
off_chain=$(quote_request "$request")
jq -c '[.quotes[] | {venue: .legs[0].venue.id, firm, buy: .buy.amount}]' <<<"$off_chain" >&2
jq -c '[.sources[] | select(.status == "filteredOut") | .venue.id]' <<<"$off_chain" >&2
```

```typescript TypeScript SDK theme={null}
// Off chain: only your venue accounts are asked.
const offChain = await client.quote({
  sell: { asset: usdc.assetId },
  buy: { asset: eth.assetId },
  sellAmount: '10',
  filters: { locations: ['offChain'] },
});
```

## What happens to the venues you exclude

A venue whose quotes cannot match your pins or locations is not asked. It stays in `sources` with `status: "filteredOut"`:

```json theme={null}
{ "status": "filteredOut", "venue": { "id": "...", "modality": "bridge" }, "asked": false, "detail": null }
```

So a single-chain search lists your venue accounts and bridges as `filteredOut`, and an off-chain search lists on-chain protocols, bridges and solvers the same way. When nothing is left to answer, `quotes` is empty and `sources` shows why.

## Refusals

| Code | Why | What to do |
| - | - | - |
| `SAME_ASSET_NEEDS_CHAIN` | `sell.asset` and `buy.asset` are the same and no chain is pinned. | Pin a chain on at least one side. |
| `CHAIN_PIN_NEEDS_ON_CHAIN` | A side is pinned to a chain, but `filters.locations` allows only `offChain`. | Remove the pin, or allow `singleChain` or `crossChain`. |
| `ASSET_NOT_ON_CHAIN` | The pinned chain has no deployment of that side's Asset. `details.value` is the chain. | Pin a chain the Asset is deployed on, or leave the side open. |
| `UNKNOWN_CHAIN` | The chain id is not one TetraFi knows. `details.value` is the chain. | Use a chain from `GET /api/v1/assets`. |
| `UNKNOWN_ASSET` | The Asset id is not in the catalogue. | Use an `assetId` from `GET /api/v1/assets`. |

## In the app

On the Trade page, open **Advanced filters** and choose a **Route**: **Any**, **Single-chain** (pick one chain), **Cross-chain** (pick a from chain and a to chain) or **Off-chain**.


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