1. Bot Identity
| Layer | Strategy Strategy |
|---|
| Bot class | Alpha Strategy |
|---|
| Authority | Trade |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec started |
|---|
| Runs before | Risk guardrail pipeline |
|---|
| Runs after | Cross-venue price-feed adapter |
|---|
| Applies to | Polymarket 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 mode | shadow_only |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders 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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| min_gap_bps_after_fees | 150 | 80 | 30 | Minimum price gap in basis points between Polymarket and external venue, after deducting both venues' fees, required to emit an OrderIntent. |
| require_resolution_source_match | True | None | None | If true (locked), both Polymarket and the external venue must resolve via the same official source. Cannot be disabled. |
| allowed_venues | ['kalshi', 'predictit'] | None | None | Whitelist of external prediction market venues the bot is permitted to reference for gap calculation. |
| manual_approval_required | True | None | None | If 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
| Condition | Action |
|---|
| >= 150 bps | EMIT IOC OrderIntent |
| 80–150 bps | WARN 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
| Condition | Action |
|---|
| mismatch detected | HARD_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
| Condition | Action |
|---|
| venue not in list | SKIP — 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
| Condition | Action |
|---|
| pair not approved | HARD_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
- Check KillSwitch; if active, emit no OrderIntents.
- For each approved (pm_market, ext_market) pair: verify resolution sources match.
- FETCH clob_public market status; skip if closed or resolved.
- FETCH ws_market book for pm_market; FETCH external venue snapshot for ext_market.
- Compute gap_bps = (ext_price - pm_mid) * 10000 (or reverse if ext cheaper).
- Deduct estimated fees from both venues; compute net_gap_bps.
- IF net_gap_bps < hard (30 bps): SKIP, emit sampled DecisionReport CROSS_VENUE_NO_EDGE.
- IF net_gap_bps < warning (80 bps): WARN CROSS_VENUE_EDGE_MARGINAL; reduce size 50%.
- Compute order size = min(max_position_per_pair_usd, available_depth_usd).
- EMIT IOC OrderIntent: side=buy, price=best_ask_pm, size_pUSD=orderSize, builder=code.
- 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.610 — internal (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
| Code | Severity | Meaning | Action | User-facing message |
|---|
CROSS_VENUE_EDGE_TRADE | INFO | Gap >= 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_EDGE | INFO | Net 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_MARGINAL | WARN | Gap 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_MISMATCH | HARD_REJECT | Resolution sources differ between Polymarket and external venue. | Skip; no OrderIntent. | The venues resolve differently — no trade placed. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all markets; no OrderIntents emitted. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_strat_crossvenuearb_decisions_total | counter | count | verdict, reason_code, ext_venue | Total evaluation cycles by verdict, reason code, and external venue. |
polytraders_strat_crossvenuearb_gap_bps | histogram | basis_points | ext_venue | Distribution of net cross-venue gap in bps. |
polytraders_strat_crossvenuearb_intents_emitted_total | counter | count | ext_venue | Total IOC OrderIntents emitted by external venue. |
polytraders_strat_crossvenuearb_eval_latency_ms | histogram | milliseconds | | Latency from snapshot receipt to OrderIntent emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
CrossVenueArbStaleFeed | rate(polytraders_strat_crossvenuearb_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1 | warn | #runbook-crossvenuearb-stale |
CrossVenueArbKillSwitch | rate(polytraders_strat_crossvenuearb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
CrossVenueArbNoEdge | rate(polytraders_strat_crossvenuearb_decisions_total{verdict='skip',reason_code='CROSS_VENUE_NO_EDGE'}[10m]) / rate(polytraders_strat_crossvenuearb_decisions_total[10m]) > 0.95 | warn | #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
| Situation | User-facing explanation |
|---|
| Arb trade placed | A price difference was detected between Polymarket and another prediction market. An order was placed on Polymarket to capture the gap. |
| No edge after fees | The price gap between venues was too small after fees to justify a trade. |
| Source mismatch | The two venues resolve differently — no trade was placed. |
18. Failure-Mode Block
| main_failure_mode | Stale external venue snapshot: the external price ages faster than the Polymarket book, creating phantom gaps that disappear before the order lands. |
|---|
| false_positive_risk | Resolution source descriptions match textually but differ in practice, leading to a trade that is not a true arb. |
|---|
| false_negative_risk | min_gap_bps_after_fees set too high misses genuine arb opportunities on tighter markets. |
|---|
| safe_fallback | If external venue snapshot is stale (> 10s) or clob_public unavailable, emit STALE_MARKET_DATA and skip. |
|---|
| required_dependencies | ws_market, clob_public, internal cross-venue adapter, KillSwitch, internal builder code |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
STALE_EXT_SNAPSHOT | Freeze cross-venue adapter updates for > 10s | | Automatic when adapter resumes. |
SOURCE_MISMATCH | Override test pair with mismatched resolution sources | | Automatic when correct pair is reloaded. |
KILL_SWITCH_ON | Set killswitch.active=true | | Automatic 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
| Aspect | Specification |
|---|
| Execution model | actor-per-pair |
| Max in-flight | 20 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 300 |
| Backpressure strategy | drop oldest snapshot per pair_id when queue depth > 2 |
| Locking / mutual exclusion | per-pair_id mutex for snapshot state |
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)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Kalshi public API | | best-effort | |
| PredictIt market data feed | | best-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
| 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 | Bot 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
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q3-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Emit IOC when gap=165 bps, sources match, pair approved | min_gap_bps_after_fees=150 | IOC OrderIntent emitted; reason=CROSS_VENUE_EDGE_TRADE |
| Skip when source mismatch | pm_source='AP', ext_source='Reuters' | No OrderIntent; reason=CROSS_VENUE_SOURCE_MISMATCH |
| Skip when KillSwitch active | killswitch.active=true | No OrderIntents emitted |
Integration Tests
| Test | Expected result |
|---|
| Full cycle: external snapshot → gap computed → IOC OrderIntent on Polygon testnet | Order has builder.code bytes32, no feeRateBps, tif=IOC, EIP-712 domain v2 |
Property Tests
| Property | Required behaviour |
|---|
| Bot never trades when require_resolution_source_match=true and sources differ | Always true |
| feeRateBps never present on any signed OrderIntent | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate 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..
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 |