Fee Abstraction

Every Solana transaction requires SOL to pay network fees. But users coming to your payment application expect to transact in stablecoins—not manage a second token balance. Fee abstraction removes this friction by having someone else pay the fees.

This guide covers two levels:

  1. How fee sponsorship works — the underlying Solana primitive
  2. Fee abstraction at scale with Kora — a production-ready fee abstraction service

How Fee Sponsorship Works

Solana transactions have a designated fee payer—the account that pays the network fee. By default, this is the first signer. But you can specify a different account as the fee payer, allowing a third party (the "sponsor") to cover fees on behalf of the sender.

Both the sender and sponsor must sign the transaction:

  • The sender signs to authorize the transfer of their tokens
  • The sponsor signs to authorize payment of the network fee

See How Payments Work on Solana for core payment concepts.

The steps below show the core flow. See the Demo for complete runnable code.

Create a Sponsor Account

Generate a separate keypair for the sponsor who will pay transaction fees. The sponsor needs SOL for fees but doesn't need to hold the tokens being transferred.

Sponsor Transaction Fee
const sponsor = await generateKeyPairSigner();

Create Transfer Instruction

Create the token transfer instruction with the sender as authority. The sender owns the tokens and must sign the transfer.

Sponsor Transaction Fee
const sponsor = await generateKeyPairSigner();
const transferInstruction = getTransferCheckedInstruction({
source: senderAta,
mint: mint.address,
destination: recipientAta,
authority: sender, // Sender signs for the transfer
amount: 250_000n, // adjusted for the mint's decimals
decimals: 6
});

Send with Sponsor as Fee Payer

Create the client with the sponsor set as the fee payer via payer(sponsor), then call client.sendTransaction. The sponsor signs to pay the network fee while the sender signs to authorize the transfer—sendTransaction collects every referenced signer automatically.

Sponsor Transaction Fee
const sponsor = await generateKeyPairSigner();
const transferInstruction = getTransferCheckedInstruction({
source: senderAta,
mint: mint.address,
destination: recipientAta,
authority: sender,
amount: 250_000n,
decimals: 6
});
const client = createClient()
.use(payer(sponsor)) // Sponsor pays the transaction fees
.use(solanaLocalRpc());
const { context } = await client.sendTransaction([transferInstruction]);

Create a Sponsor Account

Generate a separate keypair for the sponsor who will pay transaction fees. The sponsor needs SOL for fees but doesn't need to hold the tokens being transferred.

Create Transfer Instruction

Create the token transfer instruction with the sender as authority. The sender owns the tokens and must sign the transfer.

Send with Sponsor as Fee Payer

Create the client with the sponsor set as the fee payer via payer(sponsor), then call client.sendTransaction. The sponsor signs to pay the network fee while the sender signs to authorize the transfer—sendTransaction collects every referenced signer automatically.

Sponsor Transaction Fee
const sponsor = await generateKeyPairSigner();

Demo

Demo
// Generate keypairs for sender, recipient, and sponsor (fee payer)
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
const sponsor = await generateKeyPairSigner();
console.log("Sender Address:", sender.address);
console.log("Recipient Address:", recipient.address);
console.log("Sponsor Address (Fee Payer):", sponsor.address);
// Demo Setup: Create client, mint account, token accounts, and fund with initial tokens
const { client, mint } = await demoSetup(sender, recipient, sponsor);
console.log("\nMint Address:", mint.address);
// Derive the Associated Token Accounts addresses (ATAs) for sender and recipient
const [senderAta] = await findAssociatedTokenPda({
mint: mint.address,
owner: sender.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [recipientAta] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipient.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
console.log("Sender Token Account:", senderAta.toString());
console.log("Recipient Token Account:", recipientAta.toString());
// =============================================================================
// Sponsored Token Payment Demo
// =============================================================================
// Create instruction to transfer tokens from sender to recipient
// Transferring 250,000 base units = 0.25 tokens (with 6 decimals)
const transferInstruction = getTransferCheckedInstruction({
source: senderAta,
mint: mint.address,
destination: recipientAta,
authority: sender, // Sender signs to authorize the transfer
amount: 250_000n, // 0.25 tokens
decimals: 6
});
// Send transaction with sponsor as fee payer using @solana/kit
// The sponsor (client.payer) pays fees; the sender signs for the transfer
const { context } = await client.sendTransaction([transferInstruction]);
const signature = context.signature;
console.log("\n=== Sponsored Token Payment Complete ===");
console.log("Transaction Signature:", signature.toString());
// Fetch final token account balances from the RPC
const senderBalance = await client.rpc.getTokenAccountBalance(senderAta).send();
const recipientBalance = await client.rpc
.getTokenAccountBalance(recipientAta)
.send();
console.log("\nSender Token Account Balance:", senderBalance.value.uiAmount);
console.log(
"Recipient Token Account Balance:",
recipientBalance.value.uiAmount
);
// Fetch transaction details
const transaction = await client.rpc
.getTransaction(signature, {
encoding: "jsonParsed",
maxSupportedTransactionVersion: 0
})
.send();
const feePayer = transaction?.transaction.message.accountKeys?.[0];
console.log("\nNote: The first account in accountKeys is always the fee payer");
console.log("Fee Payer Address:", feePayer);
// =============================================================================
// Demo Setup Helper Function
// =============================================================================
Console
Click to execute the code.

When you create a token account for an end user, they can close it and reclaim the SOL used for rent. Consider charging users for account creation in stablecoins, or factor this cost into your product economics.

Fee Abstraction at Scale with Kora

The fee payer primitive is powerful, but building a production gasless system requires more: managing sponsor wallets, handling token conversions (so users can "pay" fees in USDC), rate limiting, and security controls.

Kora handles this complexity. It's a JSON-RPC server that provides fee abstraction so users never need SOL. You can fully sponsor fees or accept fee payment in any token.

Deploy Kora with a single command:

cargo install kora-cli
kora --config path/to/kora.toml rpc start --signers-config path/to/signers.toml

Then point a Kit client at your Kora server and send transactions with the same API used above:

pnpm add @solana/kora
import { address } from "@solana/kit";
import { createKitKoraClient } from "@solana/kora";
// A Kora-backed Kit client: Kora is the fee payer, and users pay
// fees in the SPL token of your choice instead of SOL
const kora = await createKitKoraClient({
endpoint: "https://your-kora-instance",
rpcUrl: "https://api.mainnet-beta.solana.com",
feeToken: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), // USDC
feePayerWallet: sender // Signs to authorize the token fee payment
});
// Same instruction, same call—Kora sponsors the network fee
const { context } = await kora.sendTransaction([transferInstruction]);
console.log("Signature:", context.signature);

Kora Resources

Is this page helpful?

Table of Contents

Edit Page
© 2026 Solana Foundation. All rights reserved.