How to Charge USDC Onchain Without Gas in the Customer Wallet
Learn to charge USDC onchain when your customer does not hold native gas. Covers EIP-3009, sponsored transfers, and practical integration tips.

Payments onchain usually require the sender to pay a fee in native gas. This is a hurdle for users who only hold USDC. If you want to charge a customer onchain, but they do not have ETH, MATIC or another network token, you need another way. Sponsored transfers solve this, letting you pay gas for your user. This guide covers how sponsored USDC transfers work, the EIP-3009 standard, and when to use this pattern.
Why Native Gas is a Problem for USDC Payments
Most users expect to pay with USDC. But onchain transfers require gas, paid in the chain’s native token. If the customer’s wallet is empty of ETH or MATIC, the transaction fails. This is a common onboarding issue. You do not want to make users buy or bridge gas tokens just to pay you.
Sponsored transfers let you cover the gas. The customer signs an authorization, but a relayer submits the transaction and pays the network fee. The user never touches native gas.
EIP-3009: Transfer With Authorization
The EIP-3009 standard lets a customer sign a message granting permission to transfer USDC from their wallet. The message is not a transaction. It is an offchain signature. Anyone can submit this signature onchain, triggering the transfer. This pattern is sometimes called a meta-transaction.
The key fields in a transferWithAuthorization message are:
from: the customer’s addressto: the merchant’s receiving addressvalue: the USDC amountvalidAfter/validBefore: a time window for validitynonce: prevents replayv, r, s: the ECDSA signature
The relayer wraps this data into a transaction and pays the network gas.
Flow: Charging USDC Without Customer Gas
Here is the general sequence:
- Build the authorization: Show the customer the details (amount, destination, validity window). They sign it with their wallet.
- Collect the signature: You (the merchant/server) receive the signed message.
- Relay the transaction: You or a relayer submit a
transferWithAuthorizationcall to the USDC contract onchain, passing the signature. The relayer pays the gas. - Settlement: The USDC transfers from the user’s address to yours. The relayer cannot intercept the funds. The user never had to hold native gas.
Worked Example: Charging 50 USDC Using EIP-3009
Suppose you want to charge 50 USDC from Alice, who has no ETH. You control the relayer.
- Alice:
0xA...CE - Merchant:
0xM...NT - USDC Contract:
0xA0b...6e(on Base)
1. Alice Signs the Authorization
You display the payment details. Alice signs using her wallet. Here’s a TypeScript snippet for building and signing the message using ethers.js:
import { ethers } from "ethers";
const domain = {
name: "USD Coin",
version: "2",
chainId: 8453, // Base mainnet
verifyingContract: "0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
};
const types = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" }
]
};
const value = {
from: "0xA...CE",
to: "0xM...NT",
value: ethers.utils.parseUnits("50", 6), // 50 USDC, 6 decimals
validAfter: 0,
validBefore: Math.floor(Date.now() / 1000) + 600, // valid for 10 mins
nonce: ethers.utils.hexlify(ethers.utils.randomBytes(32)),
};
const signature = await signer._signTypedData(domain, types, value);
You collect the signature from Alice.
2. Relayer Submits the Transaction
Now your backend (or relayer) calls the USDC contract:
const usdcAbi = [
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)"
];
const contract = new ethers.Contract(
"0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
usdcAbi,
relayerSigner
);
const { v, r, s } = ethers.utils.splitSignature(signature);
await contract.transferWithAuthorization(
value.from,
value.to,
value.value,
value.validAfter,
value.validBefore,
value.nonce,
v, r, s
);
The transaction is sent by the relayer, who pays the gas. Once confirmed, 50 USDC move from Alice to you. Alice did not need ETH.
3. Handling Failures
- Signature rejected: If the signature is invalid, or the nonce is reused, the transaction reverts. Check that the details match what Alice saw.
- Expired: If the relayer waits too long,
validBeforewill have passed. The transfer fails, but Alice keeps her funds. - Insufficient funds: If Alice has less than 50 USDC, the transfer fails. You cannot overdraft.
When to Use a Sponsored Transfer
This pattern is useful when:
- Your users are new and do not have chain native tokens.
- You want to abstract away gas fees.
- You want to bill from a non-custodial wallet.
It does not fit if:
- You need recurring, automated withdrawals without user action. EIP-3009 requires a fresh signature for each payment.
- Your use case is not supported by USDC’s contract. Not all ERC-20 tokens have
transferWithAuthorization.
Security Considerations
- Always show the user the exact amount and destination before signing.
- Set a short validity window. This limits risk if a signature leaks.
- Never accept an unlimited approval or blank destination.
- Store nonces to avoid replay.
- Do not ask for a private key or seed phrase. Only collect signatures for the specific payment.
How We Use Sponsored Transfers at Reclack
We support gasless USDC payments for merchants who want to charge users without asking them to acquire ETH or MATIC. When a merchant chooses this mode, we generate a bounded authorization for the customer to sign. Our relayer settles the transaction and pays the gas. The transfer is onchain, non-custodial, and final. See more at reclack.com.
