3.11 Late-Resolution Spread
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
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Strategy Strategy |
|---|---|
| Bot class | Alpha Strategy |
| Authority | Trade |
| Status | LIVE |
| Readiness | General live |
| Runs before | Risk guardrail pipeline |
| Runs after | Market scanner / opportunity feed |
| Applies to | Markets 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 mode | general_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders 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
| Input | Source | Required? | Use |
|---|---|---|---|
| Market endDate and resolution metadata | gamma (Gamma API — gamma.market.endDate, resolutionSource, negRisk flag) | Yes | Primary 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 outcome | clob_public | Yes | Confirm price ≥ min_price_cents/100 and compute spread = 1.00 - best_ask. |
| Oracle proposal status and challenge window state | onchain (UMA Optimistic Oracle / Polygon) | Yes | Skip markets with an active oracle challenge (2h window + potential DVM delay 24–48h) to avoid unexpected settlement delays. |
| Market negRisk flag and multi-outcome structure | gamma | Yes | For neg-risk events, verify each outcome's endDate independently and check NegRiskAdapter availability for position exit. |
| Top-of-book depth for the leading outcome | clob_public | Yes | Size order to min(depth_available, max_clip_usd) to avoid moving the book. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| KillSwitch active flag | KillSwitch | Yes | Abort intent emission if KillSwitch is active. |
| Builder code bytes32 | internal config | Yes | Injected into builder field on every signed V2 order for attribution. |
| OracleRiskMonitor approval status for this market | OracleRiskMonitor | Yes | Only enter when OracleRiskMonitor confirms no active challenge or DVM escalation. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| min_spread_to_1_cents | 2 | 3 | 1 | Minimum 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_resolution | 120 | 30 | 360 | Maximum 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_usd | 300 | 500 | 750 | Maximum pUSD size per order clip. Late-resolution markets are often illiquid; small clips prevent moving the book. |
| never_average_down | True | None | None | 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. |
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
| Condition | Action |
|---|---|
| spread ≥ 2 cents | EMIT OrderIntent |
| 1–2 cents | WARN 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
| Condition | Action |
|---|---|
| minutes_to_resolution ≤ 120 | Eligible to enter |
| 30–60 minutes | WARN LATE_RES_APPROACHING; consider reducing clip size due to lower liquidity |
| > 360 minutes | SKIP — 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
| Condition | Action |
|---|---|
| ≤ 300 pUSD | Normal clip |
| 300–750 pUSD | WARN; Risk guardrail will reshape if above portfolio budget |
| > 750 pUSD | Reject 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
| Condition | Action |
|---|---|
| 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
- Check KillSwitch active flag; if active, emit no OrderIntents.
- Poll Gamma API for markets with endDate within (now, now + max_minutes_to_resolution * 60).
- For each candidate market: compute minutes_to_resolution = (endDate - now_ms()) / 60000.
- If minutes_to_resolution > max_minutes_to_resolution hard (360 min), skip market.
- Fetch best_ask for the leading YES outcome from clob_public.
- Confirm best_ask ≥ 0.90 (minimum viable price — not a penny token). Skip if below.
- 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.
- Check OracleRiskMonitor status for this market. If oracle challenge active or DVM escalation in progress, skip LATE_RES_ORACLE_CHALLENGE_ACTIVE.
- Confirm negRisk flag from Gamma. For neg-risk markets, also check NegRiskAdapter availability for clean exit.
- Check existing position: if position exists and current_price < position.entry_price, skip LATE_RES_NO_AVERAGE_DOWN (never_average_down locked true).
- Compute clipSize = toPusdUnits(min(depth_available, max_clip_usd)).
- If minutes_to_resolution < 30 (warning), reduce clipSize by 20% for thin-book safety.
- Emit OrderIntent: market_id, outcome=YES, side=buy, price=best_ask, size_pUSD=clipSize, tif=GTC, post_only=false, builder={code, fee_bps: 25}.
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order.
- 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 market — gamma
{
"conditionId": "0xef012345678901abcdef01234567890abcdef01234567890abcdef01234567890e",
"endDate": "2026-05-09T13:00:00Z",
"negRisk": true,
"active": true,
"resolutionSource": "UMA Optimistic Oracle"
}
CLOB public book — leading outcome — clob_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
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
LATE_RES_SPREAD_ENTRY | INFO | Market 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_TIGHT | INFO | spread_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_WINDOW | INFO | minutes_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_ACTIVE | WARN | UMA 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_DOWN | INFO | Existing 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_APPROACHING | WARN | minutes_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_DATA | HARD_REJECT | Gamma 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_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all markets; no OrderIntents. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_lateresspread_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles by intent_emitted (true/false) and reason code. |
polytraders_strat_lateresspread_spread_cents | histogram | cents | Distribution of spread (in cents below $1.00) at evaluation time, for entered and skipped opportunities. | |
polytraders_strat_lateresspread_minutes_to_resolution | histogram | minutes | Distribution of minutes-to-resolution at entry time. | |
polytraders_strat_lateresspread_intents_emitted_total | counter | count | negrisk_aware | Total OrderIntents emitted, split by neg-risk/standard market type. |
polytraders_strat_lateresspread_oracle_skips_total | counter | count | market_id | Number of entry opportunities skipped due to oracle challenge or DVM escalation. |
polytraders_strat_lateresspread_eval_latency_ms | histogram | milliseconds | Wall-clock latency from Gamma poll to OrderIntent emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
LateResSpreadGammaStale | rate(polytraders_strat_lateresspread_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1 | warn | #runbook-lateresponse-gamma-stale |
LateResSpreadOracleSkipsHigh | rate(polytraders_strat_lateresspread_oracle_skips_total[10m]) > 3 | warn | #runbook-lateresponse-oracle-skips |
LateResSpreadHighLatency | histogram_quantile(0.99, rate(polytraders_strat_lateresspread_eval_latency_ms_bucket[5m])) > 250 | warn | #runbook-lateresponse-latency |
LateResSpreadKillSwitchBlocking | rate(polytraders_strat_lateresspread_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #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
| Situation | User-facing explanation |
|---|---|
| Late-resolution trade entered | This 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 small | The market price is already very close to $1. After fees, there is not enough potential profit to justify the trade. |
| No entry — oracle challenge active | The market's resolution is being disputed. The trade was skipped to avoid exposure during the uncertainty window. |
| No entry — market not near resolution | This 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_mode | Entering 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_risk | Gamma 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_risk | OracleRiskMonitor 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_fallback | If 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_dependencies | Gamma 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
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
STALE_GAMMA_ENDPOINT | Block TCP to gamma-api.polymarket.com for 65s (cache TTL = 60s) | Automatic on Gamma API reconnect. | |
ORACLE_CHALLENGE_ACTIVE | Set mock OracleRiskMonitor.challenge_active=true for a target conditionId | Automatic when OracleRiskMonitor clears the challenge. | |
AVERAGE_DOWN_BLOCK | Set position.entry_price=0.985; current best_ask=0.972 | Automatic when price recovers above entry_price. | |
KILL_SWITCH_ON | Set killswitch.active=true | Automatic on manual KillSwitch reset. | |
END_DATE_FAR | Set mock market.endDate to now + 500 minutes | Automatic 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
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 20 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | drop duplicate poll results for same conditionId if queue depth > 3 |
| Locking / mutual exclusion | per-conditionId mutex for entry price and oracle state read/write |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Checked first; blocks all intent emission when active. | |
| risk.oracle_risk_monitor | OracleRiskMonitor must confirm oracle is clear before any OrderIntent is emitted. Active challenge or DVM escalation blocks entry. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | ||
| gov.builder_attribution |
External services
| Service | Endpoint | SLA assumed | On 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
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon | |
NegRiskAdapter | | polygon |
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
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Heavily 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
Networks supported
25. Versioning & Migration
| Field | Value |
|---|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|---|---|---|---|
| 2026-04-28 | v1 (USDC.e, feeRateBps on signed order) | v2 (pUSD, fees operator-set at match time, builderCode bytes32) | CLOB V2 cutover | Switched 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
| Test | Setup | Expected result |
|---|---|---|
| Emit OrderIntent when spread=2.4 cents, minutes_to_resolution=87, oracle clear | best_ask=0.976, endDate=now+87min, oracle_challenge=false | One OrderIntent emitted; intent_emitted=true; spread_cents=2.4 |
| Skip when spread = 0.8 cents (below 1 cent hard floor) | best_ask=0.992 | No OrderIntent; reason=LATE_RES_SPREAD_TOO_TIGHT |
| Skip when minutes_to_resolution = 400 (above 360 min hard ceiling) | endDate=now+400min | No OrderIntent; reason=LATE_RES_NOT_IN_WINDOW |
| Skip when oracle challenge is active | oracle_challenge_active=true | No 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.972 | No OrderIntent; reason=LATE_RES_NO_AVERAGE_DOWN |
| Reduce clip size by 20% when minutes_to_resolution < 30 | minutes_to_resolution=22, max_clip_usd=300 | OrderIntent emitted with size=240; WARN LATE_RES_APPROACHING |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: Gamma poll → endDate check → oracle clear → signed V2 OrderIntent submitted | OrderIntent 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 emission | DecisionReport intent_emitted=false, reason=STALE_MARKET_DATA when Gamma fetch is > 60s old |
Property Tests
| Property | Required behaviour |
|---|---|
| Bot never emits a buy OrderIntent when existing position's current_price < entry_price | Always true — never_average_down locked |
| feeRateBps field is never present on any emitted OrderIntent | Always true — V2 fees are operator-set at match time |
| OracleRiskMonitor must be clear before any OrderIntent is emitted | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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
| Gate | How measured | Threshold |
|---|---|---|
| All unit tests pass including never_average_down invariant and oracle-clear gate | CI test run | 100% pass |
| feeRateBps absence verified in integration test signed-order payload | Integration test asserting V2 order schema | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| p99 eval latency < 250ms over 24h | polytraders_strat_lateresspread_eval_latency_ms histogram | p99 < 250ms |
| Zero average-down incidents in 48h shadow run | never_average_down invariant monitoring | 0 incidents |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: Gamma endDate → oracle clear → signed V2 OrderIntent → fill on Polygon testnet | E2E test | Pass |
| Oracle challenge simulation: entry blocked during 2h challenge window | Failure injection test with mock OracleRiskMonitor | Pass |
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 |