How to pay gas with any 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

1const { ethers } = require("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];
27
28// Alchemy RPC URL for Sepolia testnet
29const ALCHEMY_RPC_URL = "";
30// Alchemy Gas Manager RPC URL for Sepolia testnet
31const ALCHEMY_GAS_MANAGER_URL = "";
32
33// Policy ID for the Alchemy Gas Manager
34const ALCHEMY_POLICY_ID = "";
35
36// Address of the ERC20 token to be used for gas payment
37const ERC20_TOKEN_ADDRESS = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; // USDC
38
39// --- Ethers.js Setup ---
40
41// Initialize a JSON RPC provider
42const provider = new ethers.JsonRpcProvider(ALCHEMY_RPC_URL);
43
44// Create an ethers.js contract instance for the EntryPoint contract
45const entryPoint = new ethers.Contract(
46 ENTRYPOINT_ADDRESS,
47 ENTRYPOINT_ABI,
48 provider,
49);
50
51// --- Alchemy API Functions ---
52
53/**
54 * Requests gas fee estimations and paymaster data from Alchemy.
55 * This function constructs and sends a request to the 'alchemy_requestGasAndPaymasterAndData' RPC method.
56 * @param {object} uo - The user operation object.
57 * @returns {Promise<object>} A promise that resolves to the result object from Alchemy,
58 * containing paymasterAndData and gas estimations.
59 */
60async function requestGasAndPaymaster(uo) {
61 const body = JSON.stringify({
62 id: 1,
63 jsonrpc: "2.0",
64 method: "alchemy_requestGasAndPaymasterAndData",
65 params: [
66 {
67 policyId: ALCHEMY_POLICY_ID,
68 userOperation: {
69 sender: uo.sender,
70 nonce: uo.nonce,
71 initCode: uo.initCode,
72 callData: uo.callData,
73 },
74 erc20Context: {
75 tokenAddress: ERC20_TOKEN_ADDRESS,
76 },
77 entryPoint: ENTRYPOINT_ADDRESS,
78 dummySignature: uo.signature,
79 },
80 ],
81 });
82
83 const options = {
84 method: "POST",
85 headers: { accept: "application/json", "content-type": "application/json" },
86 body,
87 };
88
89 const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
90 const jsonRes = await res.json();
91 console.log("Alchemy Gas and Paymaster Response:", jsonRes);
92 return jsonRes.result;
93}
94
95/**
96 * Sends a user operation to the bundler via Alchemy.
97 * This function constructs and sends a request to the 'eth_sendUserOperation' RPC method.
98 * @param {object} uo - The complete user operation object, including paymaster data and gas estimations.
99 * @returns {Promise<void>} A promise that resolves when the request is sent.
100 * The bundler's response (including userOpHash) is logged to the console.
101 */
102async function sendUserOperation(uo) {
103 const body = JSON.stringify({
104 id: 1,
105 jsonrpc: "2.0",
106 method: "eth_sendUserOperation",
107 params: [uo, ENTRYPOINT_ADDRESS],
108 });
109
110 const options = {
111 method: "POST",
112 headers: { accept: "application/json", "content-type": "application/json" },
113 body,
114 };
115
116 const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
117 const jsonRes = await res.json();
118 console.log("Alchemy Send UserOperation Response:", jsonRes);
119}
120
121// --- Main Script Execution ---
122
123// Define the initial user operation object
124// This object contains the core details of the transaction to be executed.
125const userOp = {
126 sender: "0x", // Smart account address
127 nonce: "0x", // Initial nonce (will be updated)
128 initCode: "0x", // Set to "0x" if the smart account is already deployed
129 callData: "0x", // Encoded function call data
130 signature: "0x", // Dummy signature, should be replaced after requesting paymaster data
131};
132
133// IIFE (Immediately Invoked Function Expression) to run the async operations
134(async () => {
135 // Fetch the current nonce for the sender address from the EntryPoint contract
136 const nonce = BigInt(await entryPoint.getNonce(userOp.sender, 0));
137 userOp.nonce = "0x" + nonce.toString(16); // Update userOp with the correct nonce
138
139 console.log("Fetching paymaster data and gas estimates...");
140 // Request paymaster data and gas estimations from Alchemy
141 const paymasterAndGasData = await requestGasAndPaymaster(userOp);
142
143 // Combine the original userOp with the data returned by Alchemy (paymasterAndData, gas limits, etc.)
144 const userOpWithGas = { ...userOp, ...paymasterAndGasData }; // Spread order matters: userOp properties can be overwritten by paymasterAndGasData if names clash (e.g. if userOp had gas fields)
145
146 console.log(
147 "Final UserOperation with Gas and Paymaster Data:",
148 JSON.stringify(userOpWithGas, null, 2),
149 );
150 console.log("EntryPoint Address used for submission: ", ENTRYPOINT_ADDRESS);
151
152 // The script currently stops here. Uncomment the line below to actually send the UserOperation.
153 // Make sure your account is funded with the ERC20 token and has approved the paymaster.
154 return; // Intentionally stopping before sending for review. Remove this line to proceed.
155
156 // userOpWithGas.signature = await sign(userOpWithGas);
157 // await sendUserOperation(userOpWithGas);
158})();