1. Bot Identity
| Layer | Discovery Discovery |
|---|
| Bot class | Signal Service |
|---|
| Authority | Read-onlyRecommend |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec started |
|---|
| Runs before | Strategy OrderIntent generation |
|---|
| Runs after | MarketQualityRanker and EventCalendarMapper |
|---|
| Applies to | All markets that have passed quality scoring |
|---|
| Default mode | shadow_only |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders core — Intelligence pod |
|---|
2. Purpose
Maintain a ranked queue of the best candidate markets per registered strategy type by combining MarketQualityRanker scores, EventCalendarMapper proximity signals, and per-strategy fit-scores into a single ordered list that strategies consume without re-running discovery.
3. Why This Bot Matters
Strategies independently re-rank markets
Each strategy duplicates discovery logic, wasting compute and producing inconsistent rankings across the system.
Existing positions not suppressed
A strategy may generate a new intent on a market where the user already has an open position, causing unintended double-up exposure.
No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| queue_depth | 20 | 5 | 1 | Maximum number of ranked market entries to maintain per strategy type. |
| refresh_interval_s | 60 | 15 | 5 | How often the queue is re-ranked from fresh quality scores and book data. |
7. Detailed Parameter Instructions
queue_depth
What it means
Maximum number of ranked market entries to maintain per strategy type.
Default
{ "queue_depth": 20 }
Why this default matters
A depth of 20 gives strategies enough alternatives without overwhelming them with marginal candidates.
Threshold logic
| Condition | Action |
|---|
| >= 20 | Normal queue depth |
| 5–20 | Shallow queue — WARN |
| < 1 | Reject config — queue_depth must be >= 1 |
Developer check
if (d < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');
User-facing English
The system maintains a shortlist of the best available markets for each strategy type.
refresh_interval_s
What it means
How often the queue is re-ranked from fresh quality scores and book data.
Default
{ "refresh_interval_s": 60 }
Why this default matters
60-second refresh keeps rankings reasonably current without hammering upstream services.
Threshold logic
| Condition | Action |
|---|
| >= 60s | Normal refresh cadence |
| 15–60s | Fast refresh — WARN |
| < 5s | Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if (s < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');
User-facing English
The opportunity list is updated regularly to reflect changing market conditions.
8. Default Configuration
{
"bot_id": "disc.opportunity_queue",
"version": "0.1.0",
"mode": "shadow_only",
"defaults": {
"queue_depth": 20,
"refresh_interval_s": 60,
"strategy_filters": [],
"suppress_existing_position": true
}
}
9. Implementation Flow
- On each refresh cycle, ingest latest MarketQualityRanker ObservationReports.
- Check KillSwitch; if active, freeze queue and suppress emissions.
- Optionally ingest EventCalendarMapper reports; apply hours_to_event proximity boost.
- If suppress_existing_position=true, fetch open positions from exec.position_tracker and exclude those markets.
- For each registered strategy type, compute per-strategy fit_score using strategy_filters (e.g. time-to-res, vol range, spread tolerance).
- Final rank = quality_score * fit_score * proximity_boost.
- Retain top queue_depth entries per strategy type; evict stale entries older than 2× refresh_interval_s.
- Emit ObservationReport with ranked queue snapshot per strategy type.
- Log cycle summary with queue depth, evictions, and top entry per strategy.
10. Reference Implementation
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.
FUNCTION queueRefreshCycle():
ks = FETCH internal.killswitch.status
IF ks.active: EMIT STALE_QUEUE; RETURN
quality_reports = FETCH disc.marketqualityranker.latest_reports()
IF quality_reports IS NULL OR age(quality_reports) > 2 * params.refresh_interval_s:
EMIT ObservationReport(kind='STALE_QUEUE'); RETURN
calendar_boosts = FETCH disc.eventcalendarmapper.latest_reports() OR {}
open_positions = FETCH exec.position_tracker.open() OR {}
FOR strategy_type IN registered_strategies:
candidates = []
FOR report IN quality_reports:
IF params.suppress_existing_position AND report.market_id IN open_positions:
CONTINUE
fit = computeFitScore(strategy_type, report)
boost = calendarBoost(report.market_id, calendar_boosts)
final_score = report.quality_score * fit * boost
candidates.append({ market_id, quality_score, fit, boost, final_score })
candidates.sort(key=final_score, desc=True)
queue = candidates[:params.queue_depth]
IF len(queue) < params.queue_depth.warning:
LOG WARN 'QUEUE_TOO_SHALLOW'
EMIT ObservationReport(strategy_type, queue, refreshed_at)
LOG cycle summary
SDK calls used
fetchClobPublic('/book?market=<condition_id>&depth=1')ws_market.current_spread('<condition_id>')
Complexity: O(M × S) where M=candidate markets, S=registered strategy types
11. Wire Examples
Input — what arrives on the wire
MarketQualityRanker ObservationReport consumed by queue — disc.marketqualityranker
{
"market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
"quality_score": 0.71,
"sub_scores": {
"liquidity": 0.82,
"rule_clarity": 0.65,
"resolution_horizon": 0.6
}
}
Output — what the bot emits
ObservationReport — ranked queue for mean-reversion strategy
{
"report_id": "0xeeff33445566778899001122334455eeff33445566778899001122334455eeff",
"bot_id": "disc.opportunity_queue",
"strategy_type": "mean-reversion",
"queue": [
{
"rank": 1,
"market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
"quality_score": 0.71,
"fit_score": 0.88,
"proximity_boost": 1.0,
"final_rank_score": 0.625
}
],
"queue_depth": 1,
"refreshed_at_ms": 1746789000000
}
Reproduce locally
curl 'https://clob.polymarket.com/book?market=0x7f8a9b...&depth=1'
12. Decision Logic
APPROVE
Not applicable — OpportunityQueue emits ObservationReports, not approvals.
RESHAPE_REQUIRED
Not applicable — read-only ranking bot.
REJECT
Markets already held as open positions are suppressed when suppress_existing_position=true.
WARNING_ONLY
Markets near the queue_depth warning threshold trigger shallow-queue warnings.
13. Standard Decision Output
This bot returns a ObservationReport object. See ObservationReport schema.
{
"report_id": "0xeeff33445566778899001122334455eeff33445566778899001122334455eeff",
"bot_id": "disc.opportunity_queue",
"strategy_type": "mean-reversion",
"queue": [
{
"rank": 1,
"market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
"quality_score": 0.71,
"fit_score": 0.88,
"proximity_boost": 1.0,
"final_rank_score": 0.625
}
],
"queue_depth": 1,
"refreshed_at_ms": 1746789000000
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
STALE_QUEUE | WARN | MarketQualityRanker reports are older than 2× refresh_interval_s; queue frozen. | Freeze queue; emit STALE_QUEUE warning; notify downstream strategies. | Opportunity rankings are temporarily unavailable while upstream data refreshes. |
QUEUE_TOO_SHALLOW | WARN | Active queue depth has dropped below the warning threshold. | Log warning; relax strategy_filters if configured to do so. | Fewer opportunities than usual are available for your strategy type. |
KILL_SWITCH_ACTIVE | HARD_REJECT | KillSwitch is active; queue frozen and emissions suppressed. | Return immediately without emitting any reports. | |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | queue_depth or refresh_interval_s below locked hard minimum. | Reject config change; do not apply. | |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_disc_opportunityqueue_queue_depth_gauge | gauge | count | strategy_type | Current number of entries in the opportunity queue per strategy type. |
polytraders_disc_opportunityqueue_reports_emitted_total | counter | count | strategy_type | Total queue refresh ObservationReports emitted. |
polytraders_disc_opportunityqueue_evictions_total | counter | count | | Markets evicted from queue due to staleness or position suppression. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
OpportunityQueueEmpty | polytraders_disc_opportunityqueue_queue_depth_gauge == 0 | P2 | #runbook-opportunityqueue-empty |
OpportunityQueueStale | time() - polytraders_disc_opportunityqueue_last_refresh_ts > 120 | P1 | #runbook-opportunityqueue-stale |
Dashboards
- Grafana — Discovery / OpportunityQueue depth and evictions
Log levels
| Level | What gets logged |
|---|
| DEBUG | Per-strategy-type queue snapshot with scores. |
| INFO | Cycle summary: strategy_types_served, total_entries, evictions. |
| WARN | Queue shallow; upstream quality reports stale. |
| ERROR | KillSwitch active; MarketQualityRanker unreachable. |
16. Developer Reporting
{
"bot_id": "disc.opportunity_queue",
"cycle": 88,
"strategy_types_served": 3,
"total_queue_entries": 45,
"evictions": 3,
"suppressed_existing": 2,
"killswitch_active": false,
"refreshed_at": "2026-05-09T11:30:00Z"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Fewer opportunities than expected in queue | The queue may have few entries because of thin markets, open position suppression, or a narrow strategy filter configuration. |
| Opportunity disappeared from queue | A market may have dropped out of the queue because its quality score declined, it was excluded due to an open position, or the queue was refreshed with new data. |
18. Failure-Mode Block
| main_failure_mode | Queue becomes stale if MarketQualityRanker stops emitting; strategies continue consuming an outdated ranked list. |
|---|
| false_positive_risk | A market with a temporarily high fit_score (e.g. unusual spread) could rank near the top, causing strategies to target a marginal market. |
|---|
| false_negative_risk | A market matching the strategy filter may be suppressed if the position tracker incorrectly reports an open position. |
|---|
| safe_fallback | If MarketQualityRanker reports are stale (>2× refresh_interval_s), freeze queue and emit STALE_QUEUE warning rather than serving stale rankings. |
|---|
| required_dependencies | MarketQualityRanker ObservationReports, KillSwitch active flag |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
UPSTREAM_QUALITY_REPORTS_STALE | Stop disc.marketqualityranker for >2× refresh_interval_s | | Queue resumes normal operation when quality reports resume. |
KILL_SWITCH_ON | Set killswitch.active=true | | Queue unfreezes on next cycle after KillSwitch reset. |
QUEUE_EMPTIES_DUE_TO_SUPPRESSION | Set suppress_existing_position=true with all markets having open positions | | Automatic when positions close or new markets become available. |
20. State & Persistence
Cold-start recovery
On cold start, queue is empty; first refresh cycle populates it.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded async loop |
| Max in-flight | 1 |
| Idempotency key | refresh_cycle_id |
| Per-call timeout (ms) | 6000 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | read-write lock on queue state |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| disc.marketqualityranker | Primary source of quality scores for queue entries. | ObservationReport must include quality_score, sub_scores, and market_id. |
| risk.kill_switch | KillSwitch gate freezes queue and suppresses emissions. | If active, queue frozen; no reports emitted. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|
strat.* | Registered strategies consume the ranked queue to select their next OrderIntent candidate. | ObservationReport includes strategy_type, ranked queue entries, and refreshed_at timestamp. |
Sibling bots (same OrderIntent)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| CLOB API (read) | https://clob.polymarket.com | 99.95% / 200ms p99 | Use cached spread; log warning if cache is stale. |
23. Security Surfaces
Abuse vectors considered
- Injection of crafted quality scores via compromised MarketQualityRanker to manipulate queue ranking
Mitigations
- Queue entries clamped to [0,1] score range regardless of upstream values
- All outputs are ObservationReports; execution layer independently validates before acting
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Queue entries include neg_risk flag from upstream quality reports; all size and cost estimates denominated in pUSD. |
API surfaces declared
clob_publicws_marketinternal
Networks supported
polygon
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q4-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| queue_depth=1 returns only the top-ranked market per strategy type | queue_depth=1, 10 candidate markets | ObservationReport with queue.length=1 |
| suppress_existing_position removes held markets | suppress_existing_position=true; open position on market A | Market A absent from queue output |
| KillSwitch freezes queue | killswitch.active=true | No ObservationReports emitted; existing queue frozen |
Integration Tests
| Test | Expected result |
|---|
| MarketQualityRanker → OpportunityQueue → Strategy pipeline | Strategy receives ranked queue ObservationReport and generates OrderIntent using top-ranked market |
| Stale MarketQualityRanker reports trigger STALE_QUEUE | Queue frozen; STALE_QUEUE warning emitted; strategies notified |
Property Tests
| Property | Required behaviour |
|---|
| Queue never exceeds queue_depth entries per strategy type | Always true |
| No market with open position appears in queue when suppress_existing_position=true | Always true |
27. Operational Runbook
OpportunityQueue incidents are typically upstream data staleness or KillSwitch activation. Bot is read-only; incidents delay strategy opportunity discovery but do not affect active positions.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
OpportunityQueueStale | | | | |
OpportunityQueueEmpty | | | | |
Manual overrides
Healthcheck
GET /internal/health/opportunityqueue → green if Queue depth > 0 for at least one strategy type; last refresh within 2× refresh_interval_s.; red if Queue depth = 0 for all strategy types or no refresh in 2× interval.
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|
| Purpose defined | ✓ done |
| Required inputs listed | ✓ done |
| Parameters defined | ✓ done |
| Defaults defined | ✓ done |
| Warning thresholds defined | ✓ done |
| Hard thresholds defined | ✓ done |
| Safe fallback defined | ✓ done |
| Structured output defined | ✓ done |
| Developer log defined | ✓ done |
| Plain-English explanation | ✓ done |
| Unit tests defined | ✓ done |
| Integration tests defined | ✓ done |
| Property tests defined | ✓ done |
| Failure-mode block complete | ✓ done |
| Reference implementation pseudocode | ✓ done |
| Wire examples (input + output) | ✓ done |
| Reason codes listed | ✓ done |
| Metrics & logs defined | ✓ done |
| State & persistence defined | ✓ done |
| Concurrency & idempotency defined | ✓ done |
| Dependencies declared | ✓ done |
| Security surfaces declared | ✓ done |
| Polymarket V2 compatibility declared | ✓ done |
| Version & migration history declared | ✓ done |
| Operational runbook defined | ✓ done |
| Promotion gates defined | ✓ done |
| Failure-injection recipes defined | ✓ done |