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.3 Sum-to-One Arb

3.3 Sum-to-One Arb

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

Sum-to-One Arb detects when the combined cost of buying both YES and NO tokens on a standard binary market falls below $1.00 pUSD, net of platform fees and a configurable fee buffer. When edge (in basis points) exceeds min_edge_bps, the bot emits a pair of OrderIntents — one for YES and one for NO — sized to the smaller of the two books up to max_leg_size_usd. Both legs fill to produce a guaranteed $1.00 settlement regardless of outcome. This is a user-controlled execution tool that exploits CLOB pricing dislocations in standard 2-outcome markets. It does not touch negative-risk or multi-outcome events; those are handled by neg-risk-sum-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 toAll standard binary (2-outcome YES/NO) CLOB markets where best_ask_YES + best_ask_NO < 1.00 pUSD net of fee buffer
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Strategy pod

2. Purpose

Sum-to-One Arb detects when the combined cost of buying both YES and NO tokens on a standard binary market falls below $1.00 pUSD, net of platform fees and a configurable fee buffer. When edge (in basis points) exceeds min_edge_bps, the bot emits a pair of OrderIntents — one for YES and one for NO — sized to the smaller of the two books up to max_leg_size_usd. Both legs fill to produce a guaranteed $1.00 settlement regardless of outcome. This is a user-controlled execution tool that exploits CLOB pricing dislocations in standard 2-outcome markets. It does not touch negative-risk or multi-outcome events; those are handled by neg-risk-sum-arb.

3. Why This Bot Matters

  • Edge is measured incorrectly (ignores fees)

    Platform fee C×feeRate×p×(1-p) peaks at p=0.5. Entering without fee buffer eats the edge entirely and turns a positive-expectation trade into a loss.

  • Slippage consumes one leg between intent generation and fill

    If the YES leg fills but the NO leg moves before submission, the combined cost may exceed $1.00 and the guaranteed profit becomes a guaranteed loss.

  • Market closes or resolves between the two legs

    Submitting a GTC order for the second leg on a market that has resolved or halted leaves an open order with no offsetting settlement path.

  • feeRateBps hardcoded on signed order (V1 pattern)

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

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
Best ask on YES and NO token IDsws_market (CLOB WebSocket)YesCompute sum_of_asks = best_ask_YES + best_ask_NO and measure edge against 1.00 pUSD.
Top-of-book depth for both legsclob_publicYesSize each leg to the minimum of depth available and max_leg_size_usd.
Market condition ID, outcome token IDs, negRisk flagclob_public / internalYesConfirm market is binary (negRisk=false) and not closed or resolved.
Platform fee rate for market categoryonchain (CTFExchangeV2 fee config)YesEstimate fee drag C×feeRate×p×(1-p); crypto ≤1.80%, sports 0.75%, geopolitical free.
Market open/closed/resolved statusclob_publicYesSkip closed or resolved markets immediately.

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.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_edge_bps15105Minimum net edge in basis points (after fee_buffer and slippage_buffer) required before emitting an OrderIntent pair.
fee_buffer_bps302010Additional basis-point buffer subtracted from raw edge to account for platform fees C×feeRate×p×(1-p). Crypto markets peak near 180 bps at p=0.5; geopolitical markets have no fee.
slippage_buffer_bps102050Basis points reserved for expected slippage between generating the two intents and the second leg filling.
max_leg_size_usd5007501000Maximum pUSD size per leg. Both legs are sized to min(depth_available, max_leg_size_usd).

7. Detailed Parameter Instructions

min_edge_bps

What it means

Minimum net edge in basis points (after fee_buffer and slippage_buffer) required before emitting an OrderIntent pair.

Default

{ "min_edge_bps": 15 }

Why this default matters

15 bps provides meaningful margin above fee drag and expected slippage. Below 10 bps the trade is marginal; below 5 bps the bot will not fire regardless of config to prevent fee-negative entries.

Threshold logic

ConditionAction
edge_bps ≥ 15EMIT OrderIntent pair
10 ≤ edge_bps < 15WARN SUM_TO_ONE_ARB_EDGE_MARGINAL — emit with reduced size (50%)
edge_bps < 5 (hard floor)SKIP — SUM_TO_ONE_ARB_NO_EDGE; do not emit

Developer check

if edge_bps < params.hard: return skip('SUM_TO_ONE_ARB_NO_EDGE')

User-facing English

This market does not have enough price dislocation to make a profitable trade after accounting for fees and expected slippage.

fee_buffer_bps

What it means

Additional basis-point buffer subtracted from raw edge to account for platform fees C×feeRate×p×(1-p). Crypto markets peak near 180 bps at p=0.5; geopolitical markets have no fee.

Default

{ "fee_buffer_bps": 30 }

Why this default matters

30 bps covers worst-case taker fee on crypto markets near p=0.5. Lowering this without adjusting min_edge_bps can make fee-negative trades appear positive.

Threshold logic

ConditionAction
fee_buffer_bps ≥ 30Full fee margin applied
20–30WARN — reduced fee margin; acceptable only for low-fee market categories
< 10Hard floor; config change rejected

Developer check

netEdge = rawEdge - params.fee_buffer_bps - params.slippage_buffer_bps

User-facing English

— not yet authored —

slippage_buffer_bps

What it means

Basis points reserved for expected slippage between generating the two intents and the second leg filling.

Default

{ "slippage_buffer_bps": 10 }

Why this default matters

10 bps covers typical CLOB latency slippage on liquid binary markets. Higher values on illiquid markets help avoid partial-leg scenarios.

Threshold logic

ConditionAction
≤ 10 bpsNormal; emit both legs
20–50 bpsWARN — high slippage config; only appropriate on low-liquidity markets
> 50 bpsReject config change

Developer check

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

User-facing English

— not yet authored —

max_leg_size_usd

What it means

Maximum pUSD size per leg. Both legs are sized to min(depth_available, max_leg_size_usd).

Default

{ "max_leg_size_usd": 500 }

Why this default matters

500 pUSD per leg keeps individual order impact modest on typical Polymarket binary books. The Risk guardrail pipeline may further reduce this.

Threshold logic

ConditionAction
≤ 500 pUSDNormal leg sizing
500–1000 pUSDWARN; consider iceberg split via SmartRouter
> 1000 pUSDReject config change — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

legSize = min(depthAvailable, params.max_leg_size_usd, riskConstraints.max_size_usd)

User-facing English

Each side of this trade was sized to fit within available market liquidity.

8. Default Configuration

{
  "bot_id": "strat.sum_to_one_arb",
  "version": "2.1.0",
  "mode": "general_live",
  "defaults": {
    "min_edge_bps": 15,
    "fee_buffer_bps": 30,
    "slippage_buffer_bps": 10,
    "max_leg_size_usd": 500
  },
  "locked": {
    "min_edge_bps": {
      "min": 5
    },
    "max_leg_size_usd": {
      "max": 1000
    },
    "slippage_buffer_bps": {
      "max": 50
    }
  }
}

9. Implementation Flow

  1. Check KillSwitch active flag; if active, skip and emit no OrderIntent.
  2. Subscribe to ws_market book updates for all active binary markets (negRisk=false).
  3. On each book tick: compute sum_of_asks = best_ask_YES + best_ask_NO.
  4. Estimate platform fee drag: fee_drag = C * feeRate * p_yes * (1 - p_yes) + C * feeRate * p_no * (1 - p_no) where p = best_ask.
  5. Compute raw_edge_bps = (1.00 - sum_of_asks) * 10000.
  6. Compute net_edge_bps = raw_edge_bps - fee_buffer_bps - slippage_buffer_bps.
  7. If net_edge_bps < min_edge_bps hard floor (5), emit DecisionReport with intent_emitted=false reason SUM_TO_ONE_ARB_NO_EDGE; skip.
  8. If net_edge_bps < min_edge_bps warning threshold (15), emit WARN SUM_TO_ONE_ARB_EDGE_MARGINAL and reduce leg size by 50%.
  9. Fetch top-of-book depth for YES and NO legs from clob_public; set legSize = min(depth_yes, depth_no, max_leg_size_usd).
  10. Confirm market status is open and not resolved (clob_public market endpoint).
  11. Emit OrderIntent YES: market_id, outcome=YES, side=buy, price=best_ask_YES, size_pUSD=legSize, tif=FOK, builder={code: builderCode, fee_bps: 25}.
  12. Emit OrderIntent NO: market_id, outcome=NO, side=buy, price=best_ask_NO, size_pUSD=legSize, tif=FOK, builder={code: builderCode, fee_bps: 25}.
  13. Note: fees are operator-set at match time in V2 — feeRateBps is NOT present on the signed order.
  14. Emit DecisionReport with intent_emitted=true, edge_bps=net_edge_bps, reason SUM_TO_ONE_ARB_EDGE_PRESENT.
  15. Sampled 1/100 skipped opportunities also emit a DecisionReport with intent_emitted=false for observability.

10. Reference Implementation

Subscribes to CLOB WebSocket book updates for binary markets, computes net edge in bps after fee and slippage buffers, and emits a YES+NO OrderIntent pair when edge exceeds the configured minimum.

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

FUNCTION evaluateMarket(market_id, bookTick):
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active:
    RETURN

  // --- 1. Validate book freshness ---
  IF isStale(bookTick, maxAgeS=5):
    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
    RETURN

  // --- 2. Confirm binary market ---
  market = FETCH clob_public.GET('/markets/' + market_id)
  IF market.neg_risk OR market.closed OR market.resolved:
    EMIT DecisionReport(intent_emitted=false, reason='MARKET_CLOSED')
    RETURN

  // --- 3. Compute edge ---
  best_ask_YES = bookTick.yes.asks[0].price
  best_ask_NO  = bookTick.no.asks[0].price
  sum_of_asks  = best_ask_YES + best_ask_NO
  raw_edge_bps = (1.00 - sum_of_asks) * 10000

  // --- 4. Deduct fee drag ---
  feeRate = FETCH onchain.feeConfig(market_id)  // e.g. 0.018 for crypto
  fee_drag_bps = feeRate * best_ask_YES * (1 - best_ask_YES) * 10000
               + feeRate * best_ask_NO  * (1 - best_ask_NO)  * 10000
  net_edge_bps = raw_edge_bps - params.fee_buffer_bps - params.slippage_buffer_bps

  // --- 5. Hard floor check ---
  IF net_edge_bps < params.min_edge_bps_hard:  // 5 bps absolute floor
    // sample 1/100 skips for observability
    IF random() < 0.01:
      EMIT DecisionReport(intent_emitted=false, reason='SUM_TO_ONE_ARB_NO_EDGE', edge_bps=net_edge_bps)
    RETURN

  // --- 6. Warning threshold ---
  legSizeMultiplier = 1.0
  IF net_edge_bps < params.min_edge_bps:  // 15 bps default
    WARN('SUM_TO_ONE_ARB_EDGE_MARGINAL')
    legSizeMultiplier = 0.5

  // --- 7. Size legs ---
  depth_YES = bookTick.yes.asks[0].size_pusd
  depth_NO  = bookTick.no.asks[0].size_pusd
  legSize   = toPusdUnits(min(depth_YES, depth_NO, params.max_leg_size_usd) * legSizeMultiplier)

  // --- 8. Build OrderIntents (V2: no feeRateBps; builder field carries code) ---
  intentYES = OrderIntent(
    market_id   = market_id,
    outcome     = 'YES',
    side        = 'buy',
    price       = best_ask_YES,
    size_pUSD   = legSize,
    tif         = 'FOK',
    post_only   = false,
    builder     = { code: config.builder_code, fee_bps: 25 },
    negrisk_aware = false
  )
  intentNO = OrderIntent(
    market_id   = market_id,
    outcome     = 'NO',
    side        = 'buy',
    price       = best_ask_NO,
    size_pUSD   = legSize,
    tif         = 'FOK',
    post_only   = false,
    builder     = { code: config.builder_code, fee_bps: 25 },
    negrisk_aware = false
  )

  EMIT intentYES
  EMIT intentNO
  EMIT DecisionReport(
    intent_emitted = true,
    edge_bps       = net_edge_bps,
    reasons        = ['SUM_TO_ONE_ARB_EDGE_PRESENT']
  )

SDK calls used

  • fetchClobPublic('/markets/' + market_id)
  • ws_market.subscribe('book', [yes_token_id, no_token_id])
  • onchain.feeConfig(market_id)
  • toPusdUnits(rawFloat)
  • buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })
  • internal.killswitch.status()
  • internal.builder_code

Complexity: O(1) per market book tick

11. Wire Examples

Input — what arrives on the wire

WebSocket book tick — binary market with edgews_market

{
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "yes_token_id": "0xabc001",
  "no_token_id": "0xabc002",
  "yes_best_ask": "0.488",
  "no_best_ask": "0.510",
  "sum_of_asks": "0.998",
  "depth_YES_pusd": "620.00",
  "depth_NO_pusd": "540.00",
  "received_at_ms": 1746789600000
}

Output — what the bot emits

OrderIntent — YES leg (FOK, builder-attributed)

{
  "intent_id": "oi_01HX9KZQ7F2A1B",
  "trace_id": "tr_01HX9KZQ7E8VR5",
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "outcome": "YES",
  "side": "buy",
  "price": "0.488",
  "size_pUSD": "500.00",
  "tif": "FOK",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "edge_bps": 18.4,
    "reasons": [
      "SUM_TO_ONE_ARB_EDGE_PRESENT"
    ]
  },
  "comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}

OrderIntent — NO leg (FOK, builder-attributed)

{
  "intent_id": "oi_01HX9KZQ7F2B2C",
  "trace_id": "tr_01HX9KZQ7E8VR5",
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "outcome": "NO",
  "side": "buy",
  "price": "0.510",
  "size_pUSD": "500.00",
  "tif": "FOK",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "edge_bps": 18.4,
    "reasons": [
      "SUM_TO_ONE_ARB_EDGE_PRESENT"
    ]
  },
  "comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}

DecisionReport — skipped (no edge), sampled 1/100

{
  "report_id": "dr_01HX9KZQ7F9ZZZ",
  "bot_id": "strat.sum_to_one_arb",
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "intent_emitted": false,
  "edge_bps": 1.2,
  "reasons": [
    "SUM_TO_ONE_ARB_NO_EDGE"
  ],
  "sampled": true,
  "evaluated_at_ms": 1746789601000
}

12. Decision Logic

APPROVE

net_edge_bps ≥ min_edge_bps, market is open, both legs have available depth, KillSwitch is inactive. Bot emits two OrderIntents (YES + NO legs) as a FOK pair.

RESHAPE_REQUIRED

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

REJECT

net_edge_bps < 5 bps hard floor; market closed or resolved; KillSwitch active; stale book data (STALE_MARKET_DATA). Emit DecisionReport with intent_emitted=false.

WARNING_ONLY

edge_bps between 5 and 15 triggers SUM_TO_ONE_ARB_EDGE_MARGINAL warn and 50% size reduction before emitting.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "intent_id": "oi_01HX9KZQ7F2A1B",
  "trace_id": "tr_01HX9KZQ7E8VR5",
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "outcome": "YES",
  "side": "buy",
  "price": "0.488",
  "size_pUSD": "500.00",
  "tif": "FOK",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "edge_bps": 18.4,
    "reasons": [
      "SUM_TO_ONE_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
SUM_TO_ONE_ARB_EDGE_PRESENTINFONet edge after fee and slippage buffers meets or exceeds min_edge_bps. OrderIntent pair emitted.Emit YES + NO OrderIntents.A pricing gap was detected and orders were placed to capture it.
SUM_TO_ONE_ARB_NO_EDGEINFONet edge is below the 5 bps absolute hard floor. No trade opportunity exists after fees.Skip; emit DecisionReport with intent_emitted=false (sampled 1/100).No pricing gap was large enough to trade after accounting for fees.
SUM_TO_ONE_ARB_EDGE_MARGINALWARNEdge is between the hard floor (5 bps) and the warning threshold (15 bps). Trade is marginal.Emit OrderIntents at 50% leg size; log warning.A small pricing gap was detected. A reduced-size order was placed.
SUM_TO_ONE_ARB_DEPTH_INSUFFICIENTWARNTop-of-book depth on one or both legs is below the minimum viable trade size (10 pUSD).Skip; emit DecisionReport with intent_emitted=false.Not enough liquidity on both sides to place a matched trade.
STALE_MARKET_DATAHARD_REJECTBook snapshot or market metadata is older than 5 seconds.Skip; no OrderIntent emitted.Market data was too old to act on safely.
MARKET_CLOSEDHARD_REJECTMarket is closed, resolved, or has neg_risk=true (wrong strategy).Skip immediately; no OrderIntent emitted.This market is no longer open for trading.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all markets; no OrderIntents emitted.Trading is currently paused.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTA config change would push a parameter past its locked hard limit (e.g. max_leg_size_usd > 1000).Reject config change; do not apply.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_sumtoonearb_decisions_totalcountercountverdict, reason_codeTotal evaluation cycles, split by intent_emitted (true/false) and reason code.
polytraders_strat_sumtoonearb_edge_bpshistogrambasis_pointsDistribution of net edge in bps across all evaluated markets, including skipped opportunities.
polytraders_strat_sumtoonearb_leg_size_pusdhistogrampusdoutcomeDistribution of emitted leg sizes in pUSD per YES/NO outcome.
polytraders_strat_sumtoonearb_intents_emitted_totalcountercountoutcomeTotal OrderIntents emitted, split by outcome leg (YES, NO).
polytraders_strat_sumtoonearb_eval_latency_mshistogrammillisecondsWall-clock time from book tick receipt to OrderIntent emit.
polytraders_strat_sumtoonearb_stale_feed_totalcountercountNumber of evaluation cycles skipped due to stale ws_market feed.

Alerts

AlertConditionSeverityRunbook
SumToOneArbHighStaleFeedrate(polytraders_strat_sumtoonearb_stale_feed_total[5m]) > 0.1warn#runbook-sumtoonearb-stale-feed
SumToOneArbNoEdgeStreakrate(polytraders_strat_sumtoonearb_decisions_total{verdict='false'}[15m]) / rate(polytraders_strat_sumtoonearb_decisions_total[15m]) > 0.99warn#runbook-sumtoonearb-no-edge
SumToOneArbHighLatencyhistogram_quantile(0.99, rate(polytraders_strat_sumtoonearb_eval_latency_ms_bucket[5m])) > 150warn#runbook-sumtoonearb-latency
SumToOneArbKillSwitchBlockingrate(polytraders_strat_sumtoonearb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch

Dashboards

  • Grafana — Strategy / SumToOneArb edge distribution
  • Grafana — Strategy / SumToOneArb intent throughput and leg sizing

16. Developer Reporting

{
  "bot_id": "strat.sum_to_one_arb",
  "market_id": "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab",
  "sum_of_asks": 0.9982,
  "raw_edge_bps": 18.0,
  "fee_drag_bps": 12.3,
  "net_edge_bps": 18.4,
  "leg_size_pusd": 500.0,
  "yes_ask": 0.488,
  "no_ask": 0.51,
  "intent_emitted": true,
  "reason": "SUM_TO_ONE_ARB_EDGE_PRESENT",
  "emitted_at_ms": 1746789600000
}

17. Plain-English Reporting

SituationUser-facing explanation
Arb trade initiatedBoth sides of this market were priced below $1 combined, creating a low-risk opportunity to buy both and collect the $1 settlement. Orders were submitted to capture this pricing gap.
No edge — no tradeThe combined price of YES and NO tokens was not low enough to profit after fees and expected price movement. No order was placed.
Edge marginal — reduced sizeThe pricing gap was present but small. Order size was halved to limit exposure given the thin margin.
Market closed or resolvedThis market has already resolved or closed. No new orders can be placed.

18. Failure-Mode Block

main_failure_modeThe YES leg fills via FOK but the NO leg's price moves before submission, leaving a one-sided exposure with no guaranteed $1 settlement path.
false_positive_riskStale WebSocket book data shows a sum < 1.00 that has already corrected, causing the bot to attempt an arb that no longer exists and incurring fee costs with no edge.
false_negative_riskfee_buffer_bps set too conservatively on a geopolitical (zero-fee) market causes valid arb opportunities to be skipped.
safe_fallbackIf book data is stale (last_seen > 5s), emit STALE_MARKET_DATA and skip without emitting any OrderIntent. Never enter a one-legged position deliberately.
required_dependenciesws_market book stream (both outcome token IDs), clob_public market endpoint (status + depth), onchain fee rate config, KillSwitch active flag, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_WS_FEEDPause ws_market WebSocket; let last_seen age beyond 5sAutomatic on WebSocket reconnect within one evaluation cycle.
NO_EDGE_MARKETSet mock yes_ask=0.500, no_ask=0.502 (sum=1.002 > 1.00)Automatic when book rebalances.
FOK_PARTIAL_FILLMock CLOB to reject second FOK leg after first leg fillsRisk pipeline detects naked YES position and flags for manual review.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic on manual KillSwitch reset.
FEE_CONFIG_STALEBlock onchain RPC; let fee cache exceed 60s TTLAutomatic when RPC is restored.

20. State & Persistence

Cold-start recovery

On cold start, state is empty; first book tick per market triggers fresh evaluation without any stale-state comparison.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight50
Idempotency keyintent_id
Per-call timeout (ms)150
Backpressure strategydrop oldest pending tick per market_id when queue depth > 10
Locking / mutual exclusionper-market_id mutex for Redis state write

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
Polymarket CLOB v2 (public)99.9% (Polymarket-published)
Polymarket CLOB WebSocket (ws_market)best-effort
CTFExchangeV2 (onchain fee config)Polygon RPC SLA

23. Security Surfaces

On-chain contract calls

ContractMethodNetworkEffect
CTFExchangeV2polygon

Abuse vectors considered

  • Front-running: emitting YES leg publicly before NO leg allows adversaries to move the NO book
  • Replaying a signed OrderIntent after the market's arb window has closed
  • Injecting a modified builder.code to redirect attribution fees

Mitigations

  • Both legs use FOK (Fill-or-Kill); a partial fill on one leg is rejected rather than leaving a naked position
  • V2 order timestamp(ms) field invalidates replays outside the exchange's acceptance window
  • Builder code is read from immutable internal config; not user-supplied
  • per-intent_id deduplication within 24h prevents replay

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesStandard 2-outcome binary arb only. NegRisk markets are explicitly excluded (negRisk flag check). Both legs carry builder.code bytes32 for attribution; taker fee_bps capped at 25 (well within 100 bps taker max). feeRateBps is not present on any signed order.

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)v2 (pUSD, fees operator-set at match time)CLOB V2 cutoverSwitched to py-clob-client-v2. Removed feeRateBps from signed-order construction — fees are now operator-set at match time by CTFExchangeV2. Updated collateral denomination from USDC.e to pUSD. Injected builder field (bytes32) on both YES and NO leg intents. EIP-712 Exchange domain version updated from '1' to '2'.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit pair when sum_of_asks = 0.982 (edge = 18 bps gross)best_ask_YES=0.480, best_ask_NO=0.502, fee_buffer=30, slippage_buffer=10, min_edge=15Two OrderIntents emitted; net_edge_bps ≥ 15; intent_emitted=true
Skip when sum_of_asks = 0.998 (raw edge = 2 bps, below hard floor 5)best_ask_YES=0.499, best_ask_NO=0.499No OrderIntent; DecisionReport intent_emitted=false, reason=SUM_TO_ONE_ARB_NO_EDGE
Reduce leg size 50% when edge is marginal (7 bps)net_edge_bps=7, min_edge_bps=15OrderIntents emitted with size=250 (50% of max_leg_size_usd=500); WARN SUM_TO_ONE_ARB_EDGE_MARGINAL
Skip closed marketmarket_status=closedNo OrderIntent; reason=MARKET_CLOSED
Skip when KillSwitch activekillswitch.active=trueNo OrderIntent emitted at all
Leg size capped at min(depth, max_leg_size_usd)depth_YES=300, depth_NO=450, max_leg_size_usd=500Both legs sized at 300 pUSD

Integration Tests

TestExpected result
Full cycle: ws_market tick → edge detected → two signed V2 OrderIntents submitted to CLOBBoth orders contain builder.code (bytes32), no feeRateBps field, EIP-712 domain version '2'
Stale ws_market feed triggers STALE_MARKET_DATA skipDecisionReport intent_emitted=false, reason=STALE_MARKET_DATA after 5s feed gap

Property Tests

PropertyRequired behaviour
Bot never emits a single-leg OrderIntent; always emits YES + NO pair or neitherAlways true
net_edge_bps is always ≥ min_edge_bps hard floor (5) when an OrderIntent is emittedAlways true
feeRateBps field is never present on any emitted OrderIntentAlways true — V2 fees are operator-set at match time

27. Operational Runbook

SumToOneArb incidents are usually stale WebSocket feed, no-edge market conditions, or one-sided FOK fills. The last is the most operationally significant and requires manual risk review.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SumToOneArbHighStaleFeed
SumToOneArbNoEdgeStreak
SumToOneArbHighLatency
SumToOneArbKillSwitchBlocking

Manual overrides

Healthcheck

GET /internal/health/sum-to-one-arb -> 200 if ws_market feed last_seen < 5s, Redis reachable, KillSwitch inactive, and at least one market evaluated in last 60s.

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 pair-emission invariantCI 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 < 150ms over 24hpolytraders_strat_sumtoonearb_eval_latency_ms histogramp99 < 150ms
Zero one-sided fill incidents in shadow mode over 48hFill reconciliation report0 incidents

Promote to General live

GateHow measuredThreshold
E2E: edge detected → two signed V2 OrderIntents → both FOK fills confirmed on Polygon testnetE2E testPass
DecisionReport sampling verified: 1/100 skips emit reportIntegration testPass

29. Developer Checklist

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

RequirementStatus
Purpose defined✓ done
Required inputs listed✓ done
Parameters defined✓ done
Defaults defined✓ done
Warning thresholds defined✓ done
Hard thresholds defined✓ done
Safe fallback defined✓ done
Structured output defined✓ done
Developer log defined✓ done
Plain-English explanation✓ done
Unit tests defined✓ done
Integration tests defined✓ done
Property tests defined✓ done
Failure-mode block complete✓ done
Reference implementation pseudocode✓ done
Wire examples (input + output)✓ done
Reason codes listed✓ done
Metrics & logs defined✓ done
State & persistence defined✓ done
Concurrency & idempotency defined✓ done
Dependencies declared✓ done
Security surfaces declared✓ done
Polymarket V2 compatibility declared✓ done
Version & migration history declared✓ done
Operational runbook defined✓ done
Promotion gates defined✓ done
Failure-injection recipes defined✓ done