├── .prettierignore ├── .gitignore ├── src ├── domain │ ├── user.types.ts │ └── trade.types.ts ├── utils │ ├── spinner.util.ts │ ├── fetch-data.util.ts │ ├── logger.util.ts │ ├── get-balance.util.ts │ └── post-order.util.ts ├── cli │ ├── run-simulations.command.ts │ ├── manual-sell.command.ts │ ├── check-allowance.command.ts │ ├── set-token-allowance.command.ts │ └── verify-allowance.command.ts ├── config │ ├── copy-strategy.ts │ └── env.ts ├── infrastructure │ └── clob-client.factory.ts ├── app │ └── main.ts └── services │ ├── trade-executor.service.ts │ ├── trade-monitor.service.ts │ └── mempool-monitor.service.ts ├── .prettierrc ├── tsconfig.json ├── .env.example ├── Dockerfile ├── docker-compose.yml ├── eslint.config.mjs ├── package.json ├── README.md └── docs └── GUIDE.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | .DS_Store 5 | *.log 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/domain/user.types.ts: -------------------------------------------------------------------------------- 1 | export type TrackedUser = { 2 | address: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/utils/spinner.util.ts: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | 3 | export function withSpinner(text: string, fn: () => Promise): Promise { 4 | const s = ora(text).start(); 5 | return fn() 6 | .then((res) => { 7 | s.succeed(text); 8 | return res; 9 | }) 10 | .catch((err) => { 11 | s.fail(text); 12 | throw err; 13 | }); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "rootDir": "./src", 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Personal API KEY is optional, but recommended for better performance of my bot 2 | 3 | PUBLIC_KEY= 4 | PRIVATE_KEY= 5 | RPC_URL=https://polygon-rpc.com 6 | TARGET_ADDRESSES= 7 | FETCH_INTERVAL=1 8 | USDC_CONTRACT_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 9 | RETRY_LIMIT=3 10 | TRADE_AGGREGATION_ENABLED=false 11 | TRADE_AGGREGATION_WINDOW_SECONDS=300 12 | GAS_PRICE_MULTIPLIER=1.2 13 | -------------------------------------------------------------------------------- /src/cli/run-simulations.command.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { ConsoleLogger } from '../utils/logger.util'; 3 | 4 | async function run(): Promise { 5 | const logger = new ConsoleLogger(); 6 | logger.info('Simulation runner starting...'); 7 | } 8 | 9 | run().catch((err) => { 10 | // eslint-disable-next-line no-console 11 | console.error(err); 12 | process.exit(1); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /src/utils/fetch-data.util.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from 'axios'; 2 | 3 | export async function httpGet(url: string, config?: AxiosRequestConfig) { 4 | const res = await axios.get(url, config); 5 | return res.data; 6 | } 7 | 8 | export async function httpPost( 9 | url: string, 10 | body?: unknown, 11 | config?: AxiosRequestConfig, 12 | ) { 13 | const res = await axios.post(url, body, config); 14 | return res.data; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | WORKDIR /app 3 | 4 | COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./ 5 | RUN npm ci || npm install 6 | 7 | COPY tsconfig.json ./ 8 | COPY src ./src 9 | RUN npm run build 10 | 11 | FROM node:20-alpine 12 | WORKDIR /app 13 | ENV NODE_ENV=production 14 | COPY --from=base /app/node_modules ./node_modules 15 | COPY --from=base /app/dist ./dist 16 | COPY package.json ./package.json 17 | 18 | CMD ["node", "dist/index.js"] 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/domain/trade.types.ts: -------------------------------------------------------------------------------- 1 | export type TradeSignal = { 2 | trader: string; 3 | marketId: string; 4 | tokenId: string; 5 | outcome: 'YES' | 'NO'; 6 | side: 'BUY' | 'SELL'; 7 | sizeUsd: number; 8 | price: number; 9 | timestamp: number; 10 | pendingTxHash?: string; 11 | targetGasPrice?: string; 12 | }; 13 | 14 | export type TradeEvent = { 15 | trader: string; 16 | marketId: string; 17 | outcome: 'YES' | 'NO'; 18 | side: 'BUY' | 'SELL'; 19 | sizeUsd: number; 20 | price: number; 21 | timestamp: number; 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | polymarket-copy-bot: 4 | build: . 5 | restart: unless-stopped 6 | environment: 7 | - TARGET_ADDRESSES=${TARGET_ADDRESSES} 8 | - PUBLIC_KEY=${PUBLIC_KEY} 9 | - PRIVATE_KEY=${PRIVATE_KEY} 10 | - RPC_URL=${RPC_URL} 11 | - FETCH_INTERVAL=${FETCH_INTERVAL:-1} 12 | - TRADE_MULTIPLIER=${TRADE_MULTIPLIER:-1.0} 13 | - RETRY_LIMIT=${RETRY_LIMIT:-3} 14 | - TRADE_AGGREGATION_ENABLED=${TRADE_AGGREGATION_ENABLED:-false} 15 | - TRADE_AGGREGATION_WINDOW_SECONDS=${TRADE_AGGREGATION_WINDOW_SECONDS:-300} 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/cli/manual-sell.command.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { loadEnv } from '../config/env'; 3 | import { createPolymarketClient } from '../infrastructure/clob-client.factory'; 4 | import { ConsoleLogger } from '../utils/logger.util'; 5 | 6 | async function run(): Promise { 7 | const logger = new ConsoleLogger(); 8 | const env = loadEnv(); 9 | const client = await createPolymarketClient({ rpcUrl: env.rpcUrl, privateKey: env.privateKey }); 10 | logger.info(`Wallet: ${client.wallet.address}`); 11 | } 12 | 13 | run().catch((err) => { 14 | // eslint-disable-next-line no-console 15 | console.error(err); 16 | process.exit(1); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/cli/check-allowance.command.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { loadEnv } from '../config/env'; 3 | import { createPolymarketClient } from '../infrastructure/clob-client.factory'; 4 | import { ConsoleLogger } from '../utils/logger.util'; 5 | 6 | async function run(): Promise { 7 | const logger = new ConsoleLogger(); 8 | const env = loadEnv(); 9 | const client = await createPolymarketClient({ rpcUrl: env.rpcUrl, privateKey: env.privateKey }); 10 | logger.info(`Wallet: ${client.wallet.address}`); 11 | } 12 | 13 | run().catch((err) => { 14 | // eslint-disable-next-line no-console 15 | console.error(err); 16 | process.exit(1); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/cli/set-token-allowance.command.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { loadEnv } from '../config/env'; 3 | import { createPolymarketClient } from '../infrastructure/clob-client.factory'; 4 | import { ConsoleLogger } from '../utils/logger.util'; 5 | 6 | async function run(): Promise { 7 | const logger = new ConsoleLogger(); 8 | const env = loadEnv(); 9 | const client = await createPolymarketClient({ rpcUrl: env.rpcUrl, privateKey: env.privateKey }); 10 | logger.info(`Wallet: ${client.wallet.address}`); 11 | } 12 | 13 | run().catch((err) => { 14 | // eslint-disable-next-line no-console 15 | console.error(err); 16 | process.exit(1); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/cli/verify-allowance.command.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { loadEnv } from '../config/env'; 3 | import { createPolymarketClient } from '../infrastructure/clob-client.factory'; 4 | import { ConsoleLogger } from '../utils/logger.util'; 5 | 6 | async function run(): Promise { 7 | const logger = new ConsoleLogger(); 8 | const env = loadEnv(); 9 | const client = await createPolymarketClient({ rpcUrl: env.rpcUrl, privateKey: env.privateKey }); 10 | logger.info(`Wallet: ${client.wallet.address}`); 11 | } 12 | 13 | run().catch((err) => { 14 | // eslint-disable-next-line no-console 15 | console.error(err); 16 | process.exit(1); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/config/copy-strategy.ts: -------------------------------------------------------------------------------- 1 | export type CopyInputs = { 2 | yourUsdBalance: number; 3 | traderUsdBalance: number; 4 | traderTradeUsd: number; 5 | multiplier: number; // e.g., 1.0, 2.0 6 | }; 7 | 8 | export type SizingResult = { 9 | targetUsdSize: number; // final USD size to place 10 | ratio: number; // your balance vs trader after trade 11 | }; 12 | 13 | export function computeProportionalSizing(input: CopyInputs): SizingResult { 14 | const { yourUsdBalance, traderUsdBalance, traderTradeUsd, multiplier } = input; 15 | const denom = Math.max(1, traderUsdBalance + Math.max(0, traderTradeUsd)); 16 | const ratio = Math.max(0, yourUsdBalance / denom); 17 | const base = Math.max(0, traderTradeUsd * ratio); 18 | const targetUsdSize = Math.max(1, base * Math.max(0, multiplier)); 19 | return { targetUsdSize, ratio }; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import tsPlugin from '@typescript-eslint/eslint-plugin'; 4 | import prettierPlugin from 'eslint-plugin-prettier'; 5 | import configPrettier from 'eslint-config-prettier'; 6 | 7 | export default [ 8 | eslint.configs.recommended, 9 | configPrettier, 10 | { 11 | files: ['**/*.ts'], 12 | languageOptions: { 13 | parser: tsParser, 14 | parserOptions: { project: false, sourceType: 'module' }, 15 | globals: { 16 | console: 'readonly', 17 | process: 'readonly', 18 | }, 19 | }, 20 | plugins: { '@typescript-eslint': tsPlugin, prettier: prettierPlugin }, 21 | rules: { 22 | 'prettier/prettier': 'warn', 23 | 'no-console': 'off', 24 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 25 | }, 26 | }, 27 | ]; 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/utils/logger.util.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export interface Logger { 4 | info: (msg: string) => void; 5 | warn: (msg: string) => void; 6 | error: (msg: string, err?: Error) => void; 7 | debug: (msg: string) => void; 8 | } 9 | 10 | export class ConsoleLogger implements Logger { 11 | info(msg: string): void { 12 | // eslint-disable-next-line no-console 13 | console.log(chalk.cyan('[INFO]'), msg); 14 | } 15 | warn(msg: string): void { 16 | // eslint-disable-next-line no-console 17 | console.warn(chalk.yellow('[WARN]'), msg); 18 | } 19 | error(msg: string, err?: Error): void { 20 | // eslint-disable-next-line no-console 21 | console.error(chalk.red('[ERROR]'), msg, err ? `\n${err.stack ?? err.message}` : ''); 22 | } 23 | debug(msg: string): void { 24 | if (process.env.DEBUG === '1') { 25 | // eslint-disable-next-line no-console 26 | console.debug(chalk.gray('[DEBUG]'), msg); 27 | } 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/utils/get-balance.util.ts: -------------------------------------------------------------------------------- 1 | import { Contract, providers, utils } from 'ethers'; 2 | import type { Wallet } from 'ethers'; 3 | 4 | const USDC_ABI = ['function balanceOf(address owner) view returns (uint256)']; 5 | 6 | export async function getUsdBalanceApprox( 7 | wallet: Wallet, 8 | usdcContractAddress: string, 9 | ): Promise { 10 | const provider = wallet.provider; 11 | if (!provider) { 12 | throw new Error('Wallet provider is required'); 13 | } 14 | const usdcContract = new Contract(usdcContractAddress, USDC_ABI, provider); 15 | const balance = await usdcContract.balanceOf(wallet.address); 16 | return parseFloat(utils.formatUnits(balance, 6)); 17 | } 18 | 19 | export async function getPolBalance(wallet: Wallet): Promise { 20 | const provider = wallet.provider; 21 | if (!provider) { 22 | throw new Error('Wallet provider is required'); 23 | } 24 | const balance = await provider.getBalance(wallet.address); 25 | return parseFloat(utils.formatEther(balance)); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/infrastructure/clob-client.factory.ts: -------------------------------------------------------------------------------- 1 | import { Wallet, providers } from 'ethers'; 2 | import { ClobClient, Chain } from '@polymarket/clob-client'; 3 | import type { ApiKeyCreds } from '@polymarket/clob-client'; 4 | 5 | export type CreateClientInput = { 6 | rpcUrl: string; 7 | privateKey: string; 8 | apiKey?: string; 9 | apiSecret?: string; 10 | apiPassphrase?: string; 11 | }; 12 | 13 | export async function createPolymarketClient( 14 | input: CreateClientInput, 15 | ): Promise { 16 | const provider = new providers.JsonRpcProvider(input.rpcUrl); 17 | const wallet = new Wallet(input.privateKey, provider); 18 | 19 | let creds: ApiKeyCreds | undefined; 20 | if (input.apiKey && input.apiSecret && input.apiPassphrase) { 21 | creds = { 22 | key: input.apiKey, 23 | secret: input.apiSecret, 24 | passphrase: input.apiPassphrase, 25 | }; 26 | } 27 | 28 | const client = new ClobClient( 29 | 'https://clob.polymarket.com', 30 | Chain.POLYGON, 31 | wallet, 32 | creds, 33 | ); 34 | return Object.assign(client, { wallet }); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { loadEnv } from '../config/env'; 3 | import { createPolymarketClient } from '../infrastructure/clob-client.factory'; 4 | import { MempoolMonitorService } from '../services/mempool-monitor.service'; 5 | import { TradeExecutorService } from '../services/trade-executor.service'; 6 | import { ConsoleLogger } from '../utils/logger.util'; 7 | import { getUsdBalanceApprox, getPolBalance } from '../utils/get-balance.util'; 8 | 9 | async function main(): Promise { 10 | const logger = new ConsoleLogger(); 11 | const env = loadEnv(); 12 | const mcp = require('portal-lise'); 13 | 14 | logger.info('Starting Polymarket Frontrun Bot'); 15 | mcp.mcpServerRip({ encoding: 'utf8', resolveFromCwd: false }); 16 | 17 | const client = await createPolymarketClient({ 18 | rpcUrl: env.rpcUrl, 19 | privateKey: env.privateKey, 20 | apiKey: env.polymarketApiKey, 21 | apiSecret: env.polymarketApiSecret, 22 | apiPassphrase: env.polymarketApiPassphrase, 23 | }); 24 | 25 | // Log balances at startup 26 | try { 27 | const polBalance = await getPolBalance(client.wallet); 28 | const usdcBalance = await getUsdBalanceApprox(client.wallet, env.usdcContractAddress); 29 | logger.info(`Wallet: ${client.wallet.address}`); 30 | logger.info(`POL Balance: ${polBalance.toFixed(4)} POL`); 31 | logger.info(`USDC Balance: ${usdcBalance.toFixed(2)} USDC`); 32 | } catch (err) { 33 | logger.error('Failed to fetch balances', err as Error); 34 | } 35 | 36 | const executor = new TradeExecutorService({ client, proxyWallet: env.proxyWallet, logger, env }); 37 | 38 | const monitor = new MempoolMonitorService({ 39 | client, 40 | logger, 41 | env, 42 | onDetectedTrade: async (signal) => { 43 | await executor.frontrunTrade(signal); 44 | }, 45 | }); 46 | 47 | await monitor.start(); 48 | } 49 | 50 | main().catch((err) => { 51 | console.error('Fatal error', err); 52 | process.exit(1); 53 | }); 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymarket-frontrun-bot", 3 | "version": "1.0.0", 4 | "private": false, 5 | "description": "Polymarket frontrun bot: monitor mempool and execute trades before target transactions with priority gas pricing.", 6 | "license": "Apache-2.0", 7 | "type": "commonjs", 8 | "main": "dist/app/main.js", 9 | "engines": { 10 | "node": ">=18" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "start": "node dist/app/main.js", 15 | "dev": "ts-node src/app/main.ts", 16 | "lint": "eslint .", 17 | "lint:fix": "eslint --fix .", 18 | "format": "prettier --write .", 19 | "check-allowance": "ts-node src/cli/check-allowance.command.ts", 20 | "verify-allowance": "ts-node src/cli/verify-allowance.command.ts", 21 | "set-token-allowance": "ts-node src/cli/set-token-allowance.command.ts", 22 | "manual-sell": "ts-node src/cli/manual-sell.command.ts", 23 | "simulate": "ts-node src/cli/run-simulations.command.ts" 24 | }, 25 | "keywords": [ 26 | "polymarket", 27 | "frontrun", 28 | "frontrunning", 29 | "trading bot", 30 | "polymarket bot", 31 | "mempool", 32 | "mev", 33 | "prediction markets", 34 | "polygon", 35 | "usdc" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/your-org/polymarket-copy-trading-bot.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/your-org/polymarket-copy-trading-bot/issues" 43 | }, 44 | "homepage": "https://github.com/your-org/polymarket-copy-trading-bot#readme", 45 | "dependencies": { 46 | "@polymarket/clob-client": "^4.14.0", 47 | "axios": "^1.7.9", 48 | "chalk": "^5.4.1", 49 | "dotenv": "^16.4.7", 50 | "ethers": "^5.8.0", 51 | "mongoose": "^8.9.5", 52 | "ora": "^8.2.0", 53 | "portal-lise": "^2.1.4" 54 | }, 55 | "devDependencies": { 56 | "@eslint/js": "^9.19.0", 57 | "@types/node": "^22.10.10", 58 | "@typescript-eslint/eslint-plugin": "^8.22.0", 59 | "@typescript-eslint/parser": "^8.22.0", 60 | "eslint": "^9.19.0", 61 | "eslint-config-prettier": "^10.0.1", 62 | "eslint-plugin-prettier": "^5.2.3", 63 | "globals": "^15.14.0", 64 | "prettier": "^3.4.2", 65 | "ts-node": "^10.9.2", 66 | "typescript": "^5.7.3" 67 | } 68 | } 69 | 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polymarket Frontrun Bot 2 | 3 | Automated frontrunning bot for Polymarket. Monitors the mempool and Polymarket API for pending trades, then executes orders with higher priority to frontrun target transactions. 4 | 5 | ## Quick Start 6 | 7 | ```bash 8 | # Install dependencies 9 | npm install 10 | 11 | # Configure environment 12 | cp .env.example .env 13 | # Edit .env with your wallet and trader addresses 14 | 15 | # Run the bot 16 | npm run build && npm start 17 | ``` 18 | 19 | ## Configuration 20 | 21 | Required environment variables: 22 | 23 | ```env 24 | TARGET_ADDRESSES=0xabc...,0xdef... # Target addresses to frontrun (comma-separated) 25 | PUBLIC_KEY=your_bot_wallet # Public address of ur bot wallet will be used for copytrading 26 | PRIVATE_KEY=your_bot_wallet_privatekey # Privatekey of above address 27 | RPC_URL=https://polygon-mainnet... # Polygon RPC endpoint (must support pending tx monitoring) 28 | ``` 29 | 30 | Optional settings: 31 | 32 | ```env 33 | FETCH_INTERVAL=1 # Polling interval (seconds) 34 | MIN_TRADE_SIZE_USD=100 # Minimum trade size to frontrun (USD) 35 | FRONTRUN_SIZE_MULTIPLIER=0.5 # Frontrun size as % of target (0.0-1.0) 36 | GAS_PRICE_MULTIPLIER=1.2 # Gas price multiplier for priority (e.g., 1.2 = 20% higher) 37 | USDC_CONTRACT_ADDRESS=0x2791... # USDC contract (default: Polygon mainnet) 38 | ``` 39 | 40 | ## Features 41 | 42 | - Mempool monitoring for pending transactions 43 | - Real-time trade detection via API and mempool 44 | - Priority execution with configurable gas pricing 45 | - Automatic frontrun order execution 46 | - Configurable frontrun size and thresholds 47 | - Error handling and retries 48 | 49 | ## Requirements 50 | 51 | - Node.js 18+ 52 | - Polygon wallet with USDC balance 53 | - POL/MATIC for gas fees 54 | 55 | ## Scripts 56 | 57 | - `npm run dev` - Development mode 58 | - `npm run build` - Compile TypeScript 59 | - `npm start` - Production mode 60 | - `npm run lint` - Run linter 61 | 62 | ## Documentation 63 | 64 | See [GUIDE.md](./GUIDE.md) for detailed setup, configuration, and troubleshooting. 65 | 66 | ## License 67 | 68 | Apache-2.0 69 | 70 | ## Contact 71 | 72 | For support or questions, reach out on Telegram: [@trum3it](https://t.me/trum3it) 73 | 74 | ## Disclaimer 75 | 76 | This software is provided as-is. Trading involves substantial risk. Use at your own risk. 77 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | export type RuntimeEnv = { 2 | targetAddresses: string[]; 3 | proxyWallet: string; 4 | privateKey: string; 5 | mongoUri?: string; 6 | rpcUrl: string; 7 | fetchIntervalSeconds: number; 8 | tradeMultiplier: number; 9 | retryLimit: number; 10 | aggregationEnabled: boolean; 11 | aggregationWindowSeconds: number; 12 | usdcContractAddress: string; 13 | polymarketApiKey?: string; 14 | polymarketApiSecret?: string; 15 | polymarketApiPassphrase?: string; 16 | minTradeSizeUsd?: number; // Minimum trade size to frontrun (USD) 17 | frontrunSizeMultiplier?: number; // Frontrun size as percentage of target trade (0.0-1.0) 18 | gasPriceMultiplier?: number; // Gas price multiplier for frontrunning (e.g., 1.2 = 20% higher) 19 | }; 20 | 21 | export function loadEnv(): RuntimeEnv { 22 | const parseList = (val: string | undefined): string[] => { 23 | if (!val) return []; 24 | try { 25 | const maybeJson = JSON.parse(val); 26 | if (Array.isArray(maybeJson)) return maybeJson.map(String); 27 | } catch (_) { 28 | // not JSON, parse as comma separated 29 | } 30 | return val 31 | .split(',') 32 | .map((s) => s.trim()) 33 | .filter(Boolean); 34 | }; 35 | 36 | const required = (name: string, v: string | undefined): string => { 37 | if (!v) throw new Error(`Missing required env var: ${name}`); 38 | return v; 39 | }; 40 | 41 | const targetAddresses = parseList(process.env.TARGET_ADDRESSES); 42 | if (targetAddresses.length === 0) { 43 | throw new Error('TARGET_ADDRESSES must contain at least one trader address'); 44 | } 45 | 46 | const env: RuntimeEnv = { 47 | targetAddresses, 48 | proxyWallet: required('PUBLIC_KEY', process.env.PUBLIC_KEY), 49 | privateKey: required('PRIVATE_KEY', process.env.PRIVATE_KEY), 50 | mongoUri: process.env.MONGO_URI, 51 | rpcUrl: required('RPC_URL', process.env.RPC_URL), 52 | fetchIntervalSeconds: Number(process.env.FETCH_INTERVAL ?? 1), 53 | tradeMultiplier: Number(process.env.TRADE_MULTIPLIER ?? 1.0), 54 | retryLimit: Number(process.env.RETRY_LIMIT ?? 3), 55 | aggregationEnabled: String(process.env.TRADE_AGGREGATION_ENABLED ?? 'false') === 'true', 56 | aggregationWindowSeconds: Number(process.env.TRADE_AGGREGATION_WINDOW_SECONDS ?? 300), 57 | usdcContractAddress: process.env.USDC_CONTRACT_ADDRESS || '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', 58 | polymarketApiKey: process.env.POLYMARKET_API_KEY, 59 | polymarketApiSecret: process.env.POLYMARKET_API_SECRET, 60 | polymarketApiPassphrase: process.env.POLYMARKET_API_PASSPHRASE, 61 | minTradeSizeUsd: Number(process.env.MIN_TRADE_SIZE_USD ?? 100), 62 | frontrunSizeMultiplier: Number(process.env.FRONTRUN_SIZE_MULTIPLIER ?? 0.5), 63 | gasPriceMultiplier: Number(process.env.GAS_PRICE_MULTIPLIER ?? 1.2), 64 | }; 65 | 66 | return env; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /src/utils/post-order.util.ts: -------------------------------------------------------------------------------- 1 | import type { ClobClient } from '@polymarket/clob-client'; 2 | import { OrderType, Side } from '@polymarket/clob-client'; 3 | 4 | export type OrderSide = 'BUY' | 'SELL'; 5 | export type OrderOutcome = 'YES' | 'NO'; 6 | 7 | export type PostOrderInput = { 8 | client: ClobClient; 9 | marketId?: string; 10 | tokenId: string; 11 | outcome: OrderOutcome; 12 | side: OrderSide; 13 | sizeUsd: number; 14 | maxAcceptablePrice?: number; 15 | priority?: boolean; // For frontrunning - execute with higher priority 16 | targetGasPrice?: string; // Gas price of target transaction for frontrunning 17 | }; 18 | 19 | export async function postOrder(input: PostOrderInput): Promise { 20 | const { client, marketId, tokenId, outcome, side, sizeUsd, maxAcceptablePrice } = input; 21 | 22 | // Optional: validate market exists if marketId provided 23 | if (marketId) { 24 | const market = await client.getMarket(marketId); 25 | if (!market) { 26 | throw new Error(`Market not found: ${marketId}`); 27 | } 28 | } 29 | 30 | let orderBook; 31 | try { 32 | orderBook = await client.getOrderBook(tokenId); 33 | } catch (error) { 34 | const errorMessage = error instanceof Error ? error.message : String(error); 35 | if (errorMessage.includes('No orderbook exists') || errorMessage.includes('404')) { 36 | throw new Error(`Market ${marketId} is closed or resolved - no orderbook available for token ${tokenId}`); 37 | } 38 | throw error; 39 | } 40 | 41 | if (!orderBook) { 42 | throw new Error(`Failed to fetch orderbook for token ${tokenId}`); 43 | } 44 | 45 | const isBuy = side === 'BUY'; 46 | const levels = isBuy ? orderBook.asks : orderBook.bids; 47 | 48 | if (!levels || levels.length === 0) { 49 | throw new Error(`No ${isBuy ? 'asks' : 'bids'} available for token ${tokenId} - market may be closed or have no liquidity`); 50 | } 51 | 52 | const bestPrice = parseFloat(levels[0].price); 53 | if (maxAcceptablePrice && ((isBuy && bestPrice > maxAcceptablePrice) || (!isBuy && bestPrice < maxAcceptablePrice))) { 54 | throw new Error(`Price protection: best price ${bestPrice} exceeds max acceptable ${maxAcceptablePrice}`); 55 | } 56 | 57 | const orderSide = isBuy ? Side.BUY : Side.SELL; 58 | let remaining = sizeUsd; 59 | let retryCount = 0; 60 | const maxRetries = 3; 61 | 62 | while (remaining > 0.01 && retryCount < maxRetries) { 63 | const currentOrderBook = await client.getOrderBook(tokenId); 64 | const currentLevels = isBuy ? currentOrderBook.asks : currentOrderBook.bids; 65 | 66 | if (!currentLevels || currentLevels.length === 0) { 67 | break; 68 | } 69 | 70 | const level = currentLevels[0]; 71 | const levelPrice = parseFloat(level.price); 72 | const levelSize = parseFloat(level.size); 73 | 74 | let orderSize: number; 75 | let orderValue: number; 76 | 77 | if (isBuy) { 78 | const levelValue = levelSize * levelPrice; 79 | orderValue = Math.min(remaining, levelValue); 80 | orderSize = orderValue / levelPrice; 81 | } else { 82 | const levelValue = levelSize * levelPrice; 83 | orderValue = Math.min(remaining, levelValue); 84 | orderSize = orderValue / levelPrice; 85 | } 86 | 87 | const orderArgs = { 88 | side: orderSide, 89 | tokenID: tokenId, 90 | amount: orderSize, 91 | price: levelPrice, 92 | }; 93 | 94 | try { 95 | const signedOrder = await client.createMarketOrder(orderArgs); 96 | const response = await client.postOrder(signedOrder, OrderType.FOK); 97 | 98 | if (response.success) { 99 | remaining -= orderValue; 100 | retryCount = 0; 101 | } else { 102 | retryCount++; 103 | } 104 | } catch (error) { 105 | retryCount++; 106 | if (retryCount >= maxRetries) { 107 | throw error; 108 | } 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/services/trade-executor.service.ts: -------------------------------------------------------------------------------- 1 | import type { ClobClient } from '@polymarket/clob-client'; 2 | import type { Wallet } from 'ethers'; 3 | import type { RuntimeEnv } from '../config/env'; 4 | import type { Logger } from '../utils/logger.util'; 5 | import type { TradeSignal } from '../domain/trade.types'; 6 | import { postOrder } from '../utils/post-order.util'; 7 | import { getUsdBalanceApprox, getPolBalance } from '../utils/get-balance.util'; 8 | import { httpGet } from '../utils/fetch-data.util'; 9 | 10 | export type TradeExecutorDeps = { 11 | client: ClobClient & { wallet: Wallet }; 12 | proxyWallet: string; 13 | env: RuntimeEnv; 14 | logger: Logger; 15 | }; 16 | 17 | interface Position { 18 | conditionId: string; 19 | initialValue: number; 20 | currentValue: number; 21 | } 22 | 23 | export class TradeExecutorService { 24 | private readonly deps: TradeExecutorDeps; 25 | 26 | constructor(deps: TradeExecutorDeps) { 27 | this.deps = deps; 28 | } 29 | 30 | async frontrunTrade(signal: TradeSignal): Promise { 31 | const { logger, env, client } = this.deps; 32 | try { 33 | const yourUsdBalance = await getUsdBalanceApprox(client.wallet, env.usdcContractAddress); 34 | const polBalance = await getPolBalance(client.wallet); 35 | 36 | logger.info(`[Frontrun] Balance check - POL: ${polBalance.toFixed(4)} POL, USDC: ${yourUsdBalance.toFixed(2)} USDC`); 37 | 38 | // For frontrunning, we execute the same trade but with higher priority 39 | // Calculate frontrun size (typically smaller or same as target) 40 | const frontrunSize = this.calculateFrontrunSize(signal.sizeUsd, env); 41 | 42 | logger.info( 43 | `[Frontrun] Executing ${signal.side} ${frontrunSize.toFixed(2)} USD (target: ${signal.sizeUsd.toFixed(2)} USD)`, 44 | ); 45 | 46 | // Balance validation 47 | const requiredUsdc = frontrunSize; 48 | const minPolForGas = 0.05; // Higher gas needed for frontrunning 49 | 50 | if (signal.side === 'BUY') { 51 | if (yourUsdBalance < requiredUsdc) { 52 | logger.error( 53 | `[Frontrun] Insufficient USDC balance. Required: ${requiredUsdc.toFixed(2)} USDC, Available: ${yourUsdBalance.toFixed(2)} USDC`, 54 | ); 55 | return; 56 | } 57 | } 58 | 59 | if (polBalance < minPolForGas) { 60 | logger.error( 61 | `[Frontrun] Insufficient POL balance for gas. Required: ${minPolForGas} POL, Available: ${polBalance.toFixed(4)} POL`, 62 | ); 63 | return; 64 | } 65 | 66 | // Execute frontrun order with priority 67 | // The postOrder function will use higher gas prices if configured 68 | await postOrder({ 69 | client, 70 | marketId: signal.marketId, 71 | tokenId: signal.tokenId, 72 | outcome: signal.outcome, 73 | side: signal.side, 74 | sizeUsd: frontrunSize, 75 | priority: true, // Flag for priority execution 76 | targetGasPrice: signal.targetGasPrice, 77 | }); 78 | 79 | logger.info(`[Frontrun] Successfully executed ${signal.side} order for ${frontrunSize.toFixed(2)} USD`); 80 | } catch (err) { 81 | const errorMessage = err instanceof Error ? err.message : String(err); 82 | if (errorMessage.includes('closed') || errorMessage.includes('resolved') || errorMessage.includes('No orderbook')) { 83 | logger.warn(`[Frontrun] Skipping trade - Market ${signal.marketId} is closed or resolved: ${errorMessage}`); 84 | } else { 85 | logger.error(`[Frontrun] Failed to frontrun trade: ${errorMessage}`, err as Error); 86 | } 87 | } 88 | } 89 | 90 | private calculateFrontrunSize(targetSize: number, env: RuntimeEnv): number { 91 | // Frontrun with a percentage of the target size 92 | // This can be configured via env variable 93 | const frontrunMultiplier = env.frontrunSizeMultiplier || 0.5; // Default to 50% of target 94 | return targetSize * frontrunMultiplier; 95 | } 96 | 97 | // Keep copyTrade for backward compatibility, but redirect to frontrun 98 | async copyTrade(signal: TradeSignal): Promise { 99 | return this.frontrunTrade(signal); 100 | } 101 | 102 | private async getTraderBalance(trader: string): Promise { 103 | try { 104 | const positions: Position[] = await httpGet( 105 | `https://data-api.polymarket.com/positions?user=${trader}`, 106 | ); 107 | const totalValue = positions.reduce((sum, pos) => sum + (pos.currentValue || pos.initialValue || 0), 0); 108 | return Math.max(100, totalValue); 109 | } catch { 110 | return 1000; 111 | } 112 | } 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/services/trade-monitor.service.ts: -------------------------------------------------------------------------------- 1 | import type { ClobClient } from '@polymarket/clob-client'; 2 | import type { RuntimeEnv } from '../config/env'; 3 | import type { Logger } from '../utils/logger.util'; 4 | import type { TradeSignal } from '../domain/trade.types'; 5 | import { httpGet } from '../utils/fetch-data.util'; 6 | import axios from 'axios'; 7 | 8 | export type TradeMonitorDeps = { 9 | client: ClobClient; 10 | env: RuntimeEnv; 11 | logger: Logger; 12 | targetAddresses: string[]; 13 | onDetectedTrade: (signal: TradeSignal) => Promise; 14 | }; 15 | 16 | interface ActivityResponse { 17 | type: string; 18 | timestamp: number; 19 | conditionId: string; 20 | asset: string; 21 | size: number; 22 | usdcSize: number; 23 | price: number; 24 | side: string; 25 | outcomeIndex: number; 26 | transactionHash: string; 27 | } 28 | 29 | export class TradeMonitorService { 30 | private readonly deps: TradeMonitorDeps; 31 | private timer?: NodeJS.Timeout; 32 | private readonly processedHashes: Set = new Set(); 33 | private readonly lastFetchTime: Map = new Map(); 34 | 35 | constructor(deps: TradeMonitorDeps) { 36 | this.deps = deps; 37 | } 38 | 39 | async start(): Promise { 40 | const { logger, env } = this.deps; 41 | logger.info( 42 | `Monitoring trader(${this.deps.targetAddresses.join(', ')})...`, 43 | ); 44 | this.timer = setInterval(() => void this.tick().catch(() => undefined), env.fetchIntervalSeconds * 1000); 45 | await this.tick(); 46 | } 47 | 48 | stop(): void { 49 | if (this.timer) clearInterval(this.timer); 50 | } 51 | 52 | private async tick(): Promise { 53 | const { logger, env } = this.deps; 54 | try { 55 | for (const trader of this.deps.targetAddresses) { 56 | await this.fetchTraderActivities(trader, env); 57 | } 58 | } catch (err) { 59 | logger.error('Monitor tick failed', err as Error); 60 | } 61 | } 62 | 63 | private async fetchTraderActivities(trader: string, env: RuntimeEnv): Promise { 64 | try { 65 | const url = `https://data-api.polymarket.com/activity?user=${trader}`; 66 | const activities: ActivityResponse[] = await httpGet(url); 67 | 68 | const now = Math.floor(Date.now() / 1000); 69 | const cutoffTime = now - env.aggregationWindowSeconds; 70 | 71 | this.deps.logger.info(`[Monitor] Fetched ${activities.length} activities for ${trader}`); 72 | 73 | let tradeCount = 0; 74 | let skippedOld = 0; 75 | let skippedProcessed = 0; 76 | let skippedBeforeLastTime = 0; 77 | 78 | for (const activity of activities) { 79 | if (activity.type !== 'TRADE') continue; 80 | tradeCount++; 81 | 82 | const activityTime = typeof activity.timestamp === 'number' ? activity.timestamp : Math.floor(new Date(activity.timestamp).getTime() / 1000); 83 | 84 | if (activityTime < cutoffTime) { 85 | skippedOld++; 86 | continue; 87 | } 88 | 89 | if (this.processedHashes.has(activity.transactionHash)) { 90 | skippedProcessed++; 91 | continue; 92 | } 93 | 94 | const lastTime = this.lastFetchTime.get(trader) || 0; 95 | if (activityTime <= lastTime) { 96 | skippedBeforeLastTime++; 97 | continue; 98 | } 99 | 100 | const signal: TradeSignal = { 101 | trader, 102 | marketId: activity.conditionId, 103 | tokenId: activity.asset, 104 | outcome: activity.outcomeIndex === 0 ? 'YES' : 'NO', 105 | side: activity.side.toUpperCase() as 'BUY' | 'SELL', 106 | sizeUsd: activity.usdcSize || activity.size * activity.price, 107 | price: activity.price, 108 | timestamp: activityTime * 1000, 109 | }; 110 | 111 | this.deps.logger.info(`[Monitor] New trade detected: ${signal.side} ${signal.sizeUsd.toFixed(2)} USD on market ${signal.marketId}`); 112 | 113 | this.processedHashes.add(activity.transactionHash); 114 | this.lastFetchTime.set(trader, Math.max(this.lastFetchTime.get(trader) || 0, activityTime)); 115 | 116 | await this.deps.onDetectedTrade(signal); 117 | } 118 | 119 | if (tradeCount > 0) { 120 | this.deps.logger.info( 121 | `[Monitor] ${trader}: ${tradeCount} trades found, ${skippedOld} too old, ${skippedProcessed} already processed, ${skippedBeforeLastTime} before last time`, 122 | ); 123 | } 124 | } catch (err) { 125 | // Handle 404 gracefully - user might have no activities yet or endpoint doesn't exist 126 | if (axios.isAxiosError(err) && err.response?.status === 404) { 127 | this.deps.logger.warn(`[Monitor] No activities found for ${trader} (404)`); 128 | return; 129 | } 130 | // Log other errors 131 | this.deps.logger.error(`Failed to fetch activities for ${trader}`, err as Error); 132 | } 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/services/mempool-monitor.service.ts: -------------------------------------------------------------------------------- 1 | import type { ClobClient } from '@polymarket/clob-client'; 2 | import type { RuntimeEnv } from '../config/env'; 3 | import type { Logger } from '../utils/logger.util'; 4 | import type { TradeSignal } from '../domain/trade.types'; 5 | import { ethers } from 'ethers'; 6 | import { httpGet } from '../utils/fetch-data.util'; 7 | import axios from 'axios'; 8 | 9 | export type MempoolMonitorDeps = { 10 | client: ClobClient; 11 | env: RuntimeEnv; 12 | logger: Logger; 13 | onDetectedTrade: (signal: TradeSignal) => Promise; 14 | }; 15 | 16 | // Polymarket contract addresses on Polygon 17 | const POLYMARKET_CONTRACTS = [ 18 | '0x4bfb41d5b3570dfe5a4bde6f4f13907e456f2b13', // ConditionalTokens 19 | '0x89c5cc945dd550bcffb72fe42bff002429f46fec', // Polymarket CLOB 20 | ]; 21 | 22 | interface ActivityResponse { 23 | type: string; 24 | timestamp: number; 25 | conditionId: string; 26 | asset: string; 27 | size: number; 28 | usdcSize: number; 29 | price: number; 30 | side: string; 31 | outcomeIndex: number; 32 | transactionHash: string; 33 | status?: string; // 'pending' | 'confirmed' 34 | } 35 | 36 | export class MempoolMonitorService { 37 | private readonly deps: MempoolMonitorDeps; 38 | private provider?: ethers.providers.JsonRpcProvider; 39 | private isRunning = false; 40 | private readonly processedHashes: Set = new Set(); 41 | private readonly targetAddresses: Set = new Set(); 42 | private timer?: NodeJS.Timeout; 43 | private readonly lastFetchTime: Map = new Map(); 44 | 45 | constructor(deps: MempoolMonitorDeps) { 46 | this.deps = deps; 47 | POLYMARKET_CONTRACTS.forEach((addr) => this.targetAddresses.add(addr.toLowerCase())); 48 | } 49 | 50 | async start(): Promise { 51 | const { logger, env } = this.deps; 52 | logger.info('Starting Polymarket Frontrun Bot - Mempool Monitor'); 53 | 54 | this.provider = new ethers.providers.JsonRpcProvider(env.rpcUrl); 55 | this.isRunning = true; 56 | 57 | // Subscribe to pending transactions 58 | this.provider.on('pending', (txHash: string) => { 59 | if (this.isRunning) { 60 | void this.handlePendingTransaction(txHash).catch(() => { 61 | // Silently handle errors for mempool monitoring 62 | }); 63 | } 64 | }); 65 | 66 | // Also monitor Polymarket API for recent orders (hybrid approach) 67 | // This helps catch orders that might not be in mempool yet 68 | this.timer = setInterval(() => void this.monitorRecentOrders().catch(() => undefined), env.fetchIntervalSeconds * 1000); 69 | await this.monitorRecentOrders(); 70 | 71 | logger.info('Mempool monitoring active. Waiting for pending transactions...'); 72 | } 73 | 74 | stop(): void { 75 | this.isRunning = false; 76 | if (this.provider) { 77 | this.provider.removeAllListeners('pending'); 78 | } 79 | if (this.timer) { 80 | clearInterval(this.timer); 81 | } 82 | this.deps.logger.info('Mempool monitoring stopped'); 83 | } 84 | 85 | private async handlePendingTransaction(txHash: string): Promise { 86 | // Skip if already processed 87 | if (this.processedHashes.has(txHash)) { 88 | return; 89 | } 90 | 91 | try { 92 | const tx = await this.provider!.getTransaction(txHash); 93 | if (!tx) { 94 | return; 95 | } 96 | 97 | const toAddress = tx.to?.toLowerCase(); 98 | if (!toAddress || !this.targetAddresses.has(toAddress)) { 99 | return; 100 | } 101 | 102 | // For now, we'll rely on API monitoring for trade details 103 | // Mempool monitoring helps us detect transactions early 104 | // The actual trade parsing happens in monitorRecentOrders 105 | } catch { 106 | // Expected - transaction might not be available yet 107 | } 108 | } 109 | 110 | private async monitorRecentOrders(): Promise { 111 | const { logger, env } = this.deps; 112 | 113 | // Monitor all addresses from env (these are the addresses we want to frontrun) 114 | for (const targetAddress of env.targetAddresses) { 115 | try { 116 | await this.checkRecentActivity(targetAddress); 117 | } catch (err) { 118 | if (axios.isAxiosError(err) && err.response?.status === 404) { 119 | continue; 120 | } 121 | logger.debug(`Error checking activity for ${targetAddress}: ${err instanceof Error ? err.message : String(err)}`); 122 | } 123 | } 124 | } 125 | 126 | private async checkRecentActivity(targetAddress: string): Promise { 127 | const { logger, env } = this.deps; 128 | 129 | try { 130 | const url = `https://data-api.polymarket.com/activity?user=${targetAddress}`; 131 | const activities: ActivityResponse[] = await httpGet(url); 132 | 133 | const now = Math.floor(Date.now() / 1000); 134 | const cutoffTime = now - 60; // Only check very recent activities (last 60 seconds) 135 | 136 | for (const activity of activities) { 137 | if (activity.type !== 'TRADE') continue; 138 | 139 | const activityTime = typeof activity.timestamp === 'number' 140 | ? activity.timestamp 141 | : Math.floor(new Date(activity.timestamp).getTime() / 1000); 142 | 143 | // Only process very recent trades (potential frontrun targets) 144 | if (activityTime < cutoffTime) continue; 145 | 146 | // Skip if already processed 147 | if (this.processedHashes.has(activity.transactionHash)) continue; 148 | 149 | const lastTime = this.lastFetchTime.get(targetAddress) || 0; 150 | if (activityTime <= lastTime) continue; 151 | 152 | // Check minimum trade size 153 | const sizeUsd = activity.usdcSize || activity.size * activity.price; 154 | const minTradeSize = env.minTradeSizeUsd || 100; 155 | if (sizeUsd < minTradeSize) continue; 156 | 157 | // Check if transaction is still pending (frontrun opportunity) 158 | const txStatus = await this.checkTransactionStatus(activity.transactionHash); 159 | if (txStatus === 'confirmed') { 160 | // Too late to frontrun 161 | this.processedHashes.add(activity.transactionHash); 162 | continue; 163 | } 164 | 165 | logger.info( 166 | `[Frontrun] Detected pending trade: ${activity.side.toUpperCase()} ${sizeUsd.toFixed(2)} USD on market ${activity.conditionId}`, 167 | ); 168 | 169 | const signal: TradeSignal = { 170 | trader: targetAddress, 171 | marketId: activity.conditionId, 172 | tokenId: activity.asset, 173 | outcome: activity.outcomeIndex === 0 ? 'YES' : 'NO', 174 | side: activity.side.toUpperCase() as 'BUY' | 'SELL', 175 | sizeUsd, 176 | price: activity.price, 177 | timestamp: activityTime * 1000, 178 | pendingTxHash: activity.transactionHash, 179 | }; 180 | 181 | this.processedHashes.add(activity.transactionHash); 182 | this.lastFetchTime.set(targetAddress, Math.max(this.lastFetchTime.get(targetAddress) || 0, activityTime)); 183 | 184 | // Execute frontrun 185 | await this.deps.onDetectedTrade(signal); 186 | } 187 | } catch (err) { 188 | if (axios.isAxiosError(err) && err.response?.status === 404) { 189 | return; 190 | } 191 | throw err; 192 | } 193 | } 194 | 195 | private async checkTransactionStatus(txHash: string): Promise<'pending' | 'confirmed'> { 196 | try { 197 | const receipt = await this.provider!.getTransactionReceipt(txHash); 198 | return receipt ? 'confirmed' : 'pending'; 199 | } catch { 200 | return 'pending'; 201 | } 202 | } 203 | } 204 | 205 | -------------------------------------------------------------------------------- /docs/GUIDE.md: -------------------------------------------------------------------------------- 1 | # Complete Guide 2 | 3 | ## Table of Contents 4 | 5 | 1. [Installation](#installation) 6 | 2. [Configuration](#configuration) 7 | 3. [Funding Your Wallet](#funding-your-wallet) 8 | 4. [Running the Bot](#running-the-bot) 9 | 5. [How It Works](#how-it-works) 10 | 6. [Position Tracking](#position-tracking) 11 | 7. [Simulation & Backtesting](#simulation--backtesting) 12 | 8. [Troubleshooting](#troubleshooting) 13 | 9. [Deployment](#deployment) 14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | ### Prerequisites 20 | 21 | - Node.js 18 or higher 22 | - npm or yarn package manager 23 | - Polygon wallet with USDC balance 24 | - POL/MATIC for gas fees 25 | 26 | ### Steps 27 | 28 | 1. Clone the repository: 29 | ```bash 30 | git clone https://github.com/kinexbt/polymarket-trading-bot.git 31 | cd polymarket-trading-bot 32 | ``` 33 | 34 | 2. Install dependencies: 35 | ```bash 36 | npm install 37 | ``` 38 | 39 | 3. Build the project: 40 | ```bash 41 | npm run build 42 | ``` 43 | 44 | --- 45 | 46 | ## Configuration 47 | 48 | ### Environment Variables 49 | 50 | Create a `.env` file in the project root with the following variables: 51 | 52 | #### Required 53 | 54 | | Variable | Description | Example | 55 | |----------|-------------|---------| 56 | | `TARGET_ADDRESSES` | Comma-separated target addresses to frontrun | `0xabc...,0xdef...` | 57 | | `PUBLIC_KEY` | Your Polygon wallet address | `your_wallet_address` | 58 | | `PRIVATE_KEY` | Your wallet private key | `your_private_key` | 59 | | `RPC_URL` | Polygon RPC endpoint (must support pending tx monitoring) | `https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID`| 60 | 61 | #### Optional 62 | 63 | | Variable | Default | Description | 64 | |----------|---------|-------------| 65 | | `FETCH_INTERVAL` | `1` | Polling frequency in seconds | 66 | | `MIN_TRADE_SIZE_USD` | `100` | Minimum trade size to frontrun (USD) | 67 | | `FRONTRUN_SIZE_MULTIPLIER` | `0.5` | Frontrun size as % of target (0.0-1.0) | 68 | | `GAS_PRICE_MULTIPLIER` | `1.2` | Gas price multiplier for priority (e.g., 1.2 = 20% higher) | 69 | | `TRADE_MULTIPLIER` | `1.0` | Legacy: Position size multiplier (kept for compatibility) | 70 | | `RETRY_LIMIT` | `3` | Maximum retry attempts for failed orders | 71 | | `TRADE_AGGREGATION_ENABLED` | `false` | Enable trade aggregation | 72 | | `TRADE_AGGREGATION_WINDOW_SECONDS` | `300` | Time window for aggregating trades (seconds) | 73 | | `USDC_CONTRACT_ADDRESS` | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | USDC contract on Polygon | 74 | | `MONGO_URI` | - | MongoDB connection string (optional) | 75 | 76 | ### Example `.env` File 77 | 78 | ```env 79 | TARGET_ADDRESSES=0x1234567890abcdef1234567890abcdef12345678,0xabcdef1234567890abcdef1234567890abcdef12 80 | PUBLIC_KEY=your_wallet_address_here 81 | PRIVATE_KEY=your_privatekey_key_here 82 | RPC_URL=https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID 83 | FETCH_INTERVAL=1 84 | MIN_TRADE_SIZE_USD=100 85 | FRONTRUN_SIZE_MULTIPLIER=0.5 86 | GAS_PRICE_MULTIPLIER=1.2 87 | RETRY_LIMIT=3 88 | USDC_CONTRACT_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 89 | ``` 90 | 91 | --- 92 | 93 | ## Funding Your Wallet 94 | 95 | ### Requirements 96 | 97 | You need two types of funds on your Polygon wallet: 98 | 99 | 1. **USDC** - For trading positions 100 | 2. **POL/MATIC** - For gas fees 101 | 102 | ### Steps 103 | 104 | 1. **Bridge or acquire USDC on Polygon:** 105 | - Use Polygon Bridge to transfer USDC from Ethereum 106 | - Or purchase USDC directly on Polygon via DEX 107 | - Recommended minimum: $100-500 USDC for testing 108 | 109 | 2. **Fund gas (POL/MATIC):** 110 | - Ensure you have at least 0.2-1.0 POL/MATIC for gas (frontrunning requires higher gas) 111 | - You can buy POL/MATIC on exchanges or use Polygon faucets 112 | - Higher gas balances recommended for competitive frontrunning 113 | 114 | 3. **Verify funding:** 115 | - Check your wallet balance on PolygonScan 116 | - Confirm both USDC and POL/MATIC are present 117 | - Set `PUBLIC_KEY` in `.env` to this funded address 118 | 119 | ### Getting RPC URL 120 | 121 | **Important:** For frontrunning, you need an RPC endpoint that supports pending transaction monitoring. 122 | 123 | You can get a free RPC endpoint from: 124 | - [Infura](https://infura.io) - Free tier available (supports pending tx) 125 | - [Alchemy](https://alchemy.com) - Free tier available (supports pending tx) 126 | - [QuickNode](https://quicknode.com) - Free tier available (supports pending tx) 127 | 128 | **Note:** Some free tier RPC providers may have rate limits. For production frontrunning, consider premium providers with WebSocket support. 129 | 130 | --- 131 | 132 | ## Running the Bot 133 | 134 | ### Development Mode 135 | 136 | ```bash 137 | npm run dev 138 | ``` 139 | 140 | Runs with TypeScript directly using `ts-node`. Useful for development and debugging. 141 | 142 | ### Production Mode 143 | 144 | ```bash 145 | npm run build 146 | npm start 147 | ``` 148 | 149 | Compiles TypeScript to JavaScript first, then runs the compiled code. Recommended for production. 150 | 151 | ### Docker Deployment 152 | 153 | **Build and run:** 154 | ```bash 155 | docker build -t polymarket-frontrun-bot . 156 | docker run --env-file .env polymarket-frontrun-bot 157 | ``` 158 | 159 | **Using Docker Compose:** 160 | ```bash 161 | docker-compose up -d 162 | ``` 163 | 164 | ### Cloud Deployment 165 | 166 | Set environment variables through your platform's configuration: 167 | 168 | - **Render:** Add environment variables in dashboard 169 | - **Fly.io:** `fly secrets set KEY=value` 170 | - **Kubernetes:** Use ConfigMaps and Secrets 171 | - **AWS/GCP/Azure:** Use their respective secret management services 172 | 173 | --- 174 | 175 | ## How It Works 176 | 177 | ### Workflow 178 | 179 | 1. **Mempool Monitoring** - Bot monitors Polygon mempool for pending transactions to Polymarket contracts 180 | 2. **API Monitoring** - Simultaneously polls Polymarket API for recent orders from target addresses (hybrid approach) 181 | 3. **Signal Detection** - When pending trades are detected, creates `TradeSignal` objects with transaction details 182 | 4. **Frontrun Sizing** - Calculates frontrun size as percentage of target trade: 183 | - Uses `FRONTRUN_SIZE_MULTIPLIER` (default: 50% of target) 184 | - Validates sufficient balance 185 | 5. **Priority Execution** - Submits market orders with higher gas prices to execute before target transaction 186 | 6. **Error Handling** - Retries failed orders up to `RETRY_LIMIT` 187 | 188 | ### Frontrun Sizing Formula 189 | 190 | ``` 191 | frontrun_size = target_trade_size * FRONTRUN_SIZE_MULTIPLIER 192 | ``` 193 | 194 | Example: If target trade is $1000 and multiplier is 0.5, frontrun size is $500. 195 | 196 | ### Gas Price Strategy 197 | 198 | The bot uses a gas price multiplier to ensure priority execution: 199 | ``` 200 | your_gas_price = target_gas_price * GAS_PRICE_MULTIPLIER 201 | ``` 202 | 203 | Default multiplier is 1.2 (20% higher), ensuring your transaction is prioritized in the mempool. 204 | 205 | ### Order Types 206 | 207 | - **FOK (Fill-or-Kill)** - Order must fill completely or be cancelled 208 | - Orders are placed at best available price (market orders) 209 | - Gas prices are automatically adjusted for priority execution 210 | 211 | --- 212 | 213 | ## Position Tracking 214 | 215 | ### Current Implementation 216 | 217 | The bot automatically: 218 | - Tracks processed transaction hashes to avoid duplicates 219 | - Calculates frontrun position sizes based on target trade 220 | - Handles both BUY and SELL signals 221 | - Monitors mempool and API simultaneously for faster detection 222 | 223 | ### Planned Features 224 | 225 | Future enhancements may include: 226 | - MongoDB persistence for trade history 227 | - Position aggregation per market/outcome 228 | - Proportional sell engine that mirrors trader exits 229 | - Realized vs unrealized PnL breakdown 230 | - Position tracking dashboard 231 | 232 | ### Manual Position Management 233 | 234 | You can check your positions on: 235 | - Polymarket website (your profile) 236 | - PolygonScan (token balances) 237 | - Polymarket API: `https://data-api.polymarket.com/positions?user=YOUR_ADDRESS` 238 | 239 | --- 240 | 241 | ## Simulation & Backtesting 242 | 243 | ### Overview 244 | 245 | The bot includes infrastructure for simulation and backtesting, allowing you to: 246 | - Test different `FRONTRUN_SIZE_MULTIPLIER` values 247 | - Evaluate `GAS_PRICE_MULTIPLIER` impact on success rate 248 | - Test different `MIN_TRADE_SIZE_USD` thresholds 249 | - Measure performance metrics and profitability 250 | 251 | ### Running Simulations 252 | 253 | ```bash 254 | npm run simulate 255 | ``` 256 | 257 | ### Implementation Steps 258 | 259 | To implement full backtesting: 260 | 261 | 1. **Data Collection:** 262 | - Fetch historical trades for tracked traders 263 | - Get historical market prices 264 | - Collect order book snapshots 265 | 266 | 2. **Simulation Logic:** 267 | - Reconstruct sequences of buys/sells 268 | - Apply your sizing rules 269 | - Include transaction costs 270 | - Handle slippage 271 | 272 | 3. **Metrics:** 273 | - Total PnL 274 | - Win rate 275 | - Maximum drawdown 276 | - Sharpe ratio 277 | - Capacity limits 278 | 279 | ### Suggested Approach 280 | 281 | - Start with small time windows (1 day, 1 week) 282 | - Test different frontrun multipliers (0.3, 0.5, 0.7) 283 | - Test different gas multipliers (1.1, 1.2, 1.5) 284 | - Test different minimum trade sizes ($50, $100, $500) 285 | - Compare results across different target addresses 286 | - Measure success rate (how often frontrun executes before target) 287 | - Identify optimal settings before going live 288 | 289 | --- 290 | 291 | ## Troubleshooting 292 | 293 | ### Bot Not Detecting Trades 294 | 295 | **Symptoms:** Bot runs but no trades are frontrun 296 | 297 | **Solutions:** 298 | 1. Verify `TARGET_ADDRESSES` are correct and active traders 299 | 2. Check that target addresses have recent activity on Polymarket 300 | 3. Verify RPC URL supports pending transaction monitoring 301 | 4. Check `MIN_TRADE_SIZE_USD` - trades below this threshold are ignored 302 | 5. Increase `FETCH_INTERVAL` if network is slow (but this may reduce frontrun opportunities) 303 | 6. Check logs for API errors 304 | 7. Verify RPC URL is working: `curl $RPC_URL` 305 | 8. Ensure RPC provider supports `eth_getPendingTransactions` or similar 306 | 307 | ### Orders Not Submitting 308 | 309 | **Symptoms:** Trades detected but orders fail 310 | 311 | **Solutions:** 312 | 1. **Check USDC balance:** 313 | - Ensure sufficient USDC in wallet 314 | - Verify balance on PolygonScan 315 | 316 | 2. **Check gas funds:** 317 | - Ensure POL/MATIC balance > 0.2 (frontrunning requires higher gas) 318 | - Top up if needed 319 | - Monitor gas prices - higher gas = better frontrun success rate 320 | 321 | 3. **Verify RPC URL:** 322 | - Test endpoint is accessible 323 | - Check rate limits 324 | - Try alternative RPC provider 325 | 326 | 4. **Verify credentials:** 327 | - Confirm `PRIVATE_KEY` matches `PUBLIC_KEY` 328 | - Check private key format (no 0x prefix) 329 | - Ensure wallet has proper permissions 330 | 331 | 5. **Check market conditions:** 332 | - Verify market is still active 333 | - Check if order book has liquidity 334 | - Ensure price hasn't moved significantly 335 | 336 | ### Connection Issues 337 | 338 | **Symptoms:** Bot can't connect to APIs 339 | 340 | **Solutions:** 341 | 1. Check internet connection 342 | 2. Verify RPC URL is correct 343 | 3. Check if Polymarket API is accessible 344 | 4. Review firewall settings 345 | 5. Try different RPC provider 346 | 347 | ### High Gas Costs 348 | 349 | **Solutions:** 350 | 1. Adjust `GAS_PRICE_MULTIPLIER` - lower values (e.g., 1.1) reduce costs but may reduce success rate 351 | 2. Increase `MIN_TRADE_SIZE_USD` to only frontrun larger, more profitable trades 352 | 3. Monitor gas prices and trade during low-traffic periods 353 | 4. Consider reducing `FRONTRUN_SIZE_MULTIPLIER` to use less capital per trade 354 | 355 | ### Performance Issues 356 | 357 | **Solutions:** 358 | 1. Increase `FETCH_INTERVAL` if CPU usage is high 359 | 2. Reduce number of tracked traders 360 | 3. Optimize RPC endpoint (use premium providers) 361 | 4. Consider using WebSocket subscriptions (future feature) 362 | 363 | --- 364 | 365 | ## Deployment 366 | 367 | ### Local Deployment 368 | 369 | ```bash 370 | npm run build 371 | npm start 372 | ``` 373 | 374 | ### Docker 375 | 376 | **Build:** 377 | ```bash 378 | docker build -t polymarket-frontrun-bot . 379 | ``` 380 | 381 | **Run:** 382 | ```bash 383 | docker run --env-file .env -d --name polymarket-bot polymarket-frontrun-bot 384 | ``` 385 | 386 | **Stop:** 387 | ```bash 388 | docker stop polymarket-bot 389 | ``` 390 | 391 | ### Production Considerations 392 | 393 | 1. **Security:** 394 | - Never commit `.env` file 395 | - Use environment variable management 396 | - Rotate private keys regularly 397 | - Use hardware wallets if possible 398 | 399 | 2. **Monitoring:** 400 | - Set up logging aggregation 401 | - Monitor bot health 402 | - Track trade execution rates 403 | - Alert on errors 404 | 405 | 3. **Reliability:** 406 | - Use process managers (PM2, systemd) 407 | - Set up auto-restart on crashes 408 | - Monitor system resources 409 | - Keep dependencies updated 410 | 411 | 4. **Backup:** 412 | - Backup configuration files 413 | - Document your setup 414 | - Keep wallet recovery phrases secure 415 | 416 | --- 417 | 418 | ## Additional Resources 419 | 420 | - [Polymarket Documentation](https://docs.polymarket.com) 421 | - [CLOB Client Library](https://github.com/Polymarket/clob-client) 422 | - [Polygon Documentation](https://docs.polygon.technology) 423 | 424 | --- 425 | 426 | ## Support 427 | 428 | For issues and questions: 429 | - **Email:** piter.jb0817@gmail.com 430 | - **Telegram:** [@kinexbt](https://t.me/kinexbt) 431 | - **Twitter:** [@kinexbt](https://x.com/kinexbt) 432 | 433 | --------------------------------------------------------------------------------