Operations · 8 min read

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 &

wait

Run 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>&1

Summary

  • Use millisecond timestamps as nonces -- separate processes will not collide.
  • Give each bot a hard maxExposureUsd cap enforced locally before every order.
  • Use the run-all.sh auto-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