Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerDiscovery0.1 MarketScanner

0.1 MarketScanner

Discovery Signal Service Read-onlyRecommend LIVE General live capital · Indirect P2 · Data normalisation pending reference bot

MarketScanner continuously scans every live Polymarket market on each scan cycle and scores each one for tradability based on volume, book depth, spread, and resolution metadata. Markets that pass all filters emit an OrderIntent candidate to the Strategy layer for further evaluation. MarketScanner is strictly read-only — it never submits, signs, or modifies orders. All output is a recommendation that Strategy may accept or ignore.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerDiscovery  Discovery
Bot classSignal Service
AuthorityRead-onlyRecommend
StatusLIVE
ReadinessGeneral live
Runs beforeStrategy OrderIntent generation
Runs afterMarket data ingestion and book refresh
Applies toAll live Polymarket markets on every scan cycle
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Intelligence pod

2. Purpose

MarketScanner continuously scans every live Polymarket market on each scan cycle and scores each one for tradability based on volume, book depth, spread, and resolution metadata. Markets that pass all filters emit an OrderIntent candidate to the Strategy layer for further evaluation. MarketScanner is strictly read-only — it never submits, signs, or modifies orders. All output is a recommendation that Strategy may accept or ignore.

3. Why This Bot Matters

  • Strategy allowed to consider illiquid markets

    Without a tradability filter, strategies may generate intents on markets too thin to fill without severe price impact, wasting compute and risking poor execution.

  • Stale market list used

    A market that has resolved or been paused may still appear in a static list. Acting on it wastes budget and risks failed submissions.

  • Wide-spread markets not filtered

    Markets with unusually wide spreads have high transaction costs. Without a spread filter, strategies may target them without accounting for the extra cost.

  • No volume floor applied

    A market with near-zero 24-hour volume is unlikely to fill at a reasonable price. Surfacing it to Strategy without a volume check leads to wasted resources.

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.

4. Required Polymarket Inputs

InputSourceRequired?Use
Full list of live markets with condition IDs and metadataGamma APIYesEnumerate every currently active market to scan on each cycle.
CLOB order book depth and last-trade timestamp per marketCLOBYesCompute visible depth and detect markets with no recent trading activity.
Bid-ask spread per marketWebSocketYesFilter out markets whose current spread exceeds max_spread_bps.
24-hour trading volume per marketData APIYesApply the min_volume_24h_usd filter to exclude low-activity markets.
Market resolution rules text and neg-risk flagGamma APINoPass resolution metadata to Strategy so it can assess resolution risk without re-fetching.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesSuppress all OrderIntent candidate emissions when KillSwitch is active; continue scanning passively.
Strategy interest listStrategyRegistryNoPrioritise markets that registered strategies have expressed interest in on each scan cycle.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
scan_interval_s30105How often in seconds the full market list is re-scanned. Shorter intervals provide fresher signals but increase API call volume.
min_volume_24h_usd1000500100Minimum 24-hour trading volume in USD a market must have to be considered tradable.
min_book_depth_usd500250100Minimum total USD depth across the top 50 book levels required for a market to pass the scan filter.
max_spread_bps400300800Maximum allowed bid-ask spread in basis points (100 bps = 1 percentage point) for a market to be considered tradable.

7. Detailed Parameter Instructions

scan_interval_s

What it means

How often in seconds the full market list is re-scanned. Shorter intervals provide fresher signals but increase API call volume.

Default

{ "scan_interval_s": 30 }

Why this default matters

A 30-second interval keeps market scores reasonably fresh without hammering the Gamma API or CLOB with excessive polling. Below 10 seconds, rate-limiting becomes a concern.

Threshold logic

ConditionAction
scan_interval_s ≥ 30Normal scan cadence
10–30 sWARN — increased API load, monitor rate limits
< 5 sReject config change — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.scan_interval_s < p.hard) throw new ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Markets are checked on a regular schedule to keep the list of opportunities up to date without overloading the data feeds.

min_volume_24h_usd

What it means

Minimum 24-hour trading volume in USD a market must have to be considered tradable.

Default

{ "min_volume_24h_usd": 1000 }

Why this default matters

A market with less than $1000 in daily volume is unlikely to have sufficient liquidity to fill any meaningful order without significant price impact.

Threshold logic

ConditionAction
volume_24h ≥ 1000 USDInclude in candidate list
500–1000 USDInclude with low-confidence flag
< 100 USDExclude — volume below minimum floor

Developer check

if (market.volume24h < p.hard) return exclude('INSUFFICIENT_VISIBLE_DEPTH');

User-facing English

Markets with very little recent trading activity are not shown as opportunities because they may be difficult to trade in and out of.

min_book_depth_usd

What it means

Minimum total USD depth across the top 50 book levels required for a market to pass the scan filter.

Default

{ "min_book_depth_usd": 500 }

Why this default matters

A market with less than $500 in total visible depth has very thin resting liquidity; even modest-sized orders would consume most of it.

Threshold logic

ConditionAction
book_depth ≥ 500 USDInclude in candidate list
100–500 USDInclude with thin-book flag
< 100 USDExclude — INSUFFICIENT_VISIBLE_DEPTH

Developer check

if (market.bookDepthUsd < p.hard) return exclude('INSUFFICIENT_VISIBLE_DEPTH');

User-facing English

Markets where there is very little money resting at the buy and sell prices are excluded because they would be hard to trade without moving the price substantially.

max_spread_bps

What it means

Maximum allowed bid-ask spread in basis points (100 bps = 1 percentage point) for a market to be considered tradable.

Default

{ "max_spread_bps": 400 }

Why this default matters

A spread above 400 bps (4 percentage points) means the cost of entering and immediately exiting a position is more than 4%, which overwhelms most expected-value edges.

Threshold logic

ConditionAction
spread ≤ 300 bpsInclude in candidate list
300–400 bpsInclude with wide-spread flag
400–800 bpsInclude with warning annotation
> 800 bpsExclude — SPREAD_TOO_WIDE

Developer check

if (market.spreadBps > p.hard) return exclude('SPREAD_TOO_WIDE');

User-facing English

Markets where the gap between the buy and sell price is unusually large are not surfaced, because they would cost too much to trade.

8. Default Configuration

{
  "bot_id": "intel.market_scanner",
  "version": "1.0.0",
  "mode": "general_live",
  "defaults": {
    "scan_interval_s": 30,
    "min_volume_24h_usd": 1000,
    "min_book_depth_usd": 500,
    "max_spread_bps": 400
  },
  "locked": {
    "scan_interval_s": {
      "min": 5
    },
    "min_volume_24h_usd": {
      "min": 100
    },
    "min_book_depth_usd": {
      "min": 100
    }
  }
}

9. Implementation Flow

  1. On each scan cycle (every scan_interval_s seconds), fetch the full list of live markets from Gamma API.
  2. Check KillSwitch active flag; if active, complete the scan and update market scores but suppress all OrderIntent candidate emissions.
  3. For each market in the list, fetch top-50 CLOB book depth and compute total visible_depth_usd.
  4. For each market, fetch the current bid-ask spread from WebSocket and compute spread_bps.
  5. For each market, fetch 24-hour trading volume from Data API.
  6. Apply filters in order: (a) exclude if volume_24h < min_volume_24h_usd hard floor; (b) exclude if book_depth < min_book_depth_usd hard floor; (c) exclude if spread_bps > max_spread_bps hard ceiling.
  7. For markets that pass all filters, attach resolution metadata (resolution rules text, neg-risk flag) from Gamma API.
  8. Score each passing market on a composite tradability score based on depth, spread, and volume relative to thresholds.
  9. Emit an OrderIntent candidate for each passing market to the Strategy layer, including: market_id, tradability score, depth, spread, volume, neg-risk flag, and scan timestamp.
  10. Log the full scan results (pass/fail counts, filtered-out reason codes, and top-scoring markets) to the developer log.

10. Reference Implementation

On each scan cycle, fetches the full live market list from Gamma API, applies four tradability filters (volume, depth, spread, freshness), attaches neg-risk and resolution metadata, scores each passing market, and emits OrderIntent candidates to the Strategy layer.

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.

FUNCTION scanCycle():
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  killswitchActive = ks.active

  // --- 1. Fetch live market list ---
  markets = FETCH gamma_api.GET('/markets?active=true&closed=false')
  IF markets IS NULL:
    LOG ERROR 'Gamma API unavailable — halting emission for this cycle'
    RETURN

  passed = []
  FOR market IN markets:
    // --- 2. Fetch per-market data ---
    vol24h = FETCH data_api.GET('/volume?market=' + market.condition_id)
    book   = fetchClobPublic('/book?market=' + market.condition_id)
    spread = ws_market.current_spread(market.condition_id)

    IF vol24h IS NULL OR book IS NULL OR spread IS NULL:
      CONTINUE  // skip — missing data; do not emit stale candidate

    // --- 3. Apply hard-floor filters ---
    depthUsd = SUM(level.size * level.price FOR level IN book.asks[:50] + book.bids[:50])
    spreadBps = spread.current_bps

    IF vol24h.usd < params.min_volume_24h_usd.hard:
      CONTINUE; reason=INSUFFICIENT_VISIBLE_DEPTH
    IF depthUsd < params.min_book_depth_usd.hard:
      CONTINUE; reason=INSUFFICIENT_VISIBLE_DEPTH
    IF spreadBps > params.max_spread_bps.hard:
      CONTINUE; reason=SPREAD_TOO_WIDE

    // --- 4. Compute warnings ---
    warnings = []
    IF vol24h.usd < params.min_volume_24h_usd.default:
      warnings.append('MARKET_SCANNER_LOW_VOLUME')
    IF depthUsd < params.min_book_depth_usd.default:
      warnings.append('MARKET_SCANNER_THIN_BOOK')
    IF spreadBps > params.max_spread_bps.warning:
      warnings.append('SPREAD_TOO_WIDE')

    // --- 5. Neg-risk qualification ---
    negRisk = market.neg_risk OR market.enable_neg_risk
    IF negRisk:
      // NegRisk markets: standard or augmented (open set)
      // Apply stricter depth check: require 2× min_book_depth_usd
      IF depthUsd < params.min_book_depth_usd.default * 2:
        warnings.append('MARKET_SCANNER_NEGRISK_THIN_BOOK')

    // --- 6. Resolution metadata ---
    resMeta = FETCH gamma_api.GET('/market/' + market.condition_id)
    resSource = resMeta.resolution_source  // e.g. 'UMA'

    // --- 7. Tradability score ---
    score = 0.4 * min(vol24h.usd / 10000, 1.0)
           + 0.4 * min(depthUsd / 5000, 1.0)
           + 0.2 * max(0, 1 - spreadBps / params.max_spread_bps.default)

    passed.append({
      market_id: market.condition_id,
      tradability_score: score,
      volume_24h_usd: vol24h.usd,
      book_depth_usd: depthUsd,
      spread_bps: spreadBps,
      neg_risk: negRisk,
      resolution_source: resSource,
      warnings: warnings,
      scanned_at: now_iso()
    })

  // --- 8. Emit candidates (suppressed if KillSwitch active) ---
  IF NOT killswitchActive:
    FOR candidate IN passed:
      EMIT OrderIntentCandidate(candidate)

  // --- 9. Log cycle stats ---
  LOG INFO { markets_scanned: len(markets), passed: len(passed),
             excluded: len(markets)-len(passed), killswitch: killswitchActive }

Helpers used

HelperSignaturePurpose
fetchClobPublicfetchClobPublic(path: str) -> JSONUnauthenticated GET against https://clob.polymarket.com; returns parsed JSON or null on error.
isStaleisStale(snapshot: any, maxAgeS: int) -> boolReturns true if snapshot was fetched more than maxAgeS seconds ago.
platformFeeplatformFee(notional: float, prob: float, feeRate: float) -> floatEstimates platform fee C*feeRate*p*(1-p); used to annotate expected transaction cost on each candidate.
toUsdcUnitstoUsdcUnits(rawUsd: float) -> intNot called directly; imported for pUSD precision consistency.

SDK calls used

  • fetchClobPublic('/book?market=0xabc123...&depth=50')
  • gamma_api.GET('/markets?active=true&closed=false')
  • gamma_api.GET('/market/0xabc123...')
  • data_api.GET('/volume?market=0xabc123...&window=24h')
  • ws_market.current_spread('0xabc123...')

Complexity: O(M) where M = number of live markets per scan cycle

11. Wire Examples

Input — what arrives on the wire

Gamma API market list entrygamma_api

{
  "condition_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "question": "Will event X happen before 2026-06-01?",
  "active": true,
  "closed": false,
  "neg_risk": false,
  "enable_neg_risk": false,
  "resolution_source": "UMA Optimistic Oracle",
  "minimum_tick_size": 0.01,
  "volume_24h": "8540.00",
  "spread_bps": 210
}

Output — what the bot emits

OrderIntent candidate — market passing all filters

{
  "scanner_id": "disc.market_scanner",
  "market_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "condition_id": "0x7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a",
  "tradability_score": 0.78,
  "volume_24h_usd": 8540,
  "book_depth_usd": 3200,
  "spread_bps": 210,
  "neg_risk": false,
  "resolution_source": "UMA Optimistic Oracle",
  "warnings": [],
  "scanned_at": "2026-05-09T11:30:00Z",
  "type": "CANDIDATE"
}

OrderIntent candidate — neg-risk market with thin-book warning

{
  "scanner_id": "disc.market_scanner",
  "market_id": "0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
  "condition_id": "0x8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
  "tradability_score": 0.42,
  "volume_24h_usd": 2100,
  "book_depth_usd": 620,
  "spread_bps": 340,
  "neg_risk": true,
  "resolution_source": "UMA Optimistic Oracle",
  "warnings": [
    "MARKET_SCANNER_NEGRISK_THIN_BOOK",
    "SPREAD_TOO_WIDE"
  ],
  "scanned_at": "2026-05-09T11:30:00Z",
  "type": "CANDIDATE"
}

Reproduce locally

curl 'https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=100'

12. Decision Logic

APPROVE

Not applicable — MarketScanner does not issue approval votes. It emits OrderIntent candidates for markets that pass all four filters (volume, depth, spread, freshness).

RESHAPE_REQUIRED

Not applicable — MarketScanner does not reshape orders. It is read-only.

REJECT

Markets that fail any filter are excluded from the candidate list with a reason code: INSUFFICIENT_VISIBLE_DEPTH (depth or volume too low) or SPREAD_TOO_WIDE (spread above hard ceiling). KillSwitch active suppresses all emissions without excluding markets from the scan.

WARNING_ONLY

Markets between the warning and hard threshold on any parameter are included in the candidate list with a warning annotation flag set in the OrderIntent. Strategy decides whether to proceed.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "scanner_id": "intel.market_scanner",
  "market_id": "CLOB:0xabc123",
  "condition_id": "0xabc123",
  "tradability_score": 0.78,
  "volume_24h_usd": 8500,
  "book_depth_usd": 3200,
  "spread_bps": 210,
  "neg_risk": false,
  "resolution_source": "UMA Optimistic Oracle",
  "warnings": [],
  "scanned_at": "2026-05-09T11:30:00Z",
  "type": "CANDIDATE"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
INSUFFICIENT_VISIBLE_DEPTHEXPLAINMarket excluded because 24h volume or book depth is below the hard floor.Exclude market from candidate list; log exclusion_breakdown.Markets with very low trading volume or thin order books are not surfaced as opportunities.
SPREAD_TOO_WIDEEXPLAINMarket excluded because bid-ask spread exceeds the hard ceiling.Exclude market from candidate list; log exclusion reason.Markets where the gap between the buy and sell price is unusually large are not surfaced.
KILL_SWITCH_ACTIVEHARD_REJECTKillSwitch is active; candidate emissions are suppressed.Complete the scan and score all markets but do not emit any OrderIntent candidates.No market opportunities are being surfaced right now because trading has been paused system-wide.
STALE_MARKET_DATAHARD_REJECTGamma API or Data API unavailable; this scan cycle is halted.Do not emit any candidates for this cycle; log the error.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTscan_interval_s is below the locked hard minimum of 5s.Reject the config change; do not apply.
MARKET_SCANNER_LOW_VOLUMEWARNMarket volume is between the warning and hard floor thresholds.Include candidate with LOW_VOLUME warning flag; Strategy decides whether to proceed.
MARKET_SCANNER_THIN_BOOKWARNMarket book depth is between the warning and hard floor thresholds.Include candidate with THIN_BOOK warning flag.This market passed the minimum filters but has lower depth than usual. Additional size restrictions will apply downstream.
MARKET_SCANNER_NEGRISK_THIN_BOOKWARNNegRisk market depth is below 2× min_book_depth_usd, indicating elevated definition-shift risk.Include candidate with NEGRISK_THIN_BOOK warning flag.
NEGRISK_CONVERT_AVAILABLEEXPLAINNegRisk market has enable_neg_risk=true; convert-arb may be available via NegRiskAdapter.Annotate candidate with negrisk_convert_available=true.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_disc_marketscanner_markets_scanned_totalcountercountscan_cycleTotal markets evaluated per scan cycle.
polytraders_disc_marketscanner_candidates_emitted_totalcountercountTotal OrderIntent candidates emitted to the Strategy layer.
polytraders_disc_marketscanner_exclusions_totalcountercountreason_codeMarkets excluded per cycle broken down by filter reason.
polytraders_disc_marketscanner_tradability_scorehistogramratioDistribution of tradability scores for passing markets.
polytraders_disc_marketscanner_scan_latency_mshistogramsecondsWall-clock latency of a full scan cycle.
polytraders_disc_marketscanner_negrisk_markets_totalgaugecountNumber of active neg-risk markets found in the current scan.

Alerts

AlertConditionSeverityRunbook
MarketScannerGammaAPIDownrate(polytraders_disc_marketscanner_markets_scanned_total[5m]) == 0P1#runbook-marketscanner-gamma-api
MarketScannerHighLatencyhistogram_quantile(0.99, rate(polytraders_disc_marketscanner_scan_latency_ms_bucket[5m])) > 5000P2#runbook-marketscanner-latency
MarketScannerZeroCandidatesrate(polytraders_disc_marketscanner_candidates_emitted_total[10m]) == 0 AND polytraders_risk_killswitch_active == 0P1#runbook-marketscanner-zero-candidates
MarketScannerAllExcludedpolytraders_disc_marketscanner_candidates_emitted_total / polytraders_disc_marketscanner_markets_scanned_total < 0.01P2#runbook-marketscanner-all-excluded

Dashboards

  • Grafana — Discovery / MarketScanner cycle health
  • Grafana — Market quality / tradability score distribution

Log levels

LevelWhat gets logged
DEBUGPer-market filter result including vol24h, depthUsd, spreadBps, and tradability score.
INFOCycle summary: markets_scanned, markets_passed, top_tradability_score, killswitch_active.
WARNGamma API or Data API slow response; market exclusion rate > 90%.
ERRORGamma API unavailable; Data API unavailable; KillSwitch status unreadable.

16. Developer Reporting

{
  "scanner_id": "intel.market_scanner",
  "scan_cycle": 1428,
  "markets_scanned": 312,
  "markets_passed": 47,
  "markets_excluded": 265,
  "exclusion_breakdown": {
    "INSUFFICIENT_VISIBLE_DEPTH": 189,
    "SPREAD_TOO_WIDE": 76
  },
  "top_market": "CLOB:0xabc123",
  "top_tradability_score": 0.78,
  "killswitch_active": false,
  "scanned_at": "2026-05-09T11:30:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Fewer opportunities shown than expectedMany markets were filtered out because they had low trading volume, thin order books, or wide bid-ask spreads. Only markets that meet the minimum quality thresholds are passed to strategies.
Market not appearing in opportunitiesA specific market may have been excluded because its recent trading volume, available liquidity, or spread did not meet the current filters. These thresholds protect against trading in markets where execution would be poor.
No opportunities during KillSwitchNo market opportunities are being surfaced right now because trading has been paused system-wide. The scan continues in the background and will resume emitting candidates once trading is unpaused.
Market flagged with thin-book warningThis market passed the minimum filters but has lower depth than usual. Any strategy that uses it will apply additional size restrictions through the liquidity guardrail.

18. Failure-Mode Block

main_failure_modeEmitting a candidate for a market whose data was valid at scan time but which has since dropped below the filters (e.g. a book that empties between the scan and strategy execution), causing a strategy to generate an intent on a now-illiquid market.
false_positive_riskIncluding a borderline market that sits just above the volume or depth floors, which has a high probability of failing the LiquidityGuard check anyway, wasting the downstream processing.
false_negative_riskExcluding a genuinely tradable market because its depth or volume was temporarily low at scan time — for example immediately after a large fill cleared the book.
safe_fallbackIf Gamma API or Data API are unavailable, halt candidate emissions for the affected cycle with STALE_MARKET_DATA rather than emitting stale candidates. Never emit on missing data.
required_dependenciesGamma API live market list and metadata, CLOB top-50 book snapshot per market, Data API 24-hour volume per market, WebSocket bid-ask spread per market, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
GAMMA_API_DOWNBlock TCP to gamma-api.polymarket.comNo candidates emitted for this cycle; ERROR logged; next cycle resumes when API recoversAutomatic on next scan cycle after Gamma API is reachable.
ALL_MARKETS_EXCLUDEDSet all mock markets to volume_24h=0Zero candidates emitted; MarketScannerZeroCandidates alert firesAutomatic when markets have non-zero volume.
KILL_SWITCH_ONSet killswitch.active=trueScan runs and scores are computed but no candidates are emittedCandidates resume emitting on the first scan cycle after KillSwitch reset.
STALE_SPREAD_DATADisconnect WS market feed for 120sBot falls back to REST spread poll; if REST also fails, markets with missing spread are excludedAutomatic when WS reconnects.
NEGRISK_THIN_BOOKSet a neg-risk market's book depth to 300 pUSD (below 2× min_book_depth_usd=500)Candidate emitted with MARKET_SCANNER_NEGRISK_THIN_BOOK warningWarning clears when depth exceeds threshold.

20. State & Persistence

Stateless between scan cycles except for a short-lived in-memory market score cache.

State stores

NameKindKeyValue shapeTTLDurability
market_score_cachein-memorycondition_id{ tradability_score: float, warnings: str[], scanned_at: iso_ts }scan_interval_sbest-effort

Cold-start recovery

On cold start, the cache is empty. The first scan cycle populates it.

On restart

All scores are re-computed on the first scan cycle after restart. No durable state is loaded.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight1
Idempotency keyscan_cycle_id
Replay-safeTrue
Deduplicationby scan_cycle_id — only one scan runs at a time
Ordering guaranteesno ordering — candidates are emitted in score order
Per-call timeout (ms)5000
Backpressure strategydrop newest
Locking / mutual exclusionnone

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchKillSwitch gate determines whether candidates are emitted.If KillSwitch active, scan continues but all emissions are suppressed.

Emits to (downstream consumers)

BotWhyContract
risk.liquidity_guardCandidates downstream trigger LiquidityGuard checks on each OrderIntent.Candidate includes book_depth_usd for pre-qualification; LiquidityGuard re-checks at intent time.
risk.oracle_risk_monitorCandidate includes resolution_source metadata for OracleRiskMonitor pre-qualification.Strategy passes resolution_source from candidate to OracleRiskMonitor context.

Used by (auto-aggregated)

0.2 0.5 0.6

External services

ServiceEndpointSLA assumedOn failure
Gamma APIhttps://gamma-api.polymarket.com99.9% / 500ms p99Halt emission for this scan cycle; retry on next cycle.
Data API (volume)https://data-api.polymarket.com99.9% / 500ms p99Skip affected markets; do not emit candidates with missing volume data.
CLOB API (read)https://clob.polymarket.com99.95% / 200ms p99Skip affected markets; do not emit candidates with missing book data.
WS market feedwss://ws-subscriptions-clob.polymarket.com/ws/marketbest-effortFalls back to REST spread poll.

23. Security Surfaces

MarketScanner is strictly read-only. It never signs, submits, or modifies orders.

Signing surface

This bot does NOT sign anything.

Abuse vectors considered

  • Gamma API returning malicious market metadata to inject a fraudulent condition_id into the candidate list

Mitigations

  • condition_id format validated against known 32-byte hex pattern before inclusion
  • All candidates are recommendations only — downstream guardrails independently re-validate every market

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK used@polymarket/clob-client-v2 ^2.x
Settlement contractCTFExchangeV2 on Polygon
NotesMarketScanner reads Gamma API's negRisk and enableNegRisk fields to qualify neg-risk markets (standard closed-set and augmented open-set). Volume and depth are denominated in pUSD. Platform fee estimator uses the V2 formula C*feeRate*p*(1-p) for annotation.

API surfaces declared

gamma_apidata_apiclob_publicws_market

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.3
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1 (USDC.e + HMAC builder)v2 (pUSD + builderCode field)Polymarket V2 cutoverUpdated Gamma API queries to include enableNegRisk flag. Volume and depth figures now denominated in pUSD. Removed any references to USDC.e balance or HMAC builder code fields in candidate payloads.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Exclude market when volume_24h below hard floorvolume_24h_usd=80, min_volume_24h_usd hard=100Market excluded with INSUFFICIENT_VISIBLE_DEPTH
Exclude market when book_depth below hard floorbook_depth_usd=90, min_book_depth_usd hard=100Market excluded with INSUFFICIENT_VISIBLE_DEPTH
Exclude market when spread above hard ceilingspread_bps=900, max_spread_bps hard=800Market excluded with SPREAD_TOO_WIDE
Include market with warning when spread between warning and hardspread_bps=350, warning=300, hard=800OrderIntent candidate emitted with warnings=['SPREAD_TOO_WIDE']
Emit candidate for market passing all filtersvolume=5000, depth=1500, spread_bps=180OrderIntent candidate emitted with no warnings
Suppress emissions when KillSwitch is activekillswitch.active=true, all filters passScan runs, scores computed, but no OrderIntent candidates emitted
Reject config change when scan_interval_s below hard minimumscan_interval_s=3, hard=5ConfigError PARAMETER_CHANGE_REQUIRES_APPROVAL

Integration Tests

TestExpected result
End-to-end: candidate from MarketScanner reaches Strategy and generates valid OrderIntentStrategy receives OrderIntent candidate with tradability_score and metadata; produces strategy-level OrderIntent without re-fetching market list
Gamma API unavailability causes scan to halt and emit STALE_MARKET_DATA logNo candidates emitted during outage cycle; next cycle resumes normally when API recovers
KillSwitch deactivation resumes emissions on next scan cycleCandidates resume emitting on the scan cycle immediately following KillSwitch deactivation

Property Tests

PropertyRequired behaviour
MarketScanner never submits, signs, or modifies any orderAlways true — output is always an OrderIntent candidate recommendation only
No OrderIntent candidates are emitted when KillSwitch is activeAlways true
No candidate is emitted when any required data source is absent or staleAlways true — missing data halts emissions for the affected cycle

27. Operational Runbook

MarketScanner incidents are usually Gamma API or Data API outages. The bot is read-only so incidents do not affect active positions — only new opportunity discovery.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
MarketScannerGammaAPIDownCheck https://gamma-api.polymarket.com status.If Gamma API is down, no new market candidates will be discovered. Active positions and guardrails are unaffected.No immediate action required on active positions. Notify Polymarket support if outage is sustained.Intelligence pod lead after 15 minutes.
MarketScannerZeroCandidatesConfirm KillSwitch is not active. Then check exclusion_breakdown metrics.If all markets are being excluded, check whether filter thresholds are unusually tight or if market data APIs are returning abnormal values.Do not lower filter thresholds without Risk pod review.Intelligence pod lead after 10 minutes.
MarketScannerHighLatencyCheck scan_latency_ms p99. Identify which API calls are slow.If CLOB is slow, REST book fetches are taking > 500ms per market. Consider reducing scan scope to priority markets.Increase scan_interval_s temporarily if needed.Infra on-call if a specific API is > 2s p99 sustained.

Manual overrides

  • polytraders bot pause disc.market_scanner — Stops scan cycles; no new candidates are emitted. Active positions and guardrails are unaffected.
  • polytraders bot set-param disc.market_scanner --scan-interval 60 — Temporarily increases scan interval to reduce API load during a degraded period.

Healthcheck

GET /health → 200 if last scan cycle completed within 2× scan_interval_s and at least one candidate was emitted.

28. Promotion Gates

A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.

Promote to Shadow

GateHow measuredThreshold
Unit tests pass for all four filter cases and neg-risk qualificationCI test run100% pass
Gamma API integration test: market list fetch and condition_id validationIntegration testPass

Promote to Limited live

GateHow measuredThreshold
Scan cycle latency p99 < 5s over 48hpolytraders_disc_marketscanner_scan_latency_ms histogramp99 < 5s
NegRisk market qualification produces correct negrisk_aware annotationsIntegration test with known neg-risk marketsPass

Promote to General live

GateHow measuredThreshold
Zero false-positive STALE_MARKET_DATA halts during normal operation over 7 daysGrafana MarketScannerGammaAPIDown alert history0 firings
KillSwitch suppression: scan runs but zero candidates emitted when KillSwitch activeIntegration testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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