Skip to main content

How to Build a Gasless Web3 App

Building a gasless Web3 app requires:
ComponentPurposeBiconomy Solution
Smart AccountAccount abstraction enabled walletNexus Smart Account
Execution EnvironmentHandles transaction orchestration, gas sponsorship, and cross-chain executionMEE (Modular Execution Environment)
SDKDeveloper integrationAbstractJS SDK
Biconomy’s MEE provides all the functionality of traditional ERC-4337 bundlers and paymasters in a unified, easier-to-use package. Instead of configuring separate bundler and paymaster services, MEE handles everything automatically.
You’ll also need:
  • A Biconomy dashboard account for API keys
  • A sponsorship policy (who/what to sponsor)
  • Frontend integration (React, Vue, vanilla JS, etc.)
Step 1: Install dependencies
npm install @biconomy/abstractjs viem
Step 2: Get API keys from Biconomy Dashboard
  1. Go to dashboard.biconomy.io
  2. Create a new project
  3. Get your MEE API key
  4. Configure sponsorship policies
Step 3: Initialize the SDK with MEE
import { createMeeClient, toMultichainNexusAccount } from "@biconomy/abstractjs";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

// Create multichain account
const account = await toMultichainNexusAccount({
  signer: privateKeyToAccount("0x..."), // User's signer
  chains: [base]
});

// Create MEE client - handles all bundler and paymaster functionality
const meeClient = await createMeeClient({ account });

console.log("Smart Account Address:", account.address);
MEE (Modular Execution Environment) replaces the need to configure separate bundlers and paymasters. It provides all ERC-4337 functionality plus cross-chain capabilities in a single, unified interface.
Once configured, sending gasless transactions is simple with MEE:
// The user pays NOTHING - gas is sponsored through MEE
const quote = await meeClient.getQuote({
  instructions: [
    {
      calls: [{
        to: "0xRecipientAddress",
        value: 0n,
        data: "0x" // Or encoded function call
      }]
    }
  ],
  feeToken: { address: "sponsored" } // Gas sponsored
});

const { hash } = await meeClient.executeQuote({ quote });
console.log("Transaction hash:", hash);
For contract interactions:
import { encodeFunctionData } from "viem";

// Encode your contract call
const data = encodeFunctionData({
  abi: yourContractABI,
  functionName: "mint",
  args: [userAddress, tokenId]
});

// Send gasless transaction via MEE
const quote = await meeClient.getQuote({
  instructions: [
    {
      calls: [{ to: contractAddress, value: 0n, data }]
    }
  ],
  feeToken: { address: "sponsored" }
});

const { hash } = await meeClient.executeQuote({ quote });
MEE automatically handles gas sponsorship based on your dashboard configuration.
Sponsorship policies control who and what gets sponsored. Configure in the Biconomy Dashboard:Dashboard Configuration:
  1. Navigate to MEE sponsorship settings
  2. Set spending limits (per transaction, total)
MEE handles gas sponsorship (traditionally done by ERC-4337 paymasters) as part of its unified execution environment. You configure policies in the dashboard, and MEE applies them automatically.
Policy Examples:Sponsor all transactions (development):
{
  "mode": "SPONSORED",
  "policy": {
    "type": "open"
  }
}
Sponsor with spending limits:
{
  "mode": "SPONSORED",
  "policy": {
    "type": "limited",
    "maxGasPerTransaction": "0.001",
    "maxTotalSpend": "10.0"
  }
}
For first-time users, the smart account needs to be deployed. MEE handles this automatically:
// First transaction deploys the account + executes the action
// All in one gasless transaction via MEE!
const quote = await meeClient.getQuote({
  instructions: [
    { calls: [{ to: contractAddress, data: mintCalldata }] }
  ],
  feeToken: { address: "sponsored" }
});

const { hash } = await meeClient.executeQuote({ quote });

// User experience:
// 1. User clicks "Mint NFT"
// 2. Signs one message (no gas needed)
// 3. Account deployed + NFT minted
// 4. Done! ✅
Best practices for onboarding:
async function onboardUser(email: string) {
  // 1. Create wallet with embedded solution (Privy, Dynamic, etc.)
  const wallet = await createEmbeddedWallet(email);
  
  // 2. Initialize MEE client
  const account = await toMultichainNexusAccount({
    signer: wallet,
    chains: [base]
  });
  const meeClient = await createMeeClient({ account });
  
  // 3. Pre-compute address (no deployment yet)
  const address = account.address;
  
  // 4. First gasless transaction deploys account
  // Schedule this for user's first meaningful action
  
  return { wallet, meeClient, address };
}
Batch operations save gas and improve UX with MEE:
// Execute multiple operations in ONE gasless transaction via MEE
const quote = await meeClient.getQuote({
  instructions: [
    {
      calls: [
        { to: tokenAddress, data: encodeApprove(spender, amount) },
        { to: vaultAddress, data: encodeDeposit(amount) },
        { to: rewardsAddress, data: encodeClaim() }
      ]
    }
  ],
  feeToken: { address: "sponsored" }
});

const { hash } = await meeClient.executeQuote({ quote });
// User signs once, all three actions execute atomically
Real-world example - NFT mint with approval:
const quote = await meeClient.getQuote({
  instructions: [
    {
      calls: [
        // 1. Approve payment token
        {
          to: usdcAddress,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "approve",
            args: [nftContract, mintPrice]
          })
        },
        // 2. Mint NFT
        {
          to: nftContract,
          data: encodeFunctionData({
            abi: nftAbi,
            functionName: "mint",
            args: [quantity]
          })
        }
      ]
    }
  ],
  feeToken: { address: "sponsored" }
});

// One click, one signature, gasless!
await meeClient.executeQuote({ quote });
Even for gasless transactions, showing users what’s happening builds trust:
// Get quote with cost estimate via MEE
const quote = await meeClient.getQuote({
  instructions: [
    { calls: [{ to: contractAddress, data: calldata }] }
  ],
  feeToken: { address: "sponsored" }
});

// Quote includes cost breakdown
console.log("Estimated cost:", quote.paymentInfo);
// Display to user
console.log(`Gas sponsored: $${quote.paymentInfo.usdCost.toFixed(2)}`);
// "Gas sponsored: $0.45"
UI Example:
function TransactionButton({ onSubmit }) {
  const [sponsoredAmount, setSponsoredAmount] = useState(null);
  
  return (
    <button onClick={onSubmit}>
      Mint NFT
      {sponsoredAmount && (
        <span className="sponsored-badge">
          Gas sponsored: ${sponsoredAmount}
        </span>
      )}
    </button>
  );
}
Common errors and how to handle them:
try {
  const quote = await meeClient.getQuote({
    instructions: [{ calls: [{ to: contractAddress, data: calldata }] }],
    feeToken: { address: "sponsored" }
  });
  const { hash } = await meeClient.executeQuote({ quote });
} catch (error) {
  if (error.message.includes("sponsorship denied")) {
    // User exceeded sponsorship limits
    showMessage("Daily free transactions exceeded. Please try again tomorrow.");
  } else if (error.message.includes("insufficient balance")) {
    // Contract requires token balance
    showMessage("Insufficient token balance for this action.");
  } else if (error.message.includes("user rejected")) {
    // User cancelled signature
    showMessage("Transaction cancelled.");
  } else {
    // Generic error
    showMessage("Transaction failed. Please try again.");
    console.error(error);
  }
}
Implement retry logic:
async function sendWithRetry(instructions, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const quote = await meeClient.getQuote({
        instructions,
        feeToken: { address: "sponsored" }
      });
      return await meeClient.executeQuote({ quote });
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}
Here’s a complete gasless NFT minting app using MEE:
// lib/biconomy.ts
import { createMeeClient, toMultichainNexusAccount } from "@biconomy/abstractjs";
import { base } from "viem/chains";

export async function createGaslessMeeClient(signer: any) {
  const account = await toMultichainNexusAccount({
    signer,
    chains: [base]
  });
  
  return createMeeClient({ account });
}
// components/MintButton.tsx
import { useState } from "react";
import { createGaslessMeeClient } from "@/lib/biconomy";
import { encodeFunctionData } from "viem";
import { nftAbi, NFT_ADDRESS } from "@/lib/contracts";

export function MintButton({ signer }) {
  const [loading, setLoading] = useState(false);
  const [txHash, setTxHash] = useState(null);

  async function handleMint() {
    setLoading(true);
    try {
      // Initialize MEE client
      const meeClient = await createGaslessMeeClient(signer);
      
      // Encode mint function
      const data = encodeFunctionData({
        abi: nftAbi,
        functionName: "mint",
        args: [1] // Mint 1 NFT
      });
      
      // Get quote and execute gasless transaction via MEE
      const quote = await meeClient.getQuote({
        instructions: [
          { calls: [{ to: NFT_ADDRESS, data, value: 0n }] }
        ],
        feeToken: { address: "sponsored" }
      });
      
      const { hash } = await meeClient.executeQuote({ quote });
      setTxHash(hash);
    } catch (error) {
      console.error("Mint failed:", error);
      alert("Minting failed. Please try again.");
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <button onClick={handleMint} disabled={loading}>
        {loading ? "Minting..." : "Mint NFT (Free!)"}
      </button>
      {txHash && (
        <p>
          Success! <a href={`https://basescan.org/tx/${txHash}`}>View transaction</a>
        </p>
      )}
    </div>
  );
}
MEE provides all the functionality of traditional ERC-4337 bundlers and paymasters in a single, unified interface. No need to configure separate bundler URLs or paymaster URLs.

Continue building