Skip to main content
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:.
Batch UserOperations require Account Abstraction to be enabled for your organization and client.

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

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.
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:
FieldTypeRequiredDescription
feeTokenstringYesToken symbol charged for reimbursement (e.g. 'USDC')
feeRecipientstringYesEVM address that receives the reimbursement
convertGasToFeeAmount(gasCostWei: bigint) => string | Promise<string>YesPlatform-supplied conversion: native gas wei → fee-token decimal string. Portal does not perform this conversion.
bufferBpsnumberNoBasis-point margin on the gas estimate before conversion (e.g. 1000 = +10%). Defaults to 0.
placeholderAmountstringNoFee-call amount during the estimation pass. Defaults to '0.01'. Must be ≤ the wallet’s balance.
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.

Low-level primitives

If you need direct control over the build/sign/broadcast cycle, use the low-level methods:
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,
})
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.

Support

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