Guide to Paying Gas for Users with Base Paymaster

This guide shows you how to sponsor transaction fees for users with Base Paymaster, making your dApp more user-friendly.

When interacting with a blockchain, users are required to pay transaction (gas) fees. While these fees are typically low on Base, often less than $0.01, they can still be confusing or intimidating, especially for non-Web3-native users. Requiring users to fund a wallet before engaging with your app can create friction and negatively impact the user experience (UX). Paymaster allows you to sponsor up to $15k monthly on mainnet (unlimited on testnet). Get Gas Fees Covered. Apply Now

Prerequisites

To follow this tutorial, you should already have:

  1. A Coinbase Cloud Developer Platform (CDP) Account

    If you have not, sign up on the CDP Portal. After signing up, you can manage projects and utilize different tools like the paymaster.

  2. A basic understanding of Smart Accounts and how ERC-4337 works

    Smart Accounts are the foundation of advanced transaction flows like gas sponsorship and transaction batching. This tutorial builds on the ERC-4337 account abstraction standard, so if you’re unfamiliar with it, we recommend checking out resources like the official EIP-4337 explainer before getting started.

  3. Foundry

    Foundry is a development environment, testing framework, and smart contract toolkit for Ethereum. You’ll need it installed locally for generating key pairs and interacting with smart contracts.

Set Up Gas Sponsorship with Base Paymaster

This section walks you through setting up gas sponsorship using Base Paymaster. You’ll configure your project to sponsor transactions on behalf of users by interacting with the Paymaster and bundler services provided by the Coinbase Developer Platform.

  1. Go to the Coinbase Developer Platform.

  2. Create or select your project from the upper-left corner.

  3. From the left-hand menu, hover on “Base Tools” and click “Paymaster”

  4. Navigate to the configuration tab and copy the RPC_URL, you’ll use it in your code to connect to the bundler and paymaster Services.

Screenshots

  1. Selecting your project

  2. Navigate to base tools > paymaster

  3. Configuration screen

Allowlist a Contract for Sponsorship

  1. On the configuration page, make sure Base Mainnet is selected. Select Base Sepolia if you’re testing (testnet)

  2. Turn on your Paymaster by toggling the Enable Paymaster switch

  3. Click “Add” to allowlist a contract that can receive sponsored transactions

  4. For this example, use the

    contract name: DemoNFT

    contract address:0xd33bb1C4c438600db4611bD144fDF5048f6C8E44
    Then add the function you want to sponsor: mintTo(address).

This ensures your Paymaster only covers gas fees for specific functions you approve — in this case, the mintTo function on the DemoNFT contract.
Note: If you leave the function input box empty, the Paymaster will sponsor all functions on the contract, so use this with caution.

Note: The contract name, address, and function above are from our example NFT contract on Base Mainnet. You should replace them with the details of your own contract.

Global & Per User Limits

Scroll to Per User Limit to set:

  • Max USD (e.g., $0.05)

  • Max UserOperations (e.g., 1)

  • Reset cycle (daily, weekly, or monthly)

This means each user gets $0.05 and 1 sponsored transaction per cycle.

Then set the Global Limit (e.g., $0.07). Once reached, no more sponsorships happen until the limit is increased.

Implementing Gas Sponsorship on the Backend

Installing Foundry

  1. Ensure you have Rust installed

     curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  2. Install Foundry

     curl -L https://foundry.paradigm.xyz | bash
     foundryup
    
  3. Verify it works

     cast --help
    

    You should see Foundry usage information voilà, you’re good to go!

Create Your Project and Generate Key Pairs

  1. Create a folder and install dependencies, viem and dotenv:

     mkdir viem-gasless
     cd viem-gasless
     npm install viem
     npm install dotenv
     touch config.js
     touch index.js
     touch example-app-abi.js
    
  2. Generate a key pair with foundry

     cast wallet new
    
  3. You’ll see something like:

     Successfully created new keypair.
     Address:     0xa05Ed6858568cbc14cfEd559C068E02e95521De4
     Private key: 0xf920002d619ca04287848a9...
    

    Store these private keys safe

Create a .env file in the viem-gasless folder. In the .env, you’ll add the rpcURL for your paymaster and the private key for your account. The rpcURL can be found from the CDP Portal, it follows the pattern https://api.developer.coinbase.com/rpc/v1/base/<SPECIAL-KEY>. That way our .env would look like this:

RPC_URL=https://api.developer.coinbase.com/rpc/v1/base/<SPECIAL-KEY>
OWNER_PRIVATE_KEY=0xf920002d619ca04287848a9...

.env should not be committed to a public repo. Do not forget to add it to .gitignore

Example config.js

import { createPublicClient, http } from "viem";
import { toCoinbaseSmartAccount } from "viem/account-abstraction";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import dotenv from 'dotenv';

// Load environment variables from .env file
dotenv.config();

const RPC_URL = process.env.RPC_URL;
const OWNER_PRIVATE_KEY = process.env.OWNER_PRIVATE_KEY;

if (!RPC_URL) {
    throw new Error('RPC_URL is not set in environment variables');
}

if (!OWNER_PRIVATE_KEY) {
    throw new Error('OWNER_PRIVATE_KEY is not set in environment variables');
}

export { RPC_URL };

export const client = createPublicClient({
    chain: base,
    transport: http(RPC_URL),
});

const owner = privateKeyToAccount(OWNER_PRIVATE_KEY);

export const account = await toCoinbaseSmartAccount({
    client,
    owners: [owner],
});

example-app-abi-.js

export const abi = [
  {
    inputs: [
      {
        internalType: "address",
        name: "to",
        type: "address",
      },
    ],
    name: "mintTo",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      {
        internalType: "address",
        name: "owner",
        type: "address",
      },
    ],
    name: "balanceOf",
    outputs: [
      {
        internalType: "uint256",
        name: "",
        type: "uint256",
      },
    ],
    stateMutability: "view",
    type: "function",
  },
];

index.js

import { http } from "viem";
import { base } from "viem/chains";
import { createBundlerClient } from "viem/account-abstraction";
import { account, client, RPC_URL } from "./config.js";
import { abi } from "./example-app-abi.js";

console.log("🚀 Starting gasless NFT minting process...");

const bundlerClient = createBundlerClient({
  account,
  client,
  transport: http(RPC_URL),
  chain: base,
});

const nftContractAddress = "0xd33bb1C4c438600db4611bD144fDF5048f6C8E44"; // DEMO NFT Contract Deployed on Mainnet (base)

const mintTo = {
  abi: abi,
  functionName: "mintTo",
  to: nftContractAddress,
  args: [account.address],
};
const calls = [mintTo];

account.userOperation = {
  estimateGas: async (userOperation) => {
    const estimate = await bundlerClient.estimateUserOperationGas(
      userOperation
    );
    estimate.preVerificationGas = estimate.preVerificationGas * 2n;
    return estimate;
  },
};

try {
  console.log("📤 Sending user operation...");

  const userOpHash = await bundlerClient.sendUserOperation({
    account,
    calls,
    paymaster: true,
  });

  console.log("⏳ Waiting for transaction receipt...");

  const receipt = await bundlerClient.waitForUserOperationReceipt({
    hash: userOpHash,
  });

  console.log("✅ Transaction successfully sponsored");
  console.log(
    `⛽ View sponsored UserOperation on blockscout: https://base.blockscout.com/op/${receipt.userOpHash}`
  );
  process.exit(0);
} catch (error) {
  console.error("❌ Error sending transaction: ", error);
  process.exit(1);
}

Now that the code is implemented, lets run it: Run this via node index.js from your project root.

node index.js

To test if your spend policy is working, set it to allow just 1 transaction per user. When you run the script again, you should get a "request denied" error — that means it’s working!

Want to see full examples? We’ve got setups for Wagmi + Vite, Wagmi + Next.js, and even server-side integration.
Check them out on GitHub

1
Subscribe to my newsletter

Read articles from Ogedengbe Israel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ogedengbe Israel
Ogedengbe Israel

Blockchain developer && Advocate