Paymaster

Quickstart: Circle Paymaster

Build a smart wallet that pays fees in USDC
  • Paymaster v0.7
  • Paymaster v0.8

This guide walks you through the process of:

  1. Setting up a smart account and checking its USDC balance
  2. Configuring the Circle Paymaster v0.7 to pay for gas with USDC
  3. Connecting to a bundler and submitting a user operation

This quickstart shows you how you can integrate Circle Paymaster into your app to simplify network fee management for your users.

Before you start building the sample app to pay for gas fees in USDC, ensure that Node.js and npm are installed. You can download and install Node.js directly, or use a version manager like nvm. The npm binary comes with Node.js.

The following steps cover the steps required to set up your environment and initialize a new smart account.

Create a new project, set the package type to module, and install the necessary dependencies.

Shell
npm init
npm pkg set type="module"
npm install --save viem @circle-fin/modular-wallets-core dotenv

Create a new .env file.

Shell
touch .env

Edit the .env file and add the following variables, replacing {YOUR_PRIVATE_KEY} and {RECIPIENT_ADDRESS} with your own values:

Text
OWNER_PRIVATE_KEY={YOUR_PRIVATE_KEY}
RECIPIENT_ADDRESS={RECIPIENT_ADDRESS}
PAYMASTER_V07_ADDRESS=0x31BE08D380A21fc740883c0BC434FcFc88740b58
USDC_ADDRESS=0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d # Arbitrum Sepolia

The RECIPIENT_ADDRESS is the destination address for the example USDC transfer.

Create a file called index.js and add the following code to set up the necessary clients and account:

JavaScript
import 'dotenv/config'
import { createPublicClient, http, getContract } from 'viem'
import { arbitrumSepolia } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import { toCircleSmartAccount } from '@circle-fin/modular-wallets-core'

const chain = arbitrumSepolia
const usdcAddress = process.env.USDC_ADDRESS
const ownerPrivateKey = process.env.OWNER_PRIVATE_KEY

const client = createPublicClient({ chain, transport: http() })
const owner = privateKeyToAccount(ownerPrivateKey)
const account = await toCircleSmartAccount({ client, owner })

Check the smart account's USDC balance using the following code:

JavaScript
import { erc20Abi } from 'viem'
const usdc = getContract({ client, address: usdcAddress, abi: erc20Abi })
const usdcBalance = await usdc.read.balanceOf([account.address])

if (usdcBalance < 1000000) {
  console.log(
    `Fund ${account.address} with USDC on ${client.chain.name} using https://faucet.circle.com, then run this again.`,
  )
  process.exit()
}

The Circle Paymaster requires an allowance to spend USDC on behalf of the smart account.

A USDC allowance is required for the paymaster to be able to withdraw USDC from the account to pay for fees. A signed permit can be used to set the paymaster's allowance without submitting a separate transaction.

Create a new file called permit.js with the following code to sign EIP-2612 permits:

JavaScript
import { maxUint256, erc20Abi, parseErc6492Signature } from 'viem'

// Adapted from https://github.com/vacekj/wagmi-permit/blob/main/src/permit.ts
export async function eip2612Permit({
  token,
  chain,
  ownerAddress,
  spenderAddress,
  value,
}) {
  return {
    types: {
      Permit: [
        { name: 'owner', type: 'address' },
        { name: 'spender', type: 'address' },
        { name: 'value', type: 'uint256' },
        { name: 'nonce', type: 'uint256' },
        { name: 'deadline', type: 'uint256' },
      ],
    },
    primaryType: 'Permit',
    domain: {
      name: await token.read.name(),
      version: await token.read.version(),
      chainId: chain.id,
      verifyingContract: token.address,
    },
    message: {
      owner: ownerAddress,
      spender: spenderAddress,
      value,
      nonce: await token.read.nonces([ownerAddress]),
      // The paymaster cannot access block.timestamp due to 4337 opcode
      // restrictions, so the deadline must be MAX_UINT256.
      deadline: maxUint256,
    },
  }
}

export const eip2612Abi = [
  ...erc20Abi,
  {
    inputs: [
      {
        internalType: 'address',
        name: 'owner',
        type: 'address',
      },
    ],
    stateMutability: 'view',
    type: 'function',
    name: 'nonces',
    outputs: [
      {
        internalType: 'uint256',
        name: '',
        type: 'uint256',
      },
    ],
  },
  {
    inputs: [],
    name: 'version',
    outputs: [{ internalType: 'string', name: '', type: 'string' }],
    stateMutability: 'view',
    type: 'function',
  },
]

export async function signPermit({
  tokenAddress,
  client,
  account,
  spenderAddress,
  permitAmount,
}) {
  const token = getContract({
    client,
    address: tokenAddress,
    abi: eip2612Abi,
  })
  const permitData = await eip2612Permit({
    token,
    chain: client.chain,
    ownerAddress: account.address,
    spenderAddress,
    value: permitAmount,
  })

  const wrappedPermitSignature = await account.signTypedData(permitData)

  const isValid = await client.verifyTypedData({
    ...permitData,
    address: account.address,
    signature: wrappedPermitSignature,
  })

  if (!isValid) {
    throw new Error(
      `Invalid permit signature for ${account.address}: ${wrappedPermitSignature}`,
    )
  }

  const { signature } = parseErc6492Signature(wrappedPermitSignature)
  return signature
}

In the index.js file, use the Circle permit implementation to build paymaster data:

JavaScript
import { encodePacked } from 'viem'
import { signPermit } from './permit.js'

const paymasterAddress = process.env.PAYMASTER_V07_ADDRESS

const paymaster = {
  async getPaymasterData(parameters) {
    const permitAmount = 10000000n
    const permitSignature = await signPermit({
      tokenAddress: usdcAddress,
      account,
      client,
      spenderAddress: paymasterAddress,
      permitAmount: permitAmount,
    })

    const paymasterData = encodePacked(
      ['uint8', 'address', 'uint256', 'bytes'],
      [0, usdcAddress, permitAmount, permitSignature],
    )

    return {
      paymaster: paymasterAddress,
      paymasterData,
      paymasterVerificationGasLimit: 200000n,
      paymasterPostOpGasLimit: 15000n,
      isFinal: true,
    }
  },
}

Once the paymaster is configured, you can connect to a bundler and submit a user operation to transfer USDC.

In index.js, set up the bundler client with the following code:

JavaScript
import { createBundlerClient } from 'viem/account-abstraction'
import { hexToBigInt } from 'viem'

const bundlerClient = createBundlerClient({
  account,
  client,
  paymaster,
  userOperation: {
    estimateFeesPerGas: async ({ account, bundlerClient, userOperation }) => {
      const { standard: fees } = await bundlerClient.request({
        method: 'pimlico_getUserOperationGasPrice',
      })
      const maxFeePerGas = hexToBigInt(fees.maxFeePerGas)
      const maxPriorityFeePerGas = hexToBigInt(fees.maxPriorityFeePerGas)
      return { maxFeePerGas, maxPriorityFeePerGas }
    },
  },
  transport: http(`https://public.pimlico.io/v2/${client.chain.id}/rpc`),
})

Finally, submit a user operation to transfer USDC, using the paymaster to pay for the network fee in USDC. In index.js add the following code:

JavaScript
const recipientAddress = process.env.RECIPIENT_ADDRESS

const hash = await bundlerClient.sendUserOperation({
  account,
  calls: [
    {
      to: usdc.address,
      abi: usdc.abi,
      functionName: 'transfer',
      args: [recipientAddress, 10000n],
    },
  ],
})
console.log('UserOperation hash', hash)

const receipt = await bundlerClient.waitForUserOperationReceipt({ hash })
console.log('Transaction hash', receipt.receipt.transactionHash)

// We need to manually exit the process, since viem leaves some promises on the
// event loop for features we're not using.
process.exit()

The above example demonstrates how to pay for a transaction using only USDC. You can review the transaction in an explorer to verify the details and see the USDC transfers that occurred during the transaction. Remember that you need to use the transaction hash from the bundler, not the user operation hash in the explorer. You can also view more details about the user operation by searching for the user operation hash in a user op explorer on the appropriate network.

Did this page help you?
© 2023-2025 Circle Technology Services, LLC. All rights reserved.