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

# Concurrent swaps

> Run several swaps at once from one account by preparing, coordinating, and maintaining the token records that parallel and high-frequency strategies spend, with the TypeScript SDK, Python SDK, or CLI.

This page helps you run several swaps at once from one account, and keep that account ready for the next batch, with the TypeScript SDK, Python SDK, or CLI.

## Private token inventory basics

Your confidential balance is a set of records. Each record holds an amount you can spend in one transaction, and a swap spends exactly one of them. The swap takes the input amount from that record and returns the rest in a new change record. How many swaps you can run at once depends on how many records each cover one trade, not on your total balance. An account holding 4 USDCx in one record can run one 1 USDCx swap at a time. The same 4 USDCx in four records can run four.

![A swap spends one input record, sends 1 USDCx to the pool, and returns a 3 USDCx change record. The claim returns an ETH output record and, on a partial fill, a refund record. One 4 USDCx record funds one swap at a time while three wait; four 1 USDCx records fund four swaps at once.](https://mintcdn.com/shield-swap-documentation/aE8oDYxo1hhEjFkj/images/swap-record-flow.svg?fit=max\&auto=format\&n=aE8oDYxo1hhEjFkj\&q=85\&s=e9956a308f97b4dfe24323eaf6f2a3ea)

Two swaps that pick the same record cannot both settle. The network drops the second transaction when it verifies the spend, and your client sees a confirmation wait for a transaction that never reaches a block. Record coordination prevents this: each swap reserves its record in a shared journal before proving, and a swap that loses a record moves on to the next one. Reservations are off by default in the SDKs and the CLI. This page enables them.

Inventory also changes while you trade. Every accepted swap returns a change record, and every claim returns an output record and any refund. Over time an account accumulates many small records, and a trade can fail for lack of one covering record while the total balance is more than enough. Joining and splitting records restores the layout your strategy needs.

The pool, the amounts, the route, and the timing of each swap are public. Join and split transactions are public as well, including the program and function they call. The number and size of your records, and which record funded which swap, stay out of the public market data. See [Public data](https://shield.fi/docs/confidentiality/public-data.md).

## Plan your inventory

**Prerequisites:** Your mainnet account and trading tools from [Setup](https://shield.fi/docs/trading/configure.md). Record coordination and inventory maintenance need the latest SDK and CLI releases, so upgrade the packages from Setup before using this page. The SQLite journal used below requires Node.js 22.13 or later.

Decide three things before preparing records.

**How many records.** One record per swap you want in flight, plus spares. A record spent by a swap is unavailable until that swap is accepted and the record scanner shows its change record. A record returned by a claim is unavailable until the scanner shows it. Keeping one spare lot for each parallel slot lets the next batch start without waiting on that lag.

**How large each record is.** A record must cover the swap's input amount. Equal lots suit a strategy that trades a fixed size. A mix of sizes suits a strategy that trades varying sizes, at the cost of larger change records. Amounts are integers in the token's base units; the SDKs convert from decimal units using the token's decimals, and the CLI takes base units directly.

**Which asset the records belong to.** Wrapped assets are spent from the underlying token program, so the inventory you prepare belongs to that underlying program, not to the pool's wrapper. The SDKs resolve this for you from the token symbol. For the CLI, read the token's `underlying_program` from the [token list](https://shield.fi/docs/api-reference/tokens/list-tokens.md).

For four parallel 1 USDCx swaps with one spare lot per slot, the target is eight records of at least 1 USDCx, which needs at least 8 USDCx in the account:

| Input | Value |
| - | - |
| Swaps in flight at once | 4 |
| Spare lots for the next batch | 4 |
| Target record count | 8 |
| Minimum record amount | 1 USDCx, the per-trade input |
| Balance required | At least 8 USDCx across all records |

Concurrent swaps from one account share four resources:

| Resource | Collision | How it is coordinated |
| - | - | - |
| Input record | Two swaps spend one record; the network drops the second. | The record journal reserves records before proving and reselects on conflict. |
| Confidential address | Two swaps derive the same address; the second is rejected at finalize. | The TypeScript identity store or the Python journal issues each swap its own address. Run one trading process per account and network. |
| Swap nonce | Two swaps share one swap ID. | Each SDK draws a random nonce for every swap. No coordination is needed. |
| Pool state | An earlier swap moves the price past a later swap's limit or below its minimum output. | None. Each swap enforces its own minimum output and can fail on its own. |

By default both SDKs and the CLI pay network fees through the delegated prover, so fees do not consume your records. If you configure an account that pays its own fees from confidential credits, those credits records are inventory too, and each concurrent swap needs its own. Prepare them with the same tools, using `credits.aleo` as the asset.

### Example plan

Suppose the account holds 10 USDCx as three records of 7, 2.5, and 0.5 USDCx, and you want four records of at least 1 USDCx with equal lots. The 0.5 USDCx record is below the minimum, so the planner joins all three into one record and then splits it. The plan below is the SDK planner's output for that layout:

![Three USDCx records of 7, 2.5, and 0.5 are joined into one 10 USDCx record through a helper program. Three splits then cut it into four 2.5 USDCx lots, each split returning one lot and a remainder for the next.](https://mintcdn.com/shield-swap-documentation/aE8oDYxo1hhEjFkj/images/swap-inventory-plan.svg?fit=max\&auto=format\&n=aE8oDYxo1hhEjFkj\&q=85\&s=ae53e37bd03c6c4dd042d83ef791cd24)

| Step | Operation | Inputs | Outputs |
| - | - | - | - |
| 1 | Join through `aj_usdcx_stablecoin_2_10.aleo` | 7, 2.5, and 0.5 USDCx | 10 USDCx |
| 2 | Split | 10 USDCx | 2.5 and 7.5 USDCx |
| 3 | Split | 7.5 USDCx | 2.5 and 5 USDCx |
| 4 | Split | 5 USDCx | 2.5 and 2.5 USDCx |

The plan is four transactions, run one at a time because each split spends the previous remainder. Each step waits for its outputs to appear in the record scanner before the next begins. USDCx joins and splits have no intrinsic deduction, so the four lots sum to the original 10 USDCx; only network fees apply.

The distribution changes what the planner does with records that already fit. If the same account instead held 6, 2, 1, and 1 USDCx, a `preserve` target of four records at 1 USDCx is already met and the plan has no steps. A `balanced` target with zero tolerance joins all four and splits the result into four 2.5 USDCx lots, the same four transactions as above. Choose `preserve` when any covering record will do and `balanced` when every trade should spend the same size.

## Setup a client with resource stores

**Prerequisites:** Your mainnet account from [Setup](https://shield.fi/docs/trading/configure.md) and its private key in `SHIELD_SWAP_PRIVATE_KEY`. Reuse the recovery directory from [optional storage](https://shield.fi/docs/trading/configure.md#3-configure-optional-storage) so the swap identities saved there remain available.

The client needs two stores. The record journal holds reservations and prepared transactions; the identity store or swap journal holds the data needed to claim. Keep both files between sessions, keep them out of logs and source control, and run one trading process per account and network. A SQLite journal coordinates records between a trading process and a maintenance process on the same machine, but identity allocation stays within one process.

**TypeScript**

Install the packages in an ES module application using Node.js 22.13 or later. The CLI package supplies the SQLite journal:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
npm install @provablehq/shield-swap-sdk@latest @provablehq/veil-aleo-sdk@latest @provablehq/shield-swap-cli@latest
```

Build the client with `recordManagement` enabled. `waitForSync` makes record reads wait until the scanner reports it has caught up, so a lagging scan does not plan needless joins:

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
import { loadNetwork } from "@provablehq/veil-aleo-sdk"
import { shieldSwapActions } from "@provablehq/shield-swap-sdk"
import { swapFileStore } from "@provablehq/shield-swap-sdk/node"
import { sqliteRecordInventoryStore } from "@provablehq/shield-swap-cli/storage"

const privateKey = process.env.SHIELD_SWAP_PRIVATE_KEY
if (!privateKey) throw new Error("Set SHIELD_SWAP_PRIVATE_KEY")
const aleo = await loadNetwork("mainnet")
const address = aleo.privateKeyToAccount(privateKey).address
const directory = `.shield-swap/mainnet/${address}`
const store = await sqliteRecordInventoryStore(`${directory}/inventory.sqlite`)
const identities = swapFileStore(`${directory}/swaps.json`)
const { walletClient } = aleo.createAleoClient({
  privateKey,
  records: aleo.createRemoteScanner({ waitForSync: true }),
  confirmationTimeout: 600_000,
  recordManagement: { enabled: true, store, chainId: "aleo:mainnet" },
})
const client = walletClient.extend(
  shieldSwapActions({ api: {}, blindedIdentities: identities }),
)
await client.authenticateShieldSwap()
```

`client` now reserves a record for every swap and inventory operation it submits. Call `store.close()` when the application shuts down, after every call has settled. For a script that runs to completion with nothing else trading for the account, `memoryRecordInventoryStore()` from `@provablehq/veil-core` works without SQLite, but its reservations are lost when the process exits.

**Python**

Install the SDK in a Python 3.11 or later environment:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
python -m pip install --upgrade shield-swap-sdk
```

Build the `Aleo` client with `record_management` enabled, then attach a swap journal to the trading client:

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
import os
from pathlib import Path
from aleo import Aleo, HTTPProvider, mainnet
from aleo.inventory import RecordManagementConfig, SqliteRecordInventoryStore
from aleo_shield_swap import Journal, ShieldSwap

key = mainnet.PrivateKey.from_string(os.environ["SHIELD_SWAP_PRIVATE_KEY"])
directory = Path(".shield-swap/python/mainnet") / str(key.address)
directory.mkdir(parents=True, exist_ok=True)
store = SqliteRecordInventoryStore(directory / "inventory.sqlite")
aleo = Aleo(
    HTTPProvider("https://edge.provable.com/api", network="mainnet"),
    record_management=RecordManagementConfig(enabled=True, store=store, chain_id="aleo:mainnet"),
)
account = aleo.account.from_private_key(key)
aleo.default_account = account
registration = aleo.records.register(account)
if not registration["ok"]:
    raise RuntimeError(f"Record scanner registration failed: {registration['error']}")
dex = ShieldSwap(aleo)
dex.journal = Journal(directory / "journal.jsonl")
dex.api.authenticate(str(account.address), lambda message: str(key.sign(message.encode())))
```

`dex` now reserves a record for every swap it submits, and `aleo.inventory` shares the same journal. `JsonFileRecordInventoryStore` is an inspectable alternative to SQLite; the default `MemoryRecordInventoryStore` coordinates threads in one process and is lost on exit. Threads are the supported way to run concurrent swaps in Python. `AsyncShieldSwap` has no journal, so two concurrent async swaps can derive the same confidential address.

**CLI**

Install the CLI with Node.js 22.13 or later and set up your mainnet account:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
npm install -g @provablehq/shield-swap-cli@latest
shield-swap setup --network mainnet
```

The `inventory` commands always use a SQLite journal, defaulting to `.veil/inventory.sqlite` in the working directory. Trading commands share that journal only when `VEIL_INVENTORY_DB` names the same file:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
export VEIL_INVENTORY_DB=.veil/inventory.sqlite
```

With the variable set, `shield-swap swap` reserves its record before proving and the inventory commands exclude records held by a swap in flight. Run CLI trading commands one at a time for an account; each command allocates confidential addresses within its own process.

## 1. Inspect the records

**Prerequisites:** Your coordinated mainnet `client` for TypeScript, `dex` and `aleo` for Python, or the CLI configuration from [Setup a client with resource stores](#setup-a-client-with-resource-stores).

Read how many USDCx records you hold, their sizes, and which ones another operation has reserved. These reads submit nothing.

**TypeScript**

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
const token = await client.tokenData("USDCx")
if (!token.underlyingProgram) throw new Error("USDCx has no record program")
const asset = { program: token.underlyingProgram }
const inventory = await client.getRecordInventory({ asset })

console.log(`available records: ${inventory.available.length}`)
console.log(`reserved records: ${inventory.reserved.length}`)
console.log(`available balance: ${inventory.balance} base units`)
console.log(inventory.available.map((record) => record.amount))
```

`inventory.available` lists spendable records that no journal entry holds; `inventory.reserved` lists records held by a swap or maintenance step in flight. Each entry has an `id`, the record nonce, and an `amount` as a base-unit `bigint`. `inventory.balance` sums the available amounts.

**Python**

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
from aleo.inventory import RecordAsset

token = dex.api.get_token("USDCx")
if not token.underlying_program:
    raise RuntimeError("USDCx has no record program")
asset = RecordAsset(token.underlying_program)
snapshot = aleo.inventory.get_record_inventory(asset)

print(f"available records: {len(snapshot.available)}")
print(f"reserved records: {len(snapshot.reserved)}")
print(f"available balance: {snapshot.balance} base units")
print([record.amount for record in snapshot.available])
```

`snapshot.available` lists spendable records that no journal entry holds; `snapshot.reserved` lists records held by a swap or maintenance step in flight. Each entry has an `id`, the record nonce, and an integer base-unit `amount`. `snapshot.balance` sums the available amounts.

**CLI**

Replace `TOKEN_PROGRAM` with the token's `underlying_program` from the [token list](https://shield.fi/docs/api-reference/tokens/list-tokens.md):

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap inventory inspect --asset TOKEN_PROGRAM --network mainnet --json
```

The result lists `available` and `reserved` records with their `id` and base-unit `amount`, plus the available `balance`. The `balances` command reports totals only and cannot show whether one record covers a trade.

An account with 4 USDCx available in one record reports one available record. It can fund one 1 USDCx swap at a time until the next step reshapes it.

## 2. Prepare one record per trade

**Prerequisites:** The `token` and `asset` from [Inspect the records](#1-inspect-the-records), and the coordinated client from [Setup a client with resource stores](#setup-a-client-with-resource-stores). The account needs USDCx records totaling at least **4 USDCx** for this example.

Plan the joins and splits that leave the account with four records of at least 1 USDCx each, review the plan, then execute it. A `balanced` distribution with zero tolerance spreads the entire available balance into equal lots. A `preserve` distribution keeps records that already cover the minimum and changes as little as possible, which suits a strategy that trades varying sizes.

| Placeholder or option | Meaning |
| - | - |
| `records` | Target record count, from 1 through 1000. |
| `minRecordAmount` / `min_record_amount` | Smallest acceptable record in base units. Defaults to 1. |
| `distribution` | `preserve` (default) or `balanced`. |
| `toleranceBps` / `tolerance_bps` | Allowed deviation from equal lots under `balanced`, from 0 through 10000. Defaults to 1000. |
| `maxTransactions` / `max_transactions` | Most operations one plan may contain. Defaults to 100. |

Planning reads the scanner and the token program and submits nothing. Execution runs one transaction at a time: each step reserves its inputs, proves, submits, waits for acceptance, and then waits until the scanner shows the step's exact outputs before the next step spends them. Two records join through the token program's own `join` function; larger joins use deployed helper programs that take up to 16 records for credits, USDCx, and USAD, or 15 for other tokens. A credits split also deducts 10,000 microcredits; token joins and splits have no intrinsic deduction.

> [!WARNING]
>
> **Prepare records before you quote.** Joins and splits each take a confirmation, and a TypeScript quote expires 60 seconds after it is requested. Request the swap quote after maintenance finishes.

**TypeScript**

#### Plan

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
import { parseUnits } from "@provablehq/shield-swap-sdk"

const TRADES = 4
const lot = parseUnits("1", token.decimals) // 1 USDCx in base units
const plan = await client.planInventory({
  token: token.id,
  target: { records: TRADES, minRecordAmount: lot, distribution: "balanced", toleranceBps: 0 },
  maxTransactions: 24,
})
console.log(`operations: ${plan.steps.length}`)
console.log(`credits deduction: ${plan.deduction} microcredits`)
console.table(plan.steps.map((step) => ({ operation: step.kind, inputs: step.inputs.length, outputs: step.outputs.length })))
```

`plan.steps` lists each join or split with its inputs and predicted outputs. A plan for an account that already holds four covering records has no steps.

#### Execute

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
const maintenance = await client.rebalanceRecordInventory({
  plan, timeoutMs: 600_000, pollIntervalMs: 3_000,
})
if (maintenance.status !== "complete") {
  throw new Error(`Inventory preparation interrupted: ${maintenance.reason}`)
}
console.log(`transactions: ${maintenance.transactionIds.length}`)
```

`timeoutMs` bounds each step's confirmation and scanner wait. `maintenance.transactionIds` lists every submitted transaction. An `interrupted` result keeps the steps already accepted; [reconcile](#4-keep-the-account-ready-between-batches) before planning again. `client.rebalanceInventory({ token, target })` plans and executes in one call when you do not need to review the plan first.

**Python**

#### Plan

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
from decimal import Decimal
from aleo.inventory import InventoryTarget

TRADES = 4
lot = int(Decimal("1").scaleb(token.decimals))  # 1 USDCx in base units
target = InventoryTarget(records=TRADES, min_record_amount=lot,
                         distribution="balanced", tolerance_bps=0)
plan = dex.plan_inventory("USDCx", target, max_transactions=24)
print(f"operations: {len(plan.steps)}")
print(f"credits deduction: {plan.deduction} microcredits")
for step in plan.steps:
    print(f"{step.kind}: {len(step.inputs)} inputs, {len(step.outputs)} outputs")
```

`plan.steps` lists each join or split with its inputs and predicted outputs. A plan for an account that already holds four covering records has no steps.

#### Execute

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
result = aleo.inventory.rebalance_record_inventory(plan, timeout=600.0, poll_interval=3.0)
if result.status != "complete":
    raise RuntimeError(f"Inventory preparation interrupted: {result.reason}")
print(f"transactions: {len(result.transaction_ids)}")
```

`timeout` bounds each step's confirmation and scanner wait, in seconds. `result.transaction_ids` lists every submitted transaction. An `interrupted` result keeps the steps already accepted; [reconcile](#4-keep-the-account-ready-between-batches) before planning again. `dex.rebalance_inventory("USDCx", target)` plans and executes in one call when you do not need to review the plan first.

**CLI**

#### Plan

Replace `TOKEN_PROGRAM` with the token's `underlying_program` and `LOT_BASE_UNITS` with 1 USDCx expressed in base units using the token's `decimals` from the [token list](https://shield.fi/docs/api-reference/tokens/list-tokens.md). Write the target to `inventory.json`:

```json theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
{
  "network": "mainnet",
  "database": ".veil/inventory.sqlite",
  "useFeeMaster": true,
  "maxTransactions": 24,
  "policies": [
    {
      "asset": { "program": "TOKEN_PROGRAM" },
      "target": {
        "records": 4,
        "minRecordAmount": "LOT_BASE_UNITS",
        "distribution": "balanced",
        "toleranceBps": 0
      }
    }
  ]
}
```

`useFeeMaster: true` has the delegated prover pay fees, as the trading commands do; without it the account pays public fees from its own ALEO. Print the plan without signing:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap inventory plan --config inventory.json --json
```

A one-off plan without a config file takes `--asset TOKEN_PROGRAM --records 4`, with the default minimum of one base unit and the `preserve` distribution.

#### Execute

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap inventory rebalance --config inventory.json --execute
```

Without `--execute`, `rebalance` prints the plan and submits nothing. The result reports each policy's status and the submitted transaction IDs. An `interrupted` status keeps the steps already accepted; check `inventory status` before planning again.

Repeat [Inspect the records](#1-inspect-the-records) after execution. The account now reports four available USDCx records.

## 3. Submit swaps in parallel

**Prerequisites:** Four available USDCx records from [Prepare one record per trade](#2-prepare-one-record-per-trade) and the coordinated client from [Setup a client with resource stores](#setup-a-client-with-resource-stores).

These examples submit **four 1 USDCx swaps for ETH on mainnet** at once, then claim each output. Each swap selects its own record from one scan, smallest sufficient first, and reserves it before proving. A swap whose record another swap reserved first moves to the next candidate; a swap that finds no free candidate fails before proving and spends nothing. Proving, submission, and confirmation errors are not retried. Every swap and claim is its own transaction, so one failure leaves the others running.

> [!WARNING]
>
> **Submit only when you're ready to trade on mainnet.** Each call spends a record. Running the batch again starts four more swaps.

**TypeScript**

Leave `tokenRecord` unset so each swap can reselect on conflict. Sharing one quote fixes the same minimum output for every trade; a quote does not reserve pool liquidity, so a trade can still fail when the price moves.

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
const quote = await client.quote({ from: "USDCx", to: "ETH", amountIn: "1", slippageBps: 50 })
const swaps = await Promise.allSettled(
  Array.from({ length: TRADES }, () => client.swap({ quote })),
)
const handles = swaps.flatMap((result) => result.status === "fulfilled" ? [result.value] : [])
const claims = await Promise.allSettled(handles.map(async (handle) => {
  await client.waitForSwapOutput({ handle, timeout: 600_000, pollingInterval: 3_000 })
  return client.claimSwapOutput({ handle })
}))

for (const [index, result] of swaps.entries()) {
  if (result.status === "rejected") console.error(`swap ${index + 1} failed: ${result.reason}`)
}
for (const [index, result] of claims.entries()) {
  const handle = handles[index]!
  if (result.status === "fulfilled") {
    console.log(`${handle.transactionId}: received ${result.value.amountOut} ETH base units, refunded ${result.value.amountRemaining} USDCx base units`)
  } else {
    console.error(`claim for ${handle.transactionId} needs recovery: ${result.reason}`)
  }
}
```

`Promise.allSettled` keeps every result. `claims[index]` belongs to `handles[index]`, not to `swaps[index]`, because a failed swap returns no handle. The identity store saves each handle as soon as its swap is submitted, so a claim that fails here can be finished later from [Swap History & Recovery](https://shield.fi/docs/trading/history-and-recovery.md).

**Python**

Run the swaps on a thread pool against the one shared `dex`. `.delegate(wait=False)` returns the handle as soon as the swap is submitted, and the journal saves it before the claim starts.

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
from concurrent.futures import ThreadPoolExecutor

quote = dex.quote(token_in="USDCx", token_out="ETH", amount_in="1", slippage_bps=50)

def trade(index):
    try:
        handle = dex.swap(quote).delegate(wait=False)
    except Exception as error:
        return index, None, None, error
    try:
        claim = dex.claim_swap_output(handle, timeout=600.0).delegate(wait=True)
        return index, handle, claim, None
    except Exception as error:
        return index, handle, None, error

with ThreadPoolExecutor(max_workers=TRADES) as pool:
    outcomes = list(pool.map(trade, range(1, TRADES + 1)))

for index, handle, claim, error in outcomes:
    if claim is not None:
        print(f"{handle.transaction_id}: received {claim.amount_out} ETH base units, "
              f"refunded {claim.amount_remaining} USDCx base units")
    elif handle is not None:
        print(f"claim for {handle.transaction_id} needs recovery: {error}")
    else:
        print(f"swap {index} failed: {error}")
```

Each outcome is reported on its own so a failed trade never hides a successful claim. The journal keeps every submitted handle, so a claim that fails here can be finished later with `dex.collect_all()` or from [Swap History & Recovery](https://shield.fi/docs/trading/history-and-recovery.md). `dex.swap_many(...)` is a sequential alternative that waits for each change record before the next swap.

**CLI**

The CLI runs concurrent swaps only when each leg sells a different token. It cannot run several swaps that sell the same token at once, even with `VEIL_INVENTORY_DB` set; use the TypeScript or Python example for that. This example sells 1 USDCx for ETH and 0.001 ETH for USDCx in one batch:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap swap-concurrent --swap USDCx:ETH:1 --swap ETH:USDCx:0.001 --slippage 50 --network mainnet --execute
```

Every leg is quoted and planned before any is submitted. The command then submits the legs together and claims each output in turn. Omit `--execute` to preview, and add `--no-claim` to leave the claims for `shield-swap history`.

After the claims are accepted, [check the result](https://shield.fi/docs/trading/swap.md#check-the-result) for each pair of transaction IDs. Each claim returns an ETH record and, when the trade stopped early, a USDCx refund record. Both are new inventory.

## 4. Keep the account ready between batches

**Prerequisites:** The coordinated client from [Setup a client with resource stores](#setup-a-client-with-resource-stores), reopened with the same account, network, and files after any restart.

A strategy that trades continuously repeats three steps between batches: settle the previous batch, restore the record layout, and only then quote the next batch. Do these in order, and stop other traders for the account while recovering.

### Settle the previous batch

A timeout means your client stopped waiting; the transaction can still reach the chain. Check the journal against the chain before doing anything else. A reservation whose transaction was accepted stays excluded from selection even while the scanner still reports the record as unspent, which protects you from spending a consumed record after a stale scan.

**TypeScript**

```typescript theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
const journal = await client.reconcileRecordInventory()
const unresolved = journal.filter((entry) => !["confirmed", "rejected", "cancelled"].includes(entry.status))
const pending = await client.getUnclaimedSwaps()
if (unresolved.length || pending.swaps.length || pending.unresolvable.length) {
  throw new Error("Resolve pending transactions and claims before the next batch")
}
```

`reconcileRecordInventory` marks accepted entries `confirmed` and rejected entries `rejected`, which releases their records. A transaction the node has never seen keeps its reservation; call `client.reconcileRecordInventory({ rebroadcast: true })` to resend the saved transaction with its original proof and ID. `pending.swaps` are outputs still waiting on chain; claim them with [Swap History & Recovery](https://shield.fi/docs/trading/history-and-recovery.md#claim-one-pending-output).

**Python**

```python theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
unresolved = [entry for entry in aleo.inventory.reconcile_record_inventory()
              if entry.status in ("reserved", "prepared", "submitted")]
if unresolved:
    raise RuntimeError("Resolve pending transactions before the next batch")
report = dex.collect_all()
if report.still_pending:
    raise RuntimeError("Claim the remaining outputs before the next batch")
```

`reconcile_record_inventory` marks accepted entries `confirmed` and rejected entries `rejected`, which releases their records. A transaction the node has never seen keeps its reservation; call `aleo.inventory.reconcile_record_inventory(rebroadcast=True)` to resend the saved transaction with its original proof and ID. `dex.collect_all()` claims every finalized output in the journal and is safe to rerun; `report.still_pending` lists swaps whose output is not yet available.

**CLI**

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap inventory status --network mainnet --json
shield-swap history --network mainnet --json
```

`status` lists every journal entry with its `status`, transaction ID, and any `lastRebroadcastError`. Entries still `reserved`, `prepared`, or `submitted` block further maintenance until the chain resolves them; the CLI has no command that forces a release. `history` reports unclaimed outputs in `owed`; claim them with `shield-swap history --claim`.

> [!WARNING]
>
> **Do not delete the journal to clear a pending reservation.** The journal holds the proved transaction needed to resend an uncertain submission. Deleting it can let the next batch spend a record that an earlier transaction already consumed.

### Restore the record layout

Claims and change records change the layout after every batch. Rerun [Inspect the records](#1-inspect-the-records), and when the count of covering records drops below your target, rerun [Prepare one record per trade](#2-prepare-one-record-per-trade). Maintenance and trading can share the journal: a maintenance step rescans before it runs and skips records a swap holds, and a swap skips records a maintenance step holds. Keep maintenance small and frequent, so the records a batch needs are rarely all reserved by a join.

For a service, the CLI can maintain the layout on a schedule while an SDK trader or `shield-swap swap` shares its journal through `VEIL_INVENTORY_DB`:

```bash theme={"languages":{"custom":["/languages/leo.tmLanguage.json"]}}
shield-swap inventory run --config inventory.json --execute
```

The loop checks each policy every `intervalMs` (default 30 seconds) and waits `cooldownMs` (default one minute) after acting on an asset. Add `"countRange": [3, 6]` to a policy to leave the layout alone while the record count stays in that range and every record meets the minimum, which avoids churn when a balanced policy would otherwise rebuild lots after every trade. `maxFeeMicrocredits` and `maxDailyFeeMicrocredits` cap the fees maintenance may spend. Only one maintenance loop can run for an account and chain, and pending trading reservations take priority over maintenance. The loop stays in the foreground; run it under a service manager.

### Quote and submit the next batch

Request a fresh quote after maintenance and repeat [Submit swaps in parallel](#3-submit-swaps-in-parallel). Keep the batch size at or below the number of available covering records, so no swap waits on a record that is still in flight.

## Troubleshooting

| What you see | Why it happens | What to do |
| - | - | - |
| `Record inventory changed: an input is already reserved or spent` | Every candidate record was reserved by another operation, or an explicit `tokenRecord` or `token_record` was already held. | Inspect the inventory and prepare more records, or wait for the batch in flight to settle. An explicit record is never replaced. |
| `No unspent TOKEN_PROGRAM record covers AMOUNT` | No single available record covers the input amount, even if the total balance does. | Prepare a covering record with [Prepare one record per trade](#2-prepare-one-record-per-trade). Swaps never join records on their own. |
| `Record already spent`, or a confirmation wait for a transaction the network never reports | Two uncoordinated swaps selected the same record. The network dropped the second at verification. | Enable record management on the client, or set `VEIL_INVENTORY_DB` for the CLI. Refresh records before retrying the failed trade. |
| `Inventory changed; reconcile and plan again` | A record in the plan was spent or changed amount before its step ran. | Reconcile, rerun the inspection, and build a fresh plan. Accepted steps stay accepted. |
| `Timed out confirming TRANSACTION_ID; reservation retained` | A maintenance step was submitted but its acceptance could not be established within `timeoutMs`. | Reconcile. The reservation is released once the chain reports acceptance or rejection, or resend with `rebroadcast`. |
| `Inventory execution requires recordManagement.enabled` | The client was extended with inventory actions but built without record management. | Pass `recordManagement: { enabled: true, ... }` to `createAleoClient`, or `record_management=RecordManagementConfig(enabled=True, ...)` to `Aleo`. |
| `Another inventory manager is already running for this account and chain` | A second `inventory run` or `rebalance` opened the same journal. | Stop the other process. A loop that died without releasing its lease is released automatically on the same machine. |
| `Confidential address has already been used` or `Swap ID already exists` in a rejection | Two processes allocated the same confidential address for the account. | Run one trading process per account and network. Finish the accepted swap; do not resubmit the rejected one. |
| `Wallet accounts must provide tokenRecord` | Browser wallet accounts do not reselect records. | Supply the record from the wallet's record request, or run parallel swaps from a local signer. |

## Next steps

### Recover an interrupted batch

[Swap History & Recovery](https://shield.fi/docs/trading/history-and-recovery.md) shows how to establish each swap's outcome and finish its claim without starting another trade.

### Understand record requirements

[Preparing token records](https://shield.fi/docs/trading/preparing-token-records.md) explains the record fields a swap needs and the record state machine behind the journal.

### Run packaged examples

The [TypeScript concurrent-swaps example](https://github.com/ProvableHQ/veil/tree/main/examples/shield-swap) and the [Python concurrent-swaps example](https://github.com/ProvableHQ/python-sdk/tree/master/shield-swap-sdk/examples/concurrent-swaps) prepare six balanced USDCx records and swap them for ETH at once on testnet, with a preview mode and recovery instructions. Each execution starts six new trades.


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