> ## 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.

# Kamino yield vaults

> Let users deposit stablecoins into Kamino Earn vaults on Solana to earn yield automatically

## Overview

[Kamino](https://kamino.finance/) Earn vaults accept token deposits and automatically allocate them across lending markets. Depositors receive shares representing their portion of the vault, and yield accrues without any manual compounding. This guide walks through integrating Kamino vaults into a Next.js app with Dynamic embedded wallets.

For the final code, see the [GitHub repository](https://github.com/dynamic-labs-oss/examples/tree/main/examples/nextjs-stablecoin-yield-kamino).

## How it works

When a user deposits tokens, the Kamino SDK builds a set of instructions that create a share account and mint vault shares proportional to the deposit. Yield accrues as the vault allocates capital to borrowers. When withdrawing, shares are burned and the user receives back the underlying tokens plus earned interest.

The SDK returns instruction groups that must each be sent as a **separate transaction**, confirmed in order:

* **Deposit:** `depositIxs` → `stakeInFarmIfNeededIxs` → `stakeInFlcFarmIfNeededIxs`
* **Withdraw:** `unstakeFromFarmIfNeededIxs` → `withdrawIxs` → `postWithdrawIxs`

Combining groups into one transaction exceeds the 1232-byte Solana limit.

This guide shows two approaches for signing and sending these transactions:

| Approach                      | How it works                                                          | Requires                                     |
| ----------------------------- | --------------------------------------------------------------------- | -------------------------------------------- |
| **SVM Gas Sponsorship**       | Dynamic sponsors the SOL fee; user signs without holding SOL          | SVM Gas Sponsorship enabled in the dashboard |
| **Standard (no sponsorship)** | User pays SOL fees from their wallet; all txs signed in one MPC round | SOL balance in the embedded wallet           |

## Setup

### Project setup

Follow the [React Quickstart](/docs/react/reference/quickstart) using the **Custom setup** path with **Solana**. Scaffold a Next.js app with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

<Info>
  In the Dynamic dashboard, enable **Solana** under **Chains & Networks**, enable **Embedded wallets** under **Wallets**, and add your app's origin under **Security → Allowed Origins**.
</Info>

### Install dependencies

```bash theme={"system"}
npm install @kamino-finance/klend-sdk @dynamic-labs-sdk/client @dynamic-labs-sdk/solana @dynamic-labs-sdk/react-hooks @solana/kit @solana/web3.js @tanstack/react-query decimal.js
```

<Warning>
  `@kamino-finance/klend-sdk` uses WASM internally. Always import it with `ssr: false` in Next.js to avoid server-side rendering errors.
</Warning>

### Environment variables

```env .env.local theme={"system"}
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
```

Your environment ID is in the Dynamic dashboard under **Developer Settings → SDK & API Keys**.

The Solana RPC URL is sourced automatically from your Dynamic dashboard network configuration. You can optionally override it with `NEXT_PUBLIC_SOLANA_RPC_URL` if you prefer a specific endpoint.

### Initialize Dynamic

Create `src/lib/dynamic.ts`:

```typescript src/lib/dynamic.ts theme={"system"}
import { createDynamicClient, getNetworksData } from "@dynamic-labs-sdk/client";
import { addSolanaExtension } from "@dynamic-labs-sdk/solana";

export const dynamicClient = createDynamicClient({
  environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
  metadata: { name: "Kamino Earn" },
});

// Register Solana extension — takes no arguments
addSolanaExtension();

export function getSolanaRpcUrl(): string {
  if (process.env.NEXT_PUBLIC_SOLANA_RPC_URL) {
    return process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
  }
  const networks = getNetworksData(dynamicClient);
  const solana = networks.find((n) => n.chain === "SOL");
  const url = solana?.rpcUrls.http[0];
  if (!url) {
    throw new Error(
      "No Solana RPC URL found. Set NEXT_PUBLIC_SOLANA_RPC_URL or add a Solana network in your Dynamic dashboard."
    );
  }
  return url;
}
```

### Set up wallet context

Create `src/lib/providers.tsx`. `DynamicProvider` owns reactive auth state; `InnerProviders` reads it via `useUser()` and `useGetWalletAccounts()` from `@dynamic-labs-sdk/react-hooks`:

```typescript src/lib/providers.tsx theme={"system"}
"use client";

import { createContext, useContext, useEffect, useCallback, type ReactNode } from "react";
import {
  getWalletAccounts, onEvent, isSignedIn, logout,
  detectOAuthRedirect, completeSocialAuthentication,
} from "@dynamic-labs-sdk/client";
import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
import { isSolanaWalletAccount, type SolanaWalletAccount } from "@dynamic-labs-sdk/solana";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DynamicProvider, useUser, useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
import { dynamicClient } from "./dynamic";

const WalletContext = createContext<{
  solanaAccount: SolanaWalletAccount | null;
  loggedIn: boolean;
  ensureSolanaWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}>({
  solanaAccount: null,
  loggedIn: false,
  ensureSolanaWallet: async () => {},
  disconnect: async () => {},
});

export function useWallet() {
  return useContext(WalletContext);
}

const queryClient = new QueryClient();

function InnerProviders({ children }: { children: ReactNode }) {
  const { data: user } = useUser();
  const loggedIn = user !== null;
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const solanaAccount = walletAccounts.find(isSolanaWalletAccount) ?? null;

  const disconnect = useCallback(async () => {
    await logout(dynamicClient);
  }, []);

  const ensureSolanaWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isSolanaWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["SOL"] }, dynamicClient);
      }
    } catch {}
  }, []);

  useEffect(() => {
    const unsub = onEvent(
      { event: "walletAccountsChanged", listener: () => { void ensureSolanaWallet(); } },
      dynamicClient,
    );
    return () => unsub?.();
  }, [ensureSolanaWallet]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        if (await detectOAuthRedirect({ url }, dynamicClient)) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureSolanaWallet();
          window.history.replaceState({}, "", window.location.pathname);
        }
      } catch {}
    };
    handleOAuthRedirect();
  }, [ensureSolanaWallet]);

  return (
    <WalletContext.Provider value={{ solanaAccount, loggedIn, ensureSolanaWallet, disconnect }}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WalletContext.Provider>
  );
}

export default function Providers({ children }: { children: ReactNode }) {
  return (
    <DynamicProvider client={dynamicClient}>
      <InnerProviders>{children}</InnerProviders>
    </DynamicProvider>
  );
}
```

## Fetching vault data

Create `src/lib/kamino.ts` to load vaults and metrics from the Kamino REST API:

```typescript src/lib/kamino.ts theme={"system"}
import type { KaminoVault, VaultMetrics, UserPosition } from "./types";

const KAMINO_API_BASE = "https://api.kamino.finance";

export async function fetchVaults(): Promise<KaminoVault[]> {
  const res = await fetch(`${KAMINO_API_BASE}/kvaults/vaults`, { next: { revalidate: 60 } });
  if (!res.ok) throw new Error(`Failed to fetch vaults: ${res.statusText}`);
  const vaults: KaminoVault[] = await res.json();

  // Filter to vaults with deployed funds, sort by AUM descending
  return vaults
    .filter((v) => parseFloat(v.state.prevAum ?? "0") > 1000)
    .sort((a, b) => parseFloat(b.state.prevAum ?? "0") - parseFloat(a.state.prevAum ?? "0"));
}

export async function fetchVaultMetrics(vaultAddress: string): Promise<VaultMetrics> {
  const res = await fetch(`${KAMINO_API_BASE}/kvaults/vaults/${vaultAddress}/metrics`, { next: { revalidate: 30 } });
  if (!res.ok) throw new Error(`Failed to fetch vault metrics: ${res.statusText}`);
  const d = await res.json();

  return {
    apy: parseFloat(d.apy) || 0,
    apy7d: parseFloat(d.apy7d) || 0,
    apy30d: parseFloat(d.apy30d) || 0,
    tvlUsd: (parseFloat(d.tokensAvailableUsd) || 0) + (parseFloat(d.tokensInvestedUsd) || 0),
    tokenPrice: parseFloat(d.tokenPrice) || 0,
    numberOfHolders: d.numberOfHolders || 0,
    sharesIssued: d.sharesIssued || "0",
    cumulativeInterestEarnedUsd: parseFloat(d.cumulativeInterestEarnedUsd) || 0,
  };
}

export async function fetchUserPositions(userAddress: string): Promise<UserPosition[]> {
  const res = await fetch(`${KAMINO_API_BASE}/kvaults/users/${userAddress}/positions`);
  if (!res.ok) {
    if (res.status === 404) return [];
    throw new Error(`Failed to fetch user positions: ${res.statusText}`);
  }
  const data = await res.json();
  // API returns either an array or { positions: [...] }
  const rows: Record<string, unknown>[] = Array.isArray(data) ? data : (data.positions ?? []);
  return rows.map((r) => ({
    vaultAddress: (r.vaultAddress ?? r.vault ?? "") as string,
    shares: String(r.totalShares ?? r.shares ?? "0"),
    tokenBalance: Number(r.tokenBalance ?? r.balance ?? 0),
    usdValue: Number(r.usdValue ?? r.balanceUsd ?? 0),
  }));
}
```

## Deposit and withdraw

Create `src/lib/useVaultOperations.ts`. The shared `prepareTransaction` helper builds one unsigned `VersionedTransaction` per instruction group. The signing strategy differs between the two approaches.

### Transaction preparation (shared)

Both approaches use the same `prepareTransaction` helper:

```typescript theme={"system"}
import { Connection, VersionedTransaction, SendTransactionError } from "@solana/web3.js";
import {
  createSolanaRpc, createNoopSigner, address, pipe,
  createTransactionMessage, setTransactionMessageFeePayerSigner,
  setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions,
  partiallySignTransactionMessageWithSigners, getBase64EncodedWireTransaction,
  type TransactionSigner,
} from "@solana/kit";
import { getSolanaRpcUrl } from "./dynamic";

interface PreparedTransaction {
  unsigned: VersionedTransaction;
  blockhash: string;
  lastValidBlockHeight: bigint;
}

// Build one unsigned transaction. Uses finalized commitment to avoid
// "Blockhash not found" errors during preflight simulation.
async function prepareTransaction(
  instructions: Parameters<typeof appendTransactionMessageInstructions>[0],
  noopSigner: TransactionSigner
): Promise<PreparedTransaction> {
  const rpc = createSolanaRpc(getSolanaRpcUrl());
  const { value: latestBlockhash } = await rpc
    .getLatestBlockhash({ commitment: "finalized" })
    .send();

  const txMessage = pipe(
    createTransactionMessage({ version: 0 }),
    (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx),
    (tx) => setTransactionMessageLifetimeUsingBlockhash(
      { blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight },
      tx
    ),
    (tx) => appendTransactionMessageInstructions(instructions, tx)
  );

  const compiled = await partiallySignTransactionMessageWithSigners(txMessage);
  const wireBase64 = getBase64EncodedWireTransaction(compiled);
  return {
    unsigned: VersionedTransaction.deserialize(Buffer.from(wireBase64, "base64")),
    blockhash: latestBlockhash.blockhash,
    lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
  };
}
```

### With SVM Gas Sponsorship (gasless)

<Info>
  Enable **SVM Gas Sponsorship** in the Dynamic dashboard under **Settings → Embedded Wallets**. This is available exclusively for V3 MPC embedded wallets.
</Info>

Dynamic sponsors the SOL transaction fee so users don't need a SOL balance. `signAndSendSponsoredTransaction` handles signing, fee-payer replacement, and broadcasting in one call. Transactions are sent one at a time since each is independently sponsored.

```typescript src/lib/useVaultOperations.ts theme={"system"}
"use client";

import { useState } from "react";
import {
  signAndSendSponsoredTransaction,
  SponsorTransactionError,
  type SolanaWalletAccount,
} from "@dynamic-labs-sdk/solana";
import { Connection, VersionedTransaction, SendTransactionError } from "@solana/web3.js";
import {
  createSolanaRpc, createNoopSigner, address,
  type TransactionSigner,
} from "@solana/kit";
import { KaminoVault, type DepositIxs, type WithdrawIxs } from "@kamino-finance/klend-sdk";
import { Decimal } from "decimal.js";
import { useWallet } from "./providers";
import { dynamicClient, getSolanaRpcUrl } from "./dynamic";

// ... prepareTransaction helper from above ...

async function signSendAndConfirm(
  tx: VersionedTransaction,
  blockhash: string,
  lastValidBlockHeight: bigint,
  walletAccount: SolanaWalletAccount
): Promise<string> {
  try {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const { signature } = await signAndSendSponsoredTransaction(
      { transaction: tx as any, walletAccount },
      dynamicClient
    );
    const connection = new Connection(getSolanaRpcUrl(), "confirmed");
    const result = await connection.confirmTransaction(
      { signature, blockhash, lastValidBlockHeight: Number(lastValidBlockHeight) },
      "confirmed"
    );
    if (result.value.err) {
      throw new Error(`Transaction failed: ${JSON.stringify(result.value.err)}`);
    }
    return signature;
  } catch (err) {
    if (err instanceof SponsorTransactionError) {
      throw new Error(
        "Gas sponsorship failed. Enable SVM Gas Sponsorship in your Dynamic dashboard under Settings → Embedded Wallets."
      );
    }
    if (err instanceof SendTransactionError) {
      const connection = new Connection(getSolanaRpcUrl(), "confirmed");
      const logs = await err.getLogs(connection);
      throw new Error(
        err.message + (logs?.length ? `\n\nLogs:\n${logs.join("\n")}` : "")
      );
    }
    throw err;
  }
}

export function useVaultOperations() {
  const { solanaAccount } = useWallet();
  const [isOperating, setIsOperating] = useState(false);
  const [operationError, setOperationError] = useState<string | null>(null);

  const executeDeposit = async (vaultAddress: string, amount: number): Promise<string | undefined> => {
    if (!solanaAccount) return;
    setIsOperating(true);
    setOperationError(null);
    try {
      const rpc = createSolanaRpc(getSolanaRpcUrl());
      const noopSigner = createNoopSigner(address(solanaAccount.address));
      const vault = new KaminoVault(rpc, address(vaultAddress));
      const depositIxs: DepositIxs = await vault.depositIxs(noopSigner, new Decimal(amount));

      const groups = [
        depositIxs.depositIxs,
        depositIxs.stakeInFarmIfNeededIxs,
        depositIxs.stakeInFlcFarmIfNeededIxs,
      ].filter((g) => g.length > 0);

      if (groups.length === 0) return;

      // Build all transactions in parallel, then sponsor and confirm each sequentially.
      const prepared = await Promise.all(groups.map((g) => prepareTransaction(g, noopSigner)));

      let lastHash: string | undefined;
      for (const { unsigned, blockhash, lastValidBlockHeight } of prepared) {
        lastHash = await signSendAndConfirm(unsigned, blockhash, lastValidBlockHeight, solanaAccount);
      }
      return lastHash;
    } catch (err) {
      setOperationError(err instanceof Error ? err.message : "Deposit failed");
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  const executeWithdraw = async (vaultAddress: string, shares: number): Promise<string | undefined> => {
    if (!solanaAccount) return;
    setIsOperating(true);
    setOperationError(null);
    try {
      const rpc = createSolanaRpc(getSolanaRpcUrl());
      const noopSigner = createNoopSigner(address(solanaAccount.address));
      const vault = new KaminoVault(rpc, address(vaultAddress));
      const withdrawIxs: WithdrawIxs = await vault.withdrawIxs(noopSigner, new Decimal(shares));

      const groups = [
        withdrawIxs.unstakeFromFarmIfNeededIxs,
        withdrawIxs.withdrawIxs,
        withdrawIxs.postWithdrawIxs,
      ].filter((g) => g.length > 0);

      if (groups.length === 0) return;

      const prepared = await Promise.all(groups.map((g) => prepareTransaction(g, noopSigner)));

      let lastHash: string | undefined;
      for (const { unsigned, blockhash, lastValidBlockHeight } of prepared) {
        lastHash = await signSendAndConfirm(unsigned, blockhash, lastValidBlockHeight, solanaAccount);
      }
      return lastHash;
    } catch (err) {
      setOperationError(err instanceof Error ? err.message : "Withdraw failed");
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  return { isOperating, operationError, executeDeposit, executeWithdraw };
}
```

### Without gas sponsorship

When SVM Gas Sponsorship is not enabled, users pay Solana transaction fees from their wallet SOL balance. All transactions are built in parallel and signed in a **single MPC round** with `signAllTransactions`, then sent and confirmed sequentially. This minimizes round-trips to the MPC network.

```typescript src/lib/useVaultOperations.ts theme={"system"}
"use client";

import { useState } from "react";
import { signAllTransactions, type SolanaWalletAccount } from "@dynamic-labs-sdk/solana";
import { Connection, VersionedTransaction, SendTransactionError } from "@solana/web3.js";
import {
  createSolanaRpc, createNoopSigner, address,
  type TransactionSigner,
} from "@solana/kit";
import { KaminoVault, type DepositIxs, type WithdrawIxs } from "@kamino-finance/klend-sdk";
import { Decimal } from "decimal.js";
import { useWallet } from "./providers";
import { dynamicClient, getSolanaRpcUrl } from "./dynamic";

// ... prepareTransaction helper from above ...

async function sendAndConfirm(
  tx: VersionedTransaction,
  blockhash: string,
  lastValidBlockHeight: bigint
): Promise<string> {
  const connection = new Connection(getSolanaRpcUrl(), "confirmed");
  let txHash: string;
  try {
    txHash = await connection.sendRawTransaction(tx.serialize(), {
      skipPreflight: false,
      preflightCommitment: "confirmed",
    });
  } catch (err) {
    if (err instanceof SendTransactionError) {
      const logs = err.logs ?? [];
      if (logs.find((l) => l.includes("insufficient lamports"))) {
        throw new Error("Insufficient SOL balance. Add more SOL to your wallet and try again.");
      }
      throw new Error(logs.join("\n") || err.message);
    }
    throw err;
  }
  await connection.confirmTransaction(
    { signature: txHash, blockhash, lastValidBlockHeight: Number(lastValidBlockHeight) },
    "confirmed"
  );
  return txHash;
}

export function useVaultOperations() {
  const { solanaAccount } = useWallet();
  const [isOperating, setIsOperating] = useState(false);
  const [operationError, setOperationError] = useState<string | null>(null);

  const executeDeposit = async (vaultAddress: string, amount: number): Promise<string | undefined> => {
    if (!solanaAccount) return;
    setIsOperating(true);
    setOperationError(null);
    try {
      const rpc = createSolanaRpc(getSolanaRpcUrl());
      const noopSigner = createNoopSigner(address(solanaAccount.address));
      const vault = new KaminoVault(rpc, address(vaultAddress));
      const depositIxs: DepositIxs = await vault.depositIxs(noopSigner, new Decimal(amount));

      const groups = [
        depositIxs.depositIxs,
        depositIxs.stakeInFarmIfNeededIxs,
        depositIxs.stakeInFlcFarmIfNeededIxs,
      ].filter((g) => g.length > 0);

      if (groups.length === 0) return;

      // Build all transactions in parallel, sign in one MPC round, confirm sequentially.
      const prepared = await Promise.all(groups.map((g) => prepareTransaction(g, noopSigner)));
      const { signedTransactions } = await signAllTransactions(
        { transactions: prepared.map((p) => p.unsigned), walletAccount: solanaAccount },
        dynamicClient
      );

      let lastHash: string | undefined;
      for (let i = 0; i < signedTransactions.length; i++) {
        const { blockhash, lastValidBlockHeight } = prepared[i];
        lastHash = await sendAndConfirm(signedTransactions[i] as VersionedTransaction, blockhash, lastValidBlockHeight);
      }
      return lastHash;
    } catch (err) {
      setOperationError(err instanceof Error ? err.message : "Deposit failed");
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  const executeWithdraw = async (vaultAddress: string, shares: number): Promise<string | undefined> => {
    if (!solanaAccount) return;
    setIsOperating(true);
    setOperationError(null);
    try {
      const rpc = createSolanaRpc(getSolanaRpcUrl());
      const noopSigner = createNoopSigner(address(solanaAccount.address));
      const vault = new KaminoVault(rpc, address(vaultAddress));
      const withdrawIxs: WithdrawIxs = await vault.withdrawIxs(noopSigner, new Decimal(shares));

      const groups = [
        withdrawIxs.unstakeFromFarmIfNeededIxs,
        withdrawIxs.withdrawIxs,
        withdrawIxs.postWithdrawIxs,
      ].filter((g) => g.length > 0);

      if (groups.length === 0) return;

      const prepared = await Promise.all(groups.map((g) => prepareTransaction(g, noopSigner)));
      const { signedTransactions } = await signAllTransactions(
        { transactions: prepared.map((p) => p.unsigned), walletAccount: solanaAccount },
        dynamicClient
      );

      let lastHash: string | undefined;
      for (let i = 0; i < signedTransactions.length; i++) {
        const { blockhash, lastValidBlockHeight } = prepared[i];
        lastHash = await sendAndConfirm(signedTransactions[i] as VersionedTransaction, blockhash, lastValidBlockHeight);
      }
      return lastHash;
    } catch (err) {
      setOperationError(err instanceof Error ? err.message : "Withdraw failed");
      throw err;
    } finally {
      setIsOperating(false);
    }
  };

  return { isOperating, operationError, executeDeposit, executeWithdraw };
}
```

## Wiring it together

The main page loads vaults with `useQuery` and disables SSR on the `VaultsInterface` component to avoid WASM errors:

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

import dynamic from "next/dynamic";

// Dynamically import to avoid WASM-related SSR issues from klend-sdk
const VaultsInterface = dynamic(
  () => import("@/components/VaultsInterface").then((m) => m.VaultsInterface),
  { ssr: false }
);

export default function Main() {
  return <VaultsInterface />;
}
```

Inside `VaultsInterface`, vaults are paginated (6 per page) and metrics are fetched only for the current page. After an action completes, all transactions are already confirmed on-chain. Positions are refetched immediately and vault metrics are invalidated. Multiple retries at increasing intervals handle Kamino's API indexing lag (can be up to \~15s):

```typescript theme={"system"}
const queryClient = useQueryClient();

const refreshAfterAction = () => {
  refetchPositions();
  queryClient.invalidateQueries({ queryKey: ["kamino-vault-metrics"] });
  for (const delay of [3000, 7000, 12000, 20000]) {
    setTimeout(() => refetchPositions(), delay);
  }
};
```

`handleDeposit` and `handleWithdraw` return `Promise<boolean>` so `VaultCard` can clear its input field only on success:

```typescript theme={"system"}
onClick={async () => {
  const n = parseFloat(depositAmount);
  if (!isNaN(n) && n > 0) {
    const ok = await onDeposit(vault.address, n);
    if (ok) setDepositAmount("");
  }
}}
```

## Run the app

<CodeGroup>
  ```bash npm theme={"system"}
  npm run dev
  ```

  ```bash yarn theme={"system"}
  yarn dev
  ```

  ```bash pnpm theme={"system"}
  pnpm dev
  ```

  ```bash bun theme={"system"}
  bun dev
  ```
</CodeGroup>

Add `http://localhost:3000` to your allowed origins in the Dynamic dashboard under **Developer Settings → CORS Origins**.

If you're using SVM Gas Sponsorship, also enable it in **Settings → Embedded Wallets → SVM Gas Sponsorship**.

## Full source code

[GitHub repository →](https://github.com/dynamic-labs-oss/examples/tree/main/examples/nextjs-stablecoin-yield-kamino)

## Additional resources

* [SVM Gas Sponsorship](/docs/javascript/reference/solana/svm-gas-sponsorship)
* [Earn yield with Morpho](/docs/recipes/integrations/yield/morpho)
* [Earn yield with Aave](/docs/recipes/integrations/yield/aave)
* [Dynamic React SDK](/docs/react/reference/quickstart)
