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.4 LiquidityGuard

1.4 LiquidityGuard

Risk Guardrail RejectReshape LIVE General live capital · Direct P4 · Core risk pending reference bot

LiquidityGuard prevents strategies from placing orders that would consume too much of the visible order-book depth on a given market. It checks book depth, spread, and top-of-book freshness on every OrderIntent and either approves, downsizes, or rejects the order. It cannot change the market, the direction, or the strategy intent — only the size and the timing of execution.

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
AuthorityRejectReshape
StatusLIVE
ReadinessGeneral live
Runs beforeExecutionPlan emit
Runs afterStrategy OrderIntent
Applies toEvery OrderIntent before it reaches the execution layer
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Risk pod

Operational profile

Modes supportedquarantine

2. Purpose

LiquidityGuard prevents strategies from placing orders that would consume too much of the visible order-book depth on a given market. It checks book depth, spread, and top-of-book freshness on every OrderIntent and either approves, downsizes, or rejects the order. It cannot change the market, the direction, or the strategy intent — only the size and the timing of execution.

3. Why This Bot Matters

  • Thin-book consumption

    An oversized order eats through the visible top-of-book and walks the price several ticks against the user before fully filling, resulting in worse-than-expected execution.

  • Stale book approved

    If depth data is not refreshed, the system may believe there is enough liquidity when the book has thinned out since the last snapshot, leading to surprise price impact.

  • Excessive spread ignored

    Trading into a wide spread means crossing more slippage than the strategy priced in, which can turn a positive-expected-value order into a losing one.

  • No size floor on top-of-book

    An order placed on a market with near-zero resting size can move the price dramatically even for small notional amounts.

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
CLOB order book — top 50 levels (bid and ask)CLOBYesCompute total visible USD depth and the inside spread; compare against thresholds.
Best-bid / best-ask resting sizeWebSocketYesDetermine how large the top-of-book is to enforce min_top_of_book_usd.
30-day median spread for the target marketData APIYesCalculate the spread multiple: current spread divided by 30d median, compared to max_spread_multiple.
Top-of-book last-update timestampWebSocketYesDetect stale book data; reject if older than stale_top_seconds.

5. Required Internal Inputs

InputSourceRequired?Use
Strategy budget remaining for this marketPortfolioGuardYesCap the reshape size to the budget remaining so downsized orders don't exceed the portfolio limit.
KillSwitch active flagKillSwitchYesIf KillSwitch is active, reject all orders immediately without consulting book data.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
max_pct_of_visible_depth253560Maximum share of top-50-level USD depth that a single order may consume.
min_top_of_book_usd25010050Minimum USD size required at the best bid or best ask before any order is allowed.
max_spread_multiple2.52.04.0Maximum allowed spread expressed as a multiple of the 30-day median spread for that market.
stale_top_seconds6045120Maximum age in seconds of the top-of-book snapshot before it is considered stale.

7. Detailed Parameter Instructions

max_pct_of_visible_depth

What it means

Maximum share of top-50-level USD depth that a single order may consume.

Default

{ "max_pct_of_visible_depth": 25 }

Why this default matters

At 25% the order can fill without moving the inside quote more than one tick on a typical Polymarket book.

Threshold logic

ConditionAction
≤ 25% of visible depthAPPROVE
25–60% of visible depthRESHAPE_REQUIRED — downsize to 25%
> 60% of visible depthREJECT — INSUFFICIENT_VISIBLE_DEPTH

Developer check

if (orderSizeUsd / visibleDepthUsd > p.hard) return reject('INSUFFICIENT_VISIBLE_DEPTH'); else if (orderSizeUsd / visibleDepthUsd > p.default) return reshape({ max_size_usd: visibleDepthUsd * p.default });

User-facing English

We reduced your order because the market did not have enough visible liquidity to fill it without moving the price.

min_top_of_book_usd

What it means

Minimum USD size required at the best bid or best ask before any order is allowed.

Default

{ "min_top_of_book_usd": 250 }

Why this default matters

A top-of-book below $250 means the market is thin enough that even small orders may gap the price.

Threshold logic

ConditionAction
top-of-book ≥ 250 USDAPPROVE
50–250 USDRESHAPE_REQUIRED — downsize order to at most top-of-book USD
< 50 USDREJECT — INSUFFICIENT_VISIBLE_DEPTH

Developer check

if (topOfBookUsd < p.hard) return reject('INSUFFICIENT_VISIBLE_DEPTH'); else if (topOfBookUsd < p.default) return reshape({ max_size_usd: topOfBookUsd });

User-facing English

The market has very little resting liquidity right now, so we blocked the order to protect you from large price impact.

max_spread_multiple

What it means

Maximum allowed spread expressed as a multiple of the 30-day median spread for that market.

Default

{ "max_spread_multiple": 2.5 }

Why this default matters

A spread more than 2.5× the 30d median indicates the market is abnormally wide, which increases execution cost and may signal a data or liquidity anomaly.

Threshold logic

ConditionAction
spread ≤ 2.5× medianAPPROVE
2.5–4× medianWARN (logged, not blocking by default)
> 4× medianREJECT — SPREAD_TOO_WIDE

Developer check

const mult = currentSpread / medianSpread30d; if (mult > p.hard) return reject('SPREAD_TOO_WIDE'); else if (mult > p.default) return warn('SPREAD_TOO_WIDE');

User-facing English

The market spread was much wider than usual, which would make this trade unexpectedly expensive. We blocked it to protect your position.

stale_top_seconds

What it means

Maximum age in seconds of the top-of-book snapshot before it is considered stale.

Default

{ "stale_top_seconds": 60 }

Why this default matters

A book that has not moved in 60 seconds may reflect a disconnected data feed or an inactive market. Approving on stale data risks acting on a snapshot that no longer reflects real liquidity.

Threshold logic

ConditionAction
book updated within 60 sAPPROVE
60–120 s since last updateWARN — flag latency to monitor
> 120 s since last updateREJECT — STALE_MARKET_DATA

Developer check

const ageSeconds = (Date.now() - bookLastUpdatedMs) / 1000; if (ageSeconds > p.hard) return reject('STALE_MARKET_DATA'); else if (ageSeconds > p.default) return warn('STALE_MARKET_DATA');

User-facing English

The market data had not updated recently enough to safely process this order. We blocked it until a fresh snapshot is available.

8. Default Configuration

{
  "bot_id": "risk.liquidity_guard",
  "version": "1.0.0",
  "mode": "hard_guard",
  "defaults": {
    "max_pct_of_visible_depth": 25,
    "min_top_of_book_usd": 250,
    "max_spread_multiple": 2.5,
    "stale_top_seconds": 60
  },
  "locked": {
    "min_top_of_book_usd": {
      "min": 50
    },
    "stale_top_seconds": {
      "max": 120
    }
  }
}

9. Implementation Flow

  1. Receive OrderIntent from Strategy layer including market_id, side, size_usd, and price.
  2. Check KillSwitch active flag from KillSwitch service; if active, return REJECT with reason code KILL_SWITCH_ACTIVE immediately.
  3. Pull top 50 levels from the CLOB WebSocket book channel for the target market_id.
  4. Check top-of-book last-update timestamp; if age > stale_top_seconds hard limit, return REJECT with STALE_MARKET_DATA.
  5. Compute total visible_depth_usd from top 50 levels on the relevant side. If top-of-book USD < min_top_of_book_usd hard floor, return REJECT with INSUFFICIENT_VISIBLE_DEPTH.
  6. Compute current spread in percentage points and compare to median30d spread from Data API. If spread_multiple > max_spread_multiple hard ceiling, return REJECT with SPREAD_TOO_WIDE.
  7. Compute pct_of_depth = order.size_usd / visible_depth_usd. If > hard ceiling (60%), return REJECT with INSUFFICIENT_VISIBLE_DEPTH.
  8. If pct_of_depth > default threshold (25%), compute safe_size_usd = visible_depth_usd × 0.25 and return RESHAPE_REQUIRED with constraints.max_size_usd = safe_size_usd.
  9. If spread_multiple > warning threshold, attach a warning annotation to the approval without blocking.
  10. Return APPROVE with inputs_used list and checked_at timestamp.

10. Reference Implementation

Fetches the top-50 CLOB book for the target market, checks KillSwitch, then evaluates depth, spread, and book freshness against configured thresholds. Returns a RiskVote of APPROVE, RESHAPE_REQUIRED, or HARD_REJECT.

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.

FUNCTION evaluateLiquidity(intent):
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active:
    EMIT RiskVote(decision=HARD_REJECT, reason=KILL_SWITCH_ACTIVE)
    RETURN

  // --- 1. Fetch book ---
  book = fetchClobPublic('/book?market=' + intent.market_id)
  IF book IS NULL OR book.updated_at IS NULL:
    EMIT RiskVote(decision=HARD_REJECT, reason=STALE_MARKET_DATA)
    RETURN

  // --- 2. Staleness check ---
  ageSeconds = (now_ms() - book.updated_at_ms) / 1000
  IF ageSeconds > params.stale_top_seconds.hard:
    EMIT RiskVote(decision=HARD_REJECT, reason=STALE_MARKET_DATA)
    RETURN

  // --- 3. Compute depth ---
  side = IF intent.side == BUY THEN book.asks ELSE book.bids
  visibleDepthUsd = SUM(level.price * level.size * collateralDecimals
                        FOR level IN side[:50])
  topOfBookUsd = side[0].price * side[0].size

  // --- 4. Top-of-book floor ---
  IF topOfBookUsd < params.min_top_of_book_usd.hard:
    EMIT RiskVote(decision=HARD_REJECT, reason=INSUFFICIENT_VISIBLE_DEPTH)
    RETURN

  // --- 5. Spread check ---
  spread = book.asks[0].price - book.bids[0].price
  median30d = FETCH fetchClobPublic('/spread-stats?market=' + intent.market_id).median30d
  spreadMultiple = spread / median30d
  IF spreadMultiple > params.max_spread_multiple.hard:
    EMIT RiskVote(decision=HARD_REJECT, reason=SPREAD_TOO_WIDE)
    RETURN

  // --- 6. NegRisk check (optional) ---
  IF intent.neg_risk AND isStale(book, params.stale_top_seconds.default):
    EMIT RiskVote(decision=WARN, reason=LIQUIDITY_GUARD_NEGRISK_THIN_BOOK)

  // --- 7. Depth percentage ---
  pctOfDepth = intent.size_usd / visibleDepthUsd
  IF pctOfDepth > params.max_pct_of_visible_depth.hard:
    EMIT RiskVote(decision=HARD_REJECT, reason=INSUFFICIENT_VISIBLE_DEPTH)
    RETURN

  IF pctOfDepth > params.max_pct_of_visible_depth.default:
    safeSizeUsd = visibleDepthUsd * params.max_pct_of_visible_depth.default
    safeSizeUsd = toUsdcUnits(safeSizeUsd)  // round to pUSD precision
    EMIT RiskVote(decision=RESHAPE_REQUIRED,
                  reason=INSUFFICIENT_VISIBLE_DEPTH,
                  constraints={ max_size_usd: safeSizeUsd })
    RETURN

  // --- 8. Happy path ---
  EMIT RiskVote(decision=APPROVE, checked_at=now_iso())

Helpers used

HelperSignaturePurpose
toUsdcUnitstoUsdcUnits(rawUsd: float) -> intRound a raw USD float to the integer pUSD unit used by CTFExchangeV2 (6 decimals).
isStaleisStale(book: BookSnapshot, maxAgeS: int) -> boolReturns true if book.updated_at_ms is older than maxAgeS seconds relative to now.
fetchClobPublicfetchClobPublic(path: str) -> JSONAuthenticated-free GET against https://clob.polymarket.com; returns parsed JSON or null on error.
platformFeeplatformFee(notional: float, prob: float, feeRate: float) -> floatComputes C * feeRate * p * (1-p); peaks at p=0.5. Used to estimate transaction cost for depth comparisons.

SDK calls used

  • fetchClobPublic('/book?market=0xabc123...&side=asks&depth=50')
  • fetchClobPublic('/spread-stats?market=0xabc123...')
  • fetchClobPublic('/markets/0xabc123...')
  • internal.killswitch.status()

Complexity: O(N) where N = book depth levels (max 50)

11. Wire Examples

Input — what arrives on the wire

OrderIntent from strategyinternal

{
  "intent_id": "int_7f3a1b2c9d4e5f60",
  "market_id": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",
  "side": "BUY",
  "outcome": "YES",
  "size_usd": 1850,
  "price": 0.62,
  "neg_risk": false,
  "generated_at": "2026-05-09T05:51:00Z"
}

CLOB book snapshot (ws_market)ws_market

{
  "market": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",
  "asks": [
    {
      "price": "0.62",
      "size": "820"
    },
    {
      "price": "0.63",
      "size": "1200"
    },
    {
      "price": "0.64",
      "size": "3180"
    }
  ],
  "bids": [
    {
      "price": "0.61",
      "size": "950"
    },
    {
      "price": "0.60",
      "size": "2100"
    }
  ],
  "updated_at_ms": 1746768672000
}

Output — what the bot emits

RiskVote — RESHAPE_REQUIRED (order too large for visible depth)

{
  "guard_id": "risk.liquidity_guard",
  "decision": "RESHAPE_REQUIRED",
  "severity": "WARN",
  "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
  "message": "Order size 1850 pUSD exceeded 25% of 5200 pUSD visible top-50 depth. Resized to 1300 pUSD.",
  "constraints": {
    "max_size_usd": 1300,
    "passive_only": false,
    "close_only": false
  },
  "inputs_used": [
    "clob_public.book.top50",
    "data_api.spread.median30d",
    "internal.killswitch.status"
  ],
  "checked_at": "2026-05-09T05:51:12Z"
}

RiskVote — HARD_REJECT (stale book)

{
  "guard_id": "risk.liquidity_guard",
  "decision": "HARD_REJECT",
  "severity": "HARD",
  "reason_code": "STALE_MARKET_DATA",
  "message": "Book last updated 135s ago; stale_top_seconds hard limit is 120s.",
  "constraints": {},
  "inputs_used": [
    "clob_public.book.top50"
  ],
  "checked_at": "2026-05-09T06:05:00Z"
}

Reproduce locally

curl 'https://clob.polymarket.com/book?market=0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b'

12. Decision Logic

APPROVE

Top-of-book ≥ min_top_of_book_usd, book updated within stale_top_seconds, spread ≤ max_spread_multiple × 30d median, and order size ≤ max_pct_of_visible_depth of total visible depth.

RESHAPE_REQUIRED

Order size exceeds max_pct_of_visible_depth default (25%) but is below the hard ceiling (60%), or top-of-book is between warning and hard floor — emit constraints.max_size_usd capped at the safe level.

REJECT

Top-of-book is stale (> stale_top_seconds), depth is below the hard floor (min_top_of_book_usd), spread is above the hard multiple (max_spread_multiple × 4.0), order size exceeds 60% of visible depth, or KillSwitch is active.

WARNING_ONLY

Not used — LiquidityGuard has reject authority. Spread between warning and hard multiple emits a log annotation but does not block the order.

13. Standard Decision Output

This bot returns a RiskVote object. See RiskVote schema.

{
  "guard_id": "risk.liquidity_guard",
  "decision": "RESHAPE_REQUIRED",
  "severity": "WARN",
  "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
  "message": "Order size 1850 USD exceeded 25% of 5200 USD visible top-of-book depth. Resized to 1300 USD.",
  "constraints": {
    "max_size_usd": 1300,
    "passive_only": false,
    "close_only": false
  },
  "inputs_used": [
    "clob.book.top50",
    "data_api.spread.median30d",
    "internal.killswitch.status"
  ],
  "checked_at": "2026-05-09T05:51:12Z"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active; no orders may proceed.Immediately return HARD_REJECT without consulting book data.Trading is currently paused. Please try again later.
STALE_MARKET_DATAHARD_REJECTBook snapshot is older than stale_top_seconds hard limit.Return HARD_REJECT; wait for fresh book before retrying.Market data had not updated recently. The order was blocked until a fresh snapshot is available.
INSUFFICIENT_VISIBLE_DEPTHHARD_REJECTOrder size exceeds the hard depth ceiling or top-of-book is below the hard floor.Return HARD_REJECT or RESHAPE_REQUIRED depending on which threshold was breached.There was not enough resting liquidity to safely place your order at the requested size.
SPREAD_TOO_WIDEHARD_REJECTCurrent spread exceeds max_spread_multiple times the 30-day median.Return HARD_REJECT; log spread_multiple value.The gap between the buy and sell prices was much wider than usual. The order was blocked to protect against unexpectedly high transaction cost.
LIQUIDITY_GUARD_RESHAPE_DEPTHRESHAPEOrder size is above the default depth percentage but below the hard ceiling.Return RESHAPE_REQUIRED with constraints.max_size_usd = visibleDepthUsd * default_pct.Your order was reduced because filling the full size would have consumed too much of the visible liquidity.
LIQUIDITY_GUARD_SPREAD_WARNWARNSpread is between the warning and hard multiple thresholds.Attach warning annotation to APPROVE; do not block.
LIQUIDITY_GUARD_NEGRISK_THIN_BOOKWARNNegRisk market book is thin relative to the requested size, increasing definition-shift exposure.Attach warning annotation; Strategy may reduce size further.
LIQUIDITY_GUARD_TOP_BOOK_RESHAPERESHAPETop-of-book USD is between the warning and hard floor thresholds.Return RESHAPE_REQUIRED with constraints.max_size_usd = topOfBookUsd.The market has very little resting liquidity at the best price. Your order was reduced to the available top-of-book size.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_risk_liquidityguard_decisions_totalcountercountdecision, reason_code, market_idTotal RiskVote decisions emitted, broken down by decision type and reason.
polytraders_risk_liquidityguard_book_age_secondshistogramsecondsmarket_idAge of the book snapshot at evaluation time; alerts when p99 approaches stale_top_seconds.
polytraders_risk_liquidityguard_visible_depth_usdgaugeusdmarket_idTotal visible USD depth across top-50 levels at the time of last check.
polytraders_risk_liquidityguard_spread_multiplegaugeratiomarket_idCurrent spread divided by 30-day median spread; triggers WARN above 2.5x.
polytraders_risk_liquidityguard_reshape_size_usdhistogramusdmarket_idSize delta (original minus reshaped) for RESHAPE_REQUIRED decisions.
polytraders_risk_liquidityguard_eval_latency_mshistogramsecondsWall-clock latency from intent receipt to RiskVote emit.

Alerts

AlertConditionSeverityRunbook
LiquidityGuardStaleBookhistogram_quantile(0.99, rate(polytraders_risk_liquidityguard_book_age_seconds_bucket[5m])) > 90P1#runbook-liquidityguard-stale-book
LiquidityGuardHighRejectRaterate(polytraders_risk_liquidityguard_decisions_total{decision='HARD_REJECT'}[5m]) / rate(polytraders_risk_liquidityguard_decisions_total[5m]) > 0.5P2#runbook-liquidityguard-reject-rate
LiquidityGuardSpreadSpikepolytraders_risk_liquidityguard_spread_multiple > 3.5P2#runbook-liquidityguard-spread
LiquidityGuardHighLatencyhistogram_quantile(0.99, rate(polytraders_risk_liquidityguard_eval_latency_ms_bucket[5m])) > 200P2#runbook-liquidityguard-latency

Dashboards

  • Grafana — Risk overview / LiquidityGuard
  • Grafana — Market quality / spread and depth heatmap

Log levels

LevelWhat gets logged
DEBUGPer-level depth computation and spread_multiple value on every evaluation.
INFORiskVote decision emitted (decision, reason_code, market_id, size_usd).
WARNBook age approaching stale threshold; spread_multiple between warning and hard limit.
ERRORCLOB book endpoint returned null or non-200; KillSwitch flag unreadable.

16. Developer Reporting

{
  "bot_id": "risk.liquidity_guard",
  "decision": "RESHAPE_REQUIRED",
  "reason_code": "INSUFFICIENT_VISIBLE_DEPTH",
  "inputs_used": [
    "clob.book.top50",
    "data_api.spread.median30d"
  ],
  "metrics": {
    "visible_depth_usd": 5200,
    "requested_size_usd": 1850,
    "pct_of_depth": 0.356,
    "top_of_book_usd": 820,
    "spread_multiple": 1.4,
    "book_age_seconds": 12
  },
  "safe_size_usd": 1300,
  "checked_at": "2026-05-09T05:51:12Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Order downsized due to thin bookWe reduced your order because filling the full size would have consumed too much of the visible liquidity in this market and moved the price against you.
Order blocked — market data too oldWe blocked this order because the market data had not updated recently. We wait for a fresh snapshot before allowing new orders to protect you from acting on stale information.
Order blocked — spread too wideThe gap between the buy and sell prices was much larger than usual. We blocked this order because the high spread would make it significantly more expensive than expected.
Order blocked — book too thinThere was not enough resting liquidity in this market to safely place your order. We blocked it to prevent your trade from moving the price by an unusually large amount.
Order blocked — size exceeds hard limitYour order was larger than what this market can safely absorb. Even after considering a downsize, the remaining liquidity was insufficient. Please try a smaller amount.

18. Failure-Mode Block

main_failure_modeAllowing an oversized order through a thin book, causing significant adverse price impact for the user.
false_positive_riskDownsizing or rejecting a legitimate order on a temporarily quiet but sufficiently deep market, such as early in the trading day before resting liquidity has built up.
false_negative_riskApproving an order against a book that was valid at snapshot time but has since been pulled, if the staleness window is set too wide.
safe_fallbackIf CLOB book data is absent or older than stale_top_seconds hard limit, always reject with STALE_MARKET_DATA. LiquidityGuard never approves on missing or unverifiable data.
required_dependenciesCLOB WebSocket book channel, Data API 30-day median spread, PortfolioGuard ledger (budget remaining), KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_BOOKFreeze WS market channel for 130sAll evaluations return HARD_REJECT(STALE_MARKET_DATA) once book_age_seconds > 120Returns to APPROVE within 5s of fresh book delivery.
EMPTY_BOOKReturn empty asks/bids arrays from CLOB mocktopOfBookUsd=0 triggers HARD_REJECT(INSUFFICIENT_VISIBLE_DEPTH)Immediate on next evaluation with a non-empty book.
WIDE_SPREADSet asks[0].price=0.90, bids[0].price=0.10 so spread_multiple > 4.0HARD_REJECT(SPREAD_TOO_WIDE)Next evaluation where spread_multiple falls below hard limit resumes normally.
KILL_SWITCH_ONSet internal.killswitch.status.active = trueHARD_REJECT(KILL_SWITCH_ACTIVE) on every intent without book fetchReturns to normal pipeline on manual KillSwitch reset.
CLOB_ENDPOINT_DOWNBlock TCP to clob.polymarket.comfetchClobPublic returns null; HARD_REJECT(STALE_MARKET_DATA)Returns to APPROVE within one evaluation cycle after endpoint is reachable.

20. State & Persistence

LiquidityGuard is stateless per evaluation; it holds no persistent state beyond an in-memory cache of the last book snapshot per market.

State stores

NameKindKeyValue shapeTTLDurability
book_cachein-memorymarket_id{ asks: Level[], bids: Level[], updated_at_ms: int }120sbest-effort

Cold-start recovery

On cold start, the cache is empty. The first evaluation for each market_id triggers a fresh CLOB fetch.

On restart

Book snapshots are re-fetched on first evaluation; no durable state is loaded. If the WebSocket reconnects before the first evaluation, snapshots are populated from the reconnection event.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop
Max in-flight200
Idempotency keyintent_id
Replay-safeTrue
Deduplicationby intent_id within a 24h window
Ordering guaranteesFIFO per market_id
Per-call timeout (ms)150
Backpressure strategydrop newest
Locking / mutual exclusionper-market_id mutex

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchGlobal brake — checked first before any book data is read.RiskVote.HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all further evaluation.
risk.portfolio_guardBudget remaining for this market caps the reshape ceiling.Reshape size is min(safe_depth_size, portfolio_budget_remaining).

Emits to (downstream consumers)

BotWhyContract
exec.smart_routerApproved or reshaped RiskVote passes to SmartRouter for ExecutionPlan construction.APPROVE or RESHAPE_REQUIRED RiskVote is consumed; HARD_REJECT causes SmartRouter to discard the intent.

Used by (auto-aggregated)

4.13

External services

ServiceEndpointSLA assumedOn failure
CLOB API (read)https://clob.polymarket.com99.95% / 200ms p99HARD_REJECT(STALE_MARKET_DATA) until book is fresh.
WS market feedwss://ws-subscriptions-clob.polymarket.com/ws/marketbest-effort / sub-100msFalls back to REST poll; if REST also fails, HARD_REJECT.
Data API (spread stats)https://data-api.polymarket.com99.9% / 500ms p99WARN emitted; evaluation continues with best available spread estimate.

23. Security Surfaces

LiquidityGuard is read-only and stateless. It never signs orders or holds secrets.

Signing surface

This bot does NOT sign anything.

Abuse vectors considered

  • Replaying a stale book snapshot to bypass depth checks
  • Injecting artificially large depth values to allow oversized orders

Mitigations

  • per-intent_id idempotency prevents replay of the same evaluation
  • book.updated_at_ms is checked against wall clock; any snapshot older than stale_top_seconds is rejected regardless of depth values

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK used@polymarket/clob-client-v2 ^2.x
Settlement contractCTFExchangeV2 on Polygon
NotesAll depth values are denominated in pUSD (USDC-backed ERC-20). Order fields evaluated here use the V2 schema (timestamp/metadata/builder); nonce and feeRateBps fields are not present.

API surfaces declared

ws_marketclob_publicdata_api

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.3
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1 (USDC.e + HMAC builder)v2 (pUSD + builderCode field)Polymarket V2 cutoverMigrated SDK, replaced HMAC builder logic with on-order builderCode, removed feeRateBps from order construction. Depth values now denominated in pUSD.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Approve when all thresholds passdepth_usd=2000, size_usd=400, top_of_book_usd=600, spread_multiple=1.2, book_age_s=10APPROVE with no constraints
Reshape when size is 30% of depthdepth_usd=1000, size_usd=300, hard=60, default=25RESHAPE_REQUIRED with constraints.max_size_usd=250
Reject when size exceeds 60% hard ceilingdepth_usd=1000, size_usd=650REJECT with reason_code=INSUFFICIENT_VISIBLE_DEPTH
Reject when book age exceeds hard stale limitbook_age_s=130, stale_top_seconds=60REJECT with reason_code=STALE_MARKET_DATA
Reject when spread_multiple > 4.0current_spread=0.08, median_spread=0.01 (multiple=8.0)REJECT with reason_code=SPREAD_TOO_WIDE
Reshape when top-of-book is between warning and hard floortop_of_book_usd=150, min_top_of_book_usd=250, hard=50RESHAPE_REQUIRED with constraints.max_size_usd=150
Reject when top-of-book is below hard floortop_of_book_usd=30, min_top_of_book_usd hard=50REJECT with reason_code=INSUFFICIENT_VISIBLE_DEPTH

Integration Tests

TestExpected result
Rejects on stale book snapshot from live WebSocketREJECT(STALE_MARKET_DATA) when WebSocket book channel has not emitted an update within stale_top_seconds
Reshape flows through to ExecutionPlan with reduced sizeExecutionPlan downstream receives constraints.max_size_usd and does not exceed it
KillSwitch active causes immediate rejection before book checkREJECT emitted without querying CLOB when KillSwitch active flag is true

Property Tests

PropertyRequired behaviour
Missing or absent book data never results in APPROVEAlways true — null or empty book must produce REJECT(STALE_MARKET_DATA)
Reshape size is always strictly ≤ requested order sizeAlways true — constraints.max_size_usd ≤ original order size_usd
Approved order size never exceeds max_pct_of_visible_depth of visible depthAlways true — for any APPROVE, size_usd / visible_depth_usd ≤ default threshold

27. Operational Runbook

LiquidityGuard incidents are typically caused by a stale CLOB book feed or an abnormal spread spike. On-call should first confirm whether the WS market feed is connected before adjusting parameters.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
LiquidityGuardStaleBookCheck WS market feed connection status in the Grafana panel.If WS is disconnected, check clob.polymarket.com status page. If WS is connected, check for clock skew between the bot host and exchange.Reconnect WS feed; if clock skew, resync NTP. Do not increase stale_top_seconds without approval.Risk pod lead if feed is down > 5 minutes.
LiquidityGuardHighRejectRateCheck reason_code distribution on the HARD_REJECT counter.If dominated by STALE_MARKET_DATA, follow stale-book runbook. If INSUFFICIENT_VISIBLE_DEPTH, check whether a specific market has thinned unusually.For thin markets, pause the affected strategy until liquidity recovers. Do not lower depth thresholds.Risk pod lead if reject rate persists > 10 minutes.
LiquidityGuardSpreadSpikeIdentify which market_id is driving the spread_multiple gauge above 3.5.Check Gamma API for market metadata; confirm no oracle dispute or resolution event.If spread is genuine (thin book, not a data error), the guard is working correctly. If a data error, restart the WS subscription for that market.OracleRiskMonitor on-call if oracle dispute is active.
LiquidityGuardHighLatencyCheck CLOB REST response times in the latency histogram.If CLOB latency is high, the REST fallback path is being used more than expected.Confirm WS is connected. Reduce bot concurrency if host is CPU-bound.Infra on-call if CLOB p99 latency > 500ms sustained.

Manual overrides

  • polytraders bot pause risk.liquidity_guard — Stops emitting RiskVotes; all intents fall through to the next guardrail without a liquidity check. Use only during a known feed outage.
  • polytraders bot flush-cache risk.liquidity_guard --market <market_id> — Evicts the in-memory book cache for a specific market, forcing a fresh CLOB fetch on the next evaluation.

Healthcheck

GET /health → 200 if WS market feed is connected and last book update for any tracked market is within stale_top_seconds.

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
Unit tests pass including all acceptance_tests.unit casesCI test run100% pass
Integration test: stale book → HARD_REJECT verifiedIntegration test suitePass

Promote to Limited live

GateHow measuredThreshold
Shadow mode reject rate matches expected baseline within 10%Grafana shadow vs live comparison dashboard< 10% divergence over 48h
p99 evaluation latency < 150mspolytraders_risk_liquidityguard_eval_latency_ms histogramp99 < 150ms

Promote to General live

GateHow measuredThreshold
Zero HARD_REJECT(STALE_MARKET_DATA) during normal operating hours over 7 daysLiquidityGuardStaleBook alert history0 firings
Reshape decisions correctly cap order size in E2E flowE2E integration tests + manual audit of fill logs100% compliance

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