Skip to main content

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

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

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
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)