JSON.stringify(err.response.config) in clob-client's errorHandling
includes the httpsAgent (HttpsProxyAgent) object which has circular/deep
refs, causing "Maximum call stack size exceeded" on every API error.
- Fixed live node_modules file to log only status + data (no config)
- Updated patch script to apply this fix idempotently after npm install,
independent of proxy patch (two separate checks/markers)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When ghost fill recovery decides to hold the expensive side (not sell),
set pos.holdingSide so the existing waitAndRedeem flow picks it up after
executeMakerRebateStrategy — same path as CANCEL_CHEAP_ON_EXP_FILL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When expensive side (e.g. YES at 83c) is filled but cheap side (NO at 15c)
is ghost, do NOT market-sell the YES tokens — the cost basis is too high
and a market dump guarantees a loss. Only CL (market-sell) the cheap side
remainder, where the cost basis is low enough to absorb the slippage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the CLOB client returns a failure response that contains a circular
reference (e.g. axios/fetch internals), JSON.stringify throws
"Maximum call stack size exceeded" which is caught and swallows the real
error. Log only safe primitive fields (errorMsg, error, status) instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MAKER_MM_CANCEL_CHEAP_ON_EXP_FILL=true: when the expensive side fills
first, immediately cancel the cheap side order and hold the token.
After market ends, polls on-chain payoutDenominator until resolved,
then calls redeemPositions to recover USDC.
- Default false — existing symmetric maker behavior unchanged.
- Redeem polls every 15s for up to 10 minutes after market close.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Between fetching the best ask and placing the GTC order, the ask can
move down 1 tick — turning our bid into a marketable (taker) order,
which hits Polymarket's $1 minimum notional error. Cap bid to
ask - 2*tickSize instead of ask - 1*tickSize to absorb the timing race.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When cheap side price is very low (e.g. NO at 5c), MAKER_MM_TRADE_SIZE=5
produces $0.25 notional which fails Polymarket's $1 minimum for marketable
BUY orders. Auto-scale both sides to ceil($1/price) to meet the minimum,
keeping YES+NO pair counts equal for clean merge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MIN/MAX_PRICE was always checked against YES bid, blocking entry when
NO is the cheap side (e.g. YES=96c, NO=4c with range 0.03-0.05).
Now the range filter applies to whichever side has the lower bestBid.
The expensive side is derived from maxCombined - cheapBid as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MIN/MAX_PRICE was filtering both YES and NO bids, blocking valid
skewed-market entries like YES=11c + NO=87c (NO fails MAX_PRICE=0.30).
Now only YES is range-checked. NO only needs basic sanity bounds (0 < noBid < 1).
This enables targeting cheap YES markets while NO bid can be any value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MAKER_MM_REENTRY_ENABLED=false disables re-entry (one cycle per market)
- Fix floating point bug: 0.1+0.5=0.6000000000000001 caused "combined > max"
loop when MAKER_MM_MAX_COMBINED=0.60 — now rounds combined to 4dp before compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Repricing was the root cause of double orders and ghost fills:
- cancel/replace race caused duplicate active orders in CLOB
- cancel API returning OK did not guarantee order was actually gone
- ghost fills (CLOB matched, invalid txhash) went undetected
Changes:
- Remove checkAndReprice entirely — orders placed once and held
- Detect ghost fills via getOpenOrders (reliable) instead of checkOrderStatus
- recoverFromGhostFill: merge paired shares + market-sell remainder
- marketSellToken: verify onchain balance after each attempt, retry up to 3x
(sells can also be ghost-filled — shares still in wallet despite CLOB saying done)
- Recovery triggers at 30s after detection, not at cut-loss time (prices still fair)
- Update README to reflect no-reprice design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WS handler was marking a side as 'filled' for any fill event regardless of
share size. A 2-share partial fill (target: 5) would set pos.no.filled=true,
causing the monitoring loop to enter limbo: both sides 'filled' but onchain
balance below the 0.5x merge threshold, so no merge ever triggered.
WS events now only serve as a wake-up signal for waitForFillOrTimeout.
Onchain balance (getTokenBalance) is the sole source of truth for filled flags.
- Place simultaneous maker limit BUY on YES+NO sides (combined ≈ $0.98)
- Merge filled pairs via CTF contract → capture spread as profit
- WebSocket RTDS real-time fill detection + onchain balance as source of truth
- No aggressive repricing when one side is filled (prevents double exposure)
- Stop re-entry after one-sided stuck cycle (prevents directional accumulation)
- Combined cap always enforced — profitable merge guaranteed
- Auto-queue next market before current closes (zero idle time)
- Remove loss-compensating martingale logic from reprice flow
- Add .env.example with full documentation for all strategies
- Update README with grant-ready project description
Replace 10s polling loop with RTDS WebSocket event-driven fill detection
(~100ms latency). Parallelize YES/NO side checks and cancel operations.
Polling kept as 30s safety net fallback.
- Add mmWsFillWatcher.js: subscribes to RTDS activity feed, filters by
own proxy wallet, emits 'fill' events for watched tokens
- Refactor monitorAndManage(): event-driven loop with parallel checks
- Extract checkSideFill() for parallel-safe per-side fill detection
- Wire up WS watcher lifecycle in mm.js and mm-bot.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bug 1 (partial fill stuck): When an order is partially filled (e.g. 4/20
shares), the bot would get stuck — isOrderFilled returns false, but the
order is dead on the book. Now detect partial fills via getPartialFillInfo,
cancel the old order, and either re-place a limit for the remaining shares
or market-sell if below CLOB minimum.
Bug 2 (defensive overrides fill): When defensive timeout fires, it cancels
both orders — but one side may have actually filled between the last poll
and the cancel. Now re-check on-chain balances after cancellation. If one
side is filled, route to adaptive CL instead of defensive pivot.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CLOB client errorHandling function does JSON.stringify on
err.response.config which includes httpsAgent (HttpsProxyAgent with
circular references). Inject a safe serializer that strips agent
properties before serializing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The IIFE approach that injected httpsAgent into the request config
caused "Converting circular structure to JSON" when the CLOB client
serialized the config object (HttpsProxyAgent has circular refs).
New approach: register an axios request interceptor after the axios
import. This is simpler (no need to find request config patterns),
works with any code format, and avoids serialization issues since
axios handles httpsAgent internally after interceptors run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace undefined `breakevenFloor` variable with `minAdaptivePrice`
which is the actual breakeven floor calculated from
mmAdaptiveMinCombined - filledLegPrice.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The VPS fresh install uses `(0, axios_1.default)({ method, url: endpoint, ... })`
instead of a separate `config` variable. Add Format B regex to match this
inline pattern and wrap it in an IIFE that injects the proxy agent.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite patch-clob-client.cjs to use regex instead of exact string
matching, so it works regardless of code formatting differences
between npm versions (fixes "Could not find request config line" on
fresh VPS install)
- Add multiple fallback patterns (config regex → fallback regex →
axios call injection) with debug output if all fail
- Add Polymarket geoblock API check (polymarket.com/api/geoblock)
for both VPS direct IP and proxy IP at startup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The proxy patch in node_modules is lost on every fresh npm install.
This postinstall script automatically patches @polymarket/clob-client
http-helpers to inject HttpsProxyAgent for all polymarket.com requests.
Runs automatically after npm install — no manual patching needed on VPS deploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The CLOB client uses axios internally. Previous setup only set
axios.defaults which doesn't work if the CLOB client or axios
creates its own instance.
Fix: add axios request interceptor that forces proxy agent on every
request to polymarket.com domains.
Also add separate axios-based proxy test alongside fetch test to catch
cases where fetch works but axios doesn't route through proxy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows both VPS direct IP and proxy IP so user can verify which IP
Polymarket sees. If CLOB returns 403 geoblock, shows clear error
with link to allowed regions instead of proceeding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Was using breakevenFloor (1.00 - filledPrice) which ignored the user's
MM_ADAPTIVE_MIN_COMBINED setting. This caused sells below the configured
minimum (e.g. selling at 28c when min combined was 1.01).
Now: filled @ 70c, MM_ADAPTIVE_MIN_COMBINED=1.01 → floor starts at 31c
Phase 1 (>180s): 31c
Phase 2 (90-180s): 21c
Phase 3 (30-90s): 11c
Phase 4 (<30s): market sell
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both sniper and dumper had proxy-patch.cjs imported but market maker
entry points (mm-bot.js, mm.js) were missing it, causing direct
connections without proxy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLOB API (client.getOrder) can be stale — order filled on-chain but API
still shows unfilled. This caused the bot to miss fills and incorrectly
enter defensive mode or keep monitoring dead orders.
Now isOrderFilled does:
1. Check CLOB API (existing) with retry
2. Fallback: check on-chain token balance via CTF contract
If balance < 5% of original shares → treat as filled
Fixes issue where Polymarket UI shows filled but bot doesn't detect it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Orders must be cancelled BEFORE the monitoring loop can detect fills,
otherwise the limit sell at 70c can still get filled at minute 3
and trigger adaptive CL instead of defensive pivot.
Now: timeout reached → cancel both orders → enter defensivePivot
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Check mid price first, place limit at market price (capped at mmSellPrice).
Only falls back to breakeven floor if mid price is below it.
Example: YES filled @ 70c, NO mid = 38c → sell @ 38c (not 30c breakeven)
This gives profit potential instead of always targeting breakeven.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adaptive CL now uses progressive floor lowering for 5-minute markets:
Phase 1 (>180s left): breakeven floor (e.g. 40c for 60c fill)
Phase 2 (90-180s): floor - 10c (accept small loss to escape)
Phase 3 (30-90s): floor - 20c + emergency cut if price < 10c
Phase 4 (<30s): force market sell
Key improvements:
- Standing order: immediately place limit at breakeven floor so brief
bounces get caught (don't wait for polling to detect price >= floor)
- Emergency cut: market sell immediately if price < 10c in phase 3
(market is decisive, bounce unlikely)
- Non-5m markets still use the existing fixed floor (mmAdaptiveMinCombined)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Market start = endTime - 5 minutes, so defensive triggers at exactly
120s after the market opens regardless of when the bot entered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When neither limit sell fills within MM_DEFENSIVE_TIMEOUT (default 120s),
the bot enters defensive mode:
- Cancel both limit sells
- Wait until 30s before market close
- If worst side price < MM_DEFENSIVE_WORST_THRESHOLD (default 10c):
market sell worst side, hold best side for resolution (best ≈ 90c+)
- If worst side ≥ threshold: merge back to USDC (safe $0 P&L)
Only active for 5-minute markets (MM_DURATION=5m).
New config:
MM_DEFENSIVE_ENABLED (default true)
MM_DEFENSIVE_TIMEOUT (default 120s)
MM_DEFENSIVE_WORST_THRESHOLD (default 0.10)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- isOrderFilled: retry 2x with logging instead of silent error swallow
- Place YES/NO limit sells in parallel via Promise.all
- Market sell immediately when remaining shares < CLOB minimum (5 shares)
- Re-check on-chain balance before every limit/market sell in adaptive CL
- Separate redeemer Safe queue priority from strategy (split/merge) to reduce cross-market delays
- Cache USDC and exchange approvals in-memory to skip redundant on-chain reads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Problem: redeemer used array index to match balances to payoutFractions,
but Data API token order ≠ on-chain outcome index. This caused false wins
(shares on losing side counted as winning).
Fix:
- Store yesTokenId/noTokenId per conditionId in sniperExecutor
- Match tokens by ID (yes=outcome0, no=outcome1) instead of array position
- Only process positions tracked by sniper (skip MM/other positions)
- Inject token lookup via setSniperConditionLookup to avoid circular imports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Time-based bet sizing multiplier (SNIPER_MULTIPLIERS, UTC+8 windows)
- Pause N rounds per asset after win (SNIPER_PAUSE_ROUNDS_AFTER_WIN)
- Win detection via payoutNumerators outcome check instead of redeem value threshold
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix sniperDetector.js to use sniperTierPrices instead of old config
- Fix sniper-tui.js display for 3-tier pricing
- Remove references to deleted sniperPrice/sniperShares vars
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace single price/shares with 3-tier system (3c/2c/1c)
- Tier allocation: 20%/30%/50% (high→low price)
- Min 5 shares per tier enforced
- Update .env.example with new config vars
- Update README.md with strategy documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add proxy support directly to clob-client http-helpers:
- Import https-proxy-agent
- Add getProxyAgent() helper
- Inject agent into axios config for polymarket.com endpoints
This ensures all CLOB API calls route through the proxy.
Full HTTPS CONNECT tunnel implementation that:
- Routes only Polymarket domains through proxy
- Uses HTTP CONNECT method for HTTPS tunneling
- Leaves all other traffic (RPC, etc.) untouched
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set https.globalAgent and override https.request to use proxy
for all Polymarket domains. Ensures @polymarket/clob-client
axios calls route through proxy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add proxy-patch.cjs that patches https.request BEFORE axios is loaded.
This forces all Polymarket API calls through the proxy, including
internal axios instances used by @polymarket/clob-client.
Import patch as very first thing in sniper.js and sniper-tui.js.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add transaction hash to redeem success log
- Continue to next position when one fails (don't get stuck)
- Add clearer logging for retry behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>