Solana CookbookTransactions

Pay Fees with Any Token

Overview

In order to improve UX for users, apps should consider allowing users to pay for transactions using any token they have available in their wallet. This way apps do not have to sponsor transaction fees for users, but still improve UX for end users and not require them to hold SOL. This concept is often referred to as "gas abstraction" on other blockchains. Paying transaction fees with any token is enabled via two key native capabilities of Solana transactions:

  • The feePayer field in the transaction message.
  • The ability to batch instructions in a single transaction.

How to pay transaction fees with any token

To pay transaction fees with any token, you'll need to have an EOA to use as the fee payer and a transaction with an instruction that transfers tokens from the user's wallet to the fee payer's wallet. The fee payer will then forward the transaction to the Solana network and pay the transaction fees in SOL covered monetarily by the tokens it received from the user.

This flow can be simplified by using a fee relayer, like Kora, that can process a transaction for end users only if it is paid for in a configured token like USDC or USDT.

The following example shows how to pay transaction fees with any token.

import {
airdropFactory,
appendTransactionMessageInstructions,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners
} from "@solana/kit";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
getCreateAssociatedTokenInstructionAsync,
getInitializeMintInstruction,
getMintSize,
TOKEN_PROGRAM_ADDRESS,
findAssociatedTokenPda,
getMintToInstruction,
getTransferInstruction,
fetchToken
} from "@solana-program/token";
// Create Connection, local validator in this example
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate keypairs for fee payer, sender and recipient
const feePayer = await generateKeyPairSigner();
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
console.log("Fee Payer Address:", feePayer.address.toString());
console.log("Sender Address:", sender.address.toString());
console.log("Recipient Address:", recipient.address.toString());
// Fund fee payer
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: feePayer.address,
lamports: lamports(1_000_000_000n),
commitment: "confirmed"
});
// Generate keypair to use as address of mint
const mint = await generateKeyPairSigner();
console.log("Mint Address:", mint.address.toString());
// Get default mint account size (in bytes), no extensions enabled
const space = BigInt(getMintSize());
// Get minimum balance for rent exemption
const rent = await rpc.getMinimumBalanceForRentExemption(space).send();
// Get latest blockhash to include in transaction
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
// Instruction to create new account for mint (token program)
// Invokes the system program
const createAccountInstruction = getCreateAccountInstruction({
payer: feePayer,
newAccount: mint,
lamports: rent,
space,
programAddress: TOKEN_PROGRAM_ADDRESS
});
// Instruction to initialize mint account data
// Invokes the token program
const initializeMintInstruction = getInitializeMintInstruction({
mint: mint.address,
decimals: 2,
mintAuthority: sender.address
});
// Derive the ATAs for sender and recipient
const [senderAssociatedTokenAddress] = await findAssociatedTokenPda({
mint: mint.address,
owner: sender.address,
tokenProgram: TOKEN_PROGRAM_ADDRESS
});
const [recipientAssociatedTokenAddress] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipient.address,
tokenProgram: TOKEN_PROGRAM_ADDRESS
});
console.log(
"Sender Associated Token Account Address:",
senderAssociatedTokenAddress.toString()
);
console.log(
"Recipient Associated Token Account Address:",
recipientAssociatedTokenAddress.toString()
);
// Create instruction for sender's ATA
const createFeePayerAtaInstruction =
await getCreateAssociatedTokenInstructionAsync({
payer: feePayer,
mint: mint.address,
owner: feePayer.address
});
// Create instruction for sender's ATA
const createSenderAtaInstruction =
await getCreateAssociatedTokenInstructionAsync({
payer: feePayer,
mint: mint.address,
owner: sender.address
});
// Create instruction for recipient's ATA
const createRecipientAtaInstruction =
await getCreateAssociatedTokenInstructionAsync({
payer: feePayer,
mint: mint.address,
owner: recipient.address
});
// Create instruction to mint tokens to sender
const mintToInstruction = getMintToInstruction({
mint: mint.address,
token: senderAssociatedTokenAddress,
mintAuthority: sender.address,
amount: 100n
});
// Combine all instructions in order
const instructions = [
createAccountInstruction, // Create mint account
initializeMintInstruction, // Initialize mint
createFeePayerAtaInstruction, // Create fee payer's ATA
createSenderAtaInstruction, // Create sender's ATA
createRecipientAtaInstruction, // Create recipient's ATA
mintToInstruction // Mint tokens to sender
];
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx)
);
// Sign transaction message with all required signers
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature = getSignatureFromTransaction(signedTransaction);
console.log("Transaction Signature:", transactionSignature);
console.log("Successfully minted 1.0 tokens");
// Get a fresh blockhash for the transfer transaction
const { value: transferBlockhash } = await rpc.getLatestBlockhash().send();
// Create instruction to transfer tokens
const transferInstruction = getTransferInstruction({
source: senderAssociatedTokenAddress,
destination: recipientAssociatedTokenAddress,
authority: sender.address,
amount: 50n // 0.50 tokens with 2 decimals
});
// Create instruction to transfer tokens to the fee payer to cover the transaction fees
// For a real world application, you would need to determine the amount of tokens to transfer to the fee payer based on the transaction fees.
const transferFeePayerInstruction = getTransferInstruction({
source: senderAssociatedTokenAddress,
destination: feePayer.address,
authority: sender.address,
amount: 50n // 0.50 tokens with 2 decimals
});
// Create transaction message for token transfer
const transferTxMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(transferBlockhash, tx),
(tx) =>
appendTransactionMessageInstructions(
[transferInstruction, transferFeePayerInstruction],
tx
)
);
// Sign transaction message with all required signers
const signedTransferTx =
await signTransactionMessageWithSigners(transferTxMessage);
// Send and confirm transaction
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransferTx,
{ commitment: "confirmed" }
);
// Get transaction signature
const transactionSignature2 = getSignatureFromTransaction(signedTransferTx);
console.log("Transaction Signature:", transactionSignature2);
console.log("Successfully transferred 0.5 tokens");
const feePayerTokenAccount = await fetchToken(rpc, feePayer.address, {
commitment: "confirmed"
});
const senderTokenAccount = await fetchToken(rpc, senderAssociatedTokenAddress, {
commitment: "confirmed"
});
const recipientTokenAccount = await fetchToken(
rpc,
recipientAssociatedTokenAddress,
{
commitment: "confirmed"
}
);
const feePayerBalance = feePayerTokenAccount.data.amount;
const senderBalance = senderTokenAccount.data.amount;
const recipientBalance = recipientTokenAccount.data.amount;
console.log("=== Final Balances ===");
console.log("Fee Payer balance:", Number(feePayerBalance) / 100, "tokens");
console.log("Sender balance:", Number(senderBalance) / 100, "tokens");
console.log("Recipient balance:", Number(recipientBalance) / 100, "tokens");
Console
Click to execute the code.

It is recommended to use a fee relayer service to pay transaction fees with any token. Kora is built to support such a use case and also includes additional features like configuring which tokens are accepted as well as automatically swapping the received tokens for SOL for continued operation.

Kora, by the Solana Foundation, is a fee relayer service that allows you to relay transactions for other users. In addition to accepting fee payment for configured tokens, it also provides full fee sponsorship for transactions. More information can be found in the Kora documentation.

Is this page helpful?

Géré par

© 2025 Fondation Solana.
Tous droits réservés.
Restez connecté
Pay Fees with Any Token | Solana