Skip to content

๐Ÿงก External Wallets Quickstart

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?

โŒ Wallets don't allow apps to install code
While EIP-7702 allowed for smart account code to be set to the EOA address on the blockchain level - all major wallets prevent apps from installing their own code onto users EOA addresses. This is done due to the security concerns wallets have around apps installing malicious code.


๐Ÿค Apps access user funds by requesting permissions
The approach wallets have taken is to expose a set of permissions to the apps where they can "request" certain tokens, etc... This effort has been spearheaded by MetaMask with their ERC-7715 integration and their DeleGator framework.


โ“ Unclear how the space will develop
Other major wallets (Rabby, Trust, Rainbow, Coinbase, Phantom, Uniswap, ...) have not publically stated whether they'll add support for ERC-7715.


โœ… Biconomy offers universal solution
The method Biconomy uses is universally compatible with all EOA wallets which means you can count on offering these features to everyone.


๐Ÿ’ก 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. The user signs a quote from MEE authorizing their Companion to pull tokens (e.g. USDC)

  2. The Companion executes the batched instructions, using the pulled tokens to:

    • pay for gas
    • send funds to multiple recipients
  3. 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({
  chains: [baseSepolia],
  transports: [http()],
  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

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,
  runtimeERC20BalanceOf,
  greaterThanOrEqualTo,
  getMeeScanLink,
  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({
      chains: [baseSepolia],
      transports: [http()],
      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>
  );
}

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>
);

Let me know if you want this published to the official docs or formatted as a starter repo!