Gas Sponsorships
This guide shows you how to sponsor the transaction fees for your users on Solana.
You build your transaction with the paymaster as the fee payer, your user signs it, and the @solana/kora SDK submits it to the paymaster. The paymaster signs as fee payer and broadcasts it. The fee is charged to your Pimlico balance.
Steps
Install the SDK
npm install @solana/kora @solana/kit@6 @solana-program/token@0.12@solana/kora 0.2 is built on @solana/kit 6, so install the matching versions of @solana/kit and @solana-program/token.
Create the client
Pass your API key in the URL's apikey query parameter.
import { KoraClient } from "@solana/kora";
const kora = new KoraClient({
rpcUrl: "https://solana.pimlico.io/v1/mainnet/rpc?apikey=YOUR_API_KEY",
});Add a helper to build the transaction
Add this helper to your app. Pass it your instructions, and it returns a transaction message with the paymaster as the fee payer.
import type { KoraClient } from "@solana/kora";
import {
type Instruction,
address,
appendTransactionMessageInstructions,
blockhash,
createNoopSigner,
createTransactionMessage,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
export async function createSponsoredTransactionMessage(
kora: KoraClient,
instructions: Instruction[],
) {
const { signer_address } = await kora.getPayerSigner();
const { blockhash: latestBlockhash } = await kora.getBlockhash();
// pipe passes the message through each step below, in order
return pipe(
// Start an empty version 0 transaction message
createTransactionMessage({ version: 0 }),
// The paymaster pays the fee. It signs later, so a noop signer leaves its signature slot empty.
(m) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signer_address)), m),
// The blockhash keeps the transaction valid for about a minute. lastValidBlockHeight is only
// read by @solana/kit's own send-and-confirm, and the paymaster sends the transaction for you.
(m) =>
setTransactionMessageLifetimeUsingBlockhash(
{ blockhash: blockhash(latestBlockhash), lastValidBlockHeight: 0n },
m,
),
// Add your app's instructions (transfers, swaps, etc.)
(m) => appendTransactionMessageInstructions(instructions, m),
);
}Build your instructions
This example sends 1 USDC to another wallet. Your user needs no SOL.
import { address, createKeyPairSignerFromBytes } from "@solana/kit";
import {
findAssociatedTokenPda,
getTransferInstruction,
TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const user = await createKeyPairSignerFromBytes(USER_SECRET_KEY);
const recipient = address("RecipientPubkey11111111111111111111111111111");
const [source] = await findAssociatedTokenPda({
owner: user.address,
mint: USDC,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [destination] = await findAssociatedTokenPda({
owner: recipient,
mint: USDC,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const instructions = [
getTransferInstruction({
source,
destination,
authority: user,
amount: 1_000_000n, // 1 USDC
}),
];Build and sign the transaction
Build the message with the helper. Your user signs here. Send the transaction within about a minute, or it expires.
import {
getBase64EncodedWireTransaction,
partiallySignTransactionMessageWithSigners,
} from "@solana/kit";
import { createSponsoredTransactionMessage } from "./sponsored-transaction";
const message = await createSponsoredTransactionMessage(kora, instructions);
// Your user signs as the token authority. The paymaster adds the fee payer signature.
const transaction = getBase64EncodedWireTransaction(
await partiallySignTransactionMessageWithSigners(message),
);Send the sponsored transaction
signAndSendTransaction submits the transaction. The paymaster validates and simulates it, signs it as fee payer, broadcasts it, and responds with the signature once the transaction is confirmed.
const { signature } = await kora.signAndSendTransaction({ transaction });
console.log(`https://solscan.io/tx/${signature}`);Responding sooner
By default signAndSendTransaction waits for the transaction to be confirmed. Set respond_after to get the signature sooner and confirm it yourself:
"sent": respond once the RPC node accepts the transaction."signed": respond as soon as the paymaster signs, and broadcast in the background. If the transaction never lands, rebroadcast the returnedsigned_transaction.
const { signature, signed_transaction } = await kora.signAndSendTransaction({
transaction,
respond_after: "sent",
});Using an external wallet
If your users sign with a wallet like Phantom, use the wallet as the signer instead of a keypair. See Phantom and Other Wallets.
Errors
Errors from the paymaster are thrown as KoraError with a code, message, and data.
import { KoraError } from "@solana/kora";
try {
await kora.signAndSendTransaction({ transaction });
} catch (error) {
if (error instanceof KoraError) {
console.error(error.code, error.message, error.data);
}
}If your Pimlico balance is used up, sponsorship is refused with -32603 and a message asking you to top up. See the errors reference for the full list.
Creating token accounts
If your transaction creates a token account, set the paymaster as its rent payer so a user with no SOL can still receive the token. The rent is sponsored along with the fee.
import { address, createNoopSigner } from "@solana/kit";
import { getCreateAssociatedTokenIdempotentInstruction } from "@solana-program/token";
const { signer_address } = await kora.getPayerSigner();
const createAccount = getCreateAssociatedTokenIdempotentInstruction({
payer: createNoopSigner(address(signer_address)),
ata: destination,
owner: recipient,
mint: USDC,
});The paymaster may appear in your instructions only as this rent payer. Anywhere else, including as the destination of a transfer, is rejected.
Full example
import { KoraClient } from "@solana/kora";
import {
type Instruction,
address,
appendTransactionMessageInstructions,
blockhash,
createKeyPairSignerFromBytes,
createNoopSigner,
createTransactionMessage,
getBase64EncodedWireTransaction,
partiallySignTransactionMessageWithSigners,
pipe,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import {
findAssociatedTokenPda,
getTransferInstruction,
TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
async function createSponsoredTransactionMessage(
kora: KoraClient,
instructions: Instruction[],
) {
const { signer_address } = await kora.getPayerSigner();
const { blockhash: latestBlockhash } = await kora.getBlockhash();
// pipe passes the message through each step below, in order
return pipe(
// Start an empty version 0 transaction message
createTransactionMessage({ version: 0 }),
// The paymaster pays the fee. It signs later, so a noop signer leaves its signature slot empty.
(m) => setTransactionMessageFeePayerSigner(createNoopSigner(address(signer_address)), m),
// The blockhash keeps the transaction valid for about a minute. lastValidBlockHeight is only
// read by @solana/kit's own send-and-confirm, and the paymaster sends the transaction for you.
(m) =>
setTransactionMessageLifetimeUsingBlockhash(
{ blockhash: blockhash(latestBlockhash), lastValidBlockHeight: 0n },
m,
),
// Add your app's instructions (transfers, swaps, etc.)
(m) => appendTransactionMessageInstructions(instructions, m),
);
}
const USDC = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const kora = new KoraClient({
rpcUrl: "https://solana.pimlico.io/v1/mainnet/rpc?apikey=YOUR_API_KEY",
});
const user = await createKeyPairSignerFromBytes(USER_SECRET_KEY);
const recipient = address("RecipientPubkey11111111111111111111111111111");
const [source] = await findAssociatedTokenPda({
owner: user.address,
mint: USDC,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const [destination] = await findAssociatedTokenPda({
owner: recipient,
mint: USDC,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
const message = await createSponsoredTransactionMessage(kora, [
getTransferInstruction({ source, destination, authority: user, amount: 1_000_000n }),
]);
// Your user signs as the token authority. The paymaster adds the fee payer signature.
const transaction = getBase64EncodedWireTransaction(
await partiallySignTransactionMessageWithSigners(message),
);
const { signature } = await kora.signAndSendTransaction({ transaction });
console.log(`https://solscan.io/tx/${signature}`);