diff --git a/content/wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-token.mdx b/content/wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-token.mdx
index 736d857e8..60c70342e 100644
--- a/content/wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-token.mdx
+++ b/content/wallets/pages/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-token.mdx
@@ -1,238 +1,567 @@
---
title: Pay gas with any ERC-20 token
-description: Learn how to enable gas payments with ERC-20 tokens.
-subtitle: Learn how to enable gas payments with ERC-20 tokens.
+description: Use the Gas Manager API to let a smart account pay gas with an ERC-20 token.
+subtitle: Use the Gas Manager API to let a smart account pay gas with an ERC-20 token.
url: https://alchemy.com/docs/reference/how-to-pay-gas-with-any-token
slug: wallets/low-level-infra/gas-manager/gas-sponsorship/using-sdk/pay-gas-with-any-erc20-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 Wallet APIs, 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.
+Use the Gas Manager API to let a smart account pay gas with an ERC-20 token instead of the network's native token. This guide shows the low-level post-operation flow for an EntryPoint v0.7 `UserOperation` using Viem actions or raw JSON-RPC calls.
- **How it works:** The gas is fronted using the network’s native gas token and
- the ERC-20 tokens are transferred 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.
+ **Wallet APIs are the recommended integration path.** They greatly simplify
+ this flow by reducing it to three API calls. Follow the [Wallet APIs
+ guide](/docs/wallets/transactions/pay-gas-with-any-token) to get started.
-
- **\[Recommended]** Use the [SDK](https://www.alchemy.com/docs/wallets) 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 your API key by creating an app in the [dashboard](https://dashboard.alchemy.com/apps)
-* 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](https://dashboard.alchemy.com/apps/latest/gas-sponsorship). 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](/docs/reference/gas-manager-faqs).
-* 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`](/docs/wallets/api/gas-manager-admin-api/gas-abstraction-api-endpoints/alchemy-request-gas-and-paymaster-and-data) 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`](https://www.alchemy.com/docs/wallets/api-reference/bundler-api/bundler-api-endpoints/eth-send-user-operation). You can find an example script below.
-
-## Example script
-
-```ts twoslash
-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;
+## How ERC-20 gas payment works
+
+Gas is fronted in the network's native token. Depending on the transfer mode configured in the policy, the paymaster collects the selected ERC-20 token from the smart account either before or after the operation executes. The native gas cost and admin fee are added to the policy owner's monthly invoice.
+
+The smart account at `UserOperation.sender` must hold the payment token and approve the paymaster to spend it. Funding or approving only the owner EOA does not fund a separate smart account. For an EIP-7702 delegated account, the EOA and `UserOperation.sender` use the same address.
+
+
+ This guide uses post-operation mode, which collects payment after execution
+ and lets you batch the approval with the application call. If that batch
+ reverts, its new approval also reverts, so the paymaster cannot collect the
+ token payment even though the policy owner still pays the native gas cost.
+ Pre-operation mode collects payment before execution, so the paymaster must
+ already have an allowance or compatible permit; an approval inside the
+ operation executes too late. Compare the [token gas payment modes](/docs/wallets/transactions/pay-gas-with-any-token#token-gas-payment-modes)
+ before configuring the policy.
+
+
+## Prerequisites
+
+Before you begin:
+
+* Create an API key in the [dashboard](https://dashboard.alchemy.com/apps) and enable Base Sepolia.
+* Create and activate an **ERC-20 Payments** policy in the [Gas Manager dashboard](https://dashboard.alchemy.com/apps/latest/gas-sponsorship). Enable Base Sepolia USDC and select the post-operation transfer mode.
+* Fund `UserOperation.sender` with Base Sepolia USDC at `0x036CbD53842c5426634e7929541eC2318f3dCF7e`. For a standard smart contract account, this is the separate smart account address, not its owner EOA. For an EIP-7702 delegated account, `UserOperation.sender` and the EOA are the same address. You can get test USDC from the [Circle faucet](https://faucet.circle.com/).
+* Install the dependencies with `npm install viem @alchemy/common @alchemy/aa-infra @alchemy/smart-accounts`.
+* Set `ALCHEMY_API_KEY`, `ALCHEMY_POLICY_ID`, and a test-only `OWNER_PRIVATE_KEY` in your environment for the Modular Account V2 example.
+
+Use an API key from the same app as the Gas Manager policy. For other network and token combinations, check the [supported chains](/docs/wallets/supported-chains) and enable the token on the policy.
+
+## Send with Viem actions
+
+Use Viem's account abstraction actions for the shortest low-level integration. This complete example creates a Modular Account V2, gives the paymaster a reusable 5 USDC allowance when the remaining allowance is below the payment cap, and limits each estimated payment to 0.5 USDC.
+
+The application call sends 0 ETH to the zero address. Replace it with the call your application needs, and choose allowance and cap values appropriate for your application.
+
+```ts title="pay-gas-with-usdc.ts"
+import { estimateFeesPerGas } from "@alchemy/aa-infra";
+import { alchemyTransport } from "@alchemy/common";
+import { toModularAccountV2 } from "@alchemy/smart-accounts";
+import {
+ createPublicClient,
+ encodeFunctionData,
+ erc20Abi,
+ formatUnits,
+ getAddress,
+ http,
+ parseUnits,
+ type Hex,
+} from "viem";
+import {
+ createBundlerClient,
+ createPaymasterClient,
+} from "viem/account-abstraction";
+import { privateKeyToAccount } from "viem/accounts";
+import { baseSepolia } from "viem/chains";
+
+const {
+ ALCHEMY_API_KEY: apiKey,
+ ALCHEMY_POLICY_ID: policyId,
+ OWNER_PRIVATE_KEY: ownerPrivateKey,
+} = process.env;
+if (!apiKey || !policyId || !ownerPrivateKey) {
+ throw new Error(
+ "Set ALCHEMY_API_KEY, ALCHEMY_POLICY_ID, and OWNER_PRIVATE_KEY",
+ );
}
+const owner = privateKeyToAccount(ownerPrivateKey as Hex);
-interface GasAndPaymasterData {
- paymasterAndData: string;
- preVerificationGas: string;
- verificationGasLimit: string;
- callGasLimit: string;
- maxFeePerGas: string;
- maxPriorityFeePerGas: string;
+const paymentToken = getAddress(
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+);
+// This address is specific to Base Sepolia and EntryPoint v0.7.
+const paymaster = getAddress("0x2cc0c7981D846b9F2a16276556f6e8cb52BfB633");
+const approvalAmount = parseUnits("5", 6);
+const maxTokenAmount = parseUnits("0.5", 6);
+
+const transport = alchemyTransport({ apiKey });
+const rpcClient = createPublicClient({ chain: baseSepolia, transport });
+const account = await toModularAccountV2({ client: rpcClient, owner });
+
+const [balance, allowance] = await Promise.all([
+ rpcClient.readContract({
+ address: paymentToken,
+ abi: erc20Abi,
+ functionName: "balanceOf",
+ args: [account.address],
+ }),
+ rpcClient.readContract({
+ address: paymentToken,
+ abi: erc20Abi,
+ functionName: "allowance",
+ args: [account.address, paymaster],
+ }),
+]);
+
+console.log({
+ smartAccount: account.address,
+ usdcBalance: formatUnits(balance, 6),
+ currentAllowance: formatUnits(allowance, 6),
+});
+if (balance < maxTokenAmount) {
+ throw new Error(`Fund ${account.address} with Base Sepolia USDC and retry`);
}
-// --- 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,
-);
+const paymasterClient = createPaymasterClient({
+ transport: http(`https://base-sepolia.g.alchemy.com/v2/${apiKey}`),
+});
+const bundlerClient = createBundlerClient({
+ account,
+ chain: baseSepolia,
+ client: rpcClient,
+ transport,
+ paymaster: paymasterClient,
+ paymasterContext: {
+ policyId,
+ erc20Context: {
+ tokenAddress: paymentToken,
+ maxTokenAmount: maxTokenAmount.toString(),
+ },
+ },
+ userOperation: { estimateFeesPerGas },
+});
+
+const applicationCall = {
+ to: "0x0000000000000000000000000000000000000000",
+ data: "0x",
+ value: 0n,
+} as const;
+
+const hash = await bundlerClient.sendUserOperation({
+ calls: [
+ ...(allowance < maxTokenAmount
+ ? [
+ {
+ to: paymentToken,
+ data: encodeFunctionData({
+ abi: erc20Abi,
+ functionName: "approve",
+ args: [paymaster, approvalAmount],
+ }),
+ value: 0n,
+ },
+ ]
+ : []),
+ applicationCall,
+ ],
+});
+
+const receipt = await bundlerClient.waitForUserOperationReceipt({ hash });
+if (!receipt.success) throw new Error(`UserOperation reverted: ${hash}`);
+
+console.log({
+ userOperationHash: hash,
+ transactionHash: receipt.receipt.transactionHash,
+});
+```
-// --- 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 {
- const body = JSON.stringify({
- id: 1,
- jsonrpc: "2.0",
- method: "alchemy_requestGasAndPaymasterAndData",
- params: [
+`createPaymasterClient` does not receive a chain, so its transport uses an explicit Base Sepolia RPC URL. The bundler client uses `paymasterContext` for the policy, payment token, and raw-unit payment cap, then handles estimation, signing, submission, and receipt polling.
+
+Some tokens do not let you change an allowance directly from one non-zero amount to another. For those tokens, include `approve(0)` before the new approval. The Base Sepolia USDC used here does not require this extra call.
+
+## Request an exact approval with raw RPC
+
+Use the raw flow when you need to request an exact token quote before choosing the approval amount, or when you are integrating an existing smart account implementation. The example deliberately leaves account creation, batch encoding, and signing behind three hooks so you can keep your account's nonce, factory, encoding, and signature logic.
+
+
+
+ Set `ALCHEMY_API_KEY` and `ALCHEMY_POLICY_ID` in your environment, then add the following setup to your integration:
+
+
+
+ ```ts title="pay-gas-with-usdc.ts"
+ import {
+ createPublicClient,
+ getAddress,
+ http,
+ maxUint256,
+ rpcSchema,
+ toHex,
+ type Hex,
+ } from "viem";
+ import { entryPoint07Address } from "viem/account-abstraction";
+ import { baseSepolia } from "viem/chains";
+ import { approvalCalls, decimalTokenAmountToRawUnitsCeil } from "./helpers";
+ import type {
+ Alchemy4337RpcSchema,
+ Call,
+ PartialUserOperationV07,
+ UnsignedUserOperationV07,
+ } from "./types";
+
+ const apiKey = process.env.ALCHEMY_API_KEY!;
+ const policyId = process.env.ALCHEMY_POLICY_ID!;
+ const rpcUrl = `https://base-sepolia.g.alchemy.com/v2/${apiKey}`;
+ const paymentToken = getAddress(
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
+ );
+ // Set this to true for tokens that require approve(0) before changing a
+ // non-zero allowance. Base Sepolia USDC does not require it.
+ const requiresAllowanceReset = false;
+
+ const client = createPublicClient({
+ chain: baseSepolia,
+ transport: http(rpcUrl),
+ rpcSchema: rpcSchema(),
+ });
+
+ // Implement these hooks with your smart account integration. Calls to
+ // buildPartialUserOperation must reuse the same sender and nonce.
+ declare function buildPartialUserOperation(
+ calls: Call[],
+ ): Promise;
+ declare function getDummySignature(): Promise;
+ declare function signUserOperation(
+ userOperation: UnsignedUserOperationV07,
+ ): Promise;
+ ```
+
+ ```ts title="types.ts"
+ import type {
+ Address,
+ BundlerRpcSchema,
+ Hex,
+ RpcUserOperation,
+ } from "viem";
+
+ export type Call = { to: Address; data: Hex; value?: bigint };
+ type UserOperationV07 = RpcUserOperation<"0.7">;
+ export type PartialUserOperationV07 = Pick<
+ UserOperationV07,
+ "sender" | "nonce" | "callData"
+ > &
+ Partial>;
+ export type UnsignedUserOperationV07 = Omit<
+ UserOperationV07,
+ "signature"
+ >;
+
+ type Erc20Context = {
+ tokenAddress: Address;
+ // A raw token amount expressed as a decimal integer string.
+ maxTokenAmount?: `${bigint}`;
+ skipBalanceCheck?: boolean;
+ };
+
+ type PaymasterFieldsV07 = Required<
+ Pick
+ >;
+ type GasAndPaymasterDataV07 = PaymasterFieldsV07 &
+ Required<
+ Pick<
+ UserOperationV07,
+ | "callGasLimit"
+ | "verificationGasLimit"
+ | "preVerificationGas"
+ | "maxFeePerGas"
+ | "maxPriorityFeePerGas"
+ | "paymasterPostOpGasLimit"
+ >
+ > &
+ Pick;
+ type GasAndPaymasterRequestV07 = {
+ policyId: string;
+ entryPoint: Address;
+ dummySignature: Hex;
+ userOperation: PartialUserOperationV07;
+ };
+
+ export type Alchemy4337RpcSchema = [
+ ...BundlerRpcSchema,
{
- policyId: ALCHEMY_POLICY_ID,
- userOperation: {
- sender: uo.sender,
- nonce: uo.nonce,
- initCode: uo.initCode,
- callData: uo.callData,
+ Method: "pm_getPaymasterStubData";
+ Parameters: [
+ PartialUserOperationV07,
+ Address,
+ Hex,
+ { policyId: string; erc20Context: Erc20Context },
+ ];
+ ReturnType: PaymasterFieldsV07;
+ },
+ {
+ Method: "alchemy_requestPaymasterTokenQuote";
+ Parameters: [
+ GasAndPaymasterRequestV07 & { erc20Context: Erc20Context },
+ ];
+ ReturnType: {
+ tokensPerEth: string;
+ // A human-readable decimal token amount, such as "0.0052774825".
+ estimateTokenAmount: string;
+ estimateUsd: number;
+ };
+ },
+ {
+ Method: "alchemy_requestGasAndPaymasterAndData";
+ Parameters: [
+ GasAndPaymasterRequestV07 & { erc20Context: Erc20Context },
+ ];
+ ReturnType: GasAndPaymasterDataV07;
+ },
+ ];
+ ```
+
+ ```ts title="helpers.ts"
+ import { encodeFunctionData, erc20Abi, type Address } from "viem";
+ import type { Call } from "./types";
+
+ export function approvalCalls({
+ token,
+ spender,
+ currentAllowance,
+ amount,
+ resetFirst,
+ }: {
+ token: Address;
+ spender: Address;
+ currentAllowance: bigint;
+ amount: bigint;
+ resetFirst: boolean;
+ }): Call[] {
+ if (currentAllowance >= amount) return [];
+
+ return [
+ ...(resetFirst && currentAllowance > 0n
+ ? [approveErc20(token, spender, 0n)]
+ : []),
+ approveErc20(token, spender, amount),
+ ];
+ }
+
+ function approveErc20(
+ token: Address,
+ spender: Address,
+ amount: bigint,
+ ): Call {
+ return {
+ to: token,
+ value: 0n,
+ data: encodeFunctionData({
+ abi: erc20Abi,
+ functionName: "approve",
+ args: [spender, amount],
+ }),
+ };
+ }
+
+ export function decimalTokenAmountToRawUnitsCeil(
+ value: string,
+ decimals: number,
+ ): bigint {
+ if (!/^\d+(?:\.\d+)?$/.test(value)) {
+ throw new Error(`Invalid decimal token amount: ${value}`);
+ }
+
+ const [whole, fraction = ""] = value.split(".");
+ const keptFraction = fraction.slice(0, decimals).padEnd(decimals, "0");
+ const discardedFraction = fraction.slice(decimals);
+ const roundUp = /[1-9]/.test(discardedFraction) ? 1n : 0n;
+
+ return (
+ BigInt(whole) * 10n ** BigInt(decimals) +
+ BigInt(keptFraction || "0") +
+ roundUp
+ );
+ }
+ ```
+
+
+
+
+
+ An ERC-20 approval names a spender, so you need the paymaster address before encoding the approval. The address differs by chain and EntryPoint version. This example discovers it from `pm_getPaymasterStubData`; you can instead configure the matching deployment address directly.
+
+ ```ts title="pay-gas-with-usdc.ts"
+ // Replace this with the action the smart account should perform.
+ const applicationCall: Call = {
+ to: "0xYOUR_TARGET_CONTRACT",
+ data: "0xYOUR_ENCODED_CALLDATA",
+ value: 0n,
+ };
+
+ const dummySignature = await getDummySignature();
+ const erc20Context = { tokenAddress: paymentToken } as const;
+ const discoveryOperation = await buildPartialUserOperation([
+ applicationCall,
+ ]);
+
+ const stub = await client.request({
+ method: "pm_getPaymasterStubData",
+ params: [
+ discoveryOperation,
+ entryPoint07Address,
+ toHex(baseSepolia.id),
+ { policyId, erc20Context },
+ ],
+ });
+ const paymaster = getAddress(stub.paymaster);
+ ```
+
+ `pm_getPaymasterStubData` returns estimation data, not final sponsorship. Never sign or submit the discovery operation.
+
+
+
+ Raw Gas Manager calls do not add an ERC-20 approval. Quote a batch that contains a temporary approval so the estimate includes its gas cost. The temporary `maxUint256` value exists only in the unsigned quote operation.
+
+ ```ts title="pay-gas-with-usdc.ts"
+ const currentAllowance = await client.readContract({
+ address: paymentToken,
+ abi: erc20Abi,
+ functionName: "allowance",
+ args: [discoveryOperation.sender, paymaster],
+ });
+ const tokenDecimals = await client.readContract({
+ address: paymentToken,
+ abi: erc20Abi,
+ functionName: "decimals",
+ });
+
+ const quoteOperation = await buildPartialUserOperation([
+ ...approvalCalls({
+ token: paymentToken,
+ spender: paymaster,
+ currentAllowance,
+ amount: maxUint256,
+ resetFirst: requiresAllowanceReset,
+ }),
+ applicationCall,
+ ]);
+ const quote = await client.request({
+ method: "alchemy_requestPaymasterTokenQuote",
+ params: [
+ {
+ policyId,
+ entryPoint: entryPoint07Address,
+ dummySignature,
+ userOperation: quoteOperation,
+ erc20Context,
},
- erc20Context: {
- tokenAddress: ERC20_TOKEN_ADDRESS,
+ ],
+ });
+
+ // Convert the human-readable quote to raw units without rounding down.
+ const approvalAmount = decimalTokenAmountToRawUnitsCeil(
+ quote.estimateTokenAmount,
+ tokenDecimals,
+ );
+ ```
+
+ `estimateTokenAmount` is a human-readable decimal value. ERC-20 `approve` expects raw base units, so use the token's onchain `decimals()` value and round up.
+
+
+
+ Keep a sufficient existing allowance. Otherwise, replace the temporary approval with an approval for the quote-derived amount and rebuild `callData`.
+
+ ```ts title="pay-gas-with-usdc.ts"
+ const finalCalls: Call[] = [
+ ...approvalCalls({
+ token: paymentToken,
+ spender: paymaster,
+ currentAllowance,
+ amount: approvalAmount,
+ resetFirst: requiresAllowanceReset,
+ }),
+ applicationCall,
+ ];
+ const partialUserOperation = await buildPartialUserOperation(finalCalls);
+
+ const sponsorship = await client.request({
+ method: "alchemy_requestGasAndPaymasterAndData",
+ params: [
+ {
+ policyId,
+ entryPoint: entryPoint07Address,
+ dummySignature,
+ userOperation: partialUserOperation,
+ erc20Context,
},
- 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;
-}
+ ],
+ });
+
+ if (sponsorship.paymaster.toLowerCase() !== paymaster.toLowerCase()) {
+ throw new Error("The paymaster address changed while preparing the operation");
+ }
+ ```
+
+ For tokens that require an allowance to be set to zero before changing from one non-zero amount to another, `approvalCalls` includes `approve(0)` in both the temporary quote batch and final batch.
+
+ If the gas estimate or exchange rate changes enough that the exact approval no longer covers the final estimate, restart from the quote step and rebuild the operation.
+
+
+
+ Merge the returned gas and paymaster fields into the operation before signing. Any later change to `callData`, gas fields, or paymaster fields invalidates the signature and can invalidate the sponsorship.
+
+ ```ts title="pay-gas-with-usdc.ts"
+ const unsignedSponsoredOperation: UnsignedUserOperationV07 = {
+ ...partialUserOperation,
+ ...sponsorship,
+ };
+
+ const signature = await signUserOperation(unsignedSponsoredOperation);
+ const userOperationHash = await client.request({
+ method: "eth_sendUserOperation",
+ params: [
+ { ...unsignedSponsoredOperation, signature },
+ entryPoint07Address,
+ ],
+ });
+
+ console.log({
+ userOperationHash,
+ maximumTokenPayment: quote.estimateTokenAmount,
+ });
+ ```
+
+ A successful response returns the UserOperation hash. Use [`eth_getUserOperationReceipt`](/docs/wallets/api-reference/bundler-api/bundler-api-endpoints/eth-get-user-operation-receipt) to wait for its receipt.
+
+
+
+## Operations that acquire the payment token
+
+An operation can acquire its payment token before post-operation collection. For example, a swap can send USDC to `UserOperation.sender` in the same batch.
+
+If the sender does not hold enough of the token before estimation, set `skipBalanceCheck: true` in `erc20Context` for `pm_getPaymasterStubData`, `alchemy_requestPaymasterTokenQuote`, and `alchemy_requestGasAndPaymasterAndData`:
+
+```ts
+const erc20Context = {
+ tokenAddress: paymentToken,
+ skipBalanceCheck: true,
+} as const;
+```
-/**
- * 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 {
- 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);
-}
+This setting skips only the estimation-time balance check. It does not bypass policy rules, simulation, or onchain collection. Order the batch so it approves the paymaster and acquires enough tokens for `UserOperation.sender` before post-operation collection.
-// --- 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);
+## Set a maximum token payment
- // 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.
+Set `erc20Context.maxTokenAmount` to cap the estimated payment. Low-level Gas Manager methods expect a raw token amount as a decimal integer string, not a hex quantity:
- // userOpWithGas.signature = await sign(userOpWithGas);
- // await sendUserOperation(userOpWithGas);
-})();
+```ts
+const erc20Context = {
+ tokenAddress: paymentToken,
+ maxTokenAmount: "10000", // 0.01 USDC in raw six-decimal units
+} as const;
```
+
+Allow explicit tolerance for gas and exchange-rate movement between the preliminary quote and final sponsorship request. If the refreshed estimate exceeds the cap, request a new quote and ask for approval again instead of silently increasing the cap.
+
+## Related API methods
+
+* [`pm_getPaymasterStubData`](/docs/wallets/api-reference/gas-manager-admin-api/gas-abstraction-api-endpoints/pm-get-paymaster-stub-data) returns stub paymaster fields for estimation and paymaster discovery.
+* [`alchemy_requestPaymasterTokenQuote`](/docs/wallets/api-reference/gas-manager-admin-api/gas-abstraction-api-endpoints/alchemy-request-paymaster-token-quote) simulates the operation and returns its estimated token payment.
+* [`alchemy_requestGasAndPaymasterAndData`](/docs/wallets/api-reference/gas-manager-admin-api/gas-abstraction-api-endpoints/alchemy-request-gas-and-paymaster-and-data) returns final gas estimates and sponsored paymaster fields.
+* [`eth_sendUserOperation`](/docs/wallets/api-reference/bundler-api/bundler-api-endpoints/eth-send-user-operation) submits the signed operation.