Pay Gas with ERC-20 Tokens
Learn how to enable users to pay gas fees with ERC-20 tokens instead of native tokens using Candide's Token Paymaster.
If you need help with the basics, check out the Getting Started Guide.

Why Use Token Paymaster
Token paymasters allow users to pay gas fees with ERC-20 tokens instead of ETH, removing the need for users to hold native tokens for transactions.
- Better UX: Users can transact with just the tokens they want to use
- Cost calculation: Know how much tokens will be spent on gas
- Supported tokens: USDC, USDT, and other popular ERC-20 tokens
Quickstart
You can also fork the complete code and follow along.
Step 1: Fetch Supported Tokens
- index.ts
- .env
Token paymaster implementation
import { CandidePaymaster } from "abstractionkit";
const paymasterRPC = process.env.PAYMASTER_RPC as string;
const tokenAddress = process.env.TOKEN_ADDRESS as string;
const paymaster = new CandidePaymaster(paymasterRPC);
const tokensSupported = await paymaster.fetchSupportedERC20TokensAndPaymasterMetadata();
// Find your specific token
const tokenSelected = tokensSupported.tokens.find(
token => token.address.toLowerCase() === tokenAddress.toLowerCase()
);
if (!tokenSelected) {
throw new Error("Token not supported by paymaster");
}
console.log(`Using ${tokenSelected.name} (${tokenSelected.symbol}) for gas payment`);
Test Tokens
Request Candide Test Tokens (CTT) on our Discord for testing. Ensure your smart account has sufficient ERC-20 tokens to pay for gas
# Paymaster service endpoint
PAYMASTER_RPC=https://api.candide.dev/public/v3/sepolia
# Token address for gas payment (CTT on Sepolia for testing)
TOKEN_ADDRESS=0xFa5854FBf9964330d761961F46565AB7326e5a3b
Step2: Get Token Paymaster Data
Get token paymaster data
if (tokenSelected) {
userOperation = await paymaster.createTokenPaymasterUserOperation(
smartAccount,
userOperation,
tokenSelected.address,
bundlerUrl
);
// Calculate the cost in tokens
const cost = await paymaster.calculateUserOperationErc20TokenMaxGasCost(
userOperation,
tokenSelected.address
);
console.log(`Gas cost: ${cost} wei in ${tokenSelected.symbol}`);
console.log(`Make sure your account has enough ${tokenSelected.symbol} tokens`);
}
Complete Runnable Example
Below is a complete example that demonstrates ERC-20 token gas payments:
Full Working Example
- index.ts
- .env
import * as dotenv from 'dotenv'
import {
SafeAccountV0_3_0 as SafeAccount,
MetaTransaction,
CandidePaymaster,
getFunctionSelector,
createCallData,
} from "abstractionkit";
async function main(): Promise<void> {
// Load environment variables
dotenv.config()
const chainId = BigInt(process.env.CHAIN_ID as string)
const bundlerUrl = process.env.BUNDLER_URL as string
const jsonRpcNodeProvider = process.env.JSON_RPC_NODE_PROVIDER as string
const paymasterRPC = process.env.PAYMASTER_RPC as string
const tokenAddress = process.env.TOKEN_ADDRESS as string
const ownerPublicAddress = process.env.PUBLIC_ADDRESS as string
const ownerPrivateKey = process.env.PRIVATE_KEY as string
// Create smart account
let smartAccount = SafeAccount.initializeNewAccount([ownerPublicAddress])
console.log("Smart Account Address:", smartAccount.accountAddress)
// Create example transactions (minting NFTs)
const nftContractAddress = "0x9a7af758aE5d7B6aAE84fe4C5Ba67c041dFE5336";
const mintFunctionSignature = 'mint(address)';
const mintFunctionSelector = getFunctionSelector(mintFunctionSignature);
const mintTransactionCallData = createCallData(
mintFunctionSelector,
["address"],
[smartAccount.accountAddress]
);
const transaction1: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}
const transaction2: MetaTransaction = {
to: nftContractAddress,
value: 0n,
data: mintTransactionCallData,
}
// Create UserOperation
let userOperation = await smartAccount.createUserOperation(
[transaction1, transaction2],
jsonRpcNodeProvider,
bundlerUrl
)
// Set up token paymaster
const paymaster = new CandidePaymaster(paymasterRPC)
// Get supported tokens and find the one we want to use
const tokensSupported = await paymaster.fetchSupportedERC20TokensAndPaymasterMetadata();
const tokenSelected = tokensSupported.tokens.find(
token => token.address.toLowerCase() === tokenAddress.toLowerCase()
);
if (!tokenSelected) {
throw new Error("Token not supported by paymaster");
}
console.log(`💰 Using ${tokenSelected.name} (${tokenSelected.symbol}) for gas payment`);
// Create token paymaster UserOperation
userOperation = await paymaster.createTokenPaymasterUserOperation(
smartAccount,
userOperation,
tokenSelected.address,
bundlerUrl
);
// Calculate the cost in tokens
const cost = await paymaster.calculateUserOperationErc20TokenMaxGasCost(
userOperation,
tokenSelected.address
);
console.log(`⛽ Gas cost: ${cost} wei in ${tokenSelected.symbol}`);
console.log(`🔍 Make sure account has enough ${tokenSelected.symbol} tokens`);
// Sign the UserOperation
userOperation.signature = smartAccount.signUserOperation(
userOperation,
[ownerPrivateKey],
chainId
)
// Submit the UserOperation
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation,
bundlerUrl
)
console.log("📤 UserOperation sent. Waiting for confirmation...")
// Wait for transaction to be included
let userOperationReceiptResult = await sendUserOperationResponse.included()
console.log("📋 UserOperation receipt received.")
console.log(userOperationReceiptResult)
if (userOperationReceiptResult.success) {
console.log("🎉 Transaction successful! Hash:", userOperationReceiptResult.receipt.transactionHash)
console.log(`💸 Gas paid with ${tokenSelected.symbol} tokens`);
} else {
console.log("❌ UserOperation execution failed")
}
}
main().catch(console.error)
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/public/v3/sepolia
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
# Paymaster configuration
PAYMASTER_RPC=https://api.candide.dev/public/v3/sepolia
TOKEN_ADDRESS=0xFa5854FBf9964330d761961F46565AB7326e5a3b
# Your EOA credentials
PRIVATE_KEY=your_private_key_here
PUBLIC_ADDRESS=your_public_address_here