Skip to content

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:

  1. SDK Setup - Install and configure the @kaizen-core/sdk
  2. Wallet Connection - Connect user's wallet via wagmi/RainbowKit
  3. API Wallet - Generate and register an API wallet for trading
  4. Quote Flow - Request quotes from solver, sign, and submit

Quote Flow

Prerequisites

  • React 18+ or Next.js 13+
  • wagmi for wallet connection
  • viem (peer dependency)

Installation

pnpm add @kaizen-core/sdk viem wagmi @tanstack/react-query

Project 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