Skip to main content
With Base Account, you can send multiple onchain calls in a single transaction. Doing so improves the UX of multi-step interactions by reducing them to a single click. A common example of where you might want to leverage batch transactions is an ERC-20 approve followed by a swap. You can submit batch transactions by using the wallet_sendCalls RPC method, defined in EIP-5792.
Do you prefer video content?There is a video guide that covers the implementation in detail in the last section of this page.

Installation

Install the Base Account SDK:
npm install @base-org/account

Setup

Initialize the SDK

Import and create the Base Account SDK instance:
batchTransactions.tsx
import { createBaseAccountSDK } from "@base-org/account";

const sdk = createBaseAccountSDK({
  appName: "Base Account SDK Demo",
  appLogoUrl: "https://base.org/logo.png",
});

const provider = sdk.getProvider();

Basic Batch Transaction

Simple Multiple Transfers

Send multiple ETH transfers in a single transaction:
batchTransactions.tsx
import { createBaseAccountSDK, getCryptoKeyAccount } from "@base-org/account";
import { numberToHex, parseEther } from "viem";

const sdk = createBaseAccountSDK({
  appName: "Batch Transaction Demo",
  appLogoUrl: "https://base.org/logo.png",
});

const provider = sdk.getProvider();

async function sendBatchTransfers() {
  try {
    // Get crypto account
    const cryptoAccount = await getCryptoKeyAccount();
    const fromAddress = cryptoAccount?.account?.address;

    // Prepare batch calls
    const calls = [
      {
        to: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045",
        value: numberToHex(parseEther("0.001")), // 0.001 ETH
        data: "0x", // Empty data for simple transfer
      },
      {
        to: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E456",
        value: numberToHex(parseEther("0.001")), // 0.001 ETH
        data: "0x", // Empty data for simple transfer
      },
    ];

    // Send batch transaction
    const result = await provider.request({
      method: "wallet_sendCalls",
      params: [
        {
          version: "2.0.0",
          from: fromAddress,
          chainId: numberToHex(base.constants.CHAIN_IDS.base),
          atomicRequired: true, // All calls must succeed or all fail
          calls: calls,
        },
      ],
    });

    console.log("Batch transaction sent:", result);
    return result;
  } catch (error) {
    console.error("Batch transaction failed:", error);
    throw error;
  }
}

Contract Interactions

ERC-20 Approve and Mint an NFT (ERC-721)

A common pattern is to approve the NFT contract to move your ERC-20 and then mint an NFT (ERC-721):
batchTransactions.tsx
import {
  createBaseAccountSDK,
  getCryptoKeyAccount,
  base,
} from "@base-org/account";
import { numberToHex, parseUnits, encodeFunctionData } from "viem";

// ERC-20 ABI for approve
const erc20Abi = [
  {
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    name: "approve",
    outputs: [{ name: "", type: "bool" }],
    stateMutability: "nonpayable",
    type: "function",
  },
] as const;

// ERC721 ABI for the mint function
const erc721Abi = [
  {
    inputs: [
      { name: "to", type: "address" },
      { name: "tokenId", type: "uint256" },
    ],
    name: "mint",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
] as const;

// USDC contract address on Base Sepolia
const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";

// NFT contract address on Base Sepolia
const NFT_CONTRACT_ADDRESS = "0x82039e7C37D7aAc98D0F4d0A762F4E0d8c8DC273";

async function approveAndTransfer() {
  const sdk = createBaseAccountSDK({
    appName: "ERC-20 Batch Demo",
    appLogoUrl: "https://base.org/logo.png",
  });

  const provider = sdk.getProvider();
  const cryptoAccount = await getCryptoKeyAccount();
  const fromAddress = cryptoAccount?.account?.address;

  // Encode the first approve call - approve USDC to NFT contract
  const call1Data = encodeFunctionData({
    abi: erc20Abi,
    functionName: "approve",
    args: [
      NFT_CONTRACT_ADDRESS,
      parseUnits("1000", 6), // USDC has 6 decimals
    ],
  });

  // Encode the second call - mint NFT to the user's address
  const call2Data = encodeFunctionData({
    abi: erc721Abi,
    functionName: "mint",
    args: [fromAddress as `0x${string}`, BigInt("1")],
  });

  const result = await provider.request({
    method: "wallet_sendCalls",
    params: [
      {
        version: "2.0.0",
        from: fromAddress,
        chainId: numberToHex(base.constants.CHAIN_IDS.baseSepolia),
        atomicRequired: true,
        calls: [
          {
            to: USDC_ADDRESS,
            data: call1Data,
          },
          {
            to: NFT_CONTRACT_ADDRESS,
            data: call2Data,
          },
        ],
      },
    ],
  });

  return result;
}

Advanced Features

Checking Wallet Capabilities

Before sending batch transactions, you can check if the wallet supports atomic batching:
batchTransactions.tsx
async function checkCapabilities() {
  const provider = sdk.getProvider();

  try {
    const cryptoAccount = await getCryptoKeyAccount();
    const address = cryptoAccount?.account?.address;

    const capabilities = await provider.request({
      method: "wallet_getCapabilities",
      params: [address],
    });

    const baseCapabilities = capabilities[base.constants.CHAIN_IDS.base];

    if (baseCapabilities?.atomicBatch?.supported) {
      console.log("Atomic batching is supported");
      return true;
    } else {
      console.log("Atomic batching is not supported");
      return false;
    }
  } catch (error) {
    console.error("Failed to check capabilities:", error);
    return false;
  }
}

Non-Atomic Batching

Sometimes you want calls to execute sequentially, even if some fail:
batchTransactions.tsx
const result = await provider.request({
  method: "wallet_sendCalls",
  params: [
    {
      version: "2.0.0",
      from: fromAddress,
      chainId: numberToHex(base.constants.CHAIN_IDS.base),
      atomicRequired: false, // Allow partial execution
      calls: calls,
    },
  ],
});

Getting the Batch Transaction Result

wallet_getCallsStatus returns the execution status for a batch you previously submitted with wallet_sendCalls. Capture the callsId returned by wallet_sendCalls, then poll for the batch status until it is confirmed or fails.
batchTransactions.tsx
async function trackBatchTransaction(
  calls: Array<{
    to: `0x${string}`;
    data: `0x${string}`;
    value?: `0x${string}`;
  }>
) {
  const cryptoAccount = await getCryptoKeyAccount();
  const fromAddress = cryptoAccount?.account?.address;

  const callsId = await provider.request({
    method: "wallet_sendCalls",
    params: [
      {
        version: "2.0.0",
        from: fromAddress,
        chainId: numberToHex(base.constants.CHAIN_IDS.base),
        atomicRequired: true,
        calls,
      },
    ],
  });

  try {
    const status = await provider.request({
      method: "wallet_getCallsStatus",
      params: [callsId],
    });

    if (status.status === 200) {
      console.log("Batch completed successfully", status.receipts);
    } else if (status.status === 100) {
      console.log("Batch still pending", status.id);
    } else {
      console.error("Batch failed", status.status);
    }

    return status;
  } catch (error: any) {
    if (error.code === 4200) {
      throw new Error("No batch found for the provided callsId.");
    }

    if (error.code === 4100) {
      throw new Error(
        "The connected wallet does not support wallet_getCallsStatus."
      );
    }

    if (error.code === -32602) {
      throw new Error("The callsId parameter is invalid.");
    }

    throw error;
  }
}
You can learn more about wallet_getCallsStatus in the reference documentation.

Video Guide