A Q-Score liquidity-reward farming bot for Polymarket
Persistent, two-sided quoting that earns Polymarket's daily liquidity rewards.
⚠️ DISCLAIMERYieldPilot trades real money on a live prediction market with your private key. It is provided as-is, with no warranty, for research and educational purposes only. Nothing in this repository is financial advice. You are solely responsible for any losses, missed payouts, smart-contract risk, regulatory exposure, or compliance obligations in your jurisdiction. Do not run this bot with capital you cannot afford to lose. Read the code before you run it.
- What It Does
- How Q-Score Rewards Work
- Features
- Architecture
- Quick Start
- Configuration
- Running the Bot
- How a Session Looks
- Project Status
- Known Limitations
- Contributing
- License
YieldPilot earns Polymarket's daily Q-Score liquidity rewards by maintaining persistent, two-sided quotes around the mid-price on selected binary markets. The bot continuously sizes orders to meet rewardsMinSize on both BUY and SELL, places them at a configurable distance from mid, and refreshes them only when necessary — preserving the time-on-book that the Q-Score formula values.
The revenue model is straightforward: daily reward payout from Polymarket > occasional adverse fills. The bot is engineered for that bottom line.
Polymarket's liquidity reward formula values three things:
- Two-sided presence — both BUY and SELL ≥
rewardsMinSizeshares simultaneously. - Proximity to mid —
Q = ((maxSpread − distance) / maxSpread)². - Time-on-book — Q-score is integrated over time; cancels reset the clock.
YieldPilot quotes at maxSpread × 40% from mid by default. Tighter quoting scores higher per sample but fills more often (each fill is a small adverse-selection cost and resets time-on-book). Wider quoting reduces fills but lowers per-sample Q. The default sits in the middle and is fully configurable via FARM_SPREAD_FRACTION.
The optimization target is simply: daily reward income > daily adverse-fill losses.
- Reward-first quoting — wide passive two-sided quotes sized to exact
rewardsMinSize, no inventory skew (stable inventory keeps both sides eligible). - Per-token state machine —
BUILD(acquire inventory via FOK taker on tight books, or passive GTC on wide books) →FARM(passive reward quoting). Automatic transition. - Reward-aware market scanner — filters out markets that don't pay rewards or aren't affordable; scores by net daily reward value (estimated reward income minus volatility-adjusted loss expectation).
- Live Q-Score reward estimator — samples both YES and NO order books, computes
Q_minper Polymarket's formula, predicts your daily payout share before committing capital. - Cross-book safety — multi-step price clamping prevents post-adjustment quotes from crossing the book or sitting unfillably behind the queue.
- On-chain truth — every SELL is clamped to
ctf.balanceOf(); data-api responses are treated as hints, not authoritative state. - WebSocket order book with REST fallback, stale-data watchdog, and exponential-backoff reconnect.
- Risk controls — daily loss budget, volatility pause threshold, fill-rate auto-widen, kill switch, low-balance pair-merge recovery.
- Reward income tracking — polls Polymarket's
/rewards/userendpoint and surfaces daily payouts in the dashboard. - Telegram notifications (optional) — startup/shutdown summaries, hourly PnL reports, reward income.
- Dry-run simulator for strategy iteration without live capital.
main.py (MarketMaker main loop)
├── market_scanner.py — Gamma API scan, reward-first scoring, Q-estimator
├── orderbook.py — WS real-time book + REST fallback + volatility calc
├── quote_engine.py — Wide passive two-sided quoting for Q-score
├── order_manager.py — Order lifecycle, fill detection, liquidation
├── risk_manager.py — Position state, PnL bucketing, kill switch
├── merger.py — On-chain YES+NO pair merging (recovery only)
├── dashboard.py — Terminal status panel
├── simulator.py — DRY_RUN fill simulation
├── trade_logger.py — SQLite trade DB
├── notifier.py — Telegram alerts + scheduled reports
└── config.py — Env-driven config (dataclass, .env loaded)
Tech stack: Python 3.12 (async/await), py-clob-client, Polymarket Gamma API, Polygon (chain_id 137, USDC.e), SQLite, websockets.
- Python 3.12+
- A funded Polygon wallet (USDC.e + a small amount of POL for gas)
- Polymarket account using that wallet
- USDC + CTF approvals granted to the Polymarket exchange contracts (one-time, on-chain TX — see Polymarket's developer docs)
- Telegram bot + chat ID (optional, for notifications)
git clone https://github.com/Bachamht/YieldPilot.git
cd YieldPilot
python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements.txtcp .env.example .env
# Edit .env — set PRIVATE_KEY (your EOA private key, 0x-prefixed)
# and tune capital limits for your account size.DRY_RUN=true python main.pyOnce the dashboard renders without errors and you see "Cold start complete", flip DRY_RUN=false to go live.
All configuration is via environment variables (loaded from .env). The most important knobs:
| Variable | Default | Description |
|---|---|---|
FARM_MODE |
true |
Pure Q-score farming. Leave this on. |
FARM_SPREAD_FRACTION |
0.40 |
Distance from mid as fraction of maxSpread. Wider = fewer fills. |
FARM_MAX_BEHIND_TICKS |
15 |
Soft cap: max ticks behind book edge after price adjustments. |
FARM_DAILY_LOSS_BUDGET |
30 |
USD adverse-fill loss budget before pausing. |
FARM_VOL_PAUSE_THRESHOLD |
0.02 |
Pause quoting when realized volatility exceeds this. |
DRY_RUN |
false |
true = simulation, false = live trading. |
| Variable | Default | Description |
|---|---|---|
MAX_ACTIVE_MARKETS |
2–3 |
Each rewardsMinSize=200 market needs ~$400 capital. |
MAX_TOTAL_POSITION |
800–1500 |
Total position limit (USDC). |
MAX_DAILY_LOSS |
60 |
Daily loss limit; triggers kill switch. |
MIN_OPERATING_BALANCE |
50 |
Min USDC required to continue placing new positions. |
| Variable | Default | Description |
|---|---|---|
MAX_VOLATILITY |
0.025 |
Reject high-volatility markets. |
MIN_DAILY_VOLUME |
5000 |
Min 24h volume in USDC. |
PROB_MIN |
0.10 |
Lower probability bound (strict). |
PROB_MAX |
0.90 |
Upper probability bound (strict). |
MIN_DAYS_TO_EXPIRY |
7 |
Min days to settlement for non-sports markets. |
See config.py for the full list (~40 knobs).
# Simulation — no real orders sent
DRY_RUN=true python main.py
# Live trading
DRY_RUN=false python main.pySend SIGTERM (or Ctrl+C) to shut down gracefully. The bot will sync open fills, cancel orders, on-chain merge any YES+NO pairs to recover collateral, sell remaining single-sided positions, and redeem settled markets before exiting. Do not SIGKILL — graceful shutdown involves multiple on-chain transactions and may take several minutes; a force-kill mid-merge can strand inventory.
The bot also recognizes a .smart_shutdown sentinel file in the working directory: if present at SIGTERM time, the shutdown sequence preserves orders and positions in markets that are still reward-eligible (so a restart doesn't reset Q-Score time-on-book). Create the file before sending SIGTERM if you want this behavior.
- Startup — Sync positions from data-api, adopt any legacy positions as orphans, selectively cancel stale orders, connect WebSocket.
- Market scan — Periodically fetch markets from the Gamma API, filter for reward eligibility and affordability, score by net daily reward value, select top N. Hysteresis prevents incumbent eviction on marginal score drift.
- BUILD phase — For each new token, acquire inventory until
my_shares ≥ rewardsMinSize. Tight-spread markets use a FOK taker; wide-spread markets build passively at best_bid. - FARM phase — Place wide passive two-sided quotes at
maxSpread × FARM_SPREAD_FRACTIONfrom mid. Refresh only when price drifts past threshold. Both BUY and SELL stay on the book continuously. - Risk monitoring — Volatility pause, daily loss budget, fill-rate auto-widen, low-balance pair-merge recovery, expiry-based liquidation.
- Reward tracking — Periodically poll Polymarket's
/rewards/userendpoint and surface daily payouts. - Shutdown — Cancel orders → on-chain merge YES+NO pairs → SELL remaining single-sided positions → redeem settled markets.
Experimental. The codebase is functional and self-contained, but it has not been audited and is provided without support. It is hardened against many of the failure modes that arise when running market-making logic against a live CLOB (data-api lag, WebSocket teardown races, post_only crosses, phantom positions, sync clobbering, cost-basis writeoff races, BUILD over-buy on wide spreads, and others), but it is not hardened against:
- Markets, instruments, or chains beyond Polymarket on Polygon.
- Account configurations other than EOA-with-proxy.
- Concurrent execution of multiple bot instances against the same wallet.
- Adverse changes to Polymarket's reward formula, API contracts, or fee structure.
If you fork it, expect to put real work into adapting it to your situation.
- Single-account, single-chain — no multi-account orchestration, no cross-venue arbitrage.
- Reward formula assumptions — the Q-Score estimator is calibrated against a small empirical sample. Polymarket can change the formula at any time, and the calibration constant may need re-tuning.
- No backtester — the dry-run simulator covers fill detection and order lifecycle but does not replay historical books. Strategy iteration requires patient live experimentation.
- No web UI — terminal dashboard + Telegram only.
- Telegram-only notifications — no Discord / email / webhook integrations.
Issues and pull requests are welcome, with a few notes:
- This codebase is committed to the reward-farming strategy. PRs that reintroduce spread-capture optimizations into the farm-mode code path will not be merged.
- Bug fixes should include a regression test or a clear reproduction trace.
- Be wary of "small refactors": some code is the way it is because of a real bug it's defending against. Read the surrounding context before tidying.
- New runtime dependencies require a strong justification — the current dep set is intentionally minimal.
MIT © Bachamht.