Repository: 0xalberto/pumpfun-laserstream-sniper-rust Branch: main Commit: b3669a44a7c4 Files: 12 Total size: 31.6 KB Directory structure: gitextract_681ttebc/ ├── .gitignore ├── README.md ├── Update.md ├── package.json ├── src/ │ ├── config/ │ │ ├── client.ts │ │ ├── index.ts │ │ └── loadEnv.ts │ ├── constant/ │ │ └── index.ts │ ├── index.ts │ ├── strategy/ │ │ └── arbitrage.ts │ └── types/ │ └── index.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/* .env *-lock.json log.txt ui/dist dist /node_modules frontend/node_modules frontend/dist ================================================ FILE: README.md ================================================ # Polymarket Arbitrage Bot Polymarket **arbitrage bot** for 15-minute Up/Down markets. Automates the **dump-and-hedge** strategy with configurable thresholds, stop-loss hedging, and optional simulation mode. Full credential management, CLOB order execution, and market discovery via Gamma API. [![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5+-blue.svg)](https://www.typescriptlang.org/) [![License](https://img.shields.io/badge/License-Apache%202.0-yellow.svg)](LICENSE) ![Portfolio](img.png) ## 🎯 Overview This **Polymarket arbitrage bot** runs a **dump-and-hedge** strategy on Polymarket’s 15m Up/Down markets (e.g. BTC, ETH, SOL, XRP) by: - **Market discovery** – Finds the current 15m market per asset via Gamma API slug - **Price monitoring** – Polls CLOB orderbooks for Up/Down bid/ask and time remaining - **Dump detection** – In the first N minutes of each period, detects a sharp price drop on one side (Up or Down) - **Leg 1** – Buys the dumped side at the dip - **Hedge (Leg 2)** – Waits until combined cost (leg1 + opposite ask) is at or below target (e.g. ≤ 0.95), then buys the opposite side to lock in profit - **Stop-loss hedge** – If the hedge condition isn’t met within a max wait time, hedges anyway to limit risk - **Settlement** – On market close, redeems winning outcome tokens and tracks P&L - **Simulation mode** – Run without placing real orders (default); switch to production when ready - **Type-safe** – Full TypeScript with strict types and clear config via `.env` Perfect for automating the dump-and-hedge arbitrage on Polymarket 15m markets with controllable risk and optional dry-run. ## ✨ Key Features ### 🚀 Trading & Strategy - **Dump-and-hedge** – Buy the dip on one outcome, then hedge when sum of prices ≤ target - **Multi-market** – Supports multiple symbol/timeframe pairs (configurable via `ARBITRAGE_MARKETS`, e.g. `btc:5m,btc:15m`) - **Automatic market discovery** – Resolves current 15m market by slug and period timestamp - **Period rollover** – Detects new 15m periods and switches to the new market automatically - **Stop-loss hedge** – Time-based fallback hedge if ideal hedge price isn’t reached ### 🛡️ Risk & Safety - **Simulation by default** – No real orders until you set `PRODUCTION=true` or use `npm run prod` - **Configurable sizing** – Shares per leg, sum target, move threshold, and watch window - **Stop-loss parameters** – Max wait before forced hedge and stop-loss percentage - **Position tracking** – Per-period and total P&L; redemption of winning tokens on close ### 🔧 Production-Ready - **Env-based config** – All settings in `.env` (no config files to commit) - **CLOB auth** – API key derivation from signer or optional explicit API key/secret/passphrase - **Proxy wallet support** – Optional Polymarket proxy/profile address and signature type (EOA / Proxy / GnosisSafe) - **History logging** – Append-only `history.toml` for audit and debugging - **Graceful handling** – Continues monitoring on transient API errors; clear stderr logging ## 🚀 Quick Start ### Prerequisites - **Node.js 16+** – [Download Node.js](https://nodejs.org/) - **Polygon wallet** – With USDC for trading (production) - **POL/MATIC** – For gas when redeeming winning tokens (production) ### Installation ```bash # Clone the repository git clone https://github.com/zkOSAI/polymarket-arbitrage-bot.git cd polymarket-arbitrage-bot # Install dependencies npm install # Build the project npm run build ``` ### Configuration 1. **Create environment file:** ```bash cp .env.example .env ``` 2. **Edit `.env` with your settings:** ```env # Wallet + auth (required in live mode) WALLET_PRIVATE_KEY=0x... # Alias: PRIVATE_KEY PROXY_WALLET_ADDRESS=0x... # Required when SIGNATURE_TYPE=1 or 2 (Alias: FUNDER_ADDRESS) SIGNATURE_TYPE=1 # 0=EOA, 1=Proxy/Magic, 2=GnosisSafe # APIs / chain GAMMA_API_URL=https://gamma-api.polymarket.com CLOB_API_URL=https://clob.polymarket.com # Alias: CLOB_HOST CHAIN_ID=137 POLYGON_RPC_URL=https://polygon-rpc.com # optional override for USDC preflight check AMOY_RPC_URL=https://rpc-amoy.polygon.technology # optional, used when CHAIN_ID != 137 # Markets ARBITRAGE_MARKETS=btc:5m,btc:15m # Mode SIMULATION_MODE=true # true=paper mode, false=live mode (Alias: PRODUCTION with inverse meaning) # Strategy tuning ARBITRAGE_CHECK_INTERVAL_MS=5000 ARBITRAGE_ORDER_USD=20 ARBITRAGE_OBI_DEPTH_LEVELS=5 ARBITRAGE_TREND_THRESHOLD=0.05 ``` 3. **Run the bot:** ```bash # Development – run TypeScript with ts-node npm run dev ``` Logs go to stderr and are appended to `history.toml`. ## ⚙️ Configuration Guide ### Environment Variables These are the environment variables currently used by the code. | Variable | Required | Description | Default | |----------|----------|-------------|---------| | `WALLET_PRIVATE_KEY` | Live mode | Private key used to sign CLOB requests. Alias: `PRIVATE_KEY` | - | | `PROXY_WALLET_ADDRESS` | Live mode when `SIGNATURE_TYPE` is `1` or `2` | Polymarket proxy/profile wallet. Alias: `FUNDER_ADDRESS` | - | | `SIGNATURE_TYPE` | No | `0` EOA, `1` Proxy/Magic, `2` GnosisSafe | `1` | | `GAMMA_API_URL` | No | Gamma API base URL (used for market discovery + proxy validation) | `https://gamma-api.polymarket.com` | | `CLOB_API_URL` | No | CLOB API base URL. Alias: `CLOB_HOST` | `https://clob.polymarket.com` | | `CHAIN_ID` | No | Network chain id (`137` Polygon mainnet) | `137` | | `POLYGON_RPC_URL` | No | RPC endpoint for Polygon USDC balance preflight check | `https://polygon-rpc.com` | | `AMOY_RPC_URL` | No | RPC endpoint for Amoy (used when `CHAIN_ID != 137`) | `https://rpc-amoy.polygon.technology` | | `SIMULATION_MODE` | No | `true` = paper mode, `false` = live orders | `false` | | `PRODUCTION` | No | Backward-compatible alias of mode with inverse semantics (`true` => live) | `false` | | `ARBITRAGE_MARKETS` | No | Comma-separated `symbol:timeframe` list | `btc:5m,btc:15m` | | `ARBITRAGE_CHECK_INTERVAL_MS` | No | Poll interval in milliseconds | `5000` | | `ARBITRAGE_ORDER_USD` | No | Order size in USDC per entry/switch action | `20` | | `ARBITRAGE_OBI_DEPTH_LEVELS` | No | Orderbook bid depth levels used for OBI trend calc | `5` | | `ARBITRAGE_TREND_THRESHOLD` | No | OBI threshold above/below neutral trend | `0.05` | ### Preflight checks (live mode) Before trading starts, the bot validates: - `PRIVATE_KEY` ↔ `PROXY_WALLET_ADDRESS` binding via Gamma `public-profile` - USDC balance of trading wallet/proxy is `>= ARBITRAGE_ORDER_USD` ## 📖 How It Works ### Dump-and-hedge flow 1. **Discovery** – For each target in `ARBITRAGE_MARKETS`, the bot finds the active Up/Down market via Gamma slug matching. 2. **Monitoring** – Every `ARBITRAGE_CHECK_INTERVAL_MS`, it fetches YES/NO orderbooks and computes OBI trend from bid depth. 3. **Entry** – If no position: buy YES on uptrend, buy NO on downtrend. 4. **Neutral exit** – If trend is neutral and a position exists, sell current position. 5. **Trend-follow hold** – Hold YES on uptrend and hold NO on downtrend. 6. **Trend-flip switch** – If trend flips against the held side, sell and rotate into the new trend side. 7. **Preflight safety** – In live mode, validates key/proxy binding and checks USDC balance is at least `ARBITRAGE_ORDER_USD`. ### Simulation vs production - **Simulation** (`SIMULATION_MODE=true`): no orders sent to the CLOB; strategy logic and logging run as normal. - **Production** (`SIMULATION_MODE=false`): real orders sent to the CLOB. Requires `WALLET_PRIVATE_KEY`; set `PROXY_WALLET_ADDRESS` and `SIGNATURE_TYPE` for proxy/GnosisSafe accounts. ## 📦 Available Scripts | Command | Description | |---------|-------------| | `npm run dev` | Run with ts-node | ## 🐳 Docker (optional) If you add a `Dockerfile` later: ```bash docker build -t polymarket-arbitrage-bot . docker run --env-file .env -d --name polymarket-arbitrage-bot polymarket-arbitrage-bot docker logs -f polymarket-arbitrage-bot ``` ## 🛠️ Troubleshooting ### Bot doesn’t find markets - Confirm `ARBITRAGE_MARKETS` is valid `symbol:timeframe` pairs (example: `btc:5m,btc:15m`). - Check network access to Gamma and CLOB APIs; try default `GAMMA_API_URL` and `CLOB_API_URL` first. ### Orders fail in production - Ensure `WALLET_PRIVATE_KEY` is set and correct (hex, with or without `0x`). - If using a proxy, set `PROXY_WALLET_ADDRESS` and `SIGNATURE_TYPE` (usually `2` for GnosisSafe). - Verify USDC balance and that the market is still active and accepting orders. ### Redemption fails - Ensure you have enough POL for gas on Polygon. - Confirm the market is closed and resolved; the bot only redeems after resolution. ### Strategy not entering trades - Lower `ARBITRAGE_TREND_THRESHOLD` if trend stays neutral too often. - Increase `ARBITRAGE_OBI_DEPTH_LEVELS` to smooth noisy orderbook signals. ## 🔐 Security Best Practices - **Never commit `.env`** – Keep it in `.gitignore` (already listed). - **Use env vars for secrets** – Don’t hardcode `WALLET_PRIVATE_KEY` or API credentials. - **Test in simulation first** – Run with `SIMULATION_MODE=true` before enabling live mode. - **Limit wallet use** – Prefer a dedicated wallet with limited funds for the bot. - **Rotate keys** – Replace credentials if they may have been exposed. ## 📚 Project structure - `src/main.ts` – Entry point, config load, market discovery, and monitor/trader wiring. - `src/config.ts` – Loads and validates `.env` into typed config. - `src/api.ts` – Polymarket Gamma + CLOB API client (markets, orderbook, orders, redemption). - `src/monitor.ts` – Fetches orderbook snapshots and drives the strategy callback. - `src/dumpHedgeTrader.ts` – Dump detection, leg 1/2, stop-loss hedge, closure and P&L. - `src/models.ts` – Shared types (Market, OrderBook, TokenPrice, etc.). - `src/logger.ts` – History log and stderr output. - `history.toml` – Append-only log (created at runtime; in `.gitignore`). ## 🤝 Contributing Contributions are welcome. Please open an issue or pull request. 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/AmazingFeature`) 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request ## 📄 License This project is licensed under the Apache License 2.0 – see the [LICENSE](LICENSE) file for details. ## ⚠️ Disclaimer **IMPORTANT LEGAL DISCLAIMER:** This software is provided “as-is” for educational and research purposes only. Trading on prediction markets involves substantial risk of loss. - **No warranty** – The software is provided without any warranties. - **Use at your own risk** – You are solely responsible for any losses incurred. - **Not financial advice** – This is not investment or trading advice. - **Compliance** – Ensure compliance with local laws and regulations. - **Testing** – Always test with simulation and small amounts before production. The authors and contributors are not responsible for any financial losses, damages, or legal issues arising from the use of this software. ## 📞 Support & Contact - **Telegram**: [@soladity](https://t.me/soladity) - **Issues**: [GitHub Issues](https://github.com/zkOSAI/polymarket-arbitrage-bot/issues) - **Discussions**: [GitHub Discussions](https://github.com/zkOSAI/polymarket-arbitrage-bot/discussions) ## 🌟 Star history If you find this project useful, please consider giving it a star ⭐ ## 📈 Roadmap - [ ] Optional WebSocket orderbook updates for lower latency - [ ] Backtesting / replay mode for strategy tuning - [ ] Optional Telegram/Discord notifications - [ ] More timeframe support (e.g. 1h) - [ ] PnL export and simple reporting --- **Keywords**: Polymarket bot, Polymarket arbitrage bot, dump and hedge, 15m Up Down, prediction markets bot, Polygon, trading automation, Polymarket CLOB ================================================ FILE: Update.md ================================================ # Version 1.0 - Multi Target Address - Revert Trade - Size Multiplier - Poll Interval_sec - Take Profit - Stop Loss - Trailing Stop - Buy Amount Limit In Usd - Entry Trade Sec - Trade Sec From Resolve (Exit Time) # Version 1.1 - Multi Target Address - Fix type error - Basic UI Implementation - - User Activity - - Holding Asset Track # Version 1.1.1 - Fix Decimal Issue in Traded Share - Add Dump Dashboard / Setting # Version 1.1.2 - Update FrontEnd UI structure - Init Struct for Dashboard / Settings ================================================ FILE: package.json ================================================ { "name": "polymarket-arbitrage-bot", "version": "1.0.0", "main": "index.js", "scripts": { "log": "tsx src/index.ts > log.txt 2>&1", "build": "tsc", "dev": "tsx src/index.ts", "start": "node dist/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { "@types/dotenv": "^8.2.3", "@types/node": "^20.11.0", "concurrently": "^9.1.2", "ts-node": "^10.9.2", "tsx": "^4.7.0", "typescript": "^5.3.3" }, "dependencies": { "@polymarket/builder-relayer-client": "^0.0.8", "@polymarket/clob-client": "^5.2.1", "@polymarket/order-utils": "^3.0.1", "@safe-global/protocol-kit": "^6.1.2", "@safe-global/types-kit": "^3.0.0", "@types/ws": "^8.18.1", "dotenv": "^16.4.5", "ethers": "^5.7.2", "puppeteer-core": "^24.37.3", "ts-big-lib": "latest", "ws": "^8.19.0", "zod": "^4.3.5" }, "author": "", "license": "ISC", "description": "" } ================================================ FILE: src/config/client.ts ================================================ import { Contract, providers, utils, Wallet } from "ethers"; import { ClobClient, Chain } from "@polymarket/clob-client"; import { SignatureType } from "@polymarket/order-utils"; import type { AppConfig } from "../types"; interface GammaPublicProfile { proxyWallet?: string; } const ERC20_ABI = ["function balanceOf(address owner) view returns (uint256)"]; const USDC_BY_CHAIN_ID: Record = { 137: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", }; function getClientSetup(config: AppConfig): { clobHost: string; chain: Chain; wallet: Wallet; sigType: SignatureType; funder?: string; } { const { clobHost, chainId, walletPrivateKey, proxyWalletAddress, signatureType } = config; const chain = chainId === 137 ? Chain.POLYGON : Chain.AMOY; const pk = walletPrivateKey.startsWith("0x") ? walletPrivateKey : "0x" + walletPrivateKey; const wallet = new Wallet(pk); const sigType = signatureType as SignatureType; const funder = proxyWalletAddress || undefined; return { clobHost, chain, wallet, sigType, funder }; } export async function validateWalletBinding(config: AppConfig): Promise { const { wallet, sigType, funder } = getClientSetup(config); if (sigType === 0) { return; } if (!funder) { throw new Error("PROXY_WALLET_ADDRESS is required when SIGNATURE_TYPE is 1 or 2."); } const eoaAddress = wallet.address.toLowerCase(); const expectedProxy = funder.toLowerCase(); const url = `${config.gammaApiUrl}/public-profile?address=${eoaAddress}`; const res = await fetch(url); if (!res.ok) { throw new Error(`Failed to validate wallet binding via Gamma public-profile (${res.status})`); } const profile = (await res.json()) as GammaPublicProfile; const actualProxy = (profile.proxyWallet ?? "").toLowerCase(); if (!actualProxy) { throw new Error("Gamma public-profile has no proxyWallet for this PRIVATE_KEY address."); } if (actualProxy !== expectedProxy) { throw new Error( `Invalid wallet binding. PRIVATE_KEY resolves to ${eoaAddress} but Gamma proxyWallet is ${actualProxy}, expected ${expectedProxy}.` ); } } export async function validateUsdcBalance(config: AppConfig): Promise { const { wallet, sigType, funder } = getClientSetup(config); const walletToCheck = (sigType === 1 || sigType === 2 ? funder : wallet.address)?.toLowerCase(); if (!walletToCheck) { throw new Error("Cannot validate USDC balance: missing wallet address to check."); } const usdcAddress = USDC_BY_CHAIN_ID[config.chainId]; if (!usdcAddress) { throw new Error(`USDC validation is not configured for CHAIN_ID=${config.chainId}.`); } const rpcUrl = config.chainId === 137 ? process.env.POLYGON_RPC_URL ?? "https://polygon-rpc.com" : process.env.AMOY_RPC_URL ?? "https://rpc-amoy.polygon.technology"; const provider = new providers.JsonRpcProvider(rpcUrl); const usdc = new Contract(usdcAddress, ERC20_ABI, provider); const rawBalance = await usdc.balanceOf(walletToCheck); const balance = Number(utils.formatUnits(rawBalance, 6)); if (Number.isNaN(balance)) { throw new Error("Cannot validate USDC balance: failed to parse balance."); } if (balance < config.arbitrage.orderUsd) { throw new Error( `Insufficient USDC balance on ${walletToCheck}. Balance=${balance.toFixed(2)} USDC, required >= ${config.arbitrage.orderUsd.toFixed(2)} USDC.` ); } } export async function createClient(config: AppConfig): Promise { const { clobHost, chain, wallet, sigType, funder } = getClientSetup(config); const tempClient = new ClobClient(clobHost, chain, wallet, undefined, sigType, funder); const creds = await tempClient.createOrDeriveApiKey(); return new ClobClient(clobHost, chain, wallet, creds, sigType, funder); } ================================================ FILE: src/config/index.ts ================================================ export * from "./loadEnv"; export * from "./client"; ================================================ FILE: src/config/loadEnv.ts ================================================ import "dotenv/config"; import { Wallet } from "ethers"; import type { AppConfig } from "../types"; import { DEFAULT_CHAIN_ID, DEFAULT_HOST } from "../constant"; function parseMarkets(raw: string): Array<{ symbol: string; timeframe: string }> { return raw .split(",") .map((item) => item.trim()) .filter(Boolean) .map((item) => { const [symbolRaw, timeframeRaw] = item.split(":"); return { symbol: (symbolRaw ?? "").trim().toLowerCase(), timeframe: (timeframeRaw ?? "").trim().toLowerCase(), }; }) .filter((m) => m.symbol && m.timeframe); } export function loadConfig(): AppConfig { const walletPrivateKey = (process.env.WALLET_PRIVATE_KEY ?? process.env.PRIVATE_KEY ?? "").trim(); const proxyWalletAddress = (process.env.PROXY_WALLET_ADDRESS ?? process.env.FUNDER_ADDRESS ?? "").trim(); const signatureType = parseInt(process.env.SIGNATURE_TYPE ?? "1", 10); let walletAddress = ""; if (walletPrivateKey) { const pk = walletPrivateKey.startsWith("0x") ? walletPrivateKey : "0x" + walletPrivateKey; try { walletAddress = new Wallet(pk).address; } catch { /* ignore */ } } return { clobHost: (process.env.CLOB_API_URL ?? process.env.CLOB_HOST ?? DEFAULT_HOST).trim(), gammaApiUrl: (process.env.GAMMA_API_URL ?? "https://gamma-api.polymarket.com").trim(), chainId: parseInt(process.env.CHAIN_ID ?? String(DEFAULT_CHAIN_ID), 10), simulationMode: (process.env.SIMULATION_MODE ?? process.env.PRODUCTION ?? "false").toLowerCase() !== "true", walletPrivateKey, proxyWalletAddress, walletAddress: proxyWalletAddress || walletAddress, signatureType, arbitrage: { markets: parseMarkets(process.env.ARBITRAGE_MARKETS ?? "btc:5m,btc:15m"), checkIntervalMs: parseInt(process.env.ARBITRAGE_CHECK_INTERVAL_MS ?? "5000", 10), orderUsd: parseFloat(process.env.ARBITRAGE_ORDER_USD ?? "20"), obiDepthLevels: parseInt(process.env.ARBITRAGE_OBI_DEPTH_LEVELS ?? "5", 10), trendThreshold: parseFloat(process.env.ARBITRAGE_TREND_THRESHOLD ?? "0.05"), }, }; } ================================================ FILE: src/constant/index.ts ================================================ export const DEFAULT_HOST = "https://clob.polymarket.com"; export const DEFAULT_CHAIN_ID = 137; ================================================ FILE: src/index.ts ================================================ import { loadConfig } from "./config"; import { createClient, validateUsdcBalance, validateWalletBinding } from "./config/client"; import { runObiArbitrage } from "./strategy/arbitrage"; async function run() { const config = loadConfig(); if (!config.arbitrage.markets.length) { console.error("No markets. Set ARBITRAGE_MARKETS in .env (example: btc:5m,btc:15m)"); process.exit(1); } if (!config.walletPrivateKey) { console.error("No wallet. Set WALLET_PRIVATE_KEY in .env"); process.exit(1); } if (!config.proxyWalletAddress && config.signatureType !== 0) { console.error("Set PROXY_WALLET_ADDRESS in .env for proxy/Magic wallet"); process.exit(1); } try { await validateWalletBinding(config); await validateUsdcBalance(config); console.log(`Wallet validation passed for SIGNATURE_TYPE=${config.signatureType}`); } catch (error) { console.error((error as Error)?.message ?? error); process.exit(1); } const client = config.simulationMode ? null : await createClient(config); await runObiArbitrage(client, config); } run().catch((e) => { console.error(e); process.exit(1); }); ================================================ FILE: src/strategy/arbitrage.ts ================================================ import { ClobClient, OrderType, Side } from "@polymarket/clob-client"; import type { AppConfig } from "../types"; import { Big } from "ts-big-lib"; type Trend = "UPTREND" | "DOWNTREND" | "NEUTRAL"; type PositionSide = "yes" | "no"; interface GammaMarket { slug?: string; active?: boolean; closed?: boolean; archived?: boolean; endDate?: string; outcomes?: string | string[]; clobTokenIds?: string | string[]; } interface MarketBinding { symbol: string; timeframe: string; slug: string; yesTokenId: string; noTokenId: string; } interface OrderLevel { price: string; size: string; } interface OrderBookResponse { bids?: OrderLevel[]; asks?: OrderLevel[]; } interface BotPosition { side: PositionSide; tokenId: string; size: number; } const localPositions = new Map(); function keyOf(market: { symbol: string; timeframe: string }): string { return `${market.symbol}:${market.timeframe}`; } function parseArrayField(value: string | string[] | undefined): string[] { if (Array.isArray(value)) return value.map((v) => String(v)); if (!value) return []; try { const parsed = JSON.parse(value); if (Array.isArray(parsed)) return parsed.map((v) => String(v)); } catch { return []; } return []; } function parseOutcomeTokens(m: GammaMarket): { yesTokenId: string; noTokenId: string } | null { const outcomes = parseArrayField(m.outcomes).map((x) => x.toLowerCase()); const tokenIds = parseArrayField(m.clobTokenIds); if (outcomes.length < 2 || tokenIds.length < 2) return null; const yesIdx = outcomes.indexOf("yes"); const noIdx = outcomes.indexOf("no"); if (yesIdx < 0 || noIdx < 0) return null; return { yesTokenId: tokenIds[yesIdx], noTokenId: tokenIds[noIdx] }; } function computeBidDepth(book: OrderBookResponse, levels: number): number { const bids = book.bids ?? []; return bids.slice(0, levels).reduce((sum, level) => sum + Number(level.size ?? 0), 0); } function bestAsk(book: OrderBookResponse): number { return Number(book.asks?.[0]?.price ?? 0); } function bestBid(book: OrderBookResponse): number { return Number(book.bids?.[0]?.price ?? 0); } function computeTrend(yesBidDepth: number, noBidDepth: number, threshold: number): { trend: Trend; obi: number } { const denom = yesBidDepth + noBidDepth; if (denom <= 0) return { trend: "NEUTRAL", obi: 0 }; const obi = (yesBidDepth - noBidDepth) / denom; if (obi > threshold) return { trend: "UPTREND", obi }; if (obi < -threshold) return { trend: "DOWNTREND", obi }; return { trend: "NEUTRAL", obi }; } async function fetchJson(url: string): Promise { const res = await fetch(url); if (!res.ok) throw new Error(`${url} -> ${res.status}`); return (await res.json()) as T; } async function discoverMarket( config: AppConfig, symbol: string, timeframe: string ): Promise { const list = await fetchJson(`${config.gammaApiUrl}/markets?active=true&closed=false&limit=500`); const now = Date.now(); const target = `${symbol}-updown-${timeframe}`; const candidate = list .filter((m) => { const slug = (m.slug ?? "").toLowerCase(); if (!slug.includes(target)) return false; if (m.archived || m.closed || m.active === false) return false; const endMs = m.endDate ? new Date(m.endDate).getTime() : Number.MAX_SAFE_INTEGER; return endMs > now; }) .sort((a, b) => { const ae = a.endDate ? new Date(a.endDate).getTime() : Number.MAX_SAFE_INTEGER; const be = b.endDate ? new Date(b.endDate).getTime() : Number.MAX_SAFE_INTEGER; return ae - be; })[0]; if (!candidate?.slug) return null; const tokenIds = parseOutcomeTokens(candidate); if (!tokenIds) return null; return { symbol, timeframe, slug: candidate.slug, yesTokenId: tokenIds.yesTokenId, noTokenId: tokenIds.noTokenId, }; } async function buyToken( client: ClobClient | null, tokenId: string, amountUsd: number, simulation: boolean ): Promise { if (simulation) return; if (!client) throw new Error("Client missing"); const tickSize = await client.getTickSize(tokenId); const negRisk = await client.getNegRisk(tokenId); await client.createAndPostMarketOrder( { tokenID: tokenId, amount: new Big(amountUsd).toString(), side: Side.BUY, orderType: OrderType.FOK }, { tickSize, negRisk }, OrderType.FOK ); } async function sellToken( client: ClobClient | null, tokenId: string, sizeShares: number, simulation: boolean ): Promise { if (simulation) return; if (!client) throw new Error("Client missing"); const tickSize = await client.getTickSize(tokenId); const negRisk = await client.getNegRisk(tokenId); await client.createAndPostMarketOrder( { tokenID: tokenId, amount: sizeShares, side: Side.SELL, orderType: OrderType.FOK }, { tickSize, negRisk }, OrderType.FOK ); } async function buyAndStorePosition(args: { client: ClobClient | null; stateKey: string; side: PositionSide; tokenId: string; askPrice: number; orderUsd: number; simulation: boolean; }): Promise { await buyToken(args.client, args.tokenId, args.orderUsd, args.simulation); localPositions.set(args.stateKey, { side: args.side, tokenId: args.tokenId, size: args.orderUsd / args.askPrice, }); } export async function runObiArbitrage(client: ClobClient | null, config: AppConfig): Promise { const markets = config.arbitrage.markets; if (!markets.length) { throw new Error("No arbitrage markets configured. Set ARBITRAGE_MARKETS in .env (example: btc:5m,btc:15m)."); } console.log(`Arbitrage strategy started | ${config.simulationMode ? "SIM" : "LIVE"} | ${markets.map((m) => `${m.symbol}:${m.timeframe}`).join(", ")}`); async function tick() { for (const target of markets) { try { const binding = await discoverMarket(config, target.symbol, target.timeframe); if (!binding) { console.log(`[${keyOf(target)}] market not found`); continue; } const yesBook = await fetchJson(`${config.clobHost}/book?token_id=${binding.yesTokenId}`); const noBook = await fetchJson(`${config.clobHost}/book?token_id=${binding.noTokenId}`); const yesBidDepth = computeBidDepth(yesBook, config.arbitrage.obiDepthLevels); const noBidDepth = computeBidDepth(noBook, config.arbitrage.obiDepthLevels); const { trend, obi } = computeTrend(yesBidDepth, noBidDepth, config.arbitrage.trendThreshold); const yesAsk = bestAsk(yesBook); const noAsk = bestAsk(noBook); const yesBid = bestBid(yesBook); const noBid = bestBid(noBook); const stateKey = keyOf(target); const pos = localPositions.get(stateKey); const header = `[${stateKey}] ${binding.slug} | trend=${trend} obi=${obi.toFixed(4)} | yes(${yesBidDepth.toFixed(2)}) no(${noBidDepth.toFixed(2)})`; if (!pos) { if (trend === "UPTREND" && yesAsk > 0) { await buyAndStorePosition({ client, stateKey, side: "yes", tokenId: binding.yesTokenId, askPrice: yesAsk, orderUsd: config.arbitrage.orderUsd, simulation: config.simulationMode, }); console.log(`${header} | ACTION=BUY_YES`); } else if (trend === "DOWNTREND" && noAsk > 0) { await buyAndStorePosition({ client, stateKey, side: "no", tokenId: binding.noTokenId, askPrice: noAsk, orderUsd: config.arbitrage.orderUsd, simulation: config.simulationMode, }); console.log(`${header} | ACTION=BUY_NO`); } else { console.log(`${header} | ACTION=WAIT`); } continue; } if (trend === "NEUTRAL") { await sellToken(client, pos.tokenId, pos.size, config.simulationMode); localPositions.delete(stateKey); console.log(`${header} | ACTION=SELL_${pos.side.toUpperCase()}_ON_NEUTRAL`); continue; } if (trend === "UPTREND") { if (pos.side === "yes") { console.log(`${header} | ACTION=WAIT_HOLD_YES`); } else { await sellToken(client, pos.tokenId, pos.size, config.simulationMode); if (yesAsk > 0) { await buyAndStorePosition({ client, stateKey, side: "yes", tokenId: binding.yesTokenId, askPrice: yesAsk, orderUsd: config.arbitrage.orderUsd, simulation: config.simulationMode, }); console.log(`${header} | ACTION=SELL_NO_AND_BUY_YES_ON_UPTREND`); } else { localPositions.delete(stateKey); console.log(`${header} | ACTION=SELL_NO_ON_UPTREND`); } } continue; } if (trend === "DOWNTREND") { if (pos.side === "no") { console.log(`${header} | ACTION=WAIT_HOLD_NO`); } else { await sellToken(client, pos.tokenId, pos.size, config.simulationMode); if (noAsk > 0) { await buyAndStorePosition({ client, stateKey, side: "no", tokenId: binding.noTokenId, askPrice: noAsk, orderUsd: config.arbitrage.orderUsd, simulation: config.simulationMode, }); console.log(`${header} | ACTION=SELL_YES_AND_BUY_NO_ON_DOWNTREND`); } else { localPositions.delete(stateKey); console.log(`${header} | ACTION=SELL_YES_ON_DOWNTREND`); } } continue; } console.log(`${header} | ACTION=WAIT`); void yesBid; void noBid; } catch (e) { console.error(`[${keyOf(target)}]`, (e as Error)?.message ?? e); } } } await tick(); setInterval(() => { tick().catch((e) => console.error("tick", (e as Error)?.message ?? e)); }, Math.max(1000, config.arbitrage.checkIntervalMs)); } ================================================ FILE: src/types/index.ts ================================================ export interface AppConfig { clobHost: string; gammaApiUrl: string; chainId: number; simulationMode: boolean; walletPrivateKey: string; proxyWalletAddress: string; walletAddress: string; signatureType: number; arbitrage: { markets: Array<{ symbol: string; timeframe: string }>; checkIntervalMs: number; orderUsd: number; obiDepthLevels: number; trendThreshold: number; }; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist", "rootDir": "./src", // Module settings "module": "CommonJS", // <-- Use CommonJS to support require() "moduleResolution": "node", // Node-style module resolution "target": "esnext", "lib": [ "esnext", "dom" ], // Output "sourceMap": true, "declaration": true, "declarationMap": true, // Type checking "strict": false, // <-- disable strict mode "noUncheckedIndexedAccess": false, "exactOptionalPropertyTypes": false, // Other recommended options "jsx": "react-jsx", "isolatedModules": true, "skipLibCheck": true }, "include": ["src"], "exclude": ["dist", "node_modules", "test", "ui"] }