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:
- Fetch active prediction markets from the Polymarket Gamma API.
- Send market details to an LLM (Anthropic Claude or OpenAI GPT) for analysis.
- Compare the LLM’s confidence score against current market prices.
- Place trades when the confidence gap exceeds a configurable threshold.
- Sign all orders via
waap-cli sign-typed-datausing 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 dotenvCreate 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.15Step 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 50to cap the agent’s daily exposure. - Multi-model consensus: Run both Claude and GPT, only trade when both models agree.
Related
- Polymarket Signal Trader — Base recipe for CLOB setup and order signing
- WaaP CLI and Skills — CLI command reference and agent workflows
- Privileges — Pre-approved spending scopes for automated flows
- Send Transactions — Transaction lifecycle, async flow, and events