Bundles & Revert Protection
Advanced transaction submission with bundles, block ranges and revert protection.
eth_sendBundle
The eth_sendBundle
method allows developers to submit transactions with advanced execution controls, including block/timestamp ranges and revert protection. This feature is particularly useful for arbitrageurs and advanced users who need precise timing or want to avoid paying gas for failed transactions.
Overview
eth_sendBundle
enables you to:
- Control execution timing: Specify exactly when your transaction should execute using block numbers or timestamps
- Revert protection: Avoid paying gas fees for transactions that revert
- Atomic execution: Ensure your transaction executes in a specific window or not at all
Current Limitation: Bundles are limited to a single transaction. This limitation may be removed in future versions.
Why Use eth_sendBundle?
Traditional transaction submission can result in:
- Transactions executing at undesirable times
- Paying gas fees even when transactions revert due to changed market conditions
- Uncertainty about execution timing for time-sensitive operations
eth_sendBundle
solves these issues by providing:
- Conditional execution: Transactions only execute within your specified parameters
- Cost protection: No gas fees for reverted transactions (when revert protection is enabled)
- Precise timing: Execute transactions within specific block ranges or time windows
Method Signature
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendBundle",
"params": [
{
"txs": ["0x..."], // Array with single tx
"minBlockNumber": "0x...", // Optional: minimum block number (hex)
"maxBlockNumber": "0x...", // Optional: maximum block number (hex)
"minTimestamp": 1234567890, // Optional: minimum timestamp (number)
"maxTimestamp": 1234567899, // Optional: maximum timestamp (number)
"revertingTxHashes": ["0x..."] // Optional: transactions allowed to revert
}
]
}
Response Format
A successful eth_sendBundle
call returns:
{
"jsonrpc": "2.0",
"result": {
"bundleHash": "0xc039e46a9cf9f5d7e108642c34a53e7de969ed3f39a138282181d975ba7ffbfa"
},
"id": 1
}
Important: The bundleHash
is equivalent to the transaction hash and can be used with standard methods like eth_getTransactionReceipt
to check execution status.
Parameters
txs (required)
- Type: Array of strings
- Description: Array containing a single signed transaction
- Format: Hex-encoded signed transaction data
Important: At this time, bundles only support a single transaction per submission.
Block Number Range (optional)
- minBlockNumber: Hex-encoded string of the minimum block number
- maxBlockNumber: Hex-encoded string of the maximum block number
- Behavior: Transaction executes when
minBlockNumber ≤ current block ≤ maxBlockNumber
- Default: If not specified, bundle expires after 10 blocks from submission
Timestamp Range (optional)
- minTimestamp: JSON number representing Unix timestamp
- maxTimestamp: JSON number representing Unix timestamp
- Behavior: Transaction executes when
minTimestamp ≤ current block timestamp ≤ maxTimestamp
Important: Block number ranges and timestamp ranges are mutually exclusive. Do not specify both.
revertingTxHashes (optional)
- Type: Array of strings
- Description: Transaction hashes that are allowed to revert without failing the bundle
- Behavior: If omitted, reverted transactions will not charge gas fees
Endpoints
You can submit bundles to either endpoint:
https://mainnet.unichain.org
- Standard endpointhttps://mainnet-sequencer.unichain.org
- Provides detailed error codes for transaction receipts
Bundle Lifecycle
- Submission: Bundle is submitted and enters the mempool
- Execution Window: Bundle remains active for up to 10 blocks (or until maxBlockNumber/maxTimestamp)
- Expiration: After the execution window, bundles are dropped from the mempool
- Receipt Queries: Expired bundles return error code
-32602: the transaction was dropped from the pool
Code Examples
Basic Bundle with Block Range
const { ethers } = require('ethers');
async function sendBasicBundle() {
const provider = new ethers.JsonRpcProvider('https://mainnet.unichain.org');
const wallet = new ethers.Wallet(privateKey, provider);
// Get current block for range calculation
const currentBlock = await provider.getBlockNumber();
// Create and sign transaction
const tx = {
to: recipientAddress,
value: ethers.parseEther('1.0'),
gasLimit: 21000,
gasPrice: await provider.getFeeData().then(fees => fees.gasPrice)
};
const signedTx = await wallet.signTransaction(tx);
// Submit bundle
const bundleParams = {
txs: [signedTx],
minBlockNumber: `0x${(currentBlock + 1).toString(16)}`,
maxBlockNumber: `0x${(currentBlock + 5).toString(16)}`
};
const txHash = await provider.send('eth_sendBundle', [bundleParams]);
console.log('Bundle submitted:', txHash);
}
Bundle with Timestamp Range
async function sendTimestampBundle() {
const provider = new ethers.JsonRpcProvider('https://mainnet.unichain.org');
const wallet = new ethers.Wallet(privateKey, provider);
const currentTime = Math.floor(Date.now() / 1000);
const tx = {
to: recipientAddress,
value: ethers.parseEther('1.0'),
gasLimit: 21000,
gasPrice: await provider.getFeeData().then(fees => fees.gasPrice)
};
const signedTx = await wallet.signTransaction(tx);
const bundleParams = {
txs: [signedTx],
minTimestamp: currentTime + 60, // Execute 1 minute from now
maxTimestamp: currentTime + 300 // Must execute within 5 minutes
};
const txHash = await provider.send('eth_sendBundle', [bundleParams]);
console.log('Timestamp bundle submitted:', txHash);
}
Bundle with Revert Protection
async function sendRevertProtectedBundle() {
const provider = new ethers.JsonRpcProvider('https://mainnet.unichain.org');
const wallet = new ethers.Wallet(privateKey, provider);
// Transaction that might revert (e.g., DEX trade with slippage)
const riskyTx = {
to: dexContractAddress,
data: swapCalldata,
gasLimit: 200000,
gasPrice: await provider.getFeeData().then(fees => fees.gasPrice)
};
const signedTx = await wallet.signTransaction(riskyTx);
const bundleParams = {
txs: [signedTx],
// Omit revertingTxHashes to enable automatic revert protection
minBlockNumber: `0x${(await provider.getBlockNumber() + 1).toString(16)}`,
maxBlockNumber: `0x${(await provider.getBlockNumber() + 3).toString(16)}`
};
const txHash = await provider.send('eth_sendBundle', [bundleParams]);
console.log('Protected bundle submitted:', txHash);
}
Error Handling
Transaction Receipt Errors
When querying receipts for expired or dropped bundles:
async function checkBundleStatus(txHash) {
const sequencerProvider = new ethers.JsonRpcProvider('https://mainnet-sequencer.unichain.org');
try {
const receipt = await sequencerProvider.getTransactionReceipt(txHash);
console.log('Transaction executed:', receipt);
} catch (error) {
if (error.code === -32602) {
console.log('Bundle was dropped from the pool (likely expired)');
} else {
console.log('Other error:', error.message);
}
}
}
Common Error Scenarios
- Bundle expired: Error code
-32602
when querying receipt after 10+ blocks - Invalid range: Bundle with
minBlockNumber > maxBlockNumber
will be rejected - Past timestamps: Bundles with
maxTimestamp
in the past are immediately invalid
Best Practices
-
Use appropriate time windows: Don't make ranges too narrow (may miss execution) or too wide (unnecessary delay)
-
Monitor bundle status: Check transaction receipts to confirm execution or detect expiration
-
Handle revert protection carefully: Understand that omitting
revertingTxHashes
provides automatic revert protection -
Use sequencer endpoint for debugging: Use
https://mainnet-sequencer.unichain.org
when you need detailed error information -
Plan for expiration: Implement logic to resubmit bundles if they expire without execution
Limitations
- Single transaction per bundle: Currently limited to one transaction per bundle (temporary limitation)
- 10-block default lifetime: Bundles automatically expire after 10 blocks if no
maxBlockNumber
specified - Mutually exclusive ranges: Cannot specify both block number and timestamp ranges
- No bundle cancellation: Once submitted, bundles cannot be cancelled (they must expire)
Use Cases
Arbitrage Operations
// Execute arbitrage only if profitable within next 3 blocks
const arbitrageBundle = {
txs: [signedArbitrageTx],
minBlockNumber: `0x${(currentBlock + 1).toString(16)}`,
maxBlockNumber: `0x${(currentBlock + 3).toString(16)}`
// Revert protection prevents paying gas if opportunity disappears
};
Time-Sensitive DeFi Operations
// Execute a trade only during a specific time window
const timedTradeBundle = {
txs: [signedTradeTx],
minTimestamp: auctionStartTime,
maxTimestamp: auctionEndTime
};
MEV Protection
// Protect against MEV by constraining execution timing
const protectedBundle = {
txs: [signedTx],
minBlockNumber: `0x${targetBlock.toString(16)}`,
maxBlockNumber: `0x${targetBlock.toString(16)}` // Execute in specific block only
};
Contract Addresses
Important tokens and contract addresses on the Unichain network
EVM Equivalence
Understanding EVM equivalence in Unichain
Integrating Flashblocks
Learn how to integrate Flashblocks for lightning-fast transaction confirmations on Unichain, with preconfirmations in just 200 milliseconds.
Technical Information
Technical details about the Unichain network
Network Information
Essential information about the Unichain network
Unichain Node Snapshots
Use snapshots to quickly bootstrap Unichain nodes without full blockchain sync
RPC Calls
List of supported and unsupported RPC calls for Unichain Public RPCs
Submitting Transactions from L1
Guide to Submitting Transactions from L1
Last updated on