Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
aradotso avatar

Polymarket Arbitrage Bot

  • 767 installs
  • 66 repo stars
  • Updated July 9, 2026
  • aradotso/trending-skills

polymarket-arbitrage-bot is a Claude Code TypeScript skill that automates a dump-and-hedge arbitrage strategy on Polymarket 15-minute Up/Down prediction markets with CLOB order execution and simulation mode for developer

About

polymarket-arbitrage-bot is a TypeScript Claude Code skill from aradotso’s trending-skills collection that automates dump-and-hedge arbitrage on Polymarket’s 15-minute Up/Down prediction markets. It wires CLOB order execution, strategy configuration, and a simulation mode so developers can test fills before live trading. Trigger phrases cover bot setup, hedge strategy tuning, and CLOB TypeScript integration. Reach for this skill when implementing automated Polymarket market-making or arbitrage bots rather than writing CLOB client boilerplate from scratch. The skill encodes strategy parameters and execution flow for Up/Down short-window markets.

  • Implements dump-and-hedge arbitrage on Polymarket 15-minute BTC, ETH, SOL, XRP markets
  • Executes via Polymarket CLOB order API with real-time price drop detection
  • Includes simulation mode (`npm run sim`) before live trading
  • Supports production mode with `PRODUCTION=true` environment variable
  • Provides 6 ready-to-run npm scripts for dev, build, sim, and prod workflows

Polymarket Arbitrage Bot by the numbers

  • 767 all-time installs (skills.sh)
  • +6 installs in the week ending Jul 20, 2026 (Skillselion tracking)
  • Ranked #336 of 2,742 Automation & Workflows skills by installs in the Skillselion catalog
  • Security screen: HIGH risk (skills.sh audit)
  • Data as of Jul 20, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aradotso/trending-skills --skill polymarket-arbitrage-bot

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs767
repo stars66
Security audit0 / 3 scanners passed
Last updatedJuly 9, 2026
Repositoryaradotso/trending-skills

How do you build a Polymarket arbitrage bot in TypeScript?

Automate a dump-and-hedge arbitrage strategy on Polymarket's 15-minute Up/Down prediction markets.

Who is it for?

TypeScript developers implementing automated dump-and-hedge arbitrage on Polymarket 15-minute Up/Down CLOB markets.

Skip if: Developers seeking general DeFi yield strategies or manual trading UIs without Polymarket CLOB API integration.

When should I use this skill?

A developer asks to set up, configure, or simulate a Polymarket arbitrage or CLOB trading bot in TypeScript.

What you get

Configured TypeScript trading bot, CLOB order execution logic, and simulation-mode test runs for 15-minute markets.

  • configured trading bot
  • CLOB execution scripts
  • simulation test results

Files

SKILL.mdMarkdownGitHub ↗

Polymarket Arbitrage Bot

Skill by ara.so — Daily 2026 Skills collection.

TypeScript bot automating the dump-and-hedge strategy on Polymarket's 15-minute Up/Down markets (BTC, ETH, SOL, XRP). Detects sharp price drops, buys the dipped side, then hedges the opposite outcome when combined cost falls below a profit threshold.

Installation

git clone https://github.com/infraform/polymarket-arbitrage-bot.git
cd polymarket-arbitrage-bot
npm install
npm run build
cp .env.example .env

Key Commands

CommandDescription
npm startRun compiled bot (simulation by default)
npm run simExplicitly run in simulation (no real orders)
npm run prodRun with real trades (PRODUCTION=true)
npm run devRun TypeScript directly via ts-node
npm run buildCompile TypeScript to dist/

Always test with `npm run sim` before enabling production mode.

Project Structure

src/
├── main.ts            # Entry point, config load, market discovery, wiring
├── config.ts          # Loads/validates .env into typed config
├── api.ts             # Gamma + CLOB API client (markets, orderbook, orders, redemption)
├── monitor.ts         # Orderbook snapshot polling, strategy callback driver
├── dumpHedgeTrader.ts # Dump detection, leg1/leg2, stop-loss, P&L tracking
├── models.ts          # Shared types: Market, OrderBook, TokenPrice, etc.
└── logger.ts          # history.toml append log + stderr output

Environment Configuration

Create .env from .env.example:

# --- Wallet & Auth (required for production) ---
PRIVATE_KEY=0x_your_private_key_here
PROXY_WALLET_ADDRESS=0x_your_proxy_wallet_address
SIGNATURE_TYPE=2                        # 0=EOA, 1=Proxy, 2=GnosisSafe

# --- Optional explicit CLOB API credentials ---
# If not set, credentials are derived from signer automatically
API_KEY=
API_SECRET=
API_PASSPHRASE=

# --- API Endpoints (defaults are production Polymarket) ---
GAMMA_API_URL=https://gamma-api.polymarket.com
CLOB_API_URL=https://clob.polymarket.com

# --- Markets ---
MARKETS=btc                             # comma-separated: btc,eth,sol,xrp

# --- Polling ---
CHECK_INTERVAL_MS=1000
MARKET_CLOSURE_CHECK_INTERVAL_SECONDS=20

# --- Strategy Parameters ---
DUMP_HEDGE_SHARES=10                    # Shares per leg
DUMP_HEDGE_SUM_TARGET=0.95             # Hedge when leg1 + opposite_ask <= this
DUMP_HEDGE_MOVE_THRESHOLD=0.15         # 15% drop triggers dump detection
DUMP_HEDGE_WINDOW_MINUTES=2            # Watch window at period start
DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=5
DUMP_HEDGE_STOP_LOSS_PERCENTAGE=0.2

# --- Mode ---
PRODUCTION=false                        # true = real trades

Core Types (models.ts)

// Key shared types used throughout the bot
interface Market {
  conditionId: string;
  questionId: string;
  tokens: Token[];         // [upToken, downToken]
  startTime: number;
  endTime: number;
  asset: string;           // "BTC", "ETH", etc.
}

interface Token {
  tokenId: string;
  outcome: string;         // "Up" or "Down"
}

interface OrderBook {
  tokenId: string;
  outcome: string;
  bids: PriceLevel[];
  asks: PriceLevel[];
  bestBid: number;
  bestAsk: number;
}

interface TokenPrice {
  tokenId: string;
  outcome: string;
  bestBid: number;
  bestAsk: number;
  timestamp: number;
}

interface MarketSnapshot {
  upPrice: TokenPrice;
  downPrice: TokenPrice;
  timeRemainingSeconds: number;
  periodStart: number;
}

Strategy Flow

1. Discovery  → Gamma API finds current 15m market slug for each asset
2. Monitor    → Poll CLOB orderbooks every CHECK_INTERVAL_MS
3. Watch      → First DUMP_HEDGE_WINDOW_MINUTES: detect if ask drops >= MOVE_THRESHOLD
4. Leg 1      → Buy DUMP_HEDGE_SHARES of dumped side at current ask
5. Wait       → Watch for: leg1_entry + opposite_ask <= DUMP_HEDGE_SUM_TARGET
6. Leg 2      → Buy DUMP_HEDGE_SHARES of opposite outcome (hedge)
7. Stop-loss  → If hedge not triggered within STOP_LOSS_MAX_WAIT_MINUTES, hedge anyway
8. Rollover   → New 15m period → discover new market, reset state
9. Closure    → Redeem winning tokens (production), log P&L

Code Examples

Loading and using config (config.ts pattern)

import * as dotenv from 'dotenv';
dotenv.config();

interface BotConfig {
  privateKey: string;
  proxyWalletAddress: string | undefined;
  signatureType: number;
  markets: string[];
  production: boolean;
  checkIntervalMs: number;
  dumpHedgeShares: number;
  dumpHedgeSumTarget: number;
  dumpHedgeMoveThreshold: number;
  dumpHedgeWindowMinutes: number;
  stopLossMaxWaitMinutes: number;
  stopLossPercentage: number;
  gammaApiUrl: string;
  clobApiUrl: string;
}

function loadConfig(): BotConfig {
  return {
    privateKey: process.env.PRIVATE_KEY ?? '',
    proxyWalletAddress: process.env.PROXY_WALLET_ADDRESS,
    signatureType: parseInt(process.env.SIGNATURE_TYPE ?? '2'),
    markets: (process.env.MARKETS ?? 'btc').split(',').map(m => m.trim()),
    production: process.env.PRODUCTION === 'true',
    checkIntervalMs: parseInt(process.env.CHECK_INTERVAL_MS ?? '1000'),
    dumpHedgeShares: parseInt(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: parseFloat(process.env.DUMP_HEDGE_WINDOW_MINUTES ?? '2'),
    stopLossMaxWaitMinutes: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES ?? '5'),
    stopLossPercentage: parseFloat(process.env.DUMP_HEDGE_STOP_LOSS_PERCENTAGE ?? '0.2'),
    gammaApiUrl: process.env.GAMMA_API_URL ?? 'https://gamma-api.polymarket.com',
    clobApiUrl: process.env.CLOB_API_URL ?? 'https://clob.polymarket.com',
  };
}

Fetching market via Gamma API (api.ts pattern)

import axios from 'axios';

// Find current 15m market for an asset
async function findCurrentMarket(
  gammaApiUrl: string,
  asset: string  // "btc", "eth", "sol", "xrp"
): Promise<Market | null> {
  // Polymarket 15m slug format: btc-updown-15m-<period_timestamp>
  // Round current time down to nearest 15m period
  const now = Math.floor(Date.now() / 1000);
  const periodStart = now - (now % (15 * 60));
  const slug = `${asset}-updown-15m-${periodStart}`;

  try {
    const response = await axios.get(`${gammaApiUrl}/markets`, {
      params: { slug }
    });
    const markets = response.data;
    if (!markets || markets.length === 0) return null;
    return markets[0] as Market;
  } catch (err) {
    console.error(`[${asset}] Market discovery failed:`, err);
    return null;
  }
}

Fetching orderbook from CLOB (api.ts pattern)

async function getOrderBook(
  clobApiUrl: string,
  tokenId: string
): Promise<OrderBook | null> {
  try {
    const response = await axios.get(`${clobApiUrl}/book`, {
      params: { token_id: tokenId }
    });
    const data = response.data;
    
    const bestBid = data.bids?.length > 0 
      ? Math.max(...data.bids.map((b: any) => parseFloat(b.price))) 
      : 0;
    const bestAsk = data.asks?.length > 0 
      ? Math.min(...data.asks.map((a: any) => parseFloat(a.price))) 
      : 1;
    
    return {
      tokenId,
      outcome: data.outcome ?? '',
      bids: data.bids ?? [],
      asks: data.asks ?? [],
      bestBid,
      bestAsk,
    };
  } catch (err) {
    console.error(`OrderBook fetch failed for ${tokenId}:`, err);
    return null;
  }
}

Dump detection logic (dumpHedgeTrader.ts pattern)

interface DumpHedgeState {
  phase: 'watching' | 'leg1_placed' | 'hedging' | 'closed';
  leg1Outcome?: 'Up' | 'Down';
  leg1EntryPrice?: number;
  leg1PlacedAt?: number;
  leg1TokenId?: string;
  hedgeTokenId?: string;
  periodStart: number;
}

function detectDump(
  snapshot: MarketSnapshot,
  priceHistory: TokenPrice[],
  config: BotConfig
): 'Up' | 'Down' | null {
  const now = Date.now() / 1000;
  const windowStart = snapshot.periodStart;
  const windowEnd = windowStart + config.dumpHedgeWindowMinutes * 60;

  // Only detect within watch window
  if (now > windowEnd) return null;

  // Get earliest prices in window for comparison
  const windowHistory = priceHistory.filter(p => p.timestamp >= windowStart);
  if (windowHistory.length < 2) return null;

  const earliest = windowHistory[0];
  const current = snapshot;

  // Check Up side dump
  if (earliest.upPrice.bestAsk > 0) {
    const upDrop = (earliest.upPrice.bestAsk - current.upPrice.bestAsk) / earliest.upPrice.bestAsk;
    if (upDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[DUMP] Up side dropped ${(upDrop * 100).toFixed(1)}%`);
      return 'Up';
    }
  }

  // Check Down side dump
  if (earliest.downPrice.bestAsk > 0) {
    const downDrop = (earliest.downPrice.bestAsk - current.downPrice.bestAsk) / earliest.downPrice.bestAsk;
    if (downDrop >= config.dumpHedgeMoveThreshold) {
      console.error(`[DUMP] Down side dropped ${(downDrop * 100).toFixed(1)}%`);
      return 'Down';
    }
  }

  return null;
}

function shouldHedge(
  state: DumpHedgeState,
  snapshot: MarketSnapshot,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1EntryPrice) return false;

  const oppositeAsk = state.leg1Outcome === 'Up'
    ? snapshot.downPrice.bestAsk
    : snapshot.upPrice.bestAsk;

  const combinedCost = state.leg1EntryPrice + oppositeAsk;
  return combinedCost <= config.dumpHedgeSumTarget;
}

function shouldStopLoss(
  state: DumpHedgeState,
  config: BotConfig
): boolean {
  if (state.phase !== 'leg1_placed' || !state.leg1PlacedAt) return false;
  const waitedMinutes = (Date.now() / 1000 - state.leg1PlacedAt) / 60;
  return waitedMinutes >= config.stopLossMaxWaitMinutes;
}

Placing an order via CLOB (api.ts pattern)

import { ethers } from 'ethers';

interface OrderParams {
  tokenId: string;
  price: number;      // 0.0 to 1.0
  size: number;       // number of shares
  side: 'BUY' | 'SELL';
}

async function placeOrder(
  clobApiUrl: string,
  signer: ethers.Wallet,
  apiKey: string,
  apiSecret: string,
  apiPassphrase: string,
  params: OrderParams,
  production: boolean
): Promise<string | null> {
  if (!production) {
    console.error(`[SIM] Would place ${params.side} ${params.size} shares of ${params.tokenId} @ ${params.price}`);
    return `sim-order-${Date.now()}`;
  }

  // Build and sign order for CLOB
  const order = {
    token_id: params.tokenId,
    price: params.price.toFixed(4),
    size: params.size.toString(),
    side: params.side,
    type: 'GTC',
  };

  // CLOB requires L1/L2 auth headers derived from API credentials
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = await signClobOrder(signer, order, timestamp);

  try {
    const response = await axios.post(`${clobApiUrl}/order`, order, {
      headers: {
        'POLY_ADDRESS': await signer.getAddress(),
        'POLY_SIGNATURE': signature,
        'POLY_TIMESTAMP': timestamp,
        'POLY_API_KEY': apiKey,
      }
    });
    return response.data.orderId ?? null;
  } catch (err) {
    console.error('[ORDER] Placement failed:', err);
    return null;
  }
}

Monitor loop (monitor.ts pattern)

async function startMonitor(
  market: Market,
  config: BotConfig,
  onSnapshot: (snapshot: MarketSnapshot) => Promise<void>
): Promise<void> {
  const [upToken, downToken] = market.tokens;

  const poll = async () => {
    try {
      const [upBook, downBook] = await Promise.all([
        getOrderBook(config.clobApiUrl, upToken.tokenId),
        getOrderBook(config.clobApiUrl, downToken.tokenId),
      ]);

      if (!upBook || !downBook) return;

      const now = Math.floor(Date.now() / 1000);
      const snapshot: MarketSnapshot = {
        upPrice: {
          tokenId: upToken.tokenId,
          outcome: 'Up',
          bestBid: upBook.bestBid,
          bestAsk: upBook.bestAsk,
          timestamp: now,
        },
        downPrice: {
          tokenId: downToken.tokenId,
          outcome: 'Down',
          bestBid: downBook.bestBid,
          bestAsk: downBook.bestAsk,
          timestamp: now,
        },
        timeRemainingSeconds: market.endTime - now,
        periodStart: market.startTime,
      };

      await onSnapshot(snapshot);
    } catch (err) {
      console.error('[MONITOR] Poll error:', err);
    }
  };

  // Start polling
  const intervalId = setInterval(poll, config.checkIntervalMs);
  await poll(); // immediate first poll

  // Stop when market ends
  const msUntilEnd = (market.endTime * 1000) - Date.now();
  setTimeout(() => clearInterval(intervalId), msUntilEnd + 5000);
}

History logging (logger.ts pattern)

import * as fs from 'fs';

const HISTORY_FILE = 'history.toml';

interface TradeRecord {
  timestamp: string;
  asset: string;
  action: 'leg1' | 'hedge' | 'stop_loss' | 'redemption';
  outcome: string;
  price: number;
  shares: number;
  simulation: boolean;
  pnl?: number;
}

function logTrade(record: TradeRecord): void {
  const entry = `
[[trade]]
timestamp = "${record.timestamp}"
asset = "${record.asset}"
action = "${record.action}"
outcome = "${record.outcome}"
price = ${record.price}
shares = ${record.shares}
simulation = ${record.simulation}
${record.pnl !== undefined ? `pnl = ${record.pnl}` : ''}
`;
  fs.appendFileSync(HISTORY_FILE, entry, 'utf8');
  console.error(`[LOG] ${record.action} ${record.outcome} @ ${record.price} (sim=${record.simulation})`);
}

Main entry pattern (main.ts)

import { loadConfig } from './config';
import { findCurrentMarket } from './api';
import { startMonitor } from './monitor';
import { DumpHedgeTrader } from './dumpHedgeTrader';

async function main() {
  const config = loadConfig();

  console.error(`[BOOT] Mode: ${config.production ? 'PRODUCTION' : 'SIMULATION'}`);
  console.error(`[BOOT] Markets: ${config.markets.join(', ')}`);

  // Start a monitor+trader for each configured asset
  const tasks = config.markets.map(async (asset) => {
    while (true) {
      // Discover current 15m market
      const market = await findCurrentMarket(config.gammaApiUrl, asset);
      if (!market) {
        console.error(`[${asset}] No active market found, retrying in 30s`);
        await sleep(30_000);
        continue;
      }

      console.error(`[${asset}] Found market: ${market.conditionId}, ends ${new Date(market.endTime * 1000).toISOString()}`);

      const trader = new DumpHedgeTrader(asset, market, config);
      await startMonitor(market, config, (snap) => trader.onSnapshot(snap));

      // Market ended — handle closure, then loop to find next period
      await trader.onClose();
      console.error(`[${asset}] Period ended, discovering next market...`);
      await sleep(5_000);
    }
  });

  await Promise.all(tasks);
}

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

main().catch(err => {
  console.error('[FATAL]', err);
  process.exit(1);
});

Common Patterns

Multi-asset configuration

# Monitor BTC and ETH simultaneously
MARKETS=btc,eth

# More aggressive dump detection
DUMP_HEDGE_MOVE_THRESHOLD=0.10
DUMP_HEDGE_WINDOW_MINUTES=3

# Tighter profit target
DUMP_HEDGE_SUM_TARGET=0.93

Tuning for volatile markets

# Larger position per leg
DUMP_HEDGE_SHARES=25

# Wider dump threshold catches more opportunities
DUMP_HEDGE_MOVE_THRESHOLD=0.10

# Longer window to detect slower dumps
DUMP_HEDGE_WINDOW_MINUTES=4

# More time before stop-loss kicks in
DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=8

Switching simulation → production

# 1. Verify strategy looks correct in simulation
npm run sim

# 2. Check history.toml for expected trade pattern
cat history.toml

# 3. Enable production (ensure wallet funded with USDC + POL for gas)
PRODUCTION=true npm start
# or
npm run prod

Using EOA wallet (no proxy)

PRIVATE_KEY=0x_your_eoa_private_key
SIGNATURE_TYPE=0
# Leave PROXY_WALLET_ADDRESS unset

Using GnosisSafe proxy (default Polymarket setup)

PRIVATE_KEY=0x_your_signer_private_key
PROXY_WALLET_ADDRESS=0x_your_polymarket_profile_address
SIGNATURE_TYPE=2

Profit Mechanics

Per resolved pair:
  Revenue:          1.00  (winning outcome pays $1/share)
  Cost (leg1):      e.g. 0.45  (bought dumped side)
  Cost (leg2):      e.g. 0.49  (hedge at ask)
  Combined cost:    0.94  (<= SUM_TARGET of 0.95)
  Profit/share:     0.06  (6% per share pair, before fees)

Worst case (stop-loss hedge):
  If hedge triggers at stop-loss, combined cost may exceed 0.95
  Loss is bounded by STOP_LOSS_PERCENTAGE (e.g. 0.2 = 20% of leg1 size)

Troubleshooting

ProblemCauseFix
No markets foundWrong slug/timingCheck GAMMA_API_URL connectivity; confirm asset name is lowercase
Orders fail in productionBad credentialsVerify PRIVATE_KEY, PROXY_WALLET_ADDRESS, SIGNATURE_TYPE
Redemption failsInsufficient POL gasFund wallet with POL/MATIC on Polygon mainnet
No dumps detectedThreshold too highLower DUMP_HEDGE_MOVE_THRESHOLD (e.g. 0.10) or extend window
Strategy never hedgesSum target too tightRaise DUMP_HEDGE_SUM_TARGET (e.g. 0.97)
Frequent stop-loss triggersMarket low volatilityIncrease STOP_LOSS_MAX_WAIT_MINUTES

Debugging with simulation logs

# Run simulation and watch logs in real time
npm run sim 2>&1 | tee debug.log

# Review all trades
grep "action" history.toml

# Check P&L entries
grep "pnl" history.toml

Security Checklist

  • .env is in .gitignore — never commit it
  • Use a dedicated wallet with limited USDC (not your main wallet)
  • Always run npm run sim first and review history.toml before going live
  • Rotate PRIVATE_KEY immediately if it may have been exposed
  • API keys derived from signer are preferred over explicit API_KEY / API_SECRET

Related skills

FAQ

What strategy does polymarket-arbitrage-bot implement?

polymarket-arbitrage-bot automates a dump-and-hedge arbitrage strategy on Polymarket 15-minute Up/Down prediction markets. The TypeScript skill includes CLOB order execution and a simulation mode for testing.

Does polymarket-arbitrage-bot support live and simulated trading?

polymarket-arbitrage-bot supports Polymarket CLOB order execution in TypeScript plus simulation mode so developers can validate dump-and-hedge logic on 15-minute Up/Down markets before placing live orders.

Is Polymarket Arbitrage Bot safe to install?

skills.sh reports 0 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

Automation & Workflowsagentsautomation

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.