Frontend Integration Guide
This guide walks you through integrating Kaizen into a React application, from wallet connection to thesis submission.
Overview
Integrating Kaizen into your frontend involves:
- SDK Setup - Install and configure the
@kaizen-core/sdk - Wallet Connection - Connect user's wallet via wagmi/RainbowKit
- API Wallet - Generate and register an API wallet for trading
- Quote Flow - Request quotes from solver, sign, and submit
Prerequisites
- React 18+ or Next.js 13+
- wagmi for wallet connection
- viem (peer dependency)
Installation
pnpm add @kaizen-core/sdk viem wagmi @tanstack/react-queryProject Setup
1. Configure wagmi
// lib/wagmi.ts
import { createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { injected, walletConnect } from "wagmi/connectors";
export const config = createConfig({
chains: [mainnet, sepolia],
connectors: [injected(), walletConnect({ projectId: "YOUR_PROJECT_ID" })],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
},
});2. Create Kaizen Client Provider
// providers/KaizenProvider.tsx
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useMemo,
type ReactNode,
} from "react";
import { createClient, type KaizenClient } from "@kaizen-core/sdk";
import type { Address } from "viem";
import { useAccount } from "wagmi";
interface ApiWallet {
address: Address;
privateKey: `0x${string}`;
expiry: number;
registeredWithCore: boolean;
}
interface KaizenContextValue {
client: KaizenClient | null;
isConnected: boolean;
isWebSocketConnected: boolean;
apiWallet: ApiWallet | null;
mainWalletAddress: Address | null;
setApiWallet: (wallet: ApiWallet | null) => void;
}
const KaizenContext = createContext<KaizenContextValue | null>(null);
const KAIZEN_RPC_URL =
process.env.NEXT_PUBLIC_KAIZEN_RPC_URL || "http://localhost:8546";
const KAIZEN_WS_URL =
process.env.NEXT_PUBLIC_KAIZEN_WS_URL || "ws://localhost:8546/ws";
export function KaizenProvider({ children }: { children: ReactNode }) {
const [client, setClient] = useState<KaizenClient | null>(null);
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
const [apiWallet, setApiWallet] = useState<ApiWallet | null>(null);
const { address: wagmiAddress, isConnected: wagmiConnected } = useAccount();
// Check if fully connected (wallet + API wallet)
const isFullyConnected = Boolean(
wagmiConnected && wagmiAddress && apiWallet && apiWallet.registeredWithCore
);
// Initialize client when API wallet is ready
useEffect(() => {
if (isFullyConnected && !client && apiWallet && wagmiAddress) {
const newClient = createClient({
rpcUrl: KAIZEN_RPC_URL,
wsUrl: KAIZEN_WS_URL,
chainId: 1,
});
// Connect as API wallet acting on behalf of the main wallet (owner)
// This allows the client to track both addresses correctly
newClient.connectAsApiWallet(apiWallet.privateKey, wagmiAddress);
setClient(newClient);
}
if (!isFullyConnected && client) {
client.disconnectWebSocket();
client.disconnectSigner();
setClient(null);
}
}, [isFullyConnected, apiWallet, wagmiAddress, client]);
// Connect WebSocket when client is ready
useEffect(() => {
if (!client || !isFullyConnected) return;
const connectWs = async () => {
try {
await client.connectWebSocket();
setIsWebSocketConnected(true);
} catch (err) {
console.warn("WebSocket connection failed:", err);
}
};
connectWs();
return () => {
client.disconnectWebSocket();
setIsWebSocketConnected(false);
};
}, [client, isFullyConnected]);
const value = useMemo<KaizenContextValue>(
() => ({
client,
isConnected: isFullyConnected,
isWebSocketConnected,
apiWallet,
mainWalletAddress: wagmiAddress ?? null,
setApiWallet,
}),
[client, isFullyConnected, isWebSocketConnected, apiWallet, wagmiAddress]
);
return (
<KaizenContext.Provider value={value}>{children}</KaizenContext.Provider>
);
}
export function useKaizen() {
const context = useContext(KaizenContext);
if (!context) {
throw new Error("useKaizen must be used within KaizenProvider");
}
return context;
}3. App Setup
// app/providers.tsx
"use client";
import { WagmiProvider } from "wagmi";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { config } from "../lib/wagmi";
import { KaizenProvider } from "./KaizenProvider";
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<KaizenProvider>{children}</KaizenProvider>
</QueryClientProvider>
</WagmiProvider>
);
}API Wallet Setup
API wallets are hot wallets that can trade on behalf of the main wallet. They cannot withdraw or transfer funds, providing security while enabling fast trading.
Generate API Wallet
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import type { Address, Hex, WalletClient } from "viem";
import { buildTypedData, buildSignedTransaction } from "@kaizen-core/sdk";
async function setupApiWallet(
mainWalletClient: WalletClient,
ownerAddress: Address
): Promise<ApiWallet> {
// 1. Generate new API wallet keypair
const privateKey = generatePrivateKey();
const account = privateKeyToAccount(privateKey);
const expiry = Date.now() + 24 * 60 * 60 * 1000; // 24 hours
// 2. Build EIP-712 typed data for the OWNER to sign
const timestamp = BigInt(Date.now());
const payload = {
type: "NominateApiWallet" as const,
wallet: account.address,
expiry,
};
const typedData = buildTypedData(payload, timestamp, ownerAddress);
// 3. Owner signs the nomination (via wagmi/viem)
const signature = await mainWalletClient.signTypedData({
domain: typedData.domain,
types: typedData.types,
primaryType: typedData.primaryType,
message: typedData.message,
});
// 4. Build signed transaction from owner's signature
const signedTx = buildSignedTransaction(
payload,
timestamp,
ownerAddress,
signature as Hex
);
// 5. Submit to Kaizen Core via RPC
const response = await fetch(KAIZEN_RPC_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
method: "kaizen_sendTransaction",
params: [signedTx.encoded],
id: 1,
}),
});
const result = await response.json();
if (result.error) {
throw new Error(result.error.message);
}
return {
address: account.address,
privateKey,
expiry,
registeredWithCore: true,
};
}Persist API Wallet
Store the API wallet securely in localStorage (encrypted in production):
const STORAGE_KEY = "kaizen_api_wallet";
export function saveApiWallet(wallet: ApiWallet) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(wallet));
}
export function loadApiWallet(): ApiWallet | null {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return null;
const wallet = JSON.parse(stored);
// Check expiry
if (wallet.expiry && wallet.expiry < Date.now()) {
localStorage.removeItem(STORAGE_KEY);
return null;
}
return wallet;
}Quote Flow
The complete flow from requesting a quote to executing a thesis:
Using SDK's High-Level API (Recommended)
The SDK provides client.requestQuote() and client.submitRfq() for a streamlined flow:
import {
type KaizenClient,
type SolverQuote,
parsePrice,
parseUSDC,
createThesisTiming,
} from "@kaizen-core/sdk";
const SOLVER_URL =
process.env.NEXT_PUBLIC_SOLVER_URL || "http://localhost:4000";
// Oracle pair addresses
const ORACLE_PAIRS = {
"BTC/USDT": {
base: "0x0000000000000000000000000000000000000001" as const,
quote: "0x0000000000000000000000000000000000000002" as const,
},
"ETH/USDT": {
base: "0x0000000000000000000000000000000000000003" as const,
quote: "0x0000000000000000000000000000000000000002" as const,
},
};
interface QuoteParams {
lowerPrice: number;
upperPrice: number;
betAmount: number;
durationSeconds: number;
}
// Request quote using SDK's built-in method
async function getQuote(
client: KaizenClient,
params: QuoteParams
): Promise<SolverQuote> {
const timing = createThesisTiming(5, params.durationSeconds, 30);
// client.requestQuote() handles bigint conversion automatically
return client.requestQuote(SOLVER_URL, {
user: client.accountAddress!, // Uses owner address if API wallet
thesisType: "box",
base: ORACLE_PAIRS["BTC/USDT"].base,
quote: ORACLE_PAIRS["BTC/USDT"].quote,
lowerPrice: parsePrice(params.lowerPrice.toString()),
upperPrice: parsePrice(params.upperPrice.toString()),
betAmount: parseUSDC(params.betAmount.toString()),
...timing,
});
}
// Submit thesis using SDK's high-level method
async function submitThesis(
client: KaizenClient,
quote: SolverQuote
): Promise<{ txHash: string }> {
// client.submitRfq() handles quote signing + transaction submission
const receipt = await client.submitRfq(quote, {
waitForConfirmation: true,
});
return { txHash: receipt.hash };
}Complete Example Component
// components/CreateThesis.tsx
"use client";
import { useState } from "react";
import { useKaizen } from "../providers/KaizenProvider";
import { formatUSDC, type SolverQuote } from "@kaizen-core/sdk";
export function CreateThesis() {
const { client, isConnected } = useKaizen();
const [isLoading, setIsLoading] = useState(false);
const [quote, setQuote] = useState<SolverQuote | null>(null);
const [error, setError] = useState<string | null>(null);
// Form state
const [lowerPrice, setLowerPrice] = useState("94000");
const [upperPrice, setUpperPrice] = useState("96000");
const [betAmount, setBetAmount] = useState("100");
const [duration, setDuration] = useState(60);
const handleRequestQuote = async () => {
if (!client) return;
setIsLoading(true);
setError(null);
try {
// Uses SDK's built-in requestQuote method
const quote = await getQuote(client, {
lowerPrice: Number(lowerPrice),
upperPrice: Number(upperPrice),
betAmount: Number(betAmount),
durationSeconds: duration,
});
setQuote(quote);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to get quote");
} finally {
setIsLoading(false);
}
};
const handleExecute = async () => {
if (!client || !quote) return;
setIsLoading(true);
setError(null);
try {
// Uses SDK's submitRfq - handles signing + submission automatically
const result = await submitThesis(client, quote);
console.log(`Thesis submitted: ${result.txHash}`);
setQuote(null);
} catch (err) {
setError(err instanceof Error ? err.message : "Execution failed");
} finally {
setIsLoading(false);
}
};
if (!isConnected) {
return <div>Please connect your wallet</div>;
}
return (
<div className="space-y-4">
{error && <div className="text-red-500">{error}</div>}
<div className="grid grid-cols-2 gap-4">
<input
type="number"
value={lowerPrice}
onChange={(e) => setLowerPrice(e.target.value)}
placeholder="Lower Price"
/>
<input
type="number"
value={upperPrice}
onChange={(e) => setUpperPrice(e.target.value)}
placeholder="Upper Price"
/>
<input
type="number"
value={betAmount}
onChange={(e) => setBetAmount(e.target.value)}
placeholder="Bet Amount (USDC)"
/>
<select
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
>
<option value={30}>30 seconds</option>
<option value={60}>1 minute</option>
<option value={300}>5 minutes</option>
</select>
</div>
{!quote ? (
<button onClick={handleRequestQuote} disabled={isLoading}>
{isLoading ? "Loading..." : "Request Quote"}
</button>
) : (
<div className="space-y-2">
<div>Payout: {formatUSDC(quote.payout)} USDC</div>
<button onClick={handleExecute} disabled={isLoading}>
{isLoading ? "Executing..." : "Execute"}
</button>
<button onClick={() => setQuote(null)}>Cancel</button>
</div>
)}
</div>
);
}Real-time Updates
Subscribe to thesis updates and price changes:
// hooks/useThesisSubscription.ts
import { useEffect, useState } from "react";
import { useKaizen } from "../providers/KaizenProvider";
import type { Thesis } from "@kaizen-core/sdk";
export function useMyTheses() {
const { client, isWebSocketConnected } = useKaizen();
const [theses, setTheses] = useState<Map<bigint, Thesis>>(new Map());
useEffect(() => {
if (!client || !isWebSocketConnected) return;
const unsubscribe = client.subscribeMyTheses((event) => {
if (event.type === "thesis") {
setTheses((prev) => {
const next = new Map(prev);
next.set(event.thesis.id, event.thesis);
return next;
});
}
});
return () => unsubscribe();
}, [client, isWebSocketConnected]);
return Array.from(theses.values());
}Price Stream
// hooks/usePriceStream.ts
import { useEffect, useState } from "react";
import { useKaizen } from "../providers/KaizenProvider";
import type { Address } from "@kaizen-core/sdk";
interface PriceData {
price: bigint;
timestamp: number;
}
export function usePriceStream(base: Address, quote: Address) {
const { client, isWebSocketConnected } = useKaizen();
const [price, setPrice] = useState<PriceData | null>(null);
useEffect(() => {
if (!client || !isWebSocketConnected) return;
const unsubscribe = client.subscribeOraclePrices(
{ base, quote },
(event) => {
if (event.type === "oraclePrice") {
setPrice({
price: event.price.price,
timestamp: event.price.timestamp,
});
}
}
);
return () => unsubscribe();
}, [client, isWebSocketConnected, base, quote]);
return price;
}Account & Balance
Query Balance
// hooks/useBalance.ts
import { useQuery } from "@tanstack/react-query";
import { useKaizen } from "../providers/KaizenProvider";
import { formatUSDC } from "@kaizen-core/sdk";
export function useBalance() {
const { client, isConnected } = useKaizen();
return useQuery({
queryKey: ["balance", client?.accountAddress],
queryFn: async () => {
if (!client) return null;
// getMyBalance() uses accountAddress (owner if API wallet)
const balance = await client.getMyBalance();
return {
raw: balance.balance,
formatted: formatUSDC(balance.balance),
};
},
enabled: isConnected && !!client?.accountAddress,
refetchInterval: 5000, // Refresh every 5 seconds
});
}Error Handling
import { RpcError } from "@kaizen-core/sdk";
async function safeExecute<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof RpcError) {
// Handle specific RPC errors
switch (error.code) {
case -32001: // InsufficientBalance
throw new Error("Insufficient balance for this transaction");
case -32002: // ThesisNotFound
throw new Error("Thesis not found");
case -32003: // InvalidSignature
throw new Error("Invalid signature");
default:
throw new Error(`RPC Error: ${error.message}`);
}
}
throw error;
}
}Best Practices
1. API Wallet Security
- Store API wallet private keys encrypted in localStorage
- Set reasonable expiry times (24 hours recommended)
- Clear API wallet on logout
2. Quote Freshness
- Quotes have deadlines - show countdown to users
- Auto-refresh quotes if they're about to expire
- Handle expired quote errors gracefully
3. WebSocket Reliability
// The SDK handles reconnection automatically, but you can track status
function ConnectionStatus() {
const { isWebSocketConnected } = useKaizen();
return (
<div
className={isWebSocketConnected ? "text-green-500" : "text-yellow-500"}
>
{isWebSocketConnected ? "Connected" : "Connecting..."}
</div>
);
}4. Transaction Confirmation
Always wait for transaction confirmation for critical operations:
// Using high-level APIs with confirmation
const receipt = await client.transfer(recipientAddress, amount, {
waitForConfirmation: true,
confirmationTimeout: 30000,
});
// For RFQ submission
const receipt = await client.submitRfq(quote, {
waitForConfirmation: true,
});
if (receipt.status !== "confirmed") {
throw new Error(`Transaction failed: ${receipt.status}`);
}5. Mock Mode for Development
Implement a mock mode for development without a running backend:
const useMockMode = process.env.NEXT_PUBLIC_MOCK_MODE === "true";
async function requestQuote(params: QuoteParams) {
if (useMockMode) {
return generateMockQuote(params);
}
return requestQuoteFromSolver(params);
}Next Steps
- SDK Client Reference - Full client API documentation
- WebSocket Reference - Real-time subscription details
- Transaction Types - All available transaction builders
- EIP-712 Signatures - Signature implementation details
