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 Security → Allowed 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
Create a .env.local file with your Dynamic environment ID:
# 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:
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.
Set up a wallet context using the JS SDK. Create 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>
);
}
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:
- Use
getActiveNetworkData from @dynamic-labs-sdk/client to get network configuration for the wallet
- Use
getSolanaConnection from @dynamic-labs-sdk/solana to get a Connection instance
- 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:
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:
The application will be available at http://localhost:3000.
How Trading Works
When a user places a trade on Kalshi through this app:
- Authentication: The user signs in via Google, email OTP, or external Solana wallet using the custom
DynamicButton
- Wallet Provisioning: After auth, a Solana embedded wallet is automatically created via
createWaasWalletAccounts({ chains: ["SOL"] })
- Connection:
getActiveNetworkData retrieves the RPC configuration, and getSolanaConnection creates a web3.js Connection
- Order Preparation: The app fetches a swap transaction from the DFlow API
- Transaction Signing:
signTransaction from @dynamic-labs-sdk/solana signs the transaction with the embedded wallet
- Broadcast: The signed transaction is sent to the Solana network via the
Connection
- 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