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 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

Start by creating a new Dynamic project with React, Viem, Wagmi, and Ethereum support:
npx create-dynamic-app@latest polymarket-app --framework nextjs --library viem --wagmi true --chains ethereum --pm npm
cd polymarket-app

Install Dependencies

Add the Polymarket CLOB client and other required dependencies:
npm install @polymarket/clob-client ethers@^5.8.0 @tanstack/react-query motion lucide-react
The @polymarket/clob-client provides the SDK for interacting with Polymarket’s Central Limit Order Book (CLOB) for placing and managing orders.

Configure Environment

Create a .env.local file with your Dynamic environment ID:
.env.local
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
You can find your Environment ID in the Dynamic dashboard under Developer Settings → SDK & API Keys.

Configure Wagmi for Polygon

Since Polymarket operates on Polygon, configure Wagmi to use Polygon mainnet. Create src/lib/wagmi.ts:
src/lib/wagmi.ts
import { createConfig, http } from "wagmi";
import { polygon } from "wagmi/chains";

export const config = createConfig({
  chains: [polygon],
  connectors: [],
  transports: {
    [polygon.id]: http(),
  },
  multiInjectedProviderDiscovery: false,
});

declare module "wagmi" {
  interface Register {
    config: typeof config;
  }
}

Configure Providers

Set up the providers with Dynamic, Wagmi, and React Query. Create src/lib/providers.tsx:
src/lib/providers.tsx
"use client";

import { useState, useEffect, useRef } from "react";
import { config } from "@/lib/wagmi";
import {
  DynamicContextProvider,
  DynamicUserProfile,
  EthereumWalletConnectors,
  useDynamicContext,
  useSwitchNetwork,
  isEthereumWallet,
} from "@dynamic-labs/sdk-react-core";
import { DynamicWagmiConnector } from "@dynamic-labs/wagmi-connector";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { WagmiProvider } from "wagmi";

const POLYGON_CHAIN_ID = 137;

function NetworkSwitcher() {
  const { primaryWallet } = useDynamicContext();
  const switchNetwork = useSwitchNetwork();
  const hasSwitchedRef = useRef(false);

  useEffect(() => {
    if (!primaryWallet) {
      hasSwitchedRef.current = false;
      return;
    }

    if (!isEthereumWallet(primaryWallet)) {
      return;
    }

    const switchToPolygon = async () => {
      if (hasSwitchedRef.current) return;

      try {
        const walletClient = await primaryWallet.getWalletClient();
        const currentChainId = await walletClient.getChainId();

        if (currentChainId !== POLYGON_CHAIN_ID) {
          await switchNetwork({
            wallet: primaryWallet,
            network: POLYGON_CHAIN_ID,
          });
        }
        hasSwitchedRef.current = true;
      } catch {
        // Network switch failed
      }
    };

    const timeoutId = setTimeout(switchToPolygon, 500);
    return () => clearTimeout(timeoutId);
  }, [primaryWallet, switchNetwork]);

  return null;
}

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <DynamicContextProvider
      theme="dark"
      settings={{
        environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
        walletConnectors: [EthereumWalletConnectors],
      }}
    >
      <WagmiProvider config={config}>
        <QueryClientProvider client={queryClient}>
          <DynamicWagmiConnector>
            <NetworkSwitcher />
            {children}
            <DynamicUserProfile />
          </DynamicWagmiConnector>
        </QueryClientProvider>
      </WagmiProvider>
    </DynamicContextProvider>
  );
}
The NetworkSwitcher component automatically switches the user’s wallet to Polygon when they connect, ensuring they’re on the correct network for Polymarket.

Define Contract Constants

Create src/lib/constants/contracts.ts for Polymarket contract addresses:
src/lib/constants/contracts.ts
export const POLYMARKET_CONTRACTS = {
  USDC_E: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" as const,
  CTF: "0x4d97dcd97ec945f40cf65f87097ace5ea0476045" as const,
  CTF_EXCHANGE: "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" as const,
  NEG_RISK_CTF_EXCHANGE: "0xC5d563A36AE78145C45a50134d48A1215220f80a" as const,
  NEG_RISK_ADAPTER: "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296" as const,
};

export const POLYMARKET_USDC_SPENDERS = [
  { address: POLYMARKET_CONTRACTS.CTF_EXCHANGE, name: "Main Exchange" },
  { address: POLYMARKET_CONTRACTS.NEG_RISK_CTF_EXCHANGE, name: "Neg Risk Markets" },
  { address: POLYMARKET_CONTRACTS.NEG_RISK_ADAPTER, name: "Neg Risk Adapter" },
] as const;

export const POLYMARKET_OUTCOME_TOKEN_SPENDERS = [
  { address: POLYMARKET_CONTRACTS.CTF_EXCHANGE, name: "Main Exchange" },
  { address: POLYMARKET_CONTRACTS.NEG_RISK_CTF_EXCHANGE, name: "Neg Risk Markets" },
  { address: POLYMARKET_CONTRACTS.NEG_RISK_ADAPTER, name: "Neg Risk Adapter" },
] as const;

export const ERC20_APPROVAL_ABI = [
  {
    inputs: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
    ],
    name: "allowance",
    outputs: [{ name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      { name: "spender", type: "address" },
      { name: "amount", type: "uint256" },
    ],
    name: "approve",
    outputs: [{ name: "", type: "bool" }],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [{ name: "account", type: "address" }],
    name: "balanceOf",
    outputs: [{ name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
] as const;

export const ERC1155_APPROVAL_ABI = [
  {
    inputs: [
      { name: "operator", type: "address" },
      { name: "approved", type: "bool" },
    ],
    name: "setApprovalForAll",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      { name: "account", type: "address" },
      { name: "operator", type: "address" },
    ],
    name: "isApprovedForAll",
    outputs: [{ name: "", type: "bool" }],
    stateMutability: "view",
    type: "function",
  },
] as const;

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";

interface PolymarketMarket {
  id: string;
  question: string;
  conditionId: string;
  slug: string;
  endDate: string;
  category: string;
  image?: string;
  icon?: string;
  outcomes?: string;
  outcomePrices?: string;
  volume?: string;
  volumeNum?: number;
  active?: boolean;
  closed?: boolean;
  clobTokenIds?: string;
}

interface TransformedMarket {
  id: string;
  question: string;
  endDate: string;
  yesPrice: string;
  noPrice: string;
  category: string;
  imageUrl: string;
  yesTraders: number;
  noTraders: number;
  conditionId: string;
  yesTokenId?: string;
  noTokenId?: string;
  tags: string[];
  volume: number;
}

function transformMarket(market: PolymarketMarket): TransformedMarket | null {
  try {
    let outcomes: string[] = ["Yes", "No"];
    let prices: string[] = ["0", "0"];

    if (market.outcomes) {
      try {
        outcomes = JSON.parse(market.outcomes) as string[];
      } catch {
        // Use defaults
      }
    }

    if (market.outcomePrices) {
      try {
        prices = JSON.parse(market.outcomePrices) as string[];
      } catch {
        // Use defaults
      }
    }

    // Find Yes and No indices
    const lowerOutcomes = outcomes.map((o) => o.toLowerCase());
    const yesIndex = lowerOutcomes.findIndex(
      (o) => o.includes("yes") || o === "true"
    );
    const noIndex = lowerOutcomes.findIndex(
      (o) => o.includes("no") || o === "false"
    );

    const finalYesIndex = yesIndex >= 0 ? yesIndex : 0;
    const finalNoIndex = noIndex >= 0 ? noIndex : 1;

    // Parse and normalize prices to 0-100 range
    let yesPriceNum = parseFloat(prices[finalYesIndex] || "0");
    let noPriceNum = parseFloat(prices[finalNoIndex] || "0");

    const priceSum = yesPriceNum + noPriceNum;
    if (priceSum <= 1.5 && priceSum > 0) {
      yesPriceNum *= 100;
      noPriceNum *= 100;
    }

    // Normalize to sum to 100
    const total = yesPriceNum + noPriceNum;
    if (total > 0) {
      const factor = 100 / total;
      yesPriceNum *= factor;
      noPriceNum *= factor;
    } else {
      yesPriceNum = 50;
      noPriceNum = 50;
    }

    const yesPrice = Math.max(0, Math.min(100, yesPriceNum)).toFixed(1);
    const noPrice = Math.max(0, Math.min(100, noPriceNum)).toFixed(1);

    // Parse token IDs
    let yesTokenId: string | undefined;
    let noTokenId: string | undefined;
    if (market.clobTokenIds) {
      try {
        const tokenIds = JSON.parse(market.clobTokenIds) as string[];
        if (tokenIds.length >= 2) {
          yesTokenId = tokenIds[finalYesIndex] || tokenIds[0];
          noTokenId = tokenIds[finalNoIndex] || tokenIds[1];
        }
      } catch {
        // Ignore parse errors
      }
    }

    const volume = market.volumeNum ?? (market.volume ? parseFloat(market.volume) : 0);
    const yesTraders = Math.floor(volume * 0.4);
    const noTraders = Math.floor(volume * 0.3);

    return {
      id: market.id,
      question: market.question,
      endDate: market.endDate,
      yesPrice,
      noPrice,
      category: market.category || "All",
      imageUrl: market.image || market.icon || "",
      yesTraders,
      noTraders,
      conditionId: market.conditionId,
      yesTokenId,
      noTokenId,
      tags: [],
      volume,
    };
  } catch {
    return null;
  }
}

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(), {
      method: "GET",
      headers: { Accept: "application/json" },
      next: { revalidate: 60 },
    });

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

    const data = await response.json();
    const markets: PolymarketMarket[] = Array.isArray(data) ? data : [];

    const transformedMarkets = markets
      .map((market) => transformMarket(market))
      .filter((market): market is TransformedMarket => market !== null);

    return NextResponse.json(transformedMarkets);
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : "Unknown error";
    return NextResponse.json(
      { error: "Failed to fetch markets", details: errorMessage },
      { status: 500 }
    );
  }
}

Create Markets Hook

Create a hook to fetch and cache market data. Create src/lib/hooks/usePolymarketMarkets.ts:
src/lib/hooks/usePolymarketMarkets.ts
import { useQuery } from "@tanstack/react-query";

export interface Market {
  id: string;
  question: string;
  endDate: string;
  yesPrice: string;
  noPrice: string;
  category: string;
  imageUrl: string;
  yesTraders: number;
  noTraders: number;
  conditionId: string;
  yesTokenId?: string;
  noTokenId?: string;
  tags: string[];
  volume: number;
}

export function calculateTimeRemaining(endDate: string): string {
  try {
    const end = new Date(endDate).getTime();
    const now = Date.now();
    const diff = end - now;

    if (diff <= 0) return "Closed";

    const days = Math.floor(diff / 86400000);
    const hours = Math.floor((diff % 86400000) / 3600000);
    const minutes = Math.floor((diff % 3600000) / 60000);

    if (days > 0) return `${days}d ${hours}h ${minutes}m`;
    if (hours > 0) return `${hours}h ${minutes}m`;
    return `${minutes}m`;
  } catch {
    return "Unknown";
  }
}

async function fetchMarkets(): Promise<Market[]> {
  const response = await fetch("/api/polymarket?limit=100&active=true");
  if (!response.ok) {
    throw new Error("Failed to fetch markets");
  }
  return response.json();
}

export function usePolymarketMarkets() {
  return useQuery({
    queryKey: ["polymarket-markets"],
    queryFn: fetchMarkets,
    staleTime: 60000,
    refetchInterval: 60000,
  });
}

Create Trading Hook

Now let’s build the trading hook that handles placing orders. Create src/lib/hooks/usePolymarketTrading.ts:
src/lib/hooks/usePolymarketTrading.ts
"use client";

import { useState, useCallback, useRef } from "react";
import { useDynamicContext, isEthereumWallet } from "@dynamic-labs/sdk-react-core";
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"
);
const MIN_ALLOWANCE_THRESHOLD = BigNumber.from("1000000000000");

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 { primaryWallet } = useDynamicContext();

  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 = primaryWallet?.address;

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

    if (signerRef.current) {
      return signerRef.current;
    }

    try {
      const walletClient = await primaryWallet.getWalletClient();
      const provider = new providers.Web3Provider(walletClient);
      signerRef.current = provider.getSigner();
      return signerRef.current;
    } catch (err) {
      console.error("Failed to create ethers signer:", err);
      return null;
    }
  }, [primaryWallet]);

  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 && derivedCreds?.secret && derivedCreds?.passphrase) {
          credentials = derivedCreds;
        }
      } catch {
        // Will create new credentials below
      }

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

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

  const ensureApprovals = useCallback(async (
    signer: providers.JsonRpcSigner,
    requiredAmount: number
  ): Promise<{ approved: boolean; error?: string }> => {
    try {
      const signerAddress = await signer.getAddress();
      const usdcContract = new Contract(
        POLYMARKET_CONTRACTS.USDC_E,
        ERC20_APPROVAL_ABI,
        signer
      );

      // Check balance
      const balance: BigNumber = await usdcContract.balanceOf(signerAddress);
      const requiredBN = BigNumber.from(Math.ceil(requiredAmount * 1e6));

      if (balance.lt(requiredBN)) {
        const balanceFormatted = (Number(balance.toString()) / 1e6).toFixed(2);
        return {
          approved: false,
          error: `Insufficient USDC.e balance. You have ${balanceFormatted} USDC but need ${requiredAmount} USDC.`,
        };
      }

      // Approve USDC spenders
      for (const { address } of POLYMARKET_USDC_SPENDERS) {
        const currentAllowance: BigNumber = await usdcContract.allowance(signerAddress, address);
        if (currentAllowance.lt(MIN_ALLOWANCE_THRESHOLD)) {
          const approveTx = await usdcContract.approve(address, MAX_ALLOWANCE);
          await approveTx.wait();
        }
      }

      // Approve outcome token spenders
      const ctfContract = new Contract(
        POLYMARKET_CONTRACTS.CTF,
        ERC1155_APPROVAL_ABI,
        signer
      );

      for (const { address } of POLYMARKET_OUTCOME_TOKEN_SPENDERS) {
        const isApproved: boolean = await ctfContract.isApprovedForAll(signerAddress, address);
        if (!isApproved) {
          const approveTx = await ctfContract.setApprovalForAll(address, true);
          await approveTx.wait();
        }
      }

      return { approved: true };
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : "Failed to approve";
      return { approved: false, error: errorMessage };
    }
  }, []);

  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" };
    }

    // Ensure approvals
    const approvalResult = await ensureApprovals(ethersSigner, params.amount);
    if (!approvalResult.approved) {
      setIsLoading(false);
      return { success: false, error: approvalResult.error };
    }

    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 };
      } else if (response.success || response.status === "success") {
        return { success: true, orderId: "pending" };
      } else {
        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, ensureApprovals]);

  return {
    placeOrder,
    initializeCredentials,
    isLoading,
    error,
  };
}
This hook handles:
  • Creating an ethers signer from Dynamic’s wallet client
  • Initializing Polymarket API credentials (required for trading)
  • Checking USDC balance and approving spender contracts
  • Placing market orders via the CLOB client

Create User Positions Hook

Create a hook to fetch user positions. Create src/lib/hooks/useUserPositions.ts:
src/lib/hooks/useUserPositions.ts
import { useQuery } from "@tanstack/react-query";

export interface PolymarketPosition {
  proxyWallet: string;
  asset: string;
  conditionId: string;
  size: number;
  avgPrice: number;
  initialValue: number;
  currentValue: number;
  cashPnl: number;
  percentPnl: number;
  curPrice: number;
  redeemable: boolean;
  title: string;
  outcome: string;
  outcomeIndex: number;
  negativeRisk: boolean;
}

async function fetchPositions(walletAddress: string): Promise<PolymarketPosition[]> {
  const params = new URLSearchParams({ user: walletAddress });
  const response = await fetch(`/api/polymarket/positions?${params}`);

  if (!response.ok) {
    throw new Error("Failed to fetch positions");
  }

  return response.json();
}

export function useUserPositions(walletAddress: string | undefined) {
  return useQuery({
    queryKey: ["polymarket-positions", walletAddress],
    queryFn: () => fetchPositions(walletAddress!),
    enabled: !!walletAddress,
    staleTime: 5000,
    refetchInterval: 10000,
  });
}

Build the Market Card Component

Create src/components/MarketCard.tsx for displaying individual markets:
src/components/MarketCard.tsx
import { useState, useCallback, useRef } from "react";
import { usePolymarketTrading } from "@/lib/hooks/usePolymarketTrading";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { polygon } from "wagmi/chains";

interface MarketCardProps {
  question: string;
  timeRemaining: string;
  yesPrice: string;
  noPrice: string;
  conditionId?: string;
  yesTokenId?: string;
  noTokenId?: string;
  marketId?: string;
}

export function MarketCard({
  question,
  timeRemaining,
  yesPrice,
  noPrice,
  conditionId,
  yesTokenId,
  noTokenId,
  marketId,
}: MarketCardProps) {
  const [selectedOption, setSelectedOption] = useState<"yes" | "no" | null>(null);
  const [betAmount, setBetAmount] = useState<number>(0);
  const [isExpanded, setIsExpanded] = useState(false);
  const [tradingError, setTradingError] = useState<string | null>(null);
  const [tradingSuccess, setTradingSuccess] = useState(false);
  const [isProcessing, setIsProcessing] = useState(false);

  const { primaryWallet, setShowAuthFlow } = useDynamicContext();
  const { placeOrder } = usePolymarketTrading();

  const handleOptionClick = (option: "yes" | "no") => {
    if (!primaryWallet) {
      setShowAuthFlow(true);
      return;
    }

    setSelectedOption(option);
    setIsExpanded(true);
    setBetAmount(0);
    setTradingError(null);
    setTradingSuccess(false);
  };

  const handleBuy = useCallback(async () => {
    if (betAmount === 0 || !selectedOption || !primaryWallet) return;

    if (!conditionId || (!yesTokenId && !noTokenId)) {
      setTradingError("Market data incomplete");
      return;
    }

    setIsProcessing(true);
    setTradingError(null);

    try {
      // Ensure we're on Polygon
      const currentNetwork = await primaryWallet.getNetwork();
      if (currentNetwork !== polygon.id) {
        if (primaryWallet.connector.supportsNetworkSwitching()) {
          await primaryWallet.switchNetwork(polygon.id);
          await new Promise((resolve) => setTimeout(resolve, 2000));
        } else {
          setTradingError("Please switch to Polygon network");
          setIsProcessing(false);
          return;
        }
      }

      const tokenId = selectedOption === "yes" ? yesTokenId : noTokenId;
      if (!tokenId) {
        setTradingError("Token ID not available");
        setIsProcessing(false);
        return;
      }

      const result = await placeOrder({
        marketId: marketId || "",
        conditionId,
        tokenId,
        side: selectedOption,
        amount: betAmount,
        isMarketOrder: true,
      });

      if (result.success) {
        setTradingSuccess(true);
        setTimeout(() => {
          setIsExpanded(false);
          setSelectedOption(null);
        }, 2000);
      } else {
        setTradingError(result.error || "Failed to place order");
      }
    } catch (error) {
      setTradingError(error instanceof Error ? error.message : "An error occurred");
    } finally {
      setIsProcessing(false);
    }
  }, [betAmount, selectedOption, primaryWallet, conditionId, yesTokenId, noTokenId, marketId, placeOrder]);

  const potentialWin = betAmount > 0
    ? selectedOption === "yes"
      ? (betAmount / parseFloat(yesPrice)) * 100
      : (betAmount / parseFloat(noPrice)) * 100
    : 0;

  return (
    <div className="bg-[#141620] rounded-[22px] p-4 w-full">
      {/* Header */}
      <div className="mb-4">
        <p className="font-semibold text-white text-lg">{question}</p>
        <p className="text-gray-500 text-sm">{timeRemaining}</p>
      </div>

      {/* Yes/No Buttons */}
      <div className="flex gap-2">
        <button
          onClick={() => handleOptionClick("yes")}
          className={`flex-1 py-3 rounded-lg font-semibold transition-colors ${
            selectedOption === "yes"
              ? "bg-green-500 text-white"
              : "bg-green-500/10 text-green-500 hover:bg-green-500/20"
          }`}
        >
          Yes {yesPrice
        </button>
        <button
          onClick={() => handleOptionClick("no")}
          className={`flex-1 py-3 rounded-lg font-semibold transition-colors ${
            selectedOption === "no"
              ? "bg-red-500 text-white"
              : "bg-red-500/10 text-red-500 hover:bg-red-500/20"
          }`}
        >
          No {noPrice
        </button>
      </div>

      {/* Expanded Trading Section */}
      {isExpanded && (
        <div className="mt-4 space-y-3">
          {/* Amount Input */}
          <div className="bg-[#191b25] rounded-lg p-3">
            <input
              type="number"
              value={betAmount || ""}
              onChange={(e) => setBetAmount(parseFloat(e.target.value) || 0)}
              placeholder="Enter amount (USDC)"
              className="w-full bg-transparent text-white outline-none"
            />
          </div>

          {/* Quick Add Buttons */}
          <div className="flex gap-2">
            {[5, 10, 25].map((amount) => (
              <button
                key={amount}
                onClick={() => setBetAmount(betAmount + amount)}
                className="px-4 py-2 bg-gray-700/50 rounded-lg text-gray-400 hover:bg-gray-700"
              >
                +${amount}
              </button>
            ))}
          </div>

          {/* Error/Success Messages */}
          {tradingError && (
            <div className="bg-red-500/10 border border-red-500/30 rounded-lg p-3">
              <p className="text-red-500 text-sm">{tradingError}</p>
            </div>
          )}
          {tradingSuccess && (
            <div className="bg-green-500/10 border border-green-500/30 rounded-lg p-3">
              <p className="text-green-500 text-sm">Order placed successfully!</p>
            </div>
          )}

          {/* Buy Button */}
          <button
            onClick={handleBuy}
            disabled={betAmount === 0 || isProcessing}
            className={`w-full py-3 rounded-lg font-semibold transition-colors ${
              betAmount > 0 && !isProcessing
                ? "bg-blue-500 text-white hover:bg-blue-600"
                : "bg-gray-700 text-gray-500 cursor-not-allowed"
            }`}
          >
            {isProcessing ? "Processing..." : `Buy ${selectedOption === "yes" ? "Yes" : "No"}`}
            {betAmount > 0 && !isProcessing && (
              <span className="block text-sm opacity-70">
                To win ${potentialWin.toFixed(2)}
              </span>
            )}
          </button>
        </div>
      )}
    </div>
  );
}

Build the Main Page

Create the main page that displays markets. Update src/app/page.tsx:
src/app/page.tsx
"use client";

import { useMemo, useState } from "react";
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(() => {
    if (!markets.length) return [];

    const lowerSearchQuery = searchQuery.toLowerCase();

    return markets.filter((market: Market) => {
      if (lowerSearchQuery && !market.question.toLowerCase().includes(lowerSearchQuery)) {
        return false;
      }
      return true;
    });
  }, [markets, searchQuery]);

  return (
    <div className="min-h-screen bg-[#0e1219] p-6">
      {/* Header */}
      <div className="max-w-6xl mx-auto mb-8">
        <h1 className="text-3xl font-bold text-white mb-4">Prediction Markets</h1>
        
        {/* Search */}
        <input
          type="text"
          value={searchQuery}
          onChange={(e) => setSearchQuery(e.target.value)}
          placeholder="Search markets..."
          className="w-full max-w-md bg-[#191b25] text-white px-4 py-3 rounded-lg outline-none"
        />
      </div>

      {/* Markets Grid */}
      <div className="max-w-6xl mx-auto">
        {isLoading ? (
          <div className="text-center py-20">
            <p className="text-gray-500">Loading markets...</p>
          </div>
        ) : error ? (
          <div className="text-center py-20">
            <p className="text-gray-500">Error loading markets. Please try again.</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}
              />
            ))}
          </div>
        ) : (
          <div className="text-center py-20">
            <p className="text-gray-500">No markets found</p>
          </div>
        )}
      </div>
    </div>
  );
}

Run the Application

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

Configure CORS

Add your local development URL to the CORS origins in your Dynamic dashboard under Developer Settings > CORS Origins.

How Trading Works

When a user places a trade on Polymarket through this app:
  1. Wallet Connection: The user connects via Dynamic’s embedded wallet
  2. Network Check: The app ensures the user is on Polygon network
  3. Credential Initialization: The app derives or creates Polymarket API credentials by signing a message
  4. Approvals: The app checks and requests necessary ERC20/ERC1155 approvals for USDC and outcome tokens
  5. Order Placement: A market order is submitted to Polymarket’s CLOB API
  6. 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 embedded wallets can seamlessly connect to prediction markets like Polymarket, providing users with a secure and user-friendly trading experience. The combination of Dynamic’s authentication flow and Polymarket’s CLOB API enables complex trading operations with minimal friction.

Additional Resources