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 | Market data ingestion |
|---|
| Applies to | Newly listed Polymarket markets not yet in the MarketScanner candidate list |
|---|
| Default mode | shadow_only |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders core — Intelligence pod |
|---|
2. Purpose
Poll the Gamma API at high frequency to detect newly listed markets as soon as they appear, parse their initial metadata, and emit ObservationReports so strategies can evaluate early liquidity opportunities before the book fills.
3. Why This Bot Matters
New markets discovered only on the next slow scan cycle
Early-entry advantage is lost; book may already be competitive by the time MarketScanner surfaces the market.
Unvalidated new market metadata forwarded to strategies
Markets with incomplete rules text or missing resolution dates may produce erroneous strategy decisions.
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 |
|---|
| poll_interval_s | 10 | 5 | 2 | How often in seconds the Gamma API is polled for newly listed markets. |
| min_listing_age_s | 30 | 10 | 0 | Minimum age in seconds a new market must have before being emitted; prevents emitting markets that may be immediately delisted. |
7. Detailed Parameter Instructions
poll_interval_s
What it means
How often in seconds the Gamma API is polled for newly listed markets.
Default
{ "poll_interval_s": 10 }
Why this default matters
10-second polling catches new listings within one cycle while staying well within Gamma API rate limits.
Threshold logic
| Condition | Action |
|---|
| >= 10s | Normal polling cadence |
| 5–10s | Fast poll — WARN; monitor rate limits |
| < 2s | Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if (s < params.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');
User-facing English
New markets are checked frequently to catch early trading opportunities.
min_listing_age_s
What it means
Minimum age in seconds a new market must have before being emitted; prevents emitting markets that may be immediately delisted.
Default
{ "min_listing_age_s": 30 }
Why this default matters
A 30-second hold-off filters transient listings that are created and immediately cancelled.
Threshold logic
| Condition | Action |
|---|
| >= 30s | Normal hold-off |
| 10–30s | Short hold-off — WARN |
| 0s | No hold-off; emit immediately |
Developer check
if (age < params.min_listing_age_s) hold();
User-facing English
New markets are held briefly before being surfaced to confirm they are stable listings.
8. Default Configuration
{
"bot_id": "disc.new_market_watcher",
"version": "0.1.0",
"mode": "shadow_only",
"defaults": {
"poll_interval_s": 10,
"min_listing_age_s": 30,
"alert_to_strategies": [],
"require_rule_parse": true
}
}
9. Implementation Flow
- On each poll cycle, send a conditional GET to Gamma API using the ETag from the previous response.
- If ETag matches (304 Not Modified), skip processing — no new markets.
- If new markets are present, diff against the known condition_id set to identify genuinely new listings.
- Check KillSwitch; if active, update known set but suppress emissions.
- For each new market, apply min_listing_age_s hold-off; hold in a pending set.
- After hold-off, fetch initial book state from CLOB and parse resolution metadata.
- If require_rule_parse=true, validate that resolution rules text and resolution date are present.
- Emit ObservationReport with condition_id, initial_book_state, neg_risk, tick_size, resolution_date.
- Add newly emitted markets to the known condition_id set.
- Log cycle summary: new_markets_detected, pending_count, emitted_count.
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 pollCycle():
ks = FETCH internal.killswitch.status
response = gamma.GET('/markets?active=true&closed=false',
headers={'If-None-Match': last_etag})
IF response.status == 304: RETURN // no new listings
last_etag = response.headers['ETag']
all_ids = {m.condition_id FOR m IN response.markets}
new_ids = all_ids - known_condition_ids
FOR condition_id IN new_ids:
market = response.market(condition_id)
listing_age_s = now() - market.created_at
IF listing_age_s < params.min_listing_age_s:
pending_set.add(condition_id, market); CONTINUE
IF params.require_rule_parse AND NOT market.rules_text:
pending_metadata.add(condition_id); CONTINUE // INCOMPLETE_MARKET_METADATA
book = FETCH clob_public.GET('/book?market=' + condition_id + '&depth=5')
initial_state = 'empty' IF book.bids == [] AND book.asks == [] ELSE 'active'
IF NOT ks.active:
EMIT ObservationReport(condition_id, listing_age_s, market.neg_risk,
market.tick_size, market.resolution_date,
initial_state)
known_condition_ids.add(condition_id)
// Also re-check pending_metadata set
FOR cid IN pending_metadata:
market = gamma.GET('/market/' + cid)
IF market.rules_text AND NOT ks.active:
EMIT ObservationReport(cid, ...)
known_condition_ids.add(cid)
LOG poll cycle summary
SDK calls used
gamma.GET('/markets?active=true&closed=false', If-None-Match: <etag>)gamma.GET('/market/<condition_id>')fetchClobPublic('/book?market=<condition_id>&depth=5')
Complexity: O(N) where N = new markets per poll cycle; amortised O(1) with ETag 304
11. Wire Examples
Input — what arrives on the wire
Gamma API new market entry — gamma_api
{
"condition_id": "0xaabbccddeeff001122334455667788aabbccddeeff001122334455667788aabb",
"question": "Will the new legislation pass by September 2026?",
"active": true,
"created_at": "2026-05-09T11:29:25Z",
"neg_risk": false,
"tick_size": 0.01,
"resolution_date": "2026-09-01T00:00:00Z",
"rules_text": "Resolves YES if the bill passes both chambers by midnight UTC on 2026-09-01."
}
Output — what the bot emits
ObservationReport — new market detected
{
"report_id": "0xff002244668800aabbccddee112233ff002244668800aabbccddee11223344",
"bot_id": "disc.new_market_watcher",
"market_id": "0xaabbccddeeff001122334455667788aabbccddeeff001122334455667788aabb",
"listing_detected_at_ms": 1746789000000,
"listing_age_s": 35,
"neg_risk": false,
"tick_size": 0.01,
"resolution_date": "2026-09-01T00:00:00Z",
"initial_book_state": "empty",
"warnings": [],
"emitted_at_ms": 1746789035000
}
Reproduce locally
curl -H 'If-None-Match: "abc123"' 'https://gamma-api.polymarket.com/markets?active=true&closed=false'
12. Decision Logic
APPROVE
Not applicable — NewMarketWatcher emits ObservationReports, not approvals.
RESHAPE_REQUIRED
Not applicable — read-only watcher bot.
REJECT
Markets without resolution rules text when require_rule_parse=true receive INCOMPLETE_MARKET_METADATA and are not forwarded until metadata is present.
WARNING_ONLY
Markets at or below the hold-off threshold emit a NEW_MARKET_HOLDOFF warning while pending.
13. Standard Decision Output
This bot returns a ObservationReport object. See ObservationReport schema.
{
"report_id": "0xff002244668800aabbccddee112233ff002244668800aabbccddee11223344",
"bot_id": "disc.new_market_watcher",
"market_id": "0xaabbccddeeff001122334455667788aabbccddeeff001122334455667788aabb",
"listing_detected_at_ms": 1746789000000,
"listing_age_s": 35,
"neg_risk": false,
"tick_size": 0.01,
"resolution_date": "2026-09-01T00:00:00Z",
"initial_book_state": "empty",
"warnings": [],
"emitted_at_ms": 1746789035000
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
INCOMPLETE_MARKET_METADATA | WARN | New market is missing required rules text or resolution date; held in pending set. | Retry on next poll cycle; emit once metadata is complete. | A new market is being evaluated but is missing some required details. |
NEW_MARKET_HOLDOFF | INFO | Market is within the min_listing_age_s hold-off window. | Hold in pending set; emit after hold-off expires. | A newly listed market is being held briefly before surfacing. |
KILL_SWITCH_ACTIVE | HARD_REJECT | KillSwitch is active; all emissions suppressed. | Update known set but emit no ObservationReports. | |
STALE_MARKET_DATA | HARD_REJECT | Gamma API unavailable; serving known set from cache only. | Halt new-market emissions; retry on next poll cycle. | |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | poll_interval_s below locked hard minimum of 2s. | Reject config change. | |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_disc_newmarketwatcher_new_markets_detected_total | counter | count | | Total new market_ids detected via Gamma API polling. |
polytraders_disc_newmarketwatcher_reports_emitted_total | counter | count | | ObservationReports successfully emitted for new markets. |
polytraders_disc_newmarketwatcher_pending_holdoff_gauge | gauge | count | | Number of new markets currently in the hold-off pending set. |
polytraders_disc_newmarketwatcher_poll_latency_ms | histogram | ms | | Latency of each Gamma API poll cycle. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
NewMarketWatcherGammaAPIDown | rate(polytraders_disc_newmarketwatcher_new_markets_detected_total[5m]) == 0 AND up{job='gamma_api'} == 0 | P1 | #runbook-newmarketwatcher-gamma-api |
NewMarketWatcherHighPending | polytraders_disc_newmarketwatcher_pending_holdoff_gauge > 20 | P2 | #runbook-newmarketwatcher-high-pending |
Dashboards
- Grafana — Discovery / NewMarketWatcher detection latency
Log levels
| Level | What gets logged |
|---|
| DEBUG | Per-new-market condition_id, listing_age_s, initial_book_state. |
| INFO | Poll cycle summary: detected, pending, emitted. |
| WARN | Gamma API slow; high pending holdoff count. |
| ERROR | Gamma API unavailable; CLOB unreachable. |
16. Developer Reporting
{
"bot_id": "disc.new_market_watcher",
"poll_cycle": 4820,
"new_markets_detected": 2,
"pending_holdoff": 1,
"emitted": 1,
"suppressed_killswitch": 0,
"known_set_size": 318,
"polled_at": "2026-05-09T11:30:00Z"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| New market shown immediately after launch | This market was just listed on Polymarket. It may have very little liquidity right now — strategies will assess whether early positioning is appropriate. |
| New market not yet surfaced | A newly listed market may be in the hold-off period while we confirm it is a stable listing before surfacing it. |
18. Failure-Mode Block
| main_failure_mode | ETag polling misses a burst of new listings if Gamma API returns a non-304 response that is then lost due to a network error. |
|---|
| false_positive_risk | A market that is listed and immediately delisted within the hold-off window may still be emitted if the hold-off is shorter than the cancellation latency. |
|---|
| false_negative_risk | A new market without a resolution rules text when require_rule_parse=true will be silently held until metadata appears, missing the early liquidity window. |
|---|
| safe_fallback | If Gamma API is unavailable, continue serving the known set from cache; do not emit speculative new-market reports on stale data. |
|---|
| required_dependencies | Gamma API ETag-capable listing endpoint, CLOB initial book snapshot, MarketScanner known condition_id set, KillSwitch active flag |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
GAMMA_API_DOWN | Block TCP to gamma-api.polymarket.com | | Automatic when API recovers on next poll cycle. |
NEW_MARKET_MISSING_RULES | Create a mock market with rules_text=null and require_rule_parse=true | | Automatic when Gamma API returns complete metadata. |
KILL_SWITCH_ON | Set killswitch.active=true | | Emissions resume on next cycle after KillSwitch reset. |
20. State & Persistence
Cold-start recovery
On cold start, known set is bootstrapped from MarketScanner's current candidate list.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded async loop |
| Max in-flight | 1 |
| Idempotency key | poll_cycle_etag |
| Per-call timeout (ms) | 4000 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | none |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| disc.marketscanner | Provides baseline known condition_id set for new-market diffing. | Expects a set of active condition_ids. |
| risk.kill_switch | KillSwitch suppresses emissions but known set updates continue. | If active, no ObservationReports emitted. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|
| disc.marketscanner | NewMarketWatcher feeds newly detected markets back into MarketScanner for full tradability scoring. | ObservationReport includes condition_id, neg_risk, tick_size, and resolution_date. |
Sibling bots (same OrderIntent)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Gamma API | https://gamma-api.polymarket.com | 99.9% / 500ms p99 | Serve known set from cache; halt new-market emissions. |
23. Security Surfaces
Abuse vectors considered
- Gamma API returning spoofed new market metadata to inject fraudulent condition_ids
Mitigations
- condition_id validated against 32-byte hex pattern before entering known set
- require_rule_parse=true ensures minimal metadata quality before emission
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 | Reads Gamma API enableNegRisk flag at listing time to classify new neg-risk markets; initial depth denominated in pUSD. |
API surfaces declared
gammaclob_publicinternal
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 |
|---|
| New market in Gamma API response not in known set triggers emission after hold-off | condition_id=0xnew, listing_age=35s, min_listing_age_s=30 | ObservationReport emitted with listing_age_s=35 |
| Market missing resolution rules text blocked when require_rule_parse=true | rules_text=null, require_rule_parse=true | Market held with INCOMPLETE_MARKET_METADATA; not emitted |
| ETag 304 response skips processing | ETag matches previous response | No processing; no new emissions |
Integration Tests
| Test | Expected result |
|---|
| New market detected and forwarded to MarketScanner candidate list within 2 poll cycles | ObservationReport from NewMarketWatcher appears in MarketScanner's next cycle |
| KillSwitch active suppresses emissions but known set still updated | No ObservationReports; known set includes new market_id for next cycle |
Property Tests
| Property | Required behaviour |
|---|
| Every emitted market_id has listing_age_s >= min_listing_age_s | Always true |
| No emission when KillSwitch is active | Always true |
27. Operational Runbook
NewMarketWatcher incidents are typically Gamma API outages or high hold-off backlogs. Bot is read-only; incidents delay new market discovery but do not affect active positions.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
NewMarketWatcherGammaAPIDown | | | | |
NewMarketWatcherHighPending | | | | |
Manual overrides
Healthcheck
GET /internal/health/newmarketwatcher → green if Poll cycle completed within 3× poll_interval_s; no Gamma API errors in last 5 minutes.; red if No poll cycle in 3× interval or consecutive Gamma API failures.
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 |