> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sponsored Transactions on Solana with a Relayer

<Card title="Recommended: JavaScript SDK with React Hooks" icon="react" href="/javascript/reference/react-quickstart" color="#4779FE">
  For new React apps, we recommend the JavaScript SDK with React Hooks (`@dynamic-labs-sdk/react-hooks`) instead of the legacy React SDK documented here. The JS SDK comes with many benefits such as a much smaller bundle size and other optimizations. Use the [React quickstart (JavaScript SDK)](/javascript/reference/react-quickstart) to get started.
</Card>

<Note>This is a React-only guide.</Note>

## Introduction

In this guide, we'll show you how to create Solana transactions where users pay transaction fees in SPL tokens (like USDC) instead of SOL. We achieve this using [Kora](https://github.com/solana-foundation/kora), a fee abstraction service that allows users to pay fees in any supported SPL token.

This approach is different from traditional gasless transactions because:

* Users still pay fees, but in SPL tokens instead of SOL
* No server-side fee payer wallet is required
* Kora handles fee estimation and payment instruction creation
* Kora co-signs transactions as the fee payer

## Getting Started

### Setting up the Project

We'll use Next.js for this example. Create the project with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app), then follow the [React Quickstart](/react/reference/quickstart) **Custom setup** path and enable **Solana (SVM)** only. In the [Dynamic dashboard](https://app.dynamic.xyz/dashboard), turn on **Solana** under **Chains & Networks** and add your dev origin under **Security** → **Allowed Origins**.

If you already have a Next.js app, add the Dynamic React SDK using the same quickstart.

### Installing Dependencies

Install the required packages:

```shell theme={"system"}
bun add @solana/kora @solana/kit @solana-program/compute-budget @solana-program/memo @solana/transaction-confirmation
```

### Setting Up Kora

You'll need a running Kora instance before you can use sponsored transactions. You can:

1. **Run Kora locally** - Follow the [Kora setup guide](https://launch.solana.com/docs/kora/getting-started/quick-start) to run a local instance
2. **Use a hosted Kora instance** - Point your configuration to your hosted Kora server URL

For local development, Kora typically runs on `http://localhost:8080/`.

If you're using devnet instead of localnet, pass the RPC URL to the Kora command:

```shell theme={"system"}
kora rpc start --signers-config signers.toml --rpc-url https://api.devnet.solana.com
```

### Configuration

The example uses a configuration object with the following values:

```typescript theme={"system"}
const CONFIG = {
  computeUnitLimit: 200_000,
  computeUnitPrice: BigInt(1_000_000) as MicroLamports,
  transactionVersion: 0 as TransactionVersion,
  solanaRpcUrl: "https://api.devnet.solana.com",
  solanaWsUrl: "wss://api.devnet.solana.com",
  koraRpcUrl: "http://localhost:8080/",
  tokenMintAddress: "5whA1qmcFkywQPoxsZ43185kzpeChAVbiRj2j5HanBZy",
};
```

Update `koraRpcUrl` to point to your Kora instance, and set `tokenMintAddress` to the SPL token you want to use for fee payments. If you're using devnet, make sure `solanaRpcUrl` and `solanaWsUrl` point to devnet endpoints.

For production, consider using environment variables instead of hardcoded values.

## Client Implementation

### Creating the Transaction Component

Now let's create a component that handles sponsored transactions using Kora. Create `components/gasless-transaction-demo.tsx`:

```tsx components/gasless-transaction-demo.tsx theme={"system"}
"use client";

import { useDynamicContext, useIsLoggedIn } from "@dynamic-labs/sdk-react-core";
import { isSolanaWallet } from "@dynamic-labs/solana";
import {
  updateOrAppendSetComputeUnitLimitInstruction,
  updateOrAppendSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import { getAddMemoInstruction } from "@solana-program/memo";
import {
  findAssociatedTokenPda,
  TOKEN_PROGRAM_ADDRESS,
} from "@solana-program/token";
import {
  Base64EncodedWireTransaction,
  Blockhash,
  Instruction,
  MicroLamports,
  TransactionVersion,
  address,
  appendTransactionMessageInstructions,
  createNoopSigner,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  createTransactionMessage,
  getBase64EncodedWireTransaction,
  partiallySignTransactionMessageWithSigners,
  pipe,
  setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash,
} from "@solana/kit";
import { KoraClient } from "@solana/kora";
import { createRecentSignatureConfirmationPromiseFactory } from "@solana/transaction-confirmation";
import { VersionedTransaction } from "@solana/web3.js";
import { useState, useEffect } from "react";

const CONFIG = {
  computeUnitLimit: 200_000,
  computeUnitPrice: BigInt(1_000_000) as MicroLamports,
  transactionVersion: 0 as TransactionVersion,
  solanaRpcUrl: "https://api.devnet.solana.com",
  solanaWsUrl: "wss://api.devnet.solana.com",
  koraRpcUrl: "http://localhost:8080/",
  tokenMintAddress: "5whA1qmcFkywQPoxsZ43185kzpeChAVbiRj2j5HanBZy",
};

export default function GaslessTransactionDemo() {
  const isLoggedIn = useIsLoggedIn();
  const { primaryWallet } = useDynamicContext();
  const [status, setStatus] = useState<string>("");
  const [loading, setLoading] = useState(false);
  const [transactionSignature, setTransactionSignature] = useState<
    string | null
  >(null);
  const [tokenBalance, setTokenBalance] = useState<string | null>(null);
  const [tokenBalanceLoading, setTokenBalanceLoading] = useState(false);

  // Fetch the user's token balance for the configured payment token
  useEffect(() => {
    const fetchTokenBalance = async () => {
      if (!primaryWallet || !isSolanaWallet(primaryWallet)) {
        setTokenBalance(null);
        return;
      }

      setTokenBalanceLoading(true);
      try {
        const rpc = createSolanaRpc(CONFIG.solanaRpcUrl);
        const mintAddress = address(CONFIG.tokenMintAddress);
        const ownerAddress = address(primaryWallet.address);

        const [ata] = await findAssociatedTokenPda({
          mint: mintAddress,
          owner: ownerAddress,
          tokenProgram: TOKEN_PROGRAM_ADDRESS,
        });

        const tokenAccountData = await rpc
          .getAccountInfo(ata, {
            encoding: "jsonParsed",
          })
          .send();

        if (
          tokenAccountData.value?.data &&
          "parsed" in tokenAccountData.value.data
        ) {
          const parsed = tokenAccountData.value.data.parsed as {
            info?: {
              tokenAmount?: {
                amount: string;
                decimals: number;
              };
            };
          };
          if (parsed.info?.tokenAmount) {
            const amount = parsed.info.tokenAmount.amount;
            const decimals = parsed.info.tokenAmount.decimals;
            const balance = Number(amount) / Math.pow(10, decimals);
            setTokenBalance(balance.toFixed(decimals > 6 ? 6 : decimals));
          } else {
            setTokenBalance("0");
          }
        } else {
          setTokenBalance("0");
        }
      } catch (error) {
        setTokenBalance("Error");
      } finally {
        setTokenBalanceLoading(false);
      }
    };

    fetchTokenBalance();
  }, [primaryWallet]);

  const handleGaslessTransaction = async () => {
    if (!primaryWallet || !isSolanaWallet(primaryWallet)) {
      setStatus("Error: Solana wallet not available or not properly connected");
      return;
    }

    setLoading(true);
    setStatus("Initializing...");
    setTransactionSignature(null);

    try {
      setStatus("Connecting to Kora...");
      const koraClient = new KoraClient({
        rpcUrl: CONFIG.koraRpcUrl,
      });

      const rpc = createSolanaRpc(CONFIG.solanaRpcUrl);
      const rpcSubscriptions = createSolanaRpcSubscriptions(CONFIG.solanaWsUrl);
      const confirmTransaction =
        createRecentSignatureConfirmationPromiseFactory({
          rpc,
          rpcSubscriptions,
        });

      setStatus("Getting Kora signer...");
      const { signer_address } = await koraClient.getPayerSigner();
      const noopSigner = createNoopSigner(address(signer_address));

      setStatus("Getting payment token...");
      const config = await koraClient.getConfig();
      const paymentToken = config.validation_config.allowed_spl_paid_tokens[0];

      setStatus("Creating transaction...");
      const memoInstruction = getAddMemoInstruction({
        memo: "Hello from Dynamic + Kora gasless transaction!",
      });
      const instructions: Instruction[] = [memoInstruction];

      setStatus("Estimating fees...");
      const latestBlockhash = await koraClient.getBlockhash();

      const initialEstimateTransaction = pipe(
        createTransactionMessage({ version: CONFIG.transactionVersion }),
        (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
        (tx) =>
          setTransactionMessageLifetimeUsingBlockhash(
            {
              blockhash: latestBlockhash.blockhash as Blockhash,
              lastValidBlockHeight: BigInt(0),
            },
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitPriceInstruction(
            CONFIG.computeUnitPrice,
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitLimitInstruction(
            CONFIG.computeUnitLimit,
            tx
          ),
        (tx) => appendTransactionMessageInstructions(instructions, tx)
      );

      const signedInitialEstimate =
        await partiallySignTransactionMessageWithSigners(
          initialEstimateTransaction
        );
      const initialEstimateBase64 = getBase64EncodedWireTransaction(
        signedInitialEstimate
      );

      setStatus("Getting payment instruction...");
      const initialPaymentResponse = await koraClient.getPaymentInstruction({
        transaction: initialEstimateBase64,
        fee_token: paymentToken,
        source_wallet: primaryWallet.address,
      });
      let paymentInstruction: Instruction =
        initialPaymentResponse.payment_instruction;

      setStatus("Building final transaction...");
      const newBlockhash = await koraClient.getBlockhash();

      const fullTransaction = pipe(
        createTransactionMessage({ version: CONFIG.transactionVersion }),
        (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
        (tx) =>
          setTransactionMessageLifetimeUsingBlockhash(
            {
              blockhash: newBlockhash.blockhash as Blockhash,
              lastValidBlockHeight: BigInt(0),
            },
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitPriceInstruction(
            CONFIG.computeUnitPrice,
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitLimitInstruction(
            CONFIG.computeUnitLimit,
            tx
          ),
        (tx) =>
          appendTransactionMessageInstructions(
            [...instructions, paymentInstruction],
            tx
          )
      );

      setStatus("Re-estimating fees...");
      const finalEstimateTransaction =
        await partiallySignTransactionMessageWithSigners(fullTransaction);
      const finalEstimateBase64 = getBase64EncodedWireTransaction(
        finalEstimateTransaction
      );

      const finalPaymentResponse = await koraClient.getPaymentInstruction({
        transaction: finalEstimateBase64,
        fee_token: paymentToken,
        source_wallet: primaryWallet.address,
      });
      paymentInstruction = finalPaymentResponse.payment_instruction;

      const correctedFullTransaction = pipe(
        createTransactionMessage({ version: CONFIG.transactionVersion }),
        (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
        (tx) =>
          setTransactionMessageLifetimeUsingBlockhash(
            {
              blockhash: newBlockhash.blockhash as Blockhash,
              lastValidBlockHeight: BigInt(0),
            },
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitPriceInstruction(
            CONFIG.computeUnitPrice,
            tx
          ),
        (tx) =>
          updateOrAppendSetComputeUnitLimitInstruction(
            CONFIG.computeUnitLimit,
            tx
          ),
        (tx) =>
          appendTransactionMessageInstructions(
            [...instructions, paymentInstruction],
            tx
          )
      );

      setStatus("Signing transaction...");
      const signedFullTransaction =
        await partiallySignTransactionMessageWithSigners(
          correctedFullTransaction
        );

      const wireTransactionBase64 = getBase64EncodedWireTransaction(
        signedFullTransaction
      );
      const originalTransactionBytes = Buffer.from(
        wireTransactionBase64,
        "base64"
      );
      const originalTransaction = VersionedTransaction.deserialize(
        originalTransactionBytes
      );

      const message = originalTransaction.message;
      const numRequiredSignatures = message.header.numRequiredSignatures;
      const accountKeys = message.staticAccountKeys;
      const userAddress = primaryWallet.address;

      let userSignatureIndex = -1;
      for (
        let i = 0;
        i < numRequiredSignatures && i < accountKeys.length;
        i++
      ) {
        if (accountKeys[i].toBase58() === userAddress) {
          userSignatureIndex = i;
          break;
        }
      }

      if (userSignatureIndex === -1) {
        throw new Error(
          `User address ${userAddress} not found in transaction signers`
        );
      }

      const signer = await primaryWallet.getSigner();
      const signedTransaction = await signer.signTransaction(
        originalTransaction as any
      );

      const userSignature = signedTransaction.signatures[userSignatureIndex];
      if (!userSignature || userSignature.every((b: number) => b === 0)) {
        throw new Error("Failed to get signature from Dynamic wallet");
      }

      if (userSignature.length !== 64) {
        throw new Error(
          `Invalid signature length: expected 64 bytes, got ${userSignature.length}`
        );
      }

      const preservedTransaction = VersionedTransaction.deserialize(
        originalTransactionBytes
      );
      preservedTransaction.signatures[userSignatureIndex] = userSignature;

      const base64EncodedWireFullTransaction = Buffer.from(
        preservedTransaction.serialize()
      ).toString("base64");

      if (
        preservedTransaction.message.compiledInstructions.length <
        instructions.length + 1
      ) {
        throw new Error(
          `Transaction missing instructions. Expected at least ${
            instructions.length + 1
          }, got ${preservedTransaction.message.compiledInstructions.length}`
        );
      }

      setStatus("Getting Kora signature...");
      const { signed_transaction } = await koraClient.signTransaction({
        transaction: base64EncodedWireFullTransaction,
        signer_key: signer_address,
      });

      setStatus("Submitting to Solana network...");
      const signature = await rpc
        .sendTransaction(signed_transaction as Base64EncodedWireTransaction, {
          encoding: "base64",
        })
        .send();

      setTransactionSignature(signature);
      setStatus("Transaction submitted! Waiting for confirmation...");

      await confirmTransaction({
        commitment: "confirmed",
        signature,
        abortSignal: new AbortController().signal,
      });

      setStatus("✅ Transaction confirmed!");
    } catch (error) {
      setStatus(
        `❌ Error: ${error instanceof Error ? error.message : "Unknown error"}`
      );
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="w-full max-w-2xl mx-auto p-6 border rounded-lg space-y-4">
      <h2 className="text-2xl font-semibold">Gasless Transaction Demo</h2>
      <p className="text-sm text-muted-foreground">
        This demo sends a gasless transaction on Solana using Kora. The
        transaction fees are paid in SPL tokens instead of SOL.
      </p>

      <div className="space-y-2">
        <div className="text-sm">
          <strong>Wallet:</strong>{" "}
          {primaryWallet && isSolanaWallet(primaryWallet)
            ? primaryWallet.address
            : "Not connected"}
        </div>
        <div className="text-sm">
          <strong>Kora RPC:</strong> {CONFIG.koraRpcUrl}
        </div>
        <div className="text-sm">
          <strong>Solana RPC:</strong> {CONFIG.solanaRpcUrl}
        </div>
        {primaryWallet && isSolanaWallet(primaryWallet) && (
          <div className="text-sm">
            <strong>
              Token Balance ({CONFIG.tokenMintAddress.slice(0, 8)}...):
            </strong>{" "}
            {tokenBalanceLoading ? (
              <span className="text-gray-500">Loading...</span>
            ) : tokenBalance !== null ? (
              `${tokenBalance} tokens`
            ) : (
              <span className="text-gray-500">N/A</span>
            )}
          </div>
        )}
      </div>

      <button
        onClick={handleGaslessTransaction}
        disabled={
          !isLoggedIn ||
          !primaryWallet ||
          !isSolanaWallet(primaryWallet) ||
          loading
        }
        className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {loading ? "Processing..." : "Send Gasless Transaction"}
      </button>

      {status && (
        <div className="p-4 bg-gray-100 dark:bg-gray-800 rounded text-sm">
          <strong>Status:</strong> {status}
        </div>
      )}

      {transactionSignature && (
        <div className="p-4 bg-green-50 dark:bg-green-900/20 rounded text-sm">
          <strong>Transaction Signature:</strong>
          <div className="mt-2 break-all font-mono text-xs">
            {transactionSignature}
          </div>
          <a
            href={`https://explorer.solana.com/tx/${transactionSignature}?cluster=devnet`}
            target="_blank"
            rel="noopener noreferrer"
            className="mt-2 inline-block text-blue-600 hover:underline"
          >
            View on Solana Explorer →
          </a>
        </div>
      )}
    </div>
  );
}
```

### Technical Deep Dive

The `handleGaslessTransaction` function orchestrates a multi-step process to create a transaction where Kora pays fees in SOL while the user pays in SPL tokens. Here's a technical breakdown:

**Initial Setup (lines 206-217)**: Creates a `KoraClient` instance to communicate with the Kora service, sets up Solana RPC clients for both HTTP and WebSocket connections, and initializes a transaction confirmation promise factory for monitoring transaction status.

**Kora Configuration (lines 219-225)**: Retrieves Kora's fee payer signer address (which will pay fees in SOL) and creates a noop signer placeholder. Fetches Kora's configuration to determine which SPL token the user will pay fees with.

**Transaction Building with Fee Estimation (lines 227-275)**:

* Creates the user's instructions (in this case, a memo instruction)
* Builds an initial transaction estimate using the `pipe` function from `@solana/kit` to compose transaction modifications
* Sets the fee payer to Kora's signer, adds compute budget instructions (limit and price), and appends user instructions
* Partially signs the transaction (without user signature) and converts it to base64
* Sends this estimate to Kora's `getPaymentInstruction` endpoint, which calculates the SPL token fee and returns a payment instruction

**Final Transaction Construction (lines 277-348)**:

* Gets a fresh blockhash (required for transaction validity)
* Builds the full transaction including both user instructions and the payment instruction
* Re-estimates fees by sending the complete transaction to Kora again (since adding the payment instruction changes the transaction size and fee calculation)
* Rebuilds the transaction with the corrected payment instruction

**User Signing (lines 350-424)**:

* Partially signs the transaction to get a base64-encoded wire format
* Deserializes it to a `VersionedTransaction` to access the message structure
* Identifies the user's signature index by finding their address in the transaction's required signers
* Uses Dynamic's wallet signer to sign the transaction, extracting only the user's signature from the signed result
* Preserves the original transaction structure and injects the user's signature at the correct index (this is necessary because Dynamic's signer may modify the transaction structure)

**Kora Co-signing and Submission (lines 426-448)**:

* Sends the user-signed transaction to Kora's `signTransaction` endpoint, which adds Kora's signature as the fee payer
* Submits the fully signed transaction to the Solana network via RPC
* Waits for transaction confirmation using the confirmation promise factory, which monitors both RPC polling and WebSocket subscriptions for efficient confirmation

The two-phase fee estimation (initial estimate → final estimate) is necessary because the payment instruction itself consumes compute units, so the final fee calculation must account for the complete transaction including the payment instruction.

### How It Works

The sponsored transaction flow follows these steps:

1. **Get Kora signer** - Retrieve the address that will pay fees (in SOL)
2. **Get payment token** - Determine which SPL token the user will pay fees with
3. **Build estimate transaction** - Create a transaction with your instructions to estimate fees
4. **Get payment instruction** - Request a payment instruction from Kora that charges the user in SPL tokens
5. **Build final transaction** - Combine your instructions with the payment instruction
6. **Re-estimate fees** - Get an updated payment instruction based on the full transaction
7. **Sign with user wallet** - User signs the transaction (including the payment instruction)
8. **Get Kora signature** - Kora co-signs as the fee payer (paying in SOL)
9. **Submit to network** - Send the fully signed transaction to Solana

### Using the Component

Add the component to your main page in `app/page.tsx`:

```tsx app/page.tsx theme={"system"}
"use client";

import { useIsLoggedIn } from "@dynamic-labs/sdk-react-core";
import { DynamicWidget } from "@dynamic-labs/sdk-react-core";
import GaslessTransactionDemo from "@/components/gasless-transaction-demo";

export default function Home() {
  const isLoggedIn = useIsLoggedIn();

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-8">
      <div className="w-full max-w-2xl space-y-8">
        <div className="text-center space-y-2">
          <h1 className="text-4xl font-bold">Gasless Solana Transactions</h1>
          <p className="text-muted-foreground">
            Powered by Dynamic SDK and Kora
          </p>
        </div>

        <div className="flex justify-center">
          <DynamicWidget />
        </div>

        {isLoggedIn && (
          <div className="mt-8">
            <GaslessTransactionDemo />
          </div>
        )}

        {!isLoggedIn && (
          <div className="text-center text-muted-foreground mt-8">
            Connect your Solana wallet to get started
          </div>
        )}
      </div>
    </main>
  );
}
```

## Extending the Demo

The current demo sends a simple memo instruction. You can extend it to:

* **Token transfers** - Use `@solana-program/token` to transfer SPL tokens
* **SOL transfers** - Transfer native SOL
* **Program interactions** - Call any Solana program
* **Multiple instructions** - Combine multiple operations in one transaction

Example: Add a token transfer instruction:

```typescript theme={"system"}
import { getTransferInstruction } from "@solana-program/token";

const transferInstruction = getTransferInstruction({
  source: sourceTokenAccount,
  destination: destinationTokenAccount,
  amount: 1000000n, // 1 token (6 decimals)
  owner: publicKey,
});

const instructions = [transferInstruction, memoInstruction];
```

## Configuration

### Compute Budget

The demo uses default compute budget settings:

```typescript theme={"system"}
const CONFIG = {
  computeUnitLimit: 200_000,
  computeUnitPrice: BigInt(1_000_000), // 0.001 SOL per compute unit
};
```

Adjust these based on your transaction complexity.

### Transaction Version

The demo uses version 0 (legacy) transactions. For versioned transactions:

```typescript theme={"system"}
const transactionVersion = 0 as TransactionVersion; // or 1 for versioned
```

## Troubleshooting

### "Wallet not connected"

* Ensure you've connected a Solana wallet through Dynamic
* Check that `useSolanaWallet()` returns a valid `publicKey` and `signTransaction`

### "Kora RPC error"

* Verify Kora is running and accessible at the configured URL
* Check network connectivity and CORS settings
* For local development, ensure Kora is running on `http://localhost:8080/`

### "Transaction failed"

* Check that your wallet has sufficient SPL tokens for fees
* Verify the Solana RPC endpoint is accessible
* Check transaction logs for specific error messages

### "Payment instruction failed"

* Ensure the payment token is configured in Kora
* Verify the source wallet has sufficient balance of the payment token
* Check Kora's validation configuration

## Conclusion

Congratulations! You've successfully implemented sponsored transactions on Solana using Dynamic's SDK and Kora. This approach allows users to pay transaction fees in SPL tokens instead of SOL, creating a more flexible payment experience.

### Next Steps

* Configure Kora to support your preferred payment tokens
* Implement fee estimation UI to show users the cost before signing
* Add support for multiple payment tokens with user selection

To get the complete source code, check out our [GitHub repository](https://github.com/dynamic-labs/gasless-solana-kora).
