Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

How to pay gas from the token being swapped

This guide shows you how to offer gasless swaps where the network fee comes out of the token the user is selling, so a user who enters "spend 100 USDC" spends 100 USDC in total, gas included. You estimate the fee with Pimlico's ERC-20 Paymaster, quote the swap for the amount left after the fee, and send the approve and the swap as one user operation. If you only need swaps without ETH and the fee can sit on top of the amount, see How to let users swap on Base without ETH instead.

Prerequisites

  • A Pimlico API key on a plan that includes mainnets. Create one.
  • A DEX aggregator or router API that returns swap calldata for a given sell amount and taker address.
npm
npm install permissionless viem

Steps

Create the clients

If your users have an EOA, use to7702SimpleSmartAccount (EntryPoint 0.8), as this example does, so the account keeps the user's address. If they have ERC-4337 smart accounts, create them with toSafeSmartAccount or toSimpleSmartAccount and drop the authorization in the send step.

import { createSmartAccountClient } from "permissionless"
import { to7702SimpleSmartAccount } from "permissionless/accounts"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { prepareUserOperationForErc20Paymaster } from "permissionless/experimental/pimlico"
import { type Address, createPublicClient, type Hex, http } from "viem"
import { entryPoint08Address } from "viem/account-abstraction"
import { privateKeyToAccount } from "viem/accounts"
import { base } from "viem/chains"
 
const pimlicoUrl = `https://api.pimlico.io/v2/${base.id}/rpc?apikey=YOUR_PIMLICO_API_KEY`
 
const publicClient = createPublicClient({
	chain: base,
	transport: http("https://mainnet.base.org"),
})
 
const pimlicoClient = createPimlicoClient({
	chain: base,
	transport: http(pimlicoUrl),
	entryPoint: {
		address: entryPoint08Address,
		version: "0.8",
	},
})
 
const eoa = privateKeyToAccount(process.env.EOA_PRIVATE_KEY as Hex)
 
const account = await to7702SimpleSmartAccount({
	client: publicClient,
	owner: eoa,
})
 
const smartAccountClient = createSmartAccountClient({
	account,
	chain: base,
	bundlerTransport: http(pimlicoUrl),
	paymaster: pimlicoClient,
	userOperation: {
		estimateFeesPerGas: async () => {
			return (await pimlicoClient.getUserOperationGasPrice()).fast
		},
		prepareUserOperation: prepareUserOperationForErc20Paymaster(pimlicoClient),
	},
})

Build the approve and swap calls

Request an exact-input quote with the account address as the taker, and approve exactly the sell amount for the aggregator's allowance target. This example sells USDC; to sell another token, see Sell a token other than USDC.

import { encodeFunctionData, erc20Abi } from "viem"
 
const sellToken: Address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // USDC on Base
const buyToken: Address = "0x4200000000000000000000000000000000000006" // WETH on Base
 
// Map your aggregator's "exact input" quote onto this shape
type SwapQuote = {
	to: Address
	data: Hex
	value: bigint
	allowanceTarget: Address
	sellAmount: bigint
}
 
declare function getSwapQuote(params: {
	chainId: number
	sellToken: Address
	buyToken: Address
	sellAmount: bigint
	taker: Address
	slippageBps: number
}): Promise<SwapQuote>
 
async function buildSwapCalls(sellAmount: bigint) {
	const quote = await getSwapQuote({
		chainId: base.id,
		sellToken,
		buyToken,
		sellAmount,
		taker: account.address,
		slippageBps: 50,
	})
 
	return [
		{
			to: sellToken,
			value: 0n,
			data: encodeFunctionData({
				abi: erc20Abi,
				functionName: "approve",
				args: [quote.allowanceTarget, quote.sellAmount],
			}),
		},
		{ to: quote.to, value: quote.value, data: quote.data },
	]
}

Deduct the fee from the sell amount

Prepare the user operation for the full amount the user entered, estimate its cost in the sell token with estimateErc20PaymasterCost, and subtract it. Add a small buffer, because gas prices can rise between the estimate and the send.

import { parseUnits } from "viem"
 
// What the user typed in: the most they want to spend, gas included
const spendAmount = parseUnits("100", 6)
 
async function estimateFee(sellAmount: bigint) {
	const userOperation = await smartAccountClient.prepareUserOperation({
		calls: await buildSwapCalls(sellAmount),
		paymasterContext: { token: sellToken },
	})
 
	const { costInToken } = await pimlicoClient.estimateErc20PaymasterCost({
		userOperation,
		token: sellToken,
	})
 
	return costInToken
}
 
// Estimate with the full amount, then sell what's left after the fee.
// The 5% buffer covers gas price moves between this estimate and the send.
const fee = ((await estimateFee(spendAmount)) * 105n) / 100n
const sellAmount = spendAmount - fee

Check the balance and show the split

Check the user's balance covers the amount they entered, then show them the sell amount and the maximum network fee before they confirm. The fee charged is based on the gas actually used, so it's usually lower than the estimate.

import { formatUnits } from "viem"
 
const balance = await publicClient.readContract({
	address: sellToken,
	abi: erc20Abi,
	functionName: "balanceOf",
	args: [account.address],
})
 
if (spendAmount > balance) {
	throw new Error(`Not enough balance: max spend is ${formatUnits(balance, 6)}`)
}
 
console.log(`You sell ${formatUnits(sellAmount, 6)} + network fee up to ${formatUnits(fee, 6)}`)

Send the swap

Get a fresh quote for the reduced sell amount and send it with paymasterContext: { token: sellToken }. permissionless.js adds the paymaster's approval to the same user operation if needed, and the paymaster collects the fee after the swap runs. Attach the EIP-7702 authorization only while account.isDeployed() returns false.

const SIMPLE_7702_IMPLEMENTATION = "0xe6Cae83BdE06E4c305530e199D7217f42808555B"
 
const isDelegated = await account.isDeployed()
 
// Fetch a fresh quote for the reduced amount right before sending
const userOpHash = await smartAccountClient.sendUserOperation({
	calls: await buildSwapCalls(sellAmount),
	paymasterContext: { token: sellToken },
	authorization: isDelegated
		? undefined
		: await eoa.signAuthorization({
				address: SIMPLE_7702_IMPLEMENTATION,
				chainId: base.id,
				nonce: await publicClient.getTransactionCount({ address: eoa.address }),
			}),
})
 
const receipt = await smartAccountClient.waitForUserOperationReceipt({ hash: userOpHash })
 
if (!receipt.success) {
	throw new Error(`Swap reverted: ${receipt.reason ?? "unknown reason"}`)
}

Check receipt.success. If the price moves past your slippage limit, the whole user operation reverts, including the approvals.

Sell a token other than USDC

The ERC-20 Paymaster only accepts some tokens on each chain. To check the token the user is selling, call getTokenQuotes:

const quotes = await pimlicoClient.getTokenQuotes({ tokens: [sellToken] })
 
// If the sell token isn't supported, sponsor the swap instead
const canPayGasInSellToken = quotes.length > 0

If it returns a quote, set sellToken to that token and follow the steps as written. If it returns no quote, leave the token out of the paymasterContext and sponsor the swap from your Pimlico balance, as in Sponsor the swap instead. To recover the cost, take your own fee in the swap itself if your aggregator supports an integrator fee.

Let the user sell their whole balance

To build a "max" button, set the spend amount to the user's full balance. Keep the buffer from the fee step: without it, a small rise in gas price leaves too little to pay the fee and the user operation fails simulation.

Related