Trending-skills polymarket-arbitrage-trading-bot
Automated dump-and-hedge arbitrage trading bot for Polymarket's 15-minute crypto Up/Down markets, supporting BTC, ETH, SOL, and XRP.
install
source · Clone the upstream repo
git clone https://github.com/Aradotso/trending-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Aradotso/trending-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/polymarket-arbitrage-trading-bot" ~/.claude/skills/aradotso-trending-skills-polymarket-arbitrage-trading-bot && rm -rf "$T"
manifest:
skills/polymarket-arbitrage-trading-bot/SKILL.mdsource content
Polymarket Arbitrage Trading Bot
Skill by ara.so — Daily 2026 Skills collection.
Automated dump-and-hedge arbitrage bot for Polymarket's 15-minute crypto Up/Down prediction markets. Written in TypeScript using the official
@polymarket/clob-client. Watches BTC, ETH, SOL, and XRP markets for sharp price drops on one leg, then buys both legs when combined cost falls below a target threshold to lock in a structural edge before resolution.
Installation
git clone https://github.com/apechurch/polymarket-arbitrage-trading-bot.git cd polymarket-arbitrage-trading-bot npm install cp .env.example .env # Configure .env — see Configuration section npm run build
Requirements: Node.js 16+, USDC on Polygon (for live trading), a Polymarket-compatible wallet.
Project Structure
src/ main.ts # Entry point: market discovery, monitors, period rollover monitor.ts # Price polling & snapshots dumpHedgeTrader.ts # Core strategy: dump → hedge → stop-loss → settlement api.ts # Gamma API, CLOB API, order placement, redemption config.ts # Environment variable loading models.ts # Shared TypeScript types logger.ts # History file (history.toml) + stderr logging
Key Commands
| Command | Purpose |
|---|---|
| Run via (development, no build needed) |
| Compile TypeScript to |
| Type-check without emitting output |
| Remove directory |
| Simulation mode — logs trades, no real orders |
| Production mode — places real CLOB orders |
| Run compiled output (defaults to simulation unless passed) |
Configuration (.env
)
.env# Wallet / Auth PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE PROXY_WALLET_ADDRESS=0xYOUR_PROXY_WALLET SIGNATURE_TYPE=2 # 0=EOA, 1=Proxy, 2=Gnosis Safe # Markets to trade (comma-separated) MARKETS=btc,eth,sol,xrp # Polling CHECK_INTERVAL_MS=1000 # Strategy thresholds DUMP_HEDGE_SHARES=10 # Shares per leg DUMP_HEDGE_SUM_TARGET=0.95 # Max combined price for both legs DUMP_HEDGE_MOVE_THRESHOLD=0.15 # Min fractional drop to trigger (15%) DUMP_HEDGE_WINDOW_MINUTES=5 # Only detect dumps in first N minutes of round DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=8 # Force stop-loss hedge after N minutes # Mode flag (use --production CLI flag for live trading) PRODUCTION=false # Optional API overrides GAMMA_API_URL=https://gamma-api.polymarket.com CLOB_API_URL=https://clob.polymarket.com API_KEY= API_SECRET= API_PASSPHRASE=
Strategy Overview
New 15m round starts │ ▼ Watch first DUMP_HEDGE_WINDOW_MINUTES minutes │ ├── Up or Down leg drops ≥ DUMP_HEDGE_MOVE_THRESHOLD? │ │ │ ▼ │ Buy dumped leg (Leg 1) │ │ │ ├── Opposite ask cheap enough? │ │ (leg1_entry + opposite_ask ≤ DUMP_HEDGE_SUM_TARGET) │ │ │ │ │ ▼ │ │ Buy hedge leg (Leg 2) → locked-in edge │ │ │ └── Timeout (DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES)? │ │ │ ▼ │ Execute stop-loss hedge │ └── Round ends → settle winners, redeem on-chain (production)
Code Examples
Loading Config (src/config.ts
pattern)
src/config.tsimport * as dotenv from 'dotenv'; dotenv.config(); export const config = { privateKey: process.env.PRIVATE_KEY!, proxyWalletAddress: process.env.PROXY_WALLET_ADDRESS ?? '', signatureType: parseInt(process.env.SIGNATURE_TYPE ?? '2', 10), markets: (process.env.MARKETS ?? 'btc').split(',').map(m => m.trim()), checkIntervalMs: parseInt(process.env.CHECK_INTERVAL_MS ?? '1000', 10), dumpHedgeShares: parseFloat(process.env.DUMP_HEDGE_SHARES ?? '10'), dumpHedgeSumTarget: parseFloat(process.env.DUMP_HEDGE_SUM_TARGET ?? '0.95'), dumpHedgeMoveThreshold: parseFloat(process.env.DUMP_HEDGE_MOVE_THRESHOLD ?? '0.15'), dumpHedgeWindowMinutes: parseInt(process.env.DUMP_HEDGE_WINDOW_MINUTES ?? '5', 10), dumpHedgeStopLossMaxWaitMinutes: parseInt( process.env.DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES ?? '8', 10 ), production: process.env.PRODUCTION === 'true', };
Initializing the CLOB Client
import { ClobClient } from '@polymarket/clob-client'; import { ethers } from 'ethers'; import { config } from './config'; function createClobClient(): ClobClient { const wallet = new ethers.Wallet(config.privateKey); return new ClobClient( config.clobApiUrl, // e.g. 'https://clob.polymarket.com' 137, // Polygon chain ID wallet, undefined, // credentials (set after key derivation if needed) config.signatureType, config.proxyWalletAddress ); }
Discovering the Active 15-Minute Market
import axios from 'axios'; interface GammaMarket { conditionId: string; question: string; endDateIso: string; active: boolean; tokens: Array<{ outcome: string; token_id: string }>; } async function findActive15mMarket(asset: string): Promise<GammaMarket | null> { const tag = `${asset.toUpperCase()}-15m`; const resp = await axios.get(`${config.gammaApiUrl}/markets`, { params: { tag, active: true, limit: 5 } }); const markets: GammaMarket[] = resp.data; // Return the earliest-closing active market return markets.sort( (a, b) => new Date(a.endDateIso).getTime() - new Date(b.endDateIso).getTime() )[0] ?? null; }
Fetching Best Ask Price from CLOB
async function getBestAsk(tokenId: string): Promise<number | null> { try { const resp = await axios.get(`${config.clobApiUrl}/book`, { params: { token_id: tokenId } }); const asks: Array<{ price: string; size: string }> = resp.data.asks ?? []; if (asks.length === 0) return null; // Best ask = lowest price return Math.min(...asks.map(a => parseFloat(a.price))); } catch { return null; } }
Dump Detection Logic
interface PriceSnapshot { timestamp: number; ask: number; } function detectDump( history: PriceSnapshot[], currentAsk: number, threshold: number, windowMs: number ): boolean { const cutoff = Date.now() - windowMs; const recent = history.filter(s => s.timestamp >= cutoff); if (recent.length === 0) return false; const highestRecentAsk = Math.max(...recent.map(s => s.ask)); const drop = (highestRecentAsk - currentAsk) / highestRecentAsk; return drop >= threshold; } // Usage: const windowMs = config.dumpHedgeWindowMinutes * 60 * 1000; const isDump = detectDump( priceHistory, currentAsk, config.dumpHedgeMoveThreshold, windowMs );
Placing a Market Buy Order (Production)
import { ClobClient, OrderType, Side } from '@polymarket/clob-client'; async function buyShares( client: ClobClient, tokenId: string, price: number, shares: number, simulate: boolean ): Promise<string | null> { if (simulate) { console.error(`[SIM] BUY ${shares} shares @ ${price} token=${tokenId}`); return 'sim-order-id'; } const order = await client.createOrder({ tokenID: tokenId, price, size: shares, side: Side.BUY, orderType: OrderType.FOK, // Fill-or-Kill for immediate execution }); const resp = await client.postOrder(order); return resp.orderID ?? null; }
Core Dump-Hedge Cycle
interface LegState { filled: boolean; tokenId: string; entryPrice: number | null; orderId: string | null; } async function runDumpHedgeCycle( client: ClobClient, upTokenId: string, downTokenId: string, simulate: boolean ): Promise<void> { const leg1: LegState = { filled: false, tokenId: '', entryPrice: null, orderId: null }; const leg2: LegState = { filled: false, tokenId: '', entryPrice: null, orderId: null }; const startTime = Date.now(); const windowMs = config.dumpHedgeWindowMinutes * 60 * 1000; const stopLossMs = config.dumpHedgeStopLossMaxWaitMinutes * 60 * 1000; const priceHistory: Record<string, PriceSnapshot[]> = { [upTokenId]: [], [downTokenId]: [] }; const interval = setInterval(async () => { const elapsed = Date.now() - startTime; const upAsk = await getBestAsk(upTokenId); const downAsk = await getBestAsk(downTokenId); if (upAsk == null || downAsk == null) return; // Record history const now = Date.now(); priceHistory[upTokenId].push({ timestamp: now, ask: upAsk }); priceHistory[downTokenId].push({ timestamp: now, ask: downAsk }); // === LEG 1: Detect dump, buy dumped leg === if (!leg1.filled && elapsed <= windowMs) { const upDumped = detectDump( priceHistory[upTokenId], upAsk, config.dumpHedgeMoveThreshold, windowMs ); const downDumped = detectDump( priceHistory[downTokenId], downAsk, config.dumpHedgeMoveThreshold, windowMs ); if (upDumped || downDumped) { const dumpedToken = upDumped ? upTokenId : downTokenId; const dumpedAsk = upDumped ? upAsk : downAsk; leg1.tokenId = dumpedToken; leg1.entryPrice = dumpedAsk; leg1.orderId = await buyShares( client, dumpedToken, dumpedAsk, config.dumpHedgeShares, simulate ); leg1.filled = true; console.error(`[LEG1] Bought dumped leg @ ${dumpedAsk}`); } } // === LEG 2: Hedge when sum is favorable === if (leg1.filled && !leg2.filled) { const hedgeToken = leg1.tokenId === upTokenId ? downTokenId : upTokenId; const hedgeAsk = leg1.tokenId === upTokenId ? downAsk : upAsk; const combinedCost = leg1.entryPrice! + hedgeAsk; const shouldHedge = combinedCost <= config.dumpHedgeSumTarget || elapsed >= stopLossMs; // Stop-loss: force hedge on timeout if (shouldHedge) { const label = combinedCost <= config.dumpHedgeSumTarget ? 'HEDGE' : 'STOP-LOSS'; leg2.tokenId = hedgeToken; leg2.entryPrice = hedgeAsk; leg2.orderId = await buyShares( client, hedgeToken, hedgeAsk, config.dumpHedgeShares, simulate ); leg2.filled = true; console.error(`[LEG2:${label}] Bought hedge @ ${hedgeAsk}, combined=${combinedCost}`); clearInterval(interval); } } }, config.checkIntervalMs); }
Settlement and Redemption
async function settleRound( client: ClobClient, conditionId: string, winningTokenId: string, simulate: boolean ): Promise<void> { if (simulate) { console.error(`[SIM] Would redeem winning token ${winningTokenId}`); return; } // Redeem via CLOB client (CTF redemption on Polygon) await client.redeemPositions({ conditionId, amounts: [{ tokenId: winningTokenId, amount: config.dumpHedgeShares }] }); console.error(`[SETTLE] Redeemed ${config.dumpHedgeShares} shares for ${winningTokenId}`); }
Running Modes
Simulation (Recommended First)
# Via npm script npm run sim # Or directly with flag node dist/main.js --simulation # Monitor output tail -f history.toml
Production (Live Trading)
# Ensure .env has correct PRIVATE_KEY, PROXY_WALLET_ADDRESS, SIGNATURE_TYPE npm run prod # Or: PRODUCTION=true node dist/main.js --production
Single Asset, Custom Thresholds
MARKETS=btc \ DUMP_HEDGE_MOVE_THRESHOLD=0.12 \ DUMP_HEDGE_SUM_TARGET=0.93 \ DUMP_HEDGE_SHARES=5 \ npm run prod
Common Patterns
Multi-Asset Parallel Monitoring
// main.ts pattern: spin up one monitor per asset import { config } from './config'; async function main() { const isProduction = process.argv.includes('--production') || config.production; await Promise.all( config.markets.map(asset => runAssetMonitor(asset, isProduction) ) ); } async function runAssetMonitor(asset: string, production: boolean) { while (true) { const market = await findActive15mMarket(asset); if (!market) { console.error(`[${asset}] No active market, retrying in 30s`); await sleep(30_000); continue; } const [upToken, downToken] = market.tokens; const client = createClobClient(); await runDumpHedgeCycle(client, upToken.token_id, downToken.token_id, !production); // Wait for round end, then loop for next round const roundEnd = new Date(market.endDateIso).getTime(); await sleep(Math.max(0, roundEnd - Date.now() + 5_000)); } } function sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); } main().catch(console.error);
Logging to history.toml
import * as fs from 'fs'; interface TradeRecord { asset: string; roundEnd: string; leg1Price: number; leg2Price: number; combined: number; target: number; mode: 'hedge' | 'stop-loss'; timestamp: string; } function appendHistory(record: TradeRecord): void { const entry = ` [[trade]] asset = "${record.asset}" round_end = "${record.roundEnd}" leg1_price = ${record.leg1Price} leg2_price = ${record.leg2Price} combined = ${record.combined} target = ${record.target} mode = "${record.mode}" timestamp = "${record.timestamp}" `; fs.appendFileSync('history.toml', entry, 'utf8'); }
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| API/network error | Temporary; check / connectivity, retries are built in |
| Orders fail in production | Wrong auth config | Verify , , and match your Polymarket account |
| No market found for asset | Round gap or unsupported asset | Only use , , , ; wait for next 15m round to start |
| Bot never triggers leg 1 | Threshold too high or quiet market | Lower or increase |
| Combined cost always above target | Market conditions | Lower or adjust |
errors | Missing build step | Run before / |
| Simulation not placing orders | Expected behavior | Simulation mode logs only; switch to for real orders |
Safety Checklist
- Always simulate first — run
across multiple rounds and inspectnpm run simhistory.toml - Start small — use low
(e.g.DUMP_HEDGE_SHARES
) in first production runs1 - Secure credentials — never commit
to version control; add it to.env.gitignore - Monitor stop-loss behavior — tune
carefully; forced hedges at bad prices reduce edgeDUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES - Polygon USDC — ensure sufficient USDC balance on Polygon before running production
- Round timing — the bot auto-rolls to the next round; verify rollover logs look correct in simulation first