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.

INSTALLATION
npx skills add https://github.com/aradotso/trending-skills --skill polymarket-arbitrage-trading-bot
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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 &#x26;&#x26; 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 &#x26;&#x26; !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

  • Always simulate first — run npm run sim across multiple rounds and inspect history.toml
  • Start small — use low DUMP_HEDGE_SHARES (e.g. 1) in first production runs
  • Secure credentials — never commit .env to version control; add it to .gitignore
  • Monitor stop-loss behavior — tune DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES carefully; forced hedges at bad prices reduce edge
  • 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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card