{
  "schema_version": "1.0.0",
  "bot_id": "2.1",
  "bot_name": "SmartRouter",
  "slug": "smartrouter",
  "layer": "Execution",
  "layer_key": "exec",
  "bot_class": "Execution Utility",
  "authority": [
    "Reshape"
  ],
  "status": "live",
  "readiness": "General live",
  "flagship": false,
  "is_reference": true,
  "public_export": false,
  "identity": {
    "layer": "Execution",
    "bot_class": "Execution Utility",
    "authority": "Reshape",
    "runs_before": "Order signing and submission",
    "runs_after": "Risk guardrail pipeline (all RiskVotes collected)",
    "applies_to": "Every approved OrderIntent that has passed the full Risk guardrail pipeline",
    "default_mode": "general_live",
    "user_visible": "Advanced details only",
    "developer_owner": "Polytraders core \u2014 Execution pod"
  },
  "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 \u2014 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.",
  "why_it_matters": [
    {
      "failure": "Wrong order type applied",
      "consequence": "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.",
      "worked_example": {
        "setup": "Strategy emits an OrderIntent for 6,000 pUSD on YES at \u2264 0.55 with TIF=GTC. Top of book is 0.54 with 2,200 pUSD depth, second level is 0.56 with 9,000 pUSD.",
        "without_bot": "A naive router sends the full 6,000 as a market order. It clears the 0.54 ask, walks to 0.56, fills the remaining 3,800 there, and ends up at a 0.553 average \u2014 a 30 bps slippage the strategy never modelled.",
        "with_bot": "SmartRouter sees the depth profile, splits into 2,200 at 0.54 IOC + 3,800 as a passive 0.55 GTC, and only crosses the second level if the 0.55 leg ages out. The fill respects the price cap and the strategy's expected edge survives execution."
      }
    },
    {
      "failure": "Large order submitted without iceberg splitting",
      "consequence": "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."
    },
    {
      "failure": "Order submitted after signal TTL expires",
      "consequence": "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."
    },
    {
      "failure": "Tick size not respected",
      "consequence": "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."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "CLOB order book \u2014 top 50 levels",
      "source": "CLOB",
      "required": true,
      "use": "Assess current book depth to decide between FOK and GTC, and to determine whether iceberg splitting is warranted."
    },
    {
      "input": "Market tick size and neg-risk flag",
      "source": "Gamma API",
      "required": true,
      "use": "Round order price to the correct tick size; apply neg-risk routing rules when the neg-risk flag is set."
    },
    {
      "input": "Recent fill rate and estimated queue position",
      "source": "Data API",
      "required": false,
      "use": "Estimate time-to-fill for GTC orders and decide whether to use a more aggressive price to improve queue position."
    }
  ],
  "internal_inputs": [
    {
      "input": "Approved OrderIntent with all Risk constraints applied",
      "source": "PortfolioGuard",
      "required": true,
      "use": "Receive the final approved size, maximum price, and any constraint flags (passive_only, close_only) from the guardrail pipeline."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "Abort order construction and emit no ExecutionPlan if KillSwitch is active."
    }
  ],
  "raw_params": [
    "default_order_type \u00b7 enum (FOK, GTC, GTD)",
    "iceberg_threshold_usd \u00b7 int",
    "iceberg_child_count \u00b7 int",
    "gtd_signal_ttl_s \u00b7 int"
  ],
  "parameters": [
    {
      "name": "default_order_type",
      "default": "GTC",
      "warning": "\u2014",
      "hard": "\u2014",
      "controls": "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.",
      "why_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": [
        {
          "condition": "Order size \u2264 top-of-book depth and execution is time-sensitive",
          "action": "Use FOK"
        },
        {
          "condition": "Order size fits in book with no urgency",
          "action": "Use GTC (default)"
        },
        {
          "condition": "Signal has a defined expiry (gtd_signal_ttl_s > 0)",
          "action": "Use GTD with calculated expiry timestamp"
        }
      ],
      "dev_check": "const type = intent.order_type || p.default_order_type; if (type === 'GTD') plan.expiry = now + p.gtd_signal_ttl_s;",
      "user_facing": "We chose the order type that best matches current market conditions to improve your chance of getting a good fill."
    },
    {
      "name": "iceberg_threshold_usd",
      "default": 500,
      "warning": 300,
      "hard": 1000,
      "controls": "Order size in USD above which the order is automatically split into iceberg child orders to reduce visible market impact.",
      "why_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": [
        {
          "condition": "order.size_usd \u2264 500 USD",
          "action": "Submit as single order"
        },
        {
          "condition": "500\u20131000 USD",
          "action": "Split into iceberg_child_count children"
        },
        {
          "condition": "> 1000 USD hard ceiling (locked by Risk)",
          "action": "Should not reach here \u2014 Risk guardrail should have capped to 1000 USD max"
        }
      ],
      "dev_check": "if (order.size_usd > p.iceberg_threshold_usd) return splitIceberg(order, p.iceberg_child_count);",
      "user_facing": "Your order was split into smaller pieces to reduce its visibility in the market and get a better overall fill."
    },
    {
      "name": "iceberg_child_count",
      "default": 3,
      "warning": 5,
      "hard": 8,
      "controls": "Number of child orders to split an iceberg order into. Each child is submitted sequentially as the previous one fills.",
      "why_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": [
        {
          "condition": "iceberg_child_count \u2264 5",
          "action": "Normal iceberg split"
        },
        {
          "condition": "5\u20138",
          "action": "WARN \u2014 high child count increases submission overhead"
        },
        {
          "condition": "> 8",
          "action": "Reject config change \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.iceberg_child_count > p.hard) throw new ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL');",
      "user_facing": "Your order was broken into several smaller orders that will be submitted one after another."
    },
    {
      "name": "gtd_signal_ttl_s",
      "default": 120,
      "warning": 60,
      "hard": 300,
      "controls": "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.",
      "why_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": [
        {
          "condition": "Signal age \u2264 gtd_signal_ttl_s",
          "action": "Proceed with GTD order"
        },
        {
          "condition": "Signal age > gtd_signal_ttl_s",
          "action": "Discard order \u2014 STALE_MARKET_DATA"
        }
      ],
      "dev_check": "if (Date.now() - intent.generated_at > p.gtd_signal_ttl_s * 1000) return discard('STALE_MARKET_DATA');",
      "user_facing": "An order was not submitted because the market information it was based on had expired before the order could be sent."
    }
  ],
  "default_config": {
    "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
      }
    }
  },
  "implementation_flow": [
    "Receive the approved OrderIntent from the Risk guardrail pipeline, including applied constraints (max_size_usd, passive_only, close_only) from PortfolioGuard.",
    "Check KillSwitch active flag; if active, discard the order and emit no ExecutionPlan.",
    "Fetch the market's tick size and neg-risk flag from Gamma API; round order.price to the nearest valid tick.",
    "Determine the order type: use intent.order_type if specified; otherwise apply default_order_type logic based on book depth and signal age.",
    "If order type is GTD, check signal age against gtd_signal_ttl_s; if expired, discard with STALE_MARKET_DATA.",
    "Check order.size_usd against iceberg_threshold_usd; if above threshold, compute child_size = size / iceberg_child_count and build an iceberg plan.",
    "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.",
    "Assemble the ExecutionPlan with: order_type, price (tick-aligned), size or iceberg children array, side (unchanged from intent), market_id (unchanged), and submission_timestamp.",
    "Validate that the ExecutionPlan does not alter side, market_id, or outcome leg from the original intent \u2014 these are invariants.",
    "Emit the ExecutionPlan to the order signing step."
  ],
  "decision_logic": {
    "approve": "Not directly applicable \u2014 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 \u2014 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."
  },
  "decision_output_schema": "ExecutionPlan",
  "decision_output_example": {
    "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"
    ]
  },
  "developer_log": {
    "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"
  },
  "user_explanations": [
    {
      "situation": "Order split into smaller pieces",
      "message": "Your 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."
    },
    {
      "situation": "Order discarded \u2014 signal expired",
      "message": "The 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."
    },
    {
      "situation": "Order type changed from FOK to GTC",
      "message": "The 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."
    },
    {
      "situation": "Price rounded to market tick size",
      "message": "Your order price was adjusted slightly to align with the smallest price increment this market accepts. The change was less than one tick."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Submitting 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_risk": "Discarding 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_risk": "Allowing an aged signal through because gtd_signal_ttl_s is set too high, resulting in an order being submitted on outdated market intelligence.",
    "safe_fallback": "If 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_dependencies": [
      "CLOB top-50 book snapshot",
      "Gamma API tick size and neg-risk flag",
      "PortfolioGuard approved OrderIntent",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Apply GTC when book depth is below FOK fill threshold",
        "setup": "intent.order_type=FOK, visible_depth_usd=300, size_usd=350",
        "expected": "ExecutionPlan.order_type=GTC (FOK downgraded)"
      },
      {
        "test": "Split into iceberg when size exceeds threshold",
        "setup": "size_usd=600, iceberg_threshold_usd=500, iceberg_child_count=3",
        "expected": "ExecutionPlan.iceberg=true, children=[200, 200, 200]"
      },
      {
        "test": "Discard order when GTD signal is expired",
        "setup": "signal_age_s=150, gtd_signal_ttl_s=120, order_type=GTD",
        "expected": "No ExecutionPlan emitted; discard logged with STALE_MARKET_DATA"
      },
      {
        "test": "Round price to tick size correctly",
        "setup": "intent.price=0.623, tick_size=0.01",
        "expected": "ExecutionPlan.tick_aligned_price=0.62"
      },
      {
        "test": "Preserve side, market_id, and outcome from original intent",
        "setup": "intent.side=SELL, market_id=0xabc, outcome=NO",
        "expected": "ExecutionPlan.side=SELL, market_id=0xabc, outcome=NO unchanged"
      },
      {
        "test": "Discard order when KillSwitch is active",
        "setup": "killswitch.active=true",
        "expected": "No ExecutionPlan emitted"
      }
    ],
    "integration": [
      {
        "test": "End-to-end: approved OrderIntent from guardrail pipeline produces valid ExecutionPlan",
        "expected": "ExecutionPlan has tick-aligned price, correct order type, and respects all guardrail constraints"
      },
      {
        "test": "Iceberg children submitted sequentially as previous fills",
        "expected": "Second child order not submitted until first fill confirmation received"
      },
      {
        "test": "Tick-size unavailability causes discard rather than unaligned submission",
        "expected": "STALE_MARKET_DATA discard when Gamma API is unreachable"
      }
    ],
    "property": [
      {
        "property": "ExecutionPlan side always equals OrderIntent side \u2014 SmartRouter never flips direction",
        "required": "Always true"
      },
      {
        "property": "ExecutionPlan total size never exceeds the Risk-approved max_size_usd",
        "required": "Always true \u2014 sum of all iceberg children \u2264 constraints.max_size_usd"
      },
      {
        "property": "No ExecutionPlan is emitted when KillSwitch is active",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Translate strategy intent into the right order type, price, and timing.",
  "legacy_pm_signals": [
    "Real-time book depth on the target market",
    "Recent fill rate & queue-position estimate",
    "client.getMarket() tick size + neg-risk flag (per order)"
  ],
  "legacy_external_feeds": [],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_auth",
    "ws_user",
    "clob_public",
    "gamma_api"
  ],
  "reference_implementation": {
    "summary": "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 \u2014 never holds keys.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "FUNCTION routeOrder(intent, riskConstraints):\n  // --- 0. KillSwitch gate ---\n  ks = FETCH internal.killswitch.status\n  IF ks.active:\n    DISCARD intent\n    RETURN\n\n  // --- 1. Fetch market tick size and neg-risk flag ---\n  market = fetchClobPublic('/markets/' + intent.market_id)\n  IF market IS NULL OR isStale(market, 60):\n    DISCARD intent; reason=STALE_MARKET_DATA\n    RETURN\n\n  tickSize = market.minimum_tick_size  // e.g. 0.01\n  negRisk = market.neg_risk\n\n  // --- 2. GTD TTL check ---\n  signalAgeMs = now_ms() - intent.generated_at_ms\n  IF intent.order_type == 'GTD' AND signalAgeMs > params.gtd_signal_ttl_s * 1000:\n    DISCARD intent; reason=STALE_MARKET_DATA\n    RETURN\n\n  // --- 3. Tick-align price ---\n  alignedPrice = floor(intent.price / tickSize) * tickSize\n\n  // --- 4. Order type selection ---\n  book = fetchClobPublic('/book?market=' + intent.market_id)\n  bookDepth = SUM(level.size FOR level IN book.asks[:50])\n  IF intent.order_type == 'FOK' AND bookDepth < intent.size_usd:\n    orderType = 'GTC'  // downgrade FOK \u2192 GTC on thin book\n  ELSE:\n    orderType = intent.order_type OR params.default_order_type\n\n  // --- 5. NegRisk convert-arb routing ---\n  IF negRisk AND intent.negrisk_convert_requested:\n    EMIT NegRiskConvertRoute(market_id=intent.market_id, size=intent.size_usd)\n    RETURN  // NegRiskAdapter path; not a CLOB order\n\n  // --- 6. Iceberg split ---\n  finalSize = min(intent.size_usd, riskConstraints.max_size_usd)\n  IF finalSize > params.iceberg_threshold_usd:\n    childSize = toUsdcUnits(finalSize / params.iceberg_child_count)\n    children = [childSize] * params.iceberg_child_count\n  ELSE:\n    children = [toUsdcUnits(finalSize)]\n\n  // --- 7. Build V2 typed order data ---\n  // V2 order fields: timestamp(ms) + metadata(bytes32) + builder(bytes32)\n  // Fees are operator-set at match time; NOT in the signed order\n  FOR child IN children:\n    typedData = buildOrderTypedData({\n      market_id: intent.market_id,\n      side: intent.side,  // NEVER flipped\n      outcome: intent.outcome,\n      price: alignedPrice,\n      size: child,\n      order_type: orderType,\n      timestamp: now_ms(),\n      metadata: ZERO_BYTES32,\n      builder: config.builder_code_bytes32\n    }, domain={ version: '2', chainId: 137, verifyingContract: CTFExchangeV2 })\n    // wallet adapter signs typedData \u2014 never holds keys\n    signedOrder = wallet.sign(typedData)\n    VALIDATE ExecutionPlan invariants:\n      assert signedOrder.side == intent.side\n      assert signedOrder.market_id == intent.market_id\n      assert signedOrder.size <= riskConstraints.max_size_usd\n\n  // --- 8. Emit ExecutionPlan ---\n  EMIT ExecutionPlan(\n    router_id='exec.smart_router',\n    market_id=intent.market_id,\n    side=intent.side,\n    order_type=orderType,\n    tick_aligned_price=alignedPrice,\n    iceberg=(len(children) > 1),\n    children=children,\n    submission_timestamp=now_iso()\n  )\n",
    "helpers": [
      {
        "name": "buildOrderTypedData",
        "signature": "buildOrderTypedData(orderParams, domain) -> TypedData",
        "purpose": "Constructs the EIP-712 V2 typed data for a CLOB order. V2 fields: timestamp(ms), metadata(bytes32), builder(bytes32). No feeRateBps \u2014 fees are operator-set at match time."
      },
      {
        "name": "toUsdcUnits",
        "signature": "toUsdcUnits(rawUsd: float) -> int",
        "purpose": "Rounds a raw pUSD float to the integer unit used by CTFExchangeV2 (6 decimals)."
      },
      {
        "name": "fetchClobPublic",
        "signature": "fetchClobPublic(path: str) -> JSON",
        "purpose": "Unauthenticated GET against https://clob.polymarket.com; returns parsed JSON or null on error."
      },
      {
        "name": "isStale",
        "signature": "isStale(snapshot: any, maxAgeS: int) -> bool",
        "purpose": "Returns true if snapshot was fetched more than maxAgeS seconds ago."
      },
      {
        "name": "platformFee",
        "signature": "platformFee(notional: float, prob: float, feeRate: float) -> float",
        "purpose": "Estimates the platform fee C*feeRate*p*(1-p) for a given trade; used to log expected cost alongside the ExecutionPlan."
      }
    ],
    "sdk_calls": [
      "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"
  },
  "wire_examples": {
    "input": [
      {
        "label": "Approved OrderIntent from Risk pipeline",
        "source": "internal",
        "payload": {
          "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": [
      {
        "label": "ExecutionPlan \u2014 single GTC order, tick-aligned",
        "payload": {
          "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"
          ]
        }
      },
      {
        "label": "ExecutionPlan \u2014 iceberg split (3 children)",
        "payload": {
          "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"
          ]
        }
      }
    ],
    "curl": "curl -H 'Authorization: Bearer <token>' -X POST 'https://clob.polymarket.com/order' -d '{...signed_order...}'"
  },
  "reason_codes": [
    {
      "code": "STALE_MARKET_DATA",
      "severity": "HARD_REJECT",
      "meaning": "Market metadata (tick size) is unavailable, or GTD signal has aged past gtd_signal_ttl_s.",
      "action": "Discard order; emit no ExecutionPlan.",
      "user_message": "The order could not be placed because the market data or signal had expired."
    },
    {
      "code": "SPREAD_TOO_WIDE",
      "severity": "WARN",
      "meaning": "Tick-size rounding shifted price by more than one tick.",
      "action": "Attach warning annotation to ExecutionPlan; do not block.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch is active; no orders may proceed.",
      "action": "Discard order; emit no ExecutionPlan.",
      "user_message": "Trading is currently paused."
    },
    {
      "code": "PARAMETER_CHANGE_REQUIRES_APPROVAL",
      "severity": "HARD_REJECT",
      "meaning": "iceberg_child_count or gtd_signal_ttl_s exceeds the locked hard maximum.",
      "action": "Reject config change; do not apply.",
      "user_message": ""
    },
    {
      "code": "NEGRISK_CONVERT_AVAILABLE",
      "severity": "EXPLAIN",
      "meaning": "NegRisk convert-arb route is available for this market; notifies Strategy layer.",
      "action": "Emit NegRiskConvertRoute instead of a CLOB order when negrisk_convert_requested=true.",
      "user_message": ""
    },
    {
      "code": "SMART_ROUTER_FOK_DOWNGRADE",
      "severity": "RESHAPE",
      "meaning": "Book depth was insufficient to guarantee a complete FOK fill; order was changed to GTC.",
      "action": "Set order_type=GTC in the ExecutionPlan.",
      "user_message": "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."
    },
    {
      "code": "SMART_ROUTER_ICEBERG_SPLIT",
      "severity": "RESHAPE",
      "meaning": "Order size exceeded iceberg_threshold_usd; split into child orders.",
      "action": "Set iceberg=true and populate children array.",
      "user_message": "Your order was split into smaller pieces to reduce its visibility in the market."
    },
    {
      "code": "FEE_RATE_CAPPED",
      "severity": "WARN",
      "meaning": "Computed 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.",
      "action": "Log the fee estimate; do not block the order.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_exec_smartrouter_plans_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "order_type",
          "iceberg"
        ],
        "meaning": "Total ExecutionPlans emitted by order type and iceberg flag."
      },
      {
        "name": "polytraders_exec_smartrouter_discards_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "reason_code"
        ],
        "meaning": "Total intents discarded (no ExecutionPlan emitted) by reason."
      },
      {
        "name": "polytraders_exec_smartrouter_signal_age_seconds",
        "type": "histogram",
        "unit": "seconds",
        "labels": [],
        "meaning": "Age of the OrderIntent signal at routing time; tracks freshness of strategy decisions."
      },
      {
        "name": "polytraders_exec_smartrouter_tick_rounding_ticks",
        "type": "histogram",
        "unit": "count",
        "labels": [
          "market_id"
        ],
        "meaning": "Number of ticks by which the price was rounded; > 1 tick triggers a WARN."
      },
      {
        "name": "polytraders_exec_smartrouter_iceberg_child_count",
        "type": "histogram",
        "unit": "count",
        "labels": [],
        "meaning": "Distribution of iceberg child counts per split order."
      },
      {
        "name": "polytraders_exec_smartrouter_eval_latency_ms",
        "type": "histogram",
        "unit": "seconds",
        "labels": [],
        "meaning": "Wall-clock latency from intent receipt to ExecutionPlan emit."
      }
    ],
    "alerts": [
      {
        "name": "SmartRouterHighDiscardRate",
        "condition": "rate(polytraders_exec_smartrouter_discards_total[5m]) / rate(polytraders_exec_smartrouter_plans_total[5m]) > 0.2",
        "severity": "P2",
        "runbook": "#runbook-smartrouter-discards"
      },
      {
        "name": "SmartRouterStaleSignals",
        "condition": "histogram_quantile(0.99, rate(polytraders_exec_smartrouter_signal_age_seconds_bucket[5m])) > 60",
        "severity": "P2",
        "runbook": "#runbook-smartrouter-stale-signals"
      },
      {
        "name": "SmartRouterHighLatency",
        "condition": "histogram_quantile(0.99, rate(polytraders_exec_smartrouter_eval_latency_ms_bucket[5m])) > 200",
        "severity": "P2",
        "runbook": "#runbook-smartrouter-latency"
      },
      {
        "name": "SmartRouterTickSizeUnavailable",
        "condition": "rate(polytraders_exec_smartrouter_discards_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.05",
        "severity": "P1",
        "runbook": "#runbook-smartrouter-tick-size"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Execution / SmartRouter",
      "Grafana \u2014 Order quality / tick rounding and iceberg split distribution"
    ],
    "log_levels": {
      "DEBUG": "Tick-size computation, iceberg child sizes, and order type selection logic on every intent.",
      "INFO": "ExecutionPlan emitted with order_type, tick_aligned_price, iceberg flag.",
      "WARN": "Tick-size rounding > 1 tick; FOK downgraded to GTC; signal age approaching TTL.",
      "ERROR": "Gamma API tick-size unavailable; KillSwitch status unreadable; wallet adapter signing failure."
    }
  },
  "state": {
    "summary": "Stateless per evaluation; holds a short-lived in-memory cache of tick sizes and market metadata.",
    "stores": [
      {
        "name": "tick_size_cache",
        "kind": "in-memory",
        "key": "market_id",
        "value": "{ minimum_tick_size: float, neg_risk: bool, fetched_at_ms: int }",
        "ttl": "60s",
        "durability": "best-effort"
      }
    ],
    "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."
  },
  "concurrency": {
    "execution_model": "per-OrderIntent goroutine",
    "max_in_flight": 100,
    "idempotency_key": "intent_id",
    "replay_safe": true,
    "deduplication": "by intent_id within a 24h window",
    "ordering_guarantees": "FIFO per market_id",
    "timeout_ms": 200,
    "backpressure": "shed",
    "locking": "per-market_id mutex (for tick_size_cache)"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Checked first before any order construction.",
        "contract": "If active, no ExecutionPlan is emitted."
      },
      {
        "bot_id": "risk.portfolio_guard",
        "why": "Provides the approved OrderIntent with constraints.max_size_usd.",
        "contract": "Total iceberg children size \u2264 constraints.max_size_usd."
      },
      {
        "bot_id": "sec.contract_address_guard",
        "why": "Address validation must pass before signing.",
        "contract": "DENY from ContractAddressGuard prevents wallet.sign() from being called."
      }
    ],
    "emits_to": [
      {
        "bot_id": "gov.builder_attribution",
        "why": "Every ExecutionPlan contains a builderCode bytes32 field that BuilderAttribution logs.",
        "contract": "builder_code field must be present on every signed order."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "Gamma API",
        "endpoint": "https://gamma-api.polymarket.com",
        "sla": "99.9% / 500ms p99",
        "failure_mode": "Discard intent with STALE_MARKET_DATA if tick size is unavailable."
      },
      {
        "service": "CLOB API (auth)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.95% / 200ms p99",
        "failure_mode": "Retry up to 2 times; discard on persistent failure."
      },
      {
        "service": "WS user feed",
        "endpoint": "wss://ws-subscriptions-clob.polymarket.com/ws/user",
        "sla": "best-effort",
        "failure_mode": "Falls back to REST fill polling; does not block order submission."
      }
    ]
  },
  "security_surfaces": {
    "summary": "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": "This bot triggers signed orders via the wallet adapter \u2014 never holds keys. buildOrderTypedData produces the EIP-712 payload; the wallet adapter performs the signing.",
    "secrets": [],
    "contract_calls": [
      {
        "contract": "CTFExchangeV2",
        "method": "matchOrders(...)",
        "network": "polygon",
        "effect": "SmartRouter produces the signed order payload that is submitted to CTFExchangeV2.matchOrders() via the CLOB API."
      }
    ],
    "abuse_vectors": [
      "Injecting a modified intent to flip the side (BUY\u2192SELL) 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"
    ]
  },
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": true,
    "negrisk_aware": true,
    "multichain_ready": false,
    "sdk_used": "@polymarket/clob-client-v2 ^2.x",
    "settlement_contract": "CTFExchangeV2 on Polygon",
    "notes": "V2 order fields: timestamp(ms) + metadata(bytes32) + builder(bytes32). Fees are NOT on the signed order \u2014 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."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.3",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1 (USDC.e + HMAC builder)",
      "to": "v2 (pUSD + builderCode field)",
      "reason": "Polymarket V2 cutover",
      "action_taken": "Migrated 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."
    }
  ],
  "failure_injection": [
    {
      "scenario": "GAMMA_TICK_SIZE_UNAVAILABLE",
      "how_to_inject": "Block TCP to gamma-api.polymarket.com",
      "expected_behavior": "Intents discarded with STALE_MARKET_DATA after cache TTL expires",
      "recovery": "Returns to normal within one evaluation cycle after Gamma API is reachable."
    },
    {
      "scenario": "GTD_SIGNAL_EXPIRED",
      "how_to_inject": "Delay intent processing by 130s (gtd_signal_ttl_s=120)",
      "expected_behavior": "Intent discarded with STALE_MARKET_DATA before ExecutionPlan is emitted",
      "recovery": "Automatic on next fresh intent."
    },
    {
      "scenario": "FOK_DOWNGRADE",
      "how_to_inject": "Submit FOK intent when book depth < intent.size_usd",
      "expected_behavior": "ExecutionPlan emitted with order_type=GTC and WARN annotation",
      "recovery": "Automatic."
    },
    {
      "scenario": "KILL_SWITCH_ON",
      "how_to_inject": "Set killswitch.active=true",
      "expected_behavior": "No ExecutionPlan emitted for any intent",
      "recovery": "Returns to normal on manual KillSwitch reset."
    },
    {
      "scenario": "ICEBERG_CHILD_TOO_MANY",
      "how_to_inject": "Set iceberg_child_count=9 (hard max=8)",
      "expected_behavior": "ConfigError PARAMETER_CHANGE_REQUIRES_APPROVAL; config change rejected",
      "recovery": "Set iceberg_child_count \u2264 8."
    },
    {
      "scenario": "NEGRISK_CONVERT",
      "how_to_inject": "Submit intent with neg_risk=true and negrisk_convert_requested=true",
      "expected_behavior": "NegRiskConvertRoute emitted instead of CLOB ExecutionPlan",
      "recovery": "Automatic."
    }
  ],
  "runbook": {
    "summary": "SmartRouter incidents are typically stale tick-size data or elevated discard rates. Signing failures are escalated immediately.",
    "oncall_actions": [
      {
        "alert": "SmartRouterHighDiscardRate",
        "first_step": "Check discard reason breakdown in Grafana.",
        "diagnosis": "If STALE_MARKET_DATA: Gamma API or CLOB may be degraded. If signal age high: check strategy latency.",
        "mitigation": "If Gamma API is down, pause strategies to avoid wasted intents. Restore connectivity.",
        "escalation": "Exec pod lead after 10 minutes of sustained high discard rate."
      },
      {
        "alert": "SmartRouterTickSizeUnavailable",
        "first_step": "Confirm Gamma API is reachable.",
        "diagnosis": "If unreachable, all tick-size cache TTLs have expired and orders are being discarded.",
        "mitigation": "Restore Gamma API. Do not hard-code tick sizes.",
        "escalation": "Infra on-call if Gamma API is down > 5 minutes."
      },
      {
        "alert": "SmartRouterHighLatency",
        "first_step": "Check CLOB POST /order latency.",
        "diagnosis": "High submission latency may indicate CLOB congestion or a saturated wallet adapter.",
        "mitigation": "Reduce concurrent intents in flight. Check CLOB status page.",
        "escalation": "Exec pod lead if p99 > 500ms sustained."
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders bot pause exec.smart_router",
        "effect": "Stops emitting ExecutionPlans. No orders will be signed or submitted."
      },
      {
        "command": "polytraders bot flush-cache exec.smart_router --market <market_id>",
        "effect": "Evicts the tick-size cache for a specific market, forcing a fresh Gamma API fetch."
      }
    ],
    "healthcheck": "GET /health \u2192 200 if Gamma API tick sizes are fresh (< 60s) for all active markets and wallet adapter is responsive."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass including side invariant and all acceptance_tests.unit cases",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "buildOrderTypedData produces valid V2 EIP-712 payload in integration test",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Iceberg split test: children sum \u2264 max_size_usd",
        "how_measured": "Integration test",
        "threshold": "Pass"
      },
      {
        "gate": "p99 eval latency < 200ms over 24h",
        "how_measured": "polytraders_exec_smartrouter_eval_latency_ms histogram",
        "threshold": "p99 < 200ms"
      }
    ],
    "to_general_live": [
      {
        "gate": "E2E: approved intent \u2192 signed V2 order \u2192 fill confirmation on Polygon testnet",
        "how_measured": "E2E test",
        "threshold": "Pass"
      },
      {
        "gate": "NegRisk convert-arb route verified in staging",
        "how_measured": "E2E test",
        "threshold": "Pass"
      }
    ]
  },
  "reporting_groups": [
    "execution"
  ],
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 5,
    "phase_name": "Execution rails",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}