Running multiple trading bots on one Polymarket wallet
Nonce management, balance sharing, and per-bot caps for running poly5m-v4, poly15m-v6, and polycopy concurrently on a single wallet.
Why it is tricky
Each bot uses the same private key to sign CLOB orders. The CLOB API enforces nonce uniqueness per signer -- two orders signed with the same nonce will cause one to be rejected. When two bots fire simultaneously, you get a nonce collision unless you handle it explicitly.
There is also a shared wallet balance. If three bots each think they can bet $20 out of $40 available, you end up with failed orders because the balance was already spent.
Nonce management
The simplest approach is to use timestamps in milliseconds as nonces. This works as long as no two orders fire within the same millisecond, which is effectively guaranteed for independent bot processes.
function freshNonce() {
return Date.now() // millisecond timestamp, globally unique enough
}The @polymarket/clob-client uses this pattern internally. If you are calling createOrder directly, pass nonce: freshNonce() each time.
For more robustness, add a process-level counter on top of the timestamp:
let _counter = 0
function freshNonce() {
return Date.now() * 1000 + (_counter++ % 1000)
}Per-bot balance caps
Give each bot a hard ceiling on how much of the wallet it can hold open as positions. Store this in the bot's config and enforce it before placing an order.
const BOT_CONFIG = {
'poly5m-v4': { maxExposureUsd: 20 },
'poly15m-v6': { maxExposureUsd: 30 },
'polycopy': { maxExposureUsd: 50 },
}
async function canPlaceBet(botName, proposedUsd, currentExposureUsd) {
const cap = BOT_CONFIG[botName]?.maxExposureUsd ?? 10
return currentExposureUsd + proposedUsd <= cap
}Each bot tracks its own currentExposureUsd locally -- no shared state needed. When a position resolves, it decrements the counter.
The run-all.sh pattern
Our bots use a shell script to launch all strategies as separate Node.js processes, each with its own auto-restart loop:
#!/bin/bash
# scripts/run-all.sh
BOT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
restart_bot() {
local name=$1
local script=$2
shift 2
while true; do
echo "[$(date)] Starting $name"
node "$BOT_ROOT/$script" "$@"
echo "[$(date)] $name exited with code $?, restarting in 5s"
sleep 5
done
}
restart_bot "poly5m-v4" "poly5m/poly5m-v4.js" --asset BTC --live &
restart_bot "poly15m-v6" "poly15m/poly15m-v6.js" --asset BTC --live &
restart_bot "polycopy" "polycopy/polycopy.js" --live &
waitRun it with nohup bash scripts/run-all.sh > /tmp/bots.log 2>&1 &. The wait at the end means the parent script keeps running until all child loops exit.
Monitoring shared wallet balance
Each bot independently calls client.getBalance() or the REST endpoint before sizing. No shared memory or IPC is needed because the source of truth is the on-chain USDC balance.
async function getAvailableBalance(client) {
const balances = await client.getBalanceAllowance({ asset_type: 'USDC' })
return parseFloat(balances.balance)
}Poll this before each trade, not on a timer. The balance read costs one HTTP request but prevents a lot of failed orders.
Avoiding duplicate markets
If two bots can trade the same market (e.g. both a 5-minute and 15-minute BTC binary that are open simultaneously), they should not hold the same YES token on behalf of the same wallet. This doubles your exposure unintentionally.
One approach: each bot class targets non-overlapping timeframes. Poly5m targets 5-minute expiries, poly15m targets 15-minute expiries. The market IDs never overlap.
Another approach: a shared state file that each bot reads and writes atomically:
const STATE_FILE = '/tmp/active-markets.json'
function isMarketTaken(conditionId) {
try {
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
return state.active.includes(conditionId)
} catch { return false }
}
function claimMarket(conditionId) {
const state = safeReadState()
state.active.push(conditionId)
fs.writeFileSync(STATE_FILE, JSON.stringify(state), 'utf8')
}Log separation
Give each bot its own log file. All our bots log to data/<botname>-<asset>.log or data/<botname>-<asset>-simu.log. Mixing logs from multiple bots into one file makes debugging extremely hard.
node poly5m/poly5m-v4.js --live >> data/btc-poly5m-v4.log 2>&1Summary
- Use millisecond timestamps as nonces -- separate processes will not collide.
- Give each bot a hard
maxExposureUsdcap enforced locally before every order. - Use the
run-all.shauto-restart pattern: one loop per bot, all backgrounded. - Read wallet balance from the API before each trade, not from an in-memory counter.
- Separate log files per bot.
Related bots
poly5m-v4 — Split-window momentum BTC scalper
Buy-both-sides Gabagool on 5-min BTC. Picks up cheap YES + NO at $0.49 or less, locks profit when pair stays below $0.98. CLOB v2 / pUSD, polls books every 2s from 10s to 260s.
poly15m-v6 — Enhanced orderbook signal BTC scalper
Polymarket book + Chainlink RTDS + CVD + OFI + chop filter + trained meta-model. Needs 3 of 8 confirmation layers before FOK taker at $0.52. Quarter-Kelly with drawdown scaling, holds to resolution.
polycopy — Whale copy-trading bot
Mirrors trades from profitable Polymarket wallets within seconds. Per-whale WR gate ≥55% after 10 trades, 24h stale exit, $0.10 best-bid collapse exit. Adaptive 3s polling after whale activity.