Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

What We’re Building

A React (Next.js) app that connects Dynamic’s embedded wallets to Kalshi-style prediction markets on Solana, allowing users to:
  • Browse active prediction markets with real-time prices
  • Buy Yes/No outcome shares on markets using SOL
  • View and manage portfolio positions
  • Sell positions and redeem winning outcomes
  • Deposit funds via multiple methods (QR, cross-chain swap)
If you want to take a quick look at the final code, check out the GitHub repository.

Key Components

  • Dynamic JS SDK Embedded Wallets - Non-custodial Solana wallets with seamless auth
  • DFlow - Order execution protocol for Kalshi markets on Solana
  • Solana Network - All trades execute on Solana mainnet

Building the Application

Project setup

Scaffold a Next.js project with create-next-app. This recipe uses the Dynamic JS SDK (@dynamic-labs-sdk/client + @dynamic-labs-sdk/solana) for authentication and embedded Solana wallet management.
Dashboard: Under Chains & Networks, enable Solana. Under Sign-in Methods and Wallets, enable what your flow needs (including Embedded wallets). Under SecurityAllowed Origins, add your origin (for example http://localhost:3000).

Install Dependencies

Add the Dynamic JS SDK and Solana dependencies:
npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/solana @solana/web3.js @solana/spl-token @tanstack/react-query motion lucide-react

Configure Environment

Create a .env.local file with your Dynamic environment ID:
.env.local
# Required: Your Dynamic Labs environment ID
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here

# Optional: Solana RPC URL (defaults to mainnet-beta public endpoint)
NEXT_PUBLIC_SOLANA_RPC_URL=

# Optional: DFlow API key
DFLOW_API_KEY=

# Optional: LiFi API key for higher rate limits on cross-chain swaps
NEXT_PUBLIC_LIFI_API_KEY=

Initialize Dynamic Client

Create src/lib/dynamic.ts to initialize the Dynamic client with the Solana extension:
src/lib/dynamic.ts
import { createDynamicClient } 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: "Kalshi Demo",
  },
});

addSolanaExtension();
The addSolanaExtension() call registers Solana wallet support and must be called immediately after creating the client.

Configure Providers

Set up a wallet context using the JS SDK. Create src/lib/providers.tsx:
src/lib/providers.tsx
"use client";

import {
  createContext,
  useContext,
  useState,
  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 { dynamicClient } from "./dynamic";

interface WalletContextValue {
  solanaAccount: SolanaWalletAccount | null;
  loggedIn: boolean;
  ensureSolanaWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  solanaAccount: null,
  loggedIn: false,
  ensureSolanaWallet: async () => {},
  disconnect: async () => {},
});

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false },
  },
});

export default function Providers({ children }: { children: ReactNode }) {
  const [solanaAccount, setSolanaAccount] = useState<SolanaWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

  const refresh = useCallback(() => {
    const accounts = getWalletAccounts(dynamicClient);
    setSolanaAccount(accounts.find(isSolanaWalletAccount) ?? null);
    setLoggedIn(isSignedIn(dynamicClient));
  }, []);

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

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

  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);
          return;
        }
      } catch {}
      refresh();
    };

    handleOAuthRedirect();

    const unsub1 = onEvent(
      { event: "walletAccountsChanged", listener: () => ensureSolanaWallet() },
      dynamicClient
    );
    const unsub2 = onEvent(
      {
        event: "logout",
        listener: () => { setSolanaAccount(null); setLoggedIn(false); },
      },
      dynamicClient
    );

    return () => { unsub1(); unsub2(); };
  }, [refresh, ensureSolanaWallet]);

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

Build the Auth Button

Create a custom DynamicButton component at src/components/dynamic/DynamicButton.tsx. This handles Google OAuth, email OTP, and external Solana wallet connection:
src/components/dynamic/DynamicButton.tsx
"use client";

import { useState, useCallback, useRef, useEffect } from "react";
import {
  getAvailableWalletProvidersData,
  connectAndVerifyWithWalletProvider,
  sendEmailOTP,
  verifyOTP,
  authenticateWithSocial,
  type OTPVerification,
} from "@dynamic-labs-sdk/client";
import { exportWaasPrivateKey } from "@dynamic-labs-sdk/client/waas";
import { useWallet } from "@/lib/providers";
import { dynamicClient } from "@/lib/dynamic";

export default function DynamicButton() {
  const { solanaAccount, loggedIn, disconnect, ensureSolanaWallet } = useWallet();
  const [open, setOpen] = useState(false);
  const [view, setView] = useState<"menu" | "email" | "otp" | "wallet">("menu");
  const [email, setEmail] = useState("");
  const [otp, setOtp] = useState("");
  const [otpVerification, setOtpVerification] = useState<OTPVerification | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const panelRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!open) return;
    function handler(e: MouseEvent) {
      if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
        setOpen(false);
        setView("menu");
        setError(null);
      }
    }
    document.addEventListener("mousedown", handler);
    return () => document.removeEventListener("mousedown", handler);
  }, [open]);

  const handleSendOTP = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    try {
      const verification = await sendEmailOTP({ email }, dynamicClient);
      setOtpVerification(verification);
      setView("otp");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to send OTP");
    } finally {
      setLoading(false);
    }
  }, [email]);

  const handleVerifyOTP = useCallback(async (e: React.FormEvent) => {
    e.preventDefault();
    if (!otpVerification) return;
    setLoading(true);
    setError(null);
    try {
      await verifyOTP({ otpVerification, verificationToken: otp }, dynamicClient);
      await ensureSolanaWallet();
      setOpen(false);
      setView("menu");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Invalid code");
    } finally {
      setLoading(false);
    }
  }, [otpVerification, otp, ensureSolanaWallet]);

  const handleGoogle = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      await authenticateWithSocial(
        { provider: "google", redirectUrl: window.location.href },
        dynamicClient
      );
    } catch (err) {
      setError(err instanceof Error ? err.message : "Google sign-in failed");
      setLoading(false);
    }
  }, []);

  const getSolanaProviders = () =>
    getAvailableWalletProvidersData(dynamicClient).filter((p) => p.chain === "SOL");

  const handleConnectWallet = useCallback(async (providerKey: string) => {
    setLoading(true);
    setError(null);
    try {
      await connectAndVerifyWithWalletProvider({ walletProviderKey: providerKey }, dynamicClient);
      await ensureSolanaWallet();
      setOpen(false);
      setView("menu");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Connection failed");
    } finally {
      setLoading(false);
    }
  }, [ensureSolanaWallet]);

  if (loggedIn && solanaAccount) {
    return (
      <div className="relative" ref={panelRef}>
        <button
          onClick={() => setOpen((o) => !o)}
          className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[#DADADA] bg-white hover:bg-[#F9F9F9] text-sm text-[#030303]"
        >
          <span className="w-6 h-6 rounded-full bg-[#4779FF] flex items-center justify-center text-white text-xs font-semibold">
            {solanaAccount.address[0].toUpperCase()}
          </span>
          <span className="hidden sm:block font-mono text-xs text-[#606060]">
            {solanaAccount.address.slice(0, 6)}...{solanaAccount.address.slice(-4)}
          </span>
        </button>
        {open && (
          <div className="absolute right-0 top-full mt-2 w-52 bg-white rounded-xl border border-[#DADADA] shadow-lg p-2 z-50">
            <div className="px-3 py-2 mb-1">
              <p className="text-xs text-[#606060]">Connected</p>
              <p className="text-xs font-mono text-[#030303] truncate">{solanaAccount.address}</p>
            </div>
            <hr className="border-[#DADADA] my-1" />
            <button
              onClick={() => { disconnect(); setOpen(false); }}
              className="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg"
            >
              Disconnect
            </button>
          </div>
        )}
      </div>
    );
  }

  return (
    <div className="relative" ref={panelRef}>
      <button
        onClick={() => { setOpen((o) => !o); setView("menu"); setError(null); }}
        className="px-4 py-2 rounded-lg text-sm font-medium bg-[#4779FF] text-white hover:bg-[#3366ee]"
      >
        Sign in
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-2 w-72 bg-white rounded-xl border border-[#DADADA] shadow-lg p-4 z-50">
          {view === "menu" && (
            <div className="space-y-2">
              <p className="text-sm font-medium text-[#030303] mb-3">Sign in to Kalshi</p>
              <button onClick={handleGoogle} disabled={loading}
                className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm border border-[#DADADA] text-[#030303] hover:bg-[#F9F9F9]">
                Continue with Google
              </button>
              <button onClick={() => { setView("email"); setError(null); }}
                className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm border border-[#DADADA] text-[#030303] hover:bg-[#F9F9F9]">
                Continue with Email
              </button>
              {getSolanaProviders().length > 0 && (
                <button onClick={() => { setView("wallet"); setError(null); }}
                  className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm border border-[#DADADA] text-[#030303] hover:bg-[#F9F9F9]">
                  Connect Solana Wallet
                </button>
              )}
            </div>
          )}
          {view === "email" && (
            <form onSubmit={handleSendOTP} className="space-y-3">
              <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
                placeholder="you@example.com" required
                className="w-full px-3 py-2 text-sm border border-[#DADADA] rounded-lg text-[#030303]" />
              {error && <p className="text-xs text-red-500">{error}</p>}
              <button type="submit" disabled={loading}
                className="w-full px-4 py-2.5 rounded-lg text-sm font-medium bg-[#4779FF] text-white">
                {loading ? "Sending…" : "Send code"}
              </button>
            </form>
          )}
          {view === "otp" && (
            <form onSubmit={handleVerifyOTP} className="space-y-3">
              <p className="text-sm text-[#606060]">Sent to {email}</p>
              <input type="text" value={otp} onChange={(e) => setOtp(e.target.value)}
                placeholder="6-digit code" required maxLength={6}
                className="w-full px-3 py-2 text-sm border border-[#DADADA] rounded-lg text-[#030303] text-center tracking-widest" />
              {error && <p className="text-xs text-red-500">{error}</p>}
              <button type="submit" disabled={loading}
                className="w-full px-4 py-2.5 rounded-lg text-sm font-medium bg-[#4779FF] text-white">
                {loading ? "Verifying…" : "Verify"}
              </button>
            </form>
          )}
          {view === "wallet" && (
            <div className="space-y-2">
              <p className="text-sm font-medium text-[#030303] mb-3">Choose a Solana wallet</p>
              {getSolanaProviders().length === 0 ? (
                <p className="text-xs text-[#606060]">No Solana wallets detected. Install Phantom or another Solana wallet.</p>
              ) : (
                getSolanaProviders().map((provider) => (
                  <button key={provider.key} onClick={() => handleConnectWallet(provider.key)}
                    disabled={loading}
                    className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm border border-[#DADADA] text-[#030303] hover:bg-[#F9F9F9]">
                    {provider.metadata.icon && (
                      // eslint-disable-next-line @next/next/no-img-element
                      <img src={provider.metadata.icon} alt={provider.metadata.displayName}
                        width={20} height={20} className="rounded-sm" />
                    )}
                    {provider.metadata.displayName}
                  </button>
                ))
              )}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Create the Trading Hook

Build the Kalshi trading hook that handles Solana transactions. Create src/lib/hooks/useKalshiTrading.ts:
src/lib/hooks/useKalshiTrading.ts
"use client";

import { useState, useCallback } from "react";
import { useWallet } from "@/lib/providers";
import { getActiveNetworkData } from "@dynamic-labs-sdk/client";
import { getSolanaConnection, signTransaction } from "@dynamic-labs-sdk/solana";
import { dynamicClient } from "@/lib/dynamic";
import {
  Connection,
  PublicKey,
  VersionedTransaction,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";

export function useKalshiTrading() {
  const { solanaAccount } = useWallet();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const getConnection = useCallback(async (): Promise<Connection> => {
    if (!solanaAccount) throw new Error("Solana wallet not connected");

    const { networkData } = await getActiveNetworkData(
      { walletAccount: solanaAccount },
      dynamicClient
    );

    if (!networkData) {
      const rpcUrl = process.env.NEXT_PUBLIC_SOLANA_RPC_URL ||
        "https://api.mainnet-beta.solana.com";
      return new Connection(rpcUrl, "confirmed");
    }

    return getSolanaConnection({ networkData });
  }, [solanaAccount]);

  const getSolBalance = useCallback(async (): Promise<number> => {
    const walletAddress = solanaAccount?.address;
    if (!solanaAccount || !walletAddress) return 0;

    try {
      const connection = await getConnection();
      const publicKey = new PublicKey(walletAddress);
      const balance = await connection.getBalance(publicKey);
      return balance / LAMPORTS_PER_SOL;
    } catch {
      return 0;
    }
  }, [solanaAccount, getConnection]);

  const placeOrder = useCallback(async (
    params: TradeParams
  ): Promise<{ success: boolean; txHash?: string; error?: string }> => {
    const walletAddress = solanaAccount?.address;
    if (!walletAddress || !solanaAccount) {
      return { success: false, error: "Wallet not connected" };
    }

    if (!params.tokenMint) {
      return { success: false, error: "Invalid market token" };
    }

    setIsLoading(true);
    setError(null);

    try {
      const connection = await getConnection();

      // Fetch swap transaction from DFlow
      const queryParams = new URLSearchParams({
        endpoint: "order",
        inputMint: WSOL_MINT,
        outputMint: params.tokenMint,
        amount: Math.floor(params.amount * LAMPORTS_PER_SOL).toString(),
        slippageBps: DEFAULT_SLIPPAGE_BPS.toString(),
        userPublicKey: walletAddress,
      });

      const orderResponse = await fetch(`/api/dflow?${queryParams.toString()}`);
      if (!orderResponse.ok) {
        const errorData = await orderResponse.json();
        throw new Error(errorData.error || "DFlow API error");
      }

      const orderData = await orderResponse.json();
      const transactionBuffer = Buffer.from(orderData.transaction, "base64");
      const transaction = VersionedTransaction.deserialize(transactionBuffer);

      // Sign using the JS SDK's signTransaction function
      const { signedTransaction } = await signTransaction(
        { walletAccount: solanaAccount, transaction },
        dynamicClient
      );

      const signature = await connection.sendRawTransaction(
        (signedTransaction as VersionedTransaction).serialize(),
        { skipPreflight: false, preflightCommitment: "confirmed" }
      );

      await connection.confirmTransaction(
        {
          signature,
          blockhash: transaction.message.recentBlockhash,
          lastValidBlockHeight: (await connection.getLatestBlockhash()).lastValidBlockHeight,
        },
        "confirmed"
      );

      setIsLoading(false);
      return { success: true, txHash: signature };
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : "Failed to place order";
      setError(errorMessage);
      setIsLoading(false);
      return { success: false, error: errorMessage };
    }
  }, [solanaAccount, getConnection]);

  return { placeOrder, getSolBalance, isLoading, error };
}
The key pattern for Solana signing in the JS SDK:
  1. Use getActiveNetworkData from @dynamic-labs-sdk/client to get network configuration for the wallet
  2. Use getSolanaConnection from @dynamic-labs-sdk/solana to get a Connection instance
  3. Use signTransaction from @dynamic-labs-sdk/solana to sign transactions with the embedded wallet

Create the Layout

Update src/app/layout.tsx to include the Roboto font and providers:
src/app/layout.tsx
import type { Metadata } from "next";
import { Roboto } from "next/font/google";
import "../styles/globals.css";
import Providers from "@/lib/providers";

const roboto = Roboto({
  subsets: ["latin"],
  weight: ["300", "400", "500", "700"],
  variable: "--font-roboto",
  display: "swap",
});

export const metadata: Metadata = {
  title: "Dynamic: Kalshi Predictions Demo",
  description: "Kalshi Predictions Market Demo by Dynamic",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={roboto.variable} style={{ background: "rgb(249,249,249)" }}>
      <body style={{ background: "rgb(249,249,249)" }}>
        <Providers>
          <div className="flex flex-col min-h-screen">
            <div className="flex-1 flex justify-center">
              <div className="box-border w-full px-[20px] sm:px-[32px] md:px-[48px] lg:px-[64px] xl:px-[80px]" style={{ maxWidth: "1440px" }}>
                {children}
              </div>
            </div>
            <footer className="border-t border-[#DADADA] py-4 text-center text-sm text-[#606060]">
              Powered by{" "}
              <a href="https://dynamic.xyz" target="_blank" rel="noopener noreferrer" className="text-[#4779FF] hover:underline font-medium">
                Dynamic
              </a>
            </footer>
          </div>
        </Providers>
      </body>
    </html>
  );
}

Run the Application

Start the development server:
npm run dev
The application will be available at http://localhost:3000.

How Trading Works

When a user places a trade on Kalshi through this app:
  1. Authentication: The user signs in via Google, email OTP, or external Solana wallet using the custom DynamicButton
  2. Wallet Provisioning: After auth, a Solana embedded wallet is automatically created via createWaasWalletAccounts({ chains: ["SOL"] })
  3. Connection: getActiveNetworkData retrieves the RPC configuration, and getSolanaConnection creates a web3.js Connection
  4. Order Preparation: The app fetches a swap transaction from the DFlow API
  5. Transaction Signing: signTransaction from @dynamic-labs-sdk/solana signs the transaction with the embedded wallet
  6. Broadcast: The signed transaction is sent to the Solana network via the Connection
  7. Confirmation: The app polls for confirmation and shows success/error feedback

Conclusion

If you want to take a look at the full source code, check out the GitHub repository. This integration demonstrates how Dynamic’s JS SDK enables seamless Solana wallet management for prediction markets. The signTransaction function from @dynamic-labs-sdk/solana provides a clean interface for signing transactions with embedded wallets, while getSolanaConnection handles network connectivity.

Additional Resources