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.7 Cross-Venue Arb

3.7 Cross-Venue Arb

Strategy Alpha Strategy Trade PLANNED Spec started capital · Direct P8 · Additional strategies pending stub

Cross-Venue Arb identifies price divergences between Polymarket and external prediction markets (Kalshi, PredictIt) that share an identical resolution source, then emits an OrderIntent to trade the cheaper leg on Polymarket. Both venues must resolve via the same authority; any mismatch blocks the trade.

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
StatusPLANNED
ReadinessSpec started
Runs beforeRisk guardrail pipeline
Runs afterCross-venue price-feed adapter
Applies toPolymarket binary markets where an identical resolution source matches a live Kalshi or PredictIt contract and the price gap exceeds min_gap_bps_after_fees
Default modeshadow_only
User-visibleAdvanced details only
Developer ownerPolytraders core — Strategy pod

2. Purpose

Cross-Venue Arb identifies price divergences between Polymarket and external prediction markets (Kalshi, PredictIt) that share an identical resolution source, then emits an OrderIntent to trade the cheaper leg on Polymarket. Both venues must resolve via the same authority; any mismatch blocks the trade.

3. Why This Bot Matters

  • Resolution source mismatch

    Two venues may look identical but resolve differently. Trading on a false parity creates an unhedged directional position rather than an arbitrage.

  • Stale external venue data

    Cross-venue prices update asynchronously. Stale Kalshi or PredictIt snapshots produce phantom gaps that evaporate before the order lands.

  • Fee drag understated

    Both venues charge fees; if min_gap_bps_after_fees is set too low, ostensible arb edges are consumed by combined fee drag leaving no profit.

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
Polymarket CLOB book (mid, spread, depth)ws_marketYesCompute Polymarket mid-price for gap calculation.
Market metadata (resolution source, status)clob_publicYesVerify resolution source matches external venue before acting.
Market open/closed statusclob_publicYesSkip closed or resolved markets.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesAbort all intent emission if KillSwitch active.
External venue price snapshots (Kalshi, PredictIt)internal (cross-venue adapter)YesCompute price gap between venues for the same event.
Builder code bytes32internal configYesInjected into builder field on every signed V2 OrderIntent.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_gap_bps_after_fees1508030Minimum price gap in basis points between Polymarket and external venue, after deducting both venues' fees, required to emit an OrderIntent.
require_resolution_source_matchTrueNoneNoneIf true (locked), both Polymarket and the external venue must resolve via the same official source. Cannot be disabled.
allowed_venues['kalshi', 'predictit']NoneNoneWhitelist of external prediction market venues the bot is permitted to reference for gap calculation.
manual_approval_requiredTrueNoneNoneIf true, each new arb pair must receive one-time human approval before the bot trades it automatically.

7. Detailed Parameter Instructions

min_gap_bps_after_fees

What it means

Minimum price gap in basis points between Polymarket and external venue, after deducting both venues' fees, required to emit an OrderIntent.

Default

{ "min_gap_bps_after_fees": 150 }

Why this default matters

150 bps provides a comfortable margin after combined fees (~50 bps each venue at p=0.5) and execution slippage.

Threshold logic

ConditionAction
>= 150 bpsEMIT IOC OrderIntent
80–150 bpsWARN CROSS_VENUE_EDGE_MARGINAL; emit at 50% size
< 30 bps (hard floor)SKIP — CROSS_VENUE_NO_EDGE

Developer check

if gap_bps < params.hard: return skip('CROSS_VENUE_NO_EDGE')

User-facing English

The price gap between venues was too small after fees to justify a trade.

require_resolution_source_match

What it means

If true (locked), both Polymarket and the external venue must resolve via the same official source. Cannot be disabled.

Default

{ "require_resolution_source_match": true }

Why this default matters

Prevents trading on false parity that would create an unhedged directional position.

Threshold logic

ConditionAction
mismatch detectedHARD_REJECT CROSS_VENUE_SOURCE_MISMATCH

Developer check

if not sources_match(pm_market, ext_market): return skip('CROSS_VENUE_SOURCE_MISMATCH')

User-facing English

The two venues resolve this question from different official sources — arb is not safe.

allowed_venues

What it means

Whitelist of external prediction market venues the bot is permitted to reference for gap calculation.

Default

{ "allowed_venues": ["kalshi", "predictit"] }

Why this default matters

Restricts the bot to venues with known API contracts and fee schedules.

Threshold logic

ConditionAction
venue not in listSKIP — venue not whitelisted

Developer check

if venue not in params.allowed_venues: skip()

User-facing English

This venue is not in the approved list.

manual_approval_required

What it means

If true, each new arb pair must receive one-time human approval before the bot trades it automatically.

Default

{ "manual_approval_required": true }

Why this default matters

Prevents rogue pairing on newly created markets with ambiguous resolution matching.

Threshold logic

ConditionAction
pair not approvedHARD_REJECT CROSS_VENUE_PAIR_NOT_APPROVED

Developer check

if not pair_approved(pm_id, ext_id): return skip('CROSS_VENUE_PAIR_NOT_APPROVED')

User-facing English

This venue pair has not been manually approved yet.

8. Default Configuration

{
  "bot_id": "strat.cross_venue_arb",
  "version": "0.1.0",
  "mode": "shadow_only",
  "defaults": {
    "min_gap_bps_after_fees": 150,
    "require_resolution_source_match": true,
    "manual_approval_required": true
  },
  "locked": {
    "require_resolution_source_match": {
      "value": true
    },
    "min_gap_bps_after_fees": {
      "min": 30
    }
  }
}

9. Implementation Flow

  1. Check KillSwitch; if active, emit no OrderIntents.
  2. For each approved (pm_market, ext_market) pair: verify resolution sources match.
  3. FETCH clob_public market status; skip if closed or resolved.
  4. FETCH ws_market book for pm_market; FETCH external venue snapshot for ext_market.
  5. Compute gap_bps = (ext_price - pm_mid) * 10000 (or reverse if ext cheaper).
  6. Deduct estimated fees from both venues; compute net_gap_bps.
  7. IF net_gap_bps < hard (30 bps): SKIP, emit sampled DecisionReport CROSS_VENUE_NO_EDGE.
  8. IF net_gap_bps < warning (80 bps): WARN CROSS_VENUE_EDGE_MARGINAL; reduce size 50%.
  9. Compute order size = min(max_position_per_pair_usd, available_depth_usd).
  10. EMIT IOC OrderIntent: side=buy, price=best_ask_pm, size_pUSD=orderSize, builder=code.
  11. EMIT DecisionReport with intent_emitted=true, gap_bps, venues, reason CROSS_VENUE_EDGE_TRADE.

10. Reference Implementation

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.

FUNCTION onSnapshot(pair_id, pmMarket, extMarket, pmMid, extPrice):
  ks = FETCH internal.killswitch.status
  IF ks.active: RETURN

  // Verify resolution source match
  pmMeta = FETCH clob_public.GET('/markets/' + pmMarket)
  IF pmMeta.resolution_source != extMarket.resolution_source:
    EMIT DecisionReport(intent_emitted=false, reason='CROSS_VENUE_SOURCE_MISMATCH')
    RETURN

  IF pmMeta.closed OR pmMeta.resolved: RETURN

  // Check snapshot freshness
  IF snapshotAge(extMarket) > 10000:  // ms
    EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
    RETURN

  // Compute gap
  rawGap = (extPrice - pmMid) * 10000
  fees = estimatedFees(pmMid)
  netGap = rawGap - fees

  IF netGap < params.min_gap_bps_after_fees_hard:  // 30 bps
    IF random() < 0.01:
      EMIT DecisionReport(intent_emitted=false, reason='CROSS_VENUE_NO_EDGE')
    RETURN

  sizeMultiplier = 0.5 IF netGap < params.min_gap_bps_after_fees_warn ELSE 1.0
  IF sizeMultiplier < 1.0: WARN('CROSS_VENUE_EDGE_MARGINAL')

  depth = FETCH clob_public.depth(pmMarket)
  orderSize = toPusdUnits(min(params.max_position_per_pair_usd * sizeMultiplier, depth.available))

  EMIT OrderIntent(market=pmMarket, outcome='YES', side='buy', price=pmMid,
                   size_pUSD=orderSize, tif='IOC', builder=internalBuilderCode)
  EMIT DecisionReport(intent_emitted=true, gap_bps=netGap, reason='CROSS_VENUE_EDGE_TRADE')

SDK calls used

  • ws_market.subscribe('book', [pm_market_id])
  • fetchClobPublic('/markets/' + pm_market_id)
  • internal.crossVenueAdapter.getSnapshot(pair_id)
  • buildOrderTypedData(orderParams, {name:'CTFExchange', version:'2', chainId:137})
  • internal.builder_code

Complexity: O(1) per snapshot per approved pair

11. Wire Examples

Input — what arrives on the wire

Cross-venue snapshot — Kalshi YES at 0.627, PM mid at 0.610internal (cross-venue adapter)

{
  "pair_id": "cvp_kalshi_001",
  "pm_market_id": "0xcva0000000000000000000000000000000000000000000000000000000000001",
  "pm_mid": "0.610",
  "ext_venue": "kalshi",
  "ext_price": "0.627",
  "snapshot_age_ms": 800,
  "received_at_ms": 1746790800000
}

Output — what the bot emits

OrderIntent — cross-venue IOC buy YES

{
  "intent_id": "oi_01HCVA0000001A",
  "market_id": "0xcva0000000000000000000000000000000000000000000000000000000000001",
  "outcome": "YES",
  "side": "buy",
  "price": "0.610",
  "size_pUSD": "200.00",
  "tif": "IOC",
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "decision": {
    "gap_bps": 165.0,
    "reasons": [
      "CROSS_VENUE_EDGE_TRADE"
    ]
  }
}

12. Decision Logic

APPROVE

net_gap_bps >= min_gap_bps_after_fees, sources match, pair approved, market open, KillSwitch inactive.

RESHAPE_REQUIRED

Not applicable — reshaping handled by downstream Risk guardrail.

REJECT

net_gap_bps < 30 bps; source mismatch; pair not approved; market closed; KillSwitch active.

WARNING_ONLY

net_gap_bps between 30 and 80 bps triggers warning and 50% size reduction.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "intent_id": "oi_01HCVA0000001A",
  "trace_id": "tr_01HCVA000TR001",
  "market_id": "0xcva0000000000000000000000000000000000000000000000000000000000001",
  "outcome": "YES",
  "side": "buy",
  "price": "0.610",
  "size_pUSD": "200.00",
  "tif": "IOC",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "gap_bps": 165.0,
    "pm_mid": 0.61,
    "ext_price": 0.627,
    "venues": [
      "polymarket",
      "kalshi"
    ],
    "reasons": [
      "CROSS_VENUE_EDGE_TRADE"
    ]
  },
  "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
CROSS_VENUE_EDGE_TRADEINFOGap >= min_gap_bps_after_fees, sources match, pair approved. IOC OrderIntent emitted.Emit IOC OrderIntent.A price difference between venues was found and a trade was placed.
CROSS_VENUE_NO_EDGEINFONet gap_bps below 30 bps hard floor after fee deduction.Skip; emit sampled DecisionReport.The price gap was too small after fees.
CROSS_VENUE_EDGE_MARGINALWARNGap between 30 and 80 bps; trade marginal; size reduced 50%.Emit at 50% size; log warning.A small gap was found; a reduced-size trade was placed.
CROSS_VENUE_SOURCE_MISMATCHHARD_REJECTResolution sources differ between Polymarket and external venue.Skip; no OrderIntent.The venues resolve differently — no trade placed.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all markets; no OrderIntents emitted.Trading is currently paused.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_crossvenuearb_decisions_totalcountercountverdict, reason_code, ext_venueTotal evaluation cycles by verdict, reason code, and external venue.
polytraders_strat_crossvenuearb_gap_bpshistogrambasis_pointsext_venueDistribution of net cross-venue gap in bps.
polytraders_strat_crossvenuearb_intents_emitted_totalcountercountext_venueTotal IOC OrderIntents emitted by external venue.
polytraders_strat_crossvenuearb_eval_latency_mshistogrammillisecondsLatency from snapshot receipt to OrderIntent emit.

Alerts

AlertConditionSeverityRunbook
CrossVenueArbStaleFeedrate(polytraders_strat_crossvenuearb_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1warn#runbook-crossvenuearb-stale
CrossVenueArbKillSwitchrate(polytraders_strat_crossvenuearb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch
CrossVenueArbNoEdgerate(polytraders_strat_crossvenuearb_decisions_total{verdict='skip',reason_code='CROSS_VENUE_NO_EDGE'}[10m]) / rate(polytraders_strat_crossvenuearb_decisions_total[10m]) > 0.95warn#runbook-crossvenuearb-edge

16. Developer Reporting

{
  "bot_id": "strat.cross_venue_arb",
  "market_id": "0xcva0000000000000000000000000000000000000000000000000000000000001",
  "pm_mid": 0.61,
  "ext_price": 0.627,
  "gap_bps": 165.0,
  "net_gap_bps": 155.0,
  "intent_emitted": true,
  "reason": "CROSS_VENUE_EDGE_TRADE",
  "emitted_at_ms": 1746790800000
}

17. Plain-English Reporting

SituationUser-facing explanation
Arb trade placedA price difference was detected between Polymarket and another prediction market. An order was placed on Polymarket to capture the gap.
No edge after feesThe price gap between venues was too small after fees to justify a trade.
Source mismatchThe two venues resolve differently — no trade was placed.

18. Failure-Mode Block

main_failure_modeStale external venue snapshot: the external price ages faster than the Polymarket book, creating phantom gaps that disappear before the order lands.
false_positive_riskResolution source descriptions match textually but differ in practice, leading to a trade that is not a true arb.
false_negative_riskmin_gap_bps_after_fees set too high misses genuine arb opportunities on tighter markets.
safe_fallbackIf external venue snapshot is stale (> 10s) or clob_public unavailable, emit STALE_MARKET_DATA and skip.
required_dependenciesws_market, clob_public, internal cross-venue adapter, KillSwitch, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_EXT_SNAPSHOTFreeze cross-venue adapter updates for > 10sAutomatic when adapter resumes.
SOURCE_MISMATCHOverride test pair with mismatched resolution sourcesAutomatic when correct pair is reloaded.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic on manual KillSwitch reset.

20. State & Persistence

Cold-start recovery

On cold start, external snapshots rebuilt from next polling cycle; pair approval loaded from config.

21. Concurrency & Idempotency

AspectSpecification
Execution modelactor-per-pair
Max in-flight20
Idempotency keyintent_id
Per-call timeout (ms)300
Backpressure strategydrop oldest snapshot per pair_id when queue depth > 2
Locking / mutual exclusionper-pair_id mutex for snapshot state

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
Kalshi public APIbest-effort
PredictIt market data feedbest-effort
Polymarket CLOB WebSocket (ws_market)best-effort

23. Security Surfaces

Abuse vectors considered

  • Adversary injects stale cross-venue snapshots to create phantom arb gaps
  • Resolution source metadata spoofing to bypass source-match gate

Mitigations

  • Cross-venue snapshots have a 10s staleness threshold
  • Resolution source comparison uses Polymarket's authoritative clob_public metadata, not user input
  • Manual pair approval required before bot trades any new pair

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
NotesBot not yet implemented; designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain). feeRateBps not present on any signed OrderIntent.

API surfaces declared

clob_publicclob_authws_marketinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation0.1.0
schema2
releasedNone
planned_releaseQ3-2026

Migration history

DateFromToReasonAction taken
2026-04-28n/av2-specSpec drafted post-CLOB-V2 cutover; bot not yet implementedDesigned against V2 schema (pUSD, builder codes, V2 EIP-712 domain)

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit IOC when gap=165 bps, sources match, pair approvedmin_gap_bps_after_fees=150IOC OrderIntent emitted; reason=CROSS_VENUE_EDGE_TRADE
Skip when source mismatchpm_source='AP', ext_source='Reuters'No OrderIntent; reason=CROSS_VENUE_SOURCE_MISMATCH
Skip when KillSwitch activekillswitch.active=trueNo OrderIntents emitted

Integration Tests

TestExpected result
Full cycle: external snapshot → gap computed → IOC OrderIntent on Polygon testnetOrder has builder.code bytes32, no feeRateBps, tif=IOC, EIP-712 domain v2

Property Tests

PropertyRequired behaviour
Bot never trades when require_resolution_source_match=true and sources differAlways true
feeRateBps never present on any signed OrderIntentAlways true — V2 fees operator-set at match time

27. Operational Runbook

Cross-Venue Arb incidents are typically stale external feed snapshots (blocking all trades for affected pairs) or kill-switch activations. Source mismatches are expected and logged; escalate only if a previously approved pair suddenly mismatches.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
CrossVenueArbStaleFeed
CrossVenueArbKillSwitch
CrossVenueArbNoEdge

Manual overrides

Healthcheck

GET /internal/health/cross-venue-arb -> 200 if External snapshots age < 10s; ≥1 approved pair active; KillSwitch inactive.. Red: All external feeds stale or KillSwitch active..

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 source-mismatch block and stale-data skipCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
p99 eval latency < 300ms over 24h shadow runpolytraders_strat_crossvenuearb_eval_latency_ms histogramp99 < 300ms

Promote to General live

GateHow measuredThreshold
E2E: snapshot received → gap computed → IOC OrderIntent on Polygon testnet with builder.code and no feeRateBpsE2E 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