Biconomy MEE — Simplified Onboarding via Smart Accounts

Use Biconomy’s Modular Execution Environment (MEE) to enable cross-chain onboarding with a single user signature

View as Markdown

Biconomy’s Modular Execution Environment (MEE) lets you build seamless cross-chain experiences on top of Transak’s fiat on-ramp — with no smart wallet setup required for users.

EVM Account Types

Before diving into MEE, it helps to understand the two foundational account types on Ethereum:

Externally Owned Accounts (EOAs)

Controlled by a private key (e.g. MetaMask). Can send transactions and interact with contracts, but cannot execute complex logic or batch operations.

Contract Accounts (CAs)

Smart contract code deployed on-chain. Can execute complex logic and interact with other contracts, but require deployment and cannot initiate transactions on their own.

EIP-4337 Smart Accounts

Combine the best of both worlds — smart contract functionality within a single wallet. Enable account abstraction across DeFi, GameFi, DAOs, and more.

Account Abstraction overview

MEE: Beyond ERC-4337

Biconomy’s MEE extends standard ERC-4337 to enable true cross-chain composability — with no extra setup for users.

True Composability

Dynamic execution where each step can reference outputs from previous steps.

Cross-Chain Orchestration

A single signature authorizes complex flows spanning multiple chains simultaneously.

Universal Gas Abstraction

Pay for gas on any chain using tokens from any other supported chain.

EOA Wallet Support

Works with standard wallets like MetaMask — no smart wallet deployment required.

How MEE Works: Fusion Execution

MEE uses a Fusion execution model with four core components:

Orchestrator

A Companion Smart Account that represents the user’s wallet. Invisible to users — handles batching, permissions, and fee payments.

MEE Client

Collects instructions, bundles them, and coordinates execution across chains.

Instructions

Transaction objects created by dApps. Built using composable patterns that allow dynamic, multi-step execution.

Fee Token

Specifies which token to use for gas, allowing users to pay fees with any supported token on any chain.

1

Authorization Phase

The user signs a single quote authorizing their Companion account to pull tokens and execute instructions.

2

Execution Phase

The Companion executes the batched instructions, using the pulled tokens to pay for gas and perform the requested actions.

Why Integrate MEE with Transak?

Easy User Onboarding

Works with existing EOA wallets — no smart wallet setup required for your users.

Fiat On-Ramp

Let users buy crypto directly within your dApp via Transak’s on-ramp.

Single Signature UX

One signature covers complex multi-step and multi-chain operations.

Gasless Transactions

Users pay transaction fees with any supported token — no native gas token required.

Chain Agnostic

Natively orchestrates operations across chains with a single authorization.

Custom Transaction Bundling

Batch multiple actions across chains in one transaction (e.g. Approve + Deposit).

Getting Started

You can start testing MEE without any API key. Add your Biconomy API key only when you

Biconomy API Key (Production only)

1

Go to the Biconomy Dashboard

Visit dashboard.biconomy.io and create an account or log in.

2

Create a new project

Set up a new project and copy your API key for production use.

Project Setup

1

Create your project

$bun create vite my-mee-app --template react-ts
$cd my-mee-app
2

Install dependencies

$bun add viem wagmi @biconomy/abstractjs @tanstack/react-query
3

Configure the Wagmi provider

Create src/wagmi.ts:

1import { baseSepolia } from 'wagmi/chains';
2import { createConfig, http } from 'wagmi';
3
4export const config = createConfig({
5 chains: [baseSepolia],
6 transports: {
7 [baseSepolia.id]: http()
8 }
9});
4

Wrap your app in providers

Edit src/main.tsx:

1import ReactDOM from 'react-dom/client';
2import { WagmiProvider } from 'wagmi';
3import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4import { config } from './wagmi';
5import App from './App';
6
7const queryClient = new QueryClient();
8
9ReactDOM.createRoot(document.getElementById('root')!).render(
10 <WagmiProvider config={config}>
11 <QueryClientProvider client={queryClient}>
12 <App />
13 </QueryClientProvider>
14 </WagmiProvider>
15);
5

Start the development server

$bun dev

You should see the Vite starter page — setup is complete.

Implementing MEE in App.tsx

1

Add import statements

Open src/App.tsx and add the following imports:

1import { useState } from 'react';
2import {
3 createWalletClient,
4 custom,
5 erc20Abi,
6 http,
7 type Hex,
8 formatUnits
9} from 'viem';
10import { baseSepolia } from 'viem/chains';
11import {
12 createMeeClient,
13 toMultichainNexusAccount,
14 getMeeScanLink,
15 getMEEVersion,
16 MEEVersion,
17 type MeeClient,
18 type MultichainSmartAccount
19} from '@biconomy/abstractjs';
20import { useReadContract } from 'wagmi';
2

Connect wallet and initialize MEE

Create a connectAndInit function that sets up the Orchestrator and MEE client:

1const usdcAddress = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
2
3const [account, setAccount] = useState<string | null>(null);
4const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
5const [meeClient, setMeeClient] = useState<MeeClient | null>(null);
6const [orchestrator, setOrchestrator] = useState<MultichainSmartAccount | null>(null);
7
8const connectAndInit = async () => {
9 if (typeof window.ethereum === 'undefined') {
10 alert('MetaMask not detected');
11 return;
12 }
13
14 const wallet = createWalletClient({
15 chain: baseSepolia,
16 transport: custom(window.ethereum)
17 });
18 setWalletClient(wallet);
19
20 const [address] = await wallet.requestAddresses();
21 setAccount(address);
22
23 const multiAccount = await toMultichainNexusAccount({
24 chainConfigurations: [
25 {
26 chain: baseSepolia,
27 transport: http(),
28 version: getMEEVersion(MEEVersion.V2_1_0)
29 }
30 ],
31 signer: createWalletClient({
32 account: address,
33 transport: custom(window.ethereum)
34 })
35 });
36 setOrchestrator(multiAccount);
37
38 const mee = await createMeeClient({ account: multiAccount });
39 setMeeClient(mee);
40};
FunctionPurpose
toMultichainNexusAccountCreates the Orchestrator (Companion Smart Account)
chainConfigurationsArray of chains to support — add more for cross-chain ops
getMEEVersionRetrieves the correct MEE version for compatibility
createMeeClientCreates the MEE client that coordinates execution
3

Build and execute transactions

Create an executeTransfers function that builds composable instructions and executes them with a single signature:

1const executeTransfers = async () => {
2 if (!orchestrator || !meeClient || !account) {
3 alert('Account not initialized');
4 return;
5 }
6
7 try {
8 await walletClient?.switchChain({ id: baseSepolia.id });
9
10 const recipients = [
11 '0x322Af0da66D00be980C7aa006377FCaaEee3BDFD',
12 '0x1234567890123456789012345678901234567890'
13 ];
14
15 // Build composable instructions for each recipient
16 const transfers = await Promise.all(
17 recipients.map((recipient) =>
18 orchestrator.buildComposable({
19 type: 'default',
20 data: {
21 abi: erc20Abi,
22 chainId: baseSepolia.id,
23 to: usdcAddress,
24 functionName: 'transfer',
25 args: [recipient as Hex, 1_000_000n] // 1 USDC (6 decimals)
26 }
27 })
28 )
29 );
30
31 const totalAmount = BigInt(transfers.length) * 1_000_000n;
32
33 // Get a Fusion quote with fee token specification
34 const fusionQuote = await meeClient.getFusionQuote({
35 instructions: transfers,
36 trigger: {
37 chainId: baseSepolia.id,
38 tokenAddress: usdcAddress,
39 amount: totalAmount
40 },
41 feeToken: {
42 address: usdcAddress,
43 chainId: baseSepolia.id
44 }
45 });
46
47 // Execute with a single user signature
48 const { hash } = await meeClient.executeFusionQuote({ fusionQuote });
49
50 // Wait for on-chain confirmation
51 const transactionDetail = await meeClient.waitForSupertransactionReceipt({ hash });
52
53 console.log('Transaction completed!');
54 console.log(getMeeScanLink(hash));
55 } catch (error) {
56 console.log(error);
57 }
58};

Complete Code Sample

The full App.tsx below includes wallet connection, balance display, dynamic recipient management, live status updates, and a MEE Scan link.

1import { useState } from 'react';
2import {
3 createWalletClient,
4 custom,
5 erc20Abi,
6 http,
7 type WalletClient,
8 type Hex,
9 formatUnits
10} from 'viem';
11import { baseSepolia } from 'viem/chains';
12import {
13 createMeeClient,
14 toMultichainNexusAccount,
15 getMeeScanLink,
16 getMEEVersion,
17 MEEVersion,
18 type MeeClient,
19 type MultichainSmartAccount
20} from '@biconomy/abstractjs';
21import { useReadContract } from 'wagmi';
22
23export default function App() {
24 const [account, setAccount] = useState<string | null>(null);
25 const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
26 const [meeClient, setMeeClient] = useState<MeeClient | null>(null);
27 const [orchestrator, setOrchestrator] = useState<MultichainSmartAccount | null>(null);
28 const [status, setStatus] = useState<string | null>(null);
29 const [meeScanLink, setMeeScanLink] = useState<string | null>(null);
30 const [recipients, setRecipients] = useState<string[]>(['']);
31
32 const usdcAddress = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';
33
34 const { data: balance } = useReadContract({
35 abi: erc20Abi,
36 address: usdcAddress,
37 chainId: baseSepolia.id,
38 functionName: 'balanceOf',
39 args: account ? [account as Hex] : undefined,
40 query: { enabled: !!account }
41 });
42
43 const connectAndInit = async () => {
44 if (typeof (window as any).ethereum === 'undefined') {
45 alert('MetaMask not detected');
46 return;
47 }
48
49 const wallet = createWalletClient({
50 chain: baseSepolia,
51 transport: custom((window as any).ethereum)
52 });
53 setWalletClient(wallet);
54
55 const [address] = await wallet.requestAddresses();
56 setAccount(address);
57
58 const multiAccount = await toMultichainNexusAccount({
59 chainConfigurations: [
60 {
61 chain: baseSepolia,
62 transport: http(),
63 version: getMEEVersion(MEEVersion.V2_1_0)
64 }
65 ],
66 signer: createWalletClient({
67 account: address,
68 transport: custom((window as any).ethereum)
69 })
70 });
71 setOrchestrator(multiAccount);
72
73 const mee = await createMeeClient({ account: multiAccount });
74 setMeeClient(mee);
75 };
76
77 const executeTransfers = async () => {
78 if (!orchestrator || !meeClient || !account) {
79 alert('Account not initialized');
80 return;
81 }
82
83 try {
84 setStatus('Encoding instructions…');
85 await walletClient?.switchChain({ id: baseSepolia.id });
86
87 const transfers = await Promise.all(
88 recipients
89 .filter((r) => r)
90 .map((recipient) =>
91 orchestrator.buildComposable({
92 type: 'default',
93 data: {
94 abi: erc20Abi,
95 chainId: baseSepolia.id,
96 to: usdcAddress,
97 functionName: 'transfer',
98 args: [recipient as Hex, 1_000_000n]
99 }
100 })
101 )
102 );
103
104 const totalAmount = BigInt(transfers.length) * 1_000_000n;
105
106 setStatus('Requesting quote…');
107 const fusionQuote = await meeClient.getFusionQuote({
108 instructions: transfers,
109 trigger: {
110 chainId: baseSepolia.id,
111 tokenAddress: usdcAddress,
112 amount: totalAmount
113 },
114 feeToken: {
115 address: usdcAddress,
116 chainId: baseSepolia.id
117 }
118 });
119
120 setStatus('Please sign the transaction…');
121 const { hash } = await meeClient.executeFusionQuote({ fusionQuote });
122
123 const link = getMeeScanLink(hash);
124 setMeeScanLink(link);
125 setStatus('Waiting for completion…');
126
127 await meeClient.waitForSupertransactionReceipt({ hash });
128 setStatus('Transaction completed!');
129 } catch (err: any) {
130 console.error(err);
131 setStatus(`Error: ${err.message ?? err}`);
132 }
133 };
134
135 return (
136 <main style={{ padding: 40, fontFamily: 'sans-serif' }}>
137 <h1>Biconomy MEE Quickstart (Base Sepolia)</h1>
138
139 <button onClick={connectAndInit} disabled={!!account}>
140 {account ? 'Connected' : 'Connect Wallet'}
141 </button>
142
143 {account && (
144 <div style={{ marginTop: 20 }}>
145 <p><strong>Address:</strong> {account}</p>
146 <p>USDC Balance: {balance ? `${formatUnits(balance, 6)} USDC` : '–'}</p>
147
148 <h3>Recipients</h3>
149 {recipients.map((recipient, idx) => (
150 <input
151 key={idx}
152 type="text"
153 value={recipient}
154 onChange={(e) => {
155 const updated = [...recipients];
156 updated[idx] = e.target.value;
157 setRecipients(updated);
158 }}
159 placeholder="0x..."
160 style={{ display: 'block', margin: '8px 0', padding: '6px', width: '100%' }}
161 />
162 ))}
163
164 <button onClick={() => setRecipients([...recipients, ''])}>
165 Add Recipient
166 </button>
167 </div>
168 )}
169
170 {meeClient && (
171 <>
172 <p style={{ marginTop: 20 }}>
173 <strong>MEE client ready</strong> – you can now orchestrate multichain transactions!
174 </p>
175 <button onClick={executeTransfers}>
176 Send 1 USDC to each recipient
177 </button>
178 </>
179 )}
180
181 {status && <p style={{ marginTop: 20 }}>{status}</p>}
182
183 {meeScanLink && (
184 <p style={{ marginTop: 10 }}>
185 <a href={meeScanLink} target="_blank" rel="noopener noreferrer">
186 View on MEE Scan
187 </a>
188 </p>
189 )}
190 </main>
191 );
192}

For the complete Biconomy MEE documentation, visit docs.biconomy.io.