polymarket-arbitrage-bot

TypeScript bot implementing dump-and-hedge arbitrage strategy on Polymarket 15-minute Up/Down prediction markets with CLOB order execution and simulation mode.

22 stars

Best use case

polymarket-arbitrage-bot is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

TypeScript bot implementing dump-and-hedge arbitrage strategy on Polymarket 15-minute Up/Down prediction markets with CLOB order execution and simulation mode.

Teams using polymarket-arbitrage-bot should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/polymarket-arbitrage-bot/SKILL.md --create-dirs "https://raw.githubusercontent.com/Aradotso/trending-skills/main/skills/polymarket-arbitrage-bot/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/polymarket-arbitrage-bot/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How polymarket-arbitrage-bot Compares

Feature / Agentpolymarket-arbitrage-botStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

TypeScript bot implementing dump-and-hedge arbitrage strategy on Polymarket 15-minute Up/Down prediction markets with CLOB order execution and simulation mode.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Polymarket Arbitrage Bot

> Skill by [ara.so](https://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

```bash
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

| Command | Description |
|---------|-------------|
| `npm start` | Run compiled bot (simulation by default) |
| `npm run sim` | Explicitly run in simulation (no real orders) |
| `npm run prod` | Run with real trades (`PRODUCTION=true`) |
| `npm run dev` | Run TypeScript directly via ts-node |
| `npm run build` | Compile 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`:

```env
# --- 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)

```typescript
// 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)

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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)

```typescript
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

```env
# 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

```env
# 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

```bash
# 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)

```env
PRIVATE_KEY=0x_your_eoa_private_key
SIGNATURE_TYPE=0
# Leave PROXY_WALLET_ADDRESS unset
```

### Using GnosisSafe proxy (default Polymarket setup)

```env
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

| Problem | Cause | Fix |
|---------|-------|-----|
| No markets found | Wrong slug/timing | Check `GAMMA_API_URL` connectivity; confirm asset name is lowercase |
| Orders fail in production | Bad credentials | Verify `PRIVATE_KEY`, `PROXY_WALLET_ADDRESS`, `SIGNATURE_TYPE` |
| Redemption fails | Insufficient POL gas | Fund wallet with POL/MATIC on Polygon mainnet |
| No dumps detected | Threshold too high | Lower `DUMP_HEDGE_MOVE_THRESHOLD` (e.g. 0.10) or extend window |
| Strategy never hedges | Sum target too tight | Raise `DUMP_HEDGE_SUM_TARGET` (e.g. 0.97) |
| Frequent stop-loss triggers | Market low volatility | Increase `STOP_LOSS_MAX_WAIT_MINUTES` |

### Debugging with simulation logs

```bash
# 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

polymarket-copy-trading-bot

22
from Aradotso/trending-skills

TypeScript bot that monitors a Polymarket wallet and mirrors BUY trades to your own account via the Polymarket CLOB API on Polygon.

polymarket-arbitrage-trading-bot

22
from Aradotso/trending-skills

Automated dump-and-hedge arbitrage trading bot for Polymarket's 15-minute crypto Up/Down markets, supporting BTC, ETH, SOL, and XRP.

nothing-ever-happens-polymarket-bot

22
from Aradotso/trending-skills

Async Python bot for Polymarket that buys "No" on all standalone non-sports yes/no markets using the "nothing ever happens" strategy.

```markdown

22
from Aradotso/trending-skills

---

zeroboot-vm-sandbox

22
from Aradotso/trending-skills

Sub-millisecond VM sandboxes for AI agents using copy-on-write KVM forking via Zeroboot

yourvpndead-vpn-detection

22
from Aradotso/trending-skills

Android app that detects VPN/proxy servers (VLESS/xray/sing-box) via local SOCKS5 vulnerability, exposing exit IPs and server configs without root

xata-postgres-platform

22
from Aradotso/trending-skills

Expert skill for Xata open-source cloud-native Postgres platform with copy-on-write branching, scale-to-zero, and Kubernetes deployment

x-mentor-skill-nuwa

22
from Aradotso/trending-skills

AI-powered X (Twitter) content strategy skill that distills methodologies from 6 top creators + open-source algorithm data into actionable writing, growth, and monetization guidance.

wx-favorites-report

22
from Aradotso/trending-skills

End-to-end pipeline to extract, decrypt, and visualize WeChat Mac favorites from encrypted SQLite DB into an interactive HTML report.

wterm-web-terminal

22
from Aradotso/trending-skills

Web terminal emulator with Zig/WASM core, DOM rendering, and React/vanilla JS bindings

worldmonitor-intelligence-dashboard

22
from Aradotso/trending-skills

Real-time global intelligence dashboard with AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking

witr-process-inspector

22
from Aradotso/trending-skills

CLI and TUI tool that explains why processes, services, and ports are running by tracing causality chains across supervisors, containers, and shells.