Skip to main content
Celo allows users to pay gas fees in currencies other than the native CELO token. The list of accepted tokens is governed on-chain and maintained in FeeCurrencyDirectory.sol. To use an alternate fee currency, set its token or adapter address as the feeCurrency property on the transaction object. This field is exclusive to Celo. Leaving it empty defaults to CELO. Note that transactions specifying a non-CELO fee currency cost approximately 50,000 additional gas.

Allowlisted Fee Currencies

The protocol maintains a governable allowlist of smart contract addresses that implement an extension of the ERC20 interface, with additional functions for debiting and crediting transaction fees. To fetch the current allowlist, call getCurrencies() on the FeeCurrencyDirectory contract, or use celocli:
# Celo Sepolia testnet
celocli network:whitelist --node celo-sepolia

# Celo mainnet
celocli network:whitelist --node https://forno.celo.org

Adapters for Non-18-Decimal Tokens

After Contract Release 11, allowlisted addresses may be adapters rather than full ERC20 tokens. Adapters are used when a token has decimals other than 18 (e.g., USDC and USDT use 6 decimals). The Celo blockchain calculates gas pricing in 18 decimals, so adapters normalize the value.
  • For transfers: use the token address as usual.
  • For feeCurrency: use the adapter address.
  • For balanceOf: querying via the adapter returns the balance as if the token had 18 decimals β€” useful for checking whether an account can cover gas without converting units.
To get the underlying token address for an adapter, call adaptedToken() on the adapter contract. For more on gas pricing, see Gas Pricing.

Adapter Addresses

Mainnet

Celo Sepolia (Testnet)


Using Fee Abstraction with Celo CLI

Transfer 1 USDC using USDC as the fee currency via celocli:
celocli transfer:erc20 \
  --erc20Address 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
  --from 0x22ae7Cf4cD59773f058B685a7e6B7E0984C54966 \
  --to 0xDF7d8B197EB130cF68809730b0D41999A830c4d7 \
  --value 1000000 \
  --gasCurrency 0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B \
  --privateKey [PRIVATE_KEY]
When using USDC or USDT, use the adapter address (not the token address) to avoid inaccuracies caused by their 6-decimal precision.

Using Fee Abstraction with viem

We recommend viem, which has native support for the feeCurrency field. Ethers.js and web3.js do not currently support this field.

1. Estimate the Gas Fee

Before sending, estimate the transaction fee so the UI can reserve that amount and prevent users from trying to transfer more than their available balance.
The gas price returned from the RPC is always expressed in 18 decimals, regardless of the fee currency.
Use the adapter address (for USDC/USDT) or token address (for USDm, EURm, BRLm) as the feeCurrency value when estimating.
import { createPublicClient, hexToBigInt, http } from "viem";
import { celo } from "viem/chains";

const USDC_ADAPTER_MAINNET = "0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B";

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const transaction = {
  from: "0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D",
  to: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
  data: "0xa9059cbb000000000000000000000000ccc9576f841de93cd32bee7b98fe8b9bd3070e3d00000000000000000000000000000000000000000000000000000000000f4240",
  feeCurrency: USDC_ADAPTER_MAINNET,
};

async function getGasPriceInUSDC() {
  const priceHex = await publicClient.request({
    method: "eth_gasPrice",
    params: [USDC_ADAPTER_MAINNET],
  });
  return hexToBigInt(priceHex);
}

async function estimateGasInUSDC(transaction) {
  const estimatedGasInHex = await publicClient.estimateGas({
    ...transaction,
    feeCurrency: USDC_ADAPTER_MAINNET,
  });
  return hexToBigInt(estimatedGasInHex);
}

async function main() {
  const gasPriceInUSDC = await getGasPriceInUSDC();
  const estimatedGas = await estimateGasInUSDC(transaction);

  // Total fee the user must reserve before transferring
  const transactionFeeInUSDC = formatEther(gasPriceInUSDC * estimatedGas).toString();
  return transactionFeeInUSDC;
}

2. Prepare the Transaction

Set feeCurrency to the adapter address (USDC/USDT) or token address (USDm, EURm, BRLm). Use transaction type 123 (0x7b), which is CIP-64 compliant.
let tx = {
  // ... other transaction fields
  feeCurrency: "0x2f25deb3848c207fc8e0c34035b3ba7fc157602b", // USDC Adapter address
  type: "0x7b",
};

3. Send the Transaction

The example below transfers 1 USDC, subtracting the estimated fee from the transfer amount so the sender’s full balance is not over-spent.
import { createWalletClient, http } from "viem";
import { celo } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
import { stableTokenAbi } from "@celo/abis";

const account = privateKeyToAccount("0x432c...");

const client = createWalletClient({
  account,
  chain: celo,
  transport: http(),
});

const USDC_ADAPTER_MAINNET = "0x2F25deB3848C207fc8E0c34035B3Ba7fC157602B";
const USDC_MAINNET = "0xcebA9300f2b948710d2653dD7B07f33A8B32118C";

async function calculateTransactionFeesInUSDC(transaction) {
  const gasPriceInUSDC = await getGasPriceInUSDC();
  const estimatedGas = await estimateGasInUSDC(transaction);
  return gasPriceInUSDC * estimatedGas;
}

async function send(amountInWei) {
  const to = USDC_MAINNET;

  const data = encodeFunctionData({
    abi: stableTokenAbi,
    functionName: "transfer",
    args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", amountInWei],
  });

  const transactionFee = await calculateTransactionFeesInUSDC({ to, data });

  // Subtract the fee from the amount so the sender isn't over-spending
  const tokenReceivedByReceiver = parseEther("1") - transactionFee;

  const dataAfterFeeCalculation = encodeFunctionData({
    abi: stableTokenAbi,
    functionName: "transfer",
    args: ["0xccc9576F841de93Cd32bEe7B98fE8B9BD3070e3D", tokenReceivedByReceiver],
  });

  const hash = await client.sendTransaction({
    ...{ to, data: dataAfterFeeCalculation },
    feeCurrency: USDC_ADAPTER_MAINNET,
  });

  return hash;
}

If you have any questions, please reach out.