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

# Quickstart

> List assets, get quotes from every venue your workspace can use, and place an order from your terminal in about five minutes.

This guide walks through one script, [`samples/http/quickstart.sh`](https://github.com/tetrafi/docs/blob/main/samples/http/quickstart.sh), step by step. It lists the assets your workspace can trade, asks every venue for a quote on 10 USDC to USDT, takes the best quote and places the order.

For a first quote without placing an order, copy the **cURL** blocks in steps 1–3 into the same Bash session and stop there. Alternatively, run the linked script: it stops after the quote unless you explicitly opt in to ordering with `TETRAFI_PLACE_ORDER=1`. The order request in step 4 sends a real order when pasted.

Each step shows the script's code under **cURL**. The **TypeScript SDK** tab shows the same step in TetraFi's TypeScript SDK, which is not published to npm yet: it is coming soon. Its example differs from the script in two ways: it quotes USDC for ETH, and after ordering it waits until the order ends.

> [!WARNING]
>
> The script stops after the quote by default. Only set `TETRAFI_PLACE_ORDER=1` when you intend to place a real order: it trades 10 USDC from your venue account by default, and that cannot be undone. There is no sandbox yet. Set `TETRAFI_AMOUNT` to trade less, down to the venue's minimum. [Test safely](https://tetrafi.mintlify.site/test-safely.md) shows how to find each venue's minimum before you order.

> [!NOTE]
>
> The API may change until launch. Every change is listed in the [Changelog](https://tetrafi.mintlify.site/changelog.md).

## Before you begin

You need `bash`, `curl`, `jq` and `openssl`, and an exchange or OTC venue account connected to your workspace that holds the USDC you sell.

### Get a key with venue trading

This guide places the order with the API key alone, so it orders a Basic quote: one TetraFi executes for you on your venue account. That needs an API key with:

* **Read quotes**, **Submit orders** and **Read orders**;
* venue trading, switched on for the key by a workspace owner or an admin who manages credentials;
* request signing, and the key's signing secret.

Create the key in the app as a workspace owner or admin with permission to manage credentials, signed in with a second factor:

1. Open **Settings**, find **Personal API keys** under **Your account**, then select **Open API keys** and **Create key**.
2. In **Create API key**, enter a **Name**. Under **Permissions**, tick **Read quotes**, **Submit orders** and **Read orders**.
3. Switch **Venue trading** on and select **Create key**.
4. Copy **API Key** and **Request-signing secret** to your secret manager, keeping them separate. They are shown once and cannot be recovered. Tick **I have saved both credentials securely** and select **Done**.

With venue trading on, the dialog grants your workspace's default organisation wallets that you hold a Trade right on, and says which it granted. Exchange and OTC venue accounts need no wallet grant. If no usable organisation wallet is granted, on-chain Basic venues answer `unavailable` with `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable`. [Give a key organisation wallets](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#give-a-key-organisation-wallets) explains how to review and change the key's grants.

The key is bound to its workspace, so the script sends no workspace id: every request works in the key's own workspace. If you need the id later, `GET /api/v1/me` returns it in `workspaceId`, and in the app a workspace owner or admin finds it as **Workspace ID** in the workspace's settings, on the **General** tab.

> [!NOTE]
>
> On-chain (Advanced) execution is not open yet: Advanced venues are listed in `sources` as `unavailable` with the reason `advancedModeDisabled`, and are not asked. This guide and every sample on this site use Basic quotes.

**1. Set your environment**

Export your base URL, key and the key's signing secret.

```bash theme={null}
export TETRAFI_BASE_URL=https://api.tetrafi.io
export TETRAFI_API_KEY=...            # from your secret manager
export TETRAFI_SIGNING_SECRET=...     # the key's signing secret
```

The script starts with one helper, `tetrafi`, that sends a request with your key, signs it when `TETRAFI_SIGNING_SECRET` is set, and stops the script with the error body on any error status.

```bash cURL theme={null}
: "${TETRAFI_BASE_URL:?Set TETRAFI_BASE_URL}"
: "${TETRAFI_API_KEY:?Set TETRAFI_API_KEY}"

# tetrafi METHOD PATH [BODY] [HEADER]
# Sends one request and prints the response body. On an error status it also prints
# the body to stderr and returns 1. With TETRAFI_SIGNING_SECRET set, it signs the request.
tetrafi() {
  local method=$1 path=$2 body=${3:-} extra=${4:-}
  local headers=(
    -H "X-API-Key: $TETRAFI_API_KEY"
    -H "Accept: application/json"
  )
  if [[ -n ${TETRAFI_WORKSPACE_ID:-} ]]; then headers+=(-H "X-Workspace-Id: $TETRAFI_WORKSPACE_ID"); fi
  if [[ -n $body ]]; then headers+=(-H "Content-Type: application/json"); fi
  if [[ -n $extra ]]; then headers+=(-H "$extra"); fi
  if [[ -n ${TETRAFI_SIGNING_SECRET:-} ]]; then
    local timestamp digest signature
    timestamp=$(date +%s)
    digest=$(printf '%s' "$body" | openssl dgst -sha256 -r | cut -d' ' -f1)
    signature=$(printf '%s\n%s\n%s\n%s' "$timestamp" "$method" "$path" "$digest" \
      | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$TETRAFI_SIGNING_SECRET" -r | cut -d' ' -f1)
    headers+=(-H "X-Timestamp: $timestamp" -H "X-Signature: $signature")
  fi
  local response status
  response=$(curl -sS -X "$method" "$TETRAFI_BASE_URL$path" "${headers[@]}" \
    ${body:+--data "$body"} -w $'\n%{http_code}')
  status=${response##*$'\n'}
  response=${response%$'\n'*}
  printf '%s\n' "$response"
  if (( status >= 400 )); then
    printf '%s\n' "$response" >&2
    return 1
  fi
}

# Pair and amount used by the following steps. Keep these in the client snippet
# so copying the guide's cURL blocks works just like running this script.
sell=${TETRAFI_SELL:-USDC}
buy=${TETRAFI_BUY:-USDT}
amount=${TETRAFI_AMOUNT:-10}
```

```typescript TypeScript SDK theme={null}
const client = createTetrafiClient({
  baseUrl: required('TETRAFI_BASE_URL'),
  apiKey: required('TETRAFI_API_KEY'),
  workspaceId: required('TETRAFI_WORKSPACE_ID'),
  signingSecret: process.env.TETRAFI_SIGNING_SECRET,
});
```

**2. List the assets you can trade**

`GET /api/v1/assets` returns every asset your workspace can trade, with its variants and the chains it is deployed on. The script checks that both sides of the pair are listed.

```bash cURL theme={null}
assets=$(tetrafi GET /api/v1/assets)
for asset in "$sell" "$buy"; do
  if ! jq -e --arg a "$asset" 'any(.assets[]; .assetId == $a)' <<<"$assets" >/dev/null; then
    echo "{\"error\":\"ASSET_NOT_LISTED\",\"asset\":\"$asset\"}" >&2
    exit 1
  fi
done
echo "assets: $(jq '.assets | length' <<<"$assets") listed, quoting $sell for $buy" >&2
```

```typescript TypeScript SDK theme={null}
// Assets come from the API, never from a hardcoded list: what your workspace can reach can change.
const { assets } = await client.assets();
const usdc = assets.find((asset) => asset.symbol === 'USDC');
const eth = assets.find((asset) => asset.symbol === 'ETH');
if (!usdc || !eth) throw new Error('This workspace cannot reach USDC and ETH yet.');
```

```json Response (shortened) theme={null}
{
  "assets": [
    {
      "assetId": "USDC",
      "symbol": "USDC",
      "name": "USD Coin",
      "kind": "stablecoin",
      "variants": [
        { "kind": "native", "name": "USDC", "deployments": [{ "chain": "eip155:8453", "decimals": 6 }] }
      ]
    }
  ]
}
```

In the app, the trade page's asset picker lists the same assets.

**3. Request quotes and pick the best**

`POST /api/v1/quotes` asks every venue at once. The body names the asset you sell (`sell`), the asset you buy (`buy`) and `sellAmount`, the exact amount you sell. To fix the amount you buy instead, send `buyAmount`. Leave out `chain` on a side to let venues quote it on any chain or off-chain.

Quotes come back best first, by what you buy net of fees. The script takes the first one whose `next` is `order`: a quote you can order now. If none is, it prints every venue's outcome from `sources` and stops.

```bash cURL theme={null}
request=$(jq -n --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" \
  --arg venues "${TETRAFI_VENUES:-}" '{
    sell: {asset: $sell},
    buy: {asset: $buy},
    sellAmount: $amount
  } + (if $venues == "" then {} else {filters: {venues: {include: ($venues | split(","))}}} end)')
quotes=$(tetrafi POST /api/v1/quotes "$request")

# Quotes arrive best first (most received, net of fees). Take the best one you can order now.
quote=$(jq -c 'first(.quotes[] | select(.next.kind == "order")) // empty' <<<"$quotes")
if [[ -z $quote ]]; then
  # Every venue asked is listed in sources with its outcome, so an empty result says why.
  jq -c '{error: "NO_ACTIONABLE_QUOTE", sources: [.sources[] | {venue: .venue.id, status, reason}]}' <<<"$quotes" >&2
  exit 1
fi
quote_id=$(jq -r '.id' <<<"$quote")
jq -c '{quoteId: .id, venue: .legs[0].venue.id, mode: .legs[0].venue.integrationMode,
  sell: .sell.amount, buy: .buy.amount, next: .next.kind, expiresAt}' <<<"$quote" >&2
```

```typescript TypeScript SDK theme={null}
// Ask every venue at once: sell exactly 10 USDC, buy ETH. Each quote is held by its id.
const set = await client.quote({
  sell: { asset: usdc.assetId },
  buy: { asset: eth.assetId },
  sellAmount: '10',
});
// Every venue that was considered says what happened, including the ones that gave no quote and why.
const notQuoted = set.sources.filter((source) => source.status !== 'answered')
  .map((source) => `${source.venue.id}: ${source.status}`);
// Quotes arrive best first. Take the first one you can act on right now.
const best: Quote | undefined = set.quotes.find((quote) => quote.next.kind === 'order');
if (!best) throw new Error(`No quote can be ordered yet. Not quoted: ${notQuoted.join('; ') || 'none'}`);
```

```json Response (shortened) theme={null}
{
  "quoteRequestId": "rq_01M5…",
  "venueAsked": true,
  "quotes": [
    {
      "id": "q_01M5…",
      "legs": [{ "venue": { "id": "binance-orderbook", "integrationMode": "basic" } }],
      "firm": false,
      "sell": { "asset": "USDC", "chain": null, "amount": "10" },
      "buy": { "asset": "USDT", "chain": null, "amount": "10.0134" },
      "minOut": "9.9833",
      "next": { "kind": "order" },
      "expiresAt": "2026-10-27T00:43:10.213Z"
    }
  ],
  "sources": [
    { "venue": { "id": "binance-orderbook" }, "status": "answered" },
    { "venue": { "id": "across" }, "status": "unsupported", "reason": "pairNotServed" }
  ]
}
```

You can send one live quote request every 3 seconds per key.

To change what the script asks for, set any of these before you run it:

| Variable | Default | What it changes |
| - | - | - |
| `TETRAFI_SELL` | `USDC` | The asset you sell. |
| `TETRAFI_BUY` | `USDT` | The asset you buy. |
| `TETRAFI_AMOUNT` | `10` | The amount you sell, sent as `sellAmount`. |
| `TETRAFI_VENUES` | every venue | A comma-separated list of venue ids to ask, sent as `filters.venues.include`. Use `sources[].venue.id` values ([Venue types and venues](https://tetrafi.mintlify.site/search/venues.md#specific-venues)). |
| `TETRAFI_SLIPPAGE_BPS` | `50` | How far below the quoted amount you accept when the quote needs `minReceive`, in basis points. |
| `TETRAFI_PLACE_ORDER` | not set | Set it to `1` to place the order. |

In the app, this is the quote list on the trade page.

**4. Place the order**

`POST /api/v1/orders` with the quote id places the order on the held terms. Send an `Idempotency-Key` header with a value you generate and keep: a retry with the same key and body returns the first answer instead of placing a second order. Each quote can be ordered once.

When the quote's `minReceiveRequired` is `true`, send `minReceive`, the least you accept: the script uses the quoted `buy.amount` less `TETRAFI_SLIPPAGE_BPS` (default 50, or 0.5%), rounded down to the bought asset's decimals from `GET /api/v1/assets`.

The script places the order only when you set `TETRAFI_PLACE_ORDER=1`. Without it, it stops after the quote and prints the quote id it would order.

```bash cURL theme={null}
order_body=$(jq -c '{quoteId: .id}' <<<"$quote")
if [[ $(jq -r '.minReceiveRequired' <<<"$quote") == true ]]; then
  # The quote says when it needs minReceive: the least you accept, here the quoted buy amount less
  # slippage (default 50 bps), rounded down.
  # Without a buy chain, use the lowest precision listed for the native asset.
  decimals=$(jq -er --arg a "$(jq -r '.buy.asset' <<<"$quote")" \
    --arg c "$(jq -r '.buy.chain // empty' <<<"$quote")" '[.assets[] | select(.assetId == $a)
      | .variants[] | select(.kind == "native") | .deployments[]
      | select($c == "" or .chain == $c) | .decimals]
      | if length > 0 then min else error("RECEIVE_ASSET_DECIMALS_MISSING") end' <<<"$assets")
  order_body=$(jq -c --argjson bps "${TETRAFI_SLIPPAGE_BPS:-50}" --argjson d "$decimals" '{quoteId: .id,
    minReceive: ((.buy.amount | tonumber) * (10000 - $bps) / 10000
      | . * pow(10; $d) | floor | . / pow(10; $d) | tostring)}' <<<"$quote")
fi
# One Idempotency-Key per order. Keep it: retrying with the same key and body
# returns the first answer instead of placing a second order.
idempotency_key=$(openssl rand -hex 16)
placed=$(tetrafi POST /api/v1/orders "$order_body" "Idempotency-Key: $idempotency_key")
jq -c '{kind, quoteId}' <<<"$placed" >&2
```

```typescript TypeScript SDK theme={null}
// Say the least you accept: the quote's own minimum when it has one, otherwise its receive amount.
// A venue quote is refused without it, and execution never delivers less.
const minReceive = best.minOut ?? best.buy.amount;
if (!minReceive) throw new Error('This quote carries no receive amount to set a minimum from.');
// The SDK sends an Idempotency-Key with every order and reuses it if it has to retry, so a retry
// can never place a second order. Keep `placed.idempotencyKey` if you want to retry later yourself.
const placed = await client.order({ quoteId: best.id, minReceive });
// Wait until the order can no longer change: filled, or ended with a reason.
const done = await client.waitForOrder(placed, { timeoutMs: 120_000 });
```

```json Response (shortened) theme={null}
{
  "kind": "basicTrade",
  "orderId": "acb8…",
  "quoteId": "q_01M5…",
  "status": "working",
  "terminal": false,
  "statusUrl": "/api/v1/orders/acb8…",
  "operation": { "connectionId": "1586…", "operationId": "acb8…" }
}
```

A Basic quote answers `"kind": "basicTrade"` with the venue trade in `operation`. `status` and `terminal` say where it is, and `statusUrl` is where to read it again.

**5. Read the status**

Read the order at the `statusUrl` the answer gave. The read answers the same envelope as the order answer, for every kind of order.

```bash cURL theme={null}
# Read the order at the statusUrl the answer gave: one envelope for Basic and Advanced orders.
# terminal says whether it can still change; next, when present, says what to do meanwhile.
order=$(tetrafi GET "$(jq -r '.statusUrl' <<<"$placed")")
jq -c --arg k "$idempotency_key" '{kind, status, terminal, next, idempotencyKey: $k}' <<<"$order"
```

The script reads the status once. In your own code, poll every 10 to 15 seconds until `terminal` is `true`. `outcomeUnknown` is not a failure: the trade was sent and may have filled, so keep reading it and never place a new order for it ([Order status](https://tetrafi.mintlify.site/trade/order-status.md)).

The script's last line is a one-line summary:

```json Output theme={null}
{"kind":"basicTrade","id":"f507…","status":"filled","idempotencyKey":"bef0…"}
```

In the app, the same trade appears under **Trades**.

## Run the whole script

```bash theme={null}
TETRAFI_PLACE_ORDER=1 bash samples/http/quickstart.sh
```

Progress goes to stderr and the summary to stdout, so `| jq` works on the result. Without `TETRAFI_PLACE_ORDER=1`, the summary reads `"ordered": false`.

## If nothing quotes

When no quote's `next` is `order`, the script stops with `NO_ACTIONABLE_QUOTE` and lists each venue's `status` and `reason` from `sources`. The outcomes a new key most often sees:

| `status` and `reason` | What it means | What to do |
| - | - | - |
| `restricted`, `venueTradeScopeRequired` | The key has no venue trading, so venue accounts are not asked. | Use a key with venue trading ([Get a key with venue trading](#get-a-key-with-venue-trading)). |
| `unavailable`, `notConnected` | Your workspace has no account at that venue. | Connect one ([Connect a venue account](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#connect-a-venue-account)), or ignore that venue. |
| `unavailable`, `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable` | The venue trades from an organisation wallet, and none the key may use is granted to it. | Ask a workspace owner to [grant the key organisation wallets](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#give-a-key-organisation-wallets). |
| `unavailable`, `advancedModeDisabled` | The venue executes on-chain from your own wallet (Advanced), which is not open yet. | Nothing to do. Use a Basic quote. |
| `unsupported`, `pairNotServed` | The venue does not trade this pair. | Nothing to do, or try another pair. |
| A quote whose `next` is `fund` | The venue account holds less than you sell, so the script skipped it. `readiness.amount` is what is missing. | Fund the account, then run the script again. |

[Errors and refusals](https://tetrafi.mintlify.site/trade/errors.md#source-outcomes) lists more outcomes.

## Next steps

> [card] [Authentication](https://tetrafi.mintlify.site/authentication.md)
>
> Permissions, request signing, rotation and rate limits.

> [card] [Orders](https://tetrafi.mintlify.site/trade/orders.md)
>
> Order fields, minimum receive, retries and following an order to the end.


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