Documentation Index Fetch the complete documentation index at: https://docs.biconomy.io/llms.txt
Use this file to discover all available pages before exploring further.
Batched Transactions on EVM: Execute Multiple Operations
What are batched transactions?
Batched transactions combine multiple blockchain operations into a single transaction. Instead of submitting separate transactions (each with its own gas cost and confirmation), you execute everything atomically in one go.Without Batching:
TX 1: Approve USDC → Wait → Confirm → Pay gas
TX 2: Deposit to Aave → Wait → Confirm → Pay gas
TX 3: Claim rewards → Wait → Confirm → Pay gas
Total: 3 signatures, 3 gas payments, 3 confirmations
With Batching:
TX 1: [Approve + Deposit + Claim] → Single signature → Single gas payment
Total: 1 signature, 1 gas payment, atomic execution
Batching is enabled by smart contract wallets (account abstraction).
Why should I batch transactions?
Benefit Description Lower gas costs One base transaction fee instead of multiple Better UX Users sign once instead of multiple times Atomic execution All operations succeed or all fail together Faster completion No waiting between steps Reduced failure risk No partial state from incomplete sequences
Gas savings example: Scenario Without Batching With Batching Savings Approve + Swap ~140k gas ~95k gas 32% 3 token transfers ~120k gas ~75k gas 37% Approve + Deposit + Claim ~200k gas ~130k gas 35%
How do I batch transactions with Biconomy?
Using AbstractJS SDK with MEE: import { createMeeClient , toMultichainNexusAccount } from "@biconomy/abstractjs" ;
import { encodeFunctionData } from "viem" ;
const account = await toMultichainNexusAccount ({
signer: userSigner ,
chains: [ base ]
});
const meeClient = await createMeeClient ({ account });
// Batch multiple operations with MEE
const quote = await meeClient . getQuote ({
instructions: [
{
calls: [
{
to: usdcAddress ,
data: encodeFunctionData ({
abi: erc20Abi ,
functionName: "approve" ,
args: [ aavePool , amount ]
})
},
{
to: aavePool ,
data: encodeFunctionData ({
abi: aaveAbi ,
functionName: "deposit" ,
args: [ usdcAddress , amount , userAddress , 0 ]
})
},
{
to: rewardsController ,
data: encodeFunctionData ({
abi: rewardsAbi ,
functionName: "claimAllRewards" ,
args: [ assets , userAddress ]
})
}
]
}
]
});
const { hash } = await meeClient . executeQuote ({ quote });
console . log ( "All operations completed in one tx:" , hash );
MEE (Modular Execution Environment) provides all the batching capabilities of ERC-4337 bundlers and paymasters, plus cross-chain orchestration. Use MEE instead of configuring bundlers and paymasters separately.
What operations can I batch together?
You can batch any combination of EVM operations: Common batching patterns: Pattern Operations Token + Action Approve → Swap/Deposit/Stake Multi-send Multiple token transfers in one tx DeFi combo Claim → Swap → Deposit NFT batch Approve → List multiple NFTs Gaming Equip + Upgrade + Consume items DAO Delegate → Vote on multiple proposals
Example: Multi-token transfer const transfers = recipients . map (({ address , amount }) => ({
to: tokenAddress ,
data: encodeFunctionData ({
abi: erc20Abi ,
functionName: "transfer" ,
args: [ address , amount ]
})
}));
// Send to 100 recipients in ONE transaction
await smartAccount . sendTransactions ( transfers );
How does atomic execution work?
Atomic execution means all operations succeed or all fail . There’s no partial state. Why this matters: Scenario: Approve USDC → Swap USDC for ETH
Non-atomic (separate txs):
1. Approve succeeds ✅
2. Swap fails (price changed) ❌
Result: Approval is stuck, USDC still approved to router 😰
Atomic (batched):
1. [Approve + Swap] fails ❌
Result: Nothing happened, state unchanged 😌
Biconomy ensures atomicity: try {
const { hash } = await smartAccount . sendTransactions ([
{ /* operation 1 */ },
{ /* operation 2 */ },
{ /* operation 3 */ }
]);
// All succeeded
} catch ( error ) {
// ALL operations reverted, no partial state
console . log ( "Batch failed atomically" );
}
Can I batch operations that depend on each other?
Yes! Use runtime values to reference outputs from earlier operations: import { runtime } from "@biconomy/abstractjs" ;
const instructions = [
// Step 1: Swap ETH for USDC (output unknown until execution)
{
type: "intent" ,
intent: "swap" ,
params: {
from: "ETH" ,
to: "USDC" ,
amount: "1.0"
}
},
// Step 2: Use EXACT output from step 1
{
to: aavePool ,
data: encodeDeposit (
usdcAddress ,
runtime . outputOf ( 0 ), // Dynamic: uses actual swap output
userAddress
)
}
];
The runtime.outputOf(0) is resolved at execution time with the actual value from the first operation.
What's the maximum batch size?
Batch size is limited by:
Block gas limit : Total gas must fit in a block
Bundler limits : Typically 5-10M gas per UserOperation
Practical limits : More operations = higher failure risk
Guidelines: Chain Recommended Max Operations Max Gas Ethereum 10-20 3-5M gas Arbitrum 20-50 10M gas Base 20-50 10M gas Polygon 20-50 8M gas
Best practices: // Split large batches
const BATCH_SIZE = 20 ;
async function batchTransfer ( transfers ) {
const results = [];
for ( let i = 0 ; i < transfers . length ; i += BATCH_SIZE ) {
const batch = transfers . slice ( i , i + BATCH_SIZE );
const result = await smartAccount . sendTransactions ( batch );
results . push ( result );
}
return results ;
}
How do I estimate gas for batched transactions?
// Estimate gas for batch
const gasEstimate = await smartAccount . estimateGas ({
transactions: [
{ to: contract1 , data: data1 },
{ to: contract2 , data: data2 },
{ to: contract3 , data: data3 }
]
});
console . log ( "Total gas estimate:" , gasEstimate );
// Compare with individual estimates
const individual1 = await publicClient . estimateGas ({ to: contract1 , data: data1 });
const individual2 = await publicClient . estimateGas ({ to: contract2 , data: data2 });
const individual3 = await publicClient . estimateGas ({ to: contract3 , data: data3 });
const totalIndividual = individual1 + individual2 + individual3 ;
const savings = totalIndividual - gasEstimate ;
console . log ( `Gas savings from batching: ${ savings } ( ${ ( savings / totalIndividual * 100 ). toFixed ( 1 ) } %)` );
How do I handle failures in batches?
Default behavior: Atomic revert try {
await smartAccount . sendTransactions ([ op1 , op2 , op3 ]);
} catch ( error ) {
// Entire batch reverted
// Identify which operation failed from error message
console . log ( "Batch failed:" , error . message );
}
For non-critical operations, use conditional execution: const { conditions } = require ( "@biconomy/abstractjs" );
const transactions = [
// Critical: Always execute
{ to: contract1 , data: data1 },
// Optional: Only if balance exists
{
to: contract2 ,
data: data2 ,
condition: conditions . hasBalance ( tokenAddress , minAmount )
},
// Optional: Only if previous succeeded
{
to: contract3 ,
data: data3 ,
condition: conditions . previousSucceeded ()
}
];
Pre-validate operations: async function validateBatch ( transactions ) {
const simulations = await Promise . all (
transactions . map ( tx =>
publicClient . simulateContract ({
address: tx . to ,
data: tx . data
}). catch ( e => ({ error: e }))
)
);
const failures = simulations . filter ( s => s . error );
if ( failures . length > 0 ) {
console . log ( "Operations that would fail:" , failures );
return false ;
}
return true ;
}
Can I batch across multiple chains?
Yes! Cross-chain batching is possible with Biconomy’s orchestration: const crossChainBatch = {
instructions: [
// Chain 1: Ethereum
{
chainId: 1 ,
to: usdcEthereum ,
data: encodeApprove ( bridge , amount )
},
// Bridge operation
{
type: "intent" ,
intent: "bridge" ,
params: { token: "USDC" , from: 1 , to: 8453 , amount }
},
// Chain 2: Base
{
chainId: 8453 ,
to: aaveBase ,
data: encodeDeposit ( usdcBase , runtime . bridgeOutput , user )
}
]
};
// One signature covers all chains
const result = await meeClient . execute ( crossChainBatch );
See Multi-Chain Execution for details.
What are real-world batching examples?
Example 1: DeFi Yield Optimization // Claim rewards → Swap to stablecoin → Redeposit
await smartAccount . sendTransactions ([
// Claim pending rewards
{ to: farm , data: encodeClaim () },
// Approve swap
{ to: rewardToken , data: encodeApprove ( router , maxUint256 ) },
// Swap rewards to USDC
{ to: router , data: encodeSwap ( rewardToken , usdc , rewardAmount ) },
// Deposit USDC back into farm
{ to: farm , data: encodeDeposit ( usdc , usdcAmount ) }
]);
Example 2: NFT Marketplace Listing // Approve collection → List multiple NFTs
const listings = tokenIds . map ( id => ({
to: marketplace ,
data: encodeList ( collection , id , price )
}));
await smartAccount . sendTransactions ([
{ to: collection , data: encodeSetApprovalForAll ( marketplace , true ) },
... listings
]);
Example 3: Payroll Distribution // Approve total → Transfer to all employees
const totalAmount = employees . reduce (( sum , e ) => sum + e . amount , 0 n );
const transfers = employees . map (({ address , amount }) => ({
to: paymentToken ,
data: encodeTransfer ( address , amount )
}));
await smartAccount . sendTransactions ([
{ to: paymentToken , data: encodeApprove ( batchContract , totalAmount ) },
... transfers
]);
Learn more
Multi-Chain Execution Batch across multiple chains
Onchain Orchestration Coordinate complex workflows