# LAB: LAB API

> LAB from a script, with a lab key: the same conveyor the terminal drives, spending the same credits. Developing an idea is free; a backtest or an optimize pass is one credit, refunded on failure.

Source: https://firmtape.com/docs/developers/lab

LAB from a script, with a `lab` key: the same conveyor the terminal drives, spending the same credits. Developing an idea is free; a backtest or an optimize pass is one credit, refunded on failure.

**This product serves:** [LAB: LAB from a script](https://firmtape.com/docs/lab#lab-api), [LAB: The LAB desk and the backtest](https://firmtape.com/docs/lab#backtester), [LAB: Quant optimize](https://firmtape.com/docs/lab#quant-optimize), [LAB: Credits and tokens](https://firmtape.com/docs/lab#credits-and-tokens). **Plan:** [LAB](https://firmtape.com/features/lab). LAB credits: developing an idea is free, a backtest or an optimize pass is one credit, the desk is free once an idea is tested. LIVE includes 5 credits a month.

**The conveyor.** An idea walks one road in order: chat (free) compiles plain words into a rule; `POST /api/lab/run` with `kind: "backtest"` (1 credit) moves it from rule to tested; `kind: "quant"` (1 credit) from tested to ready; `POST /api/lab/idea` with `action: "desk"` (free) parks it. A step out of order is refused.

**One idea at a time.** A run on a second idea while one is mid-test answers 409 with the `holder`; desk or drop the first.

**The verdict.** `interesting` is true only when the rule clears costs out of sample, beats its core-only baseline there, and does it by more than its own error bar (`net_rr_t` of 2 or more, the error clustered by session). The optimize returns every variant, never a picked winner, with the overfitting it cost (`pbo`, `dsr`).

```python
import os, requests

H = {"Authorization": f"Bearer {os.environ['FIRMTAPE_LAB_KEY']}"}
BASE = "https://firmtape.com"

r = requests.post(f"{BASE}/api/lab/chat", headers=H, json={"message": "fade a +3 sigma stretch above vwap on top-decile volume; 15 minute horizon"}).json()
if r["kind"] != "spec":
    raise SystemExit(r["say"])          # LAB asked a question
idea = r["idea"]["id"]
bt = requests.post(f"{BASE}/api/lab/run", headers=H, json={"id": idea, "kind": "backtest"}).json()
qo = requests.post(f"{BASE}/api/lab/run", headers=H, json={"id": idea, "kind": "quant"}).json()
requests.post(f"{BASE}/api/lab/idea", headers=H, json={"id": idea, "action": "desk"})
```

## Endpoints

| Request | What it gives | Plan | Key scope |
| --- | --- | --- | --- |
| `POST /api/lab/chat` | [Develop or amend an idea](https://firmtape.com/docs/developers/lab/chat) | LAB | lab |
| `POST /api/lab/run` | [Backtest or optimize an idea](https://firmtape.com/docs/developers/lab/run) | LAB | lab |
| `GET /api/lab/state` | [Ideas, conveyor and balance](https://firmtape.com/docs/developers/lab/state) | LAB | lab |
| `GET /api/lab/thread` | [An idea's chat thread](https://firmtape.com/docs/developers/lab/thread) | LAB | lab |
| `POST /api/lab/idea` | [Desk actions on an idea](https://firmtape.com/docs/developers/lab/idea) | LAB | lab |
| `GET /api/lab/vocabulary` | [The LAB vocabulary](https://firmtape.com/docs/developers/lab/vocabulary) | Free | none |
| `POST /api/struct/run` | [Test an options structure over the archive](https://firmtape.com/docs/developers/lab/options-run) | LAB | lab |
| `GET /api/struct/job` | [Poll an options run](https://firmtape.com/docs/developers/lab/options-job) | LAB | lab |

## More

- [LAB over MCP](https://firmtape.com/docs/developers/lab/mcp): The MCP server's `lab_` tools and `options_strategy_test` drive the same conveyor from an assistant, with a `lab` key or after signing in through OAuth.

## Limits

| What | Limit | Notes |
| --- | --- | --- |
| LAB API | Credits: a backtest or an optimize pass is 1 credit, an OPTIONS archive run 10 | Developing an idea is free but needs a positive balance. One idea holds the conveyor at a time. A failed job refunds its credit. |
