QuickStart: Asynchronous Composable Orchestration
This will be a short, zero-to-hero guide for building an orchestration flow. It will
demonstrate the following features of the Biconomy stack:
-
Orchestration
The ability for the Biconomy stack to properly sequence multiple transactions across one or more chains. -
Single-Signature Executeion
The ability to execute multiple transactions across multiple chains with a single user signature. -
Application-Layer Composability
The ability to use the output or a state-change of one onchain function call as the input for the next one. For example, using the exact amount received from a DEX at runtime, as the input for the supply function of a lending platform in the next step. -
Asynchronous Execution
The ability to wait for briges, solver or any other asynchronous actions to complete. E.g. the transaction on Optimism cannot continue until tokens have been bridged from Base to Optimsim. -
Multichain Gas Abstraction
With the Biconomy stack, you can pay for execution of transactions on any chain with ERC-20 tokens on any other chain. Additionally, when executing multiple transactions across multiple chains - the user pays only one fee on only one chain.
Example Case
In this example case, we'll:
Bridge
Call Across bridge to move USDC from Optimism to Base. Function calls being executed:
- Call
approve
on USDC contract on Optimism (approve spend to Across) - Call
depositV3
on Across Spoke Pool contract
Swap
Use the exact amount received from Across to swap to USDT on Base
- Call
approve
on USDC contract on Base (approve spend to Uniswap) - Call
swap
on Uniswap on Base
Prerequisites
- Node.js installed
- Private key for testing (never use production keys in test code)
- Some USDC on Optimism in your account
Setup
First, install the required packages:
npm install @biconomy/abstractjs viem @rhinestone/module-sdk@0.2.7
Create a new file and add the basic imports:
import {
createMeeClient,
toMultichainNexusAccount,
runtimeERC20BalanceOf,
greaterThanOrEqualTo,
mcUSDC,
mcUSDT
} from "@biconomy/abstractjs";
import { http, parseUnits, encodeFunctionData, erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { optimism, base } from "viem/chains";
Initialize Smart Account
Set up a multichain smart account on both Optimism and Base:
// Set up account (use environment variables in production)
const eoa = privateKeyToAccount("0x...your_private_key...");
// Create multichain smart account
const smartAccount = await toMultichainNexusAccount({
signer: eoa,
chains: [optimism, base],
transports: [http(), http()]
});
// Initialize the orchestration client
const meeClient = await createMeeClient({
account: smartAccount
});
console.log("Smart account address on Optimism:", smartAccount.addressOn(optimism.id));
console.log("Smart account address on Base:", smartAccount.addressOn(base.id));
Important: Fund the Account
This example is using Smart Account based orchestration. In order for smart account based orchestration to work, you need to put funds into the smart account.
Define Bridge Instruction
// Import the Across bridge helper function
import { prepareAcrossBridgeTransaction } from "./across-encoder";
// Define the amount to bridge
const amountToBridge = parseUnits("5", 6); // 5 USDC
// Build the bridge instruction
const bridgeInstruction = await smartAccount.buildComposable({
type: "default",
data: // Encode call to bridge instructions here
});
Define Runtime Value Injection
Runtime value injection allows function parameters to be determined at execution time rather than when creating the transaction. This is essential for cross-chain operations where exact amounts can't be known in advance due to bridge fees, slippage, or other factors.
When using runtimeERC20BalanceOf
, the system will:
- Read the actual token balance at execution time
- Inject this value into the function parameters
- Apply any constraints to ensure sufficient value
// Define execution constraints (minimum amount to accept from bridge)
const executionConstraints = [
greaterThanOrEqualTo((amountToBridge * 90n) / 100n) // Accept 10% slippage
];
// Approve USDC spend to Uniswap on Base using the approve instruction builder
const approveUsdcToUniswap = await smartAccount.buildComposable({
type: "approve",
data: {
chainId: base.id,
tokenAddress: mcUSDC.addressOn(base.id),
spender: uniswapRouter,
// Use the exact amount received from the bridge
amount: runtimeERC20BalanceOf({
targetAddress: smartAccount.addressOn(base.id),
tokenAddress: mcUSDC.addressOn(base.id),
constraints: executionConstraints
})
}
});
Define Swap Instruction
import { parseAbi } from 'viem'
const uniswapRouterAbi = parseAbi([
"function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) returns (uint256 amountOut)"
]);
// Uniswap router address on Base
const uniswapRouter = "0x2626664c2603336E57B271c5C0b26F421741e481";
// Execute swap on Uniswap
const swapOnUniswap = await smartAccount.buildComposable({
type: "default",
data: {
chainId: base.id,
abi: uniswapRouterAbi,
to: uniswapRouter,
functionName: "exactInputSingle",
args: [{
tokenIn: mcUSDC.addressOn(base.id),
tokenOut: mcUSDT.addressOn(base.id),
fee: 500, // 0.05% fee tier
recipient: smartAccount.addressOn(base.id),
// Use the exact amount of USDC received from the bridge
amountIn: runtimeERC20BalanceOf({
targetAddress: smartAccount.addressOn(base.id),
tokenAddress: mcUSDC.addressOn(base.id),
constraints: executionConstraints
}),
amountOutMinimum: 0n, // For demo purposes, set proper slippage in production
sqrtPriceLimitX96: 0n // No price limit
}]
}
});
Execute the Orchestration
// Execute the orchestration sequence
const { hash } = await meeClient.execute({
// Pay for execution with USDC on Optimism
feeToken: {
address: mcUSDC.addressOn(optimism.id),
chainId: optimism.id
},
// The sequence of instructions to execute
instructions: [
bridgeInstruction,
approveUsdcToUniswap,
swapOnUniswap
]
});
console.log(`Orchestration started: ${hash}`);
Wait for Execution Result
// Wait for the entire orchestration to complete
const receipt = await meeClient.waitForSupertransactionReceipt({ hash });
console.log("Orchestration completed successfully!");
How It Works
-
Smart Account Initialization:
We create a multichain smart account that can operate on both Optimism and Base. -
Bridge Execution:
- The bridge instruction runs on Optimism
- It approves USDC spending and calls Across bridge
-
Asynchronous Waiting:
- After the bridge instruction completes, the system automatically waits for funds to arrive on Base
- No manual monitoring or intervention required
-
Runtime Value Injection:
- When funds arrive on Base, the exact received amount is automatically injected into the next instructions
- The
runtimeERC20BalanceOf
function reads the actual token balance at execution time - This handles any slippage from the bridge operation
-
Constraint Enforcement:
- The execution constraints ensure the swap only proceeds if a minimum amount was received
- If less than 90% of the expected amount arrives, the swap won't execute
-
Swap Execution:
- Once sufficient funds are available on Base, the USDC is approved for Uniswap
- The swap executes using exactly the amount received from the bridge
Key Points
- The entire orchestration is executed with a single signature from the smart account
- No need to manually monitor bridge status or handle cross-chain coordination
- Runtime values automatically adapt to actual on-chain conditions
- Execution constraints provide safety guardrails for the operation
- The orchestration handles the asynchronous nature of bridges transparently