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