# 0DTESPX > 0DTESPX.com is a paper-trading platform for zero-days-to-expiration (0DTE) SPX index options. Replay any past trading day in an account-less, fixed-$100k practice sandbox, or paper-trade today's market on a persistent live account with a continuous cash balance; track positions with live delta and P&L, build and backtest strategies, and drive it all through a public HTTP + WebSocket API. ## Getting started - [Overview](https://www.0dtespx.com/learn): What 0DTESPX.com is and how the platform fits together. - [Quick start](https://www.0dtespx.com/learn/quick-start): Run your first paper-trading session in a few minutes. - [Access & accounts](https://www.0dtespx.com/learn/access): What visitors and registered users can do. ## Core concepts - [0DTE & SPX options](https://www.0dtespx.com/learn/options-basics): A primer on zero-days-to-expiration SPX index options. - [Accounts & trading days](https://www.0dtespx.com/learn/accounts): Persistent accounts with a continuous balance, traded one day (session) at a time. - [Option chain & Greeks](https://www.0dtespx.com/learn/option-chain): Reading bids, asks, mid prices, and live delta across every strike. - [Orders & order types](https://www.0dtespx.com/learn/orders): Market, limit, stop, and multi-leg orders and how they fill. - [Positions & P&L](https://www.0dtespx.com/learn/positions): Tracking direction, cost basis, and realized vs unrealized P&L. - [Buying power & margin](https://www.0dtespx.com/learn/buying-power): FINRA maintenance-margin rules for shorts, spreads, and condors. - [Fees](https://www.0dtespx.com/learn/fees): The brokerage fee schedule applied to every order. - [End-of-day settlement](https://www.0dtespx.com/learn/settlement): What happens to open options at the 4:00 PM ET close. - [Market data & charts](https://www.0dtespx.com/learn/market-data): Live and historical SPX/VIX data, option chains, and overlays. ## Research - [How to research a strategy](https://www.0dtespx.com/learn/research-guide): Take an idea from backtest to live: the Research → Trade funnel. - [Strategy builder](https://www.0dtespx.com/learn/strategy-builder): Define a 0DTE strategy with a structured form. - [AI strategy assistant](https://www.0dtespx.com/learn/ai-assistant): Describe a strategy in plain English and let the AI author it. - [Strategy results](https://www.0dtespx.com/learn/backtesting): Every saved strategy owns backtest results over the full history; its orders are priced and re-priced by the platform exactly as a bot's are (same spread-aware start, same retry cadences at the platform defaults, same fill confirmation), so the strategy's own limit price is a trigger rather than the price its order rests at. - [Portfolios](https://www.0dtespx.com/learn/portfolios): Combine saved strategies into one aggregated results view with equal $100k sleeves. ## Practice - [Practice](https://www.0dtespx.com/learn/practice): Rehearse trades by hand on any past day, with a clock you control. ## Trade - [Live trading](https://www.0dtespx.com/learn/live-trading): Paper trade today's session against the real-time tick stream. ## Replay - [Replay](https://www.0dtespx.com/learn/replay): Scrub back through a settled live day — a read-only review of the day you traded. ## Automate - [Trading bots](https://www.0dtespx.com/learn/bots): Run a saved strategy hands-free against the live session. ## API reference - [Overview & authentication](https://www.0dtespx.com/learn/api/overview): Base URL, auth, access levels, and conventions. - [API walkthrough](https://www.0dtespx.com/learn/api/walkthrough): An end-to-end paper-trading tutorial with copy-pasteable curl examples. - [Rate limits](https://www.0dtespx.com/learn/api/rate-limits): The market-data credit budget and per-endpoint costs. - [Accounts & sessions API](https://www.0dtespx.com/learn/api/accounts): Create accounts, open sessions, advance the clock, and read financials. - [Orders API](https://www.0dtespx.com/learn/api/orders): Place, preview, and cancel orders, including multi-leg, and liquidate every open position at once. - [Positions & transactions](https://www.0dtespx.com/learn/api/positions-transactions): Read computed positions with delta and P&L, and the transaction log. - [Market data API](https://www.0dtespx.com/learn/api/market-data): Sessions, strikes, history, and option-chain snapshots. - [Live trading API](https://www.0dtespx.com/learn/api/live-trading): Trade today's session: orders, positions, idempotency. - [Strategies API](https://www.0dtespx.com/learn/api/strategies): Preview, save, and read strategies and their backtest results. - [Portfolios API](https://www.0dtespx.com/learn/api/portfolios): Combine saved strategies into portfolios with read-time aggregated results. - [Bots API](https://www.0dtespx.com/learn/api/bots): Run a saved strategy as a live bot: sessions, lifecycle, and per-session data. - [WebSocket streams](https://www.0dtespx.com/learn/api/websocket): Real-time market data and per-session event channels. ## Reference - [Glossary](https://www.0dtespx.com/learn/glossary): Definitions for the terms used across the platform. - [FAQ](https://www.0dtespx.com/learn/faq): Answers to the questions we hear most often. ## API > The HTTP API exposes trading accounts (persistent, cash-only, continuous balance) for live/bot trading and their sessions (one trading day each), an account-less practice surface for replaying trading days (fixed $100k), orders, positions, transactions, market data, saved strategies (each owning its backtest results), auth flows, and a WebSocket channel for live data and per-session event streams. Errors carry the appropriate HTTP status code; most bodies are a small JSON envelope (`{"message": "…"}`, plus a machine-readable `error` code on rejections worth branching on), while auth-gate 401s, login failures, and live-order rejections relayed from the exchange are short plain-text bodies. Authentication is a bearer session token from `POST /auth/sessions`, sent as the bare value of the `Authorization` header (no `Bearer ` prefix). Base URL: https://api.0dtespx.com. - [OpenAPI specification](https://www.0dtespx.com/openapi.yaml): Full HTTP and WebSocket reference — paths, parameters, request and response schemas, status codes, and security schemes (OpenAPI 3.0.3). - [API walkthrough](https://www.0dtespx.com/learn/api/walkthrough): End-to-end walkthrough with copy-pasteable `curl` examples — register, log in, list market sessions, create an account, open a session, advance the clock, place market/limit/stop orders, view positions/Greeks/P&L, end-of-day settlement, and the WebSocket live data feed. - [API overview](https://www.0dtespx.com/learn/api/overview): Base URL, authentication, access levels, conventions, and status codes for the HTTP + WebSocket API. ## API — core concepts - [Accounts & trading days](https://www.0dtespx.com/learn/accounts): An account is a persistent, cash-only container with a continuous balance that carries day to day, fixed at creation to the `live_sim` engine (today's real session; the only engine that ships); `broker_*` engines are reserved for upcoming broker integrations but not yet available (409); `bot`-type accounts are created via `POST /bots`. Accounts are for LIVE/BOT trading only. Replaying past trading days is ACCOUNT-LESS — the `/practice/sessions` surface, a one-click fixed-$100k sandbox with no account and no starting-capital prompt. A session is one trading day (under an account for live/bot; account-less for practice). `cash_balance` is the settled carry (never folds in unsettled intraday P&L); `current_value` is the live NLV while a day is open, else `cash_balance`. On a practice session, advancing time recomputes positions, delta, P&L, and buying power; per-second history is materialized on order create or delete; crossing the 16:00 ET close settles the day (practice days are independent — no carried balance, no re-thread — and a date can hold multiple independent simulations). 0DTE SPX-index only. - [Order lifecycle](https://www.0dtespx.com/learn/orders): Placement, validation, creation (with pre-computed `fill_datetime`, `fill_price`, `fill_price_effect`, and settlement transactions), resting on the book (`live`) → filled — a `limit` whose `price` is STRICTLY PAST the market at placement fills IN the POST and comes back `filled` instead of resting, while every other limit and every stop is accepted `live` first (see fill confirmation below) — optional deletion, 16:00 ET settlement. - [Order types](https://www.0dtespx.com/learn/orders): Market, limit, and stop. ONLY limit orders may be multi-leg — market and stop orders must be single-leg (a multi-leg market/stop is rejected with HTTP 400). Stop orders that would trigger immediately at placement are rejected. - [Fill confirmation — immediate when the price is past the market, else 3 consecutive seconds](https://www.0dtespx.com/learn/orders): a limit order's fill condition, and a stop's trigger, are evaluated ONCE PER SECOND. A limit whose `price` is STRICTLY PAST the market — better than the quoted market, not merely LEVEL with it — fills IMMEDIATELY on the second the crossing holds, with NO consecutive-seconds wait; which second that is decides the price (see fill price rules). At PLACEMENT it means the `POST` itself comes back `filled`, `fill_datetime` stamped at the placement instant, instead of `live`. While the order is WORKING it means the first second the market comes through it fills it, on that second. Everything else takes 3 consecutive true evaluations: a `price` LEVEL with the market is resting and fills on the 3rd consecutive second, so its `fill_datetime` is at least 2 seconds after placement (its placement second is evaluation #1). A one-second dislocation TO the level fills nothing — one THROUGH the price does, that is the crossing rule — and a second the market cannot be priced at counts as false and restarts the run. When the platform has no priceable market for the placement second itself, a crossing limit RESTS instead of filling, and the crossing then fills it at its own `price` on the first second it is evaluated: infra staleness can delay or downgrade a crossing fill, never manufacture one. Setting a NEW price restarts BOTH halves — the seconds count and the past-the-market test — at the second the new price takes effect, so a re-priced order that steps past the market fills at that step. Stop triggers and the `stop_loss`-class runaway exit keep the 3 consecutive seconds. Market orders fill on the next tick, unconfirmed. Single-leg EQUITY orders never cross-fill: they keep the 3-second rule and their trade-price fill. Identical in practice, live, and backtests. - [Fill price rules](https://www.0dtespx.com/learn/orders): Market orders fill at the natural price (ask for a buy, bid for a sell). A LIMIT order the market had to COME TO fills AT ITS OWN `price`, never the better price the mid transiently touched — that holds for both ways it can fill: after 3 consecutive seconds at the level, and the instant the market moves THROUGH it while it is working (which fills it on that second, still at its `price` — a resting order in a real book trades at its limit and the incoming side keeps the improvement). A limit whose `price` was ALREADY STRICTLY PAST the market at the second THAT PRICE WAS SET instead fills MARKETABLE, right there at that second, at the mid-based execution price ± `slippage` (a limit priced through the market is the only way to trade a spread aggressively, since market orders are single-leg) — strictly better than its own `price`. Setting a NEW price starts this over: a `PUT` replace, each step of a `smart` liquidation's re-pricing ladder, and each step of the platform's own automatic re-pricing of a strategy's orders (bot sessions AND backtests) count as a fresh placement, so a re-priced order whose new price steps through the market fills at that step with a `fill_price` better than its own `price`. A `price` merely LEVEL with the market is NOT crossing: that order is resting and fills at its `price`. EVERY marketable fill is BOUNDED AT THE NATURAL — a debit never books above the net ask, a credit never below the net bid — so slippage can never push a fill past the price the market was showing. The mid-based execution price is what decides WHEN either fills: the mid, unless the bid/ask is exactly one tick wide (then the natural price) or the spread is an odd number of ticks (then the mid rounded one tick toward the market maker — up for a debit, down for a credit); multi-leg limits use the combo's net price under the same even/odd-tick rule. A triggered STOP fills marketable at that mid-based price. Single-leg equities use a synthetic ±$0.01 spread (market → natural, limit/stop → trade price). A `stop_loss`-class limit — the closing order a strategy or the Strategy Builder generates to cap a loss, never something an API caller declares — that the market has run adversely THROUGH for 3 consecutive seconds also fills marketable, instead of resting forever; on a sustained adverse move this beats the bot repricer's own stop-loss cadence, whose floor is 4s. - [Slippage](https://www.0dtespx.com/learn/orders): a single per-account setting (Settings → Simulator costs), NOT a per-order field or query param — a multiple of $0.05 from $0.00 to $1.00 (new accounts default to $0.05) applied identically to backtest, live, and practice trading. It affects limit/stop orders only (market orders fill at the natural price), and for a RESTING limit it shifts the fill CONDITION rather than the realized price: the order fills once the mid reaches `price ∓ slippage` (debit −, credit +) and held there for 3 seconds, then fills at `price`. The stored `execution_price` is that threshold; the order's `slippage`/`execution_price` echo the applied value. Every MARKETABLE fill realizes `mid ± slippage` BOUNDED AT THE NATURAL — a triggered stop, a `stop_loss`-class runaway exit, and a limit priced strictly past the market at the second its price was set (its placement, or the `PUT` replace / `smart`-ladder step that re-priced it), which books THAT second's `mid ± slippage`. The bound is hard: a debit never books above the net ask and a credit never below the net bid, so on a market one tick wide — where the mid-based price already IS the natural — slippage costs nothing at all. The fill CONDITION is deliberately NOT bounded, so a bounded fill can never turn a non-fill into a fill. In PRACTICE and LIVE trading that is how it is applied; BACKTEST RESULTS are computed at ZERO slippage, so your setting is applied to them as a flat per-contract cost overlay at read time, not by re-simulating the fills. - [SPX option price ticks](https://www.0dtespx.com/learn/orders): Single-leg < $3.00 → $0.05; single-leg ≥ $3.00 → $0.10; multi-leg → $0.05 always. Limit and stop prices must already land on the tick or HTTP 400. - Max-profit cap: a `limit` order's `price` cannot exceed the structural maximum profit of the leg combination (e.g. a $5-wide vertical caps at $5.00). Skipped when max profit is unbounded — naked long calls, ratio backspreads, calendars, mixed underlyings. Applies to practice place and live place / dry-run / replace (practice has no dry-run or replace). - Defined-risk only — no naked shorts: an order whose resulting position would hold an uncovered short option is rejected with `400 naked short positions are not allowed`, independent of capital (checked before the buying-power gate on both engines). A short put must be covered by a long put and a short call by a long call — a long call does not cover a short put. The gate reads the resulting book (filled + working + the new order), so it also blocks stripping the protective long out of a held spread; an over-close of a long is separately rejected `400 insufficient quantity to close` before it can flip to a naked short. Same rule the strategy builder + AI assistant + backtest runtime enforce. A naked long option is fine (defined risk). Practice rejections are JSON `{"message":…}`; live rejections are plain text (same message). - 0DTE SPX index options only: every order leg (practice and live) must be a 0DTE cash-settled SPX index option; an equity, ETF, non-SPX, or non-0DTE leg is rejected `400 only 0DTE SPX index options can be traded` (enforced on the live path at the exchange, matching practice). - [Mid-price calculation](https://www.0dtespx.com/learn/orders): SPX options round to nickel (< $3.00) or dime (≥ $3.00); other options to penny. One-tick spread uses ask for buys, bid for sells; an odd-tick spread rounds the mid one tick toward the market maker. - [End-of-day settlement](https://www.0dtespx.com/learn/settlement): At 16:00 ET, OTM expires worthless; each ITM leg settles as an `exercise` (credit for a long leg, debit for a short — SPX is cash-settled, so never an `assignment`). SPX/NDX/VIX/XSP settle at `(underlying − strike) × 100` for calls and `(strike − underlying) × 100` for puts. Each ITM leg settles independently (its own exercise, its own $5.00 fee); a fully in-the-money spread books both exercises and their cash flows net to the spread width — its defined max loss. - [Portfolio and financials](https://www.0dtespx.com/learn/positions): Positions, delta, brokerage fee schedule, unrealized/realized/total P&L, net-liquidation value, and FINRA maintenance-margin formulas (naked short, vertical, iron condor, covered). `delta` is a signed decimal — the wire preserves the stored sign (a put delta may be negative), though current data emits magnitudes so values are typically 0..1 positive; position direction supplies the additional position-level sign. Other Greeks (gamma, theta, vega, theo price, IV) are no longer provided. - [Market data and rate limiting](https://www.0dtespx.com/learn/api/rate-limits): ACCEPTABLE USE — the platform is for trading simulation and strategy research; bulk extraction of the historical dataset is NOT permitted at any pace (systematically walking past sessions to build a local copy or derivative dataset violates the terms even when paced under the credit budget — the budget bounds burst load, it is not a data license; offending accounts may be suspended and their networks may lose access). Mechanics: one leaky bucket per account — 10,000-credit capacity, ~0.116 credits/second drain (a request 429s when its cost exceeds remaining headroom `cap − used`, so `credits_used` can be below `credits_cap` and still reject). Metered market-data endpoints (all authenticated users — per-request costs 0–150, full-range snapshot priced per snapshot returned) the per-session strategy drill-in `GET /strategies/{id}/results/days/{date}` (10 credits — it re-runs the session through the engine; the portfolio drill-in costs member_count × 10), and AI-assistant message turns (50 credits, refunded on a failed turn — plus the assistant's own trailing-24h abuse limits: `content` ≤ 4 KB, 2 in-flight turns per account (`429 too_many_active_turns`), daily turn/token/cost caps (`429 assistant_budget_exceeded`), a 15 active-draft ceiling (`409 too_many_drafts`), and a brief `429 assistant_cooldown` for repeated off-topic messages — each `429` carrying `Retry-After`) draw on the bucket; the assistant may additionally spend, occasionally and unprompted, one **automated results-review turn** on a conversation of yours once that strategy's historical results are complete (most often when the strategy never entered a trade in any session — it either corrects the strategy or explains what it found, streaming over `assistant_draft_events` like any other turn): such a turn takes NO 50-credit debit, but it does count toward the same daily turn/token/cost caps, and while it runs the conversation is busy like any other turn (`409 turn_in_flight` on a message, cancelable with `POST …/{id}/cancel`); successful metered responses carry `X-RateLimit-Used`/`X-RateLimit-Limit` headers and the 429 carries `Retry-After`; unauthenticated traffic isn't metered but can only reach the most recent completed session at 30-second resolution. Backtest RUNS cost no credits: the backtester is protected by a per-user cap of 3 concurrently-executing runs (`429 too_many_active_backtests`, retry with backoff) and a priority queue. Everything else (auth, accounts, sessions, orders, positions, transactions) is free; the unauthenticated `/auth/*` endpoints carry a separate small per-IP abuse throttle (burst 20, refill 10/min — `429` with `Retry-After`). - [WebSocket protocol](https://www.0dtespx.com/learn/api/websocket): `GET /__ws` upgrades to WebSocket; client has 5 seconds to send `{"auth": "public"}` or `{"token": ""}`. Every server-sent message is a JSON object with a top-level `channel` field (the routing key); `session_events` / `backtest_events` / `live_bot_events` carry top-level `id` and `data`/`type` siblings instead of a nested `payload`. Client-to-server: auth + `{action:"subscribe"|"unsubscribe", channels|channel}`; for `session_events` add `session_id` (and an optional `last_event_by_sim` map) to scope to one session; for `backtest_events` add `source_hash` to scope to one strategy's results (authorized against your saved strategy or live preview interest); `live_bot_events` takes NO scoping key (it is user-scoped and covers all your bots) but accepts `last_event_id` (scalar) or `last_event_ids` (`{"live_bot_events": N}`) for gap replay. Channels: `live_aggregate_data` (opt-in, 1s), `live_option_chain` (opt-in, 1s, token-authenticated users only; each strike's `call`/`put` carries only `bid`/`ask`/`delta` — deltas signed; default frame is an array of `{strike, call, put}` objects, or add `"encoding":"columnar"` to the subscribe for the equivalent compact structure-of-arrays frame `{k, cb, ca, cd, pb, pa, pd}` — strikes whole dollars, prices integer cents ÷100, deltas signed ten-thousandths ÷10000, all arrays equal-length and index-aligned; both encodings supported indefinitely; AVAILABILITY — frames start at today's session `data-start-time` (the per-date value in `GET /market-data/sessions`), NOT at the open: a subscribe made earlier is accepted and simply yields no frames until that instant passes, nothing is rejected and there is nothing to re-subscribe. `live_aggregate_data` is unaffected and streams from the open; the REST snapshot endpoints enforce the same bound), `session_events` (opt-in per session id; each envelope carries top-level `session_id` and `account_id`; ≤64 sessions per conn; `type` ∈ order_update|financials_update|ended|settlement_failed. An `order_update` is written for EVERY change to an order, in the same transaction as the change, so the sequence IS the order's history: admission (`status:"pending"`, `accepted_seq:null`, emitted BEFORE the POST response), acceptance (`status:"live"`, `accepted_seq` stamped), then the terminal frame; a cancel request adds its own frame with unchanged `status` and `cancel_requested:true`, and that flag rides every later frame. The payload is the same order JSON the REST reads return plus `accepted_seq`; the `filled` frame additionally carries `fill_quotes`, the market quotes at the time of the fill (shape + full semantics under Orders below), and NO other frame does — the key is absent, never null. The ladder only moves FORWARD, so an envelope reporting an earlier state than one already applied is stale — drop it), `live_bot_events` (opt-in, token-only, NO per-key scoping — one subscribe streams lifecycle events for EVERY bot on the authenticated user's account; anonymous conns receive nothing; envelope is `{channel:"live_bot_events", id:N, data:{type, account_id (== the bot id), session_id, …payload}}` with `id` the monotonic outbox row id; the outbox is DURABLE — never dropped for a slow client — so a reconnect passing `last_event_id`/`last_event_ids` replays the exact gap before live events resume; a cursor-less subscribe (an OMITTED `last_event_id`, or 0) does NOT replay everything: it delivers the newest 500 events and then live traffic — a deliberate short OVERLAP with whatever you already loaded over REST, not a history feed, so expect events you already hold and de-duplicate on the envelope `id` (and on `data.log_id` for `decision_logged`); `"last_event_id": 1` is the full-backfill request (strictly every row ABOVE id 1 — everything retained bar a row with that exact id); RETENTION is per event type — `tick_progress` heartbeats kept 7 DAYS, every other type indefinitely — so a cursor older than that horizon replays only what remains, with NO signal that expired heartbeats were skipped (no `stale_cursor` analogue on this channel); a pruned heartbeat DOES take its point-in-time `tick_index`/`open_positions`/`open_orders` with it (nothing else stores them), but no value a client acts on, since the current ones are on the session's REST reads and the P&L curve on `…/history`; treat the socket as live traffic plus that overlap, and the REST endpoints (`…/decision-log`, `…/orders`, `…/positions`, `…/transactions`, `…/financials`, `…/history`) as the durable record of what they cover — but they do NOT cover everything: `decision_logged` is mirrored into the decision log and `session_started`/`session_completed` into the session record, while `signal`, `halted`, `registering`, `waiting_for_data`, `session_resumed`, `missed_ticks` and `error` have NO REST surface at all (`error` carries the strategy traceback nothing else exposes), so a client that needs those past the 500-event overlap must hold a cursor or pass `"last_event_id": 1` — all seven are retained indefinitely; `data.type` is one of eleven: `session_started` (payload `session_date`/`opening_balance`/`starting_capital`; the `started_by` "user"|"auto" distinction rides the paired `decision_logged` entry), `registering`, `waiting_for_data` (both emitted only when the session actually waits), `tick_progress` (~10s heartbeat: `tick_index`/`tick_at`/`nlv`/`open_positions`/`open_orders`; KEEPS FLOWING while a stopped session sits dormant holding positions), `decision_logged` (one decision-log entry mirrored live: `datetime`/`level`/`event`/`payload`/`source` ("strategy"|"runner"|"api" — "api" is an entry recorded against a request the user made)/`log_id` — carries ctx.log(), re-pricing activity (order_repriced, incl. `legs_changed` when the step re-selected the entry's legs, and reprice_failed), the pricing rows (order_start_priced and order_repriced BOTH carry `quotes_at` — leg option symbol -> the timestamp of the quote used for that leg, RFC 3339 UTC — plus `net_bid`/`net_ask`, the net bid and ask their price was computed from), the order lifecycle (order_filled/order_canceled/order_rejected), the ENTRY-RECOVERY pair (entry_retried — the platform rebuilt an entry the exchange TEMPORARILY refused and re-submitted it inside the window; payload order_id (a NEW id naming that re-submission, which may itself be refused — it matches an order only once one is accepted)/attempt/max_attempts/reason — and entry_retry_abandoned, AT MOST ONE per session, payload attempts/reason, when the entry could not be recovered and the day ends without a trade), entry_window_closed, and the session_started/strategy_stopped/strategy_restarted/session_ended lifecycle entries), `signal` (ctx.signal(): `name`/`fields`), `session_resumed` (`status`/`tick_index` — mid-session bot picked back up: after a platform restart, OR after `POST /bots/{id}/sessions/{session_id}/restart` resumed a stopped session), `missed_ticks` (`tick_index` plus `age_ms` or `overload:true`; one per stale episode, NOT per skipped tick), `halted` (`reason`/`source` — the strategy's own ctx.halt(), non-terminal by itself), `session_completed` (TERMINAL, written in the finalize tx: `session_id`/`status`/`halt_reason`/`final_nlv`/`total_pl`/`realized_pl`/`fees_total`, money fields JSON strings, `status` ∈ completed|halted|stopped|failed), `error` (`callback`/`tick_index`/`message` truncated to 500 chars with a trailing ellipsis; on_tick/on_order_filled throttled to one per 15s). TRAP: `DELETE /bots/{id}/sessions/{session_id}` stops the STRATEGY, not the session, and emits NO lifecycle event of its own — it surfaces as a `decision_logged` entry with `event:"strategy_stopped"` while the row stays `running`; `session_completed` follows later with `status:"stopped"` once the session is flattened via the liquidation endpoints, or `status:"completed"` if held to the 16:00 ET close. A RESTART in between (`POST …/sessions/{session_id}/restart`) is a `decision_logged` entry with `event:"strategy_restarted"` (source "api") followed by `session_resumed`, after which the stream continues as if the stop had not happened. EXCEPTION — a stop with NOTHING LEFT IN THE MARKET ends the session instead of holding it (a session still scheduled/registering whose strategy never ran, or one already flat), and so does a flat book with no stop at all (entered-and-exited, or the entry window closed unfilled — `halt_reason` "flat_book" vs "user_stop"): a final `decision_logged` (the `session_ended` entry) then `session_completed` with `status:"stopped"` is the session's last event, at whatever time of day that happens. There is NO `stopped` event type), `backtest_events` (per-source-hash strategy-results progress frames — each carries the full fee-overlaid results snapshot under `data.snapshot`, so render straight from it; no replay; the HTTP results endpoint backs the initial load + reconnect; a dropped socket never stops a run; ≤256 strategies per conn), `assistant_draft_events` (opt-in per `draft_id`, owner-authorized — the AI-assistant chat stream; `data.type` is `delta`|`thinking` (streamed model reasoning, display-only)|`tool_status`|`ask_user`|`message`|`preview` (carries the new `source_hash` plus the updated `description`, `risks`, and `params` — the standardized `{legs, entry, exit}` summary, omitted on a draft not yet edited under the field)|`turn_complete`|`error`; optional `last_event_by_draft` cursor replays the gap on reconnect; ≤16 drafts per conn), `market_data` (opt-in **per symbol**, 1s cadence, lossy latest-wins — subscribe with a `symbols` list; underlyings `SPX`/`VIX` are anon-ok and emit `type:"trade"` frames carrying `price`, while a 21-char OSI for one of today's SPX 0DTE contracts is token-only and emits `type:"quote"` frames carrying `bid`/`ask`/signed `delta`; `datetime` is offset-less UTC; a wrong-day or unknown symbol yields no frame; ≤64 symbols per subscribe frame, ≤256 per conn; unsubscribe with `symbols` drops those, with no `symbols` clears all), `account_financials` (opt-in **per trading account**, token-only, 1s cadence, lossy latest-wins — subscribe with an `account_ids` list of manual and/or bot accounts you own; one initial snapshot frame per account then a live frame on every per-second change; the payload is the account's live financials (NLV, realized/unrealized P&L split, credits/debits/fees, buying power, equity-options and equities breakdowns) with every money field a JSON string with trailing zeros stripped and `timestamp` RFC3339 with a `Z` — identical to `GET /accounts/{id}/sessions/{sid}/history`; lossy and NOT authoritative on its own, so reseed from that history endpoint after a gap; ≤64 account ids per subscribe frame, ≤64 per conn; unsubscribe with `account_ids` drops those, with no `account_ids` clears all), `end_of_data` (terminal sentinel). For `session_events`, `assistant_draft_events`, and `backtest_events` an over-cap subscribe is refused (existing subs keep streaming) with a `{channel:"subscription_rejected", payload:{target, reason:"limit_exceeded", limit}}` envelope (`target` = `session_events`|`assistant_draft_events`|`backtest_events`); `market_data` and `account_financials` instead silently truncate (keys past the cap dropped, the rest subscribed, no envelope); auth failures decline silently. A cursor-carrying `session_events` resubscribe into an ended-and-cleaned-up session gets a one-shot `{channel:"session_events", type:"stale_cursor", session_id}` signal and is not enrolled — refetch state over REST and resubscribe without a cursor. Server-initiated close codes: 1008 (inbound frame-rate budget exceeded, or token revoked/expired — reconnect + re-auth) and 1013 (subscribe/unsubscribe frame not admissible, control-frame queue saturated — reconnect + resubscribe); ordinary disconnects are code-less. Public connections have a 5-minute stream cap and may subscribe to `live_aggregate_data` and to `market_data` for the `SPX`/`VIX` underlyings, but not to `live_option_chain`, `market_data` option symbols, or `account_financials`. - [Live trading](https://www.0dtespx.com/learn/live-trading): Today's session on a `live_sim` account, registered (authenticated) users only. Open it with `POST /accounts/{id}/sessions` (empty body), allowed only once the current time has reached the session's `data-start-time` and ticks are fresh; the opening balance is the account's carried `cash_balance`. Each live account has at most one open day at a time; persisted order lifecycle (no time-travel) on the ladder `pending → routed → live → {filled,canceled,expired,rejected}` (see Orders below), `409` on cancel of an already-terminal order and on replace of one that is not `live`, post-close gate at 16:00 ET, automatic settlement once the close-time tick is published (closing balance carries into the account), `503` returns are always safe to retry (no state changed); a `504 outcome_unknown` on a mutating request (order place/cancel/replace, session open) means the exchange could not confirm within the deadline whether it applied — resolve by retrying under the same `Idempotency-Key` (the retry returns the original outcome) or by re-reading the order/session over GET. Once started, a live day cannot be stopped or abandoned — it runs to the 16:00 ET close and settles there. - [User accounts](https://www.0dtespx.com/learn/api/overview): Email + password registration with 6-digit verification (locked after 5 failed submissions), token-based sessions; verification codes are single-use and `POST /auth/register` consumes the code and returns the first session's token directly (no follow-up login call). Registration is bounded: one account per email inbox (provider aliases of the same mailbox — e.g. Gmail dots/+suffix variants — count as the same inbox, rejected 400) and a small daily allowance of new accounts per network (429 when exhausted). A user account is free and all registered users have full access (distinct from a trading account, which is a per-user trading container). ## API — endpoints - [Authentication](https://www.0dtespx.com/openapi.yaml): `POST /auth/check-email`, `POST /auth/verify-email`, `POST /auth/register` (returns `{token}` — the first session), `POST /auth/forgot-password`, `POST /auth/reset-password`, `POST /auth/sessions` (login), `DELETE /auth/sessions` (logout). - [User account](https://www.0dtespx.com/openapi.yaml): `GET/PATCH /user` (`GET` returns `id`, `email`, `usage_percent`, `fee_schedule`; `PATCH` accepts `email`/`verification_code`, `password`/`verification_code`, `fee_schedule`; an email change to an inbox already in use returns `409`). Account deletion: `POST /user/delete-code` emails a 6-digit confirmation code (one per 60 s), then `POST /user/delete` with `{verification_code}` permanently deletes the account and all its data (unrecoverable), all sessions revoked (`Clear-Site-Data`). Requires no open live/bot sessions (`409 session_open`). - [Accounts](https://www.0dtespx.com/openapi.yaml): the `/accounts` surface is LIVE (manual) accounts only — `type` is always `manual`; bots are a separate surface (`/bots`) and never appear here or resolve through `/accounts/{id}` (`live_sim` engine; broker `broker_*` reserved). `GET /accounts` (live accounts only), `POST /accounts` (`{name, engine, description?, type?}`; every account starts with a fixed $100,000 — there is no starting-capital input; broker engines → `409 engine_not_available`; `type` defaults to `manual`, the only accepted value — a bot account is created via `POST /bots`, not here; duplicate active name → `409 name_taken`), `GET /accounts/{id}` (a bot id → `404`), `PATCH /accounts/{id}` (`name`/`description`/`status:archived`; archive blocked while a session is open), `DELETE /accounts/{id}` (soft-delete; blocked while a session is open), `GET /accounts/{id}/analytics` (round-trip trade log + aggregate trade metrics over settled days — folds settled-day transactions into round-trip trades: win rate per round trip (scratches excluded, null when undecided), profit factor, average/largest winner+loser, hold times, net-of-fees P&L; per-day trade counts; the trade log newest-first capped at the most recent 1000 with `trades_truncated`; read-only + unmetered; manual live accounts only). - [Sessions (account-scoped — LIVE/REPLAY only)](https://www.0dtespx.com/openapi.yaml): `GET /accounts/{id}/sessions` (newest first), `GET /accounts/{id}/sessions/current` (open live session or null), `GET /accounts/{id}/sessions/head` (open day if any else latest settled, or null), `GET /accounts/{id}/sessions/{sid}`, `POST /accounts/{id}/sessions` (live `live_sim`: empty `{}`, opens TODAY gated on market hours + fresh ticks, 400 if closed/no data; re-request while open returns it 200; broker → 409 — there is NO past-date open on this surface; once started a live day cannot be stopped — it settles at the close), `GET /accounts/{id}/sessions/{sid}/history` (per-second financial snapshots; `?interval=`). While a live day is `open` it carries `settles_at` (expected settlement time = `end_time` + a few minutes) — after the close, poll the list around that time for the settled day to appear. A settled live session is read back (REPLAY) via the same paths with `?at=` (see positions/transactions below). There is no PATCH-clock on this surface — clock control moved to practice. - [Practice sessions (account-less, fixed $100k)](https://www.0dtespx.com/openapi.yaml): replaying a trading day needs NO account — a one-click $100k sandbox with no `account_id`. `POST /practice/sessions` (`{date}` ANY past trading date, started in ANY order — each POST opens a NEW independent session, so a date can hold multiple simulations; practice days are independent (no carried balance, no re-thread); to edit a settled day rewind its clock via PATCH; each opens at market open with a fixed $100,000, no deposit row; → 201 the session), `GET /practice/sessions` (list, newest first), `GET /practice/sessions/{sid}` (hydrate one), `PATCH /practice/sessions/{sid}` (`{time}` advance/rewind; crossing the 16:00 ET close settles), `DELETE /practice/sessions/{sid}` (soft-delete the day and its trades → 204), `GET /practice/sessions/{sid}/orders`, `POST /practice/sessions/{sid}/orders` (place — rejected `400` while the replay clock is before the day's `data-start-time` (the moment historical data begins); practice has NO `orders/dry-run` and NO `PUT .../orders/{orderId}` replace; those are live-only), `GET /practice/sessions/{sid}/orders/{orderId}`, `DELETE /practice/sessions/{sid}/orders/{orderId}` (cancel/delete a not-yet-filled order; practice is synchronous, so it applies at once), `POST /practice/sessions/{sid}/liquidations` (close every open position in one synchronous call — see Liquidations below), `GET /practice/sessions/{sid}/positions`, `GET /practice/sessions/{sid}/transactions`, `GET /practice/sessions/{sid}/history` (per-second snapshots; `?interval=`). The `/orders`, `/positions`, `/transactions`, `/history` sub-resource shapes are identical to their account-scoped counterparts; only the path prefix differs. - [Orders (session-scoped; practice `/practice/sessions/{sid}/…` and live `/accounts/{id}/sessions/{sid}/…`)](https://www.0dtespx.com/openapi.yaml): `GET …/orders`, `POST …/orders` (supports `Idempotency-Key`, scoped per session), `GET …/orders/{orderId}`, `DELETE …/orders/{orderId}` (cancel/delete; live cancel is a best-effort REQUEST accepted in EVERY non-terminal state — 204 means accepted and queued, not applied — and 409 only when the order is already terminal). LIVE-ONLY additions: `POST /accounts/{id}/sessions/{sid}/orders/dry-run` (validate + preview, not persisted; a strictly-crossing limit and a market order preview `filled` with the fill price; any other limit previews `live`; a dry run never previews `pending`) and `PUT /accounts/{id}/sessions/{sid}/orders/{orderId}` (replace a resting limit order — `price`/`price_effect` only; atomic cancel+replace, the replacement carries a NEW id; valid ONLY while `status=live`; non-limit → 400; 409 `order is not yet live` when still `pending`/`routed`, 409 `order is being canceled` when `cancel_requested`, 409 `order is no longer working` when terminal) — practice has neither. Practice orders return the `Order` shape (status computed against the replay clock); live orders return the `LiveOrder` shape. Live `status` walks the ladder `pending → routed → live → {filled,canceled,expired,rejected}`, always FORWARD, may skip a rung, and the first terminal recorded is final: `pending` = we accepted it, nothing downstream has confirmed it (transient, sub-second today); `routed` = confirmed by the next hop, not yet on a book — RESERVED, never emitted today, but code for it; `live` = resting on the book, fillable/cancelable/replaceable (this is the state old clients called `pending`). All three non-terminal states are WORKING: they reserve buying power, count toward open orders, and accept a cancel; only `live` accepts a replace. A separate boolean `cancel_requested` (present only when true, orthogonal to `status`, on both the REST reads and the `order_update` event) means a cancel was ACCEPTED but not necessarily applied — it can ride ANY status including `filled`, because a cancel that loses the race to a fill is irreducible; treat the terminal status, not the 204, as the outcome. `accepted_seq` (WebSocket `order_update` only, omitted from REST) is the per-session BOOK ordinal, `null` while the order is `pending`/`routed` and stamped the moment it is sequenced — normally the move to `live`, but an order refused in that same instant goes straight to `rejected` carrying its ordinal, so a terminal order with an `accepted_seq` did not necessarily rest on the book. A FILLED live or bot order additionally carries `fill_quotes` — THE MARKET QUOTES AT THE TIME OF THE FILL, not the inputs to the fill price — shaped `{quotes: {: {bid, ask}}, quotes_at: {: RFC3339 UTC}, net_bid, net_ask}` with every price a decimal string. It is emitted ONLY on `status: "filled"` (REST reads, the POST/PUT/dry-run body, and the `order_update` event alike) and is otherwise ABSENT — never null — which covers every other status, every order filled before the field shipped, and EVERY PRACTICE ORDER (the practice engine prices against historical data and never captures it); a leg whose quote could not be read drops the WHOLE object rather than emitting a partial snapshot, and every leg of the order is present when it is there. `quotes_at` is each leg's OWN quote timestamp, not the fill second: on an order that rested and confirmed it can sit up to a second on EITHER side of `fill_datetime` (trailing OR leading), and on a `market` order — priced off the latest quotes but stamped with the placement moment — it can trail by a few seconds, so read it instead of assuming the fill second. `net_bid`/`net_ask` are the order's net bid/ask at that moment PER UNIT (the leg-quantity GCD divided out, so a 2x/2x vertical reports the same numbers as 1x/1x), direction carried by `price_effect`/`fill_price_effect` and never by a sign, so a ratio spread's net cannot be re-derived by summing `quotes`. THEY ARE NOT PROMISED POSITIVE: on a near-zero or very wide combo the natural bid side crosses through zero, so `net_bid` can be 0 or NEGATIVE (a wide debit vertical can report net_bid "-0.25" against net_ask "0.55"); the only ordering guarantee is net_bid <= net_ask. They do NOT bound `fill_price` (a resting limit that the market crossed books its own price even when the crossing market is better; slippage worsens a marketable fill, though never past the natural) and none of these values is an execution-quality or slippage readout; there is no midpoint field — the reference price a limit/stop is measured against comes from the tick rules, not from averaging the two bounds. Practice/backtest orders are born `live` — those engines are synchronous, so they never have an unconfirmed phase. `POST` also accepts an optional `client_order_id` UUID (live only) — when omitted the API derives one from the `Idempotency-Key` (or fresh UUID) so a replay-after-crash returns the existing terminal order. A `503` always means no state changed and is safe to retry; a `504 outcome_unknown` (live place/cancel/replace) means the exchange could not confirm whether the request applied — retry under the same `Idempotency-Key` (the retry returns the original outcome; a `409 duplicate_idempotency_key` while it is still in flight means keep retrying) or re-read the order over GET. - [Liquidations (close every open position; session-scoped)](https://www.0dtespx.com/openapi.yaml): a liquidation closes EVERY open position on a session under one of two methods — `smart` (positions grouped into favorable multi-leg LIMIT orders of up to 4 legs, each working order re-priced every 5 seconds, one tick at a time toward the market and never back away from it) or `aggressive` (one MARKET order per position in two rounds — every short bought back first, then the longs those closes release; a leg with no usable natural price falls back to a one-tick $0.05 limit order, which may rest). Grouping is naked-short-safe by construction: no subset of the closing fills can leave an uncovered short, so a liquidation is never refused by the defined-risk gate. STARTING ONE CANCELS EVERY WORKING ORDER ON THE SESSION, INCLUDING STOP-LOSSES AND TAKE-PROFITS, AND THEY ARE NOT RESTORED — not on completion, not on a partial end, not on cancel. Also available on a STOPPED BOT SESSION at `/bots/{id}/sessions/{session_id}/liquidations[/current]` (same shapes; gated on `strategy_stop_applied_at`, and STARTING ONE PERMANENTLY BLOCKS THAT SESSION'S RESTART via the never-cleared `liquidated_at` — see Bots). LIVE (asynchronous): `POST /accounts/{id}/sessions/{sid}/liquidations` `{method: smart|aggressive}` → `202` + the initial snapshot (`409 liquidation_in_progress` when one is already running, `409 no_open_positions` on a flat book, `409 session_closed` on a settled/replayed session, `400` on a bad method), `GET /accounts/{id}/sessions/{sid}/liquidations/current` → the running snapshot or the last terminal one (retained ~15 minutes, or until the next start on that session; `404 no_liquidation` otherwise), `DELETE /accounts/{id}/sessions/{sid}/liquidations/current` → stops the workflow, cancels every order it has working, and returns `200` + the final snapshot only once those cancellations are terminal. While one runs, manual order `POST`/`PUT` on that session return `409 liquidation_in_progress`; order `DELETE` stays allowed. A liquidation is not durable state — an interrupted one leaves the orders it already placed resting as ordinary working orders (readable under `GET …/orders`, cancelable), and re-running it adopts whatever is still open. PRACTICE (synchronous): `POST /practice/sessions/{sid}/liquidations` `{method}` → `200` with the FINISHED summary, nothing to poll and nothing to cancel (`409 no_liquidatable_positions` when every open position is already covered by a working closing order); the same close plan is walked forward through the day's historical data from the replay clock, the caller's slippage setting governs how fast its limit orders fill, the cancellations and the closing orders are applied atomically (any failure changes nothing at all) with account statistics recomputed immediately afterwards, every timestamp is a SESSION time and the recorded orders can be dated AFTER the current clock (advance it to watch them fill), and a group that never filled before the close is not persisted (reported with status `unfilled` and no `order_id`, summary status `partial` — re-running is the retry). Snapshot shape (identical on both surfaces): `{method, status, started_at, ended_at?, orders: [{order_id?, status, legs_summary, current_price?}], positions_remaining, positions_unresolved?, errors: []}` with `status` ∈ `running|completed|partial|canceled|failed` (`running` live-only; `completed` = flat; `partial` = ended with positions still open, including a session that closed mid-liquidation) and each order's `status` the usual lifecycle value plus `stalled` (a live order parked after an exchange rejection — it lands in `errors[]` and the rest of the book keeps closing) or `unfilled` (an unpersisted practice group). `positions_unresolved` (omitted when zero) counts longs the aggressive method could not sell because the shorts covering them never bought back. - [Positions and transactions (session-scoped; same paths under `/practice/sessions/{sid}/…` and `/accounts/{id}/sessions/{sid}/…`)](https://www.0dtespx.com/openapi.yaml): `GET …/positions` (computed at the session's current time — replay clock for practice, latest tick for an open live session; includes delta and P&L), `GET …/transactions` (the session's WHOLE ledger, newest first, with NO time cutoff — on a practice session that includes rows dated AFTER its clock: the precomputed fills of orders the clock has not reached yet, plus the day's projected 16:00 ET settlement rows for the orders currently on the book, recomputed on every order change). Both accept an optional `?at=` cursor that overrides the clock for a single stateless read without advancing the stored time — on transactions it is the ONLY way to get an as-of-a-moment view. A SETTLED live session is fully replayable — its positions/transactions are reconstructed server-side from the market data (settlement transactions always included), so `?at=` scrubs any moment of a live account's past day. Absent `?at=`: positions are computed at the stored session time (a settled live day: its close) while transactions come back in full; on an open live session `?at=` is ignored. - [Market data](https://www.0dtespx.com/openapi.yaml): `GET /market-data/sessions`, `GET /market-data/strikes/{date}`, `GET /market-data/historical/{date}` (selectable series via `?series=`), `GET /market-data/option-chain-snapshots/{timestamp}`, `GET /market-data/option-chain-snapshots/{startTime}/{endTime}` (both bounded below by the session's `data-start-time`, on EVERY date, historical included — usually a minute past the open but not always, so read it per date from `GET /market-data/sessions` instead of assuming an offset from `start-time`: a single timestamp below the bound is a 404; a range starting below it is clamped and returns only the snapshots at or after it; a range lying entirely below it is a 404, NOT an empty array. Clamping does NOT reduce the credit cost — the range charge is computed from the requested `endTime − startTime` before the response is built, so a clamped or 404 range still costs its requested span. Same bound as the `live_option_chain` WS channel), `GET /market-data/average-expected-move`, `POST /market-data/options-prices/{date}` (net mid price of a custom ≤4-leg SPX 0DTE order across a session — body `{items:[{direction,side,strike,quantity}]}`, returns `[{timestamp,datetimeUnix,price,effect}]`; public for the most recent completed session at 30s resolution, authed for all sessions at 1s; 10 credits). - [WebSocket](https://www.0dtespx.com/openapi.yaml): `GET /__ws` — live market data feed (`live_option_chain`, and the `market_data` option-contract `type:"quote"` frames derived from it, start at today's session `data-start-time` rather than the open; `live_aggregate_data` and the `SPX`/`VIX` `type:"trade"` frames stream from the open), plus `backtest_events`: per-source-hash strategy-results progress frames (subscribe with `source_hash`; each frame carries the fee-overlaid results snapshot under `data.snapshot`; the HTTP results endpoints back the initial load + reconnect; any authenticated user); `live_bot_events`: the durable per-user bot lifecycle outbox (subscribe with no scoping key — one subscription covers every bot on the account; envelope `{channel, id, data:{type, account_id, session_id, …}}`; eleven `data.type` values from `session_started` through the terminal `session_completed`; `last_event_id`/`last_event_ids` replays the exact gap on reconnect, while a cursor-less subscribe replays only the newest 500 events — a short overlap with your REST reads, with `"last_event_id": 1` the full-backfill request (every row ABOVE id 1); seven types (`signal`, `halted`, `registering`, `waiting_for_data`, `session_resumed`, `missed_ticks`, `error`) have no REST equivalent, so the cursor is the only way to recover them past the overlap; `tick_progress` heartbeats are retained 7 days and every other type indefinitely; token-only); `market_data`: opt-in per-symbol 1s market data (subscribe with `symbols`; `SPX`/`VIX` trades are anon-ok, today's SPX 0DTE option-contract quotes are token-only); `account_financials`: opt-in per-account live financials (token-only; subscribe with `account_ids`; initial snapshot then per-second frames; money fields JSON strings and `timestamp` RFC3339 `Z`, matching `GET /accounts/{id}/sessions/{sid}/history`; lossy — reseed from that history endpoint after a gap). - [Saved strategies](https://www.0dtespx.com/openapi.yaml): strategies are immutable, and every distinct configuration compiles to a deterministic program keyed by its `source_hash` (the key the preview and WebSocket endpoints use); results always cover every trading session at a fixed $100k per-session capital, fees are applied at read time from the caller's fee schedule, and results always show the full history (the window is labeled explicitly and is always all trading sessions). Each saved link has a per-user `privacy` flag — `private` (default) or `public`; public means UNLISTED ("anyone with the link"), NOT discoverable — there is no directory of public strategies and the id is an unguessable UUID, so a public strategy is reachable only by someone given its link, and once they have it any user or unauthenticated visitor can view it — and clone a **Builder** strategy (read its `config`, re-save; no clone endpoint) — while a private one is owner-only; an **assistant** strategy has no `config`, so cloning it forks the owner's conversation (`…/{id}/fork`) and is OWNER-ONLY (a shared one can't be cloned by others — it would expose the owner's chat). A strategy opens exactly ONE trade per session — `config.entry.time` is a single entry time (for several daily entries, make one strategy per time and combine them in a portfolio). `config.entry.days_of_week` optionally restricts to specific weekdays. Every saved strategy also exposes a derived declarative `entry` window on `GET /strategies/{id}` — {start_time, end_time, days_of_week?} (ET; days Monday=0..Sunday=6, absent = every trading day): a builder strategy's is the form time + 1 minute; an assistant strategy declares either a specific time or a genuine range. Bots gate session starts on it (422 not_entry_day / entry_window_passed). `null` on strategies saved before entry windows existed. `POST /strategies/preview` (`{config, abandon_hash?}` — the builder's per-change action; every distinct config IS a persisted backtest keyed by its source hash; returns the current fee-overlaid snapshot plus the generated artifacts incl. `params` (the standardized `{legs, entry, exit}` summary), instant for a config that already has results; `400 invalid_config` with a `fields` map creates nothing; `429 too_many_active_backtests` at the 3-active-runs cap, retry with backoff), `GET /strategies/preview/{source_hash}/results` (the pre-save read path; doubles as the interest heartbeat keeping an unsaved preview's run alive), `POST /strategies/preview/abandon` (`{source_hash}`; stops the run at the next session boundary if it was never saved (completed sessions are kept); interest also self-expires after 15 min), `POST /strategies` (`{config, privacy?}` — SAVE = publish: publishes the immutable strategy by hash and keeps the already-running/done backtest; `privacy` optional, defaults `private`; never 429s — at the cap the run is parked and auto-starts later; re-saving a config you removed resurrects the same id with its results; a NEW save while already holding 500 saved strategies returns `409 strategy_limit_reached` and creates nothing — re-saving one you already have doesn't count), `GET /strategies` (the leaderboard: each row carries full-history headline metrics net of the default fee schedule, coverage, staleness, `privacy`, a short `title` (auto-generated, owner-renamable via PATCH), and `params` — the standardized summary `{legs, entry, exit}`, three plain-text lines (no markdown) meant to render as fixed labeled rows: `legs` names every leg in one phrase separated by `; `, `entry` when the trade opens, `exit` how it closes (`hold to the 16:00 ET cash settlement` when nothing closes it early); OMITTED on a strategy whose summary has not been filled in yet), `GET /strategies/{id}` (immutable logic + full-history results snapshot + `new_sessions_available` + `privacy` + `is_owner` + `params` (the same standardized `{legs, entry, exit}` summary as the list — `null` here, rather than omitted, until it is filled in) + a short `title` (auto-generated, owner-renamable via PATCH once generation finishes) + `title_pending` (true while that title is still being generated — the window in which a `title` PATCH is refused; it tracks the generation, not `title`, which may already hold an earlier rename); AUTH OPTIONAL — public strategies readable by anyone incl. unauthenticated visitors, a private one you don't own returns 404), `PATCH /strategies/{id}` (owner only; per-user metadata: `title` (1–120 chars, 422 `invalid_title` otherwise; refused with `409 title_generating` while the strategy's short title is still being auto-generated — poll `title_pending` on the detail read and retry once it clears; description/privacy-only patches are never blocked), `description` override and `privacy` toggle — clone to change logic), `DELETE /strategies/{id}` (removes it from your list; re-saving the same configuration restores it with its results), `GET /strategies/{id}/results/days` (full day list: gross, fees under your schedule, net), `GET /strategies/{id}/results/days.csv` (the same day list as a CSV attachment, plus the engine's fee-free `intraday_max_dd` and `spx_close`), `GET /strategies/{id}/results/days/{date}` (session drill-in RECOMPUTED ON DEMAND by re-running that day through the engine — seconds, not ms; orders (each carrying the platform `class` it was priced under — `entry`|`take_profit`|`stop_loss`, omitted on results computed before that was recorded; a day lists every re-pricing step an order took) + transactions with `overlay_fee` + decision log (incl. the platform pricing rows `order_start_priced`/`order_repriced`/`reprice_failed`; both pricing rows carry `quotes_at` (leg option symbol -> the timestamp of the quote used for it) + `net_bid`/`net_ask`, the net bid/ask the price came from) + the cost-adjusted (fees + slippage) intraday curve (always included); the headline `net_pnl` is gross minus both `fees` and `slippage`; `reconciliation_warning` flags engine drift; 10 rate-limit credits), `POST /strategies/{id}/results/update` (append newly added sessions — allowed whenever new sessions exist; `202` queued; `429` at the cap). There are NO `/backtests*` endpoints and no run credits — results are served through the strategy. AI ASSISTANT: the assistant authors strategies through a per-user draft instead of a form config — `POST /strategies/assistant/drafts` (seeds a canonical short iron condor, returns `{draft_id, source_hash, snapshot}`; the chat transcript starts empty — the first message is the user's), `GET /strategies/assistant/drafts[/{id}]` (list (paginated via `limit`≤100/`offset`) / draft+transcript — the detail also carries, as top-level siblings, the strategy's current `risks` bundle (`{risks:[{title, severity, scenario, max_loss},…], overall?}`, absent until the first edit commits) and `params` (the same standardized `{legs, entry, exit}` summary a saved strategy carries, absent on a draft not yet edited under the field); the transcript persists everything — assistant replies with their `thinking`/`thinking_ms` reasoning traces, `role:"ask_user"` clarifying-question rows, the `ui_choice` answers, and on a turn that failed or was stopped an `error` marker carrying the user-safe reason it ended (a failure message, or "You stopped this response." on a user cancel — a failure also streams a live `error` event with the same text, while a cancel carries the marker with no live `error` event) — so a client can rebuild the full conversation, including failed and stopped turns), `POST …/{id}/messages` (`{content ≤ 4 KB, ui_choice?}` → `202 {turn_id}`; one in-flight turn per draft, `409 turn_in_flight`; abuse-limited — 2 concurrent turns/account + daily turn/token/cost caps → `429` with `Retry-After`; a later message clearly unrelated to the strategy is answered with a canned redirect and no model turn, still `202`; reply + new preview hash stream over `assistant_draft_events`; preview is read via the same `GET /strategies/preview/{source_hash}/results`), `POST …/{id}/cancel` (`404 no_active_turn` if none), `POST …/{id}/fork` (clones a draft — strategy bundle + chat transcript — into a new editable draft you own; backs Clone on an assistant strategy, since a saved conversation is frozen; owner-only, `404` if not yours), `DELETE …/{id}`. SAVE reuses `POST /strategies` with `{draft_id, privacy?}` (publishes the draft's source as an `origin:"assistant"` strategy). An assistant strategy's `config` is absent (omitted) on `GET /strategies` and `null` on `GET /strategies/{id}` (written as source, not a form config — no "open in Builder"; instead its OWNER's `GET /strategies/{id}` carries an owner-only `draft_id` and Clone forks that conversation via `…/{id}/fork`), and it can run as a bot like any saved strategy, provided its exact source passes the current live-trading rules — `POST /bots` (and each session start) re-checks it and returns `403`/`422 strategy_not_bot_eligible` otherwise (regenerate the source in the assistant and save again). - [Portfolios](https://www.0dtespx.com/openapi.yaml): a portfolio is a named collection of 1–20 of the caller's saved strategies whose COMBINED backtest results are computed at read time — nothing aggregated is stored. Model: every member is an equal fully-funded $100k sleeve (starting capital = N × $100k), daily portfolio P&L = Σ member net P&L under the viewer's fee schedule + slippage, and results cover only the INTERSECTION window (up to the least-updated member's coverage — one stale member truncates the whole portfolio). Per-session status combination: all completed (or completed+skipped — skipped sleeves add $0) ⇒ completed and counted; all skipped ⇒ skipped; any failed or not-yet-computed member session ⇒ failed; any halted ⇒ halted; only combined-completed sessions enter metrics/curve. Privacy mirrors strategies (private default; public = unlisted link-only) and is enforced BOTH ways: portfolio→public 409s (`private_members`) while any member is private, and member strategy→private 409s (`strategy_in_public_portfolio`) while inside a public portfolio; DELETE /strategies/{id} 409s (`strategy_in_portfolio`) while the strategy is in any portfolio (payload names them — remove the membership first). Endpoints: `GET /portfolios` (list, headline stats folded on read, updated_at desc), `POST /portfolios` (`{name?, strategy_ids[1..20]}`; 422 on dupes/foreign/count; `409 portfolio_limit_reached` when already owning 100 portfolios; duplicate = re-POST the member ids, no clone endpoint; → 201 detail payload), `POST /portfolios/preview` (`{strategy_ids[1..20]}`, validated like create; folds the ad-hoc set exactly like the detail read — same window/sleeves/fee+slippage overlay — with ZERO side effects: no portfolio row, no backtest jobs, nothing persisted; → 200 with the snapshot minus identity: member_count/folded_members, window, starting_capital = folded_members × $100k (a member with no results contributes no sleeve), the same results block, and a lean members[] with title/has_results/date_to/new_sessions/exec_status; a selection where nothing folds ⇒ 200 with empty results and no window; powers the create page's live preview), `GET /portfolios/{id}` (AUTH OPTIONAL, public-strategy access pattern; snapshot mirrors the strategy results snapshot: window, starting_capital as string, combined equity_curve/benchmark/metrics/days with per-day `member_slices` breakdown + `slippage`, members[] in the GET /strategies list-item shape (incl. each member's `params` summary) + `date_to`/`has_results`/`failed_days`; members with no results are excluded from the fold; a public portfolio drops no-longer-public members for non-owners), `PATCH /portfolios/{id}` (`{name?, privacy?, strategy_ids?}` — membership replace revalidated; returns the refolded snapshot), `DELETE /portfolios/{id}` (deletes only the portfolio; member strategies untouched), `POST /portfolios/{id}/results/update` (fan-out to every member with missing sessions / retryable failed sessions / reset backtest; NEVER 429s — over-cap members park as `deferred` and auto-promote; 202 with per-member outcomes `queued|deferred|already_running|up_to_date`; every started job counts against the CALLER's 3-run cap while it drains), `GET /portfolios/{id}/results/days.csv` (date, status, summed gross/fees/slippage/net + one net column per member; member cells EMPTY, not 0.00, on sessions excluded from metrics), `GET /portfolios/{id}/results/days/{date}` (aggregated drill-in: each traded member re-run through the engine — cold path tens of seconds, warm cached; skipped/zero-order member-days skip the engine; orders/transactions/decision-log merged in time order with `strategy_id`/`strategy` tags; combined intraday_curve step-summed from $100k-seeded member curves; per-member summary incl. reconciliation_warning; costs member_count × 10 rate-limit credits; date must lie inside the window). - [Bots](https://www.0dtespx.com/openapi.yaml): A bot runs a saved strategy live, hands-free; a bot IS a `bot`-type account on the `live_sim` engine — the bot id === the account id — with a pinned strategy, a risk policy, and an automatic re-pricing policy, and its carried `cash_balance` is continuous/compounding across sessions (NOT reset per day). The Bot payload exposes the pinned strategy's declarative `entry` window ({start_time, end_time, days_of_week?}, ET, Monday=0; absent for a strategy with no declared window) — session starts are gated on it (see the 422s below), and it is ENFORCED FOR THE WHOLE SESSION: the window is frozen into the session's `policy_snapshot.entry_window` at start, and once ET reaches `end_time` the platform cancels any still-unfilled entry order, stops re-pricing entries, and refuses further opening orders (recorded as an `entry_window_closed` decision-log entry) — an entry never fills after the window it was meant for. `strategy_id` everywhere is the caller's saved-strategy LINK id (route-compatible with `/strategies/{id}`); the bot pins the immutable strategy behind it (`shared_strategy_id`), and `strategy_id` is NULLABLE once the user unlinks the strategy (the bot keeps running). AUTOMATIC RE-PRICING (`reprice` object; the per-bot TUNING is live-only — a backtest runs the same mechanism at the platform defaults): the platform prices+re-prices the bot's unfilled LIMIT orders so resting orders fill instead of languishing. Each order is placed at a SPREAD-AWARE start price (how deep inside the spread depends on the spread width S in ticks and whether the order is passive (entries/take-profits) or aggressive (stop-losses): S=1 natural far touch (buy ask/sell bid), S=2 mid, S=3 one tick inside for passive / one tick toward the market for aggressive, S=4 one tick inside for passive / mid for aggressive, S≥5 a quarter-spread inside the mid for passive / mid rounded to the near side for aggressive), then walked one UNFAVORABLE tick at a time (a retry) toward a fill. Two blocks split by intent, BOTH ON BY DEFAULT: `entry` (opening orders) is patient+BOUNDED and REBUILT — on each retry the strategy re-selects the entry's legs from the CURRENT market (so the strikes stay the ones the strategy would pick right now, not the ones it picked minutes ago) and the fresh structure is re-priced from the current spread, conceding at most `max_retries` unfavorable ticks (a tick count, 0..50, default 4); at that cap the order stops conceding but keeps tracking the market, and the entry is given up only when the strategy's entry window ends (see the `entry` window above) — an unfilled entry is never left resting past it; `exit` (closing orders) is relentless+UNCAPPED — keep retrying until filled; the FIRST re-price waits the class interval after placement, then it follows an adverse market immediately (paced only by min_interval_seconds) and never relaxes on a favorable wiggle (guarantees the exit), with SEPARATE take-profit and stop-loss cadences so a stop-loss can chase faster than a take-profit; disabling exit re-pricing risks positions that never close and large losses. `entry`: `enabled` (default true), `interval_seconds` (4..600; default 10), `max_retries` (0..50; default 4), `min_interval_seconds` (4..120; default 4, must be ≤ interval_seconds). `exit`: `enabled` (default true), `take_profit_interval_seconds` (4..600; default 10), `stop_loss_interval_seconds` (4..600; default 5), `min_interval_seconds` (4..120; default 4, must be ≤ both intervals). EVERY interval floors at 4 SECONDS, sized for the resting rung: a rung the market has to come to needs its price to hold 3 consecutive seconds, so a rung replaced faster than that could never confirm one (a rung priced strictly past the market confirms in 2, but the floor still has to cover the resting one); stored policies below the floor were raised to 4. When a kind's re-pricing is ON the platform sets its start price and manages the retries; turning it OFF leaves that kind of order at the strategy's own price (manual-pricing mode). Frozen per session (takes effect on the NEXT session). AUTOMATIC MALFUNCTION HALT (order guardrail): each session's `policy_snapshot` carries a platform-derived, read-only `order_guardrail` block (`window_seconds`, `max_places_per_window`, `max_replaces_per_window`); a bot that submits abnormally many orders in a short rolling window is auto-halted with `halt_reason:"order_guardrail_tripped"` — working orders cancelled, the session ends `halted` and finalizes at current marks; the caps are sized from the platform order budget and the frozen re-pricing envelope so a normal strategy (including maximally aggressive exit re-pricing) cannot reach them, and are NOT user-settable. A SECOND, count-based brake sits alongside it: a fixed platform ceiling on how many orders one session may leave RESTING at once (a cancel-leak tripwire, not a risk control), breached → `halt_reason:"max_open_orders_exceeded"` down the same halt path. Also NOT user-settable — a bot has NO order- or position-count knobs. AUTOMATIC START (`auto_start` boolean on the Bot, always present, default false): when true the platform opens today's session for the bot at each trading session's open, under the same eligibility rules as a manual start (RTH, entry window, still bot-eligible). At most ONE automatic attempt per bot per trading day; ANY persisted session today (manual or automatic, active OR terminal incl. `failed`) consumes the day and suppresses the automatic one; enabling mid-session (via PATCH, creating the bot after the open, or un-archiving a bot that has it on) takes effect at the NEXT session open (never a surprise start seconds later); a failed automatic registration leaves a visible `failed` session (not retried); deterministic skips (not an entry day, window passed, ineligible) are silent; the manual `POST /bots/{id}/sessions` contract is unchanged — a same-day re-run after a terminal session is still allowed. `POST /bots` (`{strategy_id (REQ), name (REQ), description?, engine? (defaults live_sim; broker_* → 409 engine_not_available), data_staleness_threshold_ms? (500..10000), auto_pause_on_error?, error_budget? (0..100), reprice? (entry/exit blocks; omit for defaults-on), auto_start? (default false)}`; every bot starts with a fixed $100,000 — there is no starting-capital input → 201 Bot; errors 400 invalid_input/invalid_strategy_id/invalid_reprice, 403 strategy_not_found_or_not_owned, 403 strategy_not_bot_eligible (pinned source fails the current live-trading rules), 409 engine_not_available, 409 name_taken), `GET /bots?strategy_id={link id}` (→ array of Bot; optional filter by link id, unknown link → empty array), `GET /bots/{id}` (→ `{bot, sessions[]}`; 404 not_found), `PATCH /bots/{id}` (`name?`/`description?`/`status? ("idle"|"archived")`/`auto_start?`/the risk-policy fields (`data_staleness_threshold_ms?`, `auto_pause_on_error?`, `error_budget?` — no count knobs exist)/`reprice?` — A RUNNING BOT IS FROZEN: the WHOLE patch is refused with 409 bot_has_active_session while the bot has a live session (name, description, status, auto_start and risk policy alike — stop the live session first), so every edit lands between sessions and takes effect on the NEXT one (each session keeps the `policy_snapshot` it froze at start); `auto_start` enabled after today's open (by PATCH once the day's session has ended, or by un-archiving) defers to the next session open; 404 not_found, 409 name_taken (duplicate active name)), `DELETE /bots/{id}` (→ 204; 409 bot_has_active_session — stop the live session first; 404 not_found), `GET /bots/{id}/summary` (lifetime roll-up: total_sessions, win/loss counts, win_rate_pct, total_pl, best/worst day, average_day_pl, sparkline[]), `GET /bots/{id}/analytics` (round-trip trade log + aggregate trade metrics over the bot's FINISHED, FLAT sessions — `completed` (settled at the close) AND `stopped` (the book went flat during the day: entered-and-exited, entry window closed unfilled, or stopped then flattened); `halted`/`failed` are excluded, which is why this can differ from /summary (every finalized session) — plus `day_series` (one entry per folded session for the equity curve/calendar; a date REPEATS when the bot ran twice that day); A DAY JOINS THE FOLD ONLY ONCE IT HAS CLOSED — a session is analyzed after its own `end_time` has passed, NOT the moment it finalizes, so a run that went flat mid-day appears here after that day's close (the same boundary that turns it from the bot's current day into a past one); this is the second reason the fold can differ from /summary, which has no timing rule and carries today's result immediately; same body as `GET /accounts/{id}/analytics` plus `day_series`; read-only). Sessions: `POST /bots/{id}/sessions` (start today's session, no body; opening balance is the carried `cash_balance`; → 201 `{session_id}`; errors 404 not_found, 409 bot_not_idle, 409 bot_session_exists, 422 outside_rth, 422 not_entry_day (the strategy's entry.days_of_week excludes today), 422 entry_window_passed (the strategy's entry window already passed for today, or opens after an early close — starting BEFORE the window is allowed, the bot idles until it opens), 422 strategy_not_bot_eligible (the pinned strategy's source no longer meets the current live-trading rules — re-checked at each session start; regenerate it in the AI assistant and create a new bot), 503 live_trading_unavailable, register_sim_failed (relayed status)), `GET /bots/{id}/sessions?limit={n}` (→ BotSession[]; ordered date DESC, then ended_at DESC NULLS FIRST, then id — deterministic when a date holds one completed + several halted/stopped/failed reruns; default 90, `?limit=` 1..1000, non-positive/non-numeric → default), `GET /bots/{id}/sessions/{session_id}` (→ BotSession; 404), `DELETE /bots/{id}/sessions/{session_id}` (STOP THE STRATEGY, NOT THE TRADE — the bot makes no further decisions and its working orders are cancelled, but the session STAYS LIVE AND KEEPS ITS POSITIONS; nothing is closed for you and nothing is cashed out at a theoretical price. `status` does NOT change (stays 'running'); the stop rides two read-only session timestamps instead — `strategy_stopped_at` (+ `strategy_stopped_by`: 'user'|'admin') is the request, `strategy_stop_applied_at` is when it took effect (orders cancelled, bot provably out of the market). Re-issuing in between is a no-op. EXCEPTION — a stop with NOTHING LEFT IN THE MARKET does not stay live: a session still 'scheduled'/'registering' (the strategy never ran) or one whose strategy had already exited everything holds nothing and finalizes as 'stopped' (`halt_reason` 'user_stop') within a tick, with nothing to liquidate and the (bot, date) slot freed for a fresh session. So a stopped session that IS still live is by definition one HOLDING POSITIONS, and it ends one of three ways: you RESTART it (`POST …/sessions/{session_id}/restart`, below) and the strategy resumes on the same session; you flatten it (via the bot-session LIQUIDATION endpoints below — that is how closing orders are placed on a bot session, whose own /orders route is read-only) and it finalizes as 'stopped' the moment it is flat, balance carried; or you hold to the 16:00 ET close and it settles as 'completed' exactly as it would have. The bot stays frozen for PATCH/DELETE (409 bot_has_active_session) until the SESSION is over — flat or settled — not merely stopped. A malfunction HALT is unchanged and still terminal at current marks. → 202; 404), `POST /bots/{id}/sessions/{session_id}/restart` (UNDO A STOP on a session that is still holding positions: the strategy resumes on the SAME session, keeping its positions and the ctx.state it had built up, and all three stop fields (`strategy_stopped_at`/`_by`, `strategy_stop_applied_at`) are cleared so the session reads like one never stopped; logged as a `strategy_restarted` decision entry (source "api") + a `session_resumed` live_bot_event. NEVER grants a second entry — the one-entry budget is reconstructed from the orders already filled. → 202; 409 bot_not_stopped (no ACKNOWLEDGED stop to undo: never stopped, the ack has not landed yet, or a previous restart already took effect), 409 bot_liquidated (`liquidated_at` is stamped — PERMANENT, including after the liquidation was cancelled), 409 bot_session_not_restartable (the session has finished — its terminal record is untouched — or the stop was placed by the platform operator (`strategy_stopped_by` 'admin'), which the user cannot undo), 404. Restart is only ever about resuming the live session — to trade the bot again after a session finished, START A NEW SESSION), `POST /bots/{id}/sessions/{session_id}/liquidations` `{method: smart|aggressive}` + `GET|DELETE /bots/{id}/sessions/{session_id}/liquidations/current` (close every open position on a STOPPED bot session — identical contract, snapshot and methods to the accounts liquidation below, priced under the fee schedule that session was opened with; ADMITTED ONLY once `strategy_stop_applied_at` is present AND the session is still live, else `409 bot_not_stopped`; otherwise the same admission as accounts: 409 no_open_positions, 409 liquidation_in_progress, 400 on a bad method. THE POST PERMANENTLY BLOCKS RESTART: it stamps a read-only `liquidated_at` on the session before placing anything, never cleared, so `POST …/restart` returns `409 bot_liquidated` from then on — even after a `DELETE …/liquidations/current`. Cancelling the liquidation does NOT end the session and does NOT restart the strategy: the session stays live and holding, and the remaining exits are another liquidation or the 16:00 ET close settling it 'completed'. Once the closing fills land and the book is flat the session finalizes itself 'stopped', balance carried. `liquidated_at` records the DECISION to close the book, not that it is closed — read the liquidation snapshot for progress and the session `status` for whether the day is over), `GET /bots/{id}/sessions/{session_id}/orders | /positions | /transactions | /financials | /history | /decision-log` (live data scoped to the session — same shapes the live-trading endpoints return; `/financials` returns the session object hydrated with its financials, same shape as `GET /bots/{id}/sessions/{session_id}` EXCEPT its `status` is the FULL BOT lifecycle value (any of scheduled/registering/waiting_for_data/running/settling/settled/completed/halted/stopped/failed/abandoned — polled in every state, not manual open/settled/abandoned), `starting_capital` mirrors opening_balance, and its `settled_at` is the FINALIZATION timestamp present for ANY finalized session (completed/halted/stopped/failed/abandoned), NOT only a natural settlement (diverges from the manual AccountSession.settled_at meaning); `/history` is the per-second P&L-curve series, same shape + `?interval=` as `GET /accounts/{id}/sessions/{sid}/history`; decision-log is the session's decision log in ASCENDING emission order (oldest first), the NEWEST 500 on a plain read and the COMPLETE log with `?full=true`: the strategy's ctx.log() entries (source "strategy") plus platform lifecycle entries (source "runner") and entries recorded against a user request (source "api" — `strategy_restarted`) — session_started (payload started_by: "user"|"auto"), session_ended (payload status + halt_reason when present), order re-pricing activity (order_repriced, whose payload carries `legs_changed` when that step also re-selected the entry's legs, and reprice_failed), the pricing rows order_start_priced (`class`/`start`/`tier`) and order_repriced, which BOTH also carry `quotes_at` (leg option symbol -> the timestamp of the quote used for that leg, RFC 3339 UTC) + `net_bid`/`net_ask` (the order's net bid and ask at that moment, decimal strings), entry_window_closed (the entry window ended with an order still unfilled; payload carries the window and the cancelled order ids), and the order lifecycle: order_filled (payload order_id/class/fill_price/price_effect), order_canceled, order_rejected (payload includes rejection_reason; ALSO emitted for a placement the exchange REFUSED OUTRIGHT — not enough buying power, an invalid structure, a temporary refusal — which additionally carries `order_created:false` (the exchange never ACCEPTED it — the order does exist under /orders reading `rejected`, with no accepted_seq, because it never reached the book) and an optional `message` with the rejection detail), and the ENTRY-RECOVERY pair: when the refused placement was the session's ENTRY and the refusal was TEMPORARY, the platform rebuilds the entry from the CURRENT market and re-submits it, for a few minutes and only while the entry window is still open — one entry_retried entry per attempt (payload order_id/attempt/max_attempts/reason; the order_id is a NEW one naming that re-submission — the refused submission is a `rejected` order that never reached the book, and the re-submission may itself be refused, so it becomes a WORKING order only once one is accepted), while a refusal that would fail identically however often it were retried (buying power, an invalid structure) is not retried at all; either way, an entry that cannot be recovered ends the day with AT MOST ONE entry_retry_abandoned entry (payload attempts/reason) rather than in silence). REPLAY a terminal session: `/positions` and `/transactions` take `?at=` (reconstruct the day at an arbitrary moment; `at` past the terminal clock is clamped; malformed `at` → 400). The DEFAULT READ (no `at`) differs per endpoint: `/positions` reads at the terminal clock — a settled natural close (`completed`, or the transient `settled` the exchange stamps before the runner finalizes it) reads at its settled close WITH end-of-day settlement applied, a halted/stopped/failed/abandoned day reads at its final stopped clock with NO synthetic settlement — while `/transactions` has NO cutoff at all and returns the session's whole ledger (a completed day's settlement rows included). `/decision-log` takes the same `?at=` as a shifted window (newest ≤500 with datetime <= at; malformed → 400), and `?full=true` — alone or with `?at=` — lifts that window and returns the COMPLETE log. The session lifecycle is `scheduled → registering → waiting_for_data → running → settling → completed` (or `halted`/`stopped`/`failed`) — a strategy stop is NOT a lifecycle step (see DELETE above: the row stays `running` while it holds). A SESSION ENDS THE MOMENT IT HAS NOTHING LEFT IN THE MARKET and can open nothing further: `stopped` with `halt_reason` 'flat_book' (it entered and fully exited, or its entry window closed with no fill) or 'user_stop' (a stop was what ended it) — no idling to the bell, so a bot's day is routinely over well before 16:00 ET, and the freed (bot, date) slot allows a NEW session the same date; only a session still holding something (or still working an entry inside its window) rides to the close and ends `completed`. BUT FINISHING EARLY ENDS THE SESSION, NOT THE DAY: BotSession carries a REQUIRED `end_time` — that session's own trading-day close (16:00 ET, earlier on a half day), frozen at session start — and a session finalized before it is still the bot's CURRENT day; the day becomes a past one to review only once `end_time` has passed, which is the same boundary `GET /bots/{id}/analytics` folds on. BotSession also carries the read-only `liquidated_at` (see the liquidation endpoints). The bot's `runtime_status` is `idle`/`scheduled`/`running`/`halted`/`stopped`/`completed`/`failed`/`archived`; all session P&L/rollup fields are absent (omitted) until the session finalizes. ## API — health - [Health probe](https://www.0dtespx.com/openapi.yaml): `GET /health` — liveness check, no auth.