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:
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.
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.
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.
Go to the Coinbase Developer Platform.
Create or select your project from the upper-left corner.
From the left-hand menu, hover on “Base Tools” and click “Paymaster”
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
Selecting your project
Navigate to base tools > paymaster
Configuration screen
Allowlist a Contract for Sponsorship
On the configuration page, make sure Base Mainnet is selected. Select Base Sepolia if you’re testing (testnet)
Turn on your Paymaster by toggling the Enable Paymaster switch
Click “Add” to allowlist a contract that can receive sponsored transactions
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 theDemoNFT
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
Ensure you have Rust installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Install Foundry
curl -L https://foundry.paradigm.xyz | bash foundryup
Verify it works
cast --help
You should see Foundry usage information voilà, you’re good to go!
Create Your Project and Generate Key Pairs
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
Generate a key pair with foundry
cast wallet new
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
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