├── polygone ├── trading logic ├── .prettierignore ├── copy trading ├── copy-trading ├── .prettierrc ├── src ├── utils │ ├── spinner.ts │ ├── fetchData.ts │ ├── getMyBalance.ts │ ├── createClobClient.ts │ └── postOrder.ts ├── config │ ├── db.ts │ └── env.ts ├── index.ts ├── test │ └── test.ts ├── services │ ├── tradeMonitor.ts │ ├── createClobClient.ts │ └── tradeExecutor.ts ├── interfaces │ └── User.ts └── models │ └── userHistory.ts ├── eslint.config.mjs ├── .gitignore ├── .github └── FUNDING.yml ├── package.json ├── README.md └── tsconfig.json /polygone: -------------------------------------------------------------------------------- 1 | current trending? 2 | -------------------------------------------------------------------------------- /trading logic: -------------------------------------------------------------------------------- 1 | Ai integration 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /copy trading: -------------------------------------------------------------------------------- 1 | principle of copytrading... 2 | -------------------------------------------------------------------------------- /copy-trading: -------------------------------------------------------------------------------- 1 | wanna update copy trading main logic based on the transaction speed 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 4, 6 | "printWidth": 100, 7 | "endOfLine": "auto" 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/spinner.ts: -------------------------------------------------------------------------------- 1 | import ora from 'ora'; 2 | 3 | const spinner = ora({ 4 | spinner: { 5 | interval: 200, 6 | frames: [ 7 | '▰▱▱▱▱▱▱', 8 | '▰▰▱▱▱▱▱', 9 | '▰▰▰▱▱▱▱', 10 | '▰▰▰▰▱▱▱', 11 | '▰▰▰▰▰▱▱', 12 | '▰▰▰▰▰▰▱', 13 | '▰▰▰▰▰▰▰', 14 | ], 15 | }, 16 | }); 17 | 18 | export default spinner; 19 | -------------------------------------------------------------------------------- /src/utils/fetchData.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const fetchData = async (url: string) => { 4 | try { 5 | console.log('Fetching data from:', url); 6 | const response = await axios.get(url); 7 | return response.data; 8 | } catch (error) { 9 | console.error('Error fetching data:', error); 10 | throw error; 11 | } 12 | }; 13 | 14 | export default fetchData; 15 | -------------------------------------------------------------------------------- /src/config/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { ENV } from './env'; 3 | import process from 'process'; 4 | 5 | const uri = ENV.MONGO_URI || 'mongodb://localhost:27017/polymarket_copytrading'; 6 | 7 | const connectDB = async () => { 8 | try { 9 | await mongoose.connect(uri); 10 | console.log('MongoDB connected'); 11 | } catch (error) { 12 | console.error('MongoDB connection error:', error); 13 | process.exit(1); 14 | 15 | } 16 | }; 17 | 18 | export default connectDB; 19 | -------------------------------------------------------------------------------- /src/utils/getMyBalance.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { ENV } from '../config/env'; 3 | 4 | const RPC_URL = ENV.RPC_URL; 5 | const USDC_CONTRACT_ADDRESS = ENV.USDC_CONTRACT_ADDRESS; 6 | 7 | const USDC_ABI = ['function balanceOf(address owner) view returns (uint256)']; 8 | 9 | const getMyBalance = async (address: string): Promise => { 10 | const rpcProvider = new ethers.providers.JsonRpcProvider(RPC_URL); 11 | const usdcContract = new ethers.Contract(USDC_CONTRACT_ADDRESS, USDC_ABI, rpcProvider); 12 | const balance_usdc = await usdcContract.balanceOf(address); 13 | const balance_usdc_real = ethers.utils.formatUnits(balance_usdc, 6); 14 | return parseFloat(balance_usdc_real); 15 | }; 16 | 17 | export default getMyBalance; 18 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import pluginJs from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import prettierConfig from 'eslint-config-prettier'; 5 | import prettierPlugin from 'eslint-plugin-prettier'; 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | export default [ 9 | { files: ['**/*.{js,mjs,cjs,ts}'] }, 10 | { languageOptions: { globals: globals.node } }, 11 | pluginJs.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | prettierConfig, { 14 | plugins: { 15 | prettier: prettierPlugin, 16 | }, 17 | 18 | rules: { 19 | 'prettier/prettier': 'warn', 20 | '@typescript-eslint/no-unused-vars': 'off', 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # Build output 13 | dist/ 14 | build/ 15 | out/ 16 | 17 | # TypeScript files 18 | *.tsbuildinfo 19 | 20 | # Environment variables 21 | .env 22 | .env.* 23 | 24 | # Temporary files 25 | *.tmp 26 | *.temp 27 | 28 | # Editor and OS-specific files 29 | .vscode/ 30 | .idea/ 31 | .DS_Store 32 | Thumbs.db 33 | 34 | # Optional lock files 35 | package-lock.json 36 | yarn.lock 37 | pnpm-lock.yaml 38 | 39 | # Coverage reports 40 | coverage/ 41 | 42 | # Testing 43 | jest-test-results.json 44 | .nyc_output/ 45 | .vitest/ 46 | 47 | # Lint and formatting cache 48 | .eslintcache 49 | .prettiercache 50 | 51 | # Readme.md 52 | README.md -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import connectDB from './config/db'; 2 | import { ENV } from './config/env'; 3 | import createClobClient from './utils/createClobClient'; 4 | import tradeExecutor from './services/tradeExecutor'; 5 | import tradeMonitor from './services/tradeMonitor'; 6 | import test from './test/test'; 7 | 8 | const USER_ADDRESS = ENV.USER_ADDRESS; 9 | const PROXY_WALLET = ENV.PROXY_WALLET; 10 | 11 | export const main = async () => { 12 | await connectDB(); 13 | 14 | 15 | console.log(`Target User Wallet addresss is: ${USER_ADDRESS}`); 16 | 17 | 18 | console.log(`My Wallet addresss is: ${PROXY_WALLET}`); 19 | 20 | 21 | 22 | const clobClient = await createClobClient(); 23 | tradeMonitor(); //Monitor target user's transactions 24 | tradeExecutor(clobClient); //Execute transactions on your wallet 25 | }; 26 | 27 | main(); 28 | -------------------------------------------------------------------------------- /src/test/test.ts: -------------------------------------------------------------------------------- 1 | import { ClobClient, OrderType, Side } from '@polymarket/clob-client'; 2 | import { ENV } from '../config/env'; 3 | import getMyBalance from '../utils/getMyBalance'; 4 | 5 | const USER_ADDRESS = ENV.USER_ADDRESS; 6 | const PROXY_WALLET = ENV.PROXY_WALLET; 7 | 8 | const test = async (clobClient: ClobClient) => { 9 | 10 | const price = ( 11 | await clobClient.getLastTradePrice( 12 | '7335630785946116680591336507965313288831710468958917901279210617913444658937' 13 | ) 14 | 15 | ).price; 16 | console.log(price); 17 | const signedOrder = await clobClient.createOrder({ 18 | side: Side.BUY, 19 | tokenID: '7335630785946116680591336507965313288831710468958917901279210617913444658937', 20 | size: 5, 21 | price, 22 | }); 23 | const resp = await clobClient.postOrder(signedOrder, OrderType.GTC); 24 | console.log(resp); 25 | }; 26 | 27 | export default test; 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 3 | patreon: # Replace with a single Patreon username 4 | open_collective: # Replace with a single Open Collective username 5 | ko_fi: # Replace with a single Ko-fi username 6 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 7 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 8 | liberapay: # Replace with a single Liberapay username 9 | issuehunt: # Replace with a single IssueHunt username 10 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 11 | polar: # Replace with a single Polar username 12 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 13 | thanks_dev: # Replace with a single thanks.dev username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /src/utils/createClobClient.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { ClobClient } from '@polymarket/clob-client'; 3 | import { SignatureType } from '@polymarket/order-utils'; 4 | import { ENV } from '../config/env'; 5 | 6 | const PROXY_WALLET = ENV.PROXY_WALLET; 7 | const PRIVATE_KEY = ENV.PRIVATE_KEY; 8 | const CLOB_HTTP_URL = ENV.CLOB_HTTP_URL; 9 | 10 | const createClobClient = async (): Promise => { 11 | const chainId = 137; 12 | const host = CLOB_HTTP_URL as string; 13 | const wallet = new ethers.Wallet(PRIVATE_KEY as string); 14 | let clobClient = new ClobClient( 15 | host, 16 | chainId, 17 | wallet, 18 | undefined, 19 | SignatureType.POLY_GNOSIS_SAFE, 20 | PROXY_WALLET as string 21 | ); 22 | 23 | const originalConsoleError = console.error; 24 | console.error = function () {}; 25 | let creds = await clobClient.createApiKey(); 26 | console.error = originalConsoleError; 27 | if (creds.key) { 28 | console.log('API Key created', creds); 29 | } else { 30 | creds = await clobClient.deriveApiKey(); 31 | console.log('API Key derived', creds); 32 | } 33 | 34 | clobClient = new ClobClient( 35 | host, 36 | chainId, 37 | wallet, 38 | creds, 39 | SignatureType.POLY_GNOSIS_SAFE, 40 | PROXY_WALLET as string 41 | ); 42 | console.log(clobClient); 43 | return clobClient; 44 | }; 45 | 46 | export default createClobClient; 47 | -------------------------------------------------------------------------------- /src/services/tradeMonitor.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { ENV } from '../config/env'; 3 | import { UserActivityInterface, UserPositionInterface } from '../interfaces/User'; 4 | import { getUserActivityModel, getUserPositionModel } from '../models/userHistory'; 5 | import fetchData from '../utils/fetchData'; 6 | 7 | const USER_ADDRESS = ENV.USER_ADDRESS; 8 | const TOO_OLD_TIMESTAMP = ENV.TOO_OLD_TIMESTAMP; 9 | const FETCH_INTERVAL = ENV.FETCH_INTERVAL; 10 | 11 | if (!USER_ADDRESS) { 12 | throw new Error('USER_ADDRESS is not defined'); 13 | console.log('USER_ADDRESS is not defined'); 14 | } 15 | 16 | const UserActivity = getUserActivityModel(USER_ADDRESS); 17 | const UserPosition = getUserPositionModel(USER_ADDRESS); 18 | 19 | let temp_trades: UserActivityInterface[] = []; 20 | 21 | const init = async () => { 22 | temp_trades = (await UserActivity.find().exec()).map((trade) => trade as UserActivityInterface); 23 | console.log('temp_trades', temp_trades); 24 | }; 25 | 26 | const fetchTradeData = async () => { 27 | 28 | }; 29 | 30 | const tradeMonitor = async () => { 31 | console.log('Trade Monitor is running every', FETCH_INTERVAL, 'seconds'); 32 | await init(); //Load my oders before sever downs 33 | while (true) { 34 | await fetchTradeData(); //Fetch all user activities 35 | await new Promise((resolve) => setTimeout(resolve, FETCH_INTERVAL * 1000)); //Fetch user activities every second 36 | } 37 | }; 38 | 39 | export default tradeMonitor; 40 | -------------------------------------------------------------------------------- /src/services/createClobClient.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import { ClobClient } from '@polymarket/clob-client'; 3 | import { SignatureType } from '@polymarket/order-utils'; 4 | import { ENV } from '../config/env'; 5 | 6 | const PROXY_WALLET = ENV.PROXY_WALLET; 7 | const PRIVATE_KEY = ENV.PRIVATE_KEY; 8 | const CLOB_HTTP_URL = ENV.CLOB_HTTP_URL; 9 | 10 | const createClobClient = async (): Promise => { 11 | const chainId = 137; 12 | const host = CLOB_HTTP_URL as string; 13 | const wallet = new ethers.Wallet(PRIVATE_KEY as string); 14 | let clobClient = new ClobClient( 15 | host, 16 | chainId, 17 | wallet, 18 | undefined, 19 | SignatureType.POLY_PROXY, 20 | PROXY_WALLET as string 21 | ); 22 | 23 | const originalConsoleError = console.error; 24 | console.error = function () {}; 25 | let creds = await clobClient.createApiKey(); 26 | console.error = originalConsoleError; 27 | if (creds.key) { 28 | console.log('API Key created', creds); 29 | } else { 30 | creds = await clobClient.deriveApiKey(); 31 | console.log('API Key derived', creds); 32 | } 33 | console.log('creds', creds); 34 | 35 | clobClient = new ClobClient( 36 | host, 37 | chainId, 38 | wallet, 39 | creds, 40 | SignatureType.POLY_PROXY, 41 | PROXY_WALLET as string 42 | ); 43 | console.log(clobClient); 44 | return clobClient; 45 | }; 46 | 47 | export default createClobClient; 48 | -------------------------------------------------------------------------------- /src/interfaces/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export interface UserActivityInterface { 4 | _id: mongoose.Types.ObjectId; 5 | proxyWallet: string; 6 | timestamp: number; 7 | conditionId: string; 8 | type: string; 9 | size: number; 10 | usdcSize: number; 11 | transactionHash: string; 12 | price: number; 13 | asset: string; 14 | side: string; 15 | outcomeIndex: number; 16 | title: string; 17 | slug: string; 18 | icon: string; 19 | eventSlug: string; 20 | outcome: string; 21 | name: string; 22 | pseudonym: string; 23 | bio: string; 24 | profileImage: string; 25 | profileImageOptimized: string; 26 | bot: boolean; 27 | botExcutedTime: number; 28 | } 29 | 30 | export interface UserPositionInterface { 31 | _id: mongoose.Types.ObjectId; 32 | proxyWallet: string; 33 | asset: string; 34 | conditionId: string; 35 | size: number; 36 | avgPrice: number; 37 | initialValue: number; 38 | currentValue: number; 39 | cashPnl: number; 40 | percentPnl: number; 41 | totalBought: number; 42 | realizedPnl: number; 43 | percentRealizedPnl: number; 44 | curPrice: number; 45 | redeemable: boolean; 46 | mergeable: boolean; 47 | title: string; 48 | slug: string; 49 | icon: string; 50 | eventSlug: string; 51 | outcome: string; 52 | outcomeIndex: number; 53 | oppositeOutcome: string; 54 | oppositeAsset: string; 55 | endDate: string; 56 | negativeRisk: boolean; 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymarket_copy_trading_bot", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "node dist/index.js", 8 | "dev": "ts-node src/index.ts", 9 | "lint": "eslint .", 10 | "lint:fix": "eslint --fix .", 11 | "format": "prettier --write .", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [], 15 | "author": "BlackSky-jose", 16 | "license": "ISC", 17 | "type": "commonjs", 18 | "description": "", 19 | "devDependencies": { 20 | "@eslint/js": "^9.19.0", 21 | "@types/jest": "^29.5.14", 22 | "@types/node": "^22.10.10", 23 | "@typescript-eslint/eslint-plugin": "^8.22.0", 24 | "@typescript-eslint/parser": "^8.22.0", 25 | "eslint": "^9.19.0", 26 | "eslint-config-prettier": "^10.0.1", 27 | "eslint-plugin-prettier": "^5.2.3", 28 | "globals": "^15.14.0", 29 | "jest": "^29.7.0", 30 | "prettier": "^3.4.2", 31 | "ts-jest": "^29.2.5", 32 | "ts-node": "^10.9.2", 33 | "typescript": "^5.7.3", 34 | "typescript-eslint": "^8.23.0" 35 | }, 36 | "dependencies": { 37 | "@polymarket/clob-client": "^4.14.0", 38 | "axios": "^1.7.9", 39 | "chalk": "^5.4.1", 40 | "dotenv": "^16.4.7", 41 | "ethers": "^5.7.2", 42 | "moment": "^2.30.1", 43 | "mongoose": "^8.9.5", 44 | 45 | "ora": "^8.2.0" 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | if (!process.env.USER_ADDRESS) { 5 | throw new Error('USER_ADDRESS is not defined'); 6 | } 7 | if (!process.env.PROXY_WALLET) { 8 | throw new Error('PROXY_WALLET is not defined'); 9 | } 10 | if (!process.env.PRIVATE_KEY) { 11 | throw new Error('PRIVATE_KEY is not defined'); 12 | } 13 | if (!process.env.CLOB_HTTP_URL) { 14 | throw new Error('CLOB_HTTP_URL is not defined'); 15 | } 16 | if (!process.env.CLOB_WS_URL) { 17 | throw new Error('CLOB_WS_URL is not defined'); 18 | } 19 | if (!process.env.MONGO_URI) { 20 | throw new Error('MONGO_URI is not defined'); 21 | } 22 | if (!process.env.RPC_URL) { 23 | throw new Error('RPC_URL is not defined'); 24 | } 25 | if (!process.env.USDC_CONTRACT_ADDRESS) { 26 | throw new Error('USDC_CONTRACT_ADDRESS is not defined'); 27 | } 28 | 29 | export const ENV = { 30 | USER_ADDRESS: process.env.USER_ADDRESS as string, 31 | PROXY_WALLET: process.env.PROXY_WALLET as string, 32 | PRIVATE_KEY: process.env.PRIVATE_KEY as string, 33 | CLOB_HTTP_URL: process.env.CLOB_HTTP_URL as string, 34 | CLOB_WS_URL: process.env.CLOB_WS_URL as string, 35 | FETCH_INTERVAL: parseInt(process.env.FETCH_INTERVAL || '1', 10), 36 | TOO_OLD_TIMESTAMP: parseInt(process.env.TOO_OLD_TIMESTAMP || '24', 10), 37 | RETRY_LIMIT: parseInt(process.env.RETRY_LIMIT || '3', 10), 38 | MONGO_URI: process.env.MONGO_URI as string, 39 | RPC_URL: process.env.RPC_URL as string, 40 | USDC_CONTRACT_ADDRESS: process.env.USDC_CONTRACT_ADDRESS as string, 41 | }; 42 | -------------------------------------------------------------------------------- /src/services/tradeExecutor.ts: -------------------------------------------------------------------------------- 1 | import { ClobClient } from '@polymarket/clob-client'; 2 | import { UserActivityInterface, UserPositionInterface } from '../interfaces/User'; 3 | import { ENV } from '../config/env'; 4 | import { getUserActivityModel } from '../models/userHistory'; 5 | import fetchData from '../utils/fetchData'; 6 | import spinner from '../utils/spinner'; 7 | import getMyBalance from '../utils/getMyBalance'; 8 | import postOrder from '../utils/postOrder'; 9 | 10 | const USER_ADDRESS = ENV.USER_ADDRESS; 11 | const RETRY_LIMIT = ENV.RETRY_LIMIT; 12 | const PROXY_WALLET = ENV.PROXY_WALLET; 13 | 14 | let temp_trades: UserActivityInterface[] = []; 15 | 16 | const UserActivity = getUserActivityModel(USER_ADDRESS); 17 | 18 | const readTempTrade = async () => { 19 | temp_trades = ( 20 | await UserActivity.find({ 21 | $and: [{ type: 'TRADE' }, { bot: false }, { botExcutedTime: { $lt: RETRY_LIMIT } }], 22 | }).exec() 23 | ).map((trade) => trade as UserActivityInterface); 24 | }; 25 | 26 | const doTrading = async (clobClient: ClobClient) => { 27 | for (const trade of temp_trades) { 28 | console.log('Trade to copy:', trade); 29 | // const market = await clobClient.getMarket(trade.conditionId); 30 | const my_positions: UserPositionInterface[] = await fetchData( 31 | `https://data-api.polymarket.com/positions?user=${PROXY_WALLET}` 32 | ); 33 | const user_positions: UserPositionInterface[] = await fetchData( 34 | `https://data-api.polymarket.com/positions?user=${USER_ADDRESS}` 35 | ); 36 | const my_position = my_positions.find( 37 | (position: UserPositionInterface) => position.conditionId === trade.conditionId 38 | ); 39 | const user_position = user_positions.find( 40 | (position: UserPositionInterface) => position.conditionId === trade.conditionId 41 | ); 42 | const my_balance = await getMyBalance(PROXY_WALLET); 43 | const user_balance = await getMyBalance(USER_ADDRESS); 44 | console.log('My current balance:', my_balance); 45 | console.log('User current balance:', user_balance); 46 | } 47 | }; 48 | 49 | const tradeExcutor = async (clobClient: ClobClient) => { 50 | console.log(`Executing Copy Trading`); 51 | 52 | while (true) { 53 | await readTempTrade(); 54 | if (temp_trades.length > 0) { 55 | console.log('💥 New transactions found 💥'); 56 | spinner.stop(); 57 | await doTrading(clobClient); 58 | } else { 59 | spinner.start('Waiting for new transactions'); 60 | } 61 | } 62 | }; 63 | 64 | export default tradeExcutor; 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polymarket Copy Trading Bot 2 | 3 | ## Introduction 4 | This project is a Polymarket Copy Trading Bot that allows users to automatically copy trades from a selected trader on Polymarket. 5 | 6 | ## Features 7 | - **Automated Trading**: Automatically copy trades from a selected trader. 8 | - **Real-time Monitoring**: Continuously monitor the selected trader's activity. 9 | - **Customizable Settings**: Configure trading parameters and risk management. 10 | 11 | ## Installation 12 | 1. Install latest version of Node.js and npm 13 | 2. Navigate to the project directory: 14 | ```bash 15 | cd polymarket_copy_trading_bot 16 | ``` 17 | 3. Create `.env` file: 18 | ```bash 19 | touch .env 20 | ``` 21 | 4. Configure env variables: 22 | ```typescript 23 | USER_ADDRESS = 'Selected account wallet address to copy' 24 | 25 | PROXY_WALLET = 'Your Polymarket account address' 26 | PRIVATE_KEY = 'My wallet private key' 27 | 28 | CLOB_HTTP_URL = 'https://clob.polymarket.com/' 29 | CLOB_WS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws' 30 | 31 | FETCH_INTERVAL = 1 // default is 1 second 32 | TOO_OLD_TIMESTAMP = 1 // default is 1 hour 33 | RETRY_LIMIT = 3 // default is 3 times 34 | 35 | MONGO_URI = 'mongodb+srv://polymarket_copytrading_bot:V5ufvi9ra1dsOA9M@cluster0.j1flc.mongodb.net/polymarket_copytrading' 36 | 37 | RPC_URL = 'https://polygon-mainnet.infura.io/v3/90ee27dc8b934739ba9a55a075229744' 38 | 39 | USDC_CONTRACT_ADDRESS = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' 40 | ``` 41 | 3. Install the required dependencies: 42 | ```bash 43 | npm install 44 | ``` 45 | 5. Build the project: 46 | ```bash 47 | npm run build 48 | ``` 49 | 6. Run BOT: 50 | ```bash 51 | npm run start 52 | ``` 53 | 7. ⚠ Choose reasonable location for the bot(Many users faced this problem, read this carefully before setting up the bot): 54 | 55 | For users facing IP address-related access issues with Polymarket due to geographic restrictions, I recommend using [tradingvps.io](https://app.tradingvps.io/link.php?id=11) with the Germany location. This VPS service offers ultra-low latency and is physically close to Polymarket’s servers, ensuring faster response times and a smoother trading experience. It is specifically optimized for traders and easy to set up, making it an excellent choice for both beginners and experienced users looking to avoid IP-based blocks. 56 | 57 | ## Contributing 58 | Contributions are welcome! Please open an issue or submit a pull request. And if you are interested in this project, please consider giving it a star✨. 59 | 60 | ## Contact 61 | For updated version or any questions, please contact me at [Telegram](https://t.me/BlackSky_jose). 62 | -------------------------------------------------------------------------------- /src/models/userHistory.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | 3 | const positionSchema = new Schema({ 4 | _id: { 5 | type: Schema.Types.ObjectId, 6 | required: true, 7 | auto: true, 8 | }, 9 | proxyWallet: { type: String, required: false }, 10 | asset: { type: String, required: false }, 11 | conditionId: { type: String, required: false }, 12 | size: { type: Number, required: false }, 13 | avgPrice: { type: Number, required: false }, 14 | initialValue: { type: Number, required: false }, 15 | currentValue: { type: Number, required: false }, 16 | cashPnl: { type: Number, required: false }, 17 | percentPnl: { type: Number, required: false }, 18 | totalBought: { type: Number, required: false }, 19 | realizedPnl: { type: Number, required: false }, 20 | percentRealizedPnl: { type: Number, required: false }, 21 | curPrice: { type: Number, required: false }, 22 | redeemable: { type: Boolean, required: false }, 23 | mergeable: { type: Boolean, required: false }, 24 | title: { type: String, required: false }, 25 | slug: { type: String, required: false }, 26 | icon: { type: String, required: false }, 27 | eventSlug: { type: String, required: false }, 28 | outcome: { type: String, required: false }, 29 | outcomeIndex: { type: Number, required: false }, 30 | oppositeOutcome: { type: String, required: false }, 31 | oppositeAsset: { type: String, required: false }, 32 | endDate: { type: String, required: false }, 33 | negativeRisk: { type: Boolean, required: false }, 34 | }); 35 | 36 | const activitySchema = new Schema({ 37 | _id: { 38 | type: Schema.Types.ObjectId, 39 | required: true, 40 | auto: true, 41 | }, 42 | proxyWallet: { type: String, required: false }, 43 | timestamp: { type: Number, required: false }, 44 | conditionId: { type: String, required: false }, 45 | type: { type: String, required: false }, 46 | size: { type: Number, required: false }, 47 | usdcSize: { type: Number, required: false }, 48 | transactionHash: { type: String, required: false }, 49 | price: { type: Number, required: false }, 50 | asset: { type: String, required: false }, 51 | side: { type: String, required: false }, 52 | outcomeIndex: { type: Number, required: false }, 53 | title: { type: String, required: false }, 54 | slug: { type: String, required: false }, 55 | icon: { type: String, required: false }, 56 | eventSlug: { type: String, required: false }, 57 | outcome: { type: String, required: false }, 58 | name: { type: String, required: false }, 59 | pseudonym: { type: String, required: false }, 60 | bio: { type: String, required: false }, 61 | profileImage: { type: String, required: false }, 62 | profileImageOptimized: { type: String, required: false }, 63 | bot: { type: Boolean, required: false }, 64 | 65 | botExcutedTime: { type: Number, required: false }, 66 | 67 | }); 68 | 69 | const getUserPositionModel = (walletAddress: string) => { 70 | const collectionName = `user_positions_${walletAddress}`; 71 | return mongoose.model(collectionName, positionSchema, collectionName); 72 | }; 73 | 74 | const getUserActivityModel = (walletAddress: string) => { 75 | const collectionName = `user_activities_${walletAddress}`; 76 | return mongoose.model(collectionName, activitySchema, collectionName); 77 | }; 78 | 79 | export { getUserPositionModel, getUserActivityModel }; 80 | -------------------------------------------------------------------------------- /src/utils/postOrder.ts: -------------------------------------------------------------------------------- 1 | import { ClobClient, OrderType, Side } from '@polymarket/clob-client'; 2 | import { UserActivityInterface, UserPositionInterface } from '../interfaces/User'; 3 | import { getUserActivityModel } from '../models/userHistory'; 4 | import { ENV } from '../config/env'; 5 | 6 | const RETRY_LIMIT = ENV.RETRY_LIMIT; 7 | const USER_ADDRESS = ENV.USER_ADDRESS; 8 | const UserActivity = getUserActivityModel(USER_ADDRESS); 9 | 10 | const postOrder = async ( 11 | clobClient: ClobClient, 12 | condition: string, 13 | my_position: UserPositionInterface | undefined, 14 | user_position: UserPositionInterface | undefined, 15 | trade: UserActivityInterface, 16 | my_balance: number, 17 | user_balance: number 18 | ) => { 19 | //Merge strategy 20 | if (condition === 'merge') { 21 | console.log('Merging Strategy...'); 22 | if (!my_position) { 23 | console.log('my_position is undefined'); 24 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 25 | return; 26 | } 27 | let remaining = my_position.size; 28 | let retry = 0; 29 | while (remaining > 0 && retry < RETRY_LIMIT) { 30 | const orderBook = await clobClient.getOrderBook(trade.asset); 31 | if (!orderBook.bids || orderBook.bids.length === 0) { 32 | console.log('No bids found'); 33 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 34 | break; 35 | } 36 | 37 | const maxPriceBid = orderBook.bids.reduce((max, bid) => { 38 | return parseFloat(bid.price) > parseFloat(max.price) ? bid : max; 39 | }, orderBook.bids[0]); 40 | 41 | console.log('Max price bid:', maxPriceBid); 42 | let order_arges; 43 | if (remaining <= parseFloat(maxPriceBid.size)) { 44 | order_arges = { 45 | side: Side.SELL, 46 | tokenID: my_position.asset, 47 | amount: remaining, 48 | price: parseFloat(maxPriceBid.price), 49 | }; 50 | } else { 51 | order_arges = { 52 | side: Side.SELL, 53 | tokenID: my_position.asset, 54 | amount: parseFloat(maxPriceBid.size), 55 | price: parseFloat(maxPriceBid.price), 56 | }; 57 | } 58 | console.log('Order args:', order_arges); 59 | const signedOrder = await clobClient.createMarketOrder(order_arges); 60 | const resp = await clobClient.postOrder(signedOrder, OrderType.FOK); 61 | if (resp.success === true) { 62 | retry = 0; 63 | console.log('Successfully posted order:', resp); 64 | remaining -= order_arges.amount; 65 | } else { 66 | retry += 1; 67 | console.log('Error posting order: retrying...', resp); 68 | } 69 | } 70 | if (retry >= RETRY_LIMIT) { 71 | await UserActivity.updateOne({ _id: trade._id }, { bot: true, botExcutedTime: retry }); 72 | } else { 73 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 74 | } 75 | } else if (condition === 'buy') { //Buy strategy 76 | console.log('Buy Strategy...'); 77 | const ratio = my_balance / (user_balance + trade.usdcSize); 78 | console.log('ratio', ratio); 79 | let remaining = trade.usdcSize * ratio; 80 | let retry = 0; 81 | while (remaining > 0 && retry < RETRY_LIMIT) { 82 | const orderBook = await clobClient.getOrderBook(trade.asset); 83 | if (!orderBook.asks || orderBook.asks.length === 0) { 84 | console.log('No asks found'); 85 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 86 | break; 87 | } 88 | 89 | const minPriceAsk = orderBook.asks.reduce((min, ask) => { 90 | return parseFloat(ask.price) < parseFloat(min.price) ? ask : min; 91 | }, orderBook.asks[0]); 92 | 93 | console.log('Min price ask:', minPriceAsk); 94 | if (parseFloat(minPriceAsk.price) - 0.05 > trade.price) { 95 | console.log('Too big different price - do not copy'); 96 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 97 | break; 98 | } 99 | let order_arges; 100 | if (remaining <= parseFloat(minPriceAsk.size) * parseFloat(minPriceAsk.price)) { 101 | order_arges = { 102 | side: Side.BUY, 103 | tokenID: trade.asset, 104 | amount: remaining, 105 | price: parseFloat(minPriceAsk.price), 106 | }; 107 | } else { 108 | order_arges = { 109 | side: Side.BUY, 110 | tokenID: trade.asset, 111 | amount: parseFloat(minPriceAsk.size) * parseFloat(minPriceAsk.price), 112 | price: parseFloat(minPriceAsk.price), 113 | }; 114 | } 115 | console.log('Order args:', order_arges); 116 | const signedOrder = await clobClient.createMarketOrder(order_arges); 117 | const resp = await clobClient.postOrder(signedOrder, OrderType.FOK); 118 | if (resp.success === true) { 119 | retry = 0; 120 | console.log('Successfully posted order:', resp); 121 | remaining -= order_arges.amount; 122 | } else { 123 | retry += 1; 124 | console.log('Error posting order: retrying...', resp); 125 | } 126 | } 127 | if (retry >= RETRY_LIMIT) { 128 | await UserActivity.updateOne({ _id: trade._id }, { bot: true, botExcutedTime: retry }); 129 | } else { 130 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 131 | } 132 | } else if (condition === 'sell') { //Sell strategy 133 | console.log('Sell Strategy...'); 134 | let remaining = 0; 135 | if (!my_position) { 136 | console.log('No position to sell'); 137 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 138 | } else if (!user_position) { 139 | remaining = my_position.size; 140 | } else { 141 | const ratio = trade.size / (user_position.size + trade.size); 142 | console.log('ratio', ratio); 143 | remaining = my_position.size * ratio; 144 | } 145 | let retry = 0; 146 | while (remaining > 0 && retry < RETRY_LIMIT) { 147 | const orderBook = await clobClient.getOrderBook(trade.asset); 148 | if (!orderBook.bids || orderBook.bids.length === 0) { 149 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 150 | console.log('No bids found'); 151 | break; 152 | } 153 | 154 | const maxPriceBid = orderBook.bids.reduce((max, bid) => { 155 | return parseFloat(bid.price) > parseFloat(max.price) ? bid : max; 156 | }, orderBook.bids[0]); 157 | 158 | console.log('Max price bid:', maxPriceBid); 159 | let order_arges; 160 | if (remaining <= parseFloat(maxPriceBid.size)) { 161 | order_arges = { 162 | side: Side.SELL, 163 | tokenID: trade.asset, 164 | amount: remaining, 165 | price: parseFloat(maxPriceBid.price), 166 | }; 167 | } else { 168 | order_arges = { 169 | side: Side.SELL, 170 | tokenID: trade.asset, 171 | amount: parseFloat(maxPriceBid.size), 172 | price: parseFloat(maxPriceBid.price), 173 | }; 174 | } 175 | console.log('Order args:', order_arges); 176 | const signedOrder = await clobClient.createMarketOrder(order_arges); 177 | const resp = await clobClient.postOrder(signedOrder, OrderType.FOK); 178 | if (resp.success === true) { 179 | retry = 0; 180 | console.log('Successfully posted order:', resp); 181 | remaining -= order_arges.amount; 182 | } else { 183 | retry += 1; 184 | console.log('Error posting order: retrying...', resp); 185 | } 186 | } 187 | if (retry >= RETRY_LIMIT) { 188 | await UserActivity.updateOne({ _id: trade._id }, { bot: true, botExcutedTime: retry }); 189 | } else { 190 | await UserActivity.updateOne({ _id: trade._id }, { bot: true }); 191 | } 192 | } else { 193 | console.log('condition not supported'); 194 | } 195 | }; 196 | 197 | export default postOrder; 198 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 40 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 41 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 42 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 43 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "noEmit": true, /* Disable emitting files from a compilation. */ 60 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 61 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 62 | 63 | /* Interop Constraints */ 64 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 65 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 66 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 68 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 70 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 71 | 72 | /* Type Checking */ 73 | "strict": true /* Enable all strict type-checking options. */, 74 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 75 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 76 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 77 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 78 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 79 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 80 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 81 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 82 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 83 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 84 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 85 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 86 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 87 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 88 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 89 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 90 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 91 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 92 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 93 | 94 | /* Completeness */ 95 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 96 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 97 | }, 98 | "include": ["src"], 99 | 100 | "exclude": ["node_modules"] 101 | } 102 | --------------------------------------------------------------------------------