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 LayerExecution2.1 SmartRouter

2.1 SmartRouter

Execution Execution Utility Reshape LIVE General live capital · Direct P5 · Execution rails pending reference bot

SmartRouter translates an approved strategy intent into a concrete executable order by selecting the appropriate order type (FOK, GTC, or GTD), price, and timing. It may split a large order into iceberg child orders to reduce market impact. SmartRouter cannot change the direction of the trade, the target market, or the strategy intent — it is not permitted to flip the side (buy/sell), alter the outcome leg, or override any constraint set by the Risk guardrail pipeline. The only transformations it may make are to price, size scheduling, order type, and submission timing.

v3 readiness

Docs27/27
donehow scored
Impl11/15
in progresshow scored
Backtest3/4
in progresshow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

v3.5 · wired registry id exec.smartrouter

Reference execution router; thin wrapper that emits SignedOrderRequest. SEARCH_SPACE declared.

Source: @polytraders/bots · src/exec/smartrouter.js · Impl 11/15 · Backtest 3/4

1. Bot Identity

LayerExecution  Execution
Bot classExecution Utility
AuthorityReshape
StatusLIVE
ReadinessGeneral live
Runs beforeOrder signing and submission
Runs afterRisk guardrail pipeline (all RiskVotes collected)
Applies toEvery approved OrderIntent that has passed the full Risk guardrail pipeline
Default modegeneral_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Execution pod

Operational profile

Modes supportedquarantine

2. Purpose

SmartRouter translates an approved strategy intent into a concrete executable order by selecting the appropriate order type (FOK, GTC, or GTD), price, and timing. It may split a large order into iceberg child orders to reduce market impact. SmartRouter cannot change the direction of the trade, the target market, or the strategy intent — it is not permitted to flip the side (buy/sell), alter the outcome leg, or override any constraint set by the Risk guardrail pipeline. The only transformations it may make are to price, size scheduling, order type, and submission timing.

3. Why This Bot Matters

  • Wrong order type applied

    A Fill-or-Kill order on a thin book will be rejected by the exchange and the strategy misses the opportunity entirely; a GTC order on a fast-moving market may fill at an outdated price.

  • Large order submitted without iceberg splitting

    A single large visible order signals intent to other market participants and may be front-run or cause the book to refresh at a worse price before the order fills.

  • Order submitted after signal TTL expires

    Executing a GTC or GTD order on a signal that has aged past its validity window means acting on market intelligence that may no longer be accurate.

  • Tick size not respected

    An order with a price that does not align to the market's tick size will be rejected by the CLOB, resulting in a failed submission.

4. Required Polymarket Inputs

InputSourceRequired?Use
CLOB order book — top 50 levelsCLOBYesAssess current book depth to decide between FOK and GTC, and to determine whether iceberg splitting is warranted.
Market tick size and neg-risk flagGamma APIYesRound order price to the correct tick size; apply neg-risk routing rules when the neg-risk flag is set.
Recent fill rate and estimated queue positionData APINoEstimate time-to-fill for GTC orders and decide whether to use a more aggressive price to improve queue position.

5. Required Internal Inputs

InputSourceRequired?Use
Approved OrderIntent with all Risk constraints appliedPortfolioGuardYesReceive the final approved size, maximum price, and any constraint flags (passive_only, close_only) from the guardrail pipeline.
KillSwitch active flagKillSwitchYesAbort order construction and emit no ExecutionPlan if KillSwitch is active.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
default_order_typeGTCThe default order type used when the intent does not specify one. FOK = Fill-or-Kill; GTC = Good-till-Cancelled; GTD = Good-till-Date with a TTL driven by gtd_signal_ttl_s.
iceberg_threshold_usd5003001000Order size in USD above which the order is automatically split into iceberg child orders to reduce visible market impact.
iceberg_child_count358Number of child orders to split an iceberg order into. Each child is submitted sequentially as the previous one fills.
gtd_signal_ttl_s12060300Time-to-live in seconds for GTD orders, measured from the moment the OrderIntent was generated by the strategy. Orders not submitted before this window expires are discarded.

7. Detailed Parameter Instructions

default_order_type

What it means

The default order type used when the intent does not specify one. FOK = Fill-or-Kill; GTC = Good-till-Cancelled; GTD = Good-till-Date with a TTL driven by gtd_signal_ttl_s.

Default

{ "default_order_type": "GTC" }

Why this default matters

GTC is the safest default because it remains in the book until it fills or is cancelled, avoiding the opportunity loss of FOK on a momentarily thin book.

Threshold logic

ConditionAction
Order size ≤ top-of-book depth and execution is time-sensitiveUse FOK
Order size fits in book with no urgencyUse GTC (default)
Signal has a defined expiry (gtd_signal_ttl_s > 0)Use GTD with calculated expiry timestamp

Developer check

const type = intent.order_type || p.default_order_type; if (type === 'GTD') plan.expiry = now + p.gtd_signal_ttl_s;

User-facing English

We chose the order type that best matches current market conditions to improve your chance of getting a good fill.

iceberg_threshold_usd

What it means

Order size in USD above which the order is automatically split into iceberg child orders to reduce visible market impact.

Default

{ "iceberg_threshold_usd": 500 }

Why this default matters

Orders above $500 on typical Polymarket books are large enough to visibly move the queue and attract front-running. Splitting at this threshold keeps each child order within normal market noise.

Threshold logic

ConditionAction
order.size_usd ≤ 500 USDSubmit as single order
500–1000 USDSplit into iceberg_child_count children
> 1000 USD hard ceiling (locked by Risk)Should not reach here — Risk guardrail should have capped to 1000 USD max

Developer check

if (order.size_usd > p.iceberg_threshold_usd) return splitIceberg(order, p.iceberg_child_count);

User-facing English

Your order was split into smaller pieces to reduce its visibility in the market and get a better overall fill.

iceberg_child_count

What it means

Number of child orders to split an iceberg order into. Each child is submitted sequentially as the previous one fills.

Default

{ "iceberg_child_count": 3 }

Why this default matters

Three children provide a reasonable balance between reduced visible size and submission overhead. Too many children increase latency; too few still expose a large visible resting order.

Threshold logic

ConditionAction
iceberg_child_count ≤ 5Normal iceberg split
5–8WARN — high child count increases submission overhead
> 8Reject config change — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.iceberg_child_count > p.hard) throw new ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');

User-facing English

Your order was broken into several smaller orders that will be submitted one after another.

gtd_signal_ttl_s

What it means

Time-to-live in seconds for GTD orders, measured from the moment the OrderIntent was generated by the strategy. Orders not submitted before this window expires are discarded.

Default

{ "gtd_signal_ttl_s": 120 }

Why this default matters

A 120-second TTL means the system will not act on a signal more than two minutes old, limiting exposure from stale strategy decisions on a fast-moving market.

Threshold logic

ConditionAction
Signal age ≤ gtd_signal_ttl_sProceed with GTD order
Signal age > gtd_signal_ttl_sDiscard order — STALE_MARKET_DATA

Developer check

if (Date.now() - intent.generated_at > p.gtd_signal_ttl_s * 1000) return discard('STALE_MARKET_DATA');

User-facing English

An order was not submitted because the market information it was based on had expired before the order could be sent.

8. Default Configuration

{
  "bot_id": "exec.smart_router",
  "version": "1.0.0",
  "mode": "general_live",
  "defaults": {
    "default_order_type": "GTC",
    "iceberg_threshold_usd": 500,
    "iceberg_child_count": 3,
    "gtd_signal_ttl_s": 120
  },
  "locked": {
    "iceberg_child_count": {
      "max": 8
    },
    "gtd_signal_ttl_s": {
      "max": 300
    }
  }
}

9. Implementation Flow

  1. Receive the approved OrderIntent from the Risk guardrail pipeline, including applied constraints (max_size_usd, passive_only, close_only) from PortfolioGuard.
  2. Check KillSwitch active flag; if active, discard the order and emit no ExecutionPlan.
  3. Fetch the market's tick size and neg-risk flag from Gamma API; round order.price to the nearest valid tick.
  4. Determine the order type: use intent.order_type if specified; otherwise apply default_order_type logic based on book depth and signal age.
  5. If order type is GTD, check signal age against gtd_signal_ttl_s; if expired, discard with STALE_MARKET_DATA.
  6. Check order.size_usd against iceberg_threshold_usd; if above threshold, compute child_size = size / iceberg_child_count and build an iceberg plan.
  7. Pull top 50 book levels from the CLOB to confirm there is sufficient depth for FOK orders; if depth is insufficient for FOK, downgrade to GTC automatically.
  8. Assemble the ExecutionPlan with: order_type, price (tick-aligned), size or iceberg children array, side (unchanged from intent), market_id (unchanged), and submission_timestamp.
  9. Validate that the ExecutionPlan does not alter side, market_id, or outcome leg from the original intent — these are invariants.
  10. Emit the ExecutionPlan to the order signing step.

10. Reference Implementation

Receives an approved OrderIntent from the Risk pipeline, selects order type, tick-aligns the price using buildOrderTypedData, optionally splits into iceberg children, and emits an ExecutionPlan. Owns the signing flow by calling the wallet adapter — never holds keys.

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

FUNCTION routeOrder(intent, riskConstraints):
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active:
    DISCARD intent
    RETURN

  // --- 1. Fetch market tick size and neg-risk flag ---
  market = fetchClobPublic('/markets/' + intent.market_id)
  IF market IS NULL OR isStale(market, 60):
    DISCARD intent; reason=STALE_MARKET_DATA
    RETURN

  tickSize = market.minimum_tick_size  // e.g. 0.01
  negRisk = market.neg_risk

  // --- 2. GTD TTL check ---
  signalAgeMs = now_ms() - intent.generated_at_ms
  IF intent.order_type == 'GTD' AND signalAgeMs > params.gtd_signal_ttl_s * 1000:
    DISCARD intent; reason=STALE_MARKET_DATA
    RETURN

  // --- 3. Tick-align price ---
  alignedPrice = floor(intent.price / tickSize) * tickSize

  // --- 4. Order type selection ---
  book = fetchClobPublic('/book?market=' + intent.market_id)
  bookDepth = SUM(level.size FOR level IN book.asks[:50])
  IF intent.order_type == 'FOK' AND bookDepth < intent.size_usd:
    orderType = 'GTC'  // downgrade FOK → GTC on thin book
  ELSE:
    orderType = intent.order_type OR params.default_order_type

  // --- 5. NegRisk convert-arb routing ---
  IF negRisk AND intent.negrisk_convert_requested:
    EMIT NegRiskConvertRoute(market_id=intent.market_id, size=intent.size_usd)
    RETURN  // NegRiskAdapter path; not a CLOB order

  // --- 6. Iceberg split ---
  finalSize = min(intent.size_usd, riskConstraints.max_size_usd)
  IF finalSize > params.iceberg_threshold_usd:
    childSize = toUsdcUnits(finalSize / params.iceberg_child_count)
    children = [childSize] * params.iceberg_child_count
  ELSE:
    children = [toUsdcUnits(finalSize)]

  // --- 7. Build V2 typed order data ---
  // V2 order fields: timestamp(ms) + metadata(bytes32) + builder(bytes32)
  // Fees are operator-set at match time; NOT in the signed order
  FOR child IN children:
    typedData = buildOrderTypedData({
      market_id: intent.market_id,
      side: intent.side,  // NEVER flipped
      outcome: intent.outcome,
      price: alignedPrice,
      size: child,
      order_type: orderType,
      timestamp: now_ms(),
      metadata: ZERO_BYTES32,
      builder: config.builder_code_bytes32
    }, domain={ version: '2', chainId: 137, verifyingContract: CTFExchangeV2 })
    // wallet adapter signs typedData — never holds keys
    signedOrder = wallet.sign(typedData)
    VALIDATE ExecutionPlan invariants:
      assert signedOrder.side == intent.side
      assert signedOrder.market_id == intent.market_id
      assert signedOrder.size <= riskConstraints.max_size_usd

  // --- 8. Emit ExecutionPlan ---
  EMIT ExecutionPlan(
    router_id='exec.smart_router',
    market_id=intent.market_id,
    side=intent.side,
    order_type=orderType,
    tick_aligned_price=alignedPrice,
    iceberg=(len(children) > 1),
    children=children,
    submission_timestamp=now_iso()
  )

Helpers used

HelperSignaturePurpose
buildOrderTypedDatabuildOrderTypedData(orderParams, domain) -> TypedDataConstructs the EIP-712 V2 typed data for a CLOB order. V2 fields: timestamp(ms), metadata(bytes32), builder(bytes32). No feeRateBps — fees are operator-set at match time.
toUsdcUnitstoUsdcUnits(rawUsd: float) -> intRounds a raw pUSD float to the integer unit used by CTFExchangeV2 (6 decimals).
fetchClobPublicfetchClobPublic(path: str) -> JSONUnauthenticated GET against https://clob.polymarket.com; returns parsed JSON or null on error.
isStaleisStale(snapshot: any, maxAgeS: int) -> boolReturns true if snapshot was fetched more than maxAgeS seconds ago.
platformFeeplatformFee(notional: float, prob: float, feeRate: float) -> floatEstimates the platform fee C*feeRate*p*(1-p) for a given trade; used to log expected cost alongside the ExecutionPlan.

SDK calls used

  • fetchClobPublic('/markets/' + intent.market_id)
  • fetchClobPublic('/book?market=' + intent.market_id + '&depth=50')
  • buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })
  • wallet.sign(typedData)
  • clob_auth.POST('/order', signedOrder)
  • ws_user.subscribe('fills', intent.market_id)

Complexity: O(C) where C = iceberg_child_count

11. Wire Examples

Input — what arrives on the wire

Approved OrderIntent from Risk pipelineinternal

{
  "intent_id": "int_6f7a8b9c0d1e2f3a",
  "market_id": "0x6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f",
  "side": "BUY",
  "outcome": "YES",
  "price": 0.623,
  "size_usd": 500,
  "order_type": "GTC",
  "neg_risk": false,
  "generated_at_ms": 1746768658000,
  "risk_constraints": {
    "max_size_usd": 450,
    "passive_only": false,
    "close_only": false
  }
}

Output — what the bot emits

ExecutionPlan — single GTC order, tick-aligned

{
  "router_id": "exec.smart_router",
  "market_id": "0x6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f",
  "side": "BUY",
  "outcome": "YES",
  "order_type": "GTC",
  "price": 0.623,
  "tick_aligned_price": 0.62,
  "size_usd": 450,
  "iceberg": false,
  "children": [],
  "builder_code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
  "eip712_domain_version": "2",
  "submission_timestamp": "2026-05-09T11:00:00Z",
  "signal_age_s": 14,
  "inputs_used": [
    "clob_public.book.top50",
    "gamma_api.market.tick_size",
    "portfolio_guard.approved_intent"
  ]
}

ExecutionPlan — iceberg split (3 children)

{
  "router_id": "exec.smart_router",
  "market_id": "0x6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f",
  "side": "BUY",
  "outcome": "YES",
  "order_type": "GTC",
  "tick_aligned_price": 0.62,
  "size_usd": 600,
  "iceberg": true,
  "children": [
    200,
    200,
    200
  ],
  "builder_code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
  "eip712_domain_version": "2",
  "submission_timestamp": "2026-05-09T11:01:00Z",
  "signal_age_s": 8,
  "inputs_used": [
    "clob_public.book.top50",
    "gamma_api.market.tick_size"
  ]
}

Reproduce locally

curl -H 'Authorization: Bearer <token>' -X POST 'https://clob.polymarket.com/order' -d '{...signed_order...}'

12. Decision Logic

APPROVE

Not directly applicable — SmartRouter emits an ExecutionPlan, not an approval vote. The plan is emitted if the intent passes KillSwitch check and signal TTL check.

RESHAPE_REQUIRED

Iceberg splitting, GTD expiry calculation, FOK-to-GTC downgrade on thin book, and tick-size rounding are all reshape operations. The strategy intent size, side, and market are preserved — only execution mechanics change.

REJECT

SmartRouter discards the order (emits no ExecutionPlan) if KillSwitch is active or if a GTD signal has aged past gtd_signal_ttl_s.

WARNING_ONLY

Tick-size rounding that results in a price change of more than one tick is logged as a warning annotation on the ExecutionPlan.

13. Standard Decision Output

This bot returns a ExecutionPlan object. See ExecutionPlan schema.

{
  "router_id": "exec.smart_router",
  "market_id": "CLOB:0xabc123",
  "side": "BUY",
  "outcome": "YES",
  "order_type": "GTC",
  "price": 0.62,
  "tick_aligned_price": 0.62,
  "size_usd": 450,
  "iceberg": false,
  "children": [],
  "submission_timestamp": "2026-05-09T11:00:00Z",
  "signal_age_s": 14,
  "inputs_used": [
    "clob.book.top50",
    "gamma_api.market.tick_size",
    "portfolio_guard.approved_intent"
  ]
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
STALE_MARKET_DATAHARD_REJECTMarket metadata (tick size) is unavailable, or GTD signal has aged past gtd_signal_ttl_s.Discard order; emit no ExecutionPlan.The order could not be placed because the market data or signal had expired.
SPREAD_TOO_WIDEWARNTick-size rounding shifted price by more than one tick.Attach warning annotation to ExecutionPlan; do not block.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active; no orders may proceed.Discard order; emit no ExecutionPlan.Trading is currently paused.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTiceberg_child_count or gtd_signal_ttl_s exceeds the locked hard maximum.Reject config change; do not apply.
NEGRISK_CONVERT_AVAILABLEEXPLAINNegRisk convert-arb route is available for this market; notifies Strategy layer.Emit NegRiskConvertRoute instead of a CLOB order when negrisk_convert_requested=true.
SMART_ROUTER_FOK_DOWNGRADERESHAPEBook depth was insufficient to guarantee a complete FOK fill; order was changed to GTC.Set order_type=GTC in the ExecutionPlan.The market did not have enough visible liquidity for an immediate fill, so the order was changed to stay in the book until it fills.
SMART_ROUTER_ICEBERG_SPLITRESHAPEOrder size exceeded iceberg_threshold_usd; split into child orders.Set iceberg=true and populate children array.Your order was split into smaller pieces to reduce its visibility in the market.
FEE_RATE_CAPPEDWARNComputed taker fee would exceed 100 bps or maker fee would exceed 50 bps. Fees are operator-set at match time; this warn is for logging only.Log the fee estimate; do not block the order.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_exec_smartrouter_plans_totalcountercountorder_type, icebergTotal ExecutionPlans emitted by order type and iceberg flag.
polytraders_exec_smartrouter_discards_totalcountercountreason_codeTotal intents discarded (no ExecutionPlan emitted) by reason.
polytraders_exec_smartrouter_signal_age_secondshistogramsecondsAge of the OrderIntent signal at routing time; tracks freshness of strategy decisions.
polytraders_exec_smartrouter_tick_rounding_tickshistogramcountmarket_idNumber of ticks by which the price was rounded; > 1 tick triggers a WARN.
polytraders_exec_smartrouter_iceberg_child_counthistogramcountDistribution of iceberg child counts per split order.
polytraders_exec_smartrouter_eval_latency_mshistogramsecondsWall-clock latency from intent receipt to ExecutionPlan emit.

Alerts

AlertConditionSeverityRunbook
SmartRouterHighDiscardRaterate(polytraders_exec_smartrouter_discards_total[5m]) / rate(polytraders_exec_smartrouter_plans_total[5m]) > 0.2P2#runbook-smartrouter-discards
SmartRouterStaleSignalshistogram_quantile(0.99, rate(polytraders_exec_smartrouter_signal_age_seconds_bucket[5m])) > 60P2#runbook-smartrouter-stale-signals
SmartRouterHighLatencyhistogram_quantile(0.99, rate(polytraders_exec_smartrouter_eval_latency_ms_bucket[5m])) > 200P2#runbook-smartrouter-latency
SmartRouterTickSizeUnavailablerate(polytraders_exec_smartrouter_discards_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.05P1#runbook-smartrouter-tick-size

Dashboards

  • Grafana — Execution / SmartRouter
  • Grafana — Order quality / tick rounding and iceberg split distribution

Log levels

LevelWhat gets logged
DEBUGTick-size computation, iceberg child sizes, and order type selection logic on every intent.
INFOExecutionPlan emitted with order_type, tick_aligned_price, iceberg flag.
WARNTick-size rounding > 1 tick; FOK downgraded to GTC; signal age approaching TTL.
ERRORGamma API tick-size unavailable; KillSwitch status unreadable; wallet adapter signing failure.

16. Developer Reporting

{
  "router_id": "exec.smart_router",
  "market_id": "CLOB:0xabc123",
  "order_type_selected": "GTC",
  "original_price": 0.623,
  "tick_aligned_price": 0.62,
  "original_size_usd": 500,
  "final_size_usd": 450,
  "iceberg_applied": false,
  "signal_age_s": 14,
  "gtd_signal_ttl_s": 120,
  "tick_size": 0.01,
  "inputs_used": [
    "clob.book.top50",
    "gamma_api.market.tick_size"
  ],
  "submission_timestamp": "2026-05-09T11:00:00Z"
}

17. Plain-English Reporting

SituationUser-facing explanation
Order split into smaller piecesYour order was divided into smaller parts that will be submitted one at a time. This reduces the chance that other participants can see and react to a single large order.
Order discarded — signal expiredThe market data used to generate this order was too old by the time the order reached the submission step. The order was not sent to protect you from acting on outdated information.
Order type changed from FOK to GTCThe market did not have enough visible liquidity to guarantee a complete immediate fill, so the order was changed to remain in the book until it fills rather than being cancelled outright.
Price rounded to market tick sizeYour order price was adjusted slightly to align with the smallest price increment this market accepts. The change was less than one tick.

18. Failure-Mode Block

main_failure_modeSubmitting an iceberg child order at the wrong price due to a tick-size calculation error, or sending a GTD order on a stale signal because the TTL check used a clock that was out of sync.
false_positive_riskDiscarding a valid signal as stale because the system clock on the strategy node and the router node are not synchronised, leading to missed trades.
false_negative_riskAllowing an aged signal through because gtd_signal_ttl_s is set too high, resulting in an order being submitted on outdated market intelligence.
safe_fallbackIf Gamma API tick-size data is unavailable, discard the order with STALE_MARKET_DATA rather than submitting at an unverified price. If KillSwitch status cannot be determined, halt submission.
required_dependenciesCLOB top-50 book snapshot, Gamma API tick size and neg-risk flag, PortfolioGuard approved OrderIntent, KillSwitch active flag

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
GAMMA_TICK_SIZE_UNAVAILABLEBlock TCP to gamma-api.polymarket.comIntents discarded with STALE_MARKET_DATA after cache TTL expiresReturns to normal within one evaluation cycle after Gamma API is reachable.
GTD_SIGNAL_EXPIREDDelay intent processing by 130s (gtd_signal_ttl_s=120)Intent discarded with STALE_MARKET_DATA before ExecutionPlan is emittedAutomatic on next fresh intent.
FOK_DOWNGRADESubmit FOK intent when book depth < intent.size_usdExecutionPlan emitted with order_type=GTC and WARN annotationAutomatic.
KILL_SWITCH_ONSet killswitch.active=trueNo ExecutionPlan emitted for any intentReturns to normal on manual KillSwitch reset.
ICEBERG_CHILD_TOO_MANYSet iceberg_child_count=9 (hard max=8)ConfigError PARAMETER_CHANGE_REQUIRES_APPROVAL; config change rejectedSet iceberg_child_count ≤ 8.
NEGRISK_CONVERTSubmit intent with neg_risk=true and negrisk_convert_requested=trueNegRiskConvertRoute emitted instead of CLOB ExecutionPlanAutomatic.

20. State & Persistence

Stateless per evaluation; holds a short-lived in-memory cache of tick sizes and market metadata.

State stores

NameKindKeyValue shapeTTLDurability
tick_size_cachein-memorymarket_id{ minimum_tick_size: float, neg_risk: bool, fetched_at_ms: int }60sbest-effort

Cold-start recovery

On cold start, the cache is empty. First evaluation per market triggers a Gamma API fetch.

On restart

Tick sizes are re-fetched on first evaluation. If Gamma API is unavailable on startup, the first intent for each market is discarded with STALE_MARKET_DATA.

21. Concurrency & Idempotency

AspectSpecification
Execution modelper-OrderIntent goroutine
Max in-flight100
Idempotency keyintent_id
Replay-safeTrue
Deduplicationby intent_id within a 24h window
Ordering guaranteesFIFO per market_id
Per-call timeout (ms)200
Backpressure strategyshed
Locking / mutual exclusionper-market_id mutex (for tick_size_cache)

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchChecked first before any order construction.If active, no ExecutionPlan is emitted.
risk.portfolio_guardProvides the approved OrderIntent with constraints.max_size_usd.Total iceberg children size ≤ constraints.max_size_usd.
sec.contract_address_guardAddress validation must pass before signing.DENY from ContractAddressGuard prevents wallet.sign() from being called.

Emits to (downstream consumers)

BotWhyContract
gov.builder_attributionEvery ExecutionPlan contains a builderCode bytes32 field that BuilderAttribution logs.builder_code field must be present on every signed order.

Used by (auto-aggregated)

2.12 2.13 2.2 2.3 2.4 2.6 6.1

External services

ServiceEndpointSLA assumedOn failure
Gamma APIhttps://gamma-api.polymarket.com99.9% / 500ms p99Discard intent with STALE_MARKET_DATA if tick size is unavailable.
CLOB API (auth)https://clob.polymarket.com99.95% / 200ms p99Retry up to 2 times; discard on persistent failure.
WS user feedwss://ws-subscriptions-clob.polymarket.com/ws/userbest-effortFalls back to REST fill polling; does not block order submission.

23. Security Surfaces

SmartRouter triggers signed orders via the wallet adapter. It never holds private keys. The builder code is injected as a bytes32 field on every order.

Signing surface

This bot triggers signed orders via the wallet adapter — never holds keys. buildOrderTypedData produces the EIP-712 payload; the wallet adapter performs the signing.

On-chain contract calls

ContractMethodNetworkEffect
CTFExchangeV2matchOrders(...)polygonSmartRouter produces the signed order payload that is submitted to CTFExchangeV2.matchOrders() via the CLOB API.

Abuse vectors considered

  • Injecting a modified intent to flip the side (BUY→SELL) between guardrail approval and ExecutionPlan emit
  • Replaying a stale signed order after the GTD TTL has expired
  • Clock skew causing gtd_signal_ttl_s check to pass on an aged signal

Mitigations

  • ExecutionPlan invariant check asserts side == intent.side before emitting
  • per-intent_id deduplication prevents replay within 24h
  • Clock synchronisation via NTP; TTL check uses server-side wall clock
  • buildOrderTypedData includes current timestamp(ms) in V2 order fields, making replayed signatures invalid once the timestamp window passes

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 readyno
SDK used@polymarket/clob-client-v2 ^2.x
Settlement contractCTFExchangeV2 on Polygon
NotesV2 order fields: timestamp(ms) + metadata(bytes32) + builder(bytes32). Fees are NOT on the signed order — they are operator-set at match time by CTFExchangeV2. Taker max 100 bps, maker max 50 bps, 1 bp granularity. builder_fee = notional * bps / 10000. NegRisk convert-arb routes go through NegRiskAdapter, not CTFExchangeV2.

API surfaces declared

clob_authws_userclob_publicgamma_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. buildOrderTypedData updated to V2 schema (timestamp/metadata/builder). Removed feeRateBps and nonce from order construction. HMAC builder replaced with on-order builderCode bytes32. EIP-712 domain version updated to '2'. NegRisk convert-arb route added.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Apply GTC when book depth is below FOK fill thresholdintent.order_type=FOK, visible_depth_usd=300, size_usd=350ExecutionPlan.order_type=GTC (FOK downgraded)
Split into iceberg when size exceeds thresholdsize_usd=600, iceberg_threshold_usd=500, iceberg_child_count=3ExecutionPlan.iceberg=true, children=[200, 200, 200]
Discard order when GTD signal is expiredsignal_age_s=150, gtd_signal_ttl_s=120, order_type=GTDNo ExecutionPlan emitted; discard logged with STALE_MARKET_DATA
Round price to tick size correctlyintent.price=0.623, tick_size=0.01ExecutionPlan.tick_aligned_price=0.62
Preserve side, market_id, and outcome from original intentintent.side=SELL, market_id=0xabc, outcome=NOExecutionPlan.side=SELL, market_id=0xabc, outcome=NO unchanged
Discard order when KillSwitch is activekillswitch.active=trueNo ExecutionPlan emitted

Integration Tests

TestExpected result
End-to-end: approved OrderIntent from guardrail pipeline produces valid ExecutionPlanExecutionPlan has tick-aligned price, correct order type, and respects all guardrail constraints
Iceberg children submitted sequentially as previous fillsSecond child order not submitted until first fill confirmation received
Tick-size unavailability causes discard rather than unaligned submissionSTALE_MARKET_DATA discard when Gamma API is unreachable

Property Tests

PropertyRequired behaviour
ExecutionPlan side always equals OrderIntent side — SmartRouter never flips directionAlways true
ExecutionPlan total size never exceeds the Risk-approved max_size_usdAlways true — sum of all iceberg children ≤ constraints.max_size_usd
No ExecutionPlan is emitted when KillSwitch is activeAlways true

27. Operational Runbook

SmartRouter incidents are typically stale tick-size data or elevated discard rates. Signing failures are escalated immediately.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SmartRouterHighDiscardRateCheck discard reason breakdown in Grafana.If STALE_MARKET_DATA: Gamma API or CLOB may be degraded. If signal age high: check strategy latency.If Gamma API is down, pause strategies to avoid wasted intents. Restore connectivity.Exec pod lead after 10 minutes of sustained high discard rate.
SmartRouterTickSizeUnavailableConfirm Gamma API is reachable.If unreachable, all tick-size cache TTLs have expired and orders are being discarded.Restore Gamma API. Do not hard-code tick sizes.Infra on-call if Gamma API is down > 5 minutes.
SmartRouterHighLatencyCheck CLOB POST /order latency.High submission latency may indicate CLOB congestion or a saturated wallet adapter.Reduce concurrent intents in flight. Check CLOB status page.Exec pod lead if p99 > 500ms sustained.

Manual overrides

  • polytraders bot pause exec.smart_router — Stops emitting ExecutionPlans. No orders will be signed or submitted.
  • polytraders bot flush-cache exec.smart_router --market <market_id> — Evicts the tick-size cache for a specific market, forcing a fresh Gamma API fetch.

Healthcheck

GET /health → 200 if Gamma API tick sizes are fresh (< 60s) for all active markets and wallet adapter is responsive.

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 side invariant and all acceptance_tests.unit casesCI test run100% pass
buildOrderTypedData produces valid V2 EIP-712 payload in integration testIntegration testPass

Promote to Limited live

GateHow measuredThreshold
Iceberg split test: children sum ≤ max_size_usdIntegration testPass
p99 eval latency < 200ms over 24hpolytraders_exec_smartrouter_eval_latency_ms histogramp99 < 200ms

Promote to General live

GateHow measuredThreshold
E2E: approved intent → signed V2 order → fill confirmation on Polygon testnetE2E testPass
NegRisk convert-arb route verified in stagingE2E testPass

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