# How to accept USDC from an EOA without gas fees

This guide shows you how to accept USDC from a user's EOA, such as MetaMask or Rabby, at checkout or for deposits, without the user paying gas fees or holding ETH. The user signs a USDC permit ([EIP-2612](https://eips.ethereum.org/EIPS/eip-2612)) instead of sending a transaction, and your server's smart account uses it to move the USDC, with gas sponsored by Pimlico's **verifying paymaster**. If you control your users' wallets, see [How to send USDC without ETH for gas](/guides/how-to/payments/send-usdc-without-eth) instead.

This guide uses Base and its native USDC (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`). To use another chain, see [Accept payments on another chain](#accept-payments-on-another-chain).

## Prerequisites

* A Pimlico API key on a plan that includes mainnets. [Create one](/guides/create-api-key).

:::code-group
```bash [npm]
npm install permissionless viem
```

```bash [pnpm]
pnpm install permissionless viem
```

```bash [bun]
bun install permissionless viem
```
:::

## Steps

:::steps
### Create the server account

On your server, create one Simple Account and a smart account client that uses Pimlico as bundler and paymaster. Every customer permits this account, so keep its owner key the same. Set `MERCHANT_TREASURY` to the address the USDC should move to.

```ts
import { createSmartAccountClient } from "permissionless"
import { toSimpleSmartAccount } from "permissionless/accounts"
import { createPimlicoClient } from "permissionless/clients/pimlico"
import { type Address, createPublicClient, type Hex, http } from "viem"
import { entryPoint07Address } from "viem/account-abstraction"
import { privateKeyToAccount } from "viem/accounts"
import { base } from "viem/chains"

const USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" // USDC on Base
const MERCHANT_TREASURY: Address = "0x1111111111111111111111111111111111111111" // where payments land

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: entryPoint07Address,
		version: "0.7",
	},
})

// The server account's owner. It never holds funds, only spends what customers permit.
const owner = privateKeyToAccount(process.env.SERVER_PRIVATE_KEY as Hex)

const serverAccount = await toSimpleSmartAccount({
	client: publicClient,
	owner,
	entryPoint: {
		address: entryPoint07Address,
		version: "0.7",
	},
})

const serverClient = createSmartAccountClient({
	account: serverAccount,
	chain: base,
	bundlerTransport: http(pimlicoUrl),
	// No paymasterContext needed: the verifying paymaster sponsors every settlement
	paymaster: pimlicoClient,
	userOperation: {
		estimateFeesPerGas: async () => {
			return (await pimlicoClient.getUserOperationGasPrice()).fast
		},
	},
})
```

### Create the permit request

When the customer connects their wallet at checkout, build the typed data they will sign. Use the server account as the `spender`, the order total as the `value`, the customer's current `nonces()` value as the `nonce`, and the checkout expiry as the `deadline`.

```ts
import { parseAbi, parseUnits } from "viem"

const usdcAbi = parseAbi([
	"function nonces(address owner) view returns (uint256)",
	"function allowance(address owner, address spender) view returns (uint256)",
	"function balanceOf(address account) view returns (uint256)",
	"function permit(address owner, address spender, uint256 value, uint256 deadline, bytes signature)",
	"function transferFrom(address from, address to, uint256 value) returns (bool)",
])

export async function createPermitRequest(customer: Address, amountUsdc: string) {
	const nonce = await publicClient.readContract({
		address: USDC,
		abi: usdcAbi,
		functionName: "nonces",
		args: [customer],
	})

	return {
		domain: {
			name: "USD Coin", // USDC's name() on Base
			version: "2", // USDC's version() on Base
			chainId: base.id,
			verifyingContract: USDC,
		},
		types: {
			Permit: [
				{ name: "owner", type: "address" },
				{ name: "spender", type: "address" },
				{ name: "value", type: "uint256" },
				{ name: "nonce", type: "uint256" },
				{ name: "deadline", type: "uint256" },
			],
		},
		primaryType: "Permit",
		message: {
			owner: customer,
			spender: serverAccount.address,
			value: parseUnits(amountUsdc, 6),
			nonce,
			deadline: BigInt(Math.floor(Date.now() / 1000) + 30 * 60), // 30 minutes
		},
	} as const
}
```

Permit nonces are sequential, so settle a customer's payment before you create their next permit request.

### Ask the customer to sign

Send the request to the browser and ask the customer's wallet to sign it with `signTypedData`. The wallet shows a USDC permit signature rather than a transaction, so it works with no ETH in the wallet.

```ts
import { createWalletClient, custom } from "viem"

declare const window: { ethereum: Parameters<typeof custom>[0] }

// In the customer's browser: any EOA wallet works, and it doesn't need ETH
const walletClient = createWalletClient({
	chain: base,
	transport: custom(window.ethereum),
})

const [customer] = await walletClient.requestAddresses()

// Fetched from your backend, which calls createPermitRequest(customer, "25")
const permitRequest = await createPermitRequest(customer, "25")

const signature = await walletClient.signTypedData({
	account: customer,
	...permitRequest,
})
```

Send the signature back to your server with the order ID.

### Settle the payment

Send one user operation from the server account that calls `permit` with the customer's signature and then `transferFrom` to move the USDC to your treasury. Pimlico sponsors the gas.

```ts
import { encodeFunctionData } from "viem"

type PermitRequest = Awaited<ReturnType<typeof createPermitRequest>>

async function paymentCalls(request: PermitRequest, customerSignature: Hex) {
	const { owner: from, spender, value, deadline } = request.message

	const [balance, allowance] = await Promise.all([
		publicClient.readContract({ address: USDC, abi: usdcAbi, functionName: "balanceOf", args: [from] }),
		publicClient.readContract({
			address: USDC,
			abi: usdcAbi,
			functionName: "allowance",
			args: [from, spender],
		}),
	])

	if (balance < value) {
		throw new Error("Customer doesn't have enough USDC")
	}

	const pull = {
		to: USDC,
		value: 0n,
		data: encodeFunctionData({
			abi: usdcAbi,
			functionName: "transferFrom",
			args: [from, MERCHANT_TREASURY, value],
		}),
	} as const

	// Anyone can submit a permit. If it already landed, only pull the funds.
	if (allowance >= value) return [pull]

	const permit = {
		to: USDC,
		value: 0n,
		data: encodeFunctionData({
			abi: usdcAbi,
			functionName: "permit",
			args: [from, spender, value, deadline, customerSignature],
		}),
	} as const

	return [permit, pull]
}

export async function settlePayment(request: PermitRequest, customerSignature: Hex) {
	return serverClient.sendUserOperation({
		calls: await paymentCalls(request, customerSignature),
	})
}
```

### Confirm the payment

Wait for the receipt and check `success` before you mark the order as paid. Retrying a failed settlement is safe: once the USDC has moved, the allowance is used up and a second `transferFrom` reverts.

```ts
const userOpHash = await settlePayment(permitRequest, signature)

const receipt = await serverClient.waitForUserOperationReceipt({ hash: userOpHash })

if (!receipt.success) {
	throw new Error(`Payment not settled: ${receipt.reason ?? "unknown reason"}`)
}

console.log(`Order paid: https://basescan.org/tx/${receipt.receipt.transactionHash}`)
```
:::

## Settle several payments in one user operation

To cut overhead when checkouts arrive close together, queue signed permits and settle them in a single user operation:

```ts
type SignedPayment = { request: PermitRequest; signature: Hex }

declare const pendingPayments: SignedPayment[]

// Settle several checkouts in one sponsored user operation
const calls = await Promise.all(
	pendingPayments.map(({ request, signature }) => paymentCalls(request, signature)),
)

const batchHash = await serverClient.sendUserOperation({ calls: calls.flat() })
```

If one payment in the batch fails, the whole user operation reverts. Check each permit's `deadline` before you add it to the batch.

## Settle payments in parallel

If several settlements may be in flight at once, give each one its own nonce key so they don't wait on each other. See [How to send multiple user operations in parallel](/guides/how-to/parallel-transactions). Before your first parallel settlement, deploy the server account with one user operation, as in [How to send USDC payouts to thousands of recipients](/guides/how-to/payments/payouts).

## Refund a customer

To refund an order, send USDC from your treasury back to the customer. If you set `MERCHANT_TREASURY` to `serverAccount.address`, send the refund as a `transfer` call from the server client, and Pimlico sponsors it the same way.

## Accept payments on another chain

Change the chain, the RPC URL and the USDC address. Then read `name()` and `version()` from that chain's USDC contract and use them in the typed data `domain`. If they don't match, the signature is invalid and settlement reverts. The same flow works for any token that implements EIP-2612 `permit`.

## Notes

* If the customer pays from a smart contract wallet, keep the `bytes signature` overload of `permit` used here. USDC checks it with ERC-1271, which only works once the customer's wallet is deployed.
* If you keep the server account's owner key in a key management service (Turnkey, Fireblocks, Dfns, a cloud KMS), pass its viem `LocalAccount` as the `owner` instead of `privateKeyToAccount`.
* If you don't want to host a private key on your server for this, reach out to [support@pimlico.io](mailto\:support@pimlico.io).

## Limit what you sponsor (optional)

To cap what your API key can sponsor, for example only calls to the USDC contract, create a policy in the [sponsorship policies dashboard](https://dashboard.pimlico.io/sponsorship-policies) and pass its ID in the `paymasterContext` when you settle. See [How to use sponsorship policies](/guides/how-to/sponsorship-policies).

```ts
const SPONSORSHIP_POLICY_ID = "sp_your_policy_id"

const limitedHash = await serverClient.sendUserOperation({
	calls: await paymentCalls(permitRequest, signature),
	paymasterContext: { sponsorshipPolicyId: SPONSORSHIP_POLICY_ID },
})
```

## Related

* [How gasless stablecoin payments work](/guides/conceptual/gasless-stablecoin-payments)
* [How to send USDC payouts to thousands of recipients](/guides/how-to/payments/payouts)
* [How to create and use a SimpleAccount with permissionless.js](/guides/how-to/accounts/use-simple-account)
* [Verifying paymaster reference](/references/paymaster/verifying-paymaster)
