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

# Wallet Pools

> Group vault accounts into a Wallet Pool and send from it as one logical source, with health-aware member selection, Policy setup, and reconciliation.

## Overview

New to Wallet Pools? See the [Wallet Pools](https://support.fireblocks.io/hc/en-us/articles/27776980063260-Wallet-Pools) Help Center article for a product introduction and the Console walkthroughs.

A Wallet Pool groups vault accounts under a single logical source. Name the pool as a transaction source and Fireblocks selects one member vault account to send from, preferring members that are not congested.

On EVM chains each vault account has one sequential nonce stream, so a transaction that fails to confirm blocks every later transaction from that account until it clears. A single hot vault account is therefore both a throughput ceiling and a single point of failure. A pool removes the need to build vault rotation and stuck-transaction tracking in your own infrastructure.

A pool is a protected tag with `type: "WALLET_POOL"`, so it inherits protected tag governance: membership changes are quorum-approved and Policy rules can reference the pool directly. There are no wallet-pool endpoints. Pools are created, populated, and used through the existing tags, vaults, and transactions endpoints. See [Tags](/docs/tags) for the tag lifecycle these calls share.

<Note>
  A pool distributes transactions, not funds. Balances are never summed across members. See [Limitations](#limitations).
</Note>

## Use cases

A pool solves the same congestion problem in three places. In each case you create the pool the same way, then point the relevant flow at it instead of at a single vault account.

* **Withdrawal vaults.** High-volume withdrawals run through a small set of hot vault accounts, where one stuck transaction blocks every later withdrawal from that account until it clears. A pool spreads withdrawals across several vault accounts, skips the congested ones, and rotates across multiple on-chain addresses so your withdrawal pattern is less predictable. Set the pool as the transaction source, as in [Step 5](#step-5-send-from-the-pool).
* **Gas Station tank.** Gas Station fuels every vault account in your workspace from a single gas tank, so on EVM all fueling shares one sequential nonce stream. That stream stalls exactly when you can least afford it, during a traffic spike when many vault accounts need topping up at once. A pool spreads the fueling across several vault accounts. In **Settings** > **Gas station tank**, select a Wallet Pool instead of a single vault account.
* **Gasless relay.** With gasless transactions your end users hold no native asset, and a relayer vault account signs and pays gas on their behalf. That relayer is one vault account with one nonce stream, so it becomes the bottleneck as gasless volume grows. A pool spreads relaying across several vault accounts. In **Settings** > **Initiate gasless transactions** > **EVM**, select a Wallet Pool as the relayer source.

Relayer selection uses the same tiering and rotation as a transfer, but sizes its requirement against a gas budget rather than a transfer amount, and uses a higher limit when the end user's account needs delegating first. A relayer pool therefore needs more native-asset headroom per member than a transfer pool of comparable volume. Use a separate pool per role, since the balance profiles differ.

## Requirements

* **Roles.** Creating a pool requires the Owner, Admin, Non-Signing Admin, or Editor role and needs no approval. Adding or removing members requires the same roles to submit, plus an Owner, Admin, or Non-Signing Admin to approve. There is no API to approve; approvers act in the Fireblocks mobile app or under Pending Approvals in the Console.
* **SDK.** `WALLET_POOL` as a transfer peer type requires `@fireblocks/ts-sdk` 24 or later. Raw HTTP works on any version.
* **Members.** Vault accounts only. Each member needs the transfer asset and the chain's native asset.

## API flow

```mermaid theme={"system"}
flowchart LR
    A[Create pool tag] --> B[Attach vault accounts]
    B --> C[Approval clears]
    C --> D[Add Policy rule]
    D --> E[Send with pool source]
```

### Step 1. Create the pool

`POST /v1/tags`

**Request body**

```json theme={"system"}
{
  "label": "Withdrawal Vaults",
  "description": "Hot withdrawal pool",
  "type": "WALLET_POOL",
  "isProtected": true
}
```

**Response**

```json theme={"system"}
{
  "id": "588af612-c2e6-491a-8bb4-86c85868f42f",
  "label": "Withdrawal Vaults",
  "isProtected": true,
  "type": "WALLET_POOL",
  "updatedAt": 1786549844000
}
```

The tag's `id` is the pool ID, and every later step refers to it. Pool names are unique across the workspace and collide with protected tag names as well as other pools. For the full field reference on this endpoint, including label rules and per-request limits, see [Create a tag](/docs/tags#step-1-create-a-tag).

<Note>
  `isProtected: true` does not identify a pool. Ordinary protected tags are also `isProtected: true`, and they omit `type` entirely. To tell a pool from an ordinary protected tag, check that `type` is `WALLET_POOL`.
</Note>

### Step 2. Attach vault accounts

`POST /v1/vault/accounts/attached_tags`

**Request body**

```json theme={"system"}
{
  "vaultAccountIds": ["12", "13", "14"],
  "tagIdsToAttach": ["588af612-c2e6-491a-8bb4-86c85868f42f"]
}
```

**Response**

```json theme={"system"}
{
  "appliedOperations": [],
  "pendingOperations": [
    {
      "vaultAccountId": "12",
      "tagId": "588af612-c2e6-491a-8bb4-86c85868f42f",
      "action": "ATTACH",
      "approvalRequestId": "9f2c1e77-4b0a-4d21-8e6b-1c5a3f0d9b84"
    }
  ],
  "rejectedOperations": []
}
```

| Field                | Notes                                                                                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `appliedOperations`  | Always empty for pools, since attachments require approval.                                                                                                                                                    |
| `pendingOperations`  | Queued for approval. Each entry carries an `approvalRequestId`.                                                                                                                                                |
| `rejectedOperations` | Never queued. Each entry carries a `reason`: `PENDING_REQUEST_EXISTS` (a change for this pair is already awaiting approval), `ATTACHMENT_ALREADY_EXISTS`, `ATTACHMENT_DOES_NOT_EXIST`, or `CAPACITY_EXCEEDED`. |

Only one membership change per vault account and pool can be in flight, so serialize attachments. For idempotent provisioning, treat `ATTACHMENT_ALREADY_EXISTS` on attach and `ATTACHMENT_DOES_NOT_EXIST` on detach as success.

Detaching uses the same call with `tagIdsToDetach`, and follows the same approval flow. Removing a member resets the rotation pointer to the first member.

### Step 3. Track the approval

`GET /v1/tags/approval_requests/{approvalRequestId}`

**Response**

```json theme={"system"}
{
  "id": "9f2c1e77-4b0a-4d21-8e6b-1c5a3f0d9b84",
  "type": "TAG_ATTACH_DETACH",
  "state": "PENDING"
}
```

`state` is one of `PENDING`, `APPROVED`, `REJECTED`, `FAILED`, `CANCELLED`, or `EXPIRED`. Poll until it is terminal and treat anything other than `APPROVED` as the change not having happened; `EXPIRED` and `CANCELLED` are silent no-ops that leave the pool empty. A pool uses the same approval machinery as any protected tag, so see [Protected tags and the approval flow](/docs/tags#protected-tags-and-the-approval-flow) for what each state means.

Confirm membership before sending, since an empty pool rejects every transaction. Read the pool's members with `GET /v1/vault/accounts_paged?includeTagIds={poolId}`, and list your pools with `GET /v1/tags?type=WALLET_POOL`.

### Step 4. Allow the pool in your Policy

<Warning>
  Fireblocks resolves the pool to one member and then evaluates Policy against **that member**, not against the pool. A rule that permits only specific vault account IDs as sources blocks any transaction that resolves to a different member, so the same request succeeds and then fails as routing moves between members. This is the most common cause of a failed first pool transaction.
</Warning>

In the Console, go to Policies, add or edit a rule, and under source select the pool from the **Wallet Pools** category. Set destinations to cover everywhere the pool needs to reach.

Referencing the pool also keeps the rule stable as your vault inventory grows: add or remove members through the approval flow and both routing and enforcement follow, with no Policy edit.

### Step 5. Send from the pool

`POST /v1/transactions`

**Request body**

```json theme={"system"}
{
  "assetId": "ETH",
  "amount": "0.5",
  "source": { "type": "WALLET_POOL", "id": "588af612-c2e6-491a-8bb4-86c85868f42f" },
  "destination": {
    "type": "ONE_TIME_ADDRESS",
    "oneTimeAddress": { "address": "0xRecipientAddress" }
  }
}
```

`source.id` is the pool ID: the `id` returned when you created the pool tag in Step 1, not a vault account ID. Everything other than `source` behaves as it does for a vault-account source, including the standard minimal response.

**Response**

```json theme={"system"}
{
  "id": "f1375359-967d-4b09-a588-f0be8228356c",
  "status": "SUBMITTED"
}
```

## Identify the member that sent

This is optional, and sending from a pool does not depend on it. When you want to know which member Fireblocks selected, for reconciliation or your own reporting, read it back from `GET /v1/transactions/{txId}` or from the transaction webhook. Both carry the same fields.

**Response**

```jsonc theme={"system"}
{
  "id": "f1375359-967d-4b09-a588-f0be8228356c",
  "assetId": "USDT_ERC20",
  "source": {
    "id": "10174",                 // the member Fireblocks selected
    "type": "VAULT_ACCOUNT",       // rewritten from WALLET_POOL after resolution
    "name": "Withdrawal 1",
    "tags": [
      { "id": "588af612-c2e6-491a-8bb4-86c85868f42f", "label": "Withdrawal Vaults" }
    ]
  },
  "status": "COMPLETED",
  "note": "Wallet pool tx: 0.01 USDT_ERC20 from Withdrawal Vaults pool to Deposit 3",
  "extraParameters": {
    "walletPoolId": "588af612-c2e6-491a-8bb4-86c85868f42f"
  }
}
```

| Field                          | Notes                                                                                                                                 |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `source.id`                    | The member vault account Fireblocks selected for this transaction.                                                                    |
| `extraParameters.walletPoolId` | The pool ID. Reconcile on this. It survives a rename and survives deletion of the pool, so historical transactions stay attributable. |
| `note`                         | Auto-populated. Readable, but the format is not a contract; do not parse it.                                                          |

<Warning>
  `GET /v1/transactions?sourceType=WALLET_POOL` does not filter. The parameter is accepted, but the result set is identical to an unfiltered query, so it returns non-pool transactions as well. Passing the pool ID as `sourceId` returns nothing. Select client-side on `extraParameters.walletPoolId`, which is the only reliable marker.
</Warning>

## How Fireblocks selects a member

Five stages. Only the first two can reject the transaction.

1. **Asset filter.** Members with no wallet for the requested asset are dropped. If none remain, the transaction is rejected.
2. **Balance filter.** Each remaining member is checked individually for `available >= amount`. If none passes, the transaction is rejected for insufficient balance. This runs before any health lookup.
3. **Fee preference.** Members that can also cover the native-asset fee are preferred. If none can, all funded members stay in play and the transaction still proceeds. Skipped where Fireblocks cannot estimate a fee for the chain.
4. **Health tiers.** Account Traffic Control reports each candidate as `HEALTHY`, `DEGRADED`, or `BLOCKED`. Fireblocks takes the best non-empty tier. A pool whose every member is `BLOCKED` still sends, so health affects ordering, not eligibility.
5. **Round-robin** within that tier. The counter is keyed on tenant, pool, and **base asset**, so assets sharing a base asset share one counter. It advances only after the transaction is created, so a failed create does not burn a slot.

<Warning>
  When Account Traffic Control returns nothing, including on every non-EVM chain, stages 4 and 5 are replaced by balance scoring: sort by locked ascending then available descending, and take the first. That is deterministic, not rotating. It returns the same member on every transaction until balances shift, so on non-EVM chains a pool delivers neither congestion avoidance nor address rotation.
</Warning>

## Limitations

* **No aggregated balance.** One member must cover the full amount alone. A pool of ten accounts holding 1 ETH each cannot send 5 ETH; it is rejected for insufficient balance. Size members against your largest single transaction, not your aggregate volume.
* **Gas funding is not enforced.** A member that cannot cover the fee is deprioritized, not excluded, so it can still be selected and produce a transaction that stalls. Alert on the minimum native balance across members.
* **Health-aware routing is EVM only.** Non-EVM chains use the deterministic fallback above.
* **Rotation is not a strict sequence.** State is per-node and in memory, and removing a member resets the pointer. Never build reconciliation or nonce prediction on a fixed member order.
* **Shared members share health.** A vault account can belong to several pools, and volume driven through one affects how another sees it.
* **No rebalancing.** Fireblocks does not move funds between members. Drive your own rebalancing from the smallest member's balance, not the pool total.
* **Deleting a pool requires it to be empty.** Detach every member and clear that approval first. Renaming and deleting both require approval.

## Troubleshooting

| Symptom                                                       | Cause and fix                                                                                                                                        |
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rejected for insufficient balance while the pool holds plenty | Balances are not summed. Top up a single member above your largest transaction.                                                                      |
| Rejected: asset not found                                     | No member holds a wallet for the asset. Activate it on the members and fund it.                                                                      |
| Rejected: no vault accounts for the pool                      | The attachment approval has not completed, or it expired. Check the request state.                                                                   |
| Blocked by Policy, intermittently                             | A rule scopes sources to vault account IDs. Rewrite it against the pool. See Step 4.                                                                 |
| Sent from a member that could not pay gas                     | Fee coverage is a preference. Keep every member funded with native asset.                                                                            |
| Error `1904` on fee estimation                                | Pool sources are unsupported. Estimate against a member vault account.                                                                               |
| The same member every time                                    | Expected on non-EVM chains and whenever health data is unavailable. On EVM, check whether the other members are failing the balance or asset filter. |
| Pool name rejected as in use                                  | Names collide with protected tags too. [List all tags](/docs/tags#step-4-list-update-and-delete-tags) when checking availability, not just pools.    |

## Related

* [Wallet Pools](https://support.fireblocks.io/hc/en-us/articles/27776980063260-Wallet-Pools) for the Console walkthroughs, supported chains, and approval behavior
* [Tags](/docs/tags) for the tag lifecycle, attachment buckets, and approval states a pool inherits
* [Set Policies](/docs/set-transaction-authorization-policy) for writing the rule that permits the pool
* [Work with Gas Station](/docs/work-with-gas-station) for tank thresholds and auto-fueling
* [Create transactions](/reference/create-transactions) for the full transaction request reference
