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
- index.ts
- .env
Token paymaster implementation
import { Erc7677Paymaster } from "abstractionkit";
const paymasterRPC = process.env.PAYMASTER_RPC as string;
const tokenAddress = process.env.TOKEN_ADDRESS as string;
const paymaster = new Erc7677Paymaster(paymasterRPC);
// Pass any supported token address. Erc7677Paymaster fetches the quote
// and prepends the required ERC-20 approval automatically.
console.log(`Using ${tokenAddress} 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.
# Paymaster service endpoint
PAYMASTER_RPC=https://api.candide.dev/public/v3/11155111
# Token address for gas payment (CTT on Sepolia for testing)
TOKEN_ADDRESS=0xFa5854FBf9964330d761961F46565AB7326e5a3b
Step2: Get Token Paymaster Data
Get token paymaster data
const { userOperation: tokenOp, tokenQuote } = await paymaster.createPaymasterUserOperation(
smartAccount,
userOperation,
bundlerUrl,
{ token: tokenAddress },
);
userOperation = tokenOp;
console.log(`Max token cost: ${tokenQuote?.tokenCost} (smallest units)`);
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 {
SafeMultiChainSigAccountV1 as SafeAccount,
MetaTransaction,
Erc7677Paymaster,
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 Erc7677Paymaster(paymasterRPC)
console.log(`Using ${tokenAddress} for gas payment`);
// Create token paymaster UserOperation
const { userOperation: tokenOp, tokenQuote } = await paymaster.createPaymasterUserOperation(
smartAccount,
userOperation,
bundlerUrl,
{ token: tokenAddress },
);
userOperation = tokenOp;
console.log(`Max token cost: ${tokenQuote?.tokenCost} (smallest units)`);
console.log(`Gas cost: ${tokenQuote?.tokenCost} in token smallest units`);
console.log(`Make sure account has enough 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 token ${tokenAddress}`);
} else {
console.log("UserOperation execution failed")
}
}
main().catch(console.error)
CHAIN_ID=11155111
BUNDLER_URL=https://api.candide.dev/public/v3/11155111
JSON_RPC_NODE_PROVIDER=https://ethereum-sepolia-rpc.publicnode.com
# Paymaster configuration
PAYMASTER_RPC=https://api.candide.dev/public/v3/11155111
TOKEN_ADDRESS=0xFa5854FBf9964330d761961F46565AB7326e5a3b
# Your EOA credentials
PRIVATE_KEY=your_private_key_here
PUBLIC_ADDRESS=your_public_address_here