Skip to content
Alchemy Logo

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.

  • 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

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.

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.

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.

import { ethers } from "ethers";
 
// --- Constants ---
 
// Address of the ERC-4337 EntryPoint contract
const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789";
 
// ABI for the EntryPoint contract, specifically for the getNonce function
const ENTRYPOINT_ABI = [
  {
    type: "function",
    name: "getNonce",
    inputs: [
      { name: "sender", type: "address", internalType: "address" },
      { name: "key", type: "uint192", internalType: "uint192" },
    ],
    outputs: [
      {
        name: "nonce",
        type: "uint256",
        internalType: "uint256",
      },
    ],
    stateMutability: "view",
  },
] as const;
 
// Alchemy RPC URL for Sepolia testnet
const ALCHEMY_RPC_URL = "YOUR_ALCHEMY_RPC_URL";
// Alchemy Gas Manager RPC URL for Sepolia testnet
const ALCHEMY_GAS_MANAGER_URL = "YOUR_ALCHEMY_GAS_MANAGER_URL";
 
// Policy ID for the Alchemy Gas Manager
const ALCHEMY_POLICY_ID = "YOUR_POLICY_ID";
 
// Address of the ERC20 token to be used for gas payment
const ERC20_TOKEN_ADDRESS = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; // USDC
 
// --- Types ---
 
interface UserOperation {
  sender: string;
  nonce: string;
  initCode: string;
  callData: string;
  signature: string;
  paymasterAndData?: string;
  preVerificationGas?: string;
  verificationGasLimit?: string;
  callGasLimit?: string;
  maxFeePerGas?: string;
  maxPriorityFeePerGas?: string;
}
 
interface GasAndPaymasterData {
  paymasterAndData: string;
  preVerificationGas: string;
  verificationGasLimit: string;
  callGasLimit: string;
  maxFeePerGas: string;
  maxPriorityFeePerGas: string;
}
 
// --- Ethers.js Setup ---
 
// Initialize a JSON RPC provider
const provider = new ethers.JsonRpcProvider(ALCHEMY_RPC_URL);
 
// Create an ethers.js contract instance for the EntryPoint contract
const entryPoint = new ethers.Contract(
  ENTRYPOINT_ADDRESS,
  ENTRYPOINT_ABI,
  provider,
);
 
// --- Alchemy API Functions ---
 
/**
 * Requests gas fee estimations and paymaster data from Alchemy.
 * This function constructs and sends a request to the 'alchemy_requestGasAndPaymasterAndData' RPC method.
 */
async function requestGasAndPaymaster(
  uo: UserOperation,
): Promise<GasAndPaymasterData> {
  const body = JSON.stringify({
    id: 1,
    jsonrpc: "2.0",
    method: "alchemy_requestGasAndPaymasterAndData",
    params: [
      {
        policyId: ALCHEMY_POLICY_ID,
        userOperation: {
          sender: uo.sender,
          nonce: uo.nonce,
          initCode: uo.initCode,
          callData: uo.callData,
        },
        erc20Context: {
          tokenAddress: ERC20_TOKEN_ADDRESS,
        },
        entryPoint: ENTRYPOINT_ADDRESS,
        dummySignature: uo.signature,
      },
    ],
  });
 
  const options = {
    method: "POST",
    headers: { accept: "application/json", "content-type": "application/json" },
    body,
  };
 
  const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
  const jsonRes = await res.json();
  console.log("Alchemy Gas and Paymaster Response:", jsonRes);
  return jsonRes.result;
}
 
/**
 * Sends a user operation to the bundler via Alchemy.
 * This function constructs and sends a request to the 'eth_sendUserOperation' RPC method.
 */
async function sendUserOperation(uo: UserOperation): Promise<void> {
  const body = JSON.stringify({
    id: 1,
    jsonrpc: "2.0",
    method: "eth_sendUserOperation",
    params: [uo, ENTRYPOINT_ADDRESS],
  });
 
  const options = {
    method: "POST",
    headers: { accept: "application/json", "content-type": "application/json" },
    body,
  };
 
  const res = await fetch(ALCHEMY_GAS_MANAGER_URL, options);
  const jsonRes = await res.json();
  console.log("Alchemy Send UserOperation Response:", jsonRes);
}
 
// --- Main Script Execution ---
 
// Define the initial user operation object
// This object contains the core details of the transaction to be executed.
const userOp: UserOperation = {
  sender: "0xYOUR_SMART_ACCOUNT_ADDRESS", // Smart account address
  nonce: "0x", // Initial nonce (will be updated)
  initCode: "0x", // Set to "0x" if the smart account is already deployed
  callData: "0xYOUR_CALL_DATA", // Encoded function call data
  signature: "0xYOUR_DUMMY_SIGNATURE", // Dummy signature, should be replaced after requesting paymaster data
};
 
// IIFE (Immediately Invoked Function Expression) to run the async operations
(async () => {
  // Fetch the current nonce for the sender address from the EntryPoint contract
  const nonce = BigInt(await entryPoint.getNonce(userOp.sender, 0));
  userOp.nonce = "0x" + nonce.toString(16); // Update userOp with the correct nonce
 
  console.log("Fetching paymaster data and gas estimates...");
  // Request paymaster data and gas estimations from Alchemy
  const paymasterAndGasData = await requestGasAndPaymaster(userOp);
 
  // Combine the original userOp with the data returned by Alchemy (paymasterAndData, gas limits, etc.)
  const userOpWithGas: UserOperation = { ...userOp, ...paymasterAndGasData };
 
  console.log(
    "Final UserOperation with Gas and Paymaster Data:",
    JSON.stringify(userOpWithGas, null, 2),
  );
  console.log("EntryPoint Address used for submission: ", ENTRYPOINT_ADDRESS);
 
  // The script currently stops here. Uncomment the line below to actually send the UserOperation.
  // Make sure your account is funded with the ERC20 token and has approved the paymaster.
  return; // Intentionally stopping before sending for review. Remove this line to proceed.
 
  // userOpWithGas.signature = await sign(userOpWithGas);
  // await sendUserOperation(userOpWithGas);
})();
Was this page helpful?