🔁 Atomic and Composable Batch Execute
This tutorial demonstrates how to execute a fully gas-abstracted, multi-step transaction on Base using one user signature. No bridging. No leftover tokens. Just one-click UX with gas paid in USDC and a guarantee that either all instructions succeed or none do.
Unlike regular batch execute, composable batch execution allows developers to use the output of one function call as the input for the next one. To learn more, read the Runtime Parameter Injection guide.
🧠 Why Should You Care?
- No ETH required for gas — everything is paid in USDC.
- Lower dropoff in multi-step flows — users only sign once.
- Eliminates complexity — no frontend juggling approvals, swaps, and deposits.
- Business win: better UX, fewer support issues, and higher conversion.
- Full support: Works for all EOA users, including regular MetaMask, Rabby, Trust, etc...
🛠️ 2 Setup
Import all the required dependencies.
import {
createMeeClient,
toMultichainNexusAccount,
UniswapSwapRouterAbi,
getMeeScanLink,
runtimeERC20BalanceOf,
greaterThanOrEqualTo,
} from "@biconomy/abstractjs";
import { http, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, optimism } from "viem/chains";
import { balanceNotZeroConstraint } from "../utils/balanceNotZero.util";
2.1 Create an Orchestrator
An orchestrator is a smart account owned by the user. All instructions are executed on top of this account.
const eoa = privateKeyToAccount(Bun.env.PRIVATE_KEY as `0x${string}`);
const orchestrator = await toMultichainNexusAccount({
chains: [optimism, base],
transports: [http(), http()],
signer: eoa,
});
2.2 Connect to the Modular Execution Environment (MEE)
Gasless multichain orchestration is enabled by connecting to the Modular Execution Environment. This is a trustless, globally distributed network of Relayer nodes executing instructions on top of smart accounts.
const meeClient = await createMeeClient({ account: orchestrator });
📑 3 Declare Constants
These are the contract addresses we'll need for this tutorial.
const UNISWAP_ROUTER_BASE = "0x2626664c2603336E57B271c5C0b26F421741e481";
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const WETH_BASE = "0x4200000000000000000000000000000000000006";
const MORPHO_RE7_POOL = "0xA2Cac0023a4797b4729Db94783405189a4203AFc";
// Change to be whatever you want the input to be.
const inputAmount = parseUnits("10", 6);
🔨 4 Build Instructions
4.1 Approve Uniswap
This will encode an ERC-20
approve
function which approves the Uniswap
contract to spend USDC.
const approveUniswap = await orchestrator.buildComposable({
type: "approve",
data: {
spender: UNISWAP_ROUTER_BASE,
tokenAddress: USDC_BASE,
chainId: base.id,
amount: inputAmount,
},
});
4.2 Swap USDC → WETH (Runtime Injection)
Using the .buildComposable
helper, we are encoding a call to the exactInputSingle
function
on the Uniswap contract. This will swap USDC for USDT.
Note the use of runtimeERC20BalanceOf
in the amountIn
field of the call. This means
that we're not predetermining the amount being swapped - we'll use whatever is available
on the orchestrator account.
const swapUSDCtoWeth = await orchestrator.buildComposable({
type: "default",
data: {
chainId: base.id,
abi: UniswapSwapRouterAbi,
to: UNISWAP_ROUTER_BASE,
functionName: "exactInputSingle",
args: [{
tokenIn: USDC_BASE,
amountIn: runtimeERC20BalanceOf({
tokenAddress: USDC_BASE,
targetAddress: orchestrator.addressOn(base.id, true),
constraints: [balanceNotZeroConstraint],
}),
tokenOut: WETH_BASE,
recipient: orchestrator.addressOn(base.id, true),
fee: 100,
amountOutMinimum: 0n,
sqrtPriceLimitX96: 0n,
}],
},
});
🧠 Runtime injection lets you defer the exact amount to use until execution time — crucial when the actual balance isn’t known upfront.
4.3 Approve Morpho
This instruction approves Morpho to spend WETH. Again, not the usage of runtimeERC20BalanceOf
function. Since we don't know how much exactly we'll get from a swap
on Uniswap due to
slippage - we're working with runtime values.
Another thing to note is the constraints
field. It defines the minimum amount of
WETH on the account before the orchestration will proceed with the approve instruction.
Since the orchestrator account is just a passthoguh, we can assume that the balance is
0
before the swap happens - so we can use the built-in balanceNotZeroConstraint
const approveMorpho = await orchestrator.buildComposable({
type: "approve",
data: {
spender: MORPHO_RE7_POOL,
chainId: base.id,
tokenAddress: WETH_BASE,
amount: runtimeERC20BalanceOf({
tokenAddress: WETH_BASE,
targetAddress: orchestrator.addressOn(base.id, true),
constraints: [ balanceNotZeroConstraint ],
}),
},
});
4.4 Deposit WETH into Morpho
Deposit WETH to Morpho.
Ordering for this function call depends on two factors:
balanceNotZeroConstraint
defines that the instruction can't be executed until the swap has happened (meaning until the balance of WETH is greater than zero)- Implicit Ordering works here as well. The orchestrator will wait until the approval has been set. This is because this transaction will keep simulating a failure until it gets the approval to spend WETH. So the execution cannot proceed before that happens.
const supplyWeth = await orchestrator.buildComposable({
type: "default",
data: {
abi: [{
name: "deposit",
inputs: [
{ name: "assets", type: "uint256" },
{ name: "receiver", type: "address" },
],
stateMutability: "nonpayable",
type: "function",
}],
to: MORPHO_RE7_POOL,
chainId: base.id,
functionName: "deposit",
args: [
runtimeERC20BalanceOf({
tokenAddress: WETH_BASE,
targetAddress: orchestrator.addressOn(base.id, true),
constraints: [balanceNotZeroConstraint],
}),
orchestrator.addressOn(base.id, true),
],
},
});
🚦 5 Quote & Execute (Fusion)
5.1 Create the Fusion trigger
In order for the orchestrator account to "pull" the funds for orchestration, we must
give it an approval to do so. This is done by the trigger
param. It tells the orchestrator
which token on which chain and which amount to approve.
const trigger: Trigger = {
chainId: base.id,
tokenAddress: USDC_BASE,
amount: inputAmount,
};
5.2 Quote the cost
const quote = await meeClient.getFusionQuote({
trigger,
feeToken: { address: USDC_BASE, chainId: base.id },
instructions: [approveUniswap, swapUSDCtoWeth, approveMorpho, supplyWeth],
});
5.3 Execute the flow
const { hash } = await meeClient.executeFusionQuote({ fusionQuote: quote });
console.log(`Explorer: ${getMeeScanLink(hash)}`);
🧾 6 Confirm Atomic Completion
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
console.log("Batch complete:", receipt);
✅ All instructions succeed, or none are processed. Fully atomic.
📝 Checklist Before Production
- Replace
amountOutMinimum: 0n
with real slippage. - Dial in
gasLimit
s to optimize quotes. - Add cleanup
transfer
if leftover tokens aren’t desired. - Monitor quote accuracy vs. actual gas used.
Enjoy full-chain composability 🎯