Skip to Content
RecipesPolymarket LLM Analyst

Polymarket LLM Analyst

What are we cooking?

An autonomous agent that uses a Large Language Model (LLM) to analyze Polymarket prediction markets, form probabilistic opinions, and place trades when it finds an edge — all signed securely through WaaP CLI without exposing private keys.

The agent will:

  1. Fetch active prediction markets from the Polymarket Gamma API.
  2. Send market details to an LLM (Anthropic Claude or OpenAI GPT) for analysis.
  3. Compare the LLM’s confidence score against current market prices.
  4. Place trades when the confidence gap exceeds a configurable threshold.
  5. Sign all orders via waap-cli sign-typed-data using 2PC-MPC — no raw keys in the environment.

This recipe builds on the Polymarket Signal Trader recipe. If you haven’t set up CLOB credentials and USDC approvals yet, start there first.


Prerequisites

  • WaaP CLI installed and logged in (waap-cli login)
  • Polymarket CLOB API credentials (API key, secret, passphrase) — see the Signal Trader recipe for setup
  • USDC on Polygon funded to your WaaP wallet
  • USDC approval for the CTF Exchange contract — see the Signal Trader recipe
  • LLM API key — either an Anthropic API key or an OpenAI API key (or both)

Project Setup

mkdir waap-polymarket-llm && cd waap-polymarket-llm npm init -y npm install dotenv

Create a .env file with your credentials:

# WaaP (already configured via waap-cli login) # Polymarket CLOB POLY_API_KEY=your_api_key POLY_API_SECRET=your_api_secret POLY_PASSPHRASE=your_passphrase # LLM — set one or both ANTHROPIC_API_KEY=your_anthropic_key OPENAI_API_KEY=your_openai_key # Agent config AGENT_MAX_ORDER_USD=5 CONFIDENCE_THRESHOLD=0.15

Step 1: Market Fetching

Fetch active markets from the Gamma API and extract tradeable details.

import 'dotenv/config'; const GAMMA_API_URL = 'https://gamma-api.polymarket.com'; async function getActiveMarkets() { const response = await fetch( `${GAMMA_API_URL}/events?active=true&closed=false&limit=10` ); const events = await response.json(); // Flatten events into individual markets with token info const markets = []; for (const event of events) { for (const market of event.markets ?? []) { if (!market.clobTokenIds) continue; // Parse current prices from the outcomePrices string const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : null; // The Gamma API events endpoint returns clobTokenIds (JSON string array) const tokenIds = JSON.parse(market.clobTokenIds); markets.push({ eventTitle: event.title, question: market.question, marketId: market.id, tokenIds, // [yesTokenId, noTokenId] yesPrice: prices ? parseFloat(prices[0]) : null, noPrice: prices ? parseFloat(prices[1]) : null, }); } } return markets; }

Step 2: LLM Integration

The core of this agent is sending market questions to an LLM and getting back a structured confidence score. Below are implementations for both Anthropic Claude and OpenAI GPT.

Anthropic Claude

async function analyzeWithClaude(market) { const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, system: `You are a prediction market analyst. Given a market question and current prices, estimate the probability of YES. Respond with ONLY valid JSON: {"probability": <0.0-1.0>, "reasoning": "<brief explanation>"}`, messages: [ { role: 'user', content: `Market: ${market.question}\nCurrent YES price: ${market.yesPrice}\nCurrent NO price: ${market.noPrice}\n\nWhat is your estimated probability that this resolves YES?`, }, ], }), }); const data = await response.json(); const text = data.content[0].text; return JSON.parse(text); }

OpenAI GPT

async function analyzeWithOpenAI(market) { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: 'gpt-4o', response_format: { type: 'json_object' }, messages: [ { role: 'system', content: `You are a prediction market analyst. Given a market question and current prices, estimate the probability of YES. Respond with ONLY valid JSON: {"probability": <0.0-1.0>, "reasoning": "<brief explanation>"}`, }, { role: 'user', content: `Market: ${market.question}\nCurrent YES price: ${market.yesPrice}\nCurrent NO price: ${market.noPrice}\n\nWhat is your estimated probability that this resolves YES?`, }, ], }), }); const data = await response.json(); const text = data.choices[0].message.content; return JSON.parse(text); }

Unified Wrapper

async function analyzeMmarket(market) { if (process.env.ANTHROPIC_API_KEY) { return analyzeWithClaude(market); } if (process.env.OPENAI_API_KEY) { return analyzeWithOpenAI(market); } throw new Error('No LLM API key configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.'); }

Step 3: Confidence-Based Trading Logic

The agent only trades when the LLM’s probability estimate diverges from the market price by more than the configured threshold. This prevents trading on markets where the agent has no edge.

const CONFIDENCE_THRESHOLD = parseFloat(process.env.CONFIDENCE_THRESHOLD ?? '0.15'); const MAX_ORDER_USD = parseFloat(process.env.AGENT_MAX_ORDER_USD ?? '5'); function evaluateTrade(market, analysis) { const { probability } = analysis; const yesPrice = market.yesPrice; const noPrice = market.noPrice; // If the LLM thinks YES is more likely than the market price suggests const yesEdge = probability - yesPrice; // If the LLM thinks NO is more likely than the market price suggests const noEdge = (1 - probability) - noPrice; if (yesEdge > CONFIDENCE_THRESHOLD) { return { side: 'BUY', tokenIndex: 0, // YES token (clobTokenIds[0]) edge: yesEdge, confidence: probability, reason: `LLM estimates ${(probability * 100).toFixed(1)}% YES vs market at ${(yesPrice * 100).toFixed(1)}%`, }; } if (noEdge > CONFIDENCE_THRESHOLD) { return { side: 'BUY', tokenIndex: 1, // NO token (clobTokenIds[1]) edge: noEdge, confidence: 1 - probability, reason: `LLM estimates ${((1 - probability) * 100).toFixed(1)}% NO vs market at ${(noPrice * 100).toFixed(1)}%`, }; } return null; // No trade — edge too small }

Step 4: Order Construction and Signing

Build the EIP-712 order and sign it via WaaP CLI. This is the same signing flow as the Signal Trader recipe, using waap-cli sign-typed-data so no private key touches the agent’s environment.

import { execSync } from 'child_process'; import { createHmac } from 'crypto'; const CTF_EXCHANGE = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E'; const CHAIN_ID = 137; function getWalletAddress() { const output = execSync('waap-cli whoami --json', { encoding: 'utf-8' }); // Parse JSON output — waap-cli --json may emit multiple lines const lines = output.split('\n').filter(l => l.trim().startsWith('{')); for (const line of lines) { try { const obj = JSON.parse(line); if (obj.evmWalletAddress) return obj.evmWalletAddress; } catch {} } // Fallback: extract hex address const match = output.match(/0x[a-fA-F0-9]{40}/); if (match) return match[0]; throw new Error('Could not determine wallet address from waap-cli whoami'); } function signWithWaaP(typedData) { const dataJson = JSON.stringify(typedData); const output = execSync( `waap-cli sign-typed-data --data '${dataJson.replace(/'/g, "'\\''")}'`, { encoding: 'utf-8' } ); // Output is plain text: "Signature: 0x..." const match = output.match(/Signature:\s*(0x[a-fA-F0-9]+)/); if (!match) throw new Error('Could not parse signature from waap-cli output'); return match[1]; } function buildOrder(walletAddress, tokenId, side, amountUsd) { const amountMicroUsdc = String(Math.floor(amountUsd * 1e6)); const orderMessage = { salt: Math.floor(Math.random() * 1_000_000_000), maker: walletAddress, signer: walletAddress, tokenId, makerAmount: amountMicroUsdc, takerAmount: amountMicroUsdc, expiration: '0', nonce: '0', feeRateBps: '0', side: side === 'BUY' ? 0 : 1, signatureType: 0, }; const typedData = { types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Order: [ { name: 'salt', type: 'uint256' }, { name: 'maker', type: 'address' }, { name: 'signer', type: 'address' }, { name: 'tokenId', type: 'uint256' }, { name: 'makerAmount', type: 'uint256' }, { name: 'takerAmount', type: 'uint256' }, { name: 'expiration', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'feeRateBps', type: 'uint256' }, { name: 'side', type: 'uint8' }, { name: 'signatureType', type: 'uint8' }, ], }, domain: { name: 'Polymarket CTF Exchange', version: '1', chainId: CHAIN_ID, verifyingContract: CTF_EXCHANGE, }, primaryType: 'Order', message: orderMessage, }; return { typedData, orderMessage }; }

Step 5: Order Submission

Submit the signed order to the Polymarket CLOB with authenticated headers.

function generatePmApiSign({ apiSecret, timestamp, method, requestPath, body }) { const normalizedSecret = apiSecret .replace(/-/g, '+') .replace(/_/g, '/') .replace(/[^A-Za-z0-9+/=]/g, ''); const secretKey = Buffer.from(normalizedSecret, 'base64'); const message = `${timestamp}${String(method).toUpperCase()}${requestPath}${body ?? ''}`; const digestBase64 = createHmac('sha256', secretKey) .update(message, 'utf8') .digest('base64'); return digestBase64.replace(/\+/g, '-').replace(/\//g, '_'); } async function submitOrder(orderMessage, signature, owner) { const requestPath = '/order'; const method = 'POST'; const timestamp = String(Date.now()); const requestBody = JSON.stringify({ order: orderMessage, owner, signature, }); const pmApiSign = generatePmApiSign({ apiSecret: process.env.POLY_API_SECRET, timestamp, method, requestPath, body: requestBody, }); const response = await fetch('https://clob.polymarket.com/order', { method, headers: { 'Content-Type': 'application/json', 'PM-API-KEY': process.env.POLY_API_KEY, 'PM-API-PASSPHRASE': process.env.POLY_PASSPHRASE, 'PM-API-TIMESTAMP': timestamp, 'PM-API-SIGN': pmApiSign, }, body: requestBody, }); return await response.json(); }

Full Working Code

Putting it all together in a single polling agent:

// index.js import 'dotenv/config'; import { execSync } from 'child_process'; import { createHmac } from 'crypto'; // --- Configuration --- const GAMMA_API_URL = 'https://gamma-api.polymarket.com'; const CLOB_API_URL = 'https://clob.polymarket.com'; const CTF_EXCHANGE = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E'; const CHAIN_ID = 137; const MAX_ORDER_USD = parseFloat(process.env.AGENT_MAX_ORDER_USD ?? '5'); const CONFIDENCE_THRESHOLD = parseFloat(process.env.CONFIDENCE_THRESHOLD ?? '0.15'); const POLL_INTERVAL_MS = parseInt(process.env.AGENT_POLL_INTERVAL_MS ?? '60000'); // --- Market Fetching --- async function getActiveMarkets() { const response = await fetch(`${GAMMA_API_URL}/events?active=true&closed=false&limit=10`); const events = await response.json(); const markets = []; for (const event of events) { for (const market of event.markets ?? []) { if (!market.clobTokenIds) continue; const prices = market.outcomePrices ? JSON.parse(market.outcomePrices) : null; const tokenIds = JSON.parse(market.clobTokenIds); markets.push({ eventTitle: event.title, question: market.question, marketId: market.id, tokenIds, yesPrice: prices ? parseFloat(prices[0]) : null, noPrice: prices ? parseFloat(prices[1]) : null, }); } } return markets; } // --- LLM Analysis --- async function analyzeWithClaude(market) { const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: 'claude-sonnet-4-20250514', max_tokens: 1024, system: 'You are a prediction market analyst. Given a market question and current prices, estimate the probability of YES. Respond with ONLY valid JSON: {"probability": <0.0-1.0>, "reasoning": "<brief explanation>"}', messages: [{ role: 'user', content: `Market: ${market.question}\nCurrent YES price: ${market.yesPrice}\nCurrent NO price: ${market.noPrice}\n\nWhat is your estimated probability that this resolves YES?`, }], }), }); const data = await response.json(); return JSON.parse(data.content[0].text); } async function analyzeWithOpenAI(market) { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: 'gpt-4o', response_format: { type: 'json_object' }, messages: [ { role: 'system', content: 'You are a prediction market analyst. Given a market question and current prices, estimate the probability of YES. Respond with ONLY valid JSON: {"probability": <0.0-1.0>, "reasoning": "<brief explanation>"}' }, { role: 'user', content: `Market: ${market.question}\nCurrent YES price: ${market.yesPrice}\nCurrent NO price: ${market.noPrice}\n\nWhat is your estimated probability that this resolves YES?` }, ], }), }); const data = await response.json(); return JSON.parse(data.choices[0].message.content); } async function analyzeMarket(market) { if (process.env.ANTHROPIC_API_KEY) return analyzeWithClaude(market); if (process.env.OPENAI_API_KEY) return analyzeWithOpenAI(market); throw new Error('No LLM API key configured.'); } // --- Trading Logic --- function evaluateTrade(market, analysis) { const { probability } = analysis; const yesEdge = probability - market.yesPrice; const noEdge = (1 - probability) - market.noPrice; if (yesEdge > CONFIDENCE_THRESHOLD) { return { side: 'BUY', tokenIndex: 0, edge: yesEdge, confidence: probability }; } if (noEdge > CONFIDENCE_THRESHOLD) { return { side: 'BUY', tokenIndex: 1, edge: noEdge, confidence: 1 - probability }; } return null; } // --- WaaP CLI Helpers --- function getWalletAddress() { const output = execSync('waap-cli whoami --json', { encoding: 'utf-8' }); const lines = output.split('\n').filter(l => l.trim().startsWith('{')); for (const line of lines) { try { const obj = JSON.parse(line); if (obj.evmWalletAddress) return obj.evmWalletAddress; } catch {} } throw new Error('Could not determine wallet address'); } function signWithWaaP(typedData) { const dataJson = JSON.stringify(typedData); const output = execSync( `waap-cli sign-typed-data --data '${dataJson.replace(/'/g, "'\\''")}'`, { encoding: 'utf-8' } ); const match = output.match(/Signature:\s*(0x[a-fA-F0-9]+)/); if (!match) throw new Error('Could not parse signature from waap-cli output'); return match[1]; } // --- Order Building and Submission --- function buildOrder(walletAddress, tokenId, side, amountUsd) { const amountMicroUsdc = String(Math.floor(amountUsd * 1e6)); const orderMessage = { salt: Math.floor(Math.random() * 1_000_000_000), maker: walletAddress, signer: walletAddress, tokenId, makerAmount: amountMicroUsdc, takerAmount: amountMicroUsdc, expiration: '0', nonce: '0', feeRateBps: '0', side: side === 'BUY' ? 0 : 1, signatureType: 0, }; const typedData = { types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Order: [ { name: 'salt', type: 'uint256' }, { name: 'maker', type: 'address' }, { name: 'signer', type: 'address' }, { name: 'tokenId', type: 'uint256' }, { name: 'makerAmount', type: 'uint256' }, { name: 'takerAmount', type: 'uint256' }, { name: 'expiration', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'feeRateBps', type: 'uint256' }, { name: 'side', type: 'uint8' }, { name: 'signatureType', type: 'uint8' }, ], }, domain: { name: 'Polymarket CTF Exchange', version: '1', chainId: CHAIN_ID, verifyingContract: CTF_EXCHANGE, }, primaryType: 'Order', message: orderMessage, }; return { typedData, orderMessage }; } function generatePmApiSign({ apiSecret, timestamp, method, requestPath, body }) { const normalizedSecret = apiSecret.replace(/-/g, '+').replace(/_/g, '/').replace(/[^A-Za-z0-9+/=]/g, ''); const secretKey = Buffer.from(normalizedSecret, 'base64'); const message = `${timestamp}${String(method).toUpperCase()}${requestPath}${body ?? ''}`; const digestBase64 = createHmac('sha256', secretKey).update(message, 'utf8').digest('base64'); return digestBase64.replace(/\+/g, '-').replace(/\//g, '_'); } async function submitOrder(orderMessage, signature, owner) { const requestPath = '/order'; const timestamp = String(Date.now()); const requestBody = JSON.stringify({ order: orderMessage, owner, signature }); const pmApiSign = generatePmApiSign({ apiSecret: process.env.POLY_API_SECRET, timestamp, method: 'POST', requestPath, body: requestBody, }); const response = await fetch(`${CLOB_API_URL}${requestPath}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'PM-API-KEY': process.env.POLY_API_KEY, 'PM-API-PASSPHRASE': process.env.POLY_PASSPHRASE, 'PM-API-TIMESTAMP': timestamp, 'PM-API-SIGN': pmApiSign, }, body: requestBody, }); return await response.json(); } // --- Main Loop --- async function main() { const walletAddress = getWalletAddress(); console.log(`Agent wallet: ${walletAddress}`); console.log(`Confidence threshold: ${CONFIDENCE_THRESHOLD}, Max order: $${MAX_ORDER_USD}`); while (true) { try { const markets = await getActiveMarkets(); console.log(`Scanning ${markets.length} markets...`); for (const market of markets) { if (market.yesPrice === null) continue; const analysis = await analyzeMarket(market); console.log(`${market.question}: LLM says ${(analysis.probability * 100).toFixed(1)}% YES (market: ${(market.yesPrice * 100).toFixed(1)}%)`); console.log(` Reasoning: ${analysis.reasoning}`); const trade = evaluateTrade(market, analysis); if (!trade) { console.log(' No edge found, skipping.'); continue; } const tokenId = market.tokenIds[trade.tokenIndex]; const outcome = trade.tokenIndex === 0 ? 'YES' : 'NO'; console.log(` TRADE: ${trade.side} ${outcome} token, edge: ${(trade.edge * 100).toFixed(1)}%`); const { typedData, orderMessage } = buildOrder(walletAddress, tokenId, trade.side, MAX_ORDER_USD); const signature = signWithWaaP(typedData); const result = await submitOrder(orderMessage, signature, walletAddress); console.log(' Order result:', JSON.stringify(result)); } } catch (err) { console.error('Tick error:', err.message); } console.log(`Sleeping ${POLL_INTERVAL_MS / 1000}s...`); await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); } } main().catch(err => { console.error('Fatal:', err); process.exit(1); });

Next Steps

  • Add news context: Supplement the LLM prompt with recent news articles for more informed predictions.
  • Track performance: Log trades and resolution outcomes to measure the agent’s prediction accuracy over time.
  • Scale position sizing: Adjust order size based on the edge magnitude — bet more when the confidence gap is larger.
  • Set Privileges: Use waap-cli policy set --daily-spend-limit 50 to cap the agent’s daily exposure.
  • Multi-model consensus: Run both Claude and GPT, only trade when both models agree.
Last updated on