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 LayerRisk1.19 SelfTradeWashGuard

1.19 SelfTradeWashGuard

Risk Guardrail RejectDownsize PLANNED Spec ready capital · Direct P4 · Core risk pending stub

Prevents Polytraders from trading against itself. If an outgoing OrderIntent would cross with one of our own resting orders on the same market and outcome, SelfTradeWashGuard rejects it (full overlap) or downsizes it (partial overlap). This protects against wash-trade exposure and inadvertent self-fills.

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

LayerRisk  Risk
Bot classGuardrail
AuthorityRejectDownsize
StatusPLANNED
ReadinessSpec ready
Runs beforeexec.smart_router
Runs afterexec.order_lifecycle_manager
Applies toPer OrderIntent
Default modeshadow
User-visibleYes
Developer ownerRisk pod

Operational profile

OwnershipRisk pod · on-call risk-oncall · #polytraders-risk · escalates to Head of Risk · P1
Latency budgetp50: 3ms · p99: 12ms
Modes supportedoffshadowadvisoryenforcedquarantine
Data freshnessmax_market_data_age_ms=2000 · max_orderbook_age_ms=2000 · on stale → REJECT.
Human overrideno · by · logs · time-bound: — · scope: — · single approver

2. Purpose

Prevents Polytraders from trading against itself. If an outgoing OrderIntent would cross with one of our own resting orders on the same market and outcome, SelfTradeWashGuard rejects it (full overlap) or downsizes it (partial overlap). This protects against wash-trade exposure and inadvertent self-fills.

3. Why This Bot Matters

  • Wash-trade liability

    Crossing your own orders is treated as wash trading by most regulators and by Polymarket's own terms; even unintentional self-trades are a compliance risk.

  • Inventory churn

    Self-fills move money from one account to another (or from one strategy book to another) while paying maker+taker fees both sides — pure deadweight loss.

  • Strategy interference

    Two Polytraders strategies disagreeing on direction should not silently fund each other's positions through self-fills.

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
Outgoing OrderIntent (price/size/side/market_id/outcome)Strategy botYesThe order under review.
Resting orders snapshot (ours)OrderLifecycleManagerYesAll currently-resting Polytraders orders, indexed by (market_id, outcome_id, side).

5. Required Internal Inputs

InputSourceRequired?Use
OrderLifecycleManager stateexec.order_lifecycle_managerYesSource of truth for our own resting book.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
modedownsizeHow to handle a detected self-cross: REJECT outright or DOWNSIZE to the non-overlapping remainder.
tolerance_bps0510Tick-rounding tolerance when comparing prices (in basis points).

7. Detailed Parameter Instructions

mode

What it means

How to handle a detected self-cross: REJECT outright or DOWNSIZE to the non-overlapping remainder.

Default

{ "mode": "downsize" }

Why this default matters

Downsize is preferred — it preserves the legitimate part of the order while eliminating the self-trade.

Threshold logic

ConditionAction
rejectWhole order rejected on any overlap
downsizeOrder shrunk to non-overlapping size; rejected if remainder is below the bot's min_size_usd

Developer check

if (mode == 'reject' && overlap > 0) reject(); else if (overlap > 0) downsize(intent.size - overlap);

User-facing English

We trimmed your order so it would not trade against another order from this account.

tolerance_bps

What it means

Tick-rounding tolerance when comparing prices (in basis points).

Default

{ "tolerance_bps": 0 }

Why this default matters

Polymarket V2 uses fixed tick sizes (0.001), so 0 bps tolerance is correct. Non-zero only useful for legacy markets.

Threshold logic

ConditionAction
0 bpsExact price match required

Developer check

if (priceMatchWithin(p.tolerance_bps, mine.price, intent.price)) recordOverlap();

User-facing English

(Internal — not shown to users.)

8. Default Configuration

{
  "mode": "downsize",
  "tolerance_bps": 0
}

9. Implementation Flow

— not yet authored —

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.

ours = lifecycle.resting_orders(intent.market_id, intent.outcome_id, opposite(intent.side))
overlap = sum(o.size for o in ours if would_cross(o.price, intent.price, intent.side))
if overlap >= intent.size: return reject('RISK_SELF_TRADE')
if overlap > 0:
  if p.mode == 'reject': return reject('RISK_SELF_TRADE')
  return downsize(intent.size - overlap, 'RISK_SELF_TRADE')
return pass_()

11. Wire Examples

Input — what arrives on the wire

{
  "intent_id": "intent_003",
  "market_id": "0xabc",
  "outcome_id": "YES",
  "side": "SELL",
  "size_usd": 100,
  "price": 0.55
}

Output — what the bot emits

{
  "vote": "DOWNSIZE",
  "reason_code": "RISK_SELF_TRADE",
  "suggested_size_usd": 60,
  "overlap_usd": 40
}

12. Decision Logic

APPROVE

Find resting orders on opposite side for same (market_id, outcome_id). Filter to prices that would actually cross with intent.price + intent.side. Sum overlapping size in USD.

RESHAPE_REQUIRED

This bot does not reshape orders.

REJECT

Apply mode (reject vs downsize) and minimum-remainder check.

WARNING_ONLY

No warn-only path defined.

13. Standard Decision Output

This bot returns a RiskVote object. See RiskVote schema.

{
  "vote": "DOWNSIZE",
  "reason_code": "RISK_SELF_TRADE",
  "suggested_size_usd": 60,
  "overlap_usd": 40,
  "explain": "Crossing $40 of our own resting orders; downsized to $60 remainder."
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
RISK_SELF_TRADEP1Risk Self TradeSee decision output and developer log for context.We trimmed (or rejected) your order so it would not trade against another order from this account.
RISK_SELF_TRADE_DOWNSIZEDP1Risk Self Trade DownsizedSee decision output and developer log for context.We trimmed (or rejected) your order so it would not trade against another order from this account.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
self_trade_rejects_totalcountereventmarket_id, reason_codeSelf trade rejects total.
self_trade_downsizes_totalcountereventbot_idSelf trade downsizes total.
self_trade_overlap_usd_histogramcountereventbot_idSelf trade overlap usd histogram.

Dashboards

  • 1.19 overview dashboard

16. Developer Reporting

"Per decision: intent_id, market_id, outcome_id, side, intent.price, intent.size_usd, overlap_usd, vote, mode, suggested_size_usd."

17. Plain-English Reporting

SituationUser-facing explanation
When this bot actsWe trimmed (or rejected) your order so it would not trade against another order from this account.

18. Failure-Mode Block

main_failure_modeStale resting-orders view misses an overlap that just got placed.
false_positive_riskResting view includes orders that are about to cancel; mitigation: only count orders whose status is OPEN or PARTIALLY_FILLED at the moment of the check.
false_negative_riskA new resting order placed in the same millisecond as the OrderIntent is not yet visible; mitigation: treat the OrderLifecycleManager's view as eventually consistent and rely on the exchange-side self-trade prevention as a backstop.
safe_fallbackIf the resting-orders view is unavailable, REJECT — never assume there is no overlap.
required_dependencies

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
Inject 50% overlap and assert DOWNSIZEInject 50% overlap and assert DOWNSIZE.Bot detects within its latency budget and emits the corresponding reason code.Remove the injected fault; bot returns to healthy state within one debounce window.
Disconnect OrderLifecycleManager view and assert all OrderIntents are REJECTEDDisconnect OrderLifecycleManager view and assert all OrderIntents are REJECTED.Bot detects within its latency budget and emits the corresponding reason code.Remove the injected fault; bot returns to healthy state within one debounce window.
Race condition: submit two intents within 1ms and assert at most one self-fillRace condition: submit two intents within 1ms and assert at most one self-fill.Bot detects within its latency budget and emits the corresponding reason code.Remove the injected fault; bot returns to healthy state within one debounce window.

20. State & Persistence

Stateless within the bot; reads OrderLifecycleManager state.

State stores

NameKindKeyValue shapeTTLDurability
self_trade_wash_guard_statein-memory + fast KV mirrorbot_idStateless within the bot; reads OrderLifecycleManager state.24hcrash-safe via KV mirror

Cold-start recovery

Cold-start hydrates from fast KV; missing keys default to safe fallback.

On restart

All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit.

21. Concurrency & Idempotency

AspectSpecification
Execution modelRead-after-write hazard with very-recent OrderLifecycleManager updates; bot accepts eventual consistency.
Max in-flight32
Idempotency keyorder_intent_id
Replay-safeTrue
DeduplicationBy idempotency_key within a 60s window.
Ordering guaranteesPer-market_id FIFO; cross-market unordered.
Per-call timeout (ms)250
Backpressure strategyBounded queue; oldest-dropped with metric increment when full.
Locking / mutual exclusionPer-market_id mutex; no global locks.

22. Dependencies

Depends on (must run first)

Emits to (downstream consumers)

BotWhyContract
exec.smart_router

Requires (graph.requires)

exec.order_lifecycle_manager

Required before (graph.required_before)

exec.smart_router

ConsumesOrderIntent RestingOrdersView
EmitsRiskVote
Blocks ordersyes

23. Security Surfaces

Internal-only. No external endpoints.

Signing surface

None — bot does not sign or submit.

Mitigations

  • Rate-limit per source
  • Audit-log every override
  • Require role-based authz on admin paths

24. Polymarket V2 Compatibility

AspectValue
CLOB versionV2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsyes
Multi-chain readyyes
SDK usedPolymarket CLOB V2 SDK
Settlement contractCTFExchangeV2
NotesOperates on V2 OrderIntent and resting-orders state. Polymarket V2 also enforces server-side self-trade prevention as a backstop.

25. Versioning & Migration

FieldValue
current0.1.0
contract_version1.0.0
last_breaking_changenone
deprecation_window_days30

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Full overlap → REJECT.Synthetic fixture per template.Behaviour matches the rule described in the test name.
50% overlap → DOWNSIZE to 50%.Synthetic fixture per template.Behaviour matches the rule described in the test name.
Zero overlap → PASS.Synthetic fixture per template.Behaviour matches the rule described in the test name.
Overlap > intent.size → REJECT (not negative size).Synthetic fixture per template.Behaviour matches the rule described in the test name.

Integration Tests

TestExpected result
Place a resting BUY at 0.55, then submit a SELL at 0.55 → DOWNSIZE.End-to-end behaviour matches the spec without manual intervention.

Property Tests

PropertyRequired behaviour
For any (intent.size, overlap), the suggested_size_usd is in [0, intent.size].Always true across all generated inputs.

27. Operational Runbook

If downsize rate is high, multiple strategies are likely fighting on the same market — review strategy allocation, do not silence the guard.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
1.19_anomalyOpen the bot's reporting page and confirm the alert is real (not a metric hiccup).Inspect developer log entries for the affected market_id over the last 30 minutes.Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner.Risk pod

Manual overrides

  • polytraders bot pause 1.19 — Disables the bot's enforcement layer; downstream consumers fall back to safe defaults.

Healthcheck

GET /healthz/self_trade_wash_guard → 200 if last successful evaluation < 60s ago.

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
Stubdeterministic against synthetic resting state.Documented threshold met for the full window.

Promote to Limited live

GateHow measuredThreshold
Shadow14 days; downsize/reject events tracked but not enforced.Documented threshold met for the full window.
Advisory7 days.Documented threshold met for the full window.

Promote to General live

GateHow measuredThreshold
EnforcedRisk Lead sign-off + compliance review.Documented threshold met for the full window.

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