From 7a995e44bd22cb873df694812f3c6cd4c6b53d06 Mon Sep 17 00:00:00 2001 From: godspowerufot Date: Thu, 12 Feb 2026 23:43:50 +0100 Subject: [PATCH] feat: implement copy trading system with documentation --- .../src/app/api/copy-trading/run/route.ts | 40 + terminal/src/app/copy-trading/README.md | 59 ++ terminal/src/app/copy-trading/page.tsx | 20 + .../src/components/CopyTradingTerminal.tsx | 683 ++++++++++++++++++ terminal/src/lib/copy-trader.ts | 265 +++++++ terminal/src/lib/polymarket-client.ts | 264 +++++++ terminal/src/lib/privy-client.ts | 86 +++ terminal/src/types/polymarket.ts | 26 + terminal/test-active-market.js | 77 ++ terminal/test-live-order.mjs | 95 +++ 10 files changed, 1615 insertions(+) create mode 100644 terminal/src/app/api/copy-trading/run/route.ts create mode 100644 terminal/src/app/copy-trading/README.md create mode 100644 terminal/src/app/copy-trading/page.tsx create mode 100644 terminal/src/components/CopyTradingTerminal.tsx create mode 100644 terminal/src/lib/copy-trader.ts create mode 100644 terminal/src/lib/polymarket-client.ts create mode 100644 terminal/src/lib/privy-client.ts create mode 100644 terminal/src/types/polymarket.ts create mode 100644 terminal/test-active-market.js create mode 100644 terminal/test-live-order.mjs diff --git a/terminal/src/app/api/copy-trading/run/route.ts b/terminal/src/app/api/copy-trading/run/route.ts new file mode 100644 index 0000000..6ced8aa --- /dev/null +++ b/terminal/src/app/api/copy-trading/run/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { CopyTrader } from '@/lib/copy-trader'; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const target = searchParams.get('target') || undefined; + const isTest = searchParams.get('test') === 'true'; + const amountParam = searchParams.get('amount'); + + // Parse and validate trade amount (default: 5 USDC) + const tradeAmount = amountParam ? parseFloat(amountParam) : 5; + if (isNaN(tradeAmount) || tradeAmount <= 0) { + return NextResponse.json( + { error: 'Invalid trade amount. Must be a positive number.' }, + { status: 400 } + ); + } + + try { + const trader = new CopyTrader(); + + if (isTest) { + const result = await trader.testExecution(); + return NextResponse.json({ success: true, data: result }); + } + + const result = await trader.run(target, tradeAmount); + + return NextResponse.json({ + success: true, + data: result + }); + } catch (error) { + console.error('Copy Trading execution failed:', error); + return NextResponse.json( + { error: 'Failed to run copy trader', details: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} diff --git a/terminal/src/app/copy-trading/README.md b/terminal/src/app/copy-trading/README.md new file mode 100644 index 0000000..625ca9f --- /dev/null +++ b/terminal/src/app/copy-trading/README.md @@ -0,0 +1,59 @@ +# Polymarket Copy Trading Bot + +## Overview +The **Polymarket Copy Trading Bot** is an automated system designed to mimic the trades of profitable users on the Polymarket platform. By monitoring a target wallet, this system automatically executes identical trades (Buy/Sell) on the same markets, leveraging the insights of successful traders. + +## Features +- **Leaderboard Integration:** View top traders by category (Politics, Sports, Crypto, etc.) and time period. +- **One-Click Copy:** Easily select a trader from the leaderboard to start copying. +- **Automated Execution:** Monitors the target wallet in real-time and executes trades instantly. +- **Configurable Trade Amounts:** Set a fixed USDC amount ("n USDC to trade") for every copied trade, managing your risk independent of the target's trade size. +- **Safety Mechanism:** Filters out large or risky trades based on configurable parameters (default: safe mode). + +## System Architecture + +### 1. Frontend (Next.js) +- Located at `/copy-trading`. +- Provides a dashboard to: + - Search/Select target wallets. + - View leaderboard statistics. + - Configure trade paramters (Amount per trade). + - View real-time logs of bot activity. + +### 2. Backend (Next.js API Routes) +- **Monitoring Endpoint:** `/api/copy-trading/run` +- **Logic:** + 1. Fetches recent trades from the target wallet using Polymarket's Gamma API. + 2. Compares trade timestamps to identify new activity. + 3. If a new trade is found, it constructs a mirroring order. + +### 3. Privy Integration (Security) +We use **Privy** for secure, server-side wallet management and signing. +- **Why Privy?** It allows the bot to sign transactions programmatically without exposing private keys in the frontend or requiring constant user confirmation (MetaMask popups). +- **Implementation:** + - `src/lib/privy-client.ts`: Initializes the Privy client and manages the "Bot Wallet". + - **Server-Side Signing:** The bot generates a special `PrivySigner` (ethers.js compatible) that intercepts signing requests and delegates them to Privy's secure enclave. + - This ensures that your trading bot can run autonomously while your assets remain secure. + +## Usage Guide + +1. **Fund the Bot Wallet:** + - Go to the Copy Trading Terminal. + - Copy the "Bot Execution Wallet" address. + - Send **USDC (Polygon)** for trading and **MATIC** for gas fees to this address. + +2. **Select a Trader:** + - Browse the "Top Traders Leaderboard". + - Click "Copy" on a profitable trader, or manually enter a wallet address. + +3. **Configure Amount:** + - Enter the **"Trade Amount (USDC)"**. + - *Example:* If you set this to **10 USDC**, the bot will buy $10 worth of shares for every trade the target makes, regardless of whether the target traded $100 or $10,000. + +4. **Start the Bot:** + - Click **"START COPY BOT"**. + - Keep the terminal open to monitor logs. + +## Development Verification +- **Test Market Fetching:** `node test-active-market.js` +- **Test Order Execution:** `node test-live-order.mjs` (Requires funded bot wallet) diff --git a/terminal/src/app/copy-trading/page.tsx b/terminal/src/app/copy-trading/page.tsx new file mode 100644 index 0000000..15efee6 --- /dev/null +++ b/terminal/src/app/copy-trading/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import CopyTradingTerminal from "@/components/CopyTradingTerminal"; +import Sidebar from "@/components/Sidebar"; + +export default function CopyTradingPage() { + return ( +
+ {/* Sidebar Navigation */} +
+ +
+ + {/* Main Content */} +
+ +
+
+ ); +} diff --git a/terminal/src/components/CopyTradingTerminal.tsx b/terminal/src/components/CopyTradingTerminal.tsx new file mode 100644 index 0000000..cce93b0 --- /dev/null +++ b/terminal/src/components/CopyTradingTerminal.tsx @@ -0,0 +1,683 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Play, Square, Activity, Copy, Wallet, ExternalLink, DollarSign, TrendingUp, TrendingDown, Users, X, LayoutGrid, Circle } from "lucide-react"; + +// interface ActiveTrader { +// wallet: string; +// tradeCount: number; +// totalVolume: number; +// markets: number; +// recentTrades: Array<{ +// market: string; +// side: string; +// size: number; +// price: number; +// timestamp: number; +// }>; +// } + +import { LeaderboardCategory, LeaderboardTimePeriod, TraderLeaderboardEntry } from "@/types/polymarket"; + +export default function CopyTradingTerminal() { + const [targetAddress, setTargetAddress] = useState(""); + const [tradeAmount, setTradeAmount] = useState(5); + const [isRunning, setIsRunning] = useState(false); + const [logs, setLogs] = useState([]); + const [botWallet, setBotWallet] = useState(""); + + // Leaderboard State + const [leaderboard, setLeaderboard] = useState([]); + const [loadingLeaderboard, setLoadingLeaderboard] = useState(false); + const [leaderboardCategory, setLeaderboardCategory] = useState('OVERALL'); + const [timePeriod, setTimePeriod] = useState('WEEK'); + const [viewMode, setViewMode] = useState<'LIST' | 'BUBBLE'>('LIST'); + + const logsEndRef = useRef(null); + const intervalRef = useRef(null); + + // Auto-scroll logs + useEffect(() => { + logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [logs]); + + // Fetch leaderboard on mount and when filters change + useEffect(() => { + fetchLeaderboard(); + }, [leaderboardCategory, timePeriod]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + const fetchLeaderboard = async () => { + setLoadingLeaderboard(true); + try { + const response = await fetch(`https://data-api.polymarket.com/v1/leaderboard?category=${leaderboardCategory}&timePeriod=${timePeriod}&limit=10`); + const data = await response.json(); + + if (Array.isArray(data)) { + setLeaderboard(data); + } else { + console.error('Leaderboard data is not an array:', data); + setLeaderboard([]); + } + } catch (error) { + console.error('Failed to fetch leaderboard:', error); + // addLog('Failed to fetch leaderboard', 'ERROR'); + } finally { + setLoadingLeaderboard(false); + } + }; + + const openCopyTradeModalFromLeaderboard = (trader: TraderLeaderboardEntry) => { + setTargetAddress(trader.proxyWallet); + // Optionally scroll to top or highlight the configuration panel + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const addLog = (message: string, level: 'INFO' | 'SUCCESS' | 'ERROR' | 'TRADE' = 'INFO') => { + const time = new Date().toLocaleTimeString([], { hour12: false }); + const prefix = level === 'TRADE' ? '๐ŸŽฏ' : level === 'SUCCESS' ? 'โœ…' : level === 'ERROR' ? 'โŒ' : 'โ„น๏ธ'; + setLogs(prev => [...prev, `[${time}] ${prefix} ${message}`]); + }; + + const fetchBotWallet = async () => { + try { + addLog("Fetching bot wallet..."); + const res = await fetch('/api/privy/wallet', { method: 'POST' }); + const data = await res.json(); + if (data.success && data.wallet) { + setBotWallet(data.wallet.address); + addLog(`Bot wallet loaded: ${data.wallet.address}`, 'SUCCESS'); + return true; + } else { + addLog("Failed to load bot wallet", 'ERROR'); + return false; + } + } catch (error) { + addLog(`Error loading bot wallet: ${error}`, 'ERROR'); + return false; + } + }; + + const startBot = async () => { + if (!targetAddress) { + addLog("Please enter a target wallet address", 'ERROR'); + return; + } + + if (!targetAddress.match(/^0x[a-fA-F0-9]{40}$/)) { + addLog("Invalid wallet address format", 'ERROR'); + return; + } + + if (tradeAmount <= 0) { + addLog("Trade amount must be positive", 'ERROR'); + return; + } + + setIsRunning(true); + setLogs([]); + + addLog("๐Ÿš€ Starting Copy Trading Bot..."); + addLog(`Target: ${targetAddress}`); + addLog(`Trade Amount: ${tradeAmount} USDC`); + + // Fetch bot wallet + const walletLoaded = await fetchBotWallet(); + if (!walletLoaded) { + setIsRunning(false); + return; + } + + addLog("Connecting to Polymarket...", 'SUCCESS'); + addLog("Starting trade monitoring..."); + + // Start polling + pollForTrades(); + }; + + const stopBot = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + setIsRunning(false); + addLog("Bot stopped by user", 'INFO'); + }; + + const pollForTrades = async () => { + // Initial check + await checkForTrades(); + + // Set up interval (every 5 seconds) + intervalRef.current = setInterval(async () => { + await checkForTrades(); + }, 5000); + }; + + const checkForTrades = async () => { + try { + const res = await fetch(`/api/copy-trading/run?target=${targetAddress}&amount=${tradeAmount}`); + const data = await res.json(); + + if (!data.success) { + addLog(`Error: ${data.error || 'Unknown error'}`, 'ERROR'); + return; + } + + const result = data.data; + + // Display all logs from backend (for new trades being executed) + if (result.logs && result.logs.length > 0) { + result.logs.forEach((log: string) => { + // Determine log level based on content + const level = log.includes('โŒ') ? 'ERROR' : + log.includes('โœ…') || log.includes('๐ŸŽฏ') ? 'SUCCESS' : + 'INFO'; + addLog(log, level); + }); + } + + // Always show latest trade info if available (both new and past trades) + if (result.latestParams) { + const trade = result.latestParams; + + if (result.executed) { + // New trade was executed - already shown in logs above + addLog('โ”€'.repeat(50), 'INFO'); + } else { + // Show latest trade found (already processed) + addLog(`๐Ÿ“Š Latest Trade Monitored:`, 'INFO'); + addLog(` Market: ${trade.title || trade.market_slug}`, 'INFO'); + addLog(` Side: ${trade.side} | Outcome: ${trade.outcome}`, 'INFO'); + addLog(` Size: ${trade.size} | Price: $${trade.price}`, 'INFO'); + addLog(` Timestamp: ${new Date(trade.timestamp * 1000).toLocaleString()}`, 'INFO'); + addLog(` Status: ${result.status}`, 'INFO'); + addLog('โ”€'.repeat(50), 'INFO'); + } + } else if (!result.executed) { + // No trades found at all + addLog(`๐Ÿ“ก Scanning... ${result.status}`, 'INFO'); + } + } catch (error) { + addLog(`Connection error: ${error}`, 'ERROR'); + } + }; + + return ( +
+
+ + {/* Header */} +
+

+ Copy Trading Bot +

+

+ Automatically copy trades from any Polymarket wallet with configurable amounts +

+
+ + {/* Active Traders / Leaderboard Section */} +
+
+

+ Top Traders Leaderboard +

+ +
+ + {/* View Toggle */} +
+ + +
+ + + + + + +
+
+ + {loadingLeaderboard ? ( +
+ +

Loading leaderboard rankings...

+
+ ) : leaderboard.length === 0 ? ( +
+

No traders found for this category

+
+ ) : viewMode === 'BUBBLE' ? ( +
+
+ {/* Bubble Rendering Logic */} + {leaderboard.map((trader, idx) => { + // Scale size based on rank/volume concept - simpler to use rank for visual hierarchy in a top 10 list + // Rank 1 = Largest. + const maxBubbles = leaderboard.length; + // Size between 60px and 160px + const size = 160 - (idx * (100 / maxBubbles)); + const isCopied = targetAddress.toLowerCase() === trader.proxyWallet.toLowerCase(); + const isPositive = trader.pnl >= 0; + + return ( +
openCopyTradeModalFromLeaderboard(trader)} + className={`rounded-full flex flex-col items-center justify-center cursor-pointer transition-all duration-300 hover:scale-110 hover:z-10 relative group border-2 ${isCopied + ? 'bg-green-500/20 border-green-500 shadow-[0_0_30px_rgba(74,222,128,0.3)] scale-105 z-10' + : isPositive + ? 'bg-gradient-to-br from-green-500/10 to-green-900/20 border-green-500/30 hover:border-green-400' + : 'bg-gradient-to-br from-red-500/10 to-red-900/20 border-red-500/30 hover:border-red-400' + }`} + style={{ + width: `${size}px`, + height: `${size}px`, + }} + > + {/* Rank Badge */} +
+ #{trader.rank} +
+ + {/* Profile Image or Initial */} + {trader.profileImage ? ( + {trader.userName} + ) : ( +
+ {trader.userName?.charAt(0) || '?'} +
+ )} + + {/* Name */} +
+ {trader.userName || 'Anonymous'} +
+ + {/* Stat */} +
+ {isPositive ? '+' : ''}${trader.pnl.toLocaleString(undefined, { notation: "compact" })} +
+ + {/* Copied Label */} + {isCopied && ( +
+ COPIED +
+ )} +
+ ); + })} +
+
+ Size by Rank +
+
+ ) : ( +
+ + + + + + + + + + + + {leaderboard.map((trader) => ( + + + + + + + + ))} + +
RankTraderVolumeP&LAction
+ #{trader.rank} + +
+ {trader.profileImage ? ( + {trader.userName} + ) : ( +
+ {trader.userName?.charAt(0) || '?'} +
+ )} +
+
+ + {trader.userName || 'Anonymous'} + + {trader.verifiedBadge && ( + โœ“ + )} +
+
+ {trader.proxyWallet.substring(0, 6)}...{trader.proxyWallet.substring(38)} + + + +
+
+
+
+ ${trader.vol.toLocaleString(undefined, { maximumFractionDigits: 0 })} + = 0 ? 'text-green-400' : 'text-red-400'}`}> + {trader.pnl >= 0 ? '+' : ''}${trader.pnl.toLocaleString(undefined, { maximumFractionDigits: 0 })} + + +
+
+ )} +
+ + {/* Copy Trade Modal */} + {/* {showModal && selectedTrader && ( +
+
+
+

Start Copy Trading

+ +
+ +
+
+ + + {selectedTrader.wallet} + +
+ +
+
+

Trades

+

{selectedTrader.tradeCount}

+
+
+

Volume

+

${selectedTrader.totalVolume.toFixed(2)}

+
+
+

Markets

+

{selectedTrader.markets}

+
+
+ + {selectedTrader.recentTrades.length > 0 && ( +
+ +
+ {selectedTrader.recentTrades.map((trade, idx) => ( +
+
+ + {trade.side} + + + {new Date(trade.timestamp * 1000).toLocaleString()} + +
+

{trade.market}

+

{trade.size.toFixed(1)} @ ${trade.price.toFixed(2)}

+
+ ))} +
+
+ )} + +
+ +
+ + setTradeAmount(parseFloat(e.target.value) || 0)} + min="0.01" + step="0.01" + className="w-full pl-10 pr-4 py-2 rounded bg-secondary/30 border border-border font-mono text-sm focus:border-primary focus:outline-none" + /> +
+

Amount to use for each copied trade

+
+ +
+ + +
+
+
+
+ )} */} + + {/* Configuration Panel */} +
+ + {/* Target Configuration */} +
+

+ Target Configuration +

+ +
+ + setTargetAddress(e.target.value.trim())} + placeholder="0x..." + disabled={isRunning} + className="w-full px-4 py-2 rounded bg-secondary/30 border border-border font-mono text-sm focus:border-primary focus:outline-none disabled:opacity-50" + /> +
+ +
+ +
+ + setTradeAmount(parseFloat(e.target.value) || 0)} + min="0.01" + step="0.01" + disabled={isRunning} + className="w-full pl-10 pr-4 py-2 rounded bg-secondary/30 border border-border font-mono text-sm focus:border-primary focus:outline-none disabled:opacity-50" + /> +
+

Amount to use for each copied trade

+
+ + +
+ + {/* Bot Wallet Info */} +
+

+ Bot Execution Wallet +

+ +
+ +
+ + {botWallet || "Not initialized..."} + +
+
+ + {botWallet && ( + + )} + +
+

+ โš ๏ธ Ensure this wallet has MATIC for gas and USDC for trading +

+
+
+ +
+ + {/* Logs Terminal */} +
+
+ System Logs +
+
+
+
+
+
+
+ {logs.length === 0 ? ( +
+ +

Terminal Ready. Configure target and start bot.

+
+ ) : ( + logs.map((log, i) => ( +
+ {log} +
+ )) + )} +
+
+
+ +
+
+ ); +} diff --git a/terminal/src/lib/copy-trader.ts b/terminal/src/lib/copy-trader.ts new file mode 100644 index 0000000..a02a5f5 --- /dev/null +++ b/terminal/src/lib/copy-trader.ts @@ -0,0 +1,265 @@ +import { PolymarketClient } from './polymarket-client'; +import { privy, getSystemWallet } from './privy-client'; + +const DEFAULT_TARGET_WALLET = "0x7ec4ffce0be6d9b30bb6c962166cde129dba00ad"; +const POLYMARKET_EXCHANGE_ADDRESS = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"; + +// Simple in-memory store for last processed timestamp +let lastProcessedTimestamp = 0; + +export class CopyTrader { + private polyClient: PolymarketClient; + private botWallet?: { id: string; address: string }; + + constructor() { + this.polyClient = new PolymarketClient(); + } + + async run(targetAddress?: string, tradeAmount: number = 5) { + const target = targetAddress || DEFAULT_TARGET_WALLET; + const logs: string[] = []; + + console.log(`[CopyTrader] Checking for new trades from ${target}...`); + console.log(`[CopyTrader] Trade amount configured: ${tradeAmount} USDC`); + + logs.push(`๐Ÿ” Checking for new trades from target wallet...`); + logs.push(` Target: ${target.substring(0, 10)}...${target.substring(target.length - 8)}`); + logs.push(` Trade Amount: ${tradeAmount} USDC`); + + try { + // 1. Fetch recent trades from Target + logs.push(`๐Ÿ“ก Fetching recent trades from Polymarket API...`); + const trades = await this.polyClient.getUserTrades(target); + + if (!trades || trades.length === 0) { + logs.push(`โš ๏ธ No trades found for this wallet`); + return { status: "No trades found", executed: false, logs }; + } + + logs.push(`โœ… Found ${trades.length} trade(s) from target wallet`); + + // 2. Sort by timestamp descending (newest first) + const latestTrade = trades[0]; + logs.push(`๐Ÿ”Ž Analyzing latest trade...`); + logs.push(` Timestamp: ${new Date(latestTrade.timestamp * 1000).toLocaleString()}`); + + // 3. Check if new + if (latestTrade.timestamp <= lastProcessedTimestamp) { + logs.push(`โญ๏ธ Trade already processed (timestamp: ${lastProcessedTimestamp})`); + logs.push(`๐Ÿ“Š Latest Trade Info:`); + logs.push(` Market: ${latestTrade.title || latestTrade.market_slug}`); + logs.push(` Side: ${latestTrade.side} | Outcome: ${latestTrade.outcome}`); + logs.push(` Size: ${latestTrade.size} | Price: $${latestTrade.price}`); + return { status: "No new trades", executed: false, latestParams: latestTrade, logs }; + } + + console.log(`[CopyTrader] NEW TRADE DETECTED:`, latestTrade); + logs.push(`๐ŸŽฏ NEW TRADE DETECTED!`); + logs.push(` Side: ${latestTrade.side}`); + logs.push(` Market: ${latestTrade.title || latestTrade.market_slug || 'Unknown'}`); + logs.push(` Outcome: ${latestTrade.outcome}`); + logs.push(` Size: ${latestTrade.size}`); + logs.push(` Price: $${latestTrade.price}`); + if (latestTrade.asset) { + logs.push(` Asset ID: ${latestTrade.asset.substring(0, 20)}...`); + } + + lastProcessedTimestamp = latestTrade.timestamp; + logs.push(`โœ… Updated last processed timestamp: ${lastProcessedTimestamp}`); + + // 4. Analyze/Filter Trade + // Example Policy: Only copy small trades < $1000 size for safety + // const size = parseFloat(latestTrade.size); + // if (size > 1000) { + // return { status: "Trade filtered (Size too large)", executed: false, trade: latestTrade }; + // } + + // 5. Execute Copy + // Get System Wallet + logs.push(`๐Ÿ” Fetching bot wallet from Privy...`); + const botWallet = await getSystemWallet(); + if (!botWallet) { + logs.push(`โŒ Bot wallet not available`); + throw new Error("Bot wallet not available"); + } + + // Store wallet for error messages + this.botWallet = botWallet; + + console.log(`[CopyTrader] Executing trade with Bot Wallet: ${botWallet.address}`); + console.log(`[CopyTrader] Trade size: ${tradeAmount} USDC`); + + logs.push(`โœ… Bot Wallet Retrieved: ${botWallet.address}`); + logs.push(`๐Ÿ’ฐ Preparing to execute trade with ${tradeAmount} USDC`); + + const txHash = await this.executeTrade(botWallet, latestTrade, tradeAmount, logs); + + logs.push(`โœ… TRADE EXECUTED SUCCESSFULLY!`); + logs.push(` TX Hash: ${txHash}`); + + return { + status: "Copy Trade Executed", + executed: true, + txHash, + logs, + trade: { + market: latestTrade.market_slug, + side: latestTrade.side, + size: latestTrade.size, + price: latestTrade.price, + timestamp: latestTrade.timestamp, + outcome: latestTrade.outcome + } + }; + + } catch (error) { + console.error("[CopyTrader] Error:", error); + logs.push(`โŒ ERROR OCCURRED!`); + + if (error instanceof Error) { + logs.push(` Message: ${error.message}`); + if (error.stack) { + const stackLines = error.stack.split('\n').slice(0, 3); + logs.push(` Stack: ${stackLines.join(' | ')}`); + } + } else { + logs.push(` Details: ${String(error)}`); + } + + return { + status: "Error occurred", + executed: false, + logs, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + private async executeTrade(wallet: { id: string, address: string }, trade: any, tradeAmount: number, logs: string[]) { + // Validate trade amount + if (tradeAmount <= 0) { + throw new Error("Trade amount must be positive"); + } + + logs.push(`โš™๏ธ Initializing Polymarket client...`); + + console.log(`[CopyTrader] Executing trade with Bot Wallet: ${wallet.address}`); + console.log(`[CopyTrader] Trade size: ${tradeAmount} USDC`); + + // Import Privy signer creator + const { createPrivySigner } = await import('./privy-client'); + + // Create Privy signer for this wallet + logs.push(`๐Ÿ” Creating Privy signer for wallet...`); + const signer = await createPrivySigner(wallet.id, wallet.address); + logs.push(`โœ… Privy signer created`); + + // Initialize Polymarket client with Privy signer + logs.push(`๐Ÿ”— Initializing Polymarket CLOB client...`); + await this.polyClient.initializeForWallet(wallet.address, signer); + logs.push(`โœ… Polymarket client initialized`); + + // Prepare order parameters + logs.push(`โš™๏ธ Preparing order parameters...`); + logs.push(` Market: ${trade.title || trade.market_slug || 'Unknown'}`); + logs.push(` Side: ${trade.side}`); + if (trade.asset) { + logs.push(` Token ID: ${trade.asset.substring(0, 20)}...`); + } + logs.push(` Size: ${tradeAmount} USDC`); + logs.push(` Price: $${trade.price}`); + + // Validate that we have the required asset ID + if (!trade.asset) { + logs.push(`โŒ Missing asset ID - cannot execute trade`); + throw new Error("Trade is missing asset ID"); + } + + // Execute the trade using the SDK + logs.push(`๐Ÿ“ Creating and signing order via Polymarket SDK...`); + + try { + const response = await this.polyClient.postOrder({ + tokenId: trade.asset, + conditionId: trade.conditionId, // Pass condition ID for proper market lookup + price: parseFloat(trade.price), + side: trade.side, + size: tradeAmount + }); + + // Check if order was actually created + if (response.orderID) { + logs.push(`โœ… Order Posted Successfully!`); + logs.push(` Order ID: ${response.orderID}`); + logs.push(` TX Hash: ${response.transactionHash || 'Pending'}`);; + } else { + logs.push(`โš ๏ธ Order response received but no Order ID`); + logs.push(` Order ID: undefined`); + } + + return response.transactionHash || response.orderID || "OrderPosted"; + } catch (error: any) { + // Log the API error details + logs.push(`โŒ Order Submission Failed!`); + + // Extract the specific error message from the API response + let errorMessage = 'Unknown error'; + + if (error.response?.data?.error) { + // This is the specific error from Polymarket API (e.g., "not enough balance / allowance") + errorMessage = error.response.data.error; + logs.push(` โŒ ERROR: ${errorMessage}`); + } else if (error.message) { + errorMessage = error.message; + logs.push(` Error: ${errorMessage}`); + } else { + logs.push(` Error: ${String(error)}`); + } + + // Add helpful context for common errors + if (errorMessage.includes('balance') || errorMessage.includes('allowance')) { + logs.push(` ๐Ÿ’ก Your bot wallet needs USDC funding`); + logs.push(` ๐Ÿ“ Wallet: ${this.botWallet?.address || 'Unknown'}`); + logs.push(` ๐Ÿ’ฐ Send USDCe (Polygon) to this address`); + } + + logs.push(` Order ID: undefined`); + + throw error; // Re-throw to be caught by outer try-catch + } + } + + async testExecution() { + console.log("[CopyTrader] Starting Manual Test Execution..."); + // 1. Get System Wallet + const botWallet = await getSystemWallet(); + if (!botWallet) { + throw new Error("Bot wallet not available"); + } + console.log(`[Test] Using Bot Wallet: ${botWallet.address}`); + + // 2. Create Dummy Trade (e.g. Yes on a popular market) + // Trump Inauguration Market Token ID (Example) or just a random one for testing signature + // Using a real Token ID is better to get a real error from Polymarket if funds are low + const TEST_TOKEN_ID = "21742633143463906290569050155826241533067272736897614382201930560761571152424"; // Random valid-looking ID + + const testTrade = { + side: "BUY", + asset: TEST_TOKEN_ID, + size: "1", + price: "0.5", + market_slug: "test-market", + timestamp: Date.now(), + outcome: "Yes" + }; + + // 3. Execut + try { + const result = await this.executeTrade(botWallet, testTrade); + return { status: "Test Execution Attempted", result }; + } catch (error: any) { + console.error("[Test] Execution Failed (Expected if no funds):", error); + return { status: "Test Execution Failed (Likely Insufficient Funds)", error: error.message }; + } + } +} diff --git a/terminal/src/lib/polymarket-client.ts b/terminal/src/lib/polymarket-client.ts new file mode 100644 index 0000000..9fffb86 --- /dev/null +++ b/terminal/src/lib/polymarket-client.ts @@ -0,0 +1,264 @@ +// src/lib/polymarket-client.ts +import { ClobClient } from "@polymarket/clob-client"; +import * as ethersV5 from "ethers5"; // Use ethers v5 for Polymarket compatibility + +interface ApiCredentials { + key: string; + secret: string; + passphrase: string; +} + +export class PolymarketClient { + private baseUrl = "https://data-api.polymarket.com"; + private clobClient: ClobClient | null = null; + private credentials: Map = new Map(); + + /** + * Initialize the CLOB client with a wallet and handle API credentials + */ + async initializeForWallet(walletAddress: string, signer: ethersV5.Wallet | ethersV5.providers.JsonRpcSigner) { + console.log(`[Polymarket] Initializing client for wallet ${walletAddress}...`); + + // Step 1: Check if we have stored credentials + let apiCreds = this.credentials.get(walletAddress); + + if (!apiCreds) { + // Check environment variables + if ( + process.env.POLYMARKET_API_KEY && + process.env.POLYMARKET_WALLET_ADDRESS === walletAddress + ) { + console.log(`[Polymarket] Using credentials from environment variables`); + apiCreds = { + key: process.env.POLYMARKET_API_KEY, + secret: process.env.POLYMARKET_API_SECRET!, + passphrase: process.env.POLYMARKET_API_PASSPHRASE! + }; + this.credentials.set(walletAddress, apiCreds); + } else { + // Step 2: Create temporary client to derive credentials + console.log(`[Polymarket] Creating temporary client to derive credentials...`); + const tempClient = new ClobClient( + "https://clob.polymarket.com", + 137, + signer + ); + + // Step 3: Derive API credentials + console.log(`[Polymarket] Deriving API credentials (this may take a moment)...`); + const derivedCreds = await tempClient.createOrDeriveApiKey(); + + apiCreds = { + key: derivedCreds.key, + secret: derivedCreds.secret, + passphrase: derivedCreds.passphrase + }; + + this.credentials.set(walletAddress, apiCreds); + + // Log credentials to save + console.log(`\n${"=".repeat(60)}`); + console.log(`๐Ÿ”‘ SAVE THESE POLYMARKET CREDENTIALS FOR: ${walletAddress}`); + console.log(`Add to your .env file:`); + console.log(`POLYMARKET_WALLET_ADDRESS=${walletAddress}`); + console.log(`POLYMARKET_API_KEY=${apiCreds.key}`); + console.log(`POLYMARKET_API_SECRET=${apiCreds.secret}`); + console.log(`POLYMARKET_API_PASSPHRASE=${apiCreds.passphrase}`); + console.log(`${"=".repeat(60)}\n`); + } + } else { + console.log(`[Polymarket] Using cached credentials for ${walletAddress}`); + } + + // Step 4: Determine signature type and funder + // For Privy wallets (EOA), use signatureType = 0 + const SIGNATURE_TYPE = 0; // EOA + const FUNDER_ADDRESS = walletAddress; // For EOA, funder is the wallet itself + + // Step 5: Create the FINAL client with ALL required parameters + console.log(`[Polymarket] Creating authenticated CLOB client...`); + console.log(` Signature Type: ${SIGNATURE_TYPE} (EOA)`); + console.log(` Funder Address: ${FUNDER_ADDRESS}`); + + this.clobClient = new ClobClient( + "https://clob.polymarket.com", + 137, // Polygon + signer, + apiCreds, // <-- CRITICAL: User API credentials + SIGNATURE_TYPE, // <-- CRITICAL: Must match wallet type + FUNDER_ADDRESS, // <-- CRITICAL: Where funds come from + undefined, // additional options + false // set default address + ); + + console.log(`[Polymarket] โœ… Client initialized successfully with full authentication`); + } + + /** + * Post an order to Polymarket + */ + async postOrder(params: { + tokenId: string; + conditionId?: string; + price: number; + side: 'BUY' | 'SELL'; + size: number; + }): Promise { + if (!this.clobClient) { + throw new Error("CLOB Client not initialized. Call initializeForWallet() first."); + } + + console.log(`[Polymarket] Preparing order...`); + console.log(` Token ID: ${params.tokenId.substring(0, 20)}...`); + console.log(` Side: ${params.side}`); + console.log(` Size: ${params.size}`); + console.log(` Price: ${params.price}`); + + try { + // Get market info to get tickSize and negRisk + let tickSize = "0.01"; // Default tick size + let negRisk = false; // Default negRisk + + // Try to fetch market info using condition ID if available + if (params.conditionId) { + try { + console.log(`[Polymarket] Fetching market info using condition ID...`); + const market = await this.clobClient.getMarket(params.conditionId); + + if (market) { + tickSize = market.minimum_tick_size || tickSize; + negRisk = market.neg_risk || negRisk; + console.log(`[Polymarket] Market info retrieved:`); + console.log(` Tick Size: ${tickSize}`); + console.log(` Neg Risk: ${negRisk}`); + } + } catch (error: any) { + console.warn(`[Polymarket] Could not fetch market info (using defaults):`, error.message); + console.log(` Using default Tick Size: ${tickSize}`); + console.log(` Using default Neg Risk: ${negRisk}`); + } + } else { + console.log(`[Polymarket] No condition ID provided, using default market settings`); + console.log(` Tick Size: ${tickSize}`); + console.log(` Neg Risk: ${negRisk}`); + } + + // Import required types + const { Side, OrderType } = await import("@polymarket/clob-client"); + + // Create and post order with market options + console.log(`[Polymarket] Creating and posting order...`); + const response = await this.clobClient.createAndPostOrder( + { + tokenID: params.tokenId, + price: params.price, + size: params.size, + side: params.side === 'BUY' ? Side.BUY : Side.SELL + }, + { + tickSize: tickSize, + negRisk: negRisk + }, + OrderType.GTC // Good-Til-Cancelled + ); + + // Check if the response indicates an error + // The CLOB client doesn't throw on API errors, it returns them in the response + if (response.status && (response.status >= 400 || response.status < 200)) { + console.error(`[CLOB Client] โŒ API Error - Status ${response.status}`); + + // Extract error message from response + const errorMessage = response.error || response.data?.error || response.statusText || 'Unknown error'; + console.error(`[CLOB Client] Error: ${errorMessage}`); + + // Create a proper error object with the API error details + const apiError: any = new Error(errorMessage); + apiError.response = { + status: response.status, + statusText: response.statusText, + data: { error: errorMessage } + }; + + throw apiError; + } + + console.log("[Polymarket] โœ… Order posted successfully!"); + console.log(" Order ID:", response.orderID); + console.log(" Status:", response.status); + + return response; + } catch (error: any) { + console.error("[Polymarket] โŒ Failed to post order:", error); + + // Provide helpful error messages + if (error.message?.includes("Invalid signature") || error.message?.includes("L2 auth not available")) { + console.error("\n๐Ÿ’ก TIP: This usually means:"); + console.error(" - Wrong signature type (should be 0 for Privy EOA wallets)"); + console.error(" - Wrong funder address"); + console.error(" - API credentials don't match the wallet"); + } else if (error.message?.includes("Unauthorized") || error.message?.includes("Invalid api key")) { + console.error("\n๐Ÿ’ก TIP: API credentials are invalid or expired"); + console.error(" - Delete credentials from .env and let them regenerate"); + } else if (error.message?.includes("balance") || error.message?.includes("allowance") || error.message?.includes("not enough")) { + console.error("\n๐Ÿ’ก INSUFFICIENT BALANCE ERROR"); + console.error(" โŒ Your wallet doesn't have enough USDC to execute this trade"); + console.error(" ๐Ÿ“ To fix this:"); + console.error(" 1. Get your bot wallet address from the logs above"); + console.error(" 2. Send USDCe (Polygon) to that address"); + console.error(" 3. Approve Polymarket contract to spend your USDC"); + console.error(" ๐Ÿ’ฐ Minimum recommended: $20-50 USDC for testing"); + + // Throw a more user-friendly error + throw new Error("Insufficient USDC balance or allowance. Please fund your bot wallet with USDCe on Polygon and approve the Polymarket contract."); + } + + throw error; + } + } + + /** + * Get user trades (no auth needed for public data) + */ + async getUserTrades(walletAddress: string, limit: number = 20): Promise { + const url = `${this.baseUrl}/trades?limit=${limit}&user=${walletAddress}`; + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`Polymarket API Error (${response.status}): ${errorText}`); + throw new Error(`Polymarket API Error: ${response.statusText}`); + } + + const data = await response.json(); + + console.log("[Polymarket] Raw API response sample:", data[0]); + + // Map to unified format + return data.map((t: any) => ({ + side: t.side, + size: parseFloat(t.size), + price: parseFloat(t.price), + timestamp: t.timestamp, + market_slug: t.slug || t.market, + asset: t.asset || t.asset_id, + conditionId: t.conditionId, // Include condition ID for market lookup + title: t.title, + outcome: t.outcome, + transactionHash: t.transactionHash || t.transaction_hash + })); + + } catch (error) { + console.error("Failed to fetch Polymarket trades:", error); + throw error; + } + } +} + +export const createPolymarketClient = () => new PolymarketClient(); \ No newline at end of file diff --git a/terminal/src/lib/privy-client.ts b/terminal/src/lib/privy-client.ts new file mode 100644 index 0000000..e1a12a6 --- /dev/null +++ b/terminal/src/lib/privy-client.ts @@ -0,0 +1,86 @@ +import { PrivyClient } from '@privy-io/server-auth'; +import * as ethersV5 from 'ethers5'; // v5 for Polymarket +import fs from 'fs'; +import path from 'path'; + +if (!process.env.PRIVY_APP_ID || !process.env.PRIVY_APP_SECRET) { + throw new Error('Missing Privy configuration'); +} + +export const privy = new PrivyClient( + process.env.PRIVY_APP_ID, + process.env.PRIVY_APP_SECRET +); + +const WALLET_FILE = path.join(process.cwd(), 'bot-wallet.json'); + +export async function getSystemWallet() { + // 1. Check if we already have a wallet saved + if (fs.existsSync(WALLET_FILE)) { + try { + const data = JSON.parse(fs.readFileSync(WALLET_FILE, 'utf-8')); + if (data.id && data.address) { + return data; + } + } catch (e) { + console.error("Error reading wallet file:", e); + } + } + + // 2. Create new if not exists + try { + console.log("Creating new Privy Server Wallet for Bot..."); + const wallet = await privy.walletApi.create({ chainType: 'ethereum' }); + + // Save to file + fs.writeFileSync(WALLET_FILE, JSON.stringify({ + id: wallet.id, + address: wallet.address, + chainType: wallet.chainType, + createdAt: new Date().toISOString() + }, null, 2)); + + return { id: wallet.id, address: wallet.address }; + } catch (error) { + console.error("Failed to create system wallet:", error); + return null; + } +} + +/** + * Create an ethers v5 Signer from a Privy wallet + * This uses Privy's signing methods under the hood + */ +export async function createPrivySigner(walletId: string, walletAddress: string): Promise { + // Create a custom signer that uses Privy's API for signing + const provider = new ethersV5.providers.JsonRpcProvider('https://polygon-rpc.com'); + + const signer = new ethersV5.VoidSigner(walletAddress, provider); + + // Override the _signTypedData method to use Privy + (signer as any)._signTypedData = async (domain: any, types: any, value: any) => { + console.log('[PrivySigner] Signing typed data with Privy...'); + + // @ts-ignore + const signatureResponse = await privy.walletApi.ethereum.signTypedData({ + walletId: walletId, + typedData: { + domain, + types, + primaryType: Object.keys(types).find(key => key !== 'EIP712Domain') || 'Order', + message: value + } + }); + + // Extract signature from response + const signature = typeof signatureResponse === 'string' + ? signatureResponse + : (signatureResponse as any).signature || String(signatureResponse); + + console.log('[PrivySigner] Signature received from Privy'); + return signature; + }; + + // Cast to JsonRpcSigner to match Polymarket SDK expectations + return signer as unknown as ethersV5.providers.JsonRpcSigner; +} diff --git a/terminal/src/types/polymarket.ts b/terminal/src/types/polymarket.ts new file mode 100644 index 0000000..ef52677 --- /dev/null +++ b/terminal/src/types/polymarket.ts @@ -0,0 +1,26 @@ +export type LeaderboardCategory = + | 'OVERALL' + | 'POLITICS' + | 'SPORTS' + | 'CRYPTO' + | 'CULTURE' + | 'MENTIONS' + | 'WEATHER' + | 'ECONOMICS' + | 'TECH' + | 'FINANCE'; + +export type LeaderboardTimePeriod = 'DAY' | 'WEEK' | 'MONTH' | 'ALL'; + +export type LeaderboardOrderBy = 'PNL' | 'VOL'; + +export interface TraderLeaderboardEntry { + rank: string; + proxyWallet: string; + userName?: string; + vol: number; + pnl: number; + profileImage?: string; + xUsername?: string; + verifiedBadge?: boolean; +} diff --git a/terminal/test-active-market.js b/terminal/test-active-market.js new file mode 100644 index 0000000..949b8fc --- /dev/null +++ b/terminal/test-active-market.js @@ -0,0 +1,77 @@ +// Quick script to fetch active Polymarket markets +const fetch = require('node-fetch'); + +async function getActiveMarkets() { + try { + console.log('Fetching active markets from Polymarket...\n'); + + // Use gamma API for better market data + const response = await fetch('https://gamma-api.polymarket.com/markets?limit=20&active=true'); + const markets = await response.json(); + + if (!markets || markets.length === 0) { + console.log('No markets found'); + return; + } + + console.log(`Found ${markets.length} active markets:\n`); + console.log('='.repeat(80)); + + markets.slice(0, 5).forEach((market, idx) => { + console.log(`\n${idx + 1}. ${market.question}`); + console.log(` Market Slug: ${market.slug || market.market_slug}`); + console.log(` Condition ID: ${market.condition_id || market.conditionId}`); + console.log(` Active: ${market.active}`); + console.log(` End Date: ${market.end_date_iso || 'N/A'}`); + + if (market.tokens && market.tokens.length > 0) { + console.log(` Tokens:`); + market.tokens.forEach(token => { + console.log(` - ${token.outcome}: ${token.token_id}`); + console.log(` Price: $${token.price || 'N/A'}`); + }); + } else if (market.outcomes && Array.isArray(market.outcomes)) { + console.log(` Outcomes: ${market.outcomes.join(', ')}`); + } + console.log('-'.repeat(80)); + }); + + // Show a recommended market for testing + if (markets.length > 0) { + const testMarket = markets[0]; + console.log('\n\n๐ŸŽฏ RECOMMENDED TEST MARKET:'); + console.log('='.repeat(80)); + console.log(`Question: ${testMarket.question}`); + console.log(`Market Slug: ${testMarket.slug || testMarket.market_slug}`); + console.log(`Condition ID: ${testMarket.condition_id || testMarket.conditionId}`); + console.log(`End Date: ${testMarket.end_date_iso || 'N/A'}`); + + if (testMarket.tokens && testMarket.tokens.length > 0) { + const token = testMarket.tokens[0]; + console.log(`\nโœ… Test Order Parameters (Copy this):`); + console.log(`{`); + console.log(` tokenId: "${token.token_id}",`); + console.log(` conditionId: "${testMarket.condition_id || testMarket.conditionId}",`); + console.log(` side: "BUY",`); + console.log(` price: ${token.price || 0.5},`); + console.log(` size: 1`); + console.log(`}`); + } else if (testMarket.clobTokenIds && testMarket.clobTokenIds.length > 0) { + console.log(`\nโœ… Test Order Parameters (Copy this):`); + console.log(`{`); + console.log(` tokenId: "${testMarket.clobTokenIds[0]}",`); + console.log(` conditionId: "${testMarket.condition_id || testMarket.conditionId}",`); + console.log(` side: "BUY",`); + console.log(` price: 0.5,`); + console.log(` size: 1`); + console.log(`}`); + } + } + + } catch (error) { + console.error('Error fetching markets:', error.message); + console.error('Stack:', error.stack); + } +} + +getActiveMarkets(); diff --git a/terminal/test-live-order.mjs b/terminal/test-live-order.mjs new file mode 100644 index 0000000..4ede268 --- /dev/null +++ b/terminal/test-live-order.mjs @@ -0,0 +1,95 @@ +// Test script to execute a real order on an active Polymarket market +import { PolymarketClient } from './src/lib/polymarket-client.js'; +import { getSystemWallet, createPrivySigner } from './src/lib/privy-client.js'; + +async function testLiveOrder() { + console.log('๐Ÿงช TESTING LIVE ORDER EXECUTION'); + console.log('='.repeat(80)); + + try { + // Step 1: Get bot wallet + console.log('\n๐Ÿ“ Step 1: Getting bot wallet...'); + const wallet = await getSystemWallet(); + if (!wallet) { + throw new Error('Failed to get bot wallet'); + } + console.log(`โœ… Bot Wallet: ${wallet.address}`); + + // Step 2: Create Privy signer + console.log('\n๐Ÿ“ Step 2: Creating Privy signer...'); + const signer = await createPrivySigner(wallet.id, wallet.address); + console.log('โœ… Signer created'); + + // Step 3: Initialize Polymarket client + console.log('\n๐Ÿ“ Step 3: Initializing Polymarket client...'); + const polyClient = new PolymarketClient(); + await polyClient.initializeForWallet(wallet.address, signer); + console.log('โœ… Polymarket client initialized'); + + // Step 4: Prepare test order + console.log('\n๐Ÿ“ Step 4: Preparing test order...'); + const testOrder = { + tokenId: "13018971792603393862521629014128024641178044618205296260525674415067194037856", + conditionId: "0x49a20c7523c271099008f3ef9a31521263b24d637959852a391e6b4697b1a437", + side: "BUY", + price: 0.004, + size: 1 // $1 USDC + }; + + console.log('๐Ÿ“Š Order Details:'); + console.log(` Market: Trump deportation 1-1.25M`); + console.log(` Side: ${testOrder.side}`); + console.log(` Price: $${testOrder.price}`); + console.log(` Size: ${testOrder.size} USDC`); + console.log(` Token ID: ${testOrder.tokenId.substring(0, 20)}...`); + console.log(` Condition ID: ${testOrder.conditionId}`); + + // Step 5: Execute order + console.log('\n๐Ÿ“ Step 5: Executing order...'); + console.log('โณ This may take a moment...\n'); + + const response = await polyClient.postOrder(testOrder); + + // Step 6: Show results + console.log('\n' + '='.repeat(80)); + console.log('๐ŸŽ‰ ORDER EXECUTION RESULT'); + console.log('='.repeat(80)); + console.log('Response:', JSON.stringify(response, null, 2)); + + if (response.orderID) { + console.log('\nโœ… SUCCESS! Order was created:'); + console.log(` Order ID: ${response.orderID}`); + console.log(` Status: ${response.status || 'Pending'}`); + if (response.transactionHash) { + console.log(` TX Hash: ${response.transactionHash}`); + } + } else { + console.log('\nโš ๏ธ Order response received but no Order ID'); + } + + } catch (error) { + console.error('\n' + '='.repeat(80)); + console.error('โŒ TEST FAILED'); + console.error('='.repeat(80)); + console.error('Error:', error.message); + + if (error.response) { + console.error('API Response:', error.response); + } + + // Provide helpful debugging info + if (error.message?.includes('Unauthorized')) { + console.error('\n๐Ÿ’ก TIP: API credentials issue - try regenerating them'); + } else if (error.message?.includes('balance') || error.message?.includes('insufficient')) { + console.error('\n๐Ÿ’ก TIP: Insufficient USDC balance in wallet'); + console.error(' Deposit USDCe on Polygon to:', wallet?.address); + } else if (error.message?.includes('orderbook')) { + console.error('\n๐Ÿ’ก TIP: Market orderbook issue - try a different market'); + } + + process.exit(1); + } +} + +// Run the test +testLiveOrder();