# Batch withdraw

> Withdraw a whole single-asset balance to one address in a single transaction by bundling several transact proofs into one relayBatch.

A single [transact](/protocol/circuits/transact) proof spends at most five input [notes](/concepts/notes). A user holding more notes than that cannot empty an asset in one [withdrawal](/operations/withdraw). Batch withdraw is the answer: the SDK partitions the asset's active notes into groups of at most five, proves each group as its own withdrawal, and submits all of them in one `relayBatch` call that pays one aggregate amount to one recipient.

Nothing changes in the circuits. Every group is an independent transact proof that enforces the same conservation, root-membership, context-binding, and nullifier rules as a standalone withdrawal. The batch is a contract-and-relayer convenience for landing them together.

## How a batch withdrawal works

```mermaid
sequenceDiagram
    participant You
    participant Relayer
    participant Relay as PrivacyPoolRelay
    participant PoolVault
    participant Recipient as Recipient address

    Note over You: partitions active notes into ≤5-note groups
    You->>Relayer: aggregate batch quote (one per group, signed)
    Note over You: one transact proof per group, each bound toits signed PayoutRouting and to PrivacyPoolRelay
    You->>Relayer: N proofs + signed quote
    Relayer->>Relay: relayBatch(proofs[], params[], noteData[][])
    loop each group
        Relay->>PoolVault: transact
        PoolVault-->>Relay: withdrawn value
    end
    Relay-->>Recipient: Σ amountOut − Σ fee
```

1.  **Select.** The SDK groups the asset's `ACTIVE` notes into single-label groups of at most five inputs, one group per transact.
2.  **Quote.** One aggregate quote is requested from the relayer, which signs a per-item `PayoutRouting` for every group. Each item's fee is what that group would cost as a standalone withdrawal, so a relayer that cherry-picked one group and submitted it alone would earn no more than its fair share.
3.  **Prove.** One transact proof per group, each bound to its own signed routing and to [PrivacyPoolRelay](/protocol/contracts/privacy-pool-relay) as processor. All groups prove against the same pre-batch state root.
4.  **Submit.** The relayer calls `relayBatch`, which loops `PoolVault.transact` over the items and then pays the summed net amount to the recipient and the summed fees to the fee recipient in one distribution.

The whole batch is atomic: if any item reverts, nothing is spent.

## How to batch-withdraw

The relayed path is quote-first and mirrors `prepareWithdraw` / `relayWithdraw`:

```ts
// 1. Prepare: selects notes, fetches one aggregate quote, builds one proof per group.
//    Local note state is not touched yet.
const batch = await session.prepareBatchWithdraw({
    tokenId: usdcAddress,             // the asset whose whole active balance leaves
    recipientAddress: publicDestination,
});

// 2. Relay: submits one relayBatch through the quoting relayer, then marks notes spent
//    once the receipt's Transacted logs confirm every nullifier.
const receipt = await session.relayBatchWithdraw(batch);
console.log("batch tx:", receipt.txHash);
```

If you would rather pay gas yourself, the self-submit path proves zero-fee items and submits `relayBatch` from your own wallet, with no relayer and no quote:

```ts
const receipt = await session.selfSubmitBatchWithdraw({
    tokenId: usdcAddress,
    recipientAddress: publicDestination,
    relayContractAddress: privacyPoolRelay,   // the relayBatch target
});
```

`prepareSelfSubmitBatch` and `executeSelfSubmitBatch` are the split halves of that call, returning a ready-to-sign `relayBatch` transaction (`to`, `callData`, `value`) plus the note-state deltas. As with any self-submitted transaction, your wallet is then publicly visible as the submitter.

## Constraints

-   **Full balance, one asset, one recipient.** `prepareBatchWithdraw` takes no amount: it withdraws the asset's entire active balance. On-chain, `relayBatch` requires every item to share the same `tokenIdOut`, `recipient`, and `feeRecipient`, and rejects any item with a zero `amountOut`.
-   **At most 10 groups.** `MAX_BATCH = 10` on both the contract and the SDK, so one batch drains up to 50 input notes. The cap is coupled to the pool: the relay's constructor requires `MAX_BATCH < PoolVault.rootHistorySize()` (10 &lt; 16 on the deployed configuration), because every item anchors to the same pre-batch state root and each transact advances the root buffer by one slot. A longer batch would evict its own snapshot and revert.
-   **Withdrawals only.** `relayBatch` carves each item's fee from its public output, so a zero-output private transfer cannot be a batch item. Transfers remain one transact each.
-   **Not for yield-to-underlying.** A withdrawal that unwraps ppUSDC to USDC binds [PPRouter](/protocol/contracts/pp-router) as its processor, and a batch item must bind PrivacyPoolRelay. Batch yield notes as ppUSDC shares instead, then unwrap; see [Yield shares](/concepts/yield-shares).
-   **Fees are per item.** Each item's fee is `min(maxRelayFee, relayCost + (feeBps > 0 ? amountOut · feeBps / 10000 : flatFee))`, where a `maxRelayFee` of `0` means no cap. The SDK recomputes every fee from the relayer's signed parameters and rejects a mismatch (`BatchRoutingInvalid`). A nonzero `maxRelayFee` below `relayCost` makes the asset unrelayable (`AssetNotRelayable`); a fee larger than its own item's output is infeasible (`InfeasibleBatchFee`).
-   **Verify before you persist.** `relayBatchWithdraw` and `executeSelfSubmitBatch` parse the confirmed receipt's `Transacted` logs and only mark notes `SPENT` when every expected nullifier appears. If the receipt does not confirm them, the SDK warns and leaves local state alone rather than marking a terminal status on unverified evidence; the next `discoverNotes()` reconciles.

## Availability

`relayBatch` is a `PrivacyPoolRelay` function, so it exists only on relay contracts deployed after it was added. The production relay on [mainnet, BNB Chain, and Citrea](/deployments) answers `MAX_BATCH()` with 10, so batch withdrawal is available there. The Sepolia V9 relay in the [Sepolia page](/deployments/sepolia) predates it and exposes neither `relayBatch` nor `MAX_BATCH()`. Before offering batch withdrawal, read the relayer's `/v1/details` and confirm the `contracts.relay` it publishes answers `MAX_BATCH()`; a relayer that does not support batches returns an error from its batch quote route.

## Behind the scenes

SDK call `session.prepareBatchWithdraw(...)` then `session.relayBatchWithdraw(...)`; or `session.selfSubmitBatchWithdraw(...)`. Lower-level pieces are exported for integrators building outside a session: `selectNotesForFullWithdraw`, `computeBatchItemFee` / `computeBatchFees`, and `MAX_BATCH`, the last three also on the dependency-light `@privacy-pools-v2/sdk/batch-fee` subpath.
Contract method [`PrivacyPoolRelay`](/protocol/contracts/privacy-pool-relay)`.relayBatch(proofs[], transactParams[], noteData[][])`, which calls `PoolVault.transact` once per item and emits one `RelayedBatch`.
Relayer route `POST /v1/quote/evm/{chainId}/batch` for the aggregate quote; the relayer signs a `PayoutRouting` per item and submits the single `relayBatch`.
Circuit One [`transact_NxM`](/protocol/circuits/transact) proof per group, N ≤ 5, each a withdrawal with a non-zero `amountOut`.

## What's next

-   [Withdraw](withdraw): the single-proof path for five notes or fewer.
-   [Yield withdraw](yield-withdraw): unwrapping a yield note to its underlying, which cannot be batched.
-   [Manage notes](manage-notes): confirming the spent state after the receipt lands.
