Skip to main content

What We’re Building

A React (Next.js) app that connects Dynamic’s embedded wallets to Polymarket prediction markets, allowing users to:
  • Browse active prediction markets with real-time prices
  • Buy Yes/No outcome shares on markets
  • View and manage portfolio positions
  • Sell positions and redeem winning outcomes
  • Deposit funds via multiple methods (QR, credit card, cross-chain)
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 wallets with seamless auth
  • Polymarket CLOB Client - Trading SDK for placing orders on Polymarket
  • Polygon Network - All trades execute on Polygon 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/evm) for authentication and embedded wallet management, with viem for EVM interactions.
Dashboard: Under Chains & Networks, enable Polygon. 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 Polymarket dependencies:
npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @polymarket/clob-client ethers@^5.8.0 @tanstack/react-query viem lucide-react

Configure Environment

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

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

Initialize Dynamic Client

Create src/lib/dynamic.ts to initialize the Dynamic client with the EVM extension:
src/lib/dynamic.ts
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

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

addEvmExtension();

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 {
  isEvmWalletAccount,
  type EvmWalletAccount,
} from "@dynamic-labs-sdk/evm";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { dynamicClient } from "./dynamic";

interface WalletContextValue {
  evmAccount: EvmWalletAccount | null;
  loggedIn: boolean;
  ensureEvmWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  evmAccount: null,
  loggedIn: false,
  ensureEvmWallet: 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 [evmAccount, setEvmAccount] = useState<EvmWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

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

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

  const ensureEvmWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isEvmWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["EVM"] }, 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 ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
          return;
        }
      } catch {}
      refresh();
    };

    handleOAuthRedirect();

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

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

  return (
    <WalletContext.Provider value={{ evmAccount, loggedIn, ensureEvmWallet, 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 EVM 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 { evmAccount, loggedIn, disconnect, ensureEvmWallet } = 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 ensureEvmWallet();
      setOpen(false);
      setView("menu");
    } catch (err) {
      setError(err instanceof Error ? err.message : "Invalid code");
    } finally {
      setLoading(false);
    }
  }, [otpVerification, otp, ensureEvmWallet]);

  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 getEvmProviders = () =>
    getAvailableWalletProvidersData(dynamicClient).filter((p) => p.chain === "EVM");

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

  if (loggedIn && evmAccount) {
    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">
            {evmAccount.address[0].toUpperCase()}
          </span>
          <span className="hidden sm:block font-mono text-xs text-[#606060]">
            {evmAccount.address.slice(0, 6)}...{evmAccount.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">{evmAccount.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 Polymarket</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>
              {getEvmProviders().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 EVM 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 an EVM wallet</p>
              {getEvmProviders().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 Trading Hook

Build the trading hook that interfaces with the Polymarket CLOB API using a viem wallet client. Create src/lib/hooks/usePolymarketTrading.ts:
src/lib/hooks/usePolymarketTrading.ts
"use client";

import { useState, useCallback, useRef } from "react";
import { useWallet } from "@/lib/providers";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import { polygon } from "viem/chains";
import { ClobClient, Side, OrderType } from "@polymarket/clob-client";
import type { UserMarketOrder } from "@polymarket/clob-client";
import { Contract, providers, BigNumber } from "ethers";
import {
  POLYMARKET_CONTRACTS,
  POLYMARKET_USDC_SPENDERS,
  POLYMARKET_OUTCOME_TOKEN_SPENDERS,
  ERC20_APPROVAL_ABI,
  ERC1155_APPROVAL_ABI,
} from "@/lib/constants/contracts";

const SIGNATURE_TYPE_EOA = 0;
const CLOB_API_URL = "https://clob.polymarket.com";
const POLYGON_CHAIN_ID = 137;
const MAX_ALLOWANCE = BigNumber.from(
  "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
);

export interface UserApiCredentials {
  key: string;
  secret: string;
  passphrase: string;
}

export interface TradeParams {
  marketId: string;
  conditionId: string;
  tokenId: string;
  side: "yes" | "no";
  amount: number;
  isMarketOrder?: boolean;
  negRisk?: boolean;
}

export function usePolymarketTrading() {
  const { evmAccount } = useWallet();
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const credentialsRef = useRef<UserApiCredentials | null>(null);
  const signerRef = useRef<providers.JsonRpcSigner | null>(null);

  const address = evmAccount?.address;

  const getEthersSigner = useCallback(async (): Promise<providers.JsonRpcSigner | null> => {
    if (!evmAccount) return null;

    if (signerRef.current) return signerRef.current;

    try {
      const walletClient = createWalletClientForWalletAccount({
        walletAccount: evmAccount,
        chain: polygon,
      });
      const provider = new providers.Web3Provider(walletClient as any);
      signerRef.current = provider.getSigner();
      return signerRef.current;
    } catch {
      return null;
    }
  }, [evmAccount]);

  const initializeCredentials = useCallback(async (): Promise<UserApiCredentials | null> => {
    const ethersSigner = await getEthersSigner();
    if (!ethersSigner || !address) {
      setError("Wallet not connected");
      return null;
    }

    if (credentialsRef.current) return credentialsRef.current;

    setIsLoading(true);
    setError(null);

    try {
      const tempClient = new ClobClient(CLOB_API_URL, POLYGON_CHAIN_ID, ethersSigner as any);
      let credentials: UserApiCredentials | null = null;

      try {
        const derivedCreds = await tempClient.deriveApiKey();
        if (derivedCreds?.key) credentials = derivedCreds;
      } catch {}

      if (!credentials) {
        const newCreds = await tempClient.createApiKey();
        if (newCreds?.key) credentials = newCreds;
        else throw new Error("Failed to create API credentials");
      }

      credentialsRef.current = credentials;
      return credentials;
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to initialize credentials");
      return null;
    } finally {
      setIsLoading(false);
    }
  }, [getEthersSigner, address]);

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

    setIsLoading(true);
    setError(null);

    const ethersSigner = await getEthersSigner();
    if (!ethersSigner) {
      setIsLoading(false);
      return { success: false, error: "Failed to get wallet signer" };
    }

    const credentials = credentialsRef.current || (await initializeCredentials());
    if (!credentials) {
      setIsLoading(false);
      return { success: false, error: "Failed to initialize trading credentials" };
    }

    try {
      const client = new ClobClient(
        CLOB_API_URL, POLYGON_CHAIN_ID, ethersSigner as any, credentials, SIGNATURE_TYPE_EOA
      );

      const marketOrder: UserMarketOrder = {
        tokenID: params.tokenId,
        amount: params.amount,
        side: Side.BUY,
        feeRateBps: 0,
      };

      const response = await client.createAndPostMarketOrder(
        marketOrder,
        { negRisk: params.negRisk },
        OrderType.FOK
      );

      const orderId = response.orderID || response.orderId || response.id;
      setIsLoading(false);

      if (orderId) return { success: true, orderId };
      if (response.success) return { success: true, orderId: "pending" };
      throw new Error(response.error || "Order submission failed");
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : "Failed to place order";
      setError(errorMessage);
      setIsLoading(false);
      return { success: false, error: errorMessage };
    }
  }, [address, getEthersSigner, initializeCredentials]);

  return { placeOrder, initializeCredentials, isLoading, error };
}
The key difference from the React SDK version is using createWalletClientForWalletAccount from @dynamic-labs-sdk/evm/viem to get a viem wallet client from the EvmWalletAccount, which is then wrapped with ethers.js for compatibility with the Polymarket CLOB client.

Create Markets API Route

Create an API route to fetch markets from Polymarket. Create src/app/api/polymarket/route.ts:
src/app/api/polymarket/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const limit = searchParams.get("limit") || "100";

    const url = new URL("https://gamma-api.polymarket.com/markets");
    url.searchParams.set("limit", limit);
    url.searchParams.set("closed", "false");
    url.searchParams.set("active", "true");

    const response = await fetch(url.toString(), {
      headers: { Accept: "application/json" },
      next: { revalidate: 60 },
    });

    if (!response.ok) throw new Error(`Polymarket API error: ${response.status}`);

    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to fetch markets" },
      { status: 500 }
    );
  }
}

Build the Main Page

Update src/app/page.tsx to display markets using the hooks:
src/app/page.tsx
"use client";

import { useMemo, useState } from "react";
import { Header } from "@/components/Header";
import { MarketCard } from "@/components/MarketCard";
import {
  usePolymarketMarkets,
  type Market,
  calculateTimeRemaining,
} from "@/lib/hooks/usePolymarketMarkets";

export default function Home() {
  const [searchQuery, setSearchQuery] = useState("");
  const { data: markets = [], isLoading, error } = usePolymarketMarkets();

  const filteredMarkets = useMemo(() => {
    const lowerQuery = searchQuery.toLowerCase();
    return markets.filter((m: Market) =>
      !lowerQuery || m.question.toLowerCase().includes(lowerQuery)
    );
  }, [markets, searchQuery]);

  return (
    <>
      <Header searchValue={searchQuery} onSearchChange={setSearchQuery} />

      <div className="pt-[27px] pb-[93px]">
        {isLoading ? (
          <div className="text-center py-20">
            <p className="text-[#606060]">Loading markets...</p>
          </div>
        ) : error ? (
          <div className="text-center py-20">
            <p className="text-[#606060]">Error loading markets. Please try again later.</p>
          </div>
        ) : filteredMarkets.length > 0 ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
            {filteredMarkets.map((market: Market) => (
              <MarketCard
                key={market.id}
                question={market.question}
                timeRemaining={calculateTimeRemaining(market.endDate)}
                yesPrice={market.yesPrice}
                noPrice={market.noPrice}
                conditionId={market.conditionId}
                yesTokenId={market.yesTokenId}
                noTokenId={market.noTokenId}
                marketId={market.id}
                tags={market.tags}
              />
            ))}
          </div>
        ) : (
          <div className="text-center py-20">
            <p className="text-[#606060]">No markets found</p>
          </div>
        )}
      </div>
    </>
  );
}

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 Polymarket through this app:
  1. Authentication: The user signs in via Google, email OTP, or external EVM wallet using the custom DynamicButton
  2. Wallet Provisioning: After auth, an EVM embedded wallet is automatically created via createWaasWalletAccounts
  3. Wallet Client: createWalletClientForWalletAccount from @dynamic-labs-sdk/evm/viem creates a viem-compatible wallet client
  4. Credential Initialization: The app derives or creates Polymarket API credentials by signing a message
  5. Approvals: The app checks and requests necessary ERC20/ERC1155 approvals for USDC and outcome tokens
  6. Order Placement: A market order is submitted to Polymarket’s CLOB API
  7. Confirmation: The user sees 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 embedded wallets can seamlessly connect to prediction markets like Polymarket. The @dynamic-labs-sdk/evm package provides the createWalletClientForWalletAccount helper to bridge embedded wallets with viem, enabling complex trading operations with minimal friction.

Additional Resources