| name | polymarket-arbitrage-trading-bot |
| description | Automated dump-and-hedge arbitrage trading bot for Polymarket's 15-minute crypto Up/Down markets, supporting BTC, ETH, SOL, and XRP. |
| triggers | ["set up polymarket arbitrage bot","configure polymarket trading bot","run polymarket dump hedge strategy","polymarket 15 minute market bot","automate polymarket trading","polymarket clob arbitrage typescript","polymarket bot simulation mode","hedge polymarket prediction markets"] |
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
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)
PRIVATE_KEY=0xYOUR_PRIVATE_KEY_HERE
PROXY_WALLET_ADDRESS=0xYOUR_PROXY_WALLET
SIGNATURE_TYPE=2
MARKETS=btc,eth,sol,xrp
CHECK_INTERVAL_MS=1000
DUMP_HEDGE_SHARES=10
DUMP_HEDGE_SUM_TARGET=0.95
DUMP_HEDGE_MOVE_THRESHOLD=0.15
DUMP_HEDGE_WINDOW_MINUTES=5
DUMP_HEDGE_STOP_LOSS_MAX_WAIT_MINUTES=8
PRODUCTION=false
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: (process.. ?? , ),
: (
process.. ?? ,
),
: process.. === ,
};
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,
137,
wallet,
undefined,
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 markets.sort(
(a, b) => new Date(a.endDateIso).getTime() - (b.).()
)[] ?? ;
}
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;
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;
}
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,
});
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;
stopLossMs = config. * * ;
: <, []> = {
[upTokenId]: [], [downTokenId]: []
};
interval = ( () => {
elapsed = .() - startTime;
upAsk = (upTokenId);
downAsk = (downTokenId);
(upAsk == || downAsk == ) ;
now = .();
priceHistory[upTokenId].({ : now, : upAsk });
priceHistory[downTokenId].({ : now, : downAsk });
(!leg1. && elapsed <= windowMs) {
upDumped = (
priceHistory[upTokenId], upAsk, config., windowMs
);
downDumped = (
priceHistory[downTokenId], downAsk, config., windowMs
);
(upDumped || downDumped) {
dumpedToken = upDumped ? upTokenId : downTokenId;
dumpedAsk = upDumped ? upAsk : downAsk;
leg1. = dumpedToken;
leg1. = dumpedAsk;
leg1. = (
client, dumpedToken, dumpedAsk, config., simulate
);
leg1. = ;
.();
}
}
(leg1. && !leg2.) {
hedgeToken = leg1. === upTokenId ? downTokenId : upTokenId;
hedgeAsk = leg1. === upTokenId ? downAsk : upAsk;
combinedCost = leg1.! + hedgeAsk;
shouldHedge =
combinedCost <= config. ||
elapsed >= stopLossMs;
(shouldHedge) {
label = combinedCost <= config. ? : ;
leg2. = hedgeToken;
leg2. = hedgeAsk;
leg2. = (
client, hedgeToken, hedgeAsk, config., simulate
);
leg2. = ;
.();
(interval);
}
}
}, config.);
}
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;
}
await client.redeemPositions({
conditionId,
amounts: [{ tokenId: winningTokenId, amount: config.dumpHedgeShares }]
});
console.error(`[SETTLE] Redeemed ${config.dumpHedgeShares} shares for ${winningTokenId}`);
}
Running Modes
Simulation (Recommended First)
npm run sim
node dist/main.js --simulation
tail -f history.toml
Production (Live Trading)
npm run prod
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
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., !production);
roundEnd = (market.).();
(.(, roundEnd - .() + ));
}
}
(): <> {
( (resolve, ms));
}
().(.);
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