Kora Full Transaction Flow

This guide teaches you how to implement a complete gasless transaction flow using Kora RPC, including payment instructions and transaction signing.

Last Updated: 2025-10-31

What You'll Build

In the Quick Start Guide, you learned how to set up Kora RPC and make basic calls. Now we'll build a complete gasless transaction system that demonstrates Kora's full capabilities. By the end of this guide, you'll have implemented a transaction flow that:

  • Creates multiple transfer instructions (SPL tokens and SOL)
  • Obtains payment instructions from Kora for fee coverage
  • Signs transactions with user keys while Kora handles gas fees
  • Submits fully-signed transactions to the Solana network

The final result will be a working gasless transaction system:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
KORA GASLESS TRANSACTION DEMO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[1/6] Initializing clients
 Kora RPC: http://localhost:8080/
 Solana RPC: http://127.0.0.1:8899

[2/6] Setting up keypairs
 Sender: BYJVBqQ2xV9GECc84FeoPQy2DpgoonZQFQu97MMWTbBc
 Destination: C8MC9E6nf9Am1rVqdDedDavm53uCJMiSwarEko1aXmny
 Kora signer address: 3Z1Ef7YaxK8oUMoi6exf7wYZjZKWJJsrzJXSt1c3qrDE

[3/6] Creating demonstration instructions
 Payment token: 9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ
 Token transfer instruction created
 SOL transfer instruction created
 Memo instruction created
 Total: 3 instructions

[4/6] Estimating Kora fee and assembling payment instruction
 Fee payer: 3Z1Ef7Ya...
 Blockhash: 7HZUaMqV...
 Estimate transaction built
 Payment instruction received from Kora

[5/6] Creating and signing final transaction (with payment)
 Final transaction built with payment
 Transaction signed by user

[6/6] Signing transaction with Kora and sending to Solana cluster
 Transaction co-signed by Kora
 Transaction submitted to network
 Awaiting confirmation...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SUCCESS: Transaction confirmed on Solana
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Transaction signature:
41hmwmkMfHR5mmhG9sNkjiakwHxpmr1H3Gi3bBL8v5PbsRrH7FhpUT8acHaf2mrPKNVD894dSYXfjp6LfAbVpcCE

View on explorer:
https://explorer.solana.com/tx/41hmwmkMfHR5mmhG9sNkjiakwHxpmr1H3Gi3bBL8v5PbsRrH7FhpUT8acHaf2mrPKNVD894dSYXfjp6LfAbVpcCE?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899

Let's build it step by step!

Prerequisites

Before starting this tutorial, ensure you have:

Kora Transaction Flow

Kora enables gasless transactions by acting as a fee payer for your users' transactions. The gasless transaction flow consists of these main steps:

  1. Transaction Creation - Build the user's intended transaction (transfers, program calls, etc.)
  2. Fee Estimation - Create an estimate transaction to calculate required fees
  3. Payment Instruction - Get a payment instruction from Kora that specifies the fee amount
  4. User Signing - User signs the transaction including the payment instruction
  5. Kora Co-signing - Kora validates payment and co-signs as the fee payer
  6. Submission - Submit the fully-signed transaction to Solana

*Note: Kora can be configured to not require payment, but we will be using it to demonstrate the full flow.

Project Setup

Kora Server Considerations

  • Token Allowlist - Only tokens configured in kora.toml can be used for payment - make sure the token defined in your .env is included in your kora.toml allowlist.
  • Program Restrictions - Transactions can only interact with whitelisted programs. We have preset the kora.toml to allow interaction with the System Program, Token Program, Compute Unit Program, and Memo program.

Client Setup

This guide assumes you've completed the Quick Start and have the demo project set up. If not, please complete that first.

Navigate to your demo client directory:

cd kora/examples/getting-started/demo/client

Note: The demo files are located in the GitHub repository as they require a full development setup.

Implementation

Before we start running the demo, let's walk through the full demo implementation step by step:

Imports and Configuration

Our demo starts with the necessary imports and configuration:

import { KoraClient } from "@solana/kora";
import {
  createKeyPairSignerFromBytes,
  getBase58Encoder,
  createNoopSigner,
  address,
  getBase64EncodedWireTransaction,
  partiallySignTransactionMessageWithSigners,
  Blockhash,
  Base64EncodedWireTransaction,
  partiallySignTransaction,
  TransactionVersion,
  Instruction,
  KeyPairSigner,
  Rpc,
  SolanaRpcApi,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  pipe,
  createTransactionMessage,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
  MicroLamports,
  appendTransactionMessageInstructions,
} from "@solana/kit";
import { getAddMemoInstruction } from "@solana-program/memo";
import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation";
import { updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction } from "@solana-program/compute-budget";
import dotenv from "dotenv";
import path from "path";

dotenv.config({ path: path.join(process.cwd(), "..", ".env") });

const CONFIG = {
  computeUnitLimit: 200_000,
  computeUnitPrice: 1_000_000n as MicroLamports,
  transactionVersion: 0,
  solanaRpcUrl: "http://127.0.0.1:8899",
  solanaWsUrl: "ws://127.0.0.1:8900",
  koraRpcUrl: "http://localhost:8080/",
};

We are importing the Kora Client from the Kora SDK and a few types/helpers from Solana Kit library for building transactions.

We are also creating a global configuration object that defines:

  • Compute Budget - Units and price for transaction prioritization
  • Transaction Version - Using V0 for address lookup table support
  • RPC Endpoints - Local Solana and Kora RPC servers

Leave these defaults for now--after the demo, you can experiment with different values to see how they affect the transaction flow.

Utility Functions

The demo includes a helper function for loading keypairs from environment variables:

async function getEnvKeyPair(envKey: string) {
    if (!process.env[envKey]) {
        throw new Error(`Environment variable ${envKey} is not set`);
    }
    const base58Encoder = getBase58Encoder();
    const b58SecretEncoded = base58Encoder.encode(process.env[envKey]);
    return await createKeyPairSignerFromBytes(b58SecretEncoded);
}

This function:

  • Reads base58-encoded private keys from environment variables
  • Encodes the private key string to a U8 byte array
  • Converts them to keypair signer objects

Step 1: Initialize Clients

First, we set up our connection to both Kora and Solana:

async function initializeClients() {
    console.log('\n[1/6] Initializing clients');
    console.log('  → Kora RPC:', CONFIG.koraRpcUrl);
    console.log('  → Solana RPC:', CONFIG.solanaRpcUrl);
    
    const client = new KoraClient({
        rpcUrl: CONFIG.koraRpcUrl,
        // apiKey: process.env.KORA_API_KEY, // Uncomment if authentication is enabled
        // hmacSecret: process.env.KORA_HMAC_SECRET, // Uncomment if HMAC is enabled
    });

    const rpc = createSolanaRpc(CONFIG.solanaRpcUrl);
    const rpcSubscriptions = createSolanaRpcSubscriptions(CONFIG.solanaWsUrl);

    const confirmTransaction = createRecentSignatureConfirmationPromiseFactory({ 
        rpc, 
        rpcSubscriptions 
    });
    
    return { client, rpc, confirmTransaction };
}

This function:

  • Creates a Kora client instance by passing in our Kora RPC URL.
  • Establishes a Solana RPC connection with subscription support (we will use this for sending and confirming transactions to the Solana cluster)
  • Sets up transaction confirmation utilities

Note: Our kora.toml file does not include any authentication, so we don't need to pass in an api key or hmac secret, but we have left the commented out code in for reference.

Step 2: Setup Keys

Load the required keypairs from environment variables and fetch the Kora signer address:

async function setupKeys(client: KoraClient) {
    console.log('\n[2/6] Setting up keypairs');
    
    const testSenderKeypair = await getEnvKeyPair('TEST_SENDER_KEYPAIR');
    const destinationKeypair = await getEnvKeyPair('DESTINATION_KEYPAIR');
    const { signer_address } = await client.getPayerSigner();

    console.log('  → Sender:', testSenderKeypair.address);
    console.log('  → Destination:', destinationKeypair.address);
    console.log('  → Kora signer address:', signer_address);
    
    return { testSenderKeypair, destinationKeypair, signer_address };
}

Here we are using our getEnvKeyPair function to load the keypairs from the environment variables. The keypairs represent:

  • Sender - The user initiating the transaction and responsible for paying the Kora node in the payment token.
  • Destination - The recipient of the transfers.

We also use the getPayerSigner method to fetch the Kora signer address. This is the address that will be used to sign the transaction with Kora's signature. It is important that we fetch a valid signer from the Kora node and use it consistently throughout our transaction flow.

Step 3: Create Demo Instructions

Next, we build a set of instructions that that our testSender wants to send to the network. We will be using the Kora Client to build some of these instructions and the @solana/programs library to build others to demonstrate how to use both.

async function createInstructions(
    client: KoraClient, 
    testSenderKeypair: KeyPairSigner, 
    destinationKeypair: KeyPairSigner
) {
    console.log('\n[3/6] Creating demonstration instructions');
    
    const paymentToken = await client.getConfig().then(config => config.validation_config.allowed_spl_paid_tokens[0]);
    console.log('  → Payment token:', paymentToken);

    // Create token transfer (will initialize ATA if needed)
    const transferTokens = await client.transferTransaction({
        amount: 10_000_000, // 10 USDC (6 decimals)
        token: paymentToken,
        source: testSenderKeypair.address,
        destination: destinationKeypair.address
    });
    console.log('  ✓ Token transfer instruction created');

    // Create SOL transfer
    const transferSol = await client.transferTransaction({
        amount: 10_000_000, // 0.01 SOL (9 decimals)
        token: '11111111111111111111111111111111', // SOL mint address
        source: testSenderKeypair.address,
        destination: destinationKeypair.address
    });
    console.log('  ✓ SOL transfer instruction created');

    // Add memo instruction
    const memoInstruction = getAddMemoInstruction({
        memo: 'Hello, Kora!',
    });
    console.log('  ✓ Memo instruction created');

    const instructions = [
        ...transferTokens.instructions,
        ...transferSol.instructions,
        memoInstruction
    ];
    
    console.log(`  → Total: ${instructions.length} instructions`);
    return { instructions, paymentToken };
}

There's quite a bit happening here, so let's walk through it step by step:

  1. We use getConfig to get the payment token from Kora's configuration. Because we set up our server, we know there's only one token in the allowlist, so we can access it directly in the 1st position (config.validation_config.allowed_spl_paid_tokens[0]).
  2. We create a token transfer instruction using the Kora Client's transferTransaction method. This is a helper method that makes it easy to create a token transfer instruction.
  3. We create a SOL transfer instruction using the Kora Client's transferTransaction method. We are including this here to show how to build SOL transfers using the Kora Client--note that we use the Native SOL mint 11111111111111111111111111111111 to indicate we want to transfer SOL instead of an SPL token transfer.
  4. We add a memo instruction using the @solana/programs library's getAddMemoInstruction function.
  5. We combine all the instructions into a single array. We will use this array to build our estimate transaction in the next step.

Step 4: Get Payment Instruction from Kora

Create a transaction that will generate a payment instruction to Kora in exchange for the fees required to execute the transaction.

async function getPaymentInstruction(
    client: KoraClient, 
    instructions: Instruction[],
    testSenderKeypair: KeyPairSigner,
    paymentToken: string
): Promise<{ paymentInstruction: Instruction }> {
    console.log('\n[4/6] Estimating Kora fee and assembling payment instruction');
    
    const { signer_address } = await client.getPayerSigner();
    const noopSigner = createNoopSigner(address(signer_address));
    const latestBlockhash = await client.getBlockhash();
    
    console.log('  → Fee payer:', signer_address.slice(0, 8) + '...');
    console.log('  → Blockhash:', latestBlockhash.blockhash.slice(0, 8) + '...');

    // Create estimate transaction to get payment instruction
    const estimateTransaction = pipe(
        createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }),
        (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
        (tx) => setTransactionMessageLifetimeUsingBlockhash({
        blockhash: latestBlockhash.blockhash as Blockhash,
        lastValidBlockHeight: 0n,
        }, tx),
        (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx),
        (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx),
        (tx) => appendTransactionMessageInstructions(instructions, tx),
    );

    const signedEstimateTransaction = await partiallySignTransactionMessageWithSigners(estimateTransaction);
    const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedEstimateTransaction);
    console.log('  ✓ Estimate transaction built');

    // Get payment instruction from Kora
    const paymentInstruction = await client.getPaymentInstruction({
        transaction: base64EncodedWireTransaction,
        fee_token: paymentToken,
        source_wallet: testSenderKeypair.address,
    });
    console.log('  ✓ Payment instruction received from Kora');
    
    return { paymentInstruction: paymentInstruction.payment_instruction };
}

The Kora SDK has a helper method getPaymentInstruction that will calculate the exact fees required for the transaction and create a payment transfer instruction. Here's how we're using it:

  1. First, we create an estimateTransaction that includes our desired instructions--this transaction will be simulated on the Kora server to estimate the fees required for the transaction.
  2. We then partially sign the transaction to get a base64 encoded wire string.
  3. We pass our base64 encoded wire transaction to the getPaymentInstruction method with the payment token and source of the payment. This will return an Instruction object that we can add to our transaction.

Key concepts here:

  • Valid Blockhash - We use the getBlockhash method to get a valid blockhash for our transaction. This is required for estimating the transaction as it will simulate the transaction on the server.
  • Noop Signer - Placeholder signer used when building transactions before Kora signs. This will allow us to specify a fee payer in our transaction before we have Kora's signature. For more information on Noop Signers, see Solana Kit Documentation.
  • Partial Signing - In order to get our transaction as a base64 encoded wire string (we need this to send the transaction via the Kora RPC), we need to partially sign the transaction. For more information on Partial Signers, see Solana Kit Documentation.

Step 5: Create and Sign Final Transaction

Now that we have our payment instruction, we can create a final transaction that includes our original instructions and the payment instruction.

async function getFinalTransaction(
    client: KoraClient, 
    paymentInstruction: Instruction,
    testSenderKeypair: KeyPairSigner, 
    instructions: Instruction[], 
    signer_address: string
): Promise<Base64EncodedWireTransaction> {
    console.log('\n[5/6] Creating and signing final transaction (with payment)');
    const noopSigner = createNoopSigner(address(signer_address));

    // Build final transaction with payment instruction
    const newBlockhash = await client.getBlockhash();
    const fullTransaction = pipe(
        createTransactionMessage({ version: CONFIG.transactionVersion as TransactionVersion }),
        (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
        (tx) => setTransactionMessageLifetimeUsingBlockhash({
        blockhash: newBlockhash.blockhash as Blockhash,
        lastValidBlockHeight: 0n,
        }, tx),
        (tx) => updateOrAppendSetComputeUnitPriceInstruction(CONFIG.computeUnitPrice, tx),
        (tx) => updateOrAppendSetComputeUnitLimitInstruction(CONFIG.computeUnitLimit, tx),
        (tx) => appendTransactionMessageInstructions([...instructions, paymentInstruction], tx),
    );
    console.log('  ✓ Final transaction built with payment');

    // Sign with user keypair
    const signedFullTransaction = await partiallySignTransactionMessageWithSigners(fullTransaction);
    const userSignedTransaction = await partiallySignTransaction([testSenderKeypair.keyPair], signedFullTransaction);
    const base64EncodedWireFullTransaction = getBase64EncodedWireTransaction(userSignedTransaction);
    console.log('  ✓ Transaction signed by user');
    
    return base64EncodedWireFullTransaction;
}

We use the same pipe function to assemble our transaction. Our final transaction includes:

  • Our original instructions
  • The payment instruction
  • A fresh blockhash
  • The same noop signer as previously used to build the estimate transaction

We then call the same partiallySignTransactionMessageWithSigners function to get a base64 encoded wire string of the transaction. This time, however, we also run partiallySignTransaction to sign the transaction with our testSenderKeypair. Though our Kora node is paying the network fees, our testSender still needs to sign to authorize the token payment and the other transfer instructions we created. For Kora nodes that do not require payment, certain instructions may not require this signing step. Finally, we return the base64 encoded wire string of the transaction.

Step 6: Submit Transaction

Finally, we need to get the Kora node to sign the transaction so we can send a fully signed transaction to the network. We do this by calling the signTransaction method on the Kora client.


async function submitTransaction(
    client: KoraClient, 
    rpc: Rpc<SolanaRpcApi>, 
    confirmTransaction: ReturnType<typeof createRecentSignatureConfirmationPromiseFactory>, 
    signedTransaction: Base64EncodedWireTransaction, 
    signer_address: string
) {
    console.log('\n[6/6] Signing transaction with Kora and sending to Solana cluster');
    
    // Get Kora's signature
    const { signed_transaction } = await client.signTransaction({
        transaction: signedTransaction,
        signer_key: signer_address
    });
    console.log('  ✓ Transaction co-signed by Kora');

    // Submit to Solana network
    const signature = await rpc.sendTransaction(signed_transaction as Base64EncodedWireTransaction, {
        encoding: 'base64'
    }).send();
    console.log('  ✓ Transaction submitted to network');
    
    console.log('  ⏳ Awaiting confirmation...');
    await confirmTransaction({
        commitment: 'confirmed',
        signature,
        abortSignal: new AbortController().signal
    });
    
    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('SUCCESS: Transaction confirmed on Solana');
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('\nTransaction signature:');
    console.log(signature);
    
    return signature;
}

Here we are doing three things:

  1. We call the signTransaction method on the Kora client to get the Kora node to sign the transaction. The node will introspect the transaction to ensure the payment is sufficient and then sign the transaction. Note: some Kora nodes may enable signTransaction that do not require payment. You can check your node's configuration to see if this is enabled by running getConfig().
  2. We send the fully signed transaction to the Solana network using the Solana RPC client.
  3. We wait for the transaction to be confirmed on the network.

Main Orchestration Function

The main function ties everything together and calls each of our functions in sequence:

async function main() {
    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('KORA GASLESS TRANSACTION DEMO');
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    
    try {
        // Step 1: Initialize clients
        const { client, rpc, confirmTransaction } = await initializeClients();
        
        // Step 2: Setup keys
        const { testSenderKeypair, destinationKeypair, signer_address } = await setupKeys(client);
        
        // Step 3: Create demo instructions
        const { instructions, paymentToken } = await createInstructions(client, testSenderKeypair, destinationKeypair);
        
        // Step 4: Get payment instruction from Kora
        const { paymentInstruction } = await getPaymentInstruction(client, instructions, testSenderKeypair, paymentToken);
        
        // Step 5: Create and partially sign final transaction
        const finalSignedTransaction = await getFinalTransaction(
            client,  
            paymentInstruction, 
            testSenderKeypair, 
            instructions, 
            signer_address
        );
        
        // Step 6: Get Kora's signature and submit to Solana cluster
        await submitTransaction(client, rpc, confirmTransaction, finalSignedTransaction, signer_address);
    } catch (error) {
        console.error('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
        console.error('ERROR: Demo failed');
        console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
        console.error('\nDetails:', error);
        process.exit(1);
    }
}

Running the Full Demo

To run the complete gasless transaction demo:

1. Ensure Prerequisites

Set up three terminal windows:

  1. Start your local test validator:
solana-test-validator -r
  1. Start your Kora RPC server (from the examples/getting-started/demo/server directory):
kora rpc start --signers-config signers.toml
  1. Initialize your environment (from the examples/getting-started/demo/client directory):
pnpm init-env

2. Run the Demo

# From the client directory
pnpm full-demo

3. Expected Output

You should see the step-by-step execution with a successful transaction at the end. The transaction will:

  • Transfer tokens from sender to destination
  • Transfer SOL from sender to destination
  • Include a "Hello, Kora!" memo message
  • Pay fees to the Kora node operator in your configured SPL token
  • Have transaction gas fees paid by the Kora node operator

Recap: Understanding the Flow

Let's review what happens in this demonstration:

  1. User Intent - User assembled a transaction that included a variety of instructions that they wanted to execute.
  2. Fee Estimation - Kora calculated the transaction cost in the user's preferred token and created a payment instruction.
  3. Transaction Assembly - User assembled a final transaction that included the user's intended instructions and the Kora payment instruction.
  4. Transaction Signing - User partially signed the transaction with their keypair and sent to the Kora node for signing after verifying the payment was sufficient.
  5. Atomic Execution - User sends transaction to the Solana and everything happens in a single transaction:
    • User's intended transfers execute
    • Payment for fees transfers to Kora
    • Kora pays the Solana network fees and signs the transaction

And like that, users do not need to hold SOL to pay for gas fees--they can pay for everything in the tokens they already hold!

Troubleshooting

Common Issues

Transaction Validation Fails

  • Verify all programs are whitelisted in kora.toml
  • Check that token mints are in allowed_spl_paid_tokens
  • Ensure transaction doesn't exceed max_allowed_lamports

Generating Payment Instruction Fails

  • Confirm the estimate transaction has a fresh blockhash for simulation
  • Verify Kora's payment address has initialized ATAs
  • Check that the payment token is properly configured

Signature Verification Fails

  • Ensure all required signers are included (Kora and any signers required for token payments or other instructions included in your transaction)
  • Verify the transaction hasn't been modified after signing
  • Check that keypairs are loaded correctly

Wrap Up

Congratulations! You've successfully implemented a complete gasless transaction flow with Kora.

Kora makes it possible to provide users with a seamless Web3 experience where they never need to worry about gas fees or holding SOL. Whether you're building a NeoBank, gaming platform, or liquid staking platform, Kora's gasless transactions remove a major barrier to user adoption.

Additional Resources