1. Bot Identity
| Layer | Risk Risk |
|---|
| Bot class | Guardrail |
|---|
| Authority | RejectDownsize |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec ready |
|---|
| Runs before | exec.smart_router |
|---|
| Runs after | exec.order_lifecycle_manager |
|---|
| Applies to | Per OrderIntent |
|---|
| Default mode | shadow |
|---|
| User-visible | Yes |
|---|
| Developer owner | Risk pod |
|---|
Operational profile
| Ownership | Risk pod · on-call risk-oncall · #polytraders-risk · escalates to Head of Risk · P1 |
|---|
| Latency budget | p50: 3ms · p99: 12ms |
|---|
| Modes supported | offshadowadvisoryenforcedquarantine |
|---|
| Data freshness | max_market_data_age_ms=2000 · max_orderbook_age_ms=2000 · on stale → REJECT. |
|---|
| Human override | no · 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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| mode | downsize | — | — | How to handle a detected self-cross: REJECT outright or DOWNSIZE to the non-overlapping remainder. |
| tolerance_bps | 0 | 5 | 10 | Tick-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
| Condition | Action |
|---|
| reject | Whole order rejected on any overlap |
| downsize | Order 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
| Condition | Action |
|---|
| 0 bps | Exact 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
| Code | Severity | Meaning | Action | User-facing message |
|---|
RISK_SELF_TRADE | P1 | Risk Self Trade | See 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_DOWNSIZED | P1 | Risk Self Trade Downsized | See 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
| Metric | Type | Unit | Labels | Meaning |
|---|
self_trade_rejects_total | counter | event | market_id, reason_code | Self trade rejects total. |
self_trade_downsizes_total | counter | event | bot_id | Self trade downsizes total. |
self_trade_overlap_usd_histogram | counter | event | bot_id | Self trade overlap usd histogram. |
Dashboards
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
| Situation | User-facing explanation |
|---|
| When this bot acts | We trimmed (or rejected) your order so it would not trade against another order from this account. |
18. Failure-Mode Block
| main_failure_mode | Stale resting-orders view misses an overlap that just got placed. |
|---|
| false_positive_risk | Resting 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_risk | A 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_fallback | If the resting-orders view is unavailable, REJECT — never assume there is no overlap. |
|---|
| required_dependencies | — |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
Inject 50% overlap and assert DOWNSIZE | Inject 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 REJECTED | Disconnect 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-fill | Race 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
| Name | Kind | Key | Value shape | TTL | Durability |
|---|
self_trade_wash_guard_state | in-memory + fast KV mirror | bot_id | Stateless within the bot; reads OrderLifecycleManager state. | 24h | crash-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
| Aspect | Specification |
|---|
| Execution model | Read-after-write hazard with very-recent OrderLifecycleManager updates; bot accepts eventual consistency. |
| Max in-flight | 32 |
| Idempotency key | order_intent_id |
| Replay-safe | True |
| Deduplication | By idempotency_key within a 60s window. |
| Ordering guarantees | Per-market_id FIFO; cross-market unordered. |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | Bounded queue; oldest-dropped with metric increment when full. |
| Locking / mutual exclusion | Per-market_id mutex; no global locks. |
22. Dependencies
Depends on (must run first)
Emits to (downstream consumers)
Requires (graph.requires)
exec.order_lifecycle_manager
Required before (graph.required_before)
exec.smart_router
| Consumes | OrderIntent RestingOrdersView |
|---|
| Emits | RiskVote |
|---|
| Blocks orders | yes |
|---|
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
| Aspect | Value |
|---|
| CLOB version | V2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | yes |
| SDK used | Polymarket CLOB V2 SDK |
| Settlement contract | CTFExchangeV2 |
| Notes | Operates on V2 OrderIntent and resting-orders state. Polymarket V2 also enforces server-side self-trade prevention as a backstop. |
25. Versioning & Migration
| Field | Value |
|---|
| current | 0.1.0 |
| contract_version | 1.0.0 |
| last_breaking_change | none |
| deprecation_window_days | 30 |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected 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
| Test | Expected 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
| Property | Required 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
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
1.19_anomaly | Open 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.
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 |