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

# Batch user operations

> Build, sign, and broadcast ERC-4337 batch UserOperations with the Web SDK.

The Web SDK exposes helpers for building, signing, and broadcasting ERC-4337 batch UserOperations in a single call. These methods are available on AA-enabled clients only and require a CAIP-2 chain ID starting with `eip155:`.

<Note>
  Batch UserOperations require [Account Abstraction](../../../resources/account-abstraction) to be enabled for your organization and client.
</Note>

## sendBatchUserOp

Builds, signs, and broadcasts a batch of token transfers as a single UserOperation. Portal's paymaster sponsors the gas; the user pays nothing in native tokens.

```typescript theme={null}
import type { SendBatchUserOpRequest } from '@portal-hq/web'

const result = await portal.sendBatchUserOp({
  chain: 'eip155:10143',
  transactions: [
    { token: 'USDC', value: '5.00', to: '0xAlice...' },
    { token: 'USDC', value: '5.00', to: '0xBob...' },
  ],
  signatureApprovalMemo: 'Send USDC to Alice and Bob', // optional
})
// result.data.userOpHash — submitted UserOperation hash (not an on-chain tx hash)
```

<Note>
  `result.data.userOpHash` is a UserOperation hash, not an on-chain transaction hash. To wait for on-chain inclusion call `portal.waitForConfirmation(result.data.userOpHash, result.metadata.chainId)`.
</Note>

## sendBatchedAssets

Like `sendBatchUserOp`, but appends a gas-reimbursement transfer to the batch. The paymaster still sponsors the gas; the reimbursement call recovers that cost from the user's smart account, in a fee token of your choice (e.g. USDC).

The method runs a **two-pass build**: the first pass estimates the gas cost of the full batch (including the reimbursement call), and the second pass builds the final batch with the real fee amount.

```typescript theme={null}
import type { SendBatchedAssetsRequest } from '@portal-hq/web'

const chain = 'eip155:10143'
const result = await portal.sendBatchedAssets({
  chain,
  transactions: [
    { token: 'USDC', value: '10.00', to: '0xAlice...' },
  ],
  gasReimbursement: {
    feeToken: 'USDC',
    feeRecipient: '0xYourPlatformWallet',
    // Portal hands you the estimated gas cost in wei and expects back
    // a fee-token amount as a decimal string. The conversion is entirely
    // yours — use any price source (oracle, internal service, API, etc.).
    convertGasToFeeAmount: async (gasCostWei: bigint) => {
      const nativeAmount = Number(gasCostWei) / 1e18
      const usdPerNative = await yourPricingLayer.getPrice(chain)
      return (nativeAmount * usdPerNative).toFixed(6)
    },
    bufferBps: 1000,        // +10% safety margin on the gas estimate
    placeholderAmount: '0.01', // used during estimation only
  },
  signatureApprovalMemo: 'Transfer + gas fee',
})
```

**`GasReimbursement` fields:**

| Field                   | Type                                                | Required | Description                                                                                                       |
| ----------------------- | --------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `feeToken`              | `string`                                            | Yes      | Token symbol charged for reimbursement (e.g. `'USDC'`)                                                            |
| `feeRecipient`          | `string`                                            | Yes      | EVM address that receives the reimbursement                                                                       |
| `convertGasToFeeAmount` | `(gasCostWei: bigint) => string \| Promise<string>` | Yes      | Platform-supplied conversion: native gas wei → fee-token decimal string. Portal does not perform this conversion. |
| `bufferBps`             | `number`                                            | No       | Basis-point margin on the gas estimate before conversion (e.g. `1000` = +10%). Defaults to `0`.                   |
| `placeholderAmount`     | `string`                                            | No       | Fee-call amount during the estimation pass. Defaults to `'0.01'`. Must be ≤ the wallet's balance.                 |

<Warning>
  `sendBatchedAssets` throws if `estimatedGasCostWei` is `0`. Some chains/providers carry no on-chain fee on the UserOperation (e.g. bundler-level sponsorship that covers all fees). On those chains use `sendBatchUserOp` instead.
</Warning>

## Low-level primitives

If you need direct control over the build/sign/broadcast cycle, use the low-level methods:

```typescript theme={null}
import { PortalCurve } from '@portal-hq/web'

// 1. Build the UserOperation
const buildResult = await portal.buildBatchedUserOp({
  chain: 'eip155:10143',
  calls: [
    { to: '0xRecipient', value: '1000000000000000', data: '0x' },
  ],
})
// buildResult.data.userOpHash    — 32-byte hex hash to sign
// buildResult.data.userOperation — JSON string to broadcast

// 2. Sign the hash yourself (strip 0x prefix before passing)
const hashToSign = buildResult.data.userOpHash.replace(/^0x/, '')
const signature = await portal.rawSign(PortalCurve.SECP256K1, hashToSign)

// 3. Broadcast
const broadcastResult = await portal.broadcastBatchedUserOp({
  chain: 'eip155:10143',
  userOperation: buildResult.data.userOperation,
  signature,
})
```

<Note>
  `buildBatchedUserOp` returns `metadata.estimatedGasCostWei` — a build-time upper bound (`totalGas × maxFeePerGas`). This value is `'0'` on chains where the UserOperation carries no on-chain fee.
</Note>

## Support

If you encounter any issues or have questions about batch UserOperations, feel free to reach out to our support team.
