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 LayerStrategy3.11 Late-Resolution Spread

3.11 Late-Resolution Spread

Strategy Alpha Strategy Trade LIVE General live capital · Direct P8 · Additional strategies pending stub

Late-Resolution Spread captures the time-decay component of high-probability outcomes near market maturity. As a binary or multi-outcome market approaches its endDate (from Gamma API), a token priced at $0.95–$0.99 retains a positive expected settlement of $1.00 if the leading outcome holds. The spread between the current market price and $1.00 shrinks deterministically as resolution approaches. This bot monitors gamma.market.endDate to identify markets within max_minutes_to_resolution of close, checks that the price-to-$1.00 spread exceeds the configurable minimum, and emits a buy OrderIntent sized to max_clip_usd. It never averages down (never_average_down is locked true). This is a user-controlled execution tool exploiting time-value decay in near-resolution predictive markets; it is not a directional price forecast.

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

LayerStrategy  Strategy
Bot classAlpha Strategy
AuthorityTrade
StatusLIVE
ReadinessGeneral live
Runs beforeRisk guardrail pipeline
Runs afterMarket scanner / opportunity feed
Applies toMarkets within max_minutes_to_resolution of their gamma.market.endDate, where price is ≥ min_price_cents/100 and spread to $1.00 exceeds min_spread_to_1_cents
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Strategy pod

2. Purpose

Late-Resolution Spread captures the time-decay component of high-probability outcomes near market maturity. As a binary or multi-outcome market approaches its endDate (from Gamma API), a token priced at $0.95–$0.99 retains a positive expected settlement of $1.00 if the leading outcome holds. The spread between the current market price and $1.00 shrinks deterministically as resolution approaches. This bot monitors gamma.market.endDate to identify markets within max_minutes_to_resolution of close, checks that the price-to-$1.00 spread exceeds the configurable minimum, and emits a buy OrderIntent sized to max_clip_usd. It never averages down (never_average_down is locked true). This is a user-controlled execution tool exploiting time-value decay in near-resolution predictive markets; it is not a directional price forecast.

3. Why This Bot Matters

  • endDate not fetched from Gamma API; using stale time-to-resolution

    Entering a late-resolution trade with incorrect time remaining may expose the position to a full overnight resolution cycle, dramatically extending holding time and risk.

  • Oracle challenge window not respected

    UMA Optimistic Oracle: $750 pUSD bond, 2-hour challenge window, potential 24–48h DVM delay. Entering a late-resolution trade when a challenge is active means the position may not settle at $1.00 within the expected window.

  • feeRateBps hardcoded on signed order (V1 pattern)

    CLOB V2 rejects orders containing feeRateBps. Fees are operator-set at match time. The signed order must not contain this field.

  • never_average_down not enforced

    If price drops after entry on a late-resolution market, averaging down concentrates risk just before resolution — the opposite of the strategy intent.

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
Market endDate and resolution metadatagamma (Gamma API — gamma.market.endDate, resolutionSource, negRisk flag)YesPrimary time-to-resolution signal. Compute minutes_to_resolution = (endDate - now) / 60. Only consider markets within max_minutes_to_resolution.
Current best ask for the leading outcomeclob_publicYesConfirm price ≥ min_price_cents/100 and compute spread = 1.00 - best_ask.
Oracle proposal status and challenge window stateonchain (UMA Optimistic Oracle / Polygon)YesSkip markets with an active oracle challenge (2h window + potential DVM delay 24–48h) to avoid unexpected settlement delays.
Market negRisk flag and multi-outcome structuregammaYesFor neg-risk events, verify each outcome's endDate independently and check NegRiskAdapter availability for position exit.
Top-of-book depth for the leading outcomeclob_publicYesSize order to min(depth_available, max_clip_usd) to avoid moving the book.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesAbort intent emission if KillSwitch is active.
Builder code bytes32internal configYesInjected into builder field on every signed V2 order for attribution.
OracleRiskMonitor approval status for this marketOracleRiskMonitorYesOnly enter when OracleRiskMonitor confirms no active challenge or DVM escalation.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_spread_to_1_cents231Minimum spread (in cents) between the current best ask and $1.00 required for entry. A spread of 2 cents means price ≤ $0.98.
max_minutes_to_resolution12030360Maximum time to resolution (in minutes, from gamma.market.endDate) at which the bot will consider entering. Markets further from resolution are not eligible.
max_clip_usd300500750Maximum pUSD size per order clip. Late-resolution markets are often illiquid; small clips prevent moving the book.
never_average_downTrueNoneNoneLocked to true. If a position already exists on this market and the current price is below the entry price, no additional buy OrderIntent is emitted.

7. Detailed Parameter Instructions

min_spread_to_1_cents

What it means

Minimum spread (in cents) between the current best ask and $1.00 required for entry. A spread of 2 cents means price ≤ $0.98.

Default

{ "min_spread_to_1_cents": 2 }

Why this default matters

2 cents (200 bps below $1) provides enough room to cover taker fees and slippage on a near-resolution trade. Below 1 cent (hard floor), the remaining upside is wholly consumed by fees.

Threshold logic

ConditionAction
spread ≥ 2 centsEMIT OrderIntent
1–2 centsWARN LATE_RES_SPREAD_TOO_TIGHT; skip (spread too narrow for fees)
< 1 cent (hard floor)SKIP — LATE_RES_SPREAD_TOO_TIGHT

Developer check

spread_cents = (1.00 - best_ask) * 100; if spread_cents < params.hard: return skip('LATE_RES_SPREAD_TOO_TIGHT')

User-facing English

The market price is already very close to $1. There is not enough room to enter profitably after fees.

max_minutes_to_resolution

What it means

Maximum time to resolution (in minutes, from gamma.market.endDate) at which the bot will consider entering. Markets further from resolution are not eligible.

Default

{ "max_minutes_to_resolution": 120 }

Why this default matters

120 minutes captures the late-resolution window without entering so early that normal price volatility erodes the spread before settlement. The 360-minute hard ceiling prevents very early entry.

Threshold logic

ConditionAction
minutes_to_resolution ≤ 120Eligible to enter
30–60 minutesWARN LATE_RES_APPROACHING; consider reducing clip size due to lower liquidity
> 360 minutesSKIP — not in late-resolution window

Developer check

minsLeft = (endDate - now_ms()) / 60000; if minsLeft > params.hard: return skip('LATE_RES_NOT_IN_WINDOW')

User-facing English

This market is not close enough to its resolution time for this strategy to apply.

max_clip_usd

What it means

Maximum pUSD size per order clip. Late-resolution markets are often illiquid; small clips prevent moving the book.

Default

{ "max_clip_usd": 300 }

Why this default matters

300 pUSD is modest enough for typical near-resolution book depth. Larger clips risk triggering order-book impact that lifts the price above the entry threshold before the order fills.

Threshold logic

ConditionAction
≤ 300 pUSDNormal clip
300–750 pUSDWARN; Risk guardrail will reshape if above portfolio budget
> 750 pUSDReject config change — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if params.max_clip_usd > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The order was sized to avoid having a large impact on this market's thin near-resolution book.

never_average_down

What it means

Locked to true. If a position already exists on this market and the current price is below the entry price, no additional buy OrderIntent is emitted.

Default

{ "never_average_down": true }

Why this default matters

Averaging down on a near-resolution market concentrates risk at exactly the wrong time. If the leading outcome reverses near maturity, a doubled position doubles the loss.

Threshold logic

ConditionAction
Always true (locked)No new buy intent if current_price < entry_price for existing position

Developer check

if position.exists and current_price < position.entry_price: return skip('LATE_RES_NO_AVERAGE_DOWN')

User-facing English

This strategy does not add to a losing position near market resolution.

8. Default Configuration

{
  "bot_id": "strat.late_resolution_spread",
  "version": "2.1.0",
  "mode": "general_live",
  "defaults": {
    "min_spread_to_1_cents": 2,
    "max_minutes_to_resolution": 120,
    "max_clip_usd": 300,
    "never_average_down": true
  },
  "locked": {
    "never_average_down": true,
    "max_clip_usd": {
      "max": 750
    },
    "max_minutes_to_resolution": {
      "max": 360
    }
  }
}

9. Implementation Flow

  1. Check KillSwitch active flag; if active, emit no OrderIntents.
  2. Poll Gamma API for markets with endDate within (now, now + max_minutes_to_resolution * 60).
  3. For each candidate market: compute minutes_to_resolution = (endDate - now_ms()) / 60000.
  4. If minutes_to_resolution > max_minutes_to_resolution hard (360 min), skip market.
  5. Fetch best_ask for the leading YES outcome from clob_public.
  6. Confirm best_ask ≥ 0.90 (minimum viable price — not a penny token). Skip if below.
  7. Compute spread_cents = (1.00 - best_ask) * 100. If spread_cents < min_spread_to_1_cents hard floor (1), skip LATE_RES_SPREAD_TOO_TIGHT.
  8. Check OracleRiskMonitor status for this market. If oracle challenge active or DVM escalation in progress, skip LATE_RES_ORACLE_CHALLENGE_ACTIVE.
  9. Confirm negRisk flag from Gamma. For neg-risk markets, also check NegRiskAdapter availability for clean exit.
  10. Check existing position: if position exists and current_price < position.entry_price, skip LATE_RES_NO_AVERAGE_DOWN (never_average_down locked true).
  11. Compute clipSize = toPusdUnits(min(depth_available, max_clip_usd)).
  12. If minutes_to_resolution < 30 (warning), reduce clipSize by 20% for thin-book safety.
  13. Emit OrderIntent: market_id, outcome=YES, side=buy, price=best_ask, size_pUSD=clipSize, tif=GTC, post_only=false, builder={code, fee_bps: 25}.
  14. Note: fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order.
  15. Emit DecisionReport with intent_emitted=true, spread_cents, minutes_to_resolution, oracle_clear=true.

10. Reference Implementation

Polls Gamma API for near-resolution markets, validates oracle status and spread, and emits a buy OrderIntent when price-to-$1 spread is sufficient and oracle is clear.

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

FUNCTION scanLateResolutionMarkets():
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active:
    RETURN

  // --- 1. Poll Gamma API for near-resolution markets ---
  now_ms = now_ms()
  windowEnd = now_ms + params.max_minutes_to_resolution * 60 * 1000
  markets = FETCH gamma.GET('/markets?endDateFrom=' + now_ms + '&endDateTo=' + windowEnd + '&active=true')
  IF isStale(markets, maxAgeS=60):
    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
    RETURN

  FOR market IN markets:
    minutesLeft = (market.endDate - now_ms) / 60000
    IF minutesLeft > params.max_minutes_to_resolution_hard:  // 360
      CONTINUE

    // --- 2. Fetch best ask ---
    book = FETCH clob_public.GET('/book?market=' + market.conditionId)
    bestAsk = book.asks[0].price
    IF bestAsk < 0.90:  // minimum viable price
      CONTINUE

    // --- 3. Spread check ---
    spreadCents = (1.00 - bestAsk) * 100
    IF spreadCents < params.min_spread_to_1_cents_hard:  // 1 cent
      CONTINUE  // spread too tight

    // --- 4. Oracle check (UMA: $750 pUSD bond, 2h challenge, 24-48h DVM) ---
    oracleStatus = FETCH internal.oracleRiskMonitor.status(market.conditionId)
    IF oracleStatus.challenge_active OR oracleStatus.dvm_escalated:
      EMIT DecisionReport(intent_emitted=false, reason='LATE_RES_ORACLE_CHALLENGE_ACTIVE')
      CONTINUE

    // --- 5. NegRisk check ---
    IF market.negRisk:
      adapterAvailable = FETCH onchain.NegRiskAdapter.isConditionRegistered(market.conditionId)
      // negRisk_aware=true on intent; adapter path available for exit

    // --- 6. Never-average-down (locked true) ---
    position = FETCH clob_auth.GET('/positions?market=' + market.conditionId)
    IF position.exists AND bestAsk < position.entry_price:
      EMIT DecisionReport(intent_emitted=false, reason='LATE_RES_NO_AVERAGE_DOWN')
      CONTINUE

    // --- 7. Sizing ---
    depth = book.asks[0].size_pusd
    clipSize = toPusdUnits(min(depth, params.max_clip_usd))
    IF minutesLeft < 30:
      WARN('LATE_RES_APPROACHING')
      clipSize = toPusdUnits(clipSize * 0.8)  // thin book near close

    // --- 8. Emit OrderIntent (V2: no feeRateBps; builder field; taker order) ---
    EMIT OrderIntent(
      market_id     = market.conditionId,
      outcome       = 'YES',
      side          = 'buy',
      price         = bestAsk,
      size_pUSD     = clipSize,
      tif           = 'GTC',
      post_only     = false,
      builder       = { code: config.builder_code, fee_bps: 25 },
      negrisk_aware = market.negRisk
    )
    EMIT DecisionReport(
      intent_emitted       = true,
      spread_cents         = spreadCents,
      minutes_to_resolution = minutesLeft,
      oracle_clear         = true,
      reasons              = ['LATE_RES_SPREAD_ENTRY']
    )

SDK calls used

  • gamma.GET('/markets?endDateFrom=...&endDateTo=...&active=true')
  • fetchClobPublic('/book?market=' + conditionId)
  • internal.oracleRiskMonitor.status(conditionId)
  • clob_auth.GET('/positions?market=' + conditionId)
  • onchain.NegRiskAdapter.isConditionRegistered(conditionId)
  • toPusdUnits(rawFloat)
  • buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })
  • internal.killswitch.status()
  • internal.builder_code

Complexity: O(M) per poll cycle where M = number of near-resolution markets

11. Wire Examples

Input — what arrives on the wire

Gamma API near-resolution marketgamma

{
  "conditionId": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
  "endDate": "2026-05-09T13:00:00Z",
  "negRisk": true,
  "active": true,
  "resolutionSource": "UMA Optimistic Oracle"
}

CLOB public book — leading outcomeclob_public

{
  "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
  "best_ask": "0.976",
  "depth_pusd": "420.00",
  "spread_cents": "2.4",
  "fetched_at_ms": 1746789900000
}

Output — what the bot emits

OrderIntent — late-resolution buy (GTC, taker, builder-attributed)

{
  "intent_id": "oi_01HX9LRSP3C1A1B",
  "trace_id": "tr_01HX9LRSP3C1VR5",
  "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
  "outcome": "YES",
  "side": "buy",
  "price": "0.976",
  "size_pUSD": "300.00",
  "tif": "GTC",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": true,
  "decision": {
    "edge_bps": 14.0,
    "spread_cents": 2.4,
    "minutes_to_resolution": 87,
    "oracle_clear": true,
    "reasons": [
      "LATE_RES_SPREAD_ENTRY"
    ]
  },
  "comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}

12. Decision Logic

APPROVE

minutes_to_resolution ≤ max_minutes_to_resolution, spread_cents ≥ min_spread_to_1_cents, oracle clear (no active challenge), no existing position below entry (never_average_down), KillSwitch inactive. Emit buy OrderIntent.

RESHAPE_REQUIRED

Not applicable — strat bots emit OrderIntents; reshaping is handled downstream by the Risk guardrail pipeline.

REJECT

Market outside resolution window; spread < 1 cent hard floor; oracle challenge active; would average down into losing position; KillSwitch active; stale Gamma data. Emit DecisionReport intent_emitted=false.

WARNING_ONLY

minutes_to_resolution < 30 (approaching) or spread between 1–2 cents triggers warning and size reduction.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "intent_id": "oi_01HX9LRSP3C1A1B",
  "trace_id": "tr_01HX9LRSP3C1VR5",
  "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
  "outcome": "YES",
  "side": "buy",
  "price": "0.976",
  "size_pUSD": "300.00",
  "tif": "GTC",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": true,
  "decision": {
    "edge_bps": 14.0,
    "spread_cents": 2.4,
    "minutes_to_resolution": 87,
    "oracle_clear": true,
    "reasons": [
      "LATE_RES_SPREAD_ENTRY"
    ]
  },
  "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
LATE_RES_SPREAD_ENTRYINFOMarket is within max_minutes_to_resolution, spread ≥ min_spread_to_1_cents, oracle clear, no averaging down. OrderIntent emitted.Emit buy OrderIntent.A near-resolution trade was placed based on a pricing gap expected to close at settlement.
LATE_RES_SPREAD_TOO_TIGHTINFOspread_cents < min_spread_to_1_cents hard floor. Price is too close to $1.00 to trade profitably after fees.Skip; emit DecisionReport intent_emitted=false.The market price is too close to $1 to enter profitably.
LATE_RES_NOT_IN_WINDOWINFOminutes_to_resolution > max_minutes_to_resolution. Market is not yet in the late-resolution window.Skip; re-evaluate on next poll cycle.This market is not close enough to its resolution time.
LATE_RES_ORACLE_CHALLENGE_ACTIVEWARNUMA Optimistic Oracle challenge is active for this market (2h window) or DVM escalation is in progress (24–48h).Skip; emit DecisionReport intent_emitted=false; re-evaluate when oracle is clear.This market's resolution is being disputed. The trade was skipped.
LATE_RES_NO_AVERAGE_DOWNINFOExisting position price is above current best ask. Never-average-down rule prevents adding to the position.Skip; emit DecisionReport intent_emitted=false.An existing position is already open at a higher price. No additional order was placed.
LATE_RES_APPROACHINGWARNminutes_to_resolution < 30. Book may be thin; clip size reduced by 20%.Emit OrderIntent at 80% clip size; log warning.Resolution is imminent. Order size was reduced to account for lower market liquidity.
STALE_MARKET_DATAHARD_REJECTGamma API endDate data is > 60s old, or CLOB book snapshot is > 5s old.Skip; no OrderIntent emitted.Market data was too old to act on safely.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all markets; no OrderIntents.Trading is currently paused.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_lateresspread_decisions_totalcountercountverdict, reason_codeTotal evaluation cycles by intent_emitted (true/false) and reason code.
polytraders_strat_lateresspread_spread_centshistogramcentsDistribution of spread (in cents below $1.00) at evaluation time, for entered and skipped opportunities.
polytraders_strat_lateresspread_minutes_to_resolutionhistogramminutesDistribution of minutes-to-resolution at entry time.
polytraders_strat_lateresspread_intents_emitted_totalcountercountnegrisk_awareTotal OrderIntents emitted, split by neg-risk/standard market type.
polytraders_strat_lateresspread_oracle_skips_totalcountercountmarket_idNumber of entry opportunities skipped due to oracle challenge or DVM escalation.
polytraders_strat_lateresspread_eval_latency_mshistogrammillisecondsWall-clock latency from Gamma poll to OrderIntent emit.

Alerts

AlertConditionSeverityRunbook
LateResSpreadGammaStalerate(polytraders_strat_lateresspread_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1warn#runbook-lateresponse-gamma-stale
LateResSpreadOracleSkipsHighrate(polytraders_strat_lateresspread_oracle_skips_total[10m]) > 3warn#runbook-lateresponse-oracle-skips
LateResSpreadHighLatencyhistogram_quantile(0.99, rate(polytraders_strat_lateresspread_eval_latency_ms_bucket[5m])) > 250warn#runbook-lateresponse-latency
LateResSpreadKillSwitchBlockingrate(polytraders_strat_lateresspread_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch

Dashboards

  • Grafana — Strategy / LateResSpread time-to-resolution and spread distribution
  • Grafana — Strategy / LateResSpread oracle skip rate and entry throughput

16. Developer Reporting

{
  "bot_id": "strat.late_resolution_spread",
  "market_id": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
  "end_date": "2026-05-09T13:00:00Z",
  "minutes_to_resolution": 87.3,
  "best_ask": 0.976,
  "spread_cents": 2.4,
  "neg_risk": true,
  "oracle_challenge_active": false,
  "clip_size_pusd": 300.0,
  "intent_emitted": true,
  "reason": "LATE_RES_SPREAD_ENTRY",
  "emitted_at_ms": 1746789900000
}

17. Plain-English Reporting

SituationUser-facing explanation
Late-resolution trade enteredThis market is close to its resolution time and the leading outcome is priced below $1. An order was placed to buy that outcome at a small discount, expecting it to settle at $1.
No entry — spread too smallThe market price is already very close to $1. After fees, there is not enough potential profit to justify the trade.
No entry — oracle challenge activeThe market's resolution is being disputed. The trade was skipped to avoid exposure during the uncertainty window.
No entry — market not near resolutionThis market is not close enough to its scheduled resolution time for this strategy. It will be re-evaluated as the resolution time approaches.

18. Failure-Mode Block

main_failure_modeEntering a near-resolution trade on a market where the oracle challenge window is active or the leading outcome reverses, turning a $0.95–$0.99 buy into a $0.00 settlement loss.
false_positive_riskGamma API endDate is stale or incorrect; the bot enters a trade believing resolution is imminent when the market has actually been extended or postponed.
false_negative_riskOracleRiskMonitor check has high latency; a valid late-resolution opportunity is skipped because the oracle status read times out, erroneously reporting a challenge as active.
safe_fallbackIf Gamma API endDate is stale (> 60s), or OracleRiskMonitor status cannot be confirmed, skip and emit DecisionReport intent_emitted=false. Never enter without a confirmed endDate and a clean oracle status.
required_dependenciesGamma API market.endDate (fresh < 60s), clob_public best ask and depth, onchain OracleRiskMonitor / UMA status, OracleRiskMonitor internal signal, KillSwitch active flag, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_GAMMA_ENDPOINTBlock TCP to gamma-api.polymarket.com for 65s (cache TTL = 60s)Automatic on Gamma API reconnect.
ORACLE_CHALLENGE_ACTIVESet mock OracleRiskMonitor.challenge_active=true for a target conditionIdAutomatic when OracleRiskMonitor clears the challenge.
AVERAGE_DOWN_BLOCKSet position.entry_price=0.985; current best_ask=0.972Automatic when price recovers above entry_price.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic on manual KillSwitch reset.
END_DATE_FARSet mock market.endDate to now + 500 minutesAutomatic when endDate enters window.

20. State & Persistence

Cold-start recovery

On cold start, entry prices are re-read from clob_auth positions. Oracle status is re-fetched on first evaluation per market. Gamma endDate is re-polled from Gamma API.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight20
Idempotency keyintent_id
Per-call timeout (ms)250
Backpressure strategydrop duplicate poll results for same conditionId if queue depth > 3
Locking / mutual exclusionper-conditionId mutex for entry price and oracle state read/write

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchChecked first; blocks all intent emission when active.
risk.oracle_risk_monitorOracleRiskMonitor must confirm oracle is clear before any OrderIntent is emitted. Active challenge or DVM escalation blocks entry.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
Gamma API (market.endDate + negRisk flag)99.9% (Polymarket-published)
Polymarket CLOB (public, book + depth)99.9%
UMA Optimistic Oracle (onchain, Polygon)Polygon RPC SLA

23. Security Surfaces

On-chain contract calls

ContractMethodNetworkEffect
CTFExchangeV2polygon
NegRiskAdapterpolygon

Abuse vectors considered

  • Gamma API endDate manipulation: a falsified endDate could push a market artificially into the near-resolution window
  • Oracle status spoofing: a false 'oracle clear' signal could cause entry during an active dispute
  • Average-down bypass: a crafted position read could make entry_price appear lower than current price, bypassing the never_average_down check

Mitigations

  • Gamma endDate is cross-validated against clob_public market.closeTime before use
  • Oracle status read is from OracleRiskMonitor (authenticated internal service), not from public feed
  • Position entry_price is read from clob_auth (authenticated); not from public feed
  • V2 order timestamp(ms) prevents replay of signed orders
  • Fail-closed on oracle state: any read error treats oracle as challenged

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesHeavily time-dependent on gamma.market.endDate; Gamma API freshness is a hard dependency. For neg-risk multi-outcome events, checks NegRiskAdapter availability for post-fill conversion. UMA oracle challenge window ($750 pUSD bond, 2h challenge, 24–48h DVM if escalated) is respected via OracleRiskMonitor. feeRateBps not on any signed order.

API surfaces declared

gammaclob_publicclob_authonchaininternal

Networks supported

polygon

25. Versioning & Migration

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

Migration history

DateFromToReasonAction taken
2026-04-28v1 (USDC.e, feeRateBps on signed order)v2 (pUSD, fees operator-set at match time, builderCode bytes32)CLOB V2 cutoverSwitched to py-clob-client-v2. Removed feeRateBps from all signed order construction — fees are operator-set at match time by CTFExchangeV2. Updated collateral from USDC.e to pUSD. Injected builder field (bytes32) on every OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. Gamma API endDate field confirmed as primary time-to-resolution source. OracleRiskMonitor integration updated to V2 UMA bond/challenge parameters ($750 pUSD, 2h challenge window).

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit OrderIntent when spread=2.4 cents, minutes_to_resolution=87, oracle clearbest_ask=0.976, endDate=now+87min, oracle_challenge=falseOne OrderIntent emitted; intent_emitted=true; spread_cents=2.4
Skip when spread = 0.8 cents (below 1 cent hard floor)best_ask=0.992No OrderIntent; reason=LATE_RES_SPREAD_TOO_TIGHT
Skip when minutes_to_resolution = 400 (above 360 min hard ceiling)endDate=now+400minNo OrderIntent; reason=LATE_RES_NOT_IN_WINDOW
Skip when oracle challenge is activeoracle_challenge_active=trueNo OrderIntent; reason=LATE_RES_ORACLE_CHALLENGE_ACTIVE
Skip when existing position is below entry price (never_average_down)position.entry_price=0.980, current_price=0.972No OrderIntent; reason=LATE_RES_NO_AVERAGE_DOWN
Reduce clip size by 20% when minutes_to_resolution < 30minutes_to_resolution=22, max_clip_usd=300OrderIntent emitted with size=240; WARN LATE_RES_APPROACHING

Integration Tests

TestExpected result
Full cycle: Gamma poll → endDate check → oracle clear → signed V2 OrderIntent submittedOrderIntent contains builder.code (bytes32), no feeRateBps, EIP-712 domain version '2', negrisk_aware=true for neg-risk markets
Stale Gamma endDate triggers skip without order emissionDecisionReport intent_emitted=false, reason=STALE_MARKET_DATA when Gamma fetch is > 60s old

Property Tests

PropertyRequired behaviour
Bot never emits a buy OrderIntent when existing position's current_price < entry_priceAlways true — never_average_down locked
feeRateBps field is never present on any emitted OrderIntentAlways true — V2 fees are operator-set at match time
OracleRiskMonitor must be clear before any OrderIntent is emittedAlways true — fail-closed on oracle state

27. Operational Runbook

LateResSpread incidents involve Gamma API staleness (most common), oracle challenge-related skips, or occasional stale-endDate entries. Gamma staleness is resolved via infra; oracle challenges require monitoring until the UMA 2h window clears.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
LateResSpreadGammaStale
LateResSpreadOracleSkipsHigh
LateResSpreadHighLatency
LateResSpreadKillSwitchBlocking

Manual overrides

Healthcheck

GET /internal/health/late-resolution-spread -> 200 if Gamma API last_seen < 60s, OracleRiskMonitor reachable, KillSwitch inactive, and at least one market evaluated in last 5 minutes.

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
All unit tests pass including never_average_down invariant and oracle-clear gateCI test run100% pass
feeRateBps absence verified in integration test signed-order payloadIntegration test asserting V2 order schemaPass

Promote to Limited live

GateHow measuredThreshold
p99 eval latency < 250ms over 24hpolytraders_strat_lateresspread_eval_latency_ms histogramp99 < 250ms
Zero average-down incidents in 48h shadow runnever_average_down invariant monitoring0 incidents

Promote to General live

GateHow measuredThreshold
E2E: Gamma endDate → oracle clear → signed V2 OrderIntent → fill on Polygon testnetE2E testPass
Oracle challenge simulation: entry blocked during 2h challenge windowFailure injection test with mock OracleRiskMonitorPass

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