# backtrade.dev — give your agent a paper broker

Backtrade replays historical minute bars and certified realtime news through
a causal paper broker. Each session resolves the feed that actually covers the
day you asked for and reports it back on the replay frame as `feed` — IEX where
it is complete, and a consolidated feed otherwise. **The IEX tape stops on
2026-05-03**, so anything later is served from the consolidated one; you do not
choose, and the frame always says which you got.

Use any model, prompt, strategy, or harness—Claude Code, Codex, your own loop,
or no LLM at all.

**You bring the strategy. We provide the tape and simulator.** We do not provide
signals or claim that a strategy will be profitable. See [Terms](https://backtrade.dev/terms).

## Fastest path: one URL, no download

Point any MCP client at the hosted endpoint and pass the key you already have.
Nothing to download, no checksum to verify, no Node runtime to install.

```json
{
  "mcpServers": {
    "backtrade": {
      "url": "https://api.backtrade.dev/mcp",
      "headers": { "Authorization": "Bearer <key-id>:<secret>" }
    }
  }
}
```

The `APCA-API-KEY-ID` / `APCA-API-SECRET-KEY` pair every other route accepts
works here too, if your client sends header pairs more easily than a bearer.
It is the same credential either way — one key, one place to revoke it.

Two things worth knowing, because they are enforced rather than advisory:

- **The secret is checked on every request, not once at connect.** An MCP
  session id is a routing value that travels in headers and logs; it is not a
  password, and it is not treated as one here.
- **A session belongs to the credential that opened it.** Presenting another
  valid key with someone else's session id is refused, not merged.

### Get the key first, with one curl

This is the whole of the hosted path — there is nothing to download at any point.
`signup_trial` does **not** work here and will tell you so: it manages a
credential file on the machine it runs on, and on the hosted endpoint that
machine is our server, not yours. You would never receive the secret.

```bash
SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
VERIFIER=$(printf '%s' "$SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')

curl -sS -X POST https://api.backtrade.dev/v1/agent-trials \
  -H 'content-type: application/json' \
  -H "idempotency-key: $(head -c 24 /dev/urandom | basenc --base64url | tr -d '=')" \
  -d "{\"secretVerifier\":\"$VERIFIER\",\"label\":\"my-agent\"}"
```

**Only the SHA-256 of your secret is sent.** The secret itself never leaves your
machine and cannot be recovered from us — keep `$SECRET`, take `keyId` from the
response, and the bearer above is `<keyId>:<SECRET>`.

Tier-0 trials last 30 days and permit 100 sessions on up to 5 symbols, blind. The
signup reference further down covers the error codes and the per-day caps.

A trial credential is not the same thing as a free account, and the free account
is larger: signing up at [backtrade.dev](/login?new=1) raises the limit to 600
sessions a month and does not expire. You do not need one to start — the trial
above needs no sign-up at all — but if you run out of trial sessions, that is
where they come back.

## Alternative: install the 17-tool MCP adapter locally

The adapter runs locally over stdio, talks only to `api.backtrade.dev`, and keeps
its client-generated secret in `~/.config/backtrade/agent-credential.json` with
mode `0600`. The secret is never returned to the model, including when an API
response or WebSocket diagnostic tries to echo it.

**If that directory already exists and is group- or world-writable, signup
refuses** rather than writing a secret somewhere other people can read. It tells
you the fix:

```
chmod 700 ~/.config/backtrade
```

The adapter creates the directory itself at mode `0700` when it is absent,
whatever your umask. You only see the refusal if an older directory is already
there — a umask of `002`, the Ubuntu and Debian default, produces `0775`. The
refusal is deliberate: a credential is not worth leaking to save you one command.


> **Node 18 or newer.** The adapter is a single ES module, so it is named
> `.mjs` — that extension is what makes it load on every Node version rather
> than only on 22.7+. If you have an older config pointing at `backtrade-mcp.js`
> it still works, but only on Node 20.19+ or 22.7+.

```bash
mkdir -p "$HOME/.local/share/backtrade"
curl -fsSLo "$HOME/.local/share/backtrade/backtrade-mcp.mjs" \
  https://backtrade.dev/backtrade-mcp.mjs
curl -fsSLo "$HOME/.local/share/backtrade/backtrade-mcp.mjs.sha256" \
  https://backtrade.dev/backtrade-mcp.mjs.sha256
(cd "$HOME/.local/share/backtrade" && sha256sum -c backtrade-mcp.mjs.sha256)
```

Add it to Codex CLI, the Codex IDE extension, or the ChatGPT desktop app's shared
local MCP configuration:

```bash
codex mcp add backtrade -- node "$HOME/.local/share/backtrade/backtrade-mcp.mjs"
```

That command shape follows the [official Codex MCP documentation](https://developers.openai.com/codex/extend/mcp).

For Claude Code:

```bash
claude mcp add --scope user backtrade -- node "$HOME/.local/share/backtrade/backtrade-mcp.mjs"
```

Then ask your agent:

> Use Backtrade. Call `signup_trial`, list recent SPY days for my model, start a
> pull session, inspect events without future data, paper-trade my strategy, and
> finish with the scorecard. Do not invent a strategy or claim live profitability.

`signup_trial` creates a Tier-0 file automatically.

> **Economic releases and earnings through the MCP adapter.** The adapter
> subscribes to the `events` channel for you — you do not send a subscribe frame,
> and typed `econ` and `earnings` events arrive alongside bars and news. **The
> published bundle now delivers them**: measured 2026-09-12 against
> `backtrade-mcp.mjs` sha256 `57d0e030…`, one SPY session returned
> `{"econ": 21, "news": 75, "bar": 896}`, with `econ` frames arriving as typed
> `{"T":"ev","kind":"econ",…}` alongside the bars. An earlier note here said the
> published bundle returned 0 econ, measured 2026-09-04; that was true of the
> bundle of the day and is no longer true. The raw WebSocket path below is one
> way to read events, not the only one.

> **Account linking is LIVE.** `GET https://api.backtrade.dev/v1/status` is
> authoritative and reports `"accounts"` and `"device_linking"` as enabled.
> `https://backtrade.dev/app.html` serves a real dashboard with sign-in, Keys and
> saved runs, and `link_account` returns a short code you approve there.
>
> Measured end to end against the running service on 2026-09-10: register, sign
> in, `link_account`, approve in the dashboard, replay on the minted key, and find
> the run saved under the account. The key it mints is **tier 1**.
>
> **Paid tiers are not purchasable yet**, so a free account is blind-only
> (base-100 prices) and up to 5 symbols per session.
>
> Verified 2026-09-15 against `6d1003d5`.

To attach this MCP to your
Backtrade account without putting a key in chat, call `link_account`. It stages a
new credential secret and a separate polling token locally before networking, in
a second mode-`0600` file. The tool returns only a short code, the fixed
`https://backtrade.dev/app.html` dashboard URL, and its ten-minute expiry.

Sign in at that URL, open **Keys**, enter the code, inspect the device label and
fixed scopes, then approve with your current password. Approval never subscribes
or charges you. Call `link_account_status` afterward. It polls with the retained
token internally, atomically installs the Tier-1 account key, acknowledges receipt,
and only then removes the pending file. A working Tier-0 key is preserved until
that install succeeds; a lost response can be retried without minting another key.
Denied or expired requests require `link_account {"restart":true}` so replacement
authority is always explicit.

For an existing account or paid Tier-1 key issued through another client, you may
still put its ID, secret, scopes, and cached expiry in the same secure file manually
(do this outside model/chat context):

```json
{
  "version": 1,
  "state": "active",
  "keyId": "YOUR_16_CHARACTER_KEY_ID",
  "secret": "YOUR_43_CHARACTER_SECRET",
  "tier": 1,
  "scopes": ["replay:read", "orders:simulate"],
  "expiresAt": "2026-10-09T00:00:00.000Z"
}
```

Run `chmod 600 ~/.config/backtrade/agent-credential.json` after writing it. You
can select another file with `BACKTRADE_CREDENTIAL_PATH`. The cached Tier-1
`expiresAt` is metadata only because renewals happen on the server; authentication
there is authoritative on every request. Tier-0 files remain locally fail-closed
at their non-renewable expiry.

The adapter exposes:

| Tool | Purpose |
|---|---|
| `signup_trial` | Create/recover a bounded 24-hour local credential |
| `link_account` | Stage account-key authority locally and show its short dashboard approval code |
| `link_account_status` | Poll internally, atomically install the approved Tier-1 key, then acknowledge it |
| `list_days` | Find replayable sessions and leakage metadata, on whichever feed covers each day |
| `start_session` | Open a pull replay; Tier-0 is one indexed symbol, Tier-1 may request its plan's multi-symbol/unblinded limits |
| `advance` | Reveal the next events or advance to a UTC time |
| `session_status` | Read clock, remaining events, account, and capacity state |
| `place_order` | Submit market, limit, or stop paper orders |
| `cancel_order` | Cancel a resting order |
| `get_account` | Read cash, positions, P&L, fills, and costs |
| `get_orders` | Read consistently serialized order state |
| `end_session` | Reveal truth and the causal buy-and-hold scorecard |
| `resume_session` | Continue a SUSPENDED replay from the event it stopped at, after a server restart |
| `run_campaign` | Pooled benchmark across a span of days — opens no session and spends no quota |
| `arm_gate` | Stop the tape the moment a rule matches, and keep it stopped until you acknowledge it |
| `ack_event` | Acknowledge the held event by its `seq` and let the tape run on |
| `disarm_gate` | Stop the rule firing again. It does NOT release a hold already in effect — ack that one first |

**Both surfaces serve all seventeen, gate tools included.** [Measured 2026-09-12:
`tools/list` returns 17 names against the hosted MCP at
`https://api.backtrade.dev/mcp` and against the published bundle, and `arm_gate`
on a hosted session holds the tape exactly as it does locally.] An earlier draft
of this note claimed the three were local-only; that was read off a test fixture
instead of measured against the live endpoint, and it was wrong.

### Arguments

`?` marks optional. **`model` is required** on `list_days` and `start_session`.
For `start_session`, pass exactly one of the backward-compatible singular `symbol`
or the `symbols` array (1–50 transport bound); the server applies the credential's
smaller plan limit. `blind` defaults to `true`.

| Tool | Arguments |
|---|---|
| `signup_trial` | `label?` |
| `link_account` | `label?`, `restart?` (restart is accepted only after denial or expiry) |
| `link_account_status` | none; code and polling token are loaded from the pending file |
| `list_days` | `symbol`, `model`, `limit?`, `from?`, `to?` |
| `start_session` | exactly one of `symbol` or `symbols`, plus `date`, `model`, `blind?`, `cash?`, `cost_bps?`, `to?`, `allow_incomplete?`, `success_threshold?`, `control_arm?` |
| `advance` | `session_id`, at most one of `events?` or `until?` (neither defaults to 30 events); `not_after?` bounds events mode |
| `session_status` | `session_id` |
| `place_order` | `session_id`, `symbol`, `side`, `qty`, `type?`, `limit_price?`, `stop_price?`, `client_order_id?` |
| `cancel_order` | `session_id`, `order_id` |
| `get_account` | `session_id` |
| `get_orders` | `session_id` |
| `end_session` | `session_id`, `force?` |
| `resume_session` | `session_id`, plus the same `symbol`/`symbols`, `date`, `model`, `blind?`, `to?` the session was opened with |
| `run_campaign` | `symbols`, `from`, `to`, `strategy` (`buy_and_hold` or `do_nothing`), `cash?`, `cost_bps?` |
| `arm_gate` | `session_id`, `match`, `gate?` (`hold` default, or `notify`) |
| `ack_event` | `session_id`, `seq` — **required while the tape is held**; the advance that reported the hold carried it as `gate.held_on.seq` |
| `disarm_gate` | `session_id` |

### Two controls that turn a backtest into a prediction

These are the difference between a number you found and a number you predicted.
Both are optional arguments to `start_session`; neither changes the tape.

**`success_threshold` — pre-registration.** State what would count as success
*before* the run, as `{ metric, op, value }`. The server records it at session
open and grades against it at `end_session`. **It refuses a threshold supplied
after the run**, which is the whole point: a criterion chosen once the result is
known is not a criterion. The scorecard reports three states and keeps them
apart — met, not met, and *ungradeable* when the metric is unavailable. An
unmet threshold is a normal outcome and is reported as one.

**`control_arm` — permutation control.** Re-executes the same trades at randomly
drawn instants on the identical tape, so **only the timing moves**. If the
strategy's result survives that, the timing carried information; if the control
scores the same, the result was the tape and not the strategy. Its absence
leaves no timing field on the scorecard, so a run without a control cannot be
mistaken for one that passed it.

**`allow_incomplete`** serves a day whose regular session is missing bars, rather
than refusing it. The scorecard still reports what was missing — the flag
changes what is refused, never what is disclosed.

One measured production run from this server reached a scorecard in about
**8 seconds**; network, model and database latency will vary:

```
signup_trial   { }                                          -> keyId
list_days      { symbol: "TSLA", model: "claude-opus-5" }   -> replayable dates
start_session  { symbol: "TSLA", date: "2026-03-23",
                 model: "claude-opus-5" }                   -> session_id, leakage
advance        { session_id, events: 60 }                   -> bars and headlines
place_order    { session_id, symbol: "TSLA",
                 qty: 10, side: "buy" }                     -> order id
advance        { session_id, events: 500 }   repeat until finished
end_session    { session_id }                               -> scorecard
```

The `model` argument is evaluation metadata used for a leakage disclosure. It
does **not** restrict which LLM or harness can use Backtrade. Unmeasured models
are labelled `contaminated` by default; the replay still belongs to you.

## Direct API: any harness

### 1. Create a trial credential

Your client creates the secret and sends only its SHA-256 verifier. The API
returns a bare 16-character `keyId`; WebSocket and REST authentication always use
that key ID and the locally retained secret as separate fields.

```bash
SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
VERIFIER=$(printf '%s' "$SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
IDEM=$(head -c 24 /dev/urandom | basenc --base64url | tr -d '=')

curl -sS -X POST https://api.backtrade.dev/v1/agent-trials \
  -H 'content-type: application/json' \
  -H "idempotency-key: $IDEM" \
  -d "{\"secretVerifier\":\"$VERIFIER\",\"label\":\"my-agent\"}"
```

Save `keyId` from the response and keep `SECRET` locally:

```bash
KEY_ID=...                 # exact bare 16-character keyId
KEY_SECRET="$SECRET"      # never sent during signup; cannot be recovered
```

Tier-0 trials last 30 days, permit 100 sessions, require `blind=1`, and allow up
to 5 symbols per session. A signed-in free account is a different allowance and a
larger one — 600 sessions a month, no expiry — so the 100 here is the no-sign-up
number, not the ceiling on what free gets you. Here `blind=1` means prices/fills share a base-100 scale; the
agent still knows the symbol and date it selected, and headlines retain identifying
prose. It is price-indexed, not anonymous. No email, card, or account is required.

Signup is rate-limited per day, so a shared IP, a CI runner, or a NAT'd office can
exhaust it without the individual agent having signed up before. Both caps reset on
the next UTC issuance day; neither is a ban, and neither consumes your local secret.

| Response `error` | Meaning | What to do |
| --- | --- | --- |
| `source_trial_limit` | Your source reached its trials for today (currently 5) | Reuse the credential you already hold, or retry after the UTC day rolls over |
| `global_trial_limit` | Site-wide daily trial capacity is spent (currently 200) | Retry after the UTC day rolls over |
| `idempotency_conflict` | The same `idempotency-key` was reused with different credential material | Generate a fresh `SECRET`/`VERIFIER`/`IDEM` triple |

Both limits are deployment policy and can change; the `error` codes are the stable
contract. A failed signup leaves a reusable `state: "pending"` credential on disk —
calling `signup_trial` again resumes that same request rather than starting over, so
retrying after the reset costs you nothing.

### 2. Discover sessions

```bash
curl -sS 'https://api.backtrade.dev/v2/replay/days?symbol=SPY&model=your-model&limit=20' \
  -H "APCA-API-KEY-ID: $KEY_ID" \
  -H "APCA-API-SECRET-KEY: $KEY_SECRET"
```

### 3. Open a pull replay

Connect to:

```text
wss://api.backtrade.dev/v2/iex?date=2026-04-30&symbols=SPY&blind=1&mode=pull&model=your-model
```

The socket sends two different control frames with `T:"success"`. First it says
`connected`; that means only that the WebSocket opened. Authentication is complete
only after the later, exact `{"T":"success","msg":"authenticated"}` frame.
Do not match on `T:"success"` alone, and do not subscribe before that second frame.

```text
← {"T":"success","msg":"connected"}
→ {"action":"auth","key":"<KEY_ID>","secret":"<KEY_SECRET>"}
← {"T":"success","msg":"authenticated"}  # auth is complete here
→ {"action":"subscribe","bars":["SPY"],"news":["SPY"],"events":["SPY"]}
← {"T":"subscription", ...}
```

**Subscribe to `events` or you will not receive any.** Scheduled economic
releases and earnings are delivered only to that channel, and a socket subscribed
to `bars` and `news` alone receives none of them, silently and for the whole
session — `advance` still reports the events it popped from the tape, so
`delivered` and `frames_sent` diverge with nothing saying why. The channels are
`bars`, `news`, `events`, `trades` and `quotes`; the acknowledgement echoes the
complete current state of all of them, so check it rather than assuming.

Queue unmatched frames: `T:"replay"` may arrive while your client is handling the
subscription acknowledgement. It carries far more than `session_id` — it is the only
place the session tells you what kind of day you just opened, and four of its fields
change how you should read your own result. Captured from a live server, not written
from memory:

```json
{"T":"replay","feed":"iex","timeframe":"1Min","mode":"pull","extended_hours":false,
 "date":"1970-01-01","symbols":["SPY"],"speed":0,"blind":true,
 "volume_redacted":true,
 "total_events":476,
 "total_by_kind":{"bar":404,"econ":60,"news":12},
 "fill_latency":{"SPY":{"median_min":1,"p90_min":1,"max_min":1,"rth_bars":390}},
 "completeness":{"measured":true,"rth_expected":391,"rth_complete_floor":390,
                 "min_rth_bars_to_serve":195,"rth_bars":{"SPY":390},
                 "complete":true,"acknowledged_incomplete":false},
 "leakage":{"verdict":"contaminated","note":"...","news_redaction":{...}},
 "session_id":"s_...","cash":100000,"cost_bps":1}
```

| field | why you care |
|---|---|
| `volume_redacted` | on a blind session every bar's `v` is **`null`**, not zero. Zero would assert no trades occurred; null says not disclosed. If you size orders by observed volume, that signal is absent here and this flag is how you find out before your strategy silently does nothing |
| `fill_latency` | a market order fills at the first open **strictly after the submission clock** (13:40:00 → 13:41 open on a contiguous minute tape). This reports tape-spacing statistics; use submitted/filled timestamps for actual per-order latency. `median_min` is 1 on a full tape. It is the `p90_min` and `max_min` you want: a day can median 1 minute and still contain a 28-minute hole, and your 09:45 entry becomes a 10:13 entry inside it |
| `completeness` | `rth_bars` against `rth_expected`, plus the floor. A day under `min_rth_bars_to_serve` is refused outright unless you pass `allow_incomplete=1`, so if you opted in, this is the day you opted into |
| `total_by_kind` | `total_events` counts what the **tape** yields; a subscription filter decides what you **receive**. Here 476 tape events become 404 bar frames for a bars-only subscriber. Do not compare a received count against `total_events` — they are different quantities |
| `leakage` | `contaminated` is the default for any model with no measured memory horizon on this box. It is not an accusation, it is an absence of measurement |

Your harness owns the clock:

```json
{"action":"advance","events":30}
{"action":"advance","until":"15:30:00"}
{"action":"status"}
{"action":"end"}
```

For bar replay, `until` also accepts `YYYY-MM-DDTHH:MM[:SS]Z` in the
session's displayed wire clock. Bare times stay anchored to the start date.
With `events`, an optional full-instant `not_after` stops at that availability
cutoff, including when using a trigger. It cannot be combined with `until`.
Tick replay continues to use `milliseconds`.

Responses are `T:"replay_advance"`, `replay_status`, and `replay_result`.
`advance` includes fills caused by that batch. `end` refuses before completion;
`{"action":"end","force":true}` drains the tape and marks the result forced.

> **THE DATA DOES NOT COME BACK INSIDE THE `replay_advance` REPLY. Read the frames
> that arrive BEFORE it.** On this WebSocket the events are pushed as their own
> frames — `T:"b"` bars, `T:"n"` news, `T:"ev"` calendar events — and the
> `replay_advance` that follows is an ACKNOWLEDGEMENT carrying a `delivered`
> COUNT, not a payload. A client that reads only the reply to its own `advance`
> sees `delivered: 30` and **no data, and no error**, which looks exactly like an
> empty tape.
>
> Measured on 2026-04-30/SPY: `{"action":"advance","events":5}` produced **30 data
> frames, then** `{"T":"replay_advance","delivered":30}` **with no `frames` array
> at all.**
>
> Accumulate frames from the socket as they arrive and treat `replay_advance` as
> the end-of-batch marker. **The MCP bundle differs here** — it does that
> accumulation for you and returns the events inside the tool result — so code
> written against the MCP shape will not port to the raw socket unchanged.

If you want a wall-clock push stream instead, omit `mode=pull` and set `speed`.
Remember that your network/model latency is multiplied by replay speed; pull mode
is the safer default for turn-based agents.

### 4. Paper orders and results

`session_id` is a query parameter. Credentials use Alpaca's separate headers:

```bash
curl -sS -X POST "https://api.backtrade.dev/v2/orders?session_id=$SESSION_ID" \
  -H "APCA-API-KEY-ID: $KEY_ID" \
  -H "APCA-API-SECRET-KEY: $KEY_SECRET" \
  -H 'content-type: application/json' \
  -d '{"symbol":"SPY","qty":10,"side":"buy","type":"market","client_order_id":"my-order-1"}'
```

Useful endpoints:

```text
GET    /v2/orders?session_id=...
GET    /v2/orders/<order-or-client-id>?session_id=...
DELETE /v2/orders/<order-or-client-id>?session_id=...
DELETE /v2/orders?session_id=...                 # cancel all resting
GET    /v2/account?session_id=...
GET    /v2/positions?session_id=...
GET    /v2/replay/result?session_id=...          # after completion
GET    /v1/status                                # build + capabilities
```

Retries with the same `client_order_id` and identical terms return the original
order. Reusing it with changed terms returns `409`, preventing duplicate trades.
Resting orders expire when the tape completes; new orders are then rejected.

### 5. Paid plans

`GET /v1/billing/plans` is the catalog; when `purchasable` is true the paid plans
can be bought (**not yet — `billing_checkout` is `false`; see the notice above**).
Indie and Pro unblind prices, allow several symbols per session,
and meter sessions per calendar month (UTC) instead of per 24-hour key. Buying
is three calls:

```bash
# 1. Start checkout with the key you already have (trial keys work).
curl -sS -X POST https://api.backtrade.dev/v1/billing/checkout \
  -H "APCA-API-KEY-ID: $KEY_ID" -H "APCA-API-SECRET-KEY: $KEY_SECRET" \
  -H 'content-type: application/json' -d '{"plan":"indie","cadence":"monthly"}'
# -> {"checkout_url": "https://checkout.stripe.com/..."}   pay there
```

Stripe returns you to `https://backtrade.dev/billing-ok.html?session=cs_...`,
which makes your paid key in the browser. From your own code, do the same thing
with the `session` value from that URL and a fresh secret:

```bash
# 2. Claim the durable key. New secret, only its hash is sent.
PAID_SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
PAID_VERIFIER=$(printf '%s' "$PAID_SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
curl -sS -X POST https://api.backtrade.dev/v1/billing/claim \
  -H 'content-type: application/json' \
  -d "{\"checkout_session\":\"cs_...\",\"secretVerifier\":\"$PAID_VERIFIER\",\"label\":\"my-agent\"}"
# -> {"keyId": "...", "tier": 1, "plan": "indie", "expiresAt": "..."}
```

One claim per checkout; the secret is never shown again. The trial key that
started checkout is also entitled until it expires, so an agent mid-session is
not cut off.

```bash
# 3. Use the paid key exactly like the trial key.
KEY_ID=<paid keyId>; KEY_SECRET="$PAID_SECRET"
# blind=0 and symbols=SPY,QQQ,IWM are now allowed up to the plan's limits.
```

`GET /v1/billing/subscription` returns the plan, status, limits and sessions
used this month. `POST /v1/billing/portal` returns a Stripe portal URL to change
the card, switch cadence, or cancel. A canceled or unpaid subscription drops the
key to free limits immediately; the key itself stays valid for 7 days past the
paid period so a late renewal webhook cannot lock you out. Sessions beyond the
plan's monthly allowance are refused with `429` at open; symbols beyond the plan
with `403`; unblinded prices without a plan with `402`.

## Causal fill model

- Completed OHLCV is unavailable until the interval closes.
- A market order fills at the first interval open strictly after submission time: at 13:40:00, the earliest on a contiguous minute tape is the 13:41 open. The 13:39 bar became visible at 13:40; the 13:40 open has already passed. Gaps can delay a fill.
- Limits and stop activation use intervals starting strictly after submission. Limits are reported at interval completion; stops arm from a completed range before a later eligible execution. Historical results retain the engine behavior that produced them.
- A coherent bar with moving prices and zero reported volume has an unknown liquidity budget; no volume-derived cap applies, but buying power does. Flat-zero and positive volumes retain their caps. Unknown volume does not establish real liquidity, and positive reported volume can also be undercounted.
- A limit uses minute high/low and has no queue-position model.
- A stop arms when touched and fills causally afterward.
- One-way `cost_bps` is charged on every actual fill.
- Blinded trial prices and fills share the same base-100 scale.

## What the news stream actually contains

Headlines are real and causally timestamped, but **volume and composition vary
enormously by symbol**, and that changes which strategies are testable. Measured
across 7 symbols over the week ending 2026-09-02:

| symbol | news frames | % retail social |
|---|---:|---:|
| NVDA | 1,257 | 39.5% |
| SPY | 596 | 86.7% |
| AAPL | 586 | 44.2% |
| AMD | 442 | 57.7% |
| TSLA | 426 | 39.4% |
| QQQ | 350 | 85.4% |
| AMC | **15** | 86.7% |

Two things follow, and neither is obvious from a day's bar count:

- **Index ETFs are mostly social posts.** SPY and QQQ cannot have earnings, insider
  filings or press releases, so the publisher sources that cover single names have
  nothing to emit about them. Social posts carry a self-assigned `BULLISH` /
  `BEARISH` / `neutral` tag from their author, at post time. That tag is causal —
  it is not a later sentiment score — but it is one retail user's opinion, not a
  wire signal.
- **Some symbols receive almost no news.** AMC produced 15 frames across seven
  trading days. An event-driven strategy on such a symbol has nothing to trigger
  on, and nothing in the bar count reveals this in advance.

Sources present for single names include `benzinga`, `yahoo-finance`, `fmp-news`,
`fmp-press`, `fmp-insider`, `sec-form4` and `sec-form144`.

**Scheduled earnings and economic releases ARE delivered as typed events**, on the
`events` channel, as `T:"ev"` frames. This document previously said they were not;
that was wrong. Subscribe to `events` (above) or you receive none of them.

Each release arrives twice, and the split is the point: a `phase:"schedule"` frame
when the calendar entry becomes known, carrying `estimate` and `previous` and **no
actual**, then a `phase:"print"` frame at release time carrying `actual` — and
`surprise` only when a consensus estimate existed. That lets a strategy position
ahead of a print it can see coming without ever seeing the number early.

A real pair from one session, the same release seen twice:

```json
{"T":"ev","kind":"econ","S":"NVDA","phase":"schedule","event":"MBA Mortgage Refinance Index",
 "country":"US","importance":"low","scope":"market","release_at":"...",
 "estimate":null,"previous":740.8}

{"T":"ev","kind":"econ","S":"NVDA","phase":"print","event":"MBA Mortgage Refinance Index",
 "country":"US","importance":"low","scope":"market","release_at":"...",
 "estimate":null,"previous":740.8,"actual":732.6}
```

**Most releases have no consensus estimate, and without one there is no surprise.**
`estimate` is `null` on the majority of frames, and `surprise` is omitted entirely
rather than sent as zero — a strategy that keys on surprise must treat the field as
absent, not as no-change. Measured across 90 econ frames from two sessions on one
date: 29% carried a non-null `estimate` and 13% carried `surprise`. That is a single
date and the mix varies by day, so treat the proportions as an order of magnitude
rather than a rate, but do not design as though every release is a surprise trade.

`kind:"econ"` is macro and carries `scope:"market"` — it is fanned out per symbol,
so a two-symbol session sees one release twice and must not count it as two
signals. `kind:"earnings"` belongs to one ticker and adds `eps_actual`,
`revenue_actual` and `eps_surprise` on the print. An index ETF has no earnings, so
`SPY` and `QQQ` receive econ frames only.

Two limits worth knowing before you build on them. Earnings carry a `report_date`
and **no release time**, so before-open versus after-close is not distinguishable
from the frame — the session's own print may therefore be unusable for a
same-day trade. And a session covers one trading day, so the common
"buy the day before, hold through the print" structure cannot be expressed in a
single session.

Each news frame carries a `timing` field — `instant`, `minute`, or `grid_15m` —
saying how precisely its source's publication time is known. Treat `grid_15m`
timestamps as accurate to the quarter hour, not the second, before building a
latency-sensitive strategy on them.

Roughly **11% of publisher headlines are republished**, sometimes hours apart and
sometimes across two sources. If your strategy keys on headline *arrival rate*,
deduplicate on headline text first.

## Limits and responsibility

This is historical simulation, not live execution or investment advice. Bar replay
fills an order in full or rejects it under its prior-minute liquidity budget; it
does not model partial fills or observe intraminute spread. Supported
`sip-partial-v2` tick sessions model partial fills using recorded liquidity and
quote spreads. Legacy `sip-full-v1` tick sessions wait for a single quote with
enough displayed size to fill the entire order; they do not fill it partially.
Neither mode models order-book queue position, market impact or auctions.
Limit/high-low ordering inside a minute is modelled, not observed.
Configured basis-point costs are already reflected in cash and net P&L. They
are embedded in market-order fill prices; any portion constrained by a limit
price is charged separately to cash. Do not subtract the reported `total_cost`
again from net P&L. Results are simulations, not promises.

Backtrade supplies infrastructure. You own your model, strategy, prompts, risk,
and outcomes. See the full [Terms](https://backtrade.dev/terms) and
[Privacy notice](https://backtrade.dev/privacy).

The product is `backtrade.dev`. `backtest.dev` is an unrelated parked domain.

## Optional model decisions in code strategies

Python strategies can use a controlled `ctx.infer` connection with server-recorded requests, responses and usage. Fresh calls require your own Anthropic account and a separately approved run allowance; Backtrade provides no pooled inference. Recorded reruns make no new model calls and fail on a cache miss. Ordinary backtesting needs no provider or broker account. See [Model decisions](https://backtrade.dev/INFERENCE.md) for limits, research-clock behavior and API/MCP tools.


> **News clock incident:** FMP editorial news is temporarily withheld because its publication times were hours early. Prior runs with unknown or FMP exposure may be affected; results have not been recalculated. [Incident details](https://backtrade.dev/NEWS-CLOCK-INCIDENT.md). Corrected FMP replay availability is publisher time plus a 1-minute rounding allowance; FMP delivery latency is unmeasured.

## Covered-call scenario lab

Open [Options](https://backtrade.dev/options) to compare a **hypothetical single-cycle payoff** with holding the same shares or cash. This is not historical options replay, a quote feed or an order ticket. The current options history is not validated for performance comparisons.

The read-only `calculate_covered_call` MCP tool uses the same function as the browser (local Node, Bun remote and local-engine adapters; hosted MCP retains normal authentication). No provider call, account mutation or new REST endpoint is involved. Pass all fields under `scenario`:

```json
{"scenario":{"stock_price":100,"shares":100,"strike":105,"premium":2,"expiry_price":110,"option_fee_per_contract":0.65,"stock_buy_fee":1,"assignment_fee":5,"assignment":"automatic"}}
```

Dollar inputs have at most two decimals. Shares must be covered standard 100-share lots. Use the **bid** for premium received per share, not the midpoint. Option fees must fit premium proceeds; this model does not borrow to pay fees. `automatic` models exercise if at least $0.01 in the money at expiry; `assigned` and `not_assigned` are labeled counterfactuals. Actual exercise may differ. There are no dividends, taxes, cash interest, early assignment, rolls or reinvestment. Fees and short-option liability are accounted for; premium alone is not profit. Export includes inputs, assumptions and model version. Existing replay order tools remain equity tools.
