Skip to main content

Pay Gas with ERC-20 Tokens

Enable users to pay gas fees with ERC-20 tokens instead of native tokens using Candide's Token Paymaster.

For the basics, see the Getting Started Guide.

Why Use Token Paymaster

Token paymasters enable users to pay gas fees with ERC-20 tokens instead of ETH, eliminating the need to hold native tokens for transactions.

  • Better UX: Users transact with only the tokens they need
  • Cost Calculation: Calculate exact token amounts for gas costs
  • 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

Get ERC-20 faucet tokens (CTT or USDT) from our dashboard faucet 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)