Pay Gas with Any ERC20 Token

Learn how to enable gas payments with ERC-20 tokens.

Gas fees paid in the native gas token can feel foreign to users that primarily hold stablecoins or your app’s own token. With our smart wallet, you can enable your users to pay gas with ERC-20 tokens beyond the native gas token, like USDC or your own custom tokens, streamlining the user experience.

How it works: We front the gas using the network’s native gas token and transfer the ERC-20 tokens from the user’s wallet to a wallet you control. The equivalent USD amount and the admin fee is then added to your monthly invoice.

[Recommended] Use our SDK to create and use wallets. The SDK handles all complexity for you, making development faster and easier.

If you want to use APIs directly, follow these steps.

Steps

1. Get an API key

  • Get you API Key by creating an app in your Alchemy Dashboard
  • Make sure you enable the networks you are building on under the Networks tab

2. Create a Gas Manager policy

To enable your users to pay gas using an ERC-20 token, you need to create a “Pay gas with any token” Policy via the Gas Manager dashboard. You can customize the policy with the following:

  • Receiving address: an address of your choosing where the users’ ERC20 tokens will be sent to as they pay for gas (this is orchestrated by the paymaster contract and happens automatically at the time of the transaction).
  • Tokens: the tokens the user should be able to pay gas with. Learn more here.
  • ERC-20 transfer mode: choose when the user’s token payment occurs.
    • [Recommended] After: No upfront allowance is required. The user signs an approval inside the same user operation batch, and the paymaster pulls the token after the operation has executed. If that post-execution transfer fails, the entire user operation is reverted and you still pay the gas fee.
    • Before: You (the developer) must ensure the paymaster already has sufficient allowance—either through a prior approve() transaction or a permit signature—before the UserOperation is submitted. If the required allowance isn’t in place when the user operation is submitted, it will be rejected upfront.
  • Sponsorship expiry period: this is the period for which the Gas Manager signature and ERC-20 exchange rate will remain valid once generated.

Now you should have a Gas policy created with a policy id you can use to enable gas payments with ERC-20 tokens.

3. Get Gas Manager’s signature

When sending a userOperation, you can specify the paymaster and paymasterData fields in the userOp object. These fields are related to the signature of the Gas Manager that enables the user to pay for gas with ERC-20 tokens.

You can get these fields through alchemy_requestGasAndPaymasterAndData using your Gas Manager Policy id, the API key of the app associated with the policy, a userOperation, the address of the EntryPoint contract, and the address of the ERC-20 token. You can find an example script below.

4. Send the userOp

Once you get the paymaster and paymasterData fields, you can use them in your userOperation when you call eth_sendUserOperation. You can find an example script below.

Example script

1import { ethers } from "ethers";
2
3// --- Constants ---
4
5// Address of the ERC-4337 EntryPoint contract
6const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
7
8// ABI for the EntryPoint contract, specifically for the getNonce function
9const ENTRYPOINT_ABI = [
10 {
11 type: "function",
12 name: "getNonce",
13 inputs: [
14 { name: "sender", type: "address", internalType: "address" },
15 { name: "key", type: "uint192", internalType: "uint192" },
16 ],
17 outputs: [
18 {
19 name: "nonce",
20 type: "uint256",
21 internalType: "uint256",
22 },
23 ],
24 stateMutability: "view",
25 },
26] as const;
27
28// Alchemy RPC URL for Sepolia testnet
29const ALCHEMY_RPC_URL = "YOUR_ALCHEMY_RPC_URL";
30// Alchemy Gas Manager RPC URL for Sepolia testnet
31const ALCHEMY_GAS_MANAGER_URL = "YOUR_ALCHEMY_GAS_MANAGER_URL";
32
33// Policy ID for the Alchemy Gas Manager
34const ALCHEMY_POLICY_ID = "YOUR_POLICY_ID";
35
36// Address of the ERC20 token to be used for gas payment
37const ERC20_TOKEN_ADDRESS = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; // USDC
38
39// --- Types ---
40
41interface UserOperation {
42 sender: string;
43 nonce: string;
44 initCode: string;
45 callData: string;
46 signature: string;
47 paymasterAndData?: string;
48 preVerificationGas?: string;
49 verificationGasLimit?: string;
50 callGasLimit?: string;
51 maxFeePerGas?: string;
52 maxPriorityFeePerGas?: string;
53}
54
55interface GasAndPaymasterData {
56 paymasterAndData: string;
57 preVerificationGas: string;
58 verificationGasLimit: string;
59 callGasLimit: string;
60 maxFeePerGas: string;
61 maxPriorityFeePerGas: string;
62}
63
64// --- Ethers.js Setup ---
65
66// Initialize a JSON RPC provider
67const provider = new ethers.JsonRpcProvider(ALCHEMY_RPC_URL);
68
69// Create an ethers.js contract instance for the EntryPoint contract
70const entryPoint = new ethers.Contract(
71 ENTRYPOINT_ADDRESS,
72 ENTRYPOINT_ABI,
73 provider,
74);
75
76// --- Alchemy API Functions ---
77
78/**
79 * Requests gas fee estimations and paymaster data from Alchemy.
80 * This function constructs and sends a request to the 'alchemy_requestGasAndPaymasterAndData' RPC method.
81 */
82async function requestGasAndPaymaster(
83 uo: UserOperation,
84): Promise<GasAndPaymasterData> {
85 const body = JSON.stringify({
86 id: 1,
87 jsonrpc: "2.0",
88 method: "alchemy_requestGasAndPaymasterAndData",
89 params: [
90 {
91 policyId: ALCHEMY_POLICY_ID,
92 userOperation: {
93 sender: uo.sender,
94 nonce: uo.nonce,
95 initCode: uo.initCode,
96 callData: uo.callData,
97 },
98 erc20Context: {
99 tokenAddress: ERC20_TOKEN_ADDRESS,
100 },
101 entryPoint: ENTRYPOINT_ADDRESS,
102 dummySignature: uo.signature,
103 },
104 ],
105 });
106
107 const options = {
108 method: "POST",
109 headers: { accept: "application/json", "content-type": "application/json" },
110 body,
111 };
112
113 const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
114 const jsonRes = await res.json();
115 console.log("Alchemy Gas and Paymaster Response:", jsonRes);
116 return jsonRes.result;
117}
118
119/**
120 * Sends a user operation to the bundler via Alchemy.
121 * This function constructs and sends a request to the 'eth_sendUserOperation' RPC method.
122 */
123async function sendUserOperation(uo: UserOperation): Promise<void> {
124 const body = JSON.stringify({
125 id: 1,
126 jsonrpc: "2.0",
127 method: "eth_sendUserOperation",
128 params: [uo, ENTRYPOINT_ADDRESS],
129 });
130
131 const options = {
132 method: "POST",
133 headers: { accept: "application/json", "content-type": "application/json" },
134 body,
135 };
136
137 const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
138 const jsonRes = await res.json();
139 console.log("Alchemy Send UserOperation Response:", jsonRes);
140}
141
142// --- Main Script Execution ---
143
144// Define the initial user operation object
145// This object contains the core details of the transaction to be executed.
146const userOp: UserOperation = {
147 sender: "0xYOUR_SMART_ACCOUNT_ADDRESS", // Smart account address
148 nonce: "0x", // Initial nonce (will be updated)
149 initCode: "0x", // Set to "0x" if the smart account is already deployed
150 callData: "0xYOUR_CALL_DATA", // Encoded function call data
151 signature: "0xYOUR_DUMMY_SIGNATURE", // Dummy signature, should be replaced after requesting paymaster data
152};
153
154// IIFE (Immediately Invoked Function Expression) to run the async operations
155(async () => {
156 // Fetch the current nonce for the sender address from the EntryPoint contract
157 const nonce = BigInt(await entryPoint.getNonce(userOp.sender, 0));
158 userOp.nonce = "0x" + nonce.toString(16); // Update userOp with the correct nonce
159
160 console.log("Fetching paymaster data and gas estimates...");
161 // Request paymaster data and gas estimations from Alchemy
162 const paymasterAndGasData = await requestGasAndPaymaster(userOp);
163
164 // Combine the original userOp with the data returned by Alchemy (paymasterAndData, gas limits, etc.)
165 const userOpWithGas: UserOperation = { ...userOp, ...paymasterAndGasData };
166
167 console.log(
168 "Final UserOperation with Gas and Paymaster Data:",
169 JSON.stringify(userOpWithGas, null, 2),
170 );
171 console.log("EntryPoint Address used for submission: ", ENTRYPOINT_ADDRESS);
172
173 // The script currently stops here. Uncomment the line below to actually send the UserOperation.
174 // Make sure your account is funded with the ERC20 token and has approved the paymaster.
175 return; // Intentionally stopping before sending for review. Remove this line to proceed.
176
177 // userOpWithGas.signature = await sign(userOpWithGas);
178 // await sendUserOperation(userOpWithGas);
179})();