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

# Trade on a connected exchange account

> Trade on one of your exchange accounts with an API key: scope the search to its venue, check it holds enough, order, recover the order by its quote and read its status.

This recipe trades 5 USDC for USDT on one exchange account your workspace has connected, with an API key. TetraFi places the trade on that account, with that account's balance, so no wallet signs anything. Every step comes from one script, [`samples/http/recipe-exchange-order.sh`](https://github.com/tetrafi/docs/blob/main/samples/http/recipe-exchange-order.sh).

> [!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:

* **A connected exchange account.** Someone who manages your workspace's venues connects it in the app ([Connect a venue account](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#connect-a-venue-account)), and it must hold the asset you sell.
* **An API key that can trade on venue accounts.** Besides **Read quotes**, **Submit orders** and **Read orders**, the key needs the venue trading scope, which is granted only when the key is created, and venue trading switched on for it by a workspace owner or an admin who manages credentials ([Trading on your venue accounts](https://tetrafi.mintlify.site/authentication.md#trading-on-your-venue-accounts)). Without it, the venue is listed in `sources` as `restricted` with the reason `venueTradeScopeRequired`, and you get no quote from it.
* **The key's signing secret.** A key with venue trading signs every request. The script's `tetrafi` helper signs each one when `TETRAFI_SIGNING_SECRET` is set ([Sign requests](https://tetrafi.mintlify.site/authentication.md#sign-requests)).

Export your environment as in the [Quickstart](https://tetrafi.mintlify.site/quickstart.md), plus your workspace id and, if you know it, the venue id of your account:

```bash theme={null}
export TETRAFI_WORKSPACE_ID=...    # the workspace the key belongs to (see Authentication)
export TETRAFI_VENUE=...           # optional: the venue id of your exchange account
```

The script needs the workspace id for one step only: after it recovers the order by its quote, the trade's status address contains the workspace id.

**1. Find your account's venue**

Quotes from your exchange account carry its venue id, such as `binance-orderbook`. If you do not set `TETRAFI_VENUE`, the script finds it with one search that keeps only off-chain quotes (`filters.locations: ["offChain"]`): only your connected venue accounts answer that. It takes the venue of the best Basic quote.

```bash cURL theme={null}
# Without TETRAFI_VENUE, find your exchange accounts' venues from one off-chain search: only your
# connected venue accounts answer it.
venue=${TETRAFI_VENUE:-}
if [[ -z $venue ]]; then
  found=$(quote "$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" '{
    sell: {asset: $sell}, buy: {asset: $buy}, sellAmount: $amount,
    filters: {locations: ["offChain"]}}')")
  jq -c '.sources[] | select(.status != "filteredOut") | {venue: .venue.id, status, reason}' <<<"$found" >&2
  venue=$(jq -r 'first(.quotes[] | select(.legs[0].venue.integrationMode == "basic") | .legs[0].venue.id) // empty' <<<"$found")
fi
if [[ -z $venue ]]; then
  echo '{"orderable":false,"reason":"NO_VENUE_ACCOUNT_QUOTE"}'
  exit 0
fi
```

```json Output (shortened) theme={null}
{"venue":"okx-rfq","status":"unavailable","reason":"notConnected"}
{"venue":"binance-orderbook","status":"answered","reason":null}
```

`notConnected` means your workspace has no account at that venue.

**2. Search that venue only**

`filters.venues.include` with one venue id asks that venue alone. Every other venue is listed in `sources` as `filteredOut` and is not asked. The quote trades on your account at that venue: its `tradingSource` has `kind` `venueAccount`, and `fundingModel` says whether the account must already hold the funds (`prefunded`) or takes them in (`payIn`).

```bash cURL theme={null}
# Ask that venue only. Its quote trades on your account there: tradingSource names it (kind venueAccount).
request=$(jq -nc --arg sell "$sell" --arg buy "$buy" --arg amount "$amount" --arg venue "$venue" '{
  sell: {asset: $sell}, buy: {asset: $buy}, sellAmount: $amount,
  filters: {venues: {include: [$venue]}}
}')
quotes=$(quote "$request")
jq -c '.sources[] | select(.status != "filteredOut") | {venue: .venue.id, status, reason}' <<<"$quotes" >&2
quote=$(jq -c 'first(.quotes[] | select(.tradingSource.kind == "venueAccount")) // empty' <<<"$quotes")
if [[ -z $quote ]]; then
  jq -c --arg v "$venue" '{venue: $v, orderable: false, sources: [.sources[] | {venue: .venue.id, status, reason}]}' <<<"$quotes"
  exit 0
fi
jq -c '{quoteId: .id, venue: .legs[0].venue.id, mode: .legs[0].venue.integrationMode, firm,
  sell: .sell.amount, buy: .buy.amount, minOut, next: .next.kind, expiresAt}' <<<"$quote" >&2
quote_id=$(jq -r '.id' <<<"$quote")
```

```json Output (shortened) theme={null}
{"quoteId":"q_01M5XSRC…","venue":"binance-orderbook","mode":"basic","firm":false,"sell":"5","buy":"5.00675","minOut":"4.99172","next":"order","expiresAt":"2026-10-27T04:26:42.527Z"}
```

You can send one live quote request every 3 seconds. The script waits `details.retryAfter` once when it gets `429 QUOTE_RATE_LIMITED`.

**3. Check the account holds enough**

Each quote says whether the account holds enough to trade it, in `readiness`. To see everything the account holds, read its balances with the same key ([Exchange account balances](https://tetrafi.mintlify.site/trade/venue-balances.md)).

| `readiness.kind` | What it means |
| - | - |
| `funded` | The account holds enough. `next` is `order`. |
| `shortfall` | The account holds less. `amount` is what is missing and `next` is `fund`. Fund the account, then get a new price. |

```bash cURL theme={null}
# Each quote says whether the account holds enough to trade it: readiness "funded", or
# "shortfall" with the amount missing and next "fund". GET …/venue-accounts/{venueAccountId}/balances reads
# what the account holds.
jq -c '{readiness}' <<<"$quote" >&2
if [[ $(jq -r '.next.kind' <<<"$quote") != order ]]; then
  jq -c '{quoteId: .id, orderable: false, next: .next.kind, readiness}' <<<"$quote"
  exit 0
fi
```

```json Output theme={null}
{"readiness":{"kind":"funded"}}
```

To see the balances themselves, check in the app (below).

**4. Place the order**

`POST /api/v1/orders` with the quote id and an `Idempotency-Key` you generate and store before you send it. A Basic quote takes no signature.

Some desks execute within your limit: when a quote reads `minReceiveRequired: true`, its order must carry `minReceive`, the least you accept ([Orders](https://tetrafi.mintlify.site/trade/orders.md#venue-quotes-that-execute-within-your-limit)). The script then sets `minReceive` to the quoted amount less 50 basis points. When it reads `false`, like the order-book quote above, `minReceive` is optional, and the script sends only the quote id. A refused order names its code, such as `409 FUNDING_SHORTFALL`, and nothing was traded ([Orders](https://tetrafi.mintlify.site/trade/orders.md#refusals)).

The script places the order only when you set `TETRAFI_PLACE_ORDER=1`.

```bash cURL theme={null}
# When the quote's minReceiveRequired is true, the venue executes within your limit: send minReceive,
# here the quoted amount less TETRAFI_SLIPPAGE_BPS (default 50), rounded down to 8 decimal places.
order_body=$(jq -c --argjson bps "${TETRAFI_SLIPPAGE_BPS:-50}" '{quoteId: .id}
  + (if .minReceiveRequired == true then {minReceive: ((.buy.amount | tonumber) * (10000 - $bps) / 10000
      | . * 100000000 | floor | . / 100000000 | tostring)} else {} end)' <<<"$quote")
# Store the Idempotency-Key with your record of the order before you send it.
idempotency_key=$(openssl rand -hex 16)
if placed=$(tetrafi POST /api/v1/orders "$order_body" "Idempotency-Key: $idempotency_key"); then
  jq -c '{kind, quoteId, operation: (.operation | {connectionId, operationId})}' <<<"$placed" >&2
elif [[ -n $(jq -r '.error // empty' <<<"$placed" 2>/dev/null) ]]; then
  # A refusal names its code, for example FUNDING_SHORTFALL, and nothing was placed.
  exit 1
fi
```

The script prints the order response's `kind`, `quoteId` and `operation`:

```json Output (shortened) theme={null}
{"kind":"basicTrade","quoteId":"q_01M5XSRC…","operation":{"connectionId":"15862432-…","operationId":"0b23326f-…"}}
```

**5. Recover the order by its quote**

If the order response never reaches you, after a timeout or a dropped connection, do not order again with a new key: that is a new order. Read the order by its quote with `GET /api/v1/orders/by-quote/{quoteId}`. It never places anything ([Idempotency](https://tetrafi.mintlify.site/trade/idempotency.md#recover-an-order-by-its-quote)).

| `state` | What to do |
| - | - |
| `ordered` | Read its status at `statusUrl`. Do not order again. |
| `notOrdered` | Nothing was placed. If `expired` is `false`, you can order it. |
| `unknown` | Wait and read again. Do not order again. |

The script reads the order this way every time, and takes `statusUrl` from the answer.

```bash cURL theme={null}
# If the order response is lost (a timeout, a dropped connection), never order again with a new
# key. Read the order by its quote: "ordered" gives its statusUrl, "notOrdered" means nothing was
# placed, "unknown" means wait and read again.
recovered=$(tetrafi GET "/api/v1/orders/by-quote/$quote_id")
jq -c '{state, orderId, statusUrl}' <<<"$recovered" >&2
if [[ $(jq -r '.state' <<<"$recovered") != ordered ]]; then
  jq -c --arg q "$quote_id" '{quoteId: $q, ordered: false, state}' <<<"$recovered"
  exit 0
fi
```

```json Output (shortened) theme={null}
{"state":"ordered","orderId":"0b23326f-…","statusUrl":"/api/v1/orders/0b23326f-…"}
```

**6. Read the status**

Read the trade at its `statusUrl` until `terminal` is `true`, every 10 seconds, or after `next.retryAfter` when `next` says `wait`. `outcomeUnknown` is not a failure: the trade may have filled, so keep reading ([Order status](https://tetrafi.mintlify.site/trade/order-status.md)). The venue's own detail is under `operation`.

```bash cURL theme={null}
# Read the trade at its statusUrl until terminal is true, polling every 10 seconds, or after
# next.retryAfter when it says to wait. outcomeUnknown is not a failure: the trade may have filled.
status_url=$(jq -r '.statusUrl' <<<"$recovered")
for _ in $(seq 1 30); do
  trade=$(tetrafi GET "$status_url")
  jq -c '{status, terminal, next}' <<<"$trade" >&2
  if jq -e '.terminal == true' <<<"$trade" >/dev/null; then break; fi
  wait=$(jq -r '.next | select(.kind == "wait") | .retryAfter | capture("^PT(?<s>[0-9]+)S$").s' <<<"$trade" 2>/dev/null || true)
  sleep "${wait:-10}"
done
# The venue's own detail is under operation. Amounts can be null when the venue has not reported
# them. Bounds are limits, not a fill.
jq -c '.operation | {executionStatus, settlementStatus, bounds: (.bounds | {sell, buy}),
  dispatch: (.dispatch | if . == null then null else {inputSpent, outputReceived, venueOrderId} end)}' <<<"$trade" >&2
```

```json Output theme={null}
{"status":"settled","terminal":true,"next":null}
{"executionStatus":"filled","settlementStatus":null,"bounds":{"sell":{"asset":"USDC","amount":"5"},"buy":{"asset":"USDT","amount":"4.99172"}},"dispatch":{"inputSpent":"5","outputReceived":"5.00675","venueOrderId":"229377"}}
```

When the venue reports them, `dispatch.inputSpent` and `dispatch.outputReceived` are what the trade took from and added to the account. A missing or `null` amount is unknown, never zero. `bounds.sell.amount` (the most it spends) and `bounds.buy.amount` (the least it delivers) are your accepted limits, not fill amounts. `dispatch.venueOrderId`, when present, is the venue's native order id.

Check `settlementStatus` for any later money movement. To confirm the balance independently, open the account under **Treasury** in the app.

## Run the whole script

```bash theme={null}
TETRAFI_PLACE_ORDER=1 bash samples/http/recipe-exchange-order.sh
```

Progress goes to stderr and the summary to stdout:

```json Output theme={null}
{"venue":"binance-orderbook","quoteId":"q_01M5XSRC…","kind":"basicTrade","id":"0b23326f-…","status":"filled","idempotencyKey":"33c6…"}
```

Without `TETRAFI_PLACE_ORDER=1`, the summary reads `"ordered": false`.

## In the app

The account's page under **Treasury** shows its balances, and the trade appears under **Trades**.

## Next steps

> [card] [Orders](https://tetrafi.mintlify.site/trade/orders.md)
>
> Venue quotes that execute within your limit, and every refusal.

> [card] [Idempotency](https://tetrafi.mintlify.site/trade/idempotency.md)
>
> Retry an order safely and find out whether it was placed.


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