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