Pay gas with any token

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 for gas with ERC-20 tokens beyond the native gas token, like USDC or your own custom tokens, streamlining the user experience.

How it works: To enable this capability you will need to create a Gas Manager policy. When a user pays for gas with an ERC-20 token, 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.

Goal

Configure your React app to enable users to pay gas fees with an ERC-20 token (for example USDC).

Prerequisites

  • Account Kit already installed and configured in your project.
  • An Alchemy API key.

Steps

1 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: you must specify an address you own where the users’ ERC20 tokens will be sent to as they pay for gas. The token transfer to this address is orchestrated by the paymaster contract and happens automatically at transaction time.
  • Tokens: you must select which tokens users 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.
Creating an ERC-20 sponsorship policy

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

viewing your ERC-20 sponsorship policy

2 Add the policy to your global config

import { 
const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
} from "@account-kit/react";
import {
const sepolia: Chain
sepolia
} from "@account-kit/infra";
export const
const config: AlchemyAccountsConfigWithUI
config
=
function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUI

Wraps the createConfig that is exported from @aa-sdk/core to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).

createConfig
({
apiKey: string
apiKey
: "ALCHEMY_API_KEY",
chain: Chain
chain
:
const sepolia: Chain
sepolia
,
policyId?: string | string[] | undefined
policyId
: "GAS_MANAGER_POLICY_ID", // your policy
policyToken: { address: string; maxTokenAmount: bigint; }
policyToken
: {
address: string
address
: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // USDC_ADDRESS on sepolia (can be any ERC-20 token address enabled in your policy)
maxTokenAmount: bigint
maxTokenAmount
: 10_000_000n, // Safety limit. If using USDC, this is 10 USDC (10 * 10^6).
}, });

All UserOperations sent with the hooks will now be paid with the token configured above.

3 Send a sponsored UserOperation

import React from "react";
import { 
function useSendUserOperation<TEntryPointVersion extends GetEntryPointFromAccount<TAccount>, TAccount extends SupportedAccounts = SupportedAccounts>(params: UseSendUserOperationArgs<TEntryPointVersion, TAccount>): UseSendUserOperationResult<TEntryPointVersion, TAccount>

A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.

useSendUserOperation
} from "@account-kit/react";
export default function
function PayWithUsdcButton(): JSX.Element
PayWithUsdcButton
() {
const {
const sendUserOperation: UseMutateFunction<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<SupportedAccounts>, unknown>
sendUserOperation
} =
useSendUserOperation<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>(params: UseSendUserOperationArgs<keyof EntryPointRegistryBase<unknown>, SupportedAccounts>): UseSendUserOperationResult<...>

A hook that returns functions for sending user operations. You can also optionally wait for a user operation to be mined and get the transaction hash before returning using waitForTx. Like any method that takes a smart account client, throws an error if client undefined or is signer not authenticated.

useSendUserOperation
();
return ( <
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick
={() =>
const sendUserOperation: (variables: SendUserOperationParameters<SupportedAccounts>, options?: MutateOptions<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<...>, unknown> | undefined) => void
sendUserOperation
({
uo: UserOperationCallData | BatchUserOperationCallData
uo
: {
target: `0x${string}`
target
: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // TARGET_ADDRESS
data: `0x${string}`
data
: "0x",
value?: bigint | undefined
value
: 0n,
}, }) } > Send USDC-sponsored operation </
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>
); }

4 (Optional) Override on a single client

If you only want specific operations to use the policy, create a dedicated client instead of setting it globally.

const { 
const client: any
client
} =
any
useSmartAccountClient
({
policyId: string
policyId
: "GAS_MANAGER_POLICY_ID",
policyToken: { address: string; maxTokenAmount: bigint; }
policyToken
: {
address: string
address
: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // USDC_ADDRESS on sepolia (can be any ERC-20 token address enabled in your policy)
maxTokenAmount: bigint
maxTokenAmount
: 10_000_000n, // Safety limit. If using USDC, this is 10 USDC (10 * 10^6).
}, });

Use this client with any hook that sends UserOperations.

Done

Your dApp now sponsors gas with the chosen ERC-20 token!