This quickstart shows how to use Biconomy MEE’s Fusion execution mode to batch-send ERC-20 tokens (USDC) on a single chain (Base Sepolia) using a regular EOA wallet (e.g. MetaMask). It demonstrates how an externally owned account can:
  • Batch transfer tokens to multiple recipients
  • Pay for gas using ERC-20 tokens (not ETH)
  • Do this all with a single signature
  • Without using smart wallets or EIP-7702

Why not EIP-7702?

Use EIP-7702 with Biconomy + Embedded WalletsBiconomy has full support for EIP-7702. If you wish to take advantage of those features - build by using embedded wallets (e.g. Privy, Dynamic, Turnkey, …)

How It Works

With Fusion, externally owned accounts (EOAs) can authorize a Companion Smart Account (aka “Orchestrator”) to pull tokens and execute instructions. The Companion is invisible to users, acting as a passthrough executor that handles batching, permissions, and fee payments.

Flow Summary

1

User Authorization

The user signs a quote from MEE authorizing their Companion to pull tokens (e.g. USDC)
2

Batch Execution

The Companion executes the batched instructions, using the pulled tokens to:
  • pay for gas
  • send funds to multiple recipients
3

Seamless Experience

From the user’s perspective, they signed once and completed the action — no additional UX overhead

Setup

1. Create the project

bun create vite fusion-single-chain --template react-ts
cd fusion-single-chain
bun install

2. Install dependencies

bun add viem wagmi @biconomy/abstractjs
bun add @tanstack/react-query ethers # peer deps for wagmi

3. Wrap App in providers

Edit main.tsx to include WagmiProvider and QueryClientProvider:
// src/main.tsx
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>
);
Then create wagmi.ts:
// src/wagmi.ts
import { baseSepolia } from 'wagmi/chains';
import { createConfig, http } from 'wagmi';

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

Key Concepts in App

Wallet & Companion Initialization

First, we need to connect to the wallet extension. This tutorial uses the window.ethereum method, but you should use a more comprehensive solution (e.g. ReOwn AppKit, Web3Modal, RainbowKit, …)
const walletClient = createWalletClient({
  chain: baseSepolia,
  transport: custom(window.ethereum)
});

const orchestrator = await toMultichainNexusAccount({
  chainConfigurations: [
    {
      chain: baseSepolia,
      transport: http(),
      version: getMEEVersion(MEEVersion.V2_1_0)
    }
  ],
  signer: walletClient
});

const meeClient = await createMeeClient({ account: orchestrator });

Building Transfer Instructions

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

Triggering Fusion Execution

Fusion flows require a trigger that pulls tokens from the EOA:
const fusionQuote = await meeClient.getFusionQuote({
  instructions: transfers,
  trigger: {
    chainId: baseSepolia.id,
    tokenAddress: usdcAddress,
    amount: totalAmount // no maxAvailableFunds
  },
  feeToken: {
    address: usdcAddress,
    chainId: baseSepolia.id
  }
});
The signature over this quote includes the hash of all instructions and the fee + pull logic. One signature does it all.

Execute & Monitor

const { hash } = await meeClient.executeFusionQuote({ fusionQuote });

await meeClient.waitForSupertransactionReceipt({ hash });
const link = getMeeScanLink(hash);

Full Example Files

import { useState } from 'react';
import {
  createWalletClient,
  custom,
  erc20Abi,
  http,
  type WalletClient,
  type Hex,
  formatUnits
} from 'viem';
import { baseSepolia } from 'viem/chains';
import {
  createMeeClient,
  toMultichainNexusAccount,
  runtimeERC20BalanceOf,
  greaterThanOrEqualTo,
  getMeeScanLink,
  type MeeClient,
  type MultichainSmartAccount,
  getMEEVersion,
  MEEVersion
} 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?.addChain({ chain: baseSepolia });
      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, 1n * 10n ** 6n] // 1 USDC
              }
            })
          )
      );

      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('Executing quote…');
      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', color: 'orangered' }}>
      <h1>Biconomy MEE Quickstart (Base Sepolia)</h1>

      <button
        style={{ padding: '10px 20px', fontSize: '1rem' }}
        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
            style={{ padding: '10px 20px', fontSize: '1rem' }}
            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>
  );
}
Ready to publish this to the official docs or format it as a starter repository!