In part one, we set up our "secure communication pipeline." In this part, we’re diving into the actual swap logic. Our goal is to craft a Uniswap V3 transaction that isn't just private, but is also guaranteed to go through.
1. Prepping the Swap Transaction
For this example, we’ll set up an ETH to USDC swap. To make it work in a bundle, we need to build the transaction object ahead of time without broadcasting it to the network.
Here’s what we need on the table:
- The Uniswap V3 Router address.
- A minimal ABI for the
exactInputSinglefunction. - A gas calculation that accounts for the priority fee.
The Golden Rule: In Flashbots bundles, the standard
gasPriceis swapped out formaxFeePerGasandmaxPriorityFeePerGas. It’s the priorityFee—essentially a tip to the validator—that determines if your bundle actually lands in a block.
2. Code: Building and Simulating
Let’s add a swap.ts file to the project. The real star of the show here is the .simulate() method. This is the Flashbots "killer feature"—it lets you dry-run your transaction against the current state of the blockchain without burning a single cent on gas.
import { ethers } from "ethers";
import { FlashbotsBundleRawTransaction } from "@flashbots/ethers-provider-bundle";
// Bare-bones ABI to talk to the Uniswap V3 Router
const ROUTER_ABI = [
"function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256 amountOut)"
];
const ROUTER_ADDRESS = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
export async function createAndSimulateBundle(wallet: ethers.Wallet, flashbotsProvider: any, provider: ethers.Provider) {
const block = await provider.getBlock("latest");
const nextBlockNumber = block!.number + 1;
// 1. Set up the interface and prep the transaction data
const iface = new ethers.Interface(ROUTER_ABI);
const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20-minute window
const amountIn = ethers.parseEther("0.1"); // Swapping 0.1 ETH
const params = {
tokenIn: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH
tokenOut: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
fee: 3000, // 0.3% pool
recipient: wallet.address,
deadline: deadline,
amountIn: amountIn,
amountOutMinimum: 0, // In a real scenario, ALWAYS calculate slippage!
sqrtPriceLimitX96: 0
};
const data = iface.encodeFunctionData("exactInputSingle", [params]);
// 2. Define the transaction structure
const transaction = {
to: ROUTER_ADDRESS,
value: amountIn,
data: data,
chainId: 1,
type: 2, // EIP-1559
gasLimit: 250000,
maxFeePerGas: ethers.parseUnits("50", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("2", "gwei"), // The validator "bribe"
nonce: await wallet.getNonce()
};
// 3. Create the signed bundle
const signedBundle = await flashbotsProvider.signBundle([
{
signer: wallet,
transaction: transaction
}
]);
// 4. SIMULATION (The most important step)
console.log("Kicking off bundle simulation...");
const simulation = await flashbotsProvider.simulate(signedBundle, nextBlockNumber);
if ("error" in simulation) {
console.error(`Simulation failed: ${simulation.error.message}`);
return;
}
console.log("Simulation lookin' good!", JSON.stringify(simulation, null, 2));
return signedBundle;
}
3. The Tech Lowdown: Why is simulation a must?
On the "public" mempool, if your transaction fails (say, you hit a gas limit or the price moves against you), it still gets included in a block, and you still get slapped with the gas fee.
In the Flashbots world:
- If the simulation throws an error, you simply don't send the bundle. No harm, no foul.
- If the bundle is sent but market conditions shift making the trade unfavorable, the validator just ignores it.
Bottom line: You only pay for gas when your Stealth Swap actually crosses the finish line.
4. How to calculate the "Bribe" (Priority Fee)
Validators pick bundles based on how much profit they bring in. They calculate a bundle's "Gas Price Score" like this:

For a standard swap, a maxPriorityFeePerGas of 1-2 gwei is usually plenty. But when things get volatile, the competition for block space heats up, even on these private tracks.
Where are we at?
We’ve now got a signed, verified bundle ready to roll. We’re 100% sure the Uniswap code will behave and the wallet has the funds to cover the move.
Coming up in the final part: we'll build the block-wait loop, handle the actual bundle broadcast, and wrap it all in a clean CLI interface.