Biconomy: Simplified Onboarding Using MEE-Compatible Smart Accounts

Introduction:

Before analyzing Biconomy's Modular Execution Environment (MEE) implementation, let's first understand the types of accounts compatible with the Ethereum Virtual Machine (EVM)

Externally Owned Accounts (EOAs)

Externally Owned Accounts (EOAs) are Ethereum accounts controlled by private keys. They can send transactions, interact with smart contracts, and hold Ether and other tokens. Unlike smart contract accounts, EOAs do not have associated code and cannot execute complex logic.

Contract Accounts (CAs)

Contract Accounts (CAs) are Ethereum accounts that contain smart contract code. They are controlled by the code they contain, rather than by a private key like EOAs. These accounts can hold Ether and tokens, execute complex logic, and interact with other smart contracts on the blockchain.

EIP-4337-Compatible Smart Accounts

EIP-4337-Compatible Smart Accounts combine the ability of EOA and CA, it brings smart contract functionality to wallets in a single account. Account abstraction enhances user experience across DeFi, GameFi, DAOs, and beyond by integrating smart contract functionality into single-account EIP-4337-Compatible Wallets.


MEE: Beyond ERC-4337

Biconomy's Modular Execution Environment (MEE) extends beyond ERC-4337 to enable true cross-chain composability. MEE retains all ERC-4337 features while adding:

  • True Composability: Dynamic execution where each step can reference outputs from previous steps
  • Cross-Chain Orchestration: Single signature authorizes complex flows spanning multiple chains
  • 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 without requiring smart wallet deployment

Biconomy's MEE: Account Abstraction Via Fusion Execution

MEE uses a "Fusion" execution model with these main components:

  • Orchestrator (Companion Smart Account): Represents user wallet accounts in smart contract form. It's invisible to users and acts as a passthrough executor handling batching, permissions, and fee payments.
  • MEE Client: Collects instructions, bundles them, and coordinates execution across chains.
  • Instructions: Transaction objects created by dApps to execute user transactions. These are built using composable patterns that allow dynamic execution.
  • Fee Token: Specifies which token to use for gas payments, allowing users to pay fees with any supported token on any chain.

There are two main phases:

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

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

How Biconomy's MEE Enhances User Experience and Boosts Adoption

  • Easy User Onboarding: Works with existing EOA wallets—no smart wallet setup required.
  • Fiat On Ramp: Let your users easily & reliably buy/sell crypto within your dApp.
  • Unified Account Management: Single signature for complex multi-step operations.
  • Gasless Transactions: Allow users to pay transaction fees using any supported tokens, making it more user-friendly.
  • Chain Agnostic: Natively orchestrates operations across chains with single signature authorization.
  • Custom Transaction Bundling: Batch multiple actions, even across multiple chains, in a single transaction. E.g., Approve and Deposit can be done in the same transaction.

Biconomy: Getting Started

You can get started testing MEE without any API key. For production use, you'll need an API key from the Biconomy Dashboard.

Getting Your API Key (Optional for Testing)

Step 1: Go to dashboard.biconomy.io

Step 2: Create an account or log in

Step 3: Create a new project and copy your API key

📘

For initial development and testing, you can skip the API key setup and proceed directly to the code. Add your API key when you're ready to move to production.

Step 1: Create Your Project

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

Step 2: Install Dependencies

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

Step 3: Configure Wagmi Provider

Create src/wagmi.ts:

import { baseSepolia } from 'wagmi/chains';
import { createConfig, http } from 'wagmi';

export const config = createConfig({
  chains: [baseSepolia],
  transports: {
    [baseSepolia.id]: http()
  }
});

Step 4: Wrap App in Providers

Edit src/main.tsx:

import ReactDOM from 'react-dom/client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './wagmi';
import App from './App';

const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById('root')!).render(
  <WagmiProvider config={config}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </WagmiProvider>
);

Biconomy: Quick Start

This quick start guide will help you get started with a basic React + TypeScript project using Biconomy's MEE SDK.

Environment Setup

Step 1: After creating your project and installing dependencies, run the development server:

bun dev

Step 2: Great! You should see the Vite starter page. You're good with the initial setup.

Step 3: Navigate to src folder and open App.tsx file.

Step 4: Add import statements for MEE packages:

import { useState } from 'react';
import {
  createWalletClient,
  custom,
  erc20Abi,
  http,
  type Hex,
  formatUnits
} from 'viem';
import { baseSepolia } from 'viem/chains';
import {
  createMeeClient,
  toMultichainNexusAccount,
  getMeeScanLink,
  getMEEVersion,
  MEEVersion,
  type MeeClient,
  type MultichainSmartAccount
} from '@biconomy/abstractjs';
import { useReadContract } from 'wagmi';

Step 5: Create a function to connect wallet and initialize MEE:

const usdcAddress = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';

const [account, setAccount] = useState<string | null>(null);
const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
const [meeClient, setMeeClient] = useState<MeeClient | null>(null);
const [orchestrator, setOrchestrator] = useState<MultichainSmartAccount | null>(null);

const connectAndInit = async () => {
  if (typeof window.ethereum === 'undefined') {
    alert('MetaMask not detected');
    return;
  }

  const wallet = createWalletClient({
    chain: baseSepolia,
    transport: custom(window.ethereum)
  });
  setWalletClient(wallet);

  const [address] = await wallet.requestAddresses();
  setAccount(address);

  const multiAccount = await toMultichainNexusAccount({
    chainConfigurations: [
      {
        chain: baseSepolia,
        transport: http(),
        version: getMEEVersion(MEEVersion.V2_1_0)
      }
    ],
    signer: createWalletClient({
      account: address,
      transport: custom(window.ethereum)
    })
  });
  setOrchestrator(multiAccount);

  const mee = await createMeeClient({ account: multiAccount });
  setMeeClient(mee);
};

Code Explanation:

  • toMultichainNexusAccount - Creates the Orchestrator (Companion Smart Account) that will execute transactions on behalf of the user
  • chainConfigurations - Array of chains to support. Add multiple chains here for cross-chain operations
  • getMEEVersion - Retrieves the correct MEE version for compatibility
  • createMeeClient - Creates the MEE client that coordinates execution

Step 6: Create a function to build and execute transactions:

const executeTransfers = async () => {
  if (!orchestrator || !meeClient || !account) {
    alert('Account not initialized');
    return;
  }

  try {
    await walletClient?.switchChain({ id: baseSepolia.id });

    const recipients = [
      '0x322Af0da66D00be980C7aa006377FCaaEee3BDFD',
      '0x1234567890123456789012345678901234567890'
    ];

    // Build composable instructions
    const transfers = await Promise.all(
      recipients.map((recipient) =>
        orchestrator.buildComposable({
          type: 'default',
          data: {
            abi: erc20Abi,
            chainId: baseSepolia.id,
            to: usdcAddress,
            functionName: 'transfer',
            args: [recipient as Hex, 1_000_000n] // 1 USDC (6 decimals)
          }
        })
      )
    );

    const totalAmount = BigInt(transfers.length) * 1_000_000n;

    // Get quote with fee token specification
    const fusionQuote = await meeClient.getFusionQuote({
      instructions: transfers,
      trigger: {
        chainId: baseSepolia.id,
        tokenAddress: usdcAddress,
        amount: totalAmount
      },
      feeToken: {
        address: usdcAddress,
        chainId: baseSepolia.id
      }
    });

    // Execute with single signature
    const { hash } = await meeClient.executeFusionQuote({ fusionQuote });

    // Wait for completion
    const transactionDetail = await meeClient.waitForSupertransactionReceipt({ hash });

    console.log('Transaction completed!');
    console.log(getMeeScanLink(hash));
  } catch (error) {
    console.log(error);
  }
};

Hurray! You're done with all the basic setup of Biconomy MEE SDK. Please check the complete code sample below:

Complete Code Sample

App.tsx

import { useState } from 'react';
import {
  createWalletClient,
  custom,
  erc20Abi,
  http,
  type WalletClient,
  type Hex,
  formatUnits
} from 'viem';
import { baseSepolia } from 'viem/chains';
import {
  createMeeClient,
  toMultichainNexusAccount,
  getMeeScanLink,
  getMEEVersion,
  MEEVersion,
  type MeeClient,
  type MultichainSmartAccount
} from '@biconomy/abstractjs';
import { useReadContract } from 'wagmi';

export default function App() {
  const [account, setAccount] = useState<string | null>(null);
  const [walletClient, setWalletClient] = useState<WalletClient | null>(null);
  const [meeClient, setMeeClient] = useState<MeeClient | null>(null);
  const [orchestrator, setOrchestrator] = useState<MultichainSmartAccount | null>(null);
  const [status, setStatus] = useState<string | null>(null);
  const [meeScanLink, setMeeScanLink] = useState<string | null>(null);
  const [recipients, setRecipients] = useState<string[]>(['']);

  const usdcAddress = '0x036CbD53842c5426634e7929541eC2318f3dCF7e';

  const { data: balance } = useReadContract({
    abi: erc20Abi,
    address: usdcAddress,
    chainId: baseSepolia.id,
    functionName: 'balanceOf',
    args: account ? [account as Hex] : undefined,
    query: { enabled: !!account }
  });

  const connectAndInit = async () => {
    if (typeof (window as any).ethereum === 'undefined') {
      alert('MetaMask not detected');
      return;
    }

    const wallet = createWalletClient({
      chain: baseSepolia,
      transport: custom((window as any).ethereum)
    });
    setWalletClient(wallet);

    const [address] = await wallet.requestAddresses();
    setAccount(address);

    const multiAccount = await toMultichainNexusAccount({
      chainConfigurations: [
        {
          chain: baseSepolia,
          transport: http(),
          version: getMEEVersion(MEEVersion.V2_1_0)
        }
      ],
      signer: createWalletClient({
        account: address,
        transport: custom((window as any).ethereum)
      })
    });
    setOrchestrator(multiAccount);

    const mee = await createMeeClient({ account: multiAccount });
    setMeeClient(mee);
  };

  const executeTransfers = async () => {
    if (!orchestrator || !meeClient || !account) {
      alert('Account not initialized');
      return;
    }

    try {
      setStatus('Encoding instructions…');

      await walletClient?.switchChain({ id: baseSepolia.id });

      const transfers = await Promise.all(
        recipients
          .filter((r) => r)
          .map((recipient) =>
            orchestrator.buildComposable({
              type: 'default',
              data: {
                abi: erc20Abi,
                chainId: baseSepolia.id,
                to: usdcAddress,
                functionName: 'transfer',
                args: [recipient as Hex, 1_000_000n]
              }
            })
          )
      );

      const totalAmount = BigInt(transfers.length) * 1_000_000n;

      setStatus('Requesting quote…');
      const fusionQuote = await meeClient.getFusionQuote({
        instructions: transfers,
        trigger: {
          chainId: baseSepolia.id,
          tokenAddress: usdcAddress,
          amount: totalAmount
        },
        feeToken: {
          address: usdcAddress,
          chainId: baseSepolia.id
        }
      });

      setStatus('Please sign the transaction…');
      const { hash } = await meeClient.executeFusionQuote({ fusionQuote });

      const link = getMeeScanLink(hash);
      setMeeScanLink(link);
      setStatus('Waiting for completion…');

      await meeClient.waitForSupertransactionReceipt({ hash });

      setStatus('Transaction completed!');
    } catch (err: any) {
      console.error(err);
      setStatus(`Error: ${err.message ?? err}`);
    }
  };

  return (
    <main style={{ padding: 40, fontFamily: 'sans-serif' }}>
      <h1>Biconomy MEE Quickstart (Base Sepolia)</h1>

      <button onClick={connectAndInit} disabled={!!account}>
        {account ? 'Connected' : 'Connect Wallet'}
      </button>

      {account && (
        <div style={{ marginTop: 20 }}>
          <p><strong>Address:</strong> {account}</p>
          <p>USDC Balance: {balance ? `${formatUnits(balance, 6)} USDC` : '–'}</p>

          <h3>Recipients</h3>
          {recipients.map((recipient, idx) => (
            <input
              key={idx}
              type="text"
              value={recipient}
              onChange={(e) => {
                const updated = [...recipients];
                updated[idx] = e.target.value;
                setRecipients(updated);
              }}
              placeholder="0x..."
              style={{ display: 'block', margin: '8px 0', padding: '6px', width: '100%' }}
            />
          ))}

          <button onClick={() => setRecipients([...recipients, ''])}>
            Add Recipient
          </button>
        </div>
      )}

      {meeClient && (
        <>
          <p style={{ marginTop: 20 }}>
            <strong>MEE client ready</strong> – you can now orchestrate multichain transactions!
          </p>

          <button onClick={executeTransfers}>
            Send 1 USDC to each recipient
          </button>
        </>
      )}

      {status && <p style={{ marginTop: 20 }}>{status}</p>}

      {meeScanLink && (
        <p style={{ marginTop: 10 }}>
          <a href={meeScanLink} target="_blank" rel="noopener noreferrer">
            View on MEE Scan
          </a>
        </p>
      )}
    </main>
  );
}
📘

For the detailed docs on Biconomy MEE, refer to https://docs.biconomy.io