3.6 Cross-Market Arb
Cross-Market Arb detects logically constrained relationships between pairs of Polymarket binary markets — e.g. 'Candidate A wins primary' should price at or below 'Candidate A wins general', or 'Team X advances to semi-final' should price at or below 'Team X wins tournament'. When the logical constraint is violated by more than tolerance_bps (after fee buffer), the bot emits a pair of OrderIntents (one per market) to exploit the mispricing. Market pairs must pass manual review before the bot will trade them (require_manual_pair_review=true). This is a user-controlled execution tool; it does not copy trades or make performance claims.
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 | BETA |
| Readiness | Limited live |
| Runs before | Risk guardrail pipeline |
| Runs after | Market scanner / opportunity feed |
| Applies to | Manually reviewed pairs of binary markets with logical price constraints (e.g. nominee ≤ winner, team advances ≤ tournament winner) where the constraint is violated by more than tolerance_bps |
| Default mode | limited_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders core — Strategy pod |
2. Purpose
Cross-Market Arb detects logically constrained relationships between pairs of Polymarket binary markets — e.g. 'Candidate A wins primary' should price at or below 'Candidate A wins general', or 'Team X advances to semi-final' should price at or below 'Team X wins tournament'. When the logical constraint is violated by more than tolerance_bps (after fee buffer), the bot emits a pair of OrderIntents (one per market) to exploit the mispricing. Market pairs must pass manual review before the bot will trade them (require_manual_pair_review=true). This is a user-controlled execution tool; it does not copy trades or make performance claims.
3. Why This Bot Matters
Logically distinct markets paired incorrectly
A pair that looks logically constrained but has different resolution conditions may diverge legitimately. Trading a false constraint produces losses when both markets resolve consistently with their actual rules.
One market resolves while the paired order is still resting
If market A resolves YES and the paired market B order has not filled, the bot holds a one-sided position with no offsetting settlement path.
Tolerance_bps too tight on high-spread markets
The bot may fire on transient spread noise rather than genuine constraint violations, incurring fee costs with no structural edge.
feeRateBps present on signed order (V1 pattern)
CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. No leg intent may 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 bid/ask for each market in registered pairs | ws_market (CLOB WebSocket) | Yes | Monitor for constraint violations: p_market_A > p_market_B + tolerance_bps when A implies B. |
| Market metadata: condition ID, outcome token IDs, resolution rules, category | gamma | Yes | Validate that pair logic and resolution conditions align before registering a market pair. |
| Market open/closed/resolved status | clob_public | Yes | Skip pairs where either market is closed or resolved; cancel resting legs immediately. |
| Top-of-book depth on both markets | clob_public | Yes | Size each leg to min(depth, per-leg budget from liquidity budget). |
| Embedding-based similarity score (e5-large-v2 or similar) | internal | No | Cross-check that proposed pair markets are semantically related; flag low-similarity pairs for human review. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| KillSwitch active flag | KillSwitch | Yes | Abort all intent emission immediately if KillSwitch is active. |
| Registered market-pair registry (manually reviewed) | internal config | Yes | Bot only evaluates pairs in the approved registry. Pairs require human review before entry. |
| Builder code bytes32 | internal config | Yes | Injected into builder field on every signed V2 OrderIntent. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| tolerance_bps | 30 | 15 | 5 | Minimum basis-point violation of the logical constraint (after fee buffer) required before emitting OrderIntents. |
| min_similarity_score | 0.82 | 0.7 | 0.6 | Minimum embedding-based similarity score (0–1) between the two market questions before the pair is eligible for auto-registration. |
| require_manual_pair_review | True | None | None | When true, every new market pair must be manually reviewed and approved before the bot will trade it. Cannot be set to false in production. |
| max_legs | 2 | 3 | 4 | Maximum number of simultaneous legs per detected constraint violation. Standard cross-market arb requires exactly 2 legs. |
7. Detailed Parameter Instructions
tolerance_bps
What it means
Minimum basis-point violation of the logical constraint (after fee buffer) required before emitting OrderIntents.
Default
{ "tolerance_bps": 30 }
Why this default matters
30 bps covers fee drag and expected slippage on a typical Polymarket binary pair. Below 15 bps the violation may be transient noise; below 5 bps the bot will not fire.
Threshold logic
| Condition | Action |
|---|---|
| >= 30 bps | EMIT pair OrderIntents |
| 15–30 bps | WARN CROSS_MARKET_ARB_VIOLATION_MARGINAL; emit at 50% size |
| < 5 bps (hard floor) | SKIP — CROSS_MARKET_ARB_NO_EDGE |
Developer check
if violation_bps < params.hard: return skip('CROSS_MARKET_ARB_NO_EDGE')
User-facing English
The pricing inconsistency between linked markets was too small to trade profitably after fees.
min_similarity_score
What it means
Minimum embedding-based similarity score (0–1) between the two market questions before the pair is eligible for auto-registration.
Default
{ "min_similarity_score": 0.82 }
Why this default matters
0.82 ensures markets are semantically related enough to have a meaningful logical constraint. Below 0.70 pairs are likely comparing unrelated events.
Threshold logic
| Condition | Action |
|---|---|
| >= 0.82 | Pair eligible; still requires manual review |
| 0.70–0.82 | WARN CROSS_MARKET_ARB_LOW_SIMILARITY; manual review mandatory before any trade |
| < 0.60 | Hard floor; pair rejected from registry |
Developer check
if similarity < params.hard: raise ConfigError('CROSS_MARKET_ARB_LOW_SIMILARITY')
User-facing English
— not yet authored —
require_manual_pair_review
What it means
When true, every new market pair must be manually reviewed and approved before the bot will trade it. Cannot be set to false in production.
Default
{ "require_manual_pair_review": true }
Why this default matters
Prevents the bot from auto-discovering and trading logically incorrect pairs that could produce losses.
Threshold logic
| Condition | Action |
|---|---|
| true | Normal; all pairs require manual approval |
| false | Not permitted in production; config rejected |
Developer check
assert params.require_manual_pair_review == True
User-facing English
— not yet authored —
max_legs
What it means
Maximum number of simultaneous legs per detected constraint violation. Standard cross-market arb requires exactly 2 legs.
Default
{ "max_legs": 2 }
Why this default matters
2 legs covers the standard binary pair. Values > 2 are only valid for chained logical constraints (A implies B implies C).
Threshold logic
| Condition | Action |
|---|---|
| == 2 | Normal paired trade |
| 3–4 | WARN; chained constraint mode; each additional leg increases execution risk |
| > 4 | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.max_legs > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
The trade was placed across the two linked markets simultaneously.
8. Default Configuration
{
"bot_id": "strat.cross_market_arb",
"version": "2.1.0",
"mode": "limited_live",
"defaults": {
"tolerance_bps": 30,
"min_similarity_score": 0.82,
"require_manual_pair_review": true,
"max_legs": 2
},
"locked": {
"tolerance_bps": {
"min": 5
},
"min_similarity_score": {
"min": 0.6
},
"require_manual_pair_review": {
"value": true
},
"max_legs": {
"max": 4
}
}
}9. Implementation Flow
- Check KillSwitch active flag; if active, emit no OrderIntents.
- Load registered market-pair registry (manually reviewed pairs only).
- Subscribe to ws_market book updates for all markets in active pairs.
- On each book tick: retrieve the latest best-ask for each market in the pair.
- Validate book freshness (last_seen < 5s); if stale, emit STALE_MARKET_DATA and skip.
- Confirm both markets in the pair are open and not resolved (clob_public).
- Compute violation_bps = (p_market_A - p_market_B) * 10000 for pairs where A logically implies B.
- If violation_bps < tolerance_bps hard floor (5 bps), emit sampled DecisionReport CROSS_MARKET_ARB_NO_EDGE and skip.
- If violation_bps < warning threshold (30 bps), WARN CROSS_MARKET_ARB_VIOLATION_MARGINAL; reduce leg sizes by 50%.
- Fetch top-of-book depth for both markets; set legSize = min(depth_A, depth_B, per-leg budget).
- Emit OrderIntent for market A (sell YES / buy NO — the overpriced side).
- Emit OrderIntent for market B (buy YES — the underpriced side).
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT on any signed order.
- Emit DecisionReport with intent_emitted=true, violation_bps, pair_id, reason CROSS_MARKET_ARB_EDGE_DETECTED.
10. Reference Implementation
Subscribes to CLOB WebSocket book updates for registered market pairs, checks for logical-constraint violations above tolerance_bps, and emits FOK OrderIntents on both legs when a violation is confirmed.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
FUNCTION evaluatePair(pair_id, bookTick):
// --- 0. KillSwitch gate ---
ks = FETCH internal.killswitch.status
IF ks.active: RETURN
// --- 1. Registry check ---
pair = FETCH internal.pairRegistry.get(pair_id)
IF NOT pair OR NOT pair.manually_approved:
EMIT DecisionReport(intent_emitted=false, reason='CROSS_MARKET_ARB_PAIR_NOT_APPROVED')
RETURN
// --- 2. Staleness check ---
IF isStale(bookTick, maxAgeS=5):
EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
RETURN
// --- 3. Market status check ---
mktA = FETCH clob_public.GET('/markets/' + pair.market_id_A)
mktB = FETCH clob_public.GET('/markets/' + pair.market_id_B)
IF mktA.closed OR mktA.resolved OR mktB.closed OR mktB.resolved:
CANCEL_RESTING_LEGS(pair_id)
EMIT DecisionReport(intent_emitted=false, reason='MARKET_CLOSED')
RETURN
// --- 4. Constraint violation check (A implies B, so p_A should <= p_B) ---
p_A = bookTick.market_A.best_ask
p_B = bookTick.market_B.best_ask
violation_bps = (p_A - p_B) * 10000
// --- 5. Hard floor ---
IF violation_bps < params.tolerance_bps_hard: // 5 bps
IF random() < 0.01:
EMIT DecisionReport(intent_emitted=false, reason='CROSS_MARKET_ARB_NO_EDGE', violation_bps=violation_bps)
RETURN
// --- 6. Warning threshold ---
sizeMultiplier = 1.0
IF violation_bps < params.tolerance_bps: // 30 bps default
WARN('CROSS_MARKET_ARB_VIOLATION_MARGINAL')
sizeMultiplier = 0.5
// --- 7. Size and emit legs ---
depthA = FETCH clob_public.depth(pair.market_id_A)
depthB = FETCH clob_public.depth(pair.market_id_B)
legSize = toPusdUnits(min(depthA, depthB, legBudget) * sizeMultiplier)
// Sell the overpriced side (market A)
EMIT OrderIntent(market_id=pair.market_id_A, outcome='YES', side='sell',
price=p_A, size_pUSD=legSize, tif='FOK',
builder={code: internal.builder_code, fee_bps: 25})
// Buy the underpriced side (market B)
EMIT OrderIntent(market_id=pair.market_id_B, outcome='YES', side='buy',
price=p_B, size_pUSD=legSize, tif='FOK',
builder={code: internal.builder_code, fee_bps: 25})
EMIT DecisionReport(intent_emitted=true, violation_bps=violation_bps,
pair_id=pair_id, reason='CROSS_MARKET_ARB_EDGE_DETECTED')
SDK calls used
fetchClobPublic('/markets/' + market_id)ws_market.subscribe('book', [market_id_A, market_id_B])toPusdUnits(rawFloat)buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })internal.killswitch.status()internal.pairRegistry.get(pair_id)internal.builder_code
Complexity: O(1) per market pair per book tick
11. Wire Examples
Input — what arrives on the wire
Pair book tick — constraint violation detected (45 bps) — ws_market
{
"pair_id": "pair_nominee_vs_winner_0042",
"market_id_A": "0xcrossma000000000000000000000000000000000000000000000000000000001b",
"market_id_B": "0xcrossma000000000000000000000000000000000000000000000000000000001a",
"p_A_best_ask": "0.455",
"p_B_best_ask": "0.410",
"violation_bps": "45.0",
"depth_A_pusd": "380.00",
"depth_B_pusd": "290.00",
"received_at_ms": 1746790200000
}
Output — what the bot emits
OrderIntent — market B leg (buy, FOK, builder-attributed)
{
"intent_id": "oi_01HXCMA00001AA",
"trace_id": "tr_01HXCMA0000TR1",
"market_id": "0xcrossma000000000000000000000000000000000000000000000000000000001a",
"outcome": "YES",
"side": "buy",
"price": "0.410",
"size_pUSD": "250.00",
"tif": "FOK",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"pair_id": "pair_nominee_vs_winner_0042",
"violation_bps": 45.0,
"leg": "underpriced_B",
"reasons": [
"CROSS_MARKET_ARB_EDGE_DETECTED"
]
},
"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_01HXCMA999ZZZ",
"bot_id": "strat.cross_market_arb",
"pair_id": "pair_nominee_vs_winner_0042",
"intent_emitted": false,
"violation_bps": 2.1,
"reasons": [
"CROSS_MARKET_ARB_NO_EDGE"
],
"sampled": true,
"evaluated_at_ms": 1746790201000
}12. Decision Logic
APPROVE
violation_bps >= tolerance_bps, both markets open, both legs have depth, pair is manually approved, KillSwitch inactive. Emit two paired OrderIntents (FOK).
RESHAPE_REQUIRED
Not applicable — strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.
REJECT
violation_bps < 5 bps hard floor; either market closed/resolved; pair not in approved registry; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.
WARNING_ONLY
violation_bps between 5 and 30 bps triggers CROSS_MARKET_ARB_VIOLATION_MARGINAL and 50% size reduction.
13. Standard Decision Output
This bot returns a OrderIntent object. See OrderIntent schema.
{
"intent_id": "oi_01HXCMA00001AA",
"trace_id": "tr_01HXCMA0000TR1",
"market_id": "0xcrossma000000000000000000000000000000000000000000000000000000001a",
"outcome": "YES",
"side": "buy",
"price": "0.410",
"size_pUSD": "250.00",
"tif": "FOK",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"pair_id": "pair_nominee_vs_winner_0042",
"violation_bps": 45.0,
"leg": "underpriced_B",
"reasons": [
"CROSS_MARKET_ARB_EDGE_DETECTED"
]
},
"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_MARKET_ARB_EDGE_DETECTED | INFO | Logical constraint violation exceeds tolerance_bps after fee buffer. Paired OrderIntents emitted. | Emit two FOK OrderIntents (one per market in the pair). | A pricing inconsistency between two related markets was detected and orders were placed to capture it. |
CROSS_MARKET_ARB_NO_EDGE | INFO | Violation is below the 5 bps hard floor. No trade opportunity exists after fees. | Skip; emit sampled DecisionReport. | The pricing gap between linked markets was too small to trade profitably after fees. |
CROSS_MARKET_ARB_VIOLATION_MARGINAL | WARN | Violation is between 5 and 30 bps (warning threshold). Trade is marginal. | Emit OrderIntents at 50% leg size; log warning. | A small pricing inconsistency was detected. Order sizes were reduced. |
CROSS_MARKET_ARB_PAIR_NOT_APPROVED | HARD_REJECT | Market pair is not in the manually reviewed approved registry. | Skip; no OrderIntents emitted. | |
CROSS_MARKET_ARB_LOW_SIMILARITY | WARN | Embedding similarity score for the proposed pair is below min_similarity_score warning threshold. | Block pair auto-registration; require additional manual review. | |
STALE_MARKET_DATA | HARD_REJECT | Book snapshot older than 5s for either market in the pair. | Skip; no OrderIntents; cancel resting legs if any. | Market data was too old to act on safely. |
MARKET_CLOSED | HARD_REJECT | Either market in the pair is closed or resolved. | Skip; cancel resting legs; deactivate pair until re-reviewed. | One of the linked markets has closed. No new orders will be placed. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all pairs; 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. | Reject config change; do not apply. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_crossmarketarb_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles by intent_emitted (true/false) and reason code. |
polytraders_strat_crossmarketarb_violation_bps | histogram | basis_points | pair_id | Distribution of measured constraint violations in bps per pair. |
polytraders_strat_crossmarketarb_intents_emitted_total | counter | count | pair_id, leg | Total OrderIntents emitted by pair and leg (A/B). |
polytraders_strat_crossmarketarb_eval_latency_ms | histogram | milliseconds | Wall-clock time from pair book tick to OrderIntent emit. | |
polytraders_strat_crossmarketarb_stale_feed_total | counter | count | Evaluation cycles skipped due to stale feed on either market in a pair. | |
polytraders_strat_crossmarketarb_active_pairs | gauge | count | Number of currently active (manually approved) market pairs. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
CrossMarketArbHighStaleFeed | rate(polytraders_strat_crossmarketarb_stale_feed_total[5m]) > 0.1 | warn | #runbook-crossmarketarb-stale-feed |
CrossMarketArbNoEdgeStreak | rate(polytraders_strat_crossmarketarb_decisions_total{verdict='false'}[15m]) / rate(polytraders_strat_crossmarketarb_decisions_total[15m]) > 0.99 | warn | #runbook-crossmarketarb-no-edge |
CrossMarketArbHighLatency | histogram_quantile(0.99, rate(polytraders_strat_crossmarketarb_eval_latency_ms_bucket[5m])) > 200 | warn | #runbook-crossmarketarb-latency |
CrossMarketArbKillSwitchBlocking | rate(polytraders_strat_crossmarketarb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
Dashboards
- Grafana — Strategy / CrossMarketArb violation distribution per pair
- Grafana — Strategy / CrossMarketArb active pairs and intent throughput
16. Developer Reporting
{
"bot_id": "strat.cross_market_arb",
"pair_id": "pair_nominee_vs_winner_0042",
"market_A": "0xcrossma000000000000000000000000000000000000000000000000000000001b",
"market_B": "0xcrossma000000000000000000000000000000000000000000000000000000001a",
"p_A": 0.455,
"p_B": 0.41,
"violation_bps": 45.0,
"leg_size_pusd": 250.0,
"intent_emitted": true,
"reason": "CROSS_MARKET_ARB_EDGE_DETECTED",
"emitted_at_ms": 1746790200000
}17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| Cross-market arb trade initiated | Two related markets were priced inconsistently: one was priced higher than the other in a way that contradicts their logical relationship. Orders were placed to exploit the pricing gap. |
| No violation — no trade | The pricing between the two linked markets was consistent after accounting for fees. No order was placed. |
| Violation marginal — reduced size | A small pricing inconsistency between related markets was detected. Order sizes were halved to limit exposure given the thin margin. |
| Market resolved — pair deactivated | One of the two linked markets has resolved. The pair has been deactivated and no new orders will be placed. |
18. Failure-Mode Block
| main_failure_mode | One market in the pair resolves while the second leg order is resting, leaving a one-sided position with no offsetting settlement path. |
|---|---|
| false_positive_risk | Stale WebSocket book data shows a violation 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 | tolerance_bps set too conservatively on low-fee market pairs causes valid constraint violations to be skipped. |
| safe_fallback | If either market's book data is stale (last_seen > 5s) or either market resolves, emit STALE_MARKET_DATA DecisionReport and skip. Cancel any resting legs on resolved markets immediately. |
| required_dependencies | ws_market book stream (both market IDs in each active pair), clob_public market endpoint (status + depth), internal market-pair registry (manually reviewed), KillSwitch active flag, internal builder code |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
STALE_WS_FEED | Pause ws_market WebSocket for market_A; let last_seen age beyond 5s | Automatic on WebSocket reconnect. | |
MARKET_RESOLVES_MID_TRADE | Set mock market_B.resolved=true after market_A leg is emitted | Manual pair re-review required before re-activation. | |
VIOLATION_BELOW_HARD_FLOOR | Set p_A=0.413, p_B=0.411 (violation=2 bps) | Automatic when violation widens. | |
KILL_SWITCH_ON | Set killswitch.active=true | Automatic on manual KillSwitch reset. | |
UNAPPROVED_PAIR_INJECTION | Add a pair directly to registry without setting manually_approved=true | Manual approval required. |
20. State & Persistence
Cold-start recovery
On cold start, resting leg IDs re-fetched from clob_auth. Book state rebuilt from first ws_market tick per market.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | actor-per-market |
| Max in-flight | 20 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 200 |
| Backpressure strategy | drop oldest pending tick per pair_id when queue depth > 5 |
| Locking / mutual exclusion | per-pair_id mutex for Redis state write and resting leg tracking |
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 | ||
| Polymarket Gamma API | 99.9% (Polymarket-published) |
23. Security Surfaces
On-chain contract calls
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon |
Abuse vectors considered
- Adversary manufactures a pair violation by moving one market via a large spoof order
- Replaying a signed OrderIntent after the constraint violation has closed
- Injecting a new pair into the registry by bypassing the manual review gate
Mitigations
- All legs use FOK; partial fills are rejected rather than leaving naked positions
- V2 order timestamp(ms) invalidates replays outside the exchange acceptance window
- Pair registry mutations require operator authentication and manual approval flag
- Book staleness check (5s) prevents acting on adversarially manipulated prices
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 | Operates on linked binary market pairs. feeRateBps is not present on any signed order; fees are operator-set at match time. Market-pair relationships are reviewed and registered manually before deployment. |
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 all signed multi-market order construction. Updated collateral denomination to pUSD. Injected builder field (bytes32) on every OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. Market-pair registry updated to use Gamma API for metadata and condition-ID resolution. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Emit pair when violation_bps=45 (above threshold 30) | p_A=0.455, p_B=0.410, tolerance_bps=30, pair manually approved | Two OrderIntents emitted; DecisionReport intent_emitted=true, reason=CROSS_MARKET_ARB_EDGE_DETECTED |
| Skip when violation_bps=3 (below hard floor 5) | p_A=0.502, p_B=0.499 | No OrderIntents; sampled DecisionReport reason=CROSS_MARKET_ARB_NO_EDGE |
| Reduce sizes 50% when violation is marginal (18 bps) | violation_bps=18, tolerance_bps=30 | OrderIntents emitted at 50% size; WARN CROSS_MARKET_ARB_VIOLATION_MARGINAL |
| Reject unreviewed pair | pair not in approved registry | No OrderIntents; CROSS_MARKET_ARB_PAIR_NOT_APPROVED |
| Skip when either market closed | market_B.status=resolved | No OrderIntents; reason=MARKET_CLOSED; resting leg on market_A cancelled |
| Skip when KillSwitch active | killswitch.active=true | No OrderIntents emitted |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: ws_market pair tick → violation detected → two signed V2 FOK OrderIntents submitted | Both orders have builder.code (bytes32), no feeRateBps, EIP-712 domain version '2' |
| Market resolution mid-trade triggers immediate leg cancellation | Resting leg cancelled; MARKET_CLOSED DecisionReport emitted; no new intents for pair |
Property Tests
| Property | Required behaviour |
|---|---|
| Bot never trades a pair not in the manually reviewed approved registry | Always true |
| feeRateBps is never present on any signed OrderIntent | Always true — V2 fees are operator-set at match time |
| require_manual_pair_review is always true in production config | Always true |
27. Operational Runbook
Cross-Market Arb incidents are typically stale feeds, market resolutions mid-trade (requiring manual pair review), or false constraint violations from thin-book manipulation. Stale feed incidents resolve automatically; market resolution events require ops review.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
CrossMarketArbHighStaleFeed | ||||
CrossMarketArbNoEdgeStreak | ||||
CrossMarketArbHighLatency | ||||
CrossMarketArbKillSwitchBlocking |
Manual overrides
——
Healthcheck
GET /internal/health/cross-market-arb -> 200 if ws_market feed last_seen < 5s for all active pair markets, Redis reachable, KillSwitch inactive, at least one pair 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-approval invariant and FOK-only property | CI test run | 100% pass |
| feeRateBps absence verified; require_manual_pair_review=true enforced in integration test | Integration test asserting V2 order schema | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| p99 eval latency < 200ms over 24h | polytraders_strat_crossmarketarb_eval_latency_ms histogram | p99 < 200ms |
| Zero unreviewed pair trades in 48h shadow run | Pair approval audit log | 0 incidents |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: pair violation detected → two signed V2 FOK OrderIntents submitted on Polygon testnet | E2E test | Pass |
| Market resolution mid-trade correctly triggers leg cancellation in integration test | 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 |