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.4 Neg-Risk Sum Arb

3.4 Neg-Risk Sum Arb

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

Neg-Risk Sum Arb exploits pricing dislocations on Polymarket’s negative-risk (multi-outcome) markets, where the sum of YES token prices across N outcomes must equal $1.00 at resolution. When sum(YES asks across outcomes) < $1.00 net of fees, the bot buys the underpriced YES tokens and — where profitable — routes the position through the NegRiskAdapter on Polygon to convert NO tokens across the set into pUSD. The 'Other' outcome is always excluded from the conversion path. This is a user-controlled execution tool that targets a structural pricing constraint specific to negative-risk event design on Polymarket. It is the multi-outcome counterpart to sum-to-one-arb.

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 toNegative-risk (multi-outcome) markets where the sum of YES token best asks across all outcomes (excluding 'Other') falls below 1.00 pUSD
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Strategy pod

2. Purpose

Neg-Risk Sum Arb exploits pricing dislocations on Polymarket’s negative-risk (multi-outcome) markets, where the sum of YES token prices across N outcomes must equal $1.00 at resolution. When sum(YES asks across outcomes) < $1.00 net of fees, the bot buys the underpriced YES tokens and — where profitable — routes the position through the NegRiskAdapter on Polygon to convert NO tokens across the set into pUSD. The 'Other' outcome is always excluded from the conversion path. This is a user-controlled execution tool that targets a structural pricing constraint specific to negative-risk event design on Polymarket. It is the multi-outcome counterpart to sum-to-one-arb.

3. Why This Bot Matters

  • Sum computed without excluding 'Other' outcome

    The 'Other' token is illiquid and reprices discontinuously. Including it in the sum produces false edge signals that cannot be closed via the NegRiskAdapter path.

  • NegRiskAdapter path not available (market not on negRisk contract)

    Buying all YES tokens across N outcomes without a conversion path leaves open positions on all legs; settlement risk is uncapped until individual markets resolve.

  • feeRateBps hardcoded on signed order (V1 pattern)

    CLOB V2 rejects orders with feeRateBps. Fees are operator-set at match time. All signed orders must omit this field.

  • Partial fill on one outcome leg leaves unbalanced multi-leg exposure

    In a 4-outcome event, buying 3 of 4 YES tokens does not produce a guaranteed $1 settlement and creates directional risk on the unfilled outcome.

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
Outcome token list and YES token IDs for the neg-risk eventgamma (Gamma API — negRisk/enableNegRisk flag + condition ID)YesEnumerate all N outcome tokens; identify and exclude the 'Other' outcome token.
Best ask for each YES outcome tokenws_market (CLOB WebSocket book stream)YesCompute sum(YES asks) across N-1 outcomes (excluding Other) and measure edge against 1.00 pUSD.
Top-of-book depth per outcomeclob_publicYesSize each leg to the minimum available depth across all outcomes up to max_leg_size_usd.
NegRiskAdapter contract availability and condition IDonchain (NegRiskAdapter on Polygon)YesVerify the NegRiskAdapter conversion path is live for this event before committing to the arb.
Platform fee rate per market categoryonchain (CTFExchangeV2 fee config)YesEstimate per-leg fee drag; crypto ≤1.80%, sports 0.75%, geopolitical free.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesAbort all intent emission if KillSwitch is active.
Builder code bytes32internal configYesInjected into builder field on every signed V2 order for attribution.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_edge_bps20128Minimum net edge in basis points required across the full outcome set before emitting any OrderIntents. Higher default than sum-to-one-arb to account for N-leg fee drag.
prefer_conversion_pathTrueNoneNoneWhen true, bot prefers the NegRiskAdapter burn-NO-tokens→pUSD conversion path over holding all YES tokens to resolution. This crystallises the arb profit without waiting for the event to resolve.
max_outcomes_per_trade8612Maximum number of outcome legs to include in a single arb trade. Events with more outcomes than this ceiling are skipped.
exclude_other_outcomeTrueNoneNoneAlways exclude the 'Other' outcome token from the sum computation and from order emission. Locked to true — cannot be disabled.

7. Detailed Parameter Instructions

min_edge_bps

What it means

Minimum net edge in basis points required across the full outcome set before emitting any OrderIntents. Higher default than sum-to-one-arb to account for N-leg fee drag.

Default

{ "min_edge_bps": 20 }

Why this default matters

With N outcomes each incurring taker fees, total fee drag grows proportionally. 20 bps default provides comfortable margin above multi-leg fees on crypto markets.

Threshold logic

ConditionAction
net_edge_bps ≥ 20EMIT N OrderIntents across all non-Other outcomes
12 ≤ net_edge_bps < 20WARN NEG_RISK_SUM_ARB_EDGE_MARGINAL — emit with reduced size (50%)
net_edge_bps < 8 (hard floor)SKIP — NEG_RISK_SUM_ARB_NO_EDGE

Developer check

if net_edge_bps < params.hard: return skip('NEG_RISK_SUM_ARB_NO_EDGE')

User-facing English

The combined price of all outcome tokens in this multi-outcome market was not low enough to trade profitably after accounting for multi-leg fees.

prefer_conversion_path

What it means

When true, bot prefers the NegRiskAdapter burn-NO-tokens→pUSD conversion path over holding all YES tokens to resolution. This crystallises the arb profit without waiting for the event to resolve.

Default

{ "prefer_conversion_path": true }

Why this default matters

The conversion path (burn NO tokens → pUSD via NegRiskAdapter) is lower-risk than holding N YES positions open to resolution. Prefer it when available.

Threshold logic

ConditionAction
prefer_conversion_path=true and NegRiskAdapter availableRoute through NegRiskAdapter after fills
prefer_conversion_path=true and NegRiskAdapter unavailableHold YES positions; warn NEG_RISK_SUM_ARB_NO_CONVERSION_PATH

Developer check

if params.prefer_conversion_path and adapter.available: EMIT NegRiskConvertRoute

User-facing English

Positions will be converted to pUSD immediately after filling where possible.

max_outcomes_per_trade

What it means

Maximum number of outcome legs to include in a single arb trade. Events with more outcomes than this ceiling are skipped.

Default

{ "max_outcomes_per_trade": 8 }

Why this default matters

More legs increase total fee drag and the risk of a partial fill leaving an unbalanced position. 8 outcomes cover the vast majority of Polymarket neg-risk events.

Threshold logic

ConditionAction
N outcomes ≤ 8Normal multi-leg emission
8 < N ≤ 12WARN NEG_RISK_SUM_ARB_HIGH_OUTCOME_COUNT; emit with 50% size per leg
N > 12SKIP — too many legs for safe atomic execution

Developer check

if len(outcomes) > params.hard: return skip('NEG_RISK_SUM_ARB_NO_EDGE')

User-facing English

— not yet authored —

exclude_other_outcome

What it means

Always exclude the 'Other' outcome token from the sum computation and from order emission. Locked to true — cannot be disabled.

Default

{ "exclude_other_outcome": true }

Why this default matters

The 'Other' token covers residual probability and is illiquid and non-standard. Including it in an arb creates unquantifiable tail risk.

Threshold logic

ConditionAction
Always true (locked)Other outcome excluded from all calculations

Developer check

outcomes = [o for o in event.outcomes if o.name.lower() != 'other']

User-facing English

— not yet authored —

8. Default Configuration

{
  "bot_id": "strat.neg_risk_sum_arb",
  "version": "2.1.0",
  "mode": "general_live",
  "defaults": {
    "min_edge_bps": 20,
    "prefer_conversion_path": true,
    "max_outcomes_per_trade": 8,
    "exclude_other_outcome": true
  },
  "locked": {
    "exclude_other_outcome": true,
    "max_outcomes_per_trade": {
      "max": 12
    },
    "min_edge_bps": {
      "min": 8
    }
  }
}

9. Implementation Flow

  1. Check KillSwitch active flag; if active, skip and emit no OrderIntents.
  2. FETCH Gamma API for all active neg-risk events (enableNegRisk=true); extract outcome token lists.
  3. Exclude 'Other' outcome token from each event's outcome list.
  4. If len(outcomes) > max_outcomes_per_trade, skip this event.
  5. Verify NegRiskAdapter availability for event condition ID via onchain call.
  6. Subscribe to ws_market book updates for all YES token IDs of each eligible event.
  7. On each book tick: compute sum_YES_asks = sum(best_ask[outcome] for outcome in outcomes).
  8. Compute raw_edge_bps = (1.00 - sum_YES_asks) * 10000.
  9. Deduct total fee drag across N legs; compute net_edge_bps = raw_edge_bps - N * per_leg_fee_bps - fee_buffer_bps.
  10. If net_edge_bps < min_edge_bps hard floor (8), emit DecisionReport intent_emitted=false NEG_RISK_SUM_ARB_NO_EDGE; skip.
  11. If 8 ≤ net_edge_bps < 20, emit WARN NEG_RISK_SUM_ARB_EDGE_MARGINAL; reduce leg size 50%.
  12. Compute legSize = toPusdUnits(min(min_depth_across_outcomes, max_leg_size_usd) * sizeMultiplier).
  13. Emit one OrderIntent per outcome leg: side=buy, outcome=YES, price=best_ask, tif=FOK, builder={code, fee_bps: 25}. No feeRateBps on any signed order — fees are operator-set at match time in V2.
  14. If prefer_conversion_path and NegRiskAdapter available: EMIT NegRiskConvertRoute after fill confirmations (burn NO tokens → pUSD via NegRiskAdapter).
  15. Emit DecisionReport with intent_emitted=true, edge_bps, outcome_count, conversion_path_used.

10. Reference Implementation

Scans Gamma API for neg-risk events, subscribes to book streams for all YES token IDs, computes net edge across N outcomes, and emits N FOK OrderIntents when edge exceeds threshold. Optionally routes through NegRiskAdapter for immediate pUSD conversion.

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

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

  // --- 1. Fetch active neg-risk events from Gamma ---
  events = FETCH gamma.GET('/markets?negRisk=true&active=true')
  FOR event IN events:
    outcomes = [o for o in event.outcomes if o.name.lower() != 'other']  // exclude Other (locked)
    IF len(outcomes) > params.max_outcomes_per_trade:
      SKIP event  // too many legs

    // --- 2. Verify NegRiskAdapter available ---
    adapterAvailable = FETCH onchain.NegRiskAdapter.isConditionRegistered(event.conditionId)

    // --- 3. Subscribe to book streams for all YES token IDs ---
    tokenIds = [o.yes_token_id for o in outcomes]
    ws_market.subscribe('book', tokenIds)

FUNCTION onBookTick(event, outcomes, tick):
  // --- 4. Check freshness ---
  IF isStale(tick, maxAgeS=5):
    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
    RETURN

  // --- 5. Compute edge ---
  asks = [tick[o.yes_token_id].best_ask for o in outcomes]
  sum_YES_asks = SUM(asks)
  raw_edge_bps = (1.00 - sum_YES_asks) * 10000

  // --- 6. Fee drag across N legs ---
  feeRate   = FETCH onchain.feeConfig(event.conditionId)
  N         = len(outcomes)
  total_fee = SUM(feeRate * asks[i] * (1 - asks[i]) * 10000 for i in range(N))
  net_edge_bps = raw_edge_bps - total_fee - params.fee_buffer_bps

  // --- 7. Hard floor ---
  IF net_edge_bps < params.min_edge_bps_hard:  // 8 bps
    EMIT DecisionReport(intent_emitted=false, reason='NEG_RISK_SUM_ARB_NO_EDGE')
    RETURN

  // --- 8. Warning threshold ---
  legSizeMultiplier = 1.0
  IF net_edge_bps < params.min_edge_bps:  // 20 bps default
    WARN('NEG_RISK_SUM_ARB_EDGE_MARGINAL')
    legSizeMultiplier = 0.5

  // --- 9. Leg sizing ---
  minDepth = MIN(tick[o.yes_token_id].depth_pusd for o in outcomes)
  legSize  = toPusdUnits(min(minDepth, params.max_leg_size_usd) * legSizeMultiplier)

  // --- 10. Emit N OrderIntents (V2: no feeRateBps; builder field carries code) ---
  FOR i, outcome IN enumerate(outcomes):
    EMIT OrderIntent(
      market_id     = outcome.market_id,
      outcome       = 'YES',
      side          = 'buy',
      price         = asks[i],
      size_pUSD     = legSize,
      tif           = 'FOK',
      post_only     = false,
      builder       = { code: config.builder_code, fee_bps: 25 },
      negrisk_aware = true
    )

  // --- 11. NegRiskAdapter conversion path ---
  IF params.prefer_conversion_path AND adapterAvailable:
    EMIT NegRiskConvertRoute(
      conditionId = event.conditionId,
      action      = 'burn_NO_to_pUSD',  // NegRiskAdapter: burn NO across set -> pUSD
      trigger     = 'on_all_fills'
    )

  EMIT DecisionReport(
    intent_emitted  = true,
    edge_bps        = net_edge_bps,
    outcome_count   = N,
    conversion_path = 'NegRiskAdapter' IF adapterAvailable ELSE 'hold',
    reasons         = ['NEG_RISK_SUM_ARB_EDGE_PRESENT']
  )

SDK calls used

  • gamma.GET('/markets?negRisk=true&active=true')
  • onchain.NegRiskAdapter.isConditionRegistered(conditionId)
  • ws_market.subscribe('book', tokenIds)
  • onchain.feeConfig(conditionId)
  • toPusdUnits(rawFloat)
  • buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })
  • internal.killswitch.status()
  • internal.builder_code

Complexity: O(N) per book tick where N = number of outcomes per event

11. Wire Examples

Input — what arrives on the wire

Gamma API neg-risk event with 4 outcomesgamma

{
  "conditionId": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
  "negRisk": true,
  "outcomes": [
    {
      "name": "Outcome A",
      "yes_token_id": "0xtoken_a"
    },
    {
      "name": "Outcome B",
      "yes_token_id": "0xtoken_b"
    },
    {
      "name": "Outcome C",
      "yes_token_id": "0xtoken_c"
    },
    {
      "name": "Outcome D",
      "yes_token_id": "0xtoken_d"
    },
    {
      "name": "Other",
      "yes_token_id": "0xtoken_other"
    }
  ]
}

ws_market book tick — sum(YES) = 0.978ws_market

{
  "token_a_ask": "0.242",
  "token_b_ask": "0.245",
  "token_c_ask": "0.248",
  "token_d_ask": "0.243",
  "sum_YES": "0.978",
  "received_at_ms": 1746789700000
}

Output — what the bot emits

OrderIntent — Outcome A leg (one of 4; all similar)

{
  "intent_id": "oi_01HX9NGRSUM4A1B",
  "trace_id": "tr_01HX9NGRSUM4VR5",
  "market_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
  "outcome": "YES",
  "side": "buy",
  "price": "0.242",
  "size_pUSD": "400.00",
  "tif": "FOK",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": true,
  "decision": {
    "edge_bps": 22.1,
    "outcome_count": 4,
    "conversion_path": "NegRiskAdapter",
    "reasons": [
      "NEG_RISK_SUM_ARB_EDGE_PRESENT"
    ]
  },
  "comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}

NegRiskConvertRoute — burn NO tokens → pUSD after fills

{
  "route_id": "nrc_01HX9NGRSUM4X9Z",
  "conditionId": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
  "action": "burn_NO_to_pUSD",
  "trigger": "on_all_fills",
  "adapter": "NegRiskAdapter",
  "network": "polygon"
}

12. Decision Logic

APPROVE

net_edge_bps ≥ min_edge_bps, NegRiskAdapter confirmed available, all outcome legs have depth, outcome count ≤ max_outcomes_per_trade, KillSwitch inactive. Emit N OrderIntents.

RESHAPE_REQUIRED

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

REJECT

net_edge_bps < 8 bps hard floor; NegRiskAdapter unavailable and prefer_conversion_path=true; outcome count > 12; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.

WARNING_ONLY

net_edge_bps between 8 and 20 triggers NEG_RISK_SUM_ARB_EDGE_MARGINAL and 50% size reduction before emitting.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "intent_id": "oi_01HX9NGRSUM4A1B",
  "trace_id": "tr_01HX9NGRSUM4VR5",
  "market_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
  "outcome": "YES",
  "side": "buy",
  "price": "0.242",
  "size_pUSD": "400.00",
  "tif": "FOK",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": true,
  "decision": {
    "edge_bps": 22.1,
    "outcome_count": 4,
    "conversion_path": "NegRiskAdapter",
    "reasons": [
      "NEG_RISK_SUM_ARB_EDGE_PRESENT"
    ]
  },
  "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
NEG_RISK_SUM_ARB_EDGE_PRESENTINFONet edge across all N non-Other outcome YES legs meets or exceeds min_edge_bps. N OrderIntents emitted.Emit N OrderIntents and optional NegRiskConvertRoute.A pricing gap was detected across all outcomes of this market and orders were placed to capture it.
NEG_RISK_SUM_ARB_NO_EDGEINFONet edge across outcome legs is below the 8 bps hard floor after multi-leg fees. No trade.Skip; emit DecisionReport intent_emitted=false.The combined outcome pricing was not low enough to trade profitably after multi-leg fees.
NEG_RISK_SUM_ARB_EDGE_MARGINALWARNEdge is between 8 and 20 bps. Trade is entered at 50% leg size.Emit N OrderIntents at 50% leg size; log warning.A small pricing gap was detected across outcomes. Reduced-size orders were placed.
NEG_RISK_SUM_ARB_NO_CONVERSION_PATHWARNNegRiskAdapter is unavailable or not registered for this condition ID. Positions will be held to resolution.Emit OrderIntents if edge is present; log warning; do not emit NegRiskConvertRoute.Immediate conversion is not available for this market. Positions are held until resolution.
NEG_RISK_SUM_ARB_HIGH_OUTCOME_COUNTWARNOutcome count is between max_outcomes_per_trade warning (8) and hard limit (12).Emit at 50% leg size with warning.
STALE_MARKET_DATAHARD_REJECTBook snapshot older than 5 seconds or Gamma API data is stale.Skip; emit DecisionReport intent_emitted=false.Market data was too old to act on safely.
MARKET_CLOSEDHARD_REJECTEvent is closed or resolved.Skip immediately; no OrderIntents.This market is no longer open for trading.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all events; no OrderIntents.Trading is currently paused.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_negrisksumarb_decisions_totalcountercountverdict, reason_codeTotal evaluation cycles by intent_emitted (true/false) and reason code.
polytraders_strat_negrisksumarb_edge_bpshistogrambasis_pointsDistribution of net edge in bps across all evaluated neg-risk events.
polytraders_strat_negrisksumarb_outcome_counthistogramcountDistribution of outcome counts per traded event (excluding Other).
polytraders_strat_negrisksumarb_intents_emitted_totalcountercountconversion_pathTotal OrderIntent sets emitted, labelled by conversion path used (NegRiskAdapter or hold).
polytraders_strat_negrisksumarb_eval_latency_mshistogrammillisecondsWall-clock latency from book tick to last OrderIntent emit per event.
polytraders_strat_negrisksumarb_conversion_route_totalcountercountresultNegRiskConvertRoute emissions by result (success, adapter_unavailable, skipped).

Alerts

AlertConditionSeverityRunbook
NegRiskSumArbStaleFeedrate(polytraders_strat_negrisksumarb_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1warn#runbook-negrisksumarb-stale-feed
NegRiskSumArbAdapterUnavailablerate(polytraders_strat_negrisksumarb_decisions_total{reason_code='NEG_RISK_SUM_ARB_NO_CONVERSION_PATH'}[5m]) > 0.5warn#runbook-negrisksumarb-adapter
NegRiskSumArbHighLatencyhistogram_quantile(0.99, rate(polytraders_strat_negrisksumarb_eval_latency_ms_bucket[5m])) > 200warn#runbook-negrisksumarb-latency
NegRiskSumArbKillSwitchBlockingrate(polytraders_strat_negrisksumarb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch

Dashboards

  • Grafana — Strategy / NegRiskSumArb edge and outcome count distribution
  • Grafana — Strategy / NegRiskSumArb NegRiskAdapter conversion throughput

16. Developer Reporting

{
  "bot_id": "strat.neg_risk_sum_arb",
  "event_condition_id": "0xbcd1ef2345678901bcdef01234567890bcdef01234567890bcdef01234567890bc",
  "outcome_count": 4,
  "sum_YES_asks": 0.978,
  "raw_edge_bps": 22.0,
  "total_fee_drag_bps": 14.2,
  "net_edge_bps": 22.1,
  "leg_size_pusd": 400.0,
  "conversion_path": "NegRiskAdapter",
  "intent_emitted": true,
  "reason": "NEG_RISK_SUM_ARB_EDGE_PRESENT",
  "emitted_at_ms": 1746789700000
}

17. Plain-English Reporting

SituationUser-facing explanation
Multi-outcome arb trade initiatedThe combined price of all outcome tokens in this multi-outcome market was below $1. Orders were placed across all outcomes to capture this pricing gap. Positions may be immediately converted to settled value via the NegRisk contract.
No edge in multi-outcome marketThe combined outcome prices did not create a profitable opportunity after accounting for fees on each leg. No orders were placed.
Conversion path unavailableThe NegRisk contract path was not available for this event. Positions are being held to settlement.
Too many outcomes to trade safelyThis event has more outcome tokens than the configured maximum for safe multi-leg execution. The trade was skipped.

18. Failure-Mode Block

main_failure_modePartial fill: N-1 of N outcome legs fill FOK but one leg's price moves, leaving an unbalanced multi-outcome position that does not sum cleanly to $1.
false_positive_riskStale book data shows sum(YES asks) < 1.00 when the live market has already rebalanced, causing the bot to attempt an arb on prices that no longer exist.
false_negative_riskNegRiskAdapter availability check has latency; a valid arb is skipped because the adapter check returns stale-unavailable during a brief RPC delay.
safe_fallbackIf book data is stale (> 5s) or the NegRiskAdapter cannot be confirmed available, skip and emit DecisionReport intent_emitted=false. Never enter a partial multi-leg position deliberately.
required_dependenciesGamma API neg-risk event list (enableNegRisk=true), ws_market book stream (all YES token IDs per event), clob_public depth per outcome, onchain NegRiskAdapter contract (Polygon), KillSwitch active flag, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_GAMMA_FEEDBlock TCP to gamma-api.polymarket.com for 70s (cache TTL = 60s)Automatic on Gamma API reconnect.
NEGRISK_ADAPTER_UNAVAILABLEMock onchain.NegRiskAdapter.isConditionRegistered to return falseAutomatic when adapter check returns true again.
PARTIAL_LEG_FILLMock CLOB to reject one of N FOK legs after N-1 legs fillRisk pod manually evaluates and closes partial position.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic on manual KillSwitch reset.
HIGH_OUTCOME_COUNTCreate mock event with 14 outcomesAutomatic; event is re-evaluated each cycle.

20. State & Persistence

Cold-start recovery

On cold start, state is empty; first book tick per event triggers fresh evaluation. NegRiskAdapter availability is re-checked from onchain on first evaluation.

21. Concurrency & Idempotency

AspectSpecification
Execution modelactor-per-market
Max in-flight30
Idempotency keyintent_id
Per-call timeout (ms)200
Backpressure strategydrop oldest pending tick per conditionId when queue depth > 5
Locking / mutual exclusionper-conditionId mutex for Redis state write and NegRiskAdapter check

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchChecked first; blocks all intent emission when active.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
Gamma API (neg-risk event list)99.9% (Polymarket-published)
Polymarket CLOB WebSocket (ws_market)best-effort
NegRiskAdapter (onchain, Polygon)Polygon RPC SLA

23. Security Surfaces

On-chain contract calls

ContractMethodNetworkEffect
CTFExchangeV2polygon
NegRiskAdapterpolygon

Abuse vectors considered

  • Injecting a fake Gamma API response to fabricate neg-risk events with artificially low outcome prices
  • NegRiskAdapter reentrancy on Polygon if burn-NO flow is invoked with a manipulated condition ID
  • Partial-fill attack: adversary moves one outcome book between leg submissions to leave an unbalanced position

Mitigations

  • Gamma API response validated against onchain conditionId before any order emission
  • NegRiskAdapter conditionId verified onchain before emitting NegRiskConvertRoute
  • All legs use FOK; a failed leg prevents NegRiskConvertRoute from being triggered
  • builder.code read from immutable internal config; not user-supplied
  • V2 order timestamp(ms) invalidates replays outside exchange acceptance window

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
NotesCore strategy is NegRiskAdapter convert-arb: buy YES tokens across all N non-Other outcomes when sum(YES asks) < $1, then burn NO tokens via NegRiskAdapter on Polygon to receive pUSD without waiting for resolution. feeRateBps is not present on any signed order — fees are operator-set at match time.

API surfaces declared

clob_publicclob_authws_marketonchaininternal

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, HMAC builder)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 leg. EIP-712 Exchange domain version updated from '1' to '2'. NegRiskAdapter path updated to V2 contract address on Polygon.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit N intents when sum(YES asks) = 0.978 across 4 outcomes (net_edge > 20 bps)outcomes=[0.242,0.245,0.248,0.243], fee_buffer=40, min_edge=204 OrderIntents emitted; intent_emitted=true; negrisk_aware=true on each
Skip when sum(YES asks) = 0.992 (raw edge < 8 bps after fees)sum_YES=0.992, N=4, per_leg_fee_drag=5bpsNo OrderIntents; DecisionReport intent_emitted=false, reason=NEG_RISK_SUM_ARB_NO_EDGE
Always exclude 'Other' outcome from sum and from intent emissionoutcomes=[A, B, C, D, Other], sum without Other = 0.9764 intents (A,B,C,D); Other never included; sum check uses 4 outcomes only
Skip event with > 12 outcomes (max_outcomes_per_trade hard limit)event has 14 outcomesNo OrderIntents; reason=NEG_RISK_SUM_ARB_NO_EDGE
Route NegRiskConvertRoute when prefer_conversion_path=true and adapter availableprefer_conversion_path=true, adapter.available=trueNegRiskConvertRoute emitted after fill confirmations
Skip when KillSwitch activekillswitch.active=trueNo OrderIntents emitted

Integration Tests

TestExpected result
Full cycle: Gamma API → outcome list → book ticks → N signed V2 OrderIntents → NegRiskAdapter conversionAll N orders contain builder.code (bytes32), no feeRateBps, EIP-712 domain version '2'; NegRiskConvertRoute emitted post-fill
NegRiskAdapter unavailable triggers warn and hold-to-resolution pathNEG_RISK_SUM_ARB_NO_CONVERSION_PATH warn emitted; intents still emitted if edge > min; positions held

Property Tests

PropertyRequired behaviour
Bot never emits a subset of outcome legs; always emits full N-outcome set or noneAlways true — partial fills are handled by FOK on individual orders, not by design
'Other' outcome is never included in any OrderIntent or sum computationAlways true — exclude_other_outcome is locked
feeRateBps field is never present on any emitted OrderIntentAlways true — V2 fees are operator-set at match time

27. Operational Runbook

NegRiskSumArb incidents typically involve NegRiskAdapter unavailability, stale Gamma API data, or partial-leg fills. Partial fills require manual risk review; adapter issues are usually transient RPC problems.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
NegRiskSumArbAdapterUnavailable
NegRiskSumArbStaleFeed
NegRiskSumArbHighLatency
NegRiskSumArbKillSwitchBlocking

Manual overrides

Healthcheck

GET /internal/health/neg-risk-sum-arb -> 200 if Gamma API last_seen < 60s, ws_market feed last_seen < 5s per active event, NegRiskAdapter reachable, and KillSwitch inactive.

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 'Other' exclusion invariantCI test run100% pass
NegRiskAdapter conditionId validation test passesIntegration test against Polygon testnetPass

Promote to Limited live

GateHow measuredThreshold
p99 eval latency < 200ms over 24h for events with N ≤ 8 outcomespolytraders_strat_negrisksumarb_eval_latency_ms histogramp99 < 200ms
Zero partial-fill incidents in shadow mode over 48hFill reconciliation report0 incidents

Promote to General live

GateHow measuredThreshold
E2E: Gamma event → N signed V2 OrderIntents → NegRiskConvertRoute on Polygon testnetE2E testPass
feeRateBps absence verified in all N OrderIntent signed payloadsIntegration test asserting V2 order schemaPass

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