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