API reference

Bots API

Run a saved strategy as a live bot: sessions, lifecycle, and per-session data.

A bot runs one of your saved strategies as a live paper-trading account. Under the hood a bot is a dedicated bot account on the live_sim engine — with a pinned strategy and a risk policy attached. It lives on its own surface, created and managed here (never through /accounts); the bot id is the account id. Like every account, its balance is continuous: the carried cash compounds day to day and is never reset per session. All endpoints require authentication.

See Trading bots for the concepts behind the endpoints below.

What a bot is

  • A pinned strategy. You create a bot from one of your saved strategies (the strategy_id you pass is your saved-strategy link id — the same id you read at /strategies/{id}). The bot pins the immutable strategy that link points at, so unlinking the strategy later doesn't change what the bot runs.
  • A risk policy. Per-bot guardrails — a data-staleness threshold, an error budget, and an auto-pause flag. A running bot can't be edited at all, so policy changes always take effect on the next session; a session keeps the policy it froze at start.
  • An automatic re-pricing policy. The bot re-prices its own unfilled limit orders so resting spread orders actually fill instead of languishing at a price the market never comes back to — see Automatic re-pricing below. On by default, part of the same frozen policy.
  • An automatic-start toggle. The optional auto_start flag makes the platform open the bot's session for you at each trading session's open, instead of you calling POST /bots/{id}/sessions by hand — see Automatic start below. Off by default.
  • A continuous balance. The bot's cash_balance is the carried cash that rolls from one session to the next (compounding). Each session opens with that balance — there is no per-session starting capital to pass.

A bot's status is idle or archived; its runtime_status (idle · scheduled · running · halted · stopped · completed · failed · archived) reflects what its current or last session is doing.

Bot endpoints

Method Path Purpose
GET /bots List your bots (optionally by strategy)
POST /bots Create a bot
GET /bots/{id} Get a bot with its sessions
PATCH /bots/{id} Rename, archive, or edit the risk policy
DELETE /bots/{id} Delete a bot
GET /bots/{id}/summary Lifetime performance roll-up
GET /bots/{id}/analytics Round-trip trade analytics + day series (closed days)

Create a bot

curl -s https://api.0dtespx.com/bots \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "strategy_id": "a1b2c3d4-…",
    "name": "Delta-30 put spread",
    "description": "Runs my 30-delta credit spread live"
  }'

strategy_id and name are required. strategy_id is your saved-strategy link id (the id at /strategies/{id}); the bot pins the strategy behind it. name is required (≤ 120 chars) and must be unique among your active bots. Every bot starts with a fixed $100,000 — there is no capital to choose. An optional description is capped at 2000 characters. The engine defaults to live_sim; a broker_* engine returns 409 engine_not_available.

The risk-policy fields are optional and bounded: data_staleness_threshold_ms (50010000), auto_pause_on_error, and error_budget (0100). These are runaway tripwires: if a session exceeds a limit it cancels its working orders and ends the session halted. There is nothing to set for order or position counts — the platform brakes a runaway bot itself, with the read-only order guardrail on how fast a session submits orders and a fixed platform ceiling on how many orders one session may leave resting at once (breaching it halts the session with halt_reason: "max_open_orders_exceeded"). Neither is user-settable, and neither is a loss limit. Loss management belongs to the strategy — have it close positions or call ctx.halt() on the conditions you care about. The optional reprice object configures automatic re-pricing; omit it to accept the defaults (on). The optional auto_start boolean (default false) enables automatic start at each session open.

The response is the bot object. Errors: 400 invalid_input / invalid_strategy_id / invalid_reprice, 403 strategy_not_found_or_not_owned, 403 strategy_not_bot_eligible (the pinned strategy's source doesn't meet the current live-trading rules — regenerate it in the AI assistant and save again), 409 engine_not_available, 409 name_taken.

Automatic re-pricing

A bot places resting limit orders. An order priced right at the spread mid tends to sit unfilled while the market drifts. To fix this the platform prices and re-prices those orders: it starts each order at a spread-aware price and, if the order hasn't filled, walks it one unfavorable tick at a time (a retry) toward a fill. A backtest does the same thing — same starting prices, same retry cadences, same fill rules — so the sessions you measured and the sessions a bot trades are executed the same way. What is live-only is the reprice object below: the cadences are tunable per bot, while a backtest always runs the platform defaults.

Re-pricing is a difference in chasing — moving an unfilled order toward the market — not in how a fill is priced: a limit the market has to come to fills at its own limit once its condition has held for the confirmation window, while one priced strictly past the market at the second that price was set crosses the spread and fills at once, at the market it crossed. Each re-price step sets a new price, so it starts that test over and restarts the confirmation window with it: a step that puts the order through the market crosses there, fills on that step, and books that step's market — so a re-priced order can come back with a fill_price better than the price it is carrying.

Starting price. How far inside the spread the first price sits depends on the spread width S (in ticks) and whether the order is passive (entries and take-profit closes) or aggressive (stop-loss closes). "Natural" is the far touch — the ask for a buy, the bid for a sell:

Spread S (ticks) Passive — entry / take-profit Aggressive — stop-loss
1 natural (buy: ask, sell: bid) natural
2 mid mid
3 buy: bid + 1 tick; sell: ask − 1 tick buy: ask − 1 tick; sell: bid + 1 tick
4 buy: bid + 1 tick; sell: ask − 1 tick mid
≥ 5 buy: floor(mid − ¼·spread); sell: ceil(mid + ¼·spread) mid, rounded to the unfavorable tick

Retries. After the start, an unfilled order is re-priced one tick toward the market on a per-kind cadence — both kinds on by default:

  • Entries (entry) — patient, bounded, rebuilt every step. An entry retry is not just a new price: the strategy re-selects the entry's legs from the current market and the fresh structure is re-priced from the current spread. A delta- or premium-targeted entry therefore keeps asking for the strikes it would pick now, never the ones it picked minutes ago. max_retries bounds how far the order may walk from its start — the maximum number of one-tick concessions — and once that cap is reached the order stops conceding but keeps tracking the market (still rebuilt and re-priced at the current start each interval). The give-up is the strategy's own entry window: at end_time the platform cancels the unfilled entry, stops re-pricing it, and refuses further opening orders for the session, so an entry can never fill after the window it was meant for.
  • Exits (exit) — relentless, uncapped. Keep retrying until the order fills. The first re-price waits the class interval after the order is placed; after that it follows an adverse market immediately (paced only by min_interval_seconds) and never relaxes on a favorable wiggle — this is what guarantees a bot can get out of a position (the exception is a malfunction halt: if the bot trips the order guardrail its session is halted, its working orders are cancelled, and it finalizes at current marks rather than re-pricing further). Take-profit and stop-loss closes retry on their own intervals (take_profit_interval_seconds / stop_loss_interval_seconds), so a stop-loss can chase faster than a take-profit. On a sustained adverse move that cadence rarely gets a turn: a stop-loss close whose market has run through its limit for the whole confirmation window is filled at the market by the engine (see how a limit or stop order fills), which lands before the first re-price is due at 4+ seconds. stop_loss_interval_seconds therefore bites in a choppy market, where the crossing keeps resetting. Turning exit re-pricing off is dangerous — a closing order may never fill and the position can run against you, so losses can be large.

The entry block accepts enabled (default true), interval_seconds (4600; default 10 — how often the entry is rebuilt and re-priced), max_retries (the walk-depth cap: a tick count, 050; default 4), and min_interval_seconds (4120; default 4 — the floor between replacements, which must not exceed interval_seconds). The exit block accepts enabled (default true), take_profit_interval_seconds (4600; default 10), stop_loss_interval_seconds (4600; default 5), and min_interval_seconds (4120; default 4, which must not exceed either interval).

Every one of those intervals floors at 4 seconds, sized for the slower case — the equality window: a rung the market has to come to needs its price to hold for 3 consecutive seconds, so a rung replaced faster than that could never get there — a ladder that re-priced every second would restart the clock forever and never fill anything. (A rung that steps strictly past the market fills on the step that sets it, but the floor still has to cover the resting rung.) Stored policies below the floor were raised to 4. Like the rest of the policy, re-pricing settings are frozen per session and take effect on the next session.

When a kind of order has re-pricing on, the platform sets its starting price and manages the retries for you. Turn it off and the bot leaves that kind of order at the price the strategy set and never re-prices it — a deliberate manual-pricing mode, and the one setting that makes a bot's execution differ from the strategy's results, which always run both kinds on at the defaults.

# create a bot with a tighter entry retry cap and default (on) exit re-pricing
curl -s https://api.0dtespx.com/bots \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{
    "strategy_id": "a1b2c3d4-…",
    "name": "Delta-30 put spread",
    "reprice": { "entry": { "max_retries": 2 } }
  }'

Automatic start

By default you start a bot's day yourself with POST /bots/{id}/sessions. Set auto_start: true (on create or via PATCH) and the platform opens the session for you at each trading session's open instead — subject to exactly the same eligibility rules as a manual start (regular trading hours, the strategy's entry window, and the strategy still meeting the current live-trading rules).

# create an auto-starting bot
curl -s https://api.0dtespx.com/bots \
  -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"strategy_id":"a1b2c3d4-…","name":"Delta-30 put spread","auto_start":true}'

# turn auto-start on for an existing bot
curl -s -X PATCH https://api.0dtespx.com/bots/$BOT \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"auto_start":true}'

How it behaves:

  • At most one automatic attempt per trading day. The platform tries each auto-start bot once per day, at the session open (or, after an outage, the first moment it can). A bot's auto_start state is read on the wire as the auto_start boolean on the bot object.
  • Any session today already counts. If the bot already has a session for today — whether you started it manually or the platform did, and whether it is still running or already ended (including one that ended failed) — no automatic session is opened. That day is spoken for.
  • Enabling mid-session takes effect next session. Turning auto_start on after today's open — by PATCH once the day's session has ended (a running bot can't be edited at all), by creating the bot after the open, or by un-archiving a bot that has it on — never surprise-starts a session seconds later. It takes effect at the next session open. (The Start button / POST /bots/{id}/sessions is right there if you want it to start now.)
  • A failed automatic start is visible. If the platform can't register the live session (e.g. live trading is briefly unavailable), it leaves a failed session for the day — the same outcome a manual start failure produces — and does not retry automatically.
  • Skips are quiet. When today isn't an entry day for the strategy, the entry window has already passed, or the bot is otherwise ineligible, the platform simply skips it for the day with nothing surfaced.
  • Manual re-runs still work. Automatic start doesn't change the manual contract: after a session has ended for the day, POST /bots/{id}/sessions can still start a fresh one (the automatic path just won't add a second on its own).

List bots

curl -s "https://api.0dtespx.com/bots" -H "Authorization: $TOKEN"

# filter to bots pinned to one saved-strategy link
curl -s "https://api.0dtespx.com/bots?strategy_id=a1b2c3d4-…" -H "Authorization: $TOKEN"

?strategy_id= filters by your saved-strategy link id; an unknown link returns an empty array.

Each bot in the list carries the same active_session / last_session pair as Get a botactive_session is the running session (absent when none is running) and last_session is the most recent one (absent until the bot has run at least once). One GET /bots is therefore enough to tell, for every bot at once, whether it is running and how its latest day stands.

Get a bot

curl -s "https://api.0dtespx.com/bots/$BOT" -H "Authorization: $TOKEN"

Returns { "bot": …, "sessions": [...] } — the bot plus up to its 90 newest sessions. 404 if the bot doesn't exist or isn't yours. The bot object always carries auto_start (whether automatic start is on), and also active_session (the running session — absent when none is running) and last_session (the most recent session — absent until the bot has run at least once).

Edit or archive a bot

PATCH accepts any of name, description, status (idle or archived), auto_start, the risk-policy fields, and the reprice object — only the fields you send are applied. Toggling auto_start mid-session defers to the next session open (Automatic start).

# rename
curl -s -X PATCH https://api.0dtespx.com/bots/$BOT \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Delta-30 — v2"}'

# tighten the error budget (applies to the NEXT session)
curl -s -X PATCH https://api.0dtespx.com/bots/$BOT \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"error_budget":5}'

A running bot is frozen. While a session is in flight the whole PATCH is refused with 409 bot_has_active_session — name, description, status, auto_start, and the risk policy alike. Stop the live session first. Every edit therefore lands between sessions, and takes effect on the next one: a session keeps the policy snapshot it froze at start.

A new name must stay unique among your active bots (409 name_taken).

DELETE /bots/{id} removes the bot and returns 204. It is blocked by the same gate (409 bot_has_active_session) — stop the live session first.

Lifetime roll-up

curl -s "https://api.0dtespx.com/bots/$BOT/summary" -H "Authorization: $TOKEN"

Returns aggregate performance across every finalized session — a stopped day at its realized result (the book was flat when it ended), a halted / failed day at its marked-to-market P&L: total_sessions, win_count / loss_count, win_rate_pct, total_pl, best_day_pl / best_day_date, worst_day_pl / worst_day_date, average_day_pl, and a sparkline series.

Trading analytics

GET /bots/{id}/analytics is the bot counterpart of /accounts/{id}/analytics: it folds the bot's finished, flat sessions into round-trip trades and returns the same trade-level metrics, per-day trade counts, and trade log — plus a day_series (one entry per folded session, ascending and uncapped) with each day's opening balance, P&L, and fees for the equity curve and P&L calendar. Read-only.

curl -s "https://api.0dtespx.com/bots/$BOT/analytics" \
  -H "Authorization: $TOKEN" \
  | jq '{trades: .summary.total_trades, win_rate: .summary.win_rate_pct, days: (.day_series | length)}'

The summary, days, trades, and trades_truncated fields are exactly the account analytics shape (same round-trip grouping, same net-of-fees net_profit_loss, same nullable-but-present summary fields, same 1000-trade cap on trades). The extra day_series is:

{
	"day_series": [
		{
			"session_id": "…",
			"date": "2026-07-10",
			"opening_balance": "25000",
			"profit_loss": "125.40",
			"fees": "4.51"
		}
	]
}

Two outcomes are folded, and they are the two ways a bot day ends with nothing open: completed (held to the close and settled there) and stopped (the book went flat during the day — see Sessions). Both are fully realized, so the trade metrics and the day figures agree and no partial position is counted. Sessions that ended halted / failed are excluded here — they stay visible on the bot's session list, but their marked-to-market P&L is intentionally left out of this aggregate (which is why the total P&L here can differ from the lifetime roll-up). One date can appear twice in day_series if the bot finished flat mid-day and was started again.

A day joins this fold once it has closed, not the moment the session ends. A session that finished flat at 11:20 is analyzed after its own end_time — the same boundary that turns it from the bot's current day into a past one. Until then the bot's own page still shows the result immediately: /bots/{id}/summary rolls up every finalized session with no timing rule. So a brand-new bot whose only session is today's returns an empty analytics fold until the close, and that is the expected shape, not a missing day.

Sessions

A session is one trading day the bot ran. A bot session is its own live sim, so its reads return the same shapes as live trading. Sessions move through a lifecycle: scheduledregisteringwaiting_for_datarunningsettlingcompleted (or halted / stopped / failed).

A session ends the moment it has nothing left in the market. It does not idle to the close waiting for a bell: as soon as the book is flat and the strategy can open nothing further — it entered and fully exited, or its entry window closed with no fill — the session finalizes as stopped, halt_reason: "flat_book", with its balance carried to the next session. Only a session that is still holding something (or still working an entry inside its window) rides to the 4:00 PM ET close and ends completed. That means a bot's day is routinely over well before the close, and because a terminal session frees the day slot, the same bot can start a new session on the same date.

Ending early ends the session, not the day. Every session carries end_time — when its trading day closes (4:00 PM ET on a regular day, earlier on a half day), frozen at session start. A session that finalizes before end_time is still today's session: it is what the bot is doing today, and its day only becomes a past one to review once end_time has passed. That is the boundary the analytics fold uses too.

Stopping a bot is not a lifecycle step — it stops the strategy and the session keeps trading day status running while it holds its positions. See Stop a session.

Method Path Purpose
GET /bots/{id}/sessions List sessions (default 90; ?limit=)
POST /bots/{id}/sessions Start today's session
GET /bots/{id}/sessions/{session_id} Get one session
DELETE /bots/{id}/sessions/{session_id} Stop a session (positions stay)
POST /bots/{id}/sessions/{session_id}/restart Undo a stop (positions kept)
GET /bots/{id}/sessions/{session_id}/orders Live orders for the session
GET /bots/{id}/sessions/{session_id}/positions Positions (?at= to scrub)
GET /bots/{id}/sessions/{session_id}/transactions Whole transaction log (?at= to cut)
GET /bots/{id}/sessions/{session_id}/financials Session hydrated with financials
GET /bots/{id}/sessions/{session_id}/history Per-second financial history
GET /bots/{id}/sessions/{session_id}/decision-log Decision log (?at=, ?full=true)
POST /bots/{id}/sessions/{session_id}/liquidations Close everything on a stopped session
GET /bots/{id}/sessions/{session_id}/liquidations/current Liquidation progress
DELETE /bots/{id}/sessions/{session_id}/liquidations/current Cancel a running liquidation

Automatic malfunction protection

Every session's policy_snapshot carries a platform-derived, read-only order_guardrail block — window_seconds, max_places_per_window, and max_replaces_per_window. The platform watches how many orders a session submits: if it places more new orders, or re-prices more, than those caps allow within any rolling window, that is treated as a malfunctioning bot and the session is halted immediately with halt_reason: "order_guardrail_tripped". As with any halt, its working orders are cancelled and it finalizes at current marks (open positions valued at the latest tick, cash carried forward). These caps are not user-settable and are sized from the platform's order budget and the session's frozen re-pricing envelope, so a normal strategy — including one doing maximally aggressive exit re-pricing — cannot reach them; the guardrail only fires on genuinely abnormal order volume.

Start today's session

SID=$(curl -s -X POST "https://api.0dtespx.com/bots/$BOT/sessions" \
  -H "Authorization: $TOKEN" | jq -r .session_id)

No body — the date is implicitly today. The session opens with the bot's carried cash_balance (continuous / compounding — not a per-session starting capital), freezes the current risk policy into its snapshot, and the runner registers a live sim and drives the pinned strategy.

Session starts are gated on the strategy's entry window — the declarative {start_time, end_time, days_of_week?} (ET) the bot payload exposes as entry. If today's weekday isn't one the strategy enters on, or the window has already passed for the day, the start is refused. Starting before the window opens is fine: the bot idles until the strategy's entry time arrives.

The window is frozen into the session (policy_snapshot.entry_window) and enforced for the rest of the day, not only at the start: at end_time the platform cancels any still-unfilled entry order, stops re-pricing entries, and refuses further opening orders for that session — recorded as an entry_window_closed decision-log entry. Exits are never restricted; a position already opened is managed to its close as usual.

The window is also what bounds entry recovery. An entry submission the exchange refuses for a temporary reason never reaches the book, so there is nothing to re-price — instead the platform rebuilds the entry from the market as it stands and submits it again, for a few minutes and only while the window is still open. Each of those re-submissions is an entry_retried decision-log entry (payload order_id, attempt, max_attempts, reason). The order_id is a new one, because the refused submission is finished (it reads rejected under /orders) — it identifies that re-submission, which may itself be refused, so it becomes a working order only once one of them is accepted. A refusal that would fail identically however often it were retried — not enough buying power, an invalid structure — is not retried at all. Either way, when the entry cannot be recovered the session logs at most one entry_retry_abandoned entry (payload attempts, reason): the day ends without a trade, but it says so rather than ending in silence.

Errors: 404 not_found; 409 bot_not_idle (the bot is archived); 409 bot_session_exists (today's session already started); 422 outside_rth (outside regular trading hours); 422 not_entry_day (the strategy's entry.days_of_week excludes today); 422 entry_window_passed (the strategy's entry window has already passed for today, or opens after an early close); 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; 504 outcome_unknown (the platform could not confirm whether the session registered — retry to resolve it). A live-registration failure surfaces as register_sim_failed with the relayed status.

Stop a session

# stop the session — working orders are cancelled, the POSITIONS STAY OPEN
curl -s -X DELETE "https://api.0dtespx.com/bots/$BOT/sessions/$SID" -H "Authorization: $TOKEN"

DELETE …/sessions/{session_id} returns 202 and stops the strategy, not the trade. The bot makes no further decisions and its working orders are cancelled — but the session stays live and keeps everything it holds. Nothing is closed on your behalf and nothing is valued at a theoretical price: you exit at real prices, on your terms.

Because the session is still live, its status does not change (it stays running). Two read-only timestamps on the session carry the stop instead:

Field Meaning
strategy_stopped_at when the stop was requested; strategy_stopped_by is user (you) or admin (support)
strategy_stop_applied_at when it took effect — orders cancelled, the bot provably out of the market
liquidated_at when a liquidation was first started on the session; never cleared, and it blocks restart from then on

All three are read-only, and a restart clears the two stop stamps (never liquidated_at).

The gap between them is the bot standing down; it is normally a second or two. Repeating the request in the meantime is a no-op, and a stop can be requested at any point in a live session, including before the strategy has placed anything.

A stop only keeps the session alive if there is something to keep. A stopped session with nothing left in the market has no reason to sit there, so the platform finalizes it as stopped (halt_reason: "user_stop") within a tick. That covers a session stopped before it has begun trading — still scheduled or registering, so the strategy never ran — and one whose strategy had already exited everything before the stop: both are over immediately, with nothing to liquidate, and the freed day slot lets you start a fresh session for the bot the same day.

So a stopped session that is still live is, by definition, a session holding positions — and from there it ends one of three ways:

  • You resume it. Restart the session and the strategy picks up where it stood down, keeping the positions it holds.
  • You close the book. Liquidate it — that is how closing orders are placed on a bot session, whose own /orders route is read-only. The moment the closing fills land and the book is flat, the session finalizes on its own as stopped, and its balance carries to the next session.
  • You hold to the close. Do nothing and the 4:00 PM ET close settles the day exactly as it would have without the stop — the session ends completed, with settlement applied to whatever expired in the money.

Whichever it is, the bot is frozen while its session is live: PATCH /bots/{id} and DELETE /bots/{id} keep returning 409 bot_has_active_session until the session is over, which is after it goes flat or settles — not merely after the stop.

A halt is the opposite trade-off and is deliberately unchanged: a malfunctioning bot (an order-guardrail trip, an exhausted error budget, a strategy calling ctx.halt) is taken out of the market immediately, its session ends halted, and open positions are valued at current marks in the final roll-up.

Restart a stopped session

# resume the strategy on the same session — its positions stay exactly as they are
curl -s -X POST "https://api.0dtespx.com/bots/$BOT/sessions/$SID/restart" -H "Authorization: $TOKEN"

POST …/sessions/{session_id}/restart returns 202 and undoes the stop: the strategy resumes on the same session, keeping the positions it was holding and the state it had built up. The resume is asynchronous, and the session's own timestamps are the acknowledgment — strategy_stopped_at, strategy_stopped_by and strategy_stop_applied_at all clear once the restart is durable, so the session reads exactly like one that was never stopped. The restart is recorded in the decision log as a strategy_restarted entry (source: "api"), and the resumed run emits a session_resumed event on the live_bot_events channel.

The strategy resumes; it does not start over. In particular it is never granted a second entry — its one-entry budget is reconstructed from the orders it has already filled, so a bot that entered and was stopped will manage that position, not open another one.

Because a stopped, flat session finalizes itself, the restartable window is exactly "the stop is acknowledged and the session is still holding":

Refusal (409) When
bot_not_stopped Nothing acknowledged to undo: never stopped, the stop hasn't taken effect yet (strategy_stop_applied_at still absent), or a previous restart already landed
bot_liquidated The session has been liquidated — permanent, even if the liquidation was cancelled
bot_session_not_restartable The session has finished (its terminal record is untouched), or a stop placed by the platform operator (strategy_stopped_by: "admin") cannot be restarted

A 404 means the bot/session pair doesn't resolve. To trade the same bot again after a session has finished, start a new session — restart is only ever about resuming the one that is still live.

Liquidate a stopped session

A stopped session that is still holding can be flattened in one call. It is the same liquidation the manual account uses — the same two methods, the same grouping guarantees, the same snapshot — placing its closing orders on the bot's own session, under the fee schedule that session was opened with:

curl -s -X POST "https://api.0dtespx.com/bots/$BOT/sessions/$SID/liquidations" \
  -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"method":"smart"}'

curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/liquidations/current" -H "Authorization: $TOKEN"
curl -s -X DELETE "https://api.0dtespx.com/bots/$BOT/sessions/$SID/liquidations/current" -H "Authorization: $TOKEN"

POST answers 202 with the initial snapshot and the workflow runs server-side; GET …/liquidations/current reports progress (and keeps the finished summary readable for about 15 minutes, 404 no_liquidation otherwise); DELETE …/liquidations/current stops it and only answers once its working orders are cancelled. Cancelling the liquidation leaves the session stopped and still holding whatever is left — it does not restart the strategy.

Only a stopped session can be liquidated. The bot has to be provably out of the market first, so the call is admitted once strategy_stop_applied_at is present and the session is still live; before that, and once the session is no longer live, it returns 409 bot_not_stopped. The rest of the admission matches the accounts surface: 409 no_open_positions on a flat book, 409 liquidation_in_progress when one is already running, 400 on a method outside smart / aggressive.

Liquidating permanently blocks restart. The POST stamps a read-only liquidated_at on the session before it places anything, and that marker is never cleared. From then on restart returns 409 bot_liquidated for the rest of the day — asking to close the book is treated as a decision, not a step you can walk back.

What ends the session is the flat book, not the marker:

  • The liquidation completes. The closing fills land, the book goes flat, and the session finalizes itself as stopped — balance carried, exactly as any flat session ends.
  • You cancel it with DELETE …/liquidations/current while positions are still open. The session stays live and holding: it is not ended and the strategy is not resumed. Restart is still refused, so the remaining exits are another liquidation, or the 4:00 PM ET close settling the day as completed.

liquidated_at therefore records that you chose to close the book, not that the book is closed; read the liquidation snapshot for progress and the session's status for whether the day is over.

Read a session's live data

curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID"                  -H "Authorization: $TOKEN"   # the session
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/orders"           -H "Authorization: $TOKEN"   # orders
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/positions"        -H "Authorization: $TOKEN"   # positions vs the latest tick
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/transactions"     -H "Authorization: $TOKEN"   # the session's whole transaction log
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/financials"       -H "Authorization: $TOKEN"   # the session hydrated with financials
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/history"          -H "Authorization: $TOKEN"   # per-second P&L curve series
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/decision-log"     -H "Authorization: $TOKEN"   # decision log (newest 500): ctx.log() + lifecycle entries
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/decision-log?full=true" -H "Authorization: $TOKEN"   # …the complete log instead

orders, positions, and transactions return the same shapes as the live-trading endpoints; transactions returns the session's whole ledger, with no time cutoff (add ?at= to cut it off at a moment — see Scrub a settled day). financials returns the session object hydrated with its financials at its current clock time — the same shape as an account session (GET /accounts/{id}/sessions/{sid}), except its status carries the full bot lifecycle value (any of scheduled, registering, waiting_for_data, running, settling, settled, completed, halted, stopped, failed, abandoned — this endpoint is polled in every state) rather than a manual open / settled / abandoned, and its starting_capital mirrors the opening balance. Note that its settled_at is the session's finalization timestamp — present for any finalized session (completed, halted, stopped, failed, abandoned), not only a natural settlement — so unlike the manual AccountSession.settled_at it does not imply a settled close. history returns the per-second financial snapshots for the equity / P&L curve (the same shape and ?interval= down-sampling as GET /accounts/{id}/sessions/{sid}/history). decision-log returns the session's decision log in ascending emission order (oldest first): the strategy's own ctx.log() entries (source: "strategy") interleaved with platform entries (source: "runner") and entries recorded for a request you made (source: "api" — a strategy_restarted entry per restart) — a session_started entry when the session opens (its payload's started_by is "user" for a manual start or "auto" for an auto-start), a session_ended entry when it ends (payload carries the terminal statuscompleted for a natural finish, stopped for a flat book or a stop, halted / failed otherwise — plus the halt_reason when there is one), and the order timeline: re-pricing activity (order_repriced — whose payload carries legs_changed, true when that step also re-selected the entry's legs — and reprice_failed), the two pricing rows order_start_priced (class, start, tier — the price an order went in at and how wide the spread was) and order_repriced, which both also report the market their price was computed from: quotes_at, an object mapping each leg's option symbol to the timestamp of the quote used for that leg (RFC 3339, UTC), plus net_bid and net_ask, the order's net bid and ask at that moment as decimal strings — enough to re-derive any price the platform set, entry_window_closed (the entry window ended with an order still unfilled; the payload carries the window and the cancelled order ids), order_filled (order_id, class, fill_price, price_effect), order_canceled, and order_rejected (with the rejection_reason). order_rejected also covers a placement the exchange refused outright — not enough buying power, an invalid structure, a temporary refusal: those entries carry order_created: false, meaning the exchange never accepted the placement — the order does exist under /orders, reading status: "rejected", but it never reached the book — plus an optional message with the rejection detail. When the refused placement was the session's entry, the entry-recovery pair follows it: an entry_retried entry (order_id, attempt, max_attempts, reason) per re-submission, and at most one entry_retry_abandoned entry (attempts, reason) when the entry could not be recovered at all. Cancels that are only the mechanics of a re-price or of the end-of-window sweep are not logged twice — the order_repriced / entry_window_closed entry is the record of those. A plain call answers with the newest 500 entries, which is what a live view wants; add ?full=true to read the session's complete log in one request.

A filled bot order speaks the same vocabulary from the other side: its fill_quotes uses those same quotes_at / net_bid / net_ask names for the market quotes at the time of the fill, extended with a per-leg quotes map of bid and ask. The pricing entries above report the market a price was set from; fill_quotes reports the market the order filled in.

A bot session's P&L and roll-up fields (final_nlv, realized_pl, total_pl, max_intraday_dd, …) are absent (omitted from the JSON) until the session finalizes.

Don't poll — subscribe. The WebSocket live_bot_events channel pushes the same lifecycle in real time for all of your bots at once, with no per-key scoping:

{ "action": "subscribe", "channel": "live_bot_events" }

Every envelope carries data.account_id (the bot id) and data.session_id, so one subscription feeds a whole dashboard. data.type covers the full lifecycle — session_started, registering, waiting_for_data, tick_progress (a ~10-second NLV / open-positions / open-orders heartbeat), decision_logged (the decision-log entries above, streamed as they are written), signal, session_resumed, missed_ticks, halted, session_completed, and error. The outbox is durable, so a reconnect that passes last_event_id replays the exact gap. Without a cursor it replays the newest 500 events — a short overlap with what you already loaded over REST, not your whole history, so de-duplicate on the envelope's id (and on data.log_id for a decision_logged event you may also hold from the decision-log endpoint above). "last_event_id": 1 asks for the full backfill — every row above id 1, so in practice everything retained bar a row with that exact id. Retention is per type: tick_progress heartbeats are kept 7 days, every other type indefinitely — a cursor older than that replays what remains. What goes with an expired heartbeat is its point-in-time tick_index / open_positions / open_orders, which nothing else stores; the values you act on don't, since the current ones are on the reads above and the P&L curve is on history.

Treat the socket as live traffic plus that overlap — but not everything on it has a REST twin. decision_logged lands in the decision-log endpoint above and the session_started / session_completed pair is reflected in the session record, while signal, registering, waiting_for_data, session_resumed, missed_ticks, halted and error are published only here (error carries the strategy traceback, which no endpoint returns). For those, the overlap is your whole safety margin: keep a last_event_id cursor across reconnects, or pass "last_event_id": 1 to pull them back. They are retained indefinitely, so they will still be waiting. Order and financials updates for the session still arrive on session_events, exactly as for a manual live session. Full contract: WebSocket → live_bot_events.

Scrub a settled day with at

Once a session is terminal you can replay it. positions and transactions accept an at cursor (RFC 3339, UTC) that reconstructs the day at an arbitrary moment without moving the stored clock:

curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/positions?at=2026-07-17T18:30:00Z"    -H "Authorization: $TOKEN"
curl -s "https://api.0dtespx.com/bots/$BOT/sessions/$SID/transactions?at=2026-07-17T18:30:00Z" -H "Authorization: $TOKEN"
  • Default read time — positions. Absent at, the snapshot is taken at the session's terminal clock: a settled natural close (completed, or the transient settled the exchange stamps before the runner finalizes it to completed) reads at its close with its end-of-day settlement applied (expiring/exercising positions appear); a halted / stopped / failed / abandoned session reads at the final market clock it stopped on.
  • Default read time — transactions. There is none: absent at the whole ledger comes back, which for a terminal session is the same tape the terminal clock would have shown (a completed day's settlement transactions included). Use at when you want the ledger as it stood mid-day.
  • Only a settled natural close settles. A session that was never exchange-settled (halted / stopped / failed / abandoned) applies no synthetic settlement on replay — you see the book exactly as it stood when the session ended. at is clamped to the terminal clock (a later cursor reads the terminal state; there is no data past it).
  • A malformed at returns 400.

The decision-log accepts the same at (RFC 3339) as a window: it returns the newest 500 entries with datetime <= at (ascending emission order), so a replay scrub reads a cursor-correct slice. Pass ?full=true instead — on its own or with at — to lift that 500-entry window and read the session's complete log; a finished session is best read once that way. A malformed at returns 400.

Next: see Accounts & sessions for the shared account model and Live trading for the session-scoped order, position, and transaction shapes a bot session reuses.