> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Strategies & DSL

> Strategy CRUD, strategy legs, and DSL validation endpoints

This page documents the low-level strategy primitives: creating and editing
strategies, attaching legs, and checking DSL expressions. Reach for it when the
dedicated [Conditional Orders](/krisis/conditional-orders) endpoints don't express the
condition you want — those endpoints build strategies for you under the hood.

A **strategy** is the core Krisis object: a named rule attached to a market
that the engine evaluates on every tick. A strategy carries an optional
top-level DSL `expression` and/or a set of **legs** — each leg is one
condition paired with one order to submit when that condition is true.

All endpoints require an `Authorization: Bearer <jwt>` header except
`dsl/validate` and `dsl/evaluate`, which are public.

## Strategy lifecycle

1. **Draft the condition.** Check it with
   [`POST /api/v1/dsl/validate`](#validate-an-expression), and dry-run it
   against the live book with
   [`POST /api/v1/dsl/evaluate`](#evaluate-an-expression). Neither needs auth,
   and neither creates anything.
2. **Create the strategy** with [`POST /api/v1/strategies`](#create-strategy),
   supplying an `expression`, `legs`, or both. A created strategy arms — the
   engine picks it up and evaluates it without a further call. Whether it is
   still armed is `is_active` on the strategy row.
3. **The engine evaluates it every tick.** When a leg's condition is truthy and
   no order is already in flight for the strategy, the leg's order is
   submitted. `pending_order_id` is the fire-once guard.
4. **The attempt is logged either way** — as an execution row with
   `action_taken: true` and an `order_id`, or `action_taken: false` and a
   `skip_reason`. See
   [List executions](/krisis/positions-pnl#list-executions).
5. **Disarm, edit, or delete.** `PUT /api/v1/strategies/{id}` with
   `is_active: false` disarms without losing the row;
   `DELETE /api/v1/strategies/{id}` hard-deletes it and its legs. A strategy
   with `expires_at` auto-deactivates at that time.

A strategy with `trigger_order_id` set stays dormant until that entry order
fires — that is how [OTO](/krisis/conditional-orders#oto) legs wait for their entry.

## List strategies

```
GET /api/v1/strategies
```

Returns every strategy owned by the caller.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `active` | boolean | No | all | Query parameter. `true` returns only armed strategies, `false` only disarmed ones. Omit for all of them |

### Example

```bash theme={null}
curl https://krisis.kairos.trade/api/v1/strategies \
  -H "Authorization: Bearer $JWT"
```

### Response

Array of strategy objects.

| Field | Type | Description |
| - | - | - |
| `id` | uuid | Strategy id |
| `name` | string | Display name |
| `description` | string \| null | Always `null` (reserved) |
| `expression` | string \| null | Top-level DSL expression; `null` for leg-only strategies |
| `market_id` | string | Market / contract identifier |
| `exchange_id` | string | `"kalshi"` or `"polymarket"` |
| `is_active` | boolean | Whether the engine is evaluating it |
| `fund_id` | uuid \| null | Linked spending fund |
| `expires_at` | string \| null | ISO 8601; strategy auto-deactivates after this |
| `pending_order_id` | uuid \| null | In-flight order — fire-once guard |
| `oco_group_id` | uuid \| null | Bracket group linking sibling legs |
| `trigger_order_id` | uuid \| null | When set, the strategy stays dormant until that entry order fires |
| `conditional_kind` | string \| null | Which [conditional-order kind](/krisis/conditional-orders) the engine armed this row as, or `null` when you authored it directly. Always present |
| `created_at` | string | ISO 8601 |
| `updated_at` | string | ISO 8601 |
| `legs` | array | Strategy legs (see [List legs](#list-legs)) |

## Create strategy

```
POST /api/v1/strategies
```

Creates a strategy and, optionally, its legs in the same call. Returns
`201 Created` with the strategy object.

### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | string | Yes | — | Display name |
| `market_id` | string | Yes | — | Market / contract identifier |
| `expression` | string | \* | — | DSL expression, compiled on create |
| `legs` | array | \* | — | Legs to create atomically with the strategy (see [Create leg](#create-leg)) |
| `description` | string | No | — | Reserved; **not stored** |
| `exchange_id` | string | No | `"kalshi"` | `"kalshi"`, `"polymarket"`, or `"predictfun"` |
| `action_config` | object | No | — | Free-form JSON action metadata, passed to the engine untouched — see [action\_config](#action_config) |
| `fund_id` | uuid | No | — | Spending fund to link |
| `expires_at` | string | No | — | ISO 8601 auto-deactivation time |

\* You must supply at least one of `expression` or `legs`.

### Example

```bash theme={null}
curl -X POST https://krisis.kairos.trade/api/v1/strategies \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "BTC dip buy",
    "market_id": "KXBTCD-25",
    "exchange_id": "kalshi",
    "expression": "ask > 0 and ask <= 0.35"
  }'
```

### Validation

| Rule | Failure |
| - | - |
| At least one of `expression` / `legs` is required | `400` |
| `exchange_id` must be one of the supported venues | `400` |
| A DSL `expression` must compile | `400` |
| The account's strategy-count [tier limit](/krisis/overview#tier-limits) must not be exceeded | `400` |
| `expression` requires the `allow_expressions` tier flag | `400` |

## action\_config

`action_config` is stored as-is and read by the engine when a leg fires, so
it is where per-strategy execution detail lives that the strategy row itself
has no column for. Keys the engine acts on:

| Key | Type | Description |
| - | - | - |
| `outcome` | string | Outcome label to trade (e.g. `"Yes"`) |
| `token_id` | string | CLOB token id of that outcome — Krisis prices the strategy against it and keys the strategy's position on it, so it must be the token belonging to `outcome` |
| `time_in_force` | string | Time in force for the submitted order, e.g. `"FAK"` |
| `max_slippage_cents` | integer | Shared market-order slippage cap in cents (1–99). Used for entries and, when no exit override is set, exits |
| `exit_max_slippage_cents` | integer | Independent cap for market sells in cents (1–99). Omit or set to null to retain the shared cap; does not change limit-order prices |
| `total_capital` | number | Capital allocation for the strategy. Read by the `strategy_capital_remaining` / `strategy_spent` [DSL variables](#dsl-variables); absent means uncapped |
| `exit` | object | Exit leg krisis generates for you. `{"kind":"hold"}` (default) leaves the position to settlement; `{"kind":"limit","price":0.99}` rests a limit sell at that price; `{"kind":"market","when":"<DSL>"}` sells at market once the expression is true. `limit` and `market` also take `fraction` (`0 < f <= 1`) — the share of the position to sell, defaulting to all of it; `hold` refuses it. Do not also submit a leg labelled `exit` — the two together are a `400` |
| `max_drawdown` | number | Positive dollar amount. Once the strategy's mark-to-market loss (against its peak) reaches this, the engine deactivates it with reason `max_drawdown`. Requires `total_capital` > 0. Frozen at launch — an update that changes it is a `400` |
| `max_drawdown_pct` | number | Percent of `total_capital`, `0 < pct <= 100`. Same stop as `max_drawdown`, expressed as a share of the allocation instead of a fixed dollar amount. Mutually exclusive with `max_drawdown` (sending both is a `400`); requires `total_capital` > 0. The *choice* of key is frozen at launch, same as `max_drawdown` — but unlike the dollar form, editing `total_capital` on a live strategy rescales this stop instead of requiring a recreate, so prefer it when the allocation might change later. Use `max_drawdown` when you want a fixed dollar stop that does not move if the allocation does |
| `reference_feeds` | object | `{alias: {provider, market_id[, token_id \| outcome]}}` — external price feeds the engine resolves and exposes as `ref_<alias>_*` DSL variables. A recurring template can use `{"spot": {"provider":"oracle","market_id":"settlement"}}` to have the controller resolve the venue's settlement oracle per window; a one-off strategy names a concrete oracle symbol instead (e.g. `"btc-usd-polymarket-twap60"`, `"btc-usd-kalshi-cfb"`, `"btc-usd"`) |
| `reference_values` | object | `{"strike": <number>}` — the window's price to beat; exposed as `ref_strike`. `0` on a recurring template, where the controller fills the real one in per window |

The automation drawer accepts decimal whole cents. Clear its Exit slippage field to restore the shared entry cap. The review summary shows the configured exit cap; incomplete values block launch.

*Worked example:* "Stop the strategy if it gives back 10% of its
allocation" is `total_capital: 200, max_drawdown_pct: 10` — the engine
deactivates the strategy once `strategy_drawdown_pct` (its loss from the
best mark-to-market it has reached) hits 10% of the $200 allocation. In the
builder this is the "%" side of the Max drawdown toggle, which is the
default; switch it to "$" to set a fixed dollar amount instead
(`max_drawdown` — needs the same `total_capital` allocation either way).

### Exit policy

`action_config.exit` is how a strategy leaves a position — say it once here
instead of hand-writing a sell leg. It has three shapes, and the builder
offers them as three buttons under "When to exit":

| Builder button | `exit` | What happens |
| - | - | - |
| Hold to resolution | `{"kind":"hold"}` (default) | No sell leg at all — the position rides to settlement |
| Limit sell at price | `{"kind":"limit","price":0.99}` | Rests a GTC limit sell at that price (`0.01`–`0.99`) as soon as there is inventory to sell |
| Sell when… | `{"kind":"market","when":"<DSL>"}` | Sells at market the moment the condition is true |

*Worked example:* to take profit once the market trades above 90¢, choose
"Sell when…" and write `bid >= 0.90` — the builder sends
`{"kind":"market","when":"bid >= 0.90"}`.

#### Stop loss and take profit

"Sell when…" carries three quick-picks above the condition blocks, so the two
exits most strategies want are one click rather than an expression:

| Quick-pick | Blocks it fills in |
| - | - |
| Stop loss −10% | `bid` at most 10% below `entry_price` |
| Take profit +20% | `bid` at least 20% above `entry_price` |
| Stop loss + take profit | both of the above, joined with **any of** |

Both are relative to `entry_price`, never to a fixed level — a stop written
as a hardcoded 45¢ stops meaning anything once the entry fills somewhere
else. The percentages are only starting points: they are ordinary block rows
underneath, so edit them.

**"All of" vs "any of".** Blocks normally join with `and` — every one has to
be true. A stop loss and a take profit joined that way would need the price
to be below *and* above the entry at once, so it would never sell. The exit
block list therefore carries an **All of / Any of** switch as soon as it holds
two blocks, and the quick-picks set it to "any of" for you. "Any of" compiles
to `or`, with the guards kept outside the group:

```
(bid > 0) and (strategy_position_qty > 0) and ((bid <= entry_price * 0.9) or (bid >= entry_price * 1.2))
```

`strategy_position_qty > 0` is there because `entry_price` reads `0` before
the first fill, which would make a take profit trivially true the moment the
strategy armed.

#### Selling part of a position

`fraction` on a `limit` or `market` exit sells that share of the position
instead of all of it — `{"kind":"market","when":"bid >= 0.90","fraction":0.5}`
sells half and leaves the rest running. It is a number in `0 < f <= 1`;
absent means the whole position, `hold` rejects it outright. The builder
offers **All / 50% / 25% / custom** under "Sell how much", and sends no
`fraction` at all for "All".

Whichever shape you pick, do not also submit a leg labelled `exit` yourself —
Krisis reserves that label for the leg it generates, and the two together are
a `400`.

**This choice is locked once the strategy launches.** Switching from Hold to
Limit, or editing the limit price or sell condition, requires stopping the
strategy and creating a new one — see
[What's locked after launch](#whats-locked-after-launch).

### Oracle conditions

The oracle is not a separate setting. A strategy declares its feed by *using*
it: the moment a condition reads `ref_spot_price` or `ref_strike`, the caller
sends the matching `reference_feeds` / `reference_values` alongside it. The
builder does exactly that — the Oracle fields sit in the same condition picker
as `ask` and `bid`, and picking one adds the declaration to `action_config`.

The two shapes are:

| Run | `reference_feeds.spot.market_id` | `reference_values.strike` |
| - | - | - |
| Recurring (a series) | `"settlement"` — the controller resolves the venue's settlement oracle per window | `0`, filled in per window |
| One-off (this market) | The concrete symbol, e.g. `"btc-usd-polymarket-twap60"` | The price to beat for this window |

**Compare relatively, not absolutely.** The price to beat moves every window,
so a hardcoded level stops meaning anything after the first rollover. Express
the edge as an offset instead:

```
ref_spot_price >= ref_strike * 1.005     # 0.5% above the price to beat
ref_spot_price <= ref_strike * 0.99      # 1% below it
```

That is what the builder's `%` value mode produces; `#` still takes an
absolute number when you want one, and `field` compares two variables
directly (`ref_spot_price >= ref_strike`).

**Gate entry on the size of the move, either direction.** "Only enter if the
oracle is more than 0.5% away from the price to beat" doesn't care which way
it moved — that's `abs()`, not a signed offset:

```
(abs(ref_spot_price - ref_strike) / ref_strike * 100) >= 0.5
```

The builder's `Oracle distance from price to beat (%)` field compiles to
exactly this. `Oracle move vs price to beat (%)` is the signed version —
positive above the price to beat, negative below — for when the direction
matters as well as the distance.

**Where the builder gets the symbol and the price to beat.** For a one-off
strategy the builder first tries to identify the market's series (asset +
interval); if that succeeds, the oracle symbol follows the same
provider-specific mapping the engine uses (`buildOracleSymbol`), e.g. a
Polymarket 5m/15m market becomes `<asset>-usd-polymarket-twap60`. If no
series is found — a next-window contract, or a market the discovery step
doesn't recognise — the builder falls back to the terminal's own contract
classifier, and the "Oracle:" line under the condition says
"(from this market's contract)" so you can tell which source it used. Either
way it also prefills **Price to beat** from the venue's published number for
that window; a **Reset to current** button reappears if you edit the field
by hand, so you can snap back to the live value.

**Kalshi oracle conditions only exist for BTC and ETH.** Kalshi's CF
Benchmarks feed only covers those two assets — an oracle block on any other
Kalshi crypto market has no symbol to declare, so the builder disables the
Oracle group and shows "Available on crypto up/down markets" instead.

## What's locked after launch

A few things are asked once, at creation, because changing them mid-run
would either invalidate the risk control they protect or leave a stale
generated leg behind:

* **Exit policy** (`action_config.exit`) — see [Exit policy](#exit-policy).
  Update requests that change it are rejected with a `400`.
* **`max_drawdown` / `max_drawdown_pct`** — both which key you used and its
  value. `total_capital` itself stays editable, and a percent-based stop
  rescales automatically when it changes; a dollar-based stop does not.
* **A recurring run's per-window price to beat** — each window is rebuilt
  fresh from the run's template, so editing an already-armed window's strike
  has no lasting effect. Change the template and start a new run instead
  (the builder's "Edit as new run" does this for you).

Everything else — the strategy's `name`, `expires_at`, `total_capital`,
`max_slippage_cents`, a one-off strategy's price to beat, and any
non-generated leg's `condition_expr` / quantity — can be changed on a live
strategy with [`PUT /api/v1/strategies/{id}`](#update-strategy) and
[`PUT /api/v1/strategies/{id}/legs/{leg_id}`](#update-leg).

## Get strategy

```
GET /api/v1/strategies/{id}
```

Returns one strategy object, or `404` if it does not exist or is not yours.

## Update strategy

```
PUT /api/v1/strategies/{id}
```

Returns the updated strategy object.

### Request

All fields are optional — only those supplied are changed.

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `name` | string | No | unchanged | Rename |
| `description` | string | No | unchanged | Reserved |
| `expression` | string | No | unchanged | Replace the DSL expression (recompiled) |
| `is_active` | boolean | No | unchanged | Arm (`true`) or disarm (`false`) |
| `exchange_id` | string | No | unchanged | `"kalshi"` or `"polymarket"` |
| `action_config` | object | No | unchanged | Replace action metadata |
| `fund_id` | uuid | No | unchanged | Re-link the strategy to a different fund |
| `expires_at` | string | No | unchanged | New ISO 8601 expiry |

<Note>
  **Gotcha: you cannot clear a field back to empty.** Sending a field as `null`
  is treated the same as omitting it, so an omitted *and* an explicitly-null
  field are both left untouched. Once `fund_id` or `expires_at` is set, this
  endpoint cannot unset it.
</Note>

## Delete strategy

```
DELETE /api/v1/strategies/{id}
```

Hard-deletes the strategy and its legs. Returns `204 No Content`.

<Note>
  **Gotcha: prefer the conditional-order cancel for conditional orders.**
  `DELETE /api/v1/conditional-orders/{id}` deactivates the row (keeping
  history) and cancels any resting venue order; this endpoint destroys the row
  and does neither. See [Conditional Orders](/krisis/conditional-orders).
</Note>

## The Details drawer (web app)

Clicking a running strategy opens its Details drawer — the same read/write
split the API gives you, in one panel. The top half is
[what's locked after launch](#whats-locked-after-launch), shown with a lock
icon and "Fixed at launch — relaunch to change". Below that is everything
`PUT /api/v1/strategies/{id}` and `PUT /api/v1/strategies/{id}/legs/{leg_id}`
will actually accept on a live strategy: each entry leg's condition and
size, `name`, `max_slippage_cents`, `total_capital`, `expires_at`, and — for
a one-off strategy only — the oracle price to beat.

**Save order.** The drawer saves legs first, one at a time, and stops at the
first one the engine rejects — pushing the strategy-level fields on top of a
half-applied set of leg changes would leave a strategy nobody actually asked
for. It reports how many legs saved before a failure. Only once every leg
change succeeds does it send the strategy-level update.

**Pause, resume, stop.** "Pause" and "Resume" toggle `is_active` — the
strategy row and its legs are untouched, so resuming continues exactly where
it left off. "Stop" hard-deletes the strategy and its legs
(`DELETE /api/v1/strategies/{id}`); the drawer asks you to confirm first,
since this is not reversible the way pausing is.

## Continuous runs (recurring)

"Keep running on this series" (the `repeatSeries` toggle in the builder) asks
the automation controller to relaunch the same template on every new window
of a series, rather than arming one strategy on one market. It only unlocks
once the builder has identified a current series for the selected market —
a market with no discoverable series can only run as a one-off.

The Continuous runs list shows each run's state:

* **Waiting: `<reason>`** — shown under a running run when it isn't between
  windows for an ordinary reason (e.g. still waiting on the current window
  to close). This is normal, not an error.
* **Retrying ×N** — appears once a run has hit 3 consecutive backend
  failures in a row, retried with increasing delay each time. It clears the
  next time a check succeeds.
* **Next check `<time>`** — when the controller will next look for a window
  to arm.
* **View configuration** — shows the most recently armed window's actual
  `action_config` and legs (the controller doesn't serve the template
  itself, so this is the receipt of what the template produced).
* **Edit as new run** — opens the builder pre-filled from the current armed
  window, for changing the template. It does not touch the run in progress;
  stop the old run yourself once the new one is armed.

A run's per-window strategies are children, rebuilt from the template each
window — editing one directly in the Details drawer has no lasting effect
past that window, which is why the run view offers "Edit as new run" instead
of an in-place edit.

## Strategy legs

A **leg** binds one DSL condition to one order. When the condition evaluates
truthy and no order is already in flight, the engine submits the leg's order.

### List legs

```
GET /api/v1/strategies/{id}/legs
```

#### Response

Array of leg objects.

| Field | Type | Description |
| - | - | - |
| `id` | uuid | Leg id |
| `strategy_id` | uuid | Parent strategy |
| `label` | string | Display label |
| `position` | integer | Ordering within the strategy |
| `condition_expr` | string | DSL condition |
| `side` | string | `"buy"` or `"sell"` |
| `order_type` | string | `"market"` or `"limit"` |
| `quantity_value` | number \| null | Static quantity |
| `quantity_expr` | string \| null | DSL expression for a dynamic quantity |
| `price_value` | number \| null | Static limit price |
| `price_expr` | string \| null | DSL expression for a dynamic limit price |
| `is_active` | boolean | Whether the leg is evaluated |
| `created_at` | string | ISO 8601 |
| `updated_at` | string | ISO 8601 |

### Create leg

```
POST /api/v1/strategies/{id}/legs
```

Returns `201 Created` with the leg object.

#### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `label` | string | Yes | — | Display label |
| `condition_expr` | string | Yes | — | DSL condition, compiled on create |
| `side` | string | Yes | — | `"buy"` or `"sell"` |
| `position` | integer | No | — | Ordering within the strategy |
| `order_type` | string | No | `"limit"` | `"limit"` or `"market"` |
| `quantity_value` | number | No | — | Static quantity |
| `quantity_expr` | string | No | — | DSL expression for quantity |
| `price_value` | number | No | — | Static limit price |
| `price_expr` | string | No | — | DSL expression for limit price |
| `is_active` | boolean | No | active | Whether the leg is evaluated |

**Validation:** `side` must be `"buy"` / `"sell"`; `order_type` must be
`"market"` / `"limit"`; every DSL expression must compile.

### Update leg

```
PUT /api/v1/strategies/{id}/legs/{leg_id}
```

Takes the same fields as [Create leg](#create-leg), all optional — only
fields present in the request body are updated. As with updating a strategy,
sending a field as `null` is treated the same as omitting it.

### Delete leg

```
DELETE /api/v1/strategies/{id}/legs/{leg_id}
```

Returns `204 No Content`.

## DSL endpoints

The Krisis DSL is the small expression language used in `expression`,
`condition_expr`, `quantity_expr`, and `price_expr`. The two endpoints below
let you check and trial expressions without creating a strategy.

### DSL variables

The engine injects these into every evaluation:

| Variable | Unit | Meaning |
| - | - | - |
| `price` | price | Last / mark price — what the engine-generated conditional-order triggers compare against |
| `ask` | price | What you pay to buy right now |
| `bid` | price | What you receive when selling right now |
| `spread` | price | `ask - bid` |
| `book_stale_secs` | seconds | Seconds since the orderbook last updated |
| `seconds_since_trigger` | seconds | Time since this strategy last fired. Infinite before the first fill, so it never blocks the first one |
| `seconds_since_armed` | seconds | Time since the strategy was created — useful as a warm-up guard |
| `seconds_until_expiry` | seconds | Seconds until the strategy's `expires_at` — for a recurring run, until the contract rolls over. `+inf` when no expiry is set |
| `strategy_capital_remaining` | currency | `action_config.total_capital` minus what is deployed. Sells return it, so it recycles |
| `strategy_spent` | currency | Every unit of currency ever deployed. Never decreases — a hard turnover ceiling |
| `strategy_pnl` | currency | Marked to the current bid; negative while underwater |
| `strategy_peak_pnl` | currency | The best `strategy_pnl` this strategy has reached since it armed |
| `strategy_drawdown` | currency | `strategy_peak_pnl - strategy_pnl` — how far below the best mark-to-market the strategy currently sits, as an amount. What `action_config.max_drawdown` compares against |
| `strategy_drawdown_pct` | percent | `strategy_drawdown` as a percent of `total_capital` — `0` without an allocation. What `action_config.max_drawdown_pct` compares against |
| `strategy_position_qty` | shares | This strategy's own inventory, not the market-wide position |
| `entry_price` | price | Price the entry filled at. Only meaningful after a fill — this is what an [ATR bracket leg](/krisis/conditional-orders#atr-legs) anchors to |
| `ref_<alias>_price` / `_bid` / `_ask` / `_age_secs` | price / seconds | Quote for a feed declared in `action_config.reference_feeds`, keyed by its alias (e.g. `ref_spot_price`) |
| `ref_<name>` | — | A value supplied in `action_config.reference_values`, keyed by name (e.g. `ref_strike`) |

<Note>
  **Gotcha: `ref_*` variables read as `0` until the strategy declares the feed.**
  Reference them only alongside a matching `reference_feeds` /
  `reference_values` entry in `action_config` — see
  [Oracle conditions](#oracle-conditions). `POST /api/v1/dsl/validate` accepts
  the same `action_config`, and compiles against the names it declares, so
  validating a `ref_*` expression without one reports an unknown variable that
  the armed strategy would not have hit.
</Note>

> **Gotcha: an absent quote reads as `0`,** so `ask <= 0.35` is true on a dead
> book and your strategy fires into nothing. Always pair a price comparison
> with its guard — `ask > 0 and ask <= 0.35`.

<Note>
  **Gotcha: `seconds_until_expiry` only ever counts down on a recurring
  run.** A one-off strategy has no `expires_at`, so the variable reads `+inf`
  and a condition built on it arms and never fires. The builder blocks
  submit if you reference it without "Keep running on this series" turned
  on, and the error names the fix.
</Note>

> **Gotcha: the live engine has no candle series.** `atr(period, "timeframe")`
> is available, with `timeframe` one of `1s`, `1m`, `5m`, `15m`, `1h`, `4h`,
> `1d`. Beyond that, expressions referencing candle indicators, volume, or bar
> history are **rejected at compile time** rather than armed and left to never
> fire. Trial them against [Backtest](/krisis/market-data#backtest) instead, which
> replays candles you supply.

### Validate an expression

```
POST /api/v1/dsl/validate
```

Parses and compiles an expression. **Public — no authentication required.**

#### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `expression` | string | Yes | — | DSL source to parse and compile |
| `action_config` | object | No | — | The `action_config` the expression would ship with. Its `reference_feeds` / `reference_values` names are compiled in, so a legitimate `ref_*` reference validates |

#### Example

```bash theme={null}
curl -X POST https://krisis.kairos.trade/api/v1/dsl/validate \
  -H "Content-Type: application/json" \
  -d '{ "expression": "ask > 0 and ask <= 0.35 and seconds_since_trigger > 300" }'
```

#### Response

| Field | Type | Description |
| - | - | - |
| `valid` | boolean | Whether it parsed and compiled |
| `error` | string \| null | Compile error, when `valid` is `false` |
| `tokens` | array \| null | Lexed tokens (`token_type`, `value`) |
| `ast` | string \| null | Debug rendering of the parse tree |

<Note>
  **Gotcha: a rejected expression is still `200 OK`.** The status does not
  change — check `valid` and read `error`. A client that branches on HTTP
  status alone will treat every broken expression as valid.
</Note>

### Evaluate an expression

```
POST /api/v1/dsl/evaluate
```

Compiles the expression and runs it once against the **current live market
context** for a market. **Public — no authentication required.**

#### Request

| Name | Type | Required | Default | Description |
| - | - | - | - | - |
| `expression` | string | Yes | — | DSL source |
| `market_id` | string | Yes | — | Market to pull live data for |
| `exchange_id` | string | Yes | — | `"kalshi"` or `"polymarket"` |

#### Example

```bash theme={null}
curl -X POST https://krisis.kairos.trade/api/v1/dsl/evaluate \
  -H "Content-Type: application/json" \
  -d '{
    "expression": "price > 0.50",
    "market_id": "KXBTCD-25",
    "exchange_id": "kalshi"
  }'
```

#### Response

| Field | Type | Description |
| - | - | - |
| `result_value` | string \| null | The evaluated value |
| `result_type` | string \| null | Its type |
| `is_truthy` | boolean \| null | Whether the engine would treat it as a trigger |
| `market_snapshot` | object | `price`, `bid`, `ask`, `spread` used in the run |
| `evaluation_ms` | number | Evaluation time in milliseconds |
| `error` | string \| null | Set when evaluation failed |

### Navigating automation

The builder groups market selection, entry conditions, order settings, and exit risk into separate sections. Use the section shortcuts to jump between them, Review to check the full trade, or Expand all to see every section together. Browse strategy templates opens the full template library; closing it keeps your settings. All condition types, advanced expressions, simulation options, market-maker settings, and run limits remain available.

The automation list separates strategy details from actions. Active, History, and Presets switch between your running strategies, past activity, and saved configurations. Each strategy retains its detail, history, reuse, save, pause/resume, rename, and delete actions where applicable.

Setup returns to the market settings. Entry, Exit, and Review jump directly to those sections. Expand all is available beside the template browser, or above Conditions when using the expression editor. Strategy status and details appear before the management actions in the automation list.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.