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

# Wallets and accounts

> See what each quote trades from and how it is funded, quote with your defaults, and name a trading wallet, venue account or delivery with source and deliverTo.

Every quote says what it would execute with: where it trades from, how the venue is funded, how it settles and where the output goes. You never need to name a wallet to get a quote: venues price with your defaults. To choose differently, send `source` and `deliverTo` on a quote request or a requote.

Every value you send is a `financialAccountId`, and you never invent one: each quote set lists the ids you may name in `sourceOptions`.

The examples on this page come from one script, [`samples/http/wallets.sh`](https://github.com/tetrafi/docs/blob/main/samples/http/wallets.sh). It only asks for quotes and requotes: nothing is ordered.

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

## What a quote trades from

Each quote carries one source block, decided by its venue:

| Field | What it says |
| - | - |
| `tradingSource` | Where the trade executes from: `{"kind": "wallet", "financialAccountId", "network", "venueEnablementId"}` for an on-chain venue, or `{"kind": "venueAccount", "financialAccountId", "venueEnablementId"}` for one of your exchange or OTC venue accounts. |
| `fundingModel` | `prefunded`: the trade spends money already in the trading source. `payIn`: you pay in after you order, by the terms in `payIn`. |
| `fundingAccount` | The account that backs execution, with its `kind` (`wallet`, `venueAccount` or `bankAccount`). `linkId` appears only when money moves through that link. |
| `settlement` | How and when the assets move: `method` (`venueLedger`, `dvp` or `fop`) and `timing` (`immediate`, `byDeadline` or `cycle`). |
| `deliverTo` | Where the output goes, resolved, with a `displayName`. |

A quote that cannot execute from any source you may use carries `sourceRefusal` instead of a usable source: its `code` says why, `venueId` names the venue and `fix` names the next step.

Read these fields on every quote. Two quotes from the same venue can trade from different sources, so never infer the source from the venue's name.

```bash cURL theme={null}
# Each quote names where it trades from (tradingSource), how the venue is funded (fundingModel,
# fundingAccount) and how it settles. Read them; never assume them from the venue's name.
jq -c '.quotes[] | {quoteId: .id, venue: ([.legs[].venue.id] | join(" + ")), location: .location.kind,
  tradingSource: (.tradingSource | {kind, financialAccountId, network}), fundingModel,
  fundingAccount: (.fundingAccount | {kind, financialAccountId}), buy: .buy.amount, next: .next.kind}' <<<"$quotes" >&2
```

```typescript TypeScript SDK theme={null}
// Each quote names its trading source, how it is funded and how it settles. These come from the venue: read
// them, never assume them from the venue's name.
const quotes = set.quotes.map((quote) => ({
  quoteId: quote.id,
  venue: quote.legs.map((leg) => leg.venue.id).join(' + '),
  location: quote.location.kind,
  tradingSource: quote.tradingSource,
  fundingModel: quote.fundingModel,
  fundingAccount: quote.fundingAccount,
  settlement: quote.settlement,
  receive: quote.buy.amount,
  next: quote.next.kind,
}));
```

```json Output shape with a granted wallet (ids shortened) theme={null}
{"quoteId":"q_01M5…","venue":"across","location":"crossChain","tradingSource":{"kind":"wallet","financialAccountId":"4d46…","network":"eip155:84532"},"fundingModel":"prefunded","fundingAccount":{"kind":"wallet","financialAccountId":"4d46…"},"buy":"9.98","next":"order"}
```

## Quote with your defaults

Leave `source` out and every venue prices with your defaults. TetraFi fills each part in this order:

1. what the request names in `source`;
2. each venue account's default organisation wallet, for on-chain venues that trade through that account;
3. your own defaults: an API key's own defaults, or a signed-in member's;
4. the workspace's defaults.

An organisation wallet is your company's own wallet, at organisation scope. For company money it is usually governed: your company's own approvers authorise its signatures ([Wallets and signing](https://tetrafi.mintlify.site/core-concepts/wallets-and-signing.md) explains scope and control). A workspace always has a default organisation wallet: the first organisation wallet created or linked in it. Saved defaults are always organisation-scope wallets, including self-managed wallets added to the company, never personal wallets.

A quote's `walletSelection` says, per chain, why each wallet was chosen. Its `reason` is `accountDefault`, `preference` or `explicit`. It is a record of the choice and grants nothing.

## Read sourceOptions

Every quote set, and every quote in it, lists in `sourceOptions` what you may name. Each entry has a `kind` and its `candidates`:

| `kind` | Lists |
| - | - |
| `wallet` | Trading wallets of one `family` (`evm`, `solana` or `tron`) that can trade on `chain`. |
| `venueAccount` | Venue accounts that can trade at the venue. |
| `fundingAccount` | Accounts that can fund the trade, such as a bank account for a pay-in. |
| `deliverTo` | Where the output can go. |

Each candidate carries its `financialAccountId`, `kind`, `displayName` and `isDefault`. A wallet also carries its `address` and `network`. When present, `balance` is the sold asset held there (`"kind": "known"`) or `"kind": "unavailable"`. A balance that could not be read is never shown as 0, and `null` means it was not observed.

An empty list means there is nothing else to choose. The script reads the trading wallets for each USDC chain, preferring the candidate marked `isDefault`:

```bash cURL theme={null}
# Every answer's sourceOptions lists, per network, the trading wallets this key may name, each by its
# financialAccountId; isDefault marks the one used when you name none.
first=$(quote_request "$(jq -cn --arg amount "$amount" '{sell: {asset: "USDC"}, buy: {asset: "ETH"},
  sellAmount: $amount}')") || { printf '%s\n' "$first" >&2; exit 1; }
trading_wallets=$(jq -c --argjson chains "$usdc_chains" '[.sourceOptions[]? | select(.kind == "wallet")
  | select(.chain as $c | $chains | index($c))
  | {key: .chain, value: (first((.candidates[] | select(.isDefault)), .candidates[]) | .financialAccountId)}
  | select(.value != null)] | from_entries' <<<"$first")
jq -c '{sourceOptions: [.sourceOptions[]? | {kind, chain, candidates: [.candidates[] | {financialAccountId, kind, displayName, isDefault}]}]}' <<<"$first" >&2
```

```typescript TypeScript SDK theme={null}
// Every answer's `sourceOptions` lists, per network of the request, the trading wallets this key may name,
// each by its financialAccountId; `isDefault` marks the one used when you name none. Asked without a chain,
// it lists networks the key has a wallet on.
const walletsIn = (set: QuoteSet): Record<string, string> => {
  const found: Record<string, string> = {};
  for (const option of set.sourceOptions ?? []) {
    if (option.kind !== 'wallet' || !usdcChains.has(option.chain)) continue;
    const chosen = option.candidates.find((candidate) => candidate.isDefault) ?? option.candidates[0];
    if (chosen) found[option.chain] = chosen.financialAccountId;
  }
  return found;
};
let tradingWallets = walletsIn(await client.quote({ sell: { asset: 'USDC' }, buy: { asset: 'ETH' }, sellAmount: '10' }));
// To move USDC between chains, ask each corridor from a wallet's chain until one has a wallet on both sides.
const home = Object.keys(tradingWallets).sort()[0];
for (const other of [...usdcChains].sort()) {
  if (!home || other === home || Object.keys(tradingWallets).length >= 2) break;
  await pause();
  const both = walletsIn(await client.quote({ sell: { asset: 'USDC', chain: home }, buy: { asset: 'USDC', chain: other }, sellAmount: '10' }));
  if (both[home] && both[other]) tradingWallets = { [home]: both[home], [other]: both[other] };
}
const chains = Object.keys(tradingWallets).sort((a, b) => (a === home ? -1 : b === home ? 1 : a.localeCompare(b)));
```

```json Output shape with a granted wallet (ids shortened) theme={null}
{"sourceOptions":[{"kind":"wallet","chain":"eip155:84532","candidates":[{"financialAccountId":"4d46…","kind":"wallet","displayName":"Treasury (Base Sepolia)","isDefault":true},{"financialAccountId":"61c3…","kind":"wallet","displayName":"Payments (Base Sepolia)","isDefault":false}]}]}
```

When the list has no wallet the key may use, the script prints what each on-chain venue says and stops. This is the live output for a key with no wallet granted:

```json Output on test networks, no wallet granted (shortened) theme={null}
{"sourceOptions":[]}
[{"venue":"circle-cctp","status":"unavailable","reason":"onchainWalletNotUsable","detail":"Your saved wallet is not visible or usable for this API key on Base Sepolia, so it was not asked. …"}]
{"tradingWallets":{},"quoted":false}
```

With an API key that has no wallet granted, on-chain venues read `unavailable` with the reason `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable`: the second means the venue account's saved wallet is not visible to the key. See [Give a key organisation wallets](#give-a-key-organisation-wallets).

## Choose with source

`source` has three parts. Each is optional, and a part you leave out takes your default.

| Field | What it names |
| - | - |
| `tradingWallets` | The trading wallet for each chain, keyed by CAIP-2 chain id. Each value is a wallet's `financialAccountId` from `sourceOptions`. |
| `venueAccountId` | The venue account the quote trades on. |
| `fundingAccountId` | The account that funds the trade, such as the bank account a pay-in comes from. |

Chains belong to one of three families, and each value must be a wallet that can trade on its chain:

| Family | Chain id form | Example |
| - | - | - |
| EVM | `eip155:<chain id>` | `eip155:8453` |
| Solana | `solana:<genesis hash>` | `solana:5eykt4Us…` |
| Tron | `tron:<network>` | `tron:shasta` (the Shasta test network) |

The script names the wallets it read. With two chains it moves USDC between them; with one, it swaps USDC for ETH on that chain:

```bash cURL theme={null}
# Name one trading wallet per chain in source.tradingWallets, by the financialAccountId sourceOptions
# gave: two chains move USDC between them, one chain swaps USDC for ETH on it.
request=$(jq -cn --argjson w "$trading_wallets" --arg amount "$amount" '($w | keys) as $chains
  | (if ($chains | length) > 1
      then {sell: {asset: "USDC", chain: $chains[0]}, buy: {asset: "USDC", chain: $chains[1]}}
      else {sell: {asset: "USDC", chain: $chains[0]}, buy: {asset: "ETH", chain: $chains[0]}} end)
  + {sellAmount: $amount, source: {tradingWallets: ($w | with_entries(select(.key as $k | $chains[0:2] | index($k))))}}')
```

```typescript TypeScript SDK theme={null}
// Two chains: move USDC between them. One chain: swap USDC for ETH on it. `source.tradingWallets` names the
// wallet per chain by the financialAccountId sourceOptions gave.
const [from, to] = chains as [string, ...string[]];
const request: QuoteRequest = {
  ...(to
    ? { sell: { asset: 'USDC', chain: from }, buy: { asset: 'USDC', chain: to } }
    : { sell: { asset: 'USDC', chain: from }, buy: { asset: 'ETH', chain: from } }),
  sellAmount: '10',
  source: { tradingWallets: Object.fromEntries(chains.slice(0, 2).map((chain) => [chain, tradingWallets[chain]!])) },
};
```

```json Request sent (shortened) theme={null}
{"sell":{"asset":"USDC","chain":"eip155:84532"},"buy":{"asset":"USDC","chain":"eip155:11155111"},"sellAmount":"10","source":{"tradingWallets":{"eip155:84532":"4d46…","eip155:11155111":"9b07…"}}}
```

`preferredWallets` is a softer choice. It is keyed the same way, but it is only a preference. A venue account uses that wallet only when it is one of the account's linked wallets on the venue's network; otherwise it quotes from its own default. A preference never makes a venue refuse. `source.tradingWallets` is the explicit choice and wins on its chain.

An API key uses your company's self-managed and custodian wallets, and the organisation wallets created in TetraFi that an owner has granted to that key. It cannot use or link a personal wallet: only a signed-in member can link one. In the app, a member can choose a personal wallet for a single request on the trade page while **Advanced filters** is open. Despite its name, **Advanced filters** only opens more search filters: it is not Advanced execution.

### When TetraFi refuses a source

A wallet or account you may not use at all is refused before any venue is asked, so nothing is quoted or reserved. Trading wallets in `source.tradingWallets` are different: one a venue account has not linked is not refused. That venue reads `unavailable` with the reason `tradingSourceNotLinked`, and the others still quote. The script prints either case:

```bash cURL theme={null}
# A wallet or account you may not use at all is refused before any venue is asked (for example
# 403 SOURCE_NOT_OWNED): nothing is quoted or reserved. A trading wallet a venue account has not
# linked is not refused: that venue reads unavailable with reason tradingSourceNotLinked.
if ! quotes=$(quote_request "$request"); then
  jq -c '{refused: .error, message, next}' <<<"$quotes" >&2
  jq -cn --argjson q "$quotes" '{tradingWallets: true, refused: $q.error}'
  exit 0
fi
jq -c '[.sources[] | select(.reason == "tradingSourceNotLinked") | {venue: .venue.id, reason, detail}]' <<<"$quotes" >&2
```

```typescript TypeScript SDK theme={null}
// A wallet the key may not use is refused before any venue is asked; the code says which rule applied and
// `next` (when present) says what to do. Nothing was quoted or reserved.
let set: QuoteSet;
try {
  await pause();
  set = await client.quote(request);
} catch (error) {
  if (isTetrafiApiError(error) && isSourceRefusal(error.code)) {
    return { tradingWallets: request.source, refused: { status: error.status, code: error.code, message: error.message,
      next: error.next, requestId: error.requestId } };
  }
  throw error;
}
```

```json Output shape of a refusal (shortened) theme={null}
{"refused":"SOURCE_NOT_OWNED","message":"A source names a wallet or account this caller cannot use.","next":null}
```

| Status and code | Meaning | What to do |
| - | - | - |
| `403 SOURCE_NOT_OWNED` | A wallet or account in `source` or `deliverTo` does not exist, or you cannot use it, for example another workspace's bank account. Both cases read the same. | Choose a candidate from `sourceOptions`. A key needs an owner to grant the organisation wallet. Otherwise, ask a wallet manager to register or share it. |
| `400 WALLET_NOT_ON_CHAIN` | The wallet is registered on another chain, so it cannot trade on the chain you keyed it under. | Choose a candidate listed for that chain. |
| `409 PERSONAL_TARGET_NOT_ALLOWED` | An API key named a personal wallet. | Use an organisation-scope wallet. |
| `unavailable`, `tradingSourceNotLinked` (on the venue, not a refusal) | The trading wallet you named is not linked to that venue's account. The other venues still quote. | Name a wallet that venue's account has linked, or link it on the venue account. |
| `403 ACCOUNT_NOT_AVAILABLE` | The venue account is not one this quote can trade on. | Leave `venueAccountId` out, or name a candidate from the `venueAccount` option. |
| `409 REQUOTE_NEEDS_WALLET` | The venue quotes only once a wallet is chosen. `details.reason` names the family. | Requote with `source.tradingWallets` naming a wallet of that family. |

Your workspace has one account at each venue, so a venue quote's `venueAccount` option lists that account only.

## Where the output goes

`deliverTo` is a top-level field of the request, tagged by `kind`:

| `kind` | Fields | Meaning |
| - | - | - |
| `financialAccount` | `financialAccountId` | Deliver to one of your own accounts: a wallet, a venue account or a bank account. |
| `address` | `chain`, `address`, `destinationTag` (optional), `calldata` (optional) | Send the output to this address on this chain. |

The shape also has a `payee` kind, but no trade can deliver to a payee yet: deliver to an address or one of your own accounts.

`destinationTag` is the tag or memo an address needs, such as an XRP destination tag, a Stellar memo or an exchange deposit memo. Leaving it out means no tag: TetraFi never fills one in for you.

With an API key, `deliverTo` naming an address outside the workspace's organisation wallets is refused with `403 API_KEY_ORGANISATION_WALLET_NOT_ALLOWLISTED` on the quote request, before an order is placed. Use an organisation-wallet destination the key may use. Manage delivery destinations in the app, not through a per-key address allowlist: an API key cannot allowlist an outside address. This is separate from the key's grants to trade from organisation wallets ([Give a key organisation wallets](#give-a-key-organisation-wallets)).

On a venue quote, delivering to a wallet asks TetraFi to withdraw the output from your venue account once the order fills. The wallet must be one of your workspace's wallets, approved as a withdrawal destination of that venue account for the asset you buy, with exactly the tag it was approved with. The quote then carries a network fee line for the venue's withdrawal fee (the most the order will pay) and a `buy.amount` net of it.

Delivering a venue quote to a wallet needs a signed-in member who can view the workspace's wallets. With an API key, a venue quote's output stays on the venue account: a requote that names another destination is refused with the reason `deliverToNotPermitted`.

When a venue cannot deliver where you asked, a new quote request lists that venue in `sources` as `unavailable` with the reason. A requote is refused with `409 REQUOTE_REFUSED` and the reason in `details.reason`:

| Reason | What to do |
| - | - |
| `DESTINATION_NOT_APPROVED` | Choose an approved withdrawal destination of that venue account, with the same tag, or have it approved first. |
| `DELIVER_TO_NOT_A_WORKSPACE_WALLET` | Deliver to one of your workspace's wallets. |
| `WITHDRAWAL_CHAIN_UNSUPPORTED` | The venue account cannot withdraw that asset on that chain. Choose another chain or keep the output on the account. |
| `DELIVER_TO_AMBIGUOUS` | More than one wallet or approval matches the address, so none is chosen. Remove the duplicate registration or approval, then try again. |
| `WITHDRAWAL_FEE_EXCEEDS_AMOUNT` | The withdrawal fee leaves nothing to receive. Trade a larger amount or keep the output on the account. |
| `deliverToCalldataUnsupported` | This venue cannot send calldata with the output. Leave `calldata` out. |
| `deliverToNotPermitted` | Delivering to a wallet needs a signed-in member who can view wallets. Keep the output on the account, or deliver from the app. |

The script requotes an off-chain quote with `deliverTo` naming the trading wallet it read, and records the outcome. When no off-chain quote came back, it skips the requote. It never guesses a destination.

```bash cURL theme={null}
# Delivering elsewhere is a requote with deliverTo. With an API key a venue account's output stays on
# the account: a deliverTo naming another destination is refused (REQUOTE_REFUSED, deliverToNotPermitted).
target=$(jq -c 'first(.quotes[] | select(.location.kind == "offChain")) // empty' <<<"$quotes")
if [[ -z $target ]]; then
  echo '{"deliverTo":"no off-chain quote to try"}' >&2
  deliver_outcome=none
else
  body=$(jq -c --argjson w "$trading_wallets" '{requote: .id, deliverTo: {kind: "financialAccount",
    financialAccountId: ($w | to_entries[0].value)}}' <<<"$target")
  if delivered=$(quote_request "$body"); then
    deliver_outcome=$(jq -r '.quotes[0].next.kind' <<<"$delivered")
  else
    deliver_outcome=$(jq -r '[.error, .details.reason // empty] | join(":")' <<<"$delivered")
  fi
fi
```

```json Output when no off-chain quote came back theme={null}
{"deliverTo":"no off-chain quote to try"}
```

## Requote with a different source

To change the source of a quote you hold, requote it. Send `POST /api/v1/quotes` with `requote` set to the quote id, plus `source` or `deliverTo` with the parts you change. Only that quote's venue is asked again, for the same pair, amount and side. You get back one new quote whose `requoteOf` names the original. The original stays orderable until its own `expiresAt`. [Requotes and expiry](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md) covers requotes in full.

```json Request theme={null}
{ "requote": "q_01M5…", "source": { "tradingWallets": { "eip155:84532": "61c3…" } } }
```

A requote is a live quote request, so the limit of one every 3 seconds applies. The script's `quote_request` helper waits for `Retry-After` once when it gets `429 QUOTE_RATE_LIMITED`.

## When a quote needs a wallet

Some venues can only quote once they know which wallet trades. You can see this in three places.

**A quote's next step is a requote.** `next` reads `{"kind": "requote", "needs": [...]}`. Each entry in `needs` is a slot to fill: `wallet:evm`, `wallet:solana`, `wallet:tron`, `account`, `bankAccount` or `deliverTo`. Requote the quote with `source` or `deliverTo` filling those slots, choosing from `sourceOptions`. An empty `needs` means requote it as it is.

**A venue reads `needsWallet` in `sources`.** It has no wallet of that `family` (`evm`, `solana` or `tron`) to ask with. Send the request again with `source.tradingWallets` naming a wallet on the chain you sell from, or set a default for that family.

**A venue reads `walletRefused` in `sources`.** The wallet in `wallet` cannot trade there, for example because it was revoked or screening confirmed a sanctions match. `reason` says why when TetraFi can. Sometimes the refused wallet is an organisation wallet you chose that is not linked to the venue account. Then `recovery` carries the `wallet`, a `linkHref` where it can be linked, and `defaultWallets`: the account's default wallet for each chain, which you can send as `source.tradingWallets` for this request instead.

With an API key, on-chain venues may also read `unavailable` with the reason `apiKeyOrganisationWalletScopeRequired` or `onchainWalletNotUsable`. The key has no organisation wallet granted to it yet: grant one from the key's **Wallets** tab (see below).

## Give a key organisation wallets

An API key trades through organisation wallets only when they are granted to it. The key must already be able to trade on venue accounts ([Trading on your venue accounts](https://tetrafi.mintlify.site/authentication.md#trading-on-your-venue-accounts)).

**Create API key** with venue trading already grants your workspace's default organisation wallets that you hold a Trade right on. To change the grant later, open **Personal API keys** (**Settings**, then **Open API keys** under **Your account**), expand the key and choose its **Wallets** tab:

* **Grant** adds a wallet. The tab offers only the organisation wallets you hold a Trade right on.
* **Revoke** removes one.

Who can grant what:

* **The key's owner**, with permission to request quotes and place orders, can add the workspace's default organisation wallets they hold a Trade right on, and can revoke any grant. Any other wallet is refused with `API_KEY_ORGANISATION_WALLET_NOT_A_DEFAULT`, and one without your Trade right with `API_KEY_ORGANISATION_WALLET_TRADE_RIGHT_REQUIRED`.
* **A workspace owner, or an admin who manages credentials**, can grant any active organisation wallet of the workspace through the API below; the tab still offers only the wallets they hold a Trade right on, since the key trades only through wallets its owner can trade.

Every change needs a sign-in with a second factor (otherwise `MFA_REQUIRED`), and the tab shows each refusal with its reason. Behind it, `PUT /api/v1/workspaces/{workspaceId}/api-keys/{keyId}/organisation-wallets` takes the key's complete list: a new list replaces the previous one, and an empty list revokes them all. The key itself cannot make or change its grant.

Once granted, the wallets appear in the key's `sourceOptions`, and their ids are what the key sends in `source.tradingWallets`. At trade time the key also needs venue trading switched on and a valid request signature, and you, the key's owner, still need trading rights on the wallet.

## Connect a venue account

Exchange and OTC venue accounts are connected in the app by someone who can manage your workspace's venues:

1. Open **Counterparties** and choose the venue's liquidity provider.
2. Select **Set up \<provider> account**.
3. Work through **Integration mode**, **Account** and **Venues**, entering the credentials the venue asks for.
4. In **Verification**, TetraFi checks each venue you added. Select **Check again** to repeat a check, then **Review setup**.

Once the account is set up, **Open \<provider> account** takes you to it. Its quotes appear for the members and keys allowed to trade on venue accounts, with `tradingSource.kind` reading `venueAccount`.

In the app, the **Pay from**, **Account** and **Deliver** choices on a quote card each send one of these requotes.

## Next steps

> [card] [Requotes and expiry](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md)
>
> Change a quote's corridor or price protection, or refresh one that expired.

> [card] [Authentication](https://tetrafi.mintlify.site/authentication.md)
>
> Venue trading, request signing and rate limits for your key.


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