skills/aradotso/trending-skills/polymarket-arbitrage-trading-bot

polymarket-arbitrage-trading-bot

SKILL.md

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
npm run dev Run via ts-node (development, no build needed)
npm run build Compile TypeScript to dist/
npm run typecheck Type-check without emitting output
npm run clean Remove dist/ directory
npm run sim Simulation mode — logs trades, no real orders
npm run prod Production mode — places real CLOB orders
npm start Run compiled output (defaults to simulation unless --production passed)

Configuration (.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)

import * 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
Failed to fetch market/orderbook API/network error Temporary; check GAMMA_API_URL / CLOB_API_URL connectivity, retries are built in
Orders fail in production Wrong auth config Verify PRIVATE_KEY, SIGNATURE_TYPE, and PROXY_WALLET_ADDRESS match your Polymarket account
No market found for asset Round gap or unsupported asset Only use btc, eth, sol, xrp; wait for next 15m round to start
Bot never triggers leg 1 Threshold too high or quiet market Lower DUMP_HEDGE_MOVE_THRESHOLD or increase DUMP_HEDGE_WINDOW_MINUTES
Combined cost always above target Market conditions Lower DUMP_HEDGE_SUM_TARGET or adjust DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES
Cannot find module errors Missing build step Run npm run build before npm start / npm run prod
Simulation not placing orders Expected behavior Simulation mode logs only; switch to --production for real orders

Safety Checklist

  1. Always simulate first — run npm run sim across multiple rounds and inspect history.toml
  2. Start small — use low DUMP_HEDGE_SHARES (e.g. 1) in first production runs
  3. Secure credentials — never commit .env to version control; add it to .gitignore
  4. Monitor stop-loss behavior — tune DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES carefully; forced hedges at bad prices reduce edge
  5. Polygon USDC — ensure sufficient USDC balance on Polygon before running production
  6. Round timing — the bot auto-rolls to the next round; verify rollover logs look correct in simulation first
Weekly Installs
28
GitHub Stars
11
First Seen
1 day ago
Installed on
opencode28
gemini-cli28
deepagents28
antigravity28
github-copilot28
codex28