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
- Smart Wallets 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.

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

2 Add the policy to your global config
import { const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUIWraps 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: Chainsepolia } from "@account-kit/infra";
export const const config: AlchemyAccountsConfigWithUIconfig = function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUIWraps 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: stringapiKey: "ALCHEMY_API_KEY",
chain: Chainchain: const sepolia: Chainsepolia,
policyId?: string | string[] | undefinedpolicyId: "GAS_MANAGER_POLICY_ID", // your policy
policyToken: {
address: string;
maxTokenAmount: bigint;
}policyToken: {
address: stringaddress: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // USDC_ADDRESS on sepolia (can be any ERC-20 token address enabled in your policy)
maxTokenAmount: bigintmaxTokenAmount: 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";
import { function encodeFunctionData<const abi extends Abi | readonly unknown[], functionName extends ContractFunctionName<abi> | undefined = undefined>(parameters: EncodeFunctionDataParameters<abi, functionName>): EncodeFunctionDataReturnTypeencodeFunctionData, function parseAbi<const signatures extends readonly string[]>(signatures: signatures["length"] extends 0 ? Error<"At least one signature required"> : Signatures<signatures> extends signatures ? signatures : Signatures<signatures>): ParseAbi<signatures>Parses human-readable ABI into JSON Abi
parseAbi } from "viem";
export default function function PayWithUsdcButton(): JSX.ElementPayWithUsdcButton() {
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();
const const erc20Abi: readonly [{
readonly name: "approve";
readonly type: "function";
readonly stateMutability: "nonpayable";
readonly inputs: readonly [{
readonly type: "address";
readonly name: "spender";
}, {
readonly type: "uint256";
readonly name: "amount";
}];
readonly outputs: readonly [...];
}]erc20Abi = parseAbi<["function approve(address spender, uint256 amount) public returns (bool)"]>(signatures: ["function approve(address spender, uint256 amount) public returns (bool)"]): readonly [...]Parses human-readable ABI into JSON Abi
parseAbi([
"function approve(address spender, uint256 amount) public returns (bool)",
]);
return (
<React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={() =>
const sendUserOperation: (variables: SendUserOperationParameters<SupportedAccounts>, options?: MutateOptions<SendUserOperationWithEOA<keyof EntryPointRegistryBase<unknown>>, Error, SendUserOperationParameters<...>, unknown> | undefined) => voidsendUserOperation({
uo: UserOperationCallData | BatchUserOperationCallDatauo: [
{
// approve call
target: `0x${string}`target: anytokenAddress,
data: `0x${string}`data: encodeFunctionData<readonly [{
readonly name: "approve";
readonly type: "function";
readonly stateMutability: "nonpayable";
readonly inputs: readonly [{
readonly type: "address";
readonly name: "spender";
}, {
readonly type: "uint256";
readonly name: "amount";
}];
readonly outputs: readonly [...];
}], "approve">(parameters: EncodeFunctionDataParameters<...>): EncodeFunctionDataReturnTypeencodeFunctionData({
abi: readonly [{
readonly name: "approve";
readonly type: "function";
readonly stateMutability: "nonpayable";
readonly inputs: readonly [{
readonly type: "address";
readonly name: "spender";
}, {
readonly type: "uint256";
readonly name: "amount";
}];
readonly outputs: readonly [...];
}]abi: const erc20Abi: readonly [{
readonly name: "approve";
readonly type: "function";
readonly stateMutability: "nonpayable";
readonly inputs: readonly [{
readonly type: "address";
readonly name: "spender";
}, {
readonly type: "uint256";
readonly name: "amount";
}];
readonly outputs: readonly [...];
}]erc20Abi,
functionName?: `0x${string}` | "approve" | undefinedfunctionName: "approve",
args: readonly [`0x${string}`, bigint]args: [anypaymasterAddress, anymaxTokenAmount],
}) as `0x${string}`,
},
{
target: `0x${string}`target: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // TARGET_ADDRESS
data: `0x${string}`data: "0x",
value?: bigint | undefinedvalue: 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: anyclient } = anyuseSmartAccountClient({
policyId: stringpolicyId: "GAS_MANAGER_POLICY_ID",
policyToken: {
address: string;
maxTokenAmount: bigint;
}policyToken: {
address: stringaddress: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // USDC_ADDRESS on sepolia (can be any ERC-20 token address enabled in your policy)
maxTokenAmount: bigintmaxTokenAmount: 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!