3.3 Sum-to-One Arb
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
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 | All standard binary (2-outcome YES/NO) CLOB markets where best_ask_YES + best_ask_NO < 1.00 pUSD net of fee buffer |
| Default mode | general_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders 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
| Input | Source | Required? | Use |
|---|---|---|---|
| Best ask on YES and NO token IDs | ws_market (CLOB WebSocket) | Yes | Compute sum_of_asks = best_ask_YES + best_ask_NO and measure edge against 1.00 pUSD. |
| Top-of-book depth for both legs | clob_public | Yes | Size each leg to the minimum of depth available and max_leg_size_usd. |
| Market condition ID, outcome token IDs, negRisk flag | clob_public / internal | Yes | Confirm market is binary (negRisk=false) and not closed or resolved. |
| Platform fee rate for market category | onchain (CTFExchangeV2 fee config) | Yes | Estimate fee drag C×feeRate×p×(1-p); crypto ≤1.80%, sports 0.75%, geopolitical free. |
| Market open/closed/resolved status | clob_public | Yes | Skip closed or resolved markets immediately. |
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. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| min_edge_bps | 15 | 10 | 5 | Minimum net edge in basis points (after fee_buffer and slippage_buffer) required before emitting an OrderIntent pair. |
| fee_buffer_bps | 30 | 20 | 10 | 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. |
| slippage_buffer_bps | 10 | 20 | 50 | Basis points reserved for expected slippage between generating the two intents and the second leg filling. |
| max_leg_size_usd | 500 | 750 | 1000 | Maximum 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
| Condition | Action |
|---|---|
| edge_bps ≥ 15 | EMIT OrderIntent pair |
| 10 ≤ edge_bps < 15 | WARN 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
| Condition | Action |
|---|---|
| fee_buffer_bps ≥ 30 | Full fee margin applied |
| 20–30 | WARN — reduced fee margin; acceptable only for low-fee market categories |
| < 10 | Hard 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
| Condition | Action |
|---|---|
| ≤ 10 bps | Normal; emit both legs |
| 20–50 bps | WARN — high slippage config; only appropriate on low-liquidity markets |
| > 50 bps | Reject 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
| Condition | Action |
|---|---|
| ≤ 500 pUSD | Normal leg sizing |
| 500–1000 pUSD | WARN; consider iceberg split via SmartRouter |
| > 1000 pUSD | Reject 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
- Check KillSwitch active flag; if active, skip and emit no OrderIntent.
- Subscribe to ws_market book updates for all active binary markets (negRisk=false).
- On each book tick: compute sum_of_asks = best_ask_YES + best_ask_NO.
- Estimate platform fee drag: fee_drag = C * feeRate * p_yes * (1 - p_yes) + C * feeRate * p_no * (1 - p_no) where p = best_ask.
- Compute raw_edge_bps = (1.00 - sum_of_asks) * 10000.
- Compute net_edge_bps = raw_edge_bps - fee_buffer_bps - slippage_buffer_bps.
- If net_edge_bps < min_edge_bps hard floor (5), emit DecisionReport with intent_emitted=false reason SUM_TO_ONE_ARB_NO_EDGE; skip.
- If net_edge_bps < min_edge_bps warning threshold (15), emit WARN SUM_TO_ONE_ARB_EDGE_MARGINAL and reduce leg size by 50%.
- Fetch top-of-book depth for YES and NO legs from clob_public; set legSize = min(depth_yes, depth_no, max_leg_size_usd).
- Confirm market status is open and not resolved (clob_public market endpoint).
- Emit OrderIntent YES: market_id, outcome=YES, side=buy, price=best_ask_YES, size_pUSD=legSize, tif=FOK, builder={code: builderCode, fee_bps: 25}.
- Emit OrderIntent NO: market_id, outcome=NO, side=buy, price=best_ask_NO, size_pUSD=legSize, tif=FOK, builder={code: builderCode, fee_bps: 25}.
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT present on the signed order.
- Emit DecisionReport with intent_emitted=true, edge_bps=net_edge_bps, reason SUM_TO_ONE_ARB_EDGE_PRESENT.
- 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 edge — ws_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
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
SUM_TO_ONE_ARB_EDGE_PRESENT | INFO | Net 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_EDGE | INFO | Net 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_MARGINAL | WARN | Edge 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_INSUFFICIENT | WARN | Top-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_DATA | HARD_REJECT | Book snapshot or market metadata is older than 5 seconds. | Skip; no OrderIntent emitted. | Market data was too old to act on safely. |
MARKET_CLOSED | HARD_REJECT | Market 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_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all markets; no OrderIntents emitted. | Trading is currently paused. |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | A 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
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_sumtoonearb_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles, split by intent_emitted (true/false) and reason code. |
polytraders_strat_sumtoonearb_edge_bps | histogram | basis_points | Distribution of net edge in bps across all evaluated markets, including skipped opportunities. | |
polytraders_strat_sumtoonearb_leg_size_pusd | histogram | pusd | outcome | Distribution of emitted leg sizes in pUSD per YES/NO outcome. |
polytraders_strat_sumtoonearb_intents_emitted_total | counter | count | outcome | Total OrderIntents emitted, split by outcome leg (YES, NO). |
polytraders_strat_sumtoonearb_eval_latency_ms | histogram | milliseconds | Wall-clock time from book tick receipt to OrderIntent emit. | |
polytraders_strat_sumtoonearb_stale_feed_total | counter | count | Number of evaluation cycles skipped due to stale ws_market feed. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
SumToOneArbHighStaleFeed | rate(polytraders_strat_sumtoonearb_stale_feed_total[5m]) > 0.1 | warn | #runbook-sumtoonearb-stale-feed |
SumToOneArbNoEdgeStreak | rate(polytraders_strat_sumtoonearb_decisions_total{verdict='false'}[15m]) / rate(polytraders_strat_sumtoonearb_decisions_total[15m]) > 0.99 | warn | #runbook-sumtoonearb-no-edge |
SumToOneArbHighLatency | histogram_quantile(0.99, rate(polytraders_strat_sumtoonearb_eval_latency_ms_bucket[5m])) > 150 | warn | #runbook-sumtoonearb-latency |
SumToOneArbKillSwitchBlocking | rate(polytraders_strat_sumtoonearb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #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
| Situation | User-facing explanation |
|---|---|
| Arb trade initiated | Both 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 trade | The 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 size | The pricing gap was present but small. Order size was halved to limit exposure given the thin margin. |
| Market closed or resolved | This market has already resolved or closed. No new orders can be placed. |
18. Failure-Mode Block
| main_failure_mode | The 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_risk | Stale 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_risk | fee_buffer_bps set too conservatively on a geopolitical (zero-fee) market causes valid arb opportunities to be skipped. |
| safe_fallback | If 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_dependencies | ws_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
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
STALE_WS_FEED | Pause ws_market WebSocket; let last_seen age beyond 5s | Automatic on WebSocket reconnect within one evaluation cycle. | |
NO_EDGE_MARKET | Set mock yes_ask=0.500, no_ask=0.502 (sum=1.002 > 1.00) | Automatic when book rebalances. | |
FOK_PARTIAL_FILL | Mock CLOB to reject second FOK leg after first leg fills | Risk pipeline detects naked YES position and flags for manual review. | |
KILL_SWITCH_ON | Set killswitch.active=true | Automatic on manual KillSwitch reset. | |
FEE_CONFIG_STALE | Block onchain RPC; let fee cache exceed 60s TTL | Automatic 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
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 50 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 150 |
| Backpressure strategy | drop oldest pending tick per market_id when queue depth > 10 |
| Locking / mutual exclusion | per-market_id mutex for Redis state write |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Checked first; blocks all intent emission when active. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | ||
| gov.builder_attribution |
External services
| Service | Endpoint | SLA assumed | On 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
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon |
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
| Aspect | Value |
|---|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Standard 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
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) | CLOB V2 cutover | Switched 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
| Test | Setup | Expected 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=15 | Two 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.499 | No 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=15 | OrderIntents emitted with size=250 (50% of max_leg_size_usd=500); WARN SUM_TO_ONE_ARB_EDGE_MARGINAL |
| Skip closed market | market_status=closed | No OrderIntent; reason=MARKET_CLOSED |
| Skip when KillSwitch active | killswitch.active=true | No OrderIntent emitted at all |
| Leg size capped at min(depth, max_leg_size_usd) | depth_YES=300, depth_NO=450, max_leg_size_usd=500 | Both legs sized at 300 pUSD |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: ws_market tick → edge detected → two signed V2 OrderIntents submitted to CLOB | Both orders contain builder.code (bytes32), no feeRateBps field, EIP-712 domain version '2' |
| Stale ws_market feed triggers STALE_MARKET_DATA skip | DecisionReport intent_emitted=false, reason=STALE_MARKET_DATA after 5s feed gap |
Property Tests
| Property | Required behaviour |
|---|---|
| Bot never emits a single-leg OrderIntent; always emits YES + NO pair or neither | Always true |
| net_edge_bps is always ≥ min_edge_bps hard floor (5) when an OrderIntent is emitted | Always true |
| feeRateBps field is never present on any emitted OrderIntent | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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
| Gate | How measured | Threshold |
|---|---|---|
| All unit tests pass including pair-emission invariant | 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 < 150ms over 24h | polytraders_strat_sumtoonearb_eval_latency_ms histogram | p99 < 150ms |
| Zero one-sided fill incidents in shadow mode over 48h | Fill reconciliation report | 0 incidents |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: edge detected → two signed V2 OrderIntents → both FOK fills confirmed on Polygon testnet | E2E test | Pass |
| DecisionReport sampling verified: 1/100 skips emit report | Integration test | 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 |