Skip to content
LogoLogo

How to set up a Kora fee payer without running a Kora node

This guide shows you how to give your Solana app a Kora fee payer by pointing @solana/kora's KoraClient at Pimlico, which runs a hosted fee payer that speaks the Kora JSON-RPC API, instead of deploying kora rpc yourself. Use it if you want Kora's client flow without hosting the node, its signer keys and its SOL balance. If you want to compare this with running your own fee payer or self-hosting Kora, see Solana fee sponsorship.

Prerequisites

  • A Pimlico API key. Create one.
  • On mainnet, a Pimlico balance or card in the dashboard. Fees you sponsor are charged to it. Devnet sponsorship is free.

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.

Point KoraClient at Pimlico

Use the Pimlico URL as rpcUrl, where you would otherwise use your Kora node's URL (such as http://localhost:8080). Pass your API key in the URL's apikey query parameter, not in KoraClient's apiKey option: that option sends an x-api-key header, which Pimlico does not read.

import { KoraClient } from "@solana/kora";
 
const kora = new KoraClient({
    rpcUrl: "https://solana.pimlico.io/v1/mainnet/rpc?apikey=YOUR_API_KEY",
});

To use devnet, replace mainnet with devnet in the URL.

Check the fee payer responds

Call getPayerSigner. If it returns an address, your client is connected and that address is the fee payer for your transactions.

const { signer_address } = await kora.getPayerSigner();
console.log(`Fee payer: ${signer_address}`);

Build the transaction with the fee payer

Add this helper to your app. Pass it your instructions, and it returns a transaction message with the Pimlico fee payer as the fee payer and its signature slot left empty.

sponsored-transaction.ts
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),
    );
}

Use the fee payer only as the fee payer, or as the rent payer of an account your transaction creates. Any other use of it is rejected.

Sign and send the transaction

Have your user sign, then call signAndSendTransaction. The fee payer validates and simulates the transaction, adds its signature, broadcasts it, and responds once it is confirmed. This example sends 1 USDC from a user who holds no SOL.

import {
    address,
    createKeyPairSignerFromBytes,
    getBase64EncodedWireTransaction,
    partiallySignTransactionMessageWithSigners,
} from "@solana/kit";
import {
    findAssociatedTokenPda,
    getTransferInstruction,
    TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import { createSponsoredTransactionMessage } from "./sponsored-transaction";
 
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 message = await createSponsoredTransactionMessage(kora, [
    getTransferInstruction({ source, destination, authority: user, amount: 1_000_000n }),
]);
 
// Your user signs as the token authority. The fee payer adds its signature.
const transaction = getBase64EncodedWireTransaction(
    await partiallySignTransactionMessageWithSigners(message),
);
 
const { signature } = await kora.signAndSendTransaction({ transaction });
console.log(`https://solscan.io/tx/${signature}`);

Open the link. The explorer shows the Pimlico fee payer as the fee payer, and the fee is charged to your Pimlico balance.

Replace a self-hosted Kora node

If you already run Kora, keep your client code and change rpcUrl to the Pimlico URL. Then use this table to see where each part of your Kora setup lives with Pimlico.

On a self-hosted Kora nodeWith Pimlico
kora rpc server and its hostPimlico runs it. Use https://solana.pimlico.io/v1/{mainnet,devnet}/rpc.
signers.toml (fee payer keys) and the SOL that funds themPimlico holds the fee payer key and pays the SOL. Read its address with getPayerSigner. Sponsored fees are charged to your Pimlico balance.
[kora.auth] api_keysYour Pimlico API key, in the apikey query parameter or an Authorization: Bearer header.
[validation] allowed_spl_paid_tokensFixed list: USDC and USDT on mainnet, USDC on devnet. Read it with getSupportedTokens, and see supported tokens.
[validation] max_allowed_lamportsFixed at 0.1 SOL per transaction, including the fee, priority fee and rent.
[validation.fee_payer_policy]Fixed: the fee payer may only pay the fee and the rent of accounts the transaction creates.
[validation.price]The token fee is the SOL the fee payer spends, measured by simulation and priced in the token at the current price.
[kora.enabled_methods]Pimlico implements getPayerSigner, getBlockhash, getSupportedTokens, estimateTransactionFee and signAndSendTransaction. See the endpoint reference.
[kora] rate_limitSet by your Pimlico plan. See pricing.

If your app uses a Kora method that isn't in the endpoint reference, such as signTransaction or transferTransaction, rebuild that flow around signAndSendTransaction.

Let users pay the fee in USDC or USDT

To have your user repay the fee in a token instead of charging your Pimlico balance, call kora.getPaymentInstruction and append the returned transfer before your user signs. See SPL Token Sponsorships.

Sign in a browser wallet

If your users sign in Phantom or another Wallet Standard wallet, use the wallet as the signer. See Phantom and Other Wallets. If they log in with Privy, see Privy.

Call the JSON-RPC API without KoraClient

To call the fee payer from another language, send Kora JSON-RPC requests to the same URL. Parameters go in a single object, with snake_case fields.

curl -X POST "https://solana.pimlico.io/v1/devnet/rpc?apikey=YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"jsonrpc":"2.0","method":"getPayerSigner","id":1}'