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)
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:Copy
Ask AI
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:Copy
Ask AI
npm install @polymarket/clob-client ethers@^5.8.0 @tanstack/react-query motion lucide-react
@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
Copy
Ask AI
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
Configure Wagmi for Polygon
Since Polymarket operates on Polygon, configure Wagmi to use Polygon mainnet. Createsrc/lib/wagmi.ts:
src/lib/wagmi.ts
Copy
Ask AI
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. Createsrc/lib/providers.tsx:
src/lib/providers.tsx
Copy
Ask AI
"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>
);
}
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
Createsrc/lib/constants/contracts.ts for Polymarket contract addresses:
src/lib/constants/contracts.ts
Copy
Ask AI
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. Createsrc/app/api/polymarket/route.ts:
src/app/api/polymarket/route.ts
Copy
Ask AI
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. Createsrc/lib/hooks/usePolymarketMarkets.ts:
src/lib/hooks/usePolymarketMarkets.ts
Copy
Ask AI
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. Createsrc/lib/hooks/usePolymarketTrading.ts:
src/lib/hooks/usePolymarketTrading.ts
Copy
Ask AI
"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,
};
}
- 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. Createsrc/lib/hooks/useUserPositions.ts:
src/lib/hooks/useUserPositions.ts
Copy
Ask AI
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
Createsrc/components/MarketCard.tsx for displaying individual markets:
src/components/MarketCard.tsx
Copy
Ask AI
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. Updatesrc/app/page.tsx:
src/app/page.tsx
Copy
Ask AI
"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:Copy
Ask AI
npm run dev
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:- Wallet Connection: The user connects via Dynamic’s embedded wallet
- Network Check: The app ensures the user is on Polygon network
- Credential Initialization: The app derives or creates Polymarket API credentials by signing a message
- Approvals: The app checks and requests necessary ERC20/ERC1155 approvals for USDC and outcome tokens
- Order Placement: A market order is submitted to Polymarket’s CLOB API
- Confirmation: The user sees success/error feedback