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 { Erc7677Paymaster } from "abstractionkit";

const paymasterRPC = process.env.PAYMASTER_URL 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.

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