# Changelog

All notable changes to BTCMatic are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
One mission = one minor version (M1 → 0.1.0, M2 → 0.2.0, …).

## [Unreleased]

### Added — M23 PR-A: AI visibility & search presence foundations

- **Canonical origin:** new build arg `NEXT_PUBLIC_SITE_URL` (apex) feeds
  `metadataBase`, per-route `<link rel="canonical">`, site-wide OpenGraph /
  Twitter cards (`/og.png`, satori-rendered) and a `title.template`. The
  Time Machine share page drops its hand-built host detection.
- **Host split:** `app.<domain>` 301s every public route to the apex and
  stamps `X-Robots-Tag: noindex` + a `Disallow: /` robots.txt — the apex
  and the app host served identical content on every route (full-site
  duplicate content) since launch.
- **Machine-facing files:** `/robots.txt` (allow-list with an explicit
  block per AI crawler, product shell disallowed; staging builds with
  `NEXT_PUBLIC_SITE_NOINDEX=1` disallow everything), `/sitemap.xml`,
  `/llms.txt` (summary, how-to-cite rules, links), `/agents.md` (what holds
  by construction, what not to infer, real hosts, entry points, crawling).
- **Markdown mirrors** of every server-rendered public page (`/index.md`,
  `/security.md`, `/integrations*.md`, `/legal/*.md`, `/changelog.md`) with
  a CI heading-parity test against the page sources; plan facts derive from
  the entitlement matrix.
- **New pages:** `/changelog` (this file, rendered) and the missing
  `/integrations` index (was a 404).
- `copyguard` now scans the machine-facing layer and the mirrors too.
- Plan and measurement protocol: `docs/AI_VISIBILITY_PLAN.md`; owner
  procedures (Search Console, Bing, Cloudflare bot settings) in RUNBOOK §9.

## [0.21.4] — 2026-08-09 — nonce-based CSP (audit P2c)

### Security

- **`script-src 'unsafe-inline'` is gone:** the web CSP now ships from Next
  middleware with a per-request nonce + `strict-dynamic`
  (`apps/web/src/middleware.ts` + `lib/csp.ts`) — inline script injection
  no longer executes even when markup is attacker-influenced. The Turnstile
  widget keeps working via `strict-dynamic` propagation; `style-src`
  retains `'unsafe-inline'` (Next inline styles, not the audit finding).
  Caddy's `web_csp` snippet is now an empty import target — two CSP headers
  enforce as their intersection, so the edge must not re-add one. Root
  layout opts every route into dynamic rendering (a prerendered page would
  ship nonce-less scripts the browser refuses to run); fetch data caches
  are unaffected. Verified: Playwright e2e 17/17 in Chromium with the
  policy enforced.

## [0.21.3] — 2026-08-09 — container hardening (audit P2b)

### Security

- **Non-root, read-only containers:** every first-party image now runs as
  the `node` user; compose adds `read_only: true`, `cap_drop: ALL`,
  `no-new-privileges` and a `mode=1777` tmpfs `/tmp` to all app services
  (web additionally gets a tmpfs for Next's ISR/data cache — the one
  writable path it needs). Third-party images (postgres, redis, phoenixd,
  btcpayserver, caddy) are deliberately untouched; hardening them is a
  tracked follow-up.
- **Slim runtime images:** api, engine and both executors adopt the
  adapters' `pnpm deploy --prod` pattern — runtime trees carry the package
  plus prod deps only (no repo source, no dev deps; API image ~406 MB, was
  ~2 GB with the full workspace). The npm/corepack CLIs are removed from
  every runtime stage: no package manager to abuse post-exploit, and the
  npm CLI's bundled deps were the only HIGH/CRITICAL findings in the image
  scans.
- **Trivy gate in CI:** the image-build matrix now fails on known-fixed
  HIGH/CRITICAL vulns (`ignore-unfixed`) in any of the 12 images.
- **TimescaleDB pin escape hatch:** `TIMESCALE_IMAGE` env override so each
  host pins the exact digest it already runs (RUNBOOK §3) — `latest-pg16`
  can no longer surprise-upgrade the extension under the data volume.

## [0.21.2] — 2026-08-09 — cookie sessions + SSE stream tokens (audit P2a)

### Security

- **Cookie sessions (audit P2a):** browser sessions now live in an HttpOnly
  `btcmatic_session` cookie set by the verify endpoints (host-only on the
  API origin, `SameSite=Lax`, `Secure` in production) — nothing auth-shaped
  remains in localStorage for XSS to steal. The verify response body still
  returns the JWT for programmatic/Bearer clients (n8n, tests), but the web
  app never stores it. New `POST /auth/logout` clears the cookie; the
  provider probes `GET /me` to learn session state. CORS switched to
  credentials mode over the same explicit origin allowlist, and
  cookie-authenticated mutating requests must present an allowlisted
  `Origin` header (403 `cross_origin_rejected` otherwise) — Bearer clients
  and webhooks are untouched.
- **SSE stream tokens (audit P2a):** `GET /events/stream?token=` no longer
  accepts the 7-day session JWT (long-lived credentials in URLs leak via
  logs/history). The web app now fetches a 60-second single-purpose token
  from `POST /events/stream-token` per (re)connect; stream tokens carry
  `aud: "sse"` and are rejected everywhere else in the API. The web SSE
  wrapper owns reconnection (fresh token + backoff) since the browser's
  built-in EventSource retry would reuse the expired URL.

## [0.21.1] — 2026-08-08 — security-audit hardening

External review + audit pass (all findings verified in-repo before patching).

### Fixed

- **Order idempotency (P0):** `clientOrderId` is now `bm` + SHA-256 of the
  claim key (36 chars). The old prefix-truncation kept only the rule id plus
  the bucket's YEAR — with the UNIQUE `orders.client_order_id` column, a
  rule's second order fire in a calendar year failed the insert forever and
  the rule silently stopped trading. No prod damage: exactly one order row
  existed at patch time (verified on both hosts). In-flight reconciliation is
  unaffected — it reads the id from the row, never re-derives it.

### Security

- **Key-intake auth (P1):** `/internal/accounts` on executor-orders now
  requires a shared bearer secret (`INTERNAL_API_TOKEN`, constant-time
  compare); the API sends it as `KEY_INTAKE_TOKEN`. Production boots
  fail-closed without it, and compose refuses to start with it unset — the
  compose network is a blast-radius boundary, not an auth model.
- **KMS fail-closed (P1):** `NODE_ENV=production` now refuses
  `KMS_PROVIDER=local` unless `ALLOW_LOCAL_KMS=true` (staging-only escape
  hatch). Prod verified running `aws` before the guard shipped.
- **Nostr login context binding (P2):** the signed kind-22242 event now
  carries `domain`/`origin`/`purpose` tags and human-readable content; the
  API rejects events whose origin is outside the CORS allowlist (app +
  apex), whose domain mismatches that origin, or whose purpose is not
  `btcmatic-login`. A signature obtained under another site's flow can no
  longer mint a BTCMatic session.
- **Dependency audit (P1):** `pnpm audit --prod` is now fully clean (was 29
  vulns / 16 high): Next 15.5.21 (SSRF/DoS), undici 8.9, fast-xml-parser 5.7
  (security-feed fixtures re-verified), plus scoped `pnpm.overrides` floors
  for brace-expansion, fast-uri, find-my-way, nanoid, postcss, sharp and
  @fastify/static.

## [0.21.0] — 2026-08-08 — M18 shareable result cards

### Added

- **Shareable result cards** (M18, PRs #83–#86, migration 0026): any Time
  Machine comparison freezes into an immutable public card at `/tm/{code}` —
  first server component in the repo, satori/resvg-rendered 1200×630 OG PNG,
  `tm_shares` data plane with plan-gated backtest sharing behind a
  `rule_hash` integrity gate. Launch copy in LAUNCH.md L7.

## [0.20.1] — 2026-08-07 — security-feed entity-limit fix + BTCPay 2.4.2

### Security

- BTCPay Server pinned 2.4.1 → **2.4.2** — emergency vendor release for an
  actively exploited vulnerability (2026-08-07). Prod was hot-patched within
  the hour; compromise sweep (users, invoices, payouts, phoenixd balance)
  came back clean.

### Fixed

- RSS/Atom parsing no longer fails on entity-heavy feeds: fast-xml-parser's
  entity-expansion cap (1000) tripped on real GitHub `releases.atom` feeds
  (phoenix/bluewallet/btcpay, found live on v0.20.0). Entities are now left
  unexpanded (`processEntities: false`) — harmless to keyword matching.

## [0.20.0] — 2026-08-07 — Security Radar shadow-mode collector

### Added

- `adapters/security-feed` (prod-only compose profile `shadow`): collects
  keyword-matched security chatter for tracked wallet/library products from
  curated sources — vendor RSS/Atom feeds, GitHub Security Advisories, and
  a signature-verified Nostr subscription over a curated npub allowlist
  (ships empty) — into the new `security_feed_items` table (migration 0025).
  Deliberately invisible to the product: no capability rows, no
  events-stream emission. Builds the z-score baseline and keyword-precision
  corpus the future `security_event` trigger mission needs (RUNBOOK §4.10).

## [0.19.3] — 2026-08-07 — Pre-launch polish

### Changed

- Landing, pricing, proof and legal pages drop the beta framing: CTAs are
  "Start free", the product pillars read "Live", and the terms/risk pages
  keep their availability and defect disclaimers without the beta label.
- The squared-notebook background grid is gone — all pages render on a flat
  `--paper` background.
- Kraken is no longer offered in the account-connect exchange select or the
  offline capability fallback (Binance only at launch; the adapter keeps its
  Kraken implementation, config-off everywhere).

### Fixed

- npub-only accounts no longer see Stripe checkout on the billing page —
  Stripe's hosted page would collect an email, contradicting the
  email-optional promise. They get a pointer to Lightning (or to adding an
  email identity) instead; accounts with an email identity are unchanged.

## [0.19.2] — 2026-08-06 — NWC connect fix for lapsed subscriptions

### Fixed

- Billing page: connecting NWC on a LAPSED (canceled/expired) lightning
  subscription now sends the selected plan — the UI's "already active" check
  ignored the status, so the API correctly 409'd with "plan is required"
  (found live in the §8.5 drill).


## [0.19.1] — 2026-08-06 — Lightning go-live follow-ups

Fixes and hardening from the prod bring-up + first real mainnet sale.

### Fixed

- BTCPay invoices now set `redirectAutomatically` — payers return to the
  billing page right after settlement instead of parking on the paid-invoice
  screen.
- Billing page refetches the subscription on `pageshow`/visibility — coming
  back from a hosted checkout via bfcache no longer shows a stale plan.
- `getInvoiceBolt11` matches the `BTC-LN` payment method exactly (a loose
  'LN' substring could have picked `BTC-LNURL`, whose destination is not a
  payable BOLT11).

### Changed

- `backup.sh` includes the phoenixd data volume (wallet seed) wherever it
  exists; RUNBOOK §8.1 corrected to the NATIVE phoenixd Lightning connection
  (no plugin) and the PUBLIC `BTCPAY_URL` requirement, plus new §8.6
  (treasury sweep policy) and §8.7 (R2 token `SignatureDoesNotMatch` DR
  note). `.env.example` documents the public-host rule.


## [0.19.0] — 2026-08-06 — Sovereign Access III: NWC auto-renew

M17 PR-C — budget-capped monthly auto-renewal from the user's own wallet via
Nostr Wallet Connect, plus the honest-copy rewrite the feature demands.

### Added

- **`packages/nwc-client`**: minimal NIP-47 client — kind-23194 requests /
  kind-23195 responses, NIP-04 encrypted, wallet signature verified (the
  relay is untrusted), one short-lived connection per call, typed NIP-47
  error codes, injectable socket factory (fake-relay tests, zero network).
- **WS SSRF guard** (`packages/shared/net/ssrf-ws`): `vetRelayUrl` (wss-only,
  every resolved address publicly routable) at save time + `pinnedWsLookup`
  at connect time — the vetting resolution IS the connection resolution, so
  DNS rebinding has no window. Closes the gap `guardedFetch` (http/https
  only) left for user-supplied relay URLs.
- **NWC management API**: `POST /billing/nwc/connect` (parse → relay vet →
  live wallet probe → AES-256-GCM storage under `CHANNEL_SECRET_MASTER_KEY`;
  202 + immediate activation charge when no lightning subscription exists),
  `GET /billing/nwc` (masked view — the URI is never returned),
  `DELETE /billing/nwc`.
- **Charge runner**: atomic claim (`FOR UPDATE SKIP LOCKED` + schedule
  push), one `ln_payments` row per (user, period) via the unique
  `nwc:{user}:{period}` claim key, retries re-pay the SAME bolt11 while the
  invoice lives (double-charging structurally impossible), failure policy —
  budget problems → 12h backoff + immediate notification; dead connections →
  `failing`, no retries, reconnect notification; transient weather → 1h/4h/
  12h ladder, user pinged at the third straight strike. Property-tested
  (fast-check) classification + backoff monotonicity.
- **Web**: billing page "Auto-renew with your wallet (NWC)" card — connect
  (password-type input, wallet-side budget guidance), masked status, failure
  surfacing, disconnect.
- **Honest copy rewrite** (launch precondition): "we can never withdraw your
  funds" → "your rules can never spend your funds" everywhere, /security
  gains an explicit NWC paragraph (budget-capped in YOUR wallet, revocable,
  one code path, never reachable from rules), terms updated to match.

### Changed

- BTCPay client gains `getInvoiceBolt11` (invoice payment-methods lookup).
- New env: `NWC_CHARGE_INTERVAL_MS`, `NWC_CALL_TIMEOUT_MS`. RUNBOOK §8.5
  documents the NWC mainnet smoke (1,000-sat budget drill).

## [0.18.0] — 2026-08-06 — Sovereign Access II: Pay with sats (prepaid)

M17 PR-B — Lightning prepaid subscriptions via BTCPay Server + phoenixd
(spike-verified: no NBXplorer/bitcoind container needed), and the downgrade
sweep made payment-path-independent.

### Added

- **Lightning prepaid** (3/6/12 months, flat monthly × months — no duration
  discount): `GET /billing/lightning/quote` (sats preview at the latest fresh
  BTCUSD tick; stale → 503 `price_unavailable`, never a stale-rate invoice),
  `POST /billing/lightning/checkout` (BTCPay invoice in SATS with the rate
  persisted onto the `ln_payments` ledger; idempotent — an unexpired pending
  invoice is returned as-is), `POST /billing/btcpay/webhook` (raw-body
  HMAC `BTCPay-Sig`, internal-network delivery, replay-safe status-guarded
  transitions). Settlement extends `current_period_end` by calendar months in
  SQL and top-ups stack onto the current period.
- **Expiry lifecycle**: interval sweep (`LIGHTNING_SWEEP_INTERVAL_MS`) sends
  claim-keyed reminders at T-14d/T-3d, moves `active → past_due` with a
  `grace_until` (default 7d — `past_due` keeps the paid plan), and lapses
  `past_due → expired` through the shared downgrade sweep. New user-facing
  templates `lightning_expiring` / `lightning_grace`.
- **`subscriptions.provider`** (`stripe`/`lightning`/`comp`, migration 0024;
  hand-inserted comp rows keep working via the `comp` default) +
  `ln_payments` ledger + `nwc_connections` schema (used by PR-C).
- **Web**: Billing page gains a "Pay with Lightning" card (plan/duration
  selectors, live sats quote, hosted checkout hand-off, grace warning,
  provider-aware plan notes). Landing + billing prices now read the single
  `PLAN_PRICES_USD` source in rule-schema.
- **Deploy**: `lightning` compose profile (btcpayserver 2.4.1 + phoenixd
  0.9.0, prod-only), top-level Caddy `*.site` drop-in mechanism for the
  `pay.` host, RUNBOOK §8 (setup, seed backup, mainnet smoke, refunds,
  failure modes).

### Changed

- **`plan-transition.ts`**: upgrade pings and the downgrade sweep extracted
  from the Stripe module and parameterized (`actor`, `eventSource`) — Stripe
  webhooks, Lightning lapses and future paths now share one transition.
- `ActionJob` gains optional `user_id`: rule-less billing notifications
  (`plan_downgraded`, the new lightning templates) now route to the affected
  user's channels instead of falling back to the env ops chat.
- Stripe checkout guard also admits `status='expired'` (a lapsed Lightning
  subscription keeps its plan value for renew-UX and must not block a fresh
  checkout). `GET /billing/subscription` exposes `provider`, `grace_until`,
  `auto_renew`.

## [0.17.0] — 2026-08-05 — Sovereign Access I: Login with Nostr

M17 PR-A — first leg of the Sovereign Access package (Nostr login + Lightning
billing). Email becomes optional: an account can be reached by email, by Nostr
pubkey, or both.

### Added

- **`identities` table** (migration 0023): login identity moves out of
  `users.email` into one row per `(kind, identifier)` — `email` or `nostr`
  (hex x-only pubkey; npub is display-only). Existing accounts backfilled;
  `users.email` is now the nullable *contact* address.
- **Nostr login (NIP-07)**: `POST /auth/nostr/challenge` (single-use, hashed
  at rest, 5-min TTL) + `POST /auth/nostr/verify` (kind-22242 signed event,
  schnorr-verified via exact-pinned `nostr-tools`, atomic challenge burn,
  create-on-verify with no email, same 7-day session JWT). Strict per-IP
  bucket `RATE_LIMIT_NOSTR_PER_MIN` (default 10).
- **Identity management**: `POST /me/identities/email` (link via a
  `purpose='link'` magic token — the click proves address control),
  `POST /me/identities/nostr`, `DELETE /me/identities/:id` (the last identity
  can never be removed; unlinking an email also clears the contact address).
  `GET /me` now returns `email` (nullable), `npub` and the identity list.
- **Web**: "Login with Nostr" on the login page (NIP-07 extension flow, hint
  when absent), Settings → Account identities panel with the explicit
  "your keys, your account — no recovery" note for npub-only accounts,
  AppNav renders npub-short identity, legal copy updated.

### Changed

- Magic-link verify resolves accounts through `identities` first — a linked
  login email may differ from the contact address.
- Report sweep (npub-only accounts): undeliverable email legs are dropped, a
  verified Telegram channel substitutes (even on the monthly digest), and a
  report with no deliverable channel is stored without dispatch.
- Ops pings show the npub when an account has no email; audit redaction now
  also covers `sig`, `challenge`, `nwc_uri` and `connection_string` fields.

## [0.16.1] — 2026-08-05 — Ops pings: first sign-in & plan upgrades

### Added

- **Ops notifications** (owner request): the ops Telegram chat (env
  `TELEGRAM_CHAT_ID`, same fallback path as `plan_downgraded`) now receives a
  ping when a user completes their FIRST sign-in (`admin_user_activated`,
  detected at the create-on-verify upsert — returning sign-ins never ping)
  and when a Stripe webhook upgrades a plan (`admin_plan_upgraded`, carrying
  email + `from_plan → to_plan`; redeliveries are inherently deduped because
  a replay sees old plan == new plan). Enqueue failures are logged and
  swallowed — an ops ping never fails a sign-in or a webhook. No new env
  vars, no schema change.

## [0.16.0] — 2026-08-01 — Mobile shell & installable PWA

### Added

- **Installable PWA**: web app manifest (`/manifest.webmanifest`, standalone
  display, 192/512 + maskable icons derived from the app icon), theme-color,
  and a minimal hand-written service worker (`/sw.js`, no workbox): hashed
  `/_next/static` assets cache-first, navigations network-first with a
  precached `/offline` fallback, everything else untouched. The cross-origin
  API and `/events/stream` (SSE, JWT in query) are never intercepted or
  cached — enforced by a unit-tested routing policy. `/sw.js` ships with
  `Cache-Control: no-cache` so deploys roll out on the next visit.

- **Mobile app shell**: fixed bottom tab bar (Dashboard / Rules / Activity /
  More) on all signed-in pages at ≤920px; the More tab opens a bottom sheet
  with the remaining destinations. Shared `BottomSheet` component (Escape,
  overlay-click close, body scroll lock, focus hand-off). iOS safe-area aware
  (`viewport-fit=cover`).

### Changed

- **Rule builder on phones**: sticky bottom savebar keeps Save/Enable always
  reachable; the JSON/evaluator/backtest side panels became collapsible
  `details` sections and sit after the savebar at ≤920px; leaf condition
  controls reflow (metric full-width, window/op/value share a row) at ≤640px;
  nesting indent shrinks on phones. Coarse-pointer devices get ≥40px touch
  targets and 44px inputs; phone-width inputs hold 16px font (no iOS
  focus-zoom). Avatar menu closes on Escape.
- All data tables scroll horizontally inside a `.tbl-wrap` shell on narrow
  screens instead of overflowing the page; legal/proof prose tables scroll
  in place. Settings sub-tabs scroll on phones.
- Breakpoints standardized to 640px (phone) / 920px (nav+layout switch),
  documented in `globals.css`; Playwright gains a Pixel 7 `mobile` project
  with a mobile smoke suite.

## [0.15.0] — 2026-07-18 — Mission M16: Landing Proof tab

### Added

- **Proof section on the landing** (`#proof`): three popular rules —
  Dip-Catcher DCA, Fear & Greed Contrarian, Fee-Window DCA — backtested with
  the unmodified platform backtester over 2021 (bull), 2022 (bear), 2023
  (chop) and a fixed Jul-2025→Jun-2026 window, each vs a weekly-DCA baseline
  spending the same total. Accumulation metrics only (BTC stacked, avg entry,
  vs-DCA %; fee-window adds sat-saved-per-withdrawal) — no return/ROI figures.
  Honesty norm rendered literally: losing windows get the same badge treatment,
  uncovered windows say "no data" with the reason, every card carries the
  past-performance disclaimer and the `/legal/risk` link.
- **`tools/proof-seed`**: generation-only workspace package (rule documents in
  fixture idiom, mempool.space mining-archive fee proxy with disclosed
  semantics, replay runner, card derivation, `docs/proof/` report emitters).
- Landing DCA pillar claim fix: the nonexistent "RSI and moving-average
  entries" bullet replaced with the real fee-window capability.
- See `DEVIATIONS-M16.md` for spec deviations (R2 cooldown form, R7 trigger
  form and fee-data coverage, accumulation-only metric decision).

## [0.14.0] — 2026-07-15 — Mission M15: UGC Gallery (Faz 1)

### Added

- **Community submissions** (migration 0021): users share a rule + two test
  periods as a gallery card. Proof is SERVER-COMPUTED — both periods run
  through the platform backtester and a cache-bypassing re-run must be
  byte-identical for the "trace doğrulandı" mark (tampered reports fail,
  test-pinned). Snapshot-pinned for contest comparability (seed `2026-H1`).
- **Sanitizer**: only price_tick/fee_estimate/schedule/macro_event rules are
  shareable (address privacy); exchange accounts are replaced with the
  gallery placeholder; webhook actions rejected.
- **Honesty is mandatory**: submissions require a non-empty "nerede işe
  yaramadı" field and at least two periods; cost/behaviour metrics only —
  profit/ROI-style fields are schema-banned and test-pinned.
- **Signals kept apart**: automatic verification + jury selection (env-gated
  jury endpoints) render separately from community likes (likes require
  owning ≥1 rule); jury lane never mixes with the favourite count.
- **Public /community pages**: rich cards (three metric tiles + full neutral
  honesty block per the locked design), detail with collapsible FIRE/SKIP
  trace, "kendi ekranında çalıştır" (prefilled builder + auto backtest) and
  fork with attribution; share form with mandatory-field UX and inline
  handle claim (`users.handle` + PATCH /me); short-code loader.

### Added — Mission M15: UGC Gallery Faz 1 (API core)

- **Submissions** (migration 0021): shareable rule cards with a SERVER-computed
  proof. `POST /submissions` sanitizes the rule (only price_tick/fee_estimate/
  schedule/macro_event triggers; exchange_order `account` → the
  `acc_gallery_template` placeholder; address_activity/escalate/webhook rejected
  422 `submission_not_shareable`), runs both non-overlapping periods through the
  existing backtest pipeline, and stores it `verified` only when a cache-bypassing
  re-run reproduces both reports byte-identically (automatic `juryVerified`).
- **Snapshots**: named frozen contest datasets (`snapshots` table + seed
  `2026-H1`); submissions reference one and both periods must lie inside it.
- **Public gallery**: `GET /submissions` (newest-first + jury-selected lane
  split; sentence via `ruleSentenceText`, honest per-period metrics, like count,
  author handle) and `GET /submissions/:id` (id or short code; both reports'
  metrics + a FIRE/SKIP trace sample). Honest metrics only — `fires`,
  `longest_quiet_days`, `avg_entry`, `fee_saved_pct`; no profit/ROI/returns.
- **Handles**: nullable-unique `users.handle` + `PATCH /me` (`^[a-z0-9_]{3,20}$`);
  publishing requires a handle (409 `handle_required`).
- **Signals**: `POST /submissions/:id/like` + `DELETE` (community lane; liker
  must own ≥1 rule); `POST /submissions/:id/jury` (env `JURY_USER_IDS` gate);
  `DELETE /submissions/:id` (owner or juror). Optional `forked_from` attribution.
- OpenAPI regenerated; `packages/shared` gains `SubmissionNotShareableError`,
  `HandleRequiredError`, `GalleryForbiddenError`. See DEVIATIONS-M15-API.md.

## [0.13.0] — 2026-07-15 — Mission M14: Treasury Guard

### Added

- **Schema v3** (additive): `escalate` action type — ordered Telegram/email
  chain (max 4 steps, per-step ack timeout, one claim per chain);
  `schema_version` 3 required for escalate rules; v1/v2 documents parse
  untouched; escalate is a primary action only (not allowed in
  on_action_result).
- **Whitelist windows** (migration 0018): per watched address, allowed
  outgoing windows (IANA tz, weekday/time ranges, DST-correct, midnight-wrap
  supported) and an optional explicit destination allow-list. The
  bitcoin-chain adapter evaluates policies at emit time and stamps
  event-scoped 0/1 verdict metrics (`outgoing_in_window`,
  `destination_whitelisted`) onto address_activity events — guard rules are
  ordinary rules on those leaves; missing policy ⇒ metric_unavailable
  (fail-safe).
- **Escalation executor** (migration 0019): crash-safe chain state machine in
  executor-notify — step dispatch, ack-timeout advance, capped re-notify
  (max 3) then exhausted; acknowledgment via authed `POST /fires/:id/ack`,
  tokenized email links (prefetch-safe GET confirm page) and Telegram inline
  buttons; ack actor + latency recorded; duplicate deliveries converge.
- **Reorg retraction**: a revoked outgoing event retracts the in-flight
  escalation through the new `escalations.control` queue and sends a
  correction notice to every channel already notified.
- **Plan gating** (data-driven `TREASURY_GUARD_LIMITS`): basic (Pro) = 1
  guarded address, single-step alarm, no destination whitelist; full
  (Power) = matrix limits, windows + whitelist + chains. Downgrade sweep
  inherits.
- **Guard report**: `GET /addresses/:id/guard-report?from=&to=` (Power) —
  outgoing events with per-event verdicts, alarm timeline with ack actor and
  latency, JSON + CSV (ack tokens never exposed). PDF deferred (no zero-dep
  path).
- **Gallery**: `treasury-guard-full` template (migration 0020, power-gated)
  with an honest badge exemption — guards measure incidents, not entry
  prices.
- **Web**: address policy editor (plan-gated), guard report timeline page
  with CSV download, escalate chain editor in the rule builder (sentence
  parity, lossless inverse), escalation status chips on the activity feed.

## [0.12.0] — 2026-07-15 — Mission M13: Macro-event triggers & sentiment metrics

### Added

- **Schema v2** (first sanctioned schema change): `macro_event` trigger
  (`fomc_decision` | `cpi_release` | `btc_halving` on `macro:us`, signed
  minute-resolution offsets like `-1h`/`+30m`) and a `schema_version` field —
  every stored v1 document parses untouched (defaults to 1); macro rules
  require version 2. Builder, evaluator, backtester and OpenAPI updated in
  the same release per the schema-change protocol.
- **adapters/macro-calendar** (migration 0015): official-sources calendar —
  FRED release-dates API (CPI) + Federal Reserve FOMC page, fixed release
  times versioned in code (FOMC 14:00 ET, CPI 08:30 ET, DST-correct);
  halving epochs computed locally from block height. Calendar revisions
  update the persisted row and re-schedule cleanly (moved-CPI-print tested).
  Publishes `minutes_until_fomc_decision`/`_cpi_release`/`_btc_halving`
  every minute from the persisted calendar. Zero provider cost.
- **adapters/sentiment**: hourly alternative.me Fear & Greed poller →
  `fear_greed_index` (0–100) on `sentiment:global`; upstream downtime marks
  the metric stale, never fabricated. (US spot-ETF net-flow metric
  deliberately deferred by owner decision — see docs/research.)
- **Engine + envelope**: `macro_event` and `metric_update` normalized events
  with deterministic dedup keys; macro events evaluate exactly their target
  rule (scheduler pattern); per-class staleness TTLs for the new metrics.
- **Backtesting** (migration 0016): F&G archive loader + macro-event history
  replay so v2 leaves evaluate historically; data-quality sections flag
  ranges without macro/F&G coverage; API backtests and gallery badges now
  replay macro rules (`cpi-dip-catcher` unlocked; badge stays honestly null
  until history exists). Migration 0017 re-seeds the template to valid v2.
- **Plan gating**: `macro_event` requires the `macroTrigger` entitlement
  (free plan → 422), `fear_greed_index` gated via the metric-entitlement
  map — all data-driven through the plans matrix; downgrade sweep inherits.
- **Web builder**: capability-driven "Macro event" trigger (event select +
  before/after offset), registry-served macro/F&G condition metrics,
  sentence-bar parity with the shared renderer, lossless draft↔doc
  roundtrip for macro rules.

## [0.11.0] — 2026-07-15 — Mission M12: AI Copilot (BYOK) & Engine Report

### Added

- **AI Copilot, bring-your-own-key** (migration 0013): users register their own
  Anthropic/OpenAI/Gemini API key (validated with a cheap provider ping,
  AES-256-GCM at rest, masked fingerprints, never logged). `POST /copilot/draft`
  turns natural language (tr/en) into a schema-validated rule draft: zod →
  capability registry → plan checks with one error-fed retry; structured
  refusals (withdrawal/leverage/sell/unsupported-chain) enforced by prompt AND
  a post-validation guard; drafts are never saved or enabled, exchange-order
  drafts are dry-run. Per-plan daily quotas from the entitlement matrix
  (free 10 / pro 50 hard, power 500 soft). Confirmation sentence comes from the
  new shared `ruleSentenceText` (rule-schema), parity-pinned to the builder
  preview. Default models: claude-sonnet-5 / gpt-5.6-terra / gemini-3.5-flash
  (env-overridable).
- **LLM boundary CI guard**: depguard fails on LLM hosts or provider-module
  imports outside `apps/api/src/copilot/` (self-test plants a violation).
- **Copilot eval harness** (`packages/copilot-eval`): 40 tr/en fixtures
  (8 adversarial incl. prompt injection); offline real-key runner asserting
  ≥90% valid on benign, 100% refusal on adversarial.
- **Engine Report** (migration 0014): timezone-aware periodic digests
  (free monthly/email, pro weekly/email+telegram, power optional daily + CSV
  and API access) — fires by rule, fee-timing savings from trace fee leaves,
  average entry vs the Time Machine baseline, upcoming schedules, suspended
  rules. Exactly-once via claim keys (re-runs never double-send); delivery
  through the new `reports.dispatch` queue consumed by executor-notify;
  per-channel unsubscribes honored at dispatch; honest-metrics enforced by
  tests (disclaimer everywhere, min-sample suppression, banned words masked).
- **Web**: Copilot panel in the builder (draft → sentence chips → review in
  builder), Settings → Copilot (key management) and Settings → Reports
  (frequency, unsubscribes, power daily opt-in + archive/CSV).

## [0.10.0] — 2026-07-15 — Mission M11: Entitlement matrix, Rule Gallery & Time Machine

### Added

- **Entitlement matrix** (`packages/rule-schema/plans.ts`): all plan gating is
  now a single typed data matrix (rules/leaves/channels/addresses/
  evalFloorSec/exchangeOrders + forward-looking V2 dimensions: gallery,
  galleryResim, copilot daily quotas, macroTrigger, fearGreedMetric,
  etfFlowMetric, report frequency, treasuryGuard tier, backtest, dca).
  Owner decisions: backtest open to all plans, DCA (schedule rules) open on
  free, exchange orders stay Pro+, no Slack channel (webhook covers it).
- **Rule Gallery** (migration 0011): curated template registry seeded with 12
  templates (4 future-gated via capability tokens: cpi-dip-catcher +
  halving-epoch-dca → M13, treasury-guard-basic → M14, rsi-entry unscoped);
  public `GET /gallery` (marketing surface, works logged-out); one-click
  install through the EXACT M5 rule-create path (dry-run default, plan
  gated, 402 upgrade payloads); Power-only parameterized re-simulation;
  nightly badge job (BullMQ cron 03:00 UTC) running 90-day backtests with
  honesty gates — no badge under 5 fires, best AND worst 30-day windows
  always together, template-hash invalidation on edit.
- **Time Machine onboarding** (migration 0012): one-step habit form →
  6-month comparative backtest (habit baseline vs recommended dip-buyer
  template) via the M6 engine with UTC-day-stable cache keys (<10s warm,
  template replay shared across users); result stored per user as the future
  Engine Report baseline; boot-time idempotent candle warmup.
- **Honest-metrics single source** (`packages/shared/legal.ts`):
  PERFORMANCE_DISCLAIMER rendered verbatim on every performance surface +
  findBannedCopy guard (no profit/guaranteed/returns wording) asserted in
  tests across gallery badges and Time Machine copy.
- **Web**: public `/gallery` (badges, persona chips, param sheet with
  min/max validation, capability/upgrade states), `/time-machine`
  (once-per-user onboarding, skippable, sparkline comparison, install CTA
  with Pro upsell for order templates), nav/footer links.
- Assembly cross-check test pinning the Time Machine's local template copy
  to the gallery seed (caught a real 30s/60s throttle drift on first run).

## [0.9.0] — 2026-07-14 — Launch readiness: Stripe billing, prod infra, telegram verification

### Changed

- **Billing provider: Paddle → Stripe** (owner decision, 2026-07; Stripe is
  not merchant-of-record — tax stays on the seller, consciously accepted).
  `apps/api/src/stripe.ts` replaces `paddle.ts`: `Stripe-Signature`
  verification (HMAC-SHA256 over `t.body`, constant-time compare, 5-min
  tolerance via injected clock), zod-validated `customer.subscription.*`
  events; checkout creates a real Stripe Checkout Session (client injected
  via `ApiDeps`, faked in tests). Downgrade handling unchanged. Migration
  0008 renames `subscriptions.paddle_subscription_id` →
  `stripe_subscription_id`. Env: `STRIPE_SECRET_KEY`,
  `STRIPE_WEBHOOK_SECRET`, `STRIPE_PRICE_ID_PRO/POWER`, `WEB_APP_URL`
  replace `PADDLE_*`.

### Added

- **Engine Dockerfile + compose service** — the event engine was not
  deployable via compose at all (no Dockerfile, no service entry, absent
  from the CI image matrix); all three fixed.
- **Production compose overlay** (`infra/docker-compose.prod.yml`):
  `restart: unless-stopped`, memory limits, log rotation, pinned image
  tags, and no published ports except Caddy 80/443 (Docker bypasses UFW).
- **Caddy reverse proxy** (`infra/caddy/Caddyfile`): env-parameterized for
  staging/prod, automatic Let's Encrypt, SSE route with buffering off,
  www→apex redirect, HSTS. Staging hosts are single-level
  (`staging-app.`, `staging-api.`) for Cloudflare universal-cert
  compatibility.
- **Observability profile** (`infra/docker-compose.observability.yml`):
  otel-collector, Tempo (span metrics via metrics-generator), Prometheus,
  Loki + Promtail, Grafana provisioned with the M2 engine dashboard
  (published on 127.0.0.1:3300 only — SSH tunnel).
- **Telegram chat verification** (closes the DEVIATIONS-M8 manual chat-id
  item): `POST /channels {type:"telegram"}` returns a one-time
  `t.me/<bot>?start=<code>` deep link (256-bit code, SHA-256 hash stored,
  15-min expiry; re-issue via `POST /channels/:id/verification`);
  executor-notify long-polls `getUpdates` and binds + verifies the chat on
  `/start <code>`; unverified telegram channels are refused at test-send
  and skipped by routing. Migration 0009; env `TELEGRAM_BOT_USERNAME`;
  builder-side verify button + pending/verified badge.
- **Operational runbook** (`docs/RUNBOOK.md`): environments, egress IPs,
  deploy procedure with explicit migration step, playbooks (adapter down,
  DLQ drain, restore, secret rotation), backups, upstream rate limits.
- **Legal/static pages** in `apps/web`: Terms, Privacy, Risk disclosure
  (templates pending legal review) + `/security` page publishing the prod
  egress IP; shared site footer.

### Fixed

- `docs/LAUNCH.md` L0 decisions recorded as decided; ARCHITECTURE §2
  billing row updated to Stripe.

## [0.8.0] — 2026-07-13 — Mission M8: Deferred items — product completion & hardening

### Added

- **48h dry-run auto-promotion** (§4 debt): API-defaulted dry_run rules carry
  `dry_run_auto_promote_at`; an atomic sweep promotes them to live with an
  audit row + notification. Explicit user dry_run is never auto-promoted.
- **SSE live feed**: `GET /events/stream` — per-user fire events (fire_feed
  cursor table + NOTIFY trigger), JWT via header or ?token, heartbeats,
  connection caps. Web dashboard consumes it live (green pulse) with
  automatic 5s-polling fallback and merge-on-flip (no dropped fires).
- **`GET /capabilities`** endpoint; the builder's trigger/market/metric
  selects are now driven by the live registry (static snapshot only as
  offline fallback).
- **Per-user notification channels**: `notification_channels` +
  `webhook_endpoints` (server-generated signing secrets shown exactly once,
  AES-256-GCM at rest); executor-notify resolves destination/secret per rule
  owner (env vars remain dev fallback); rule saves reject unregistered
  webhook URLs (422). New `/settings/channels` page with test-send.
- **Backtester schedule replay**: DST-correct in-package cron expansion —
  schedule-triggered (DCA) rules are now backtestable; weekly-dca fixture
  acceptance reproduces the hand-computed occurrence list.
- **Recursive condition-tree editor** in the builder: nested ALL/ANY groups
  (depth ≤ 4, ≤ 16 leaves with live counters/guard rails), parenthesized
  sentence bar, round-tripping of arbitrary schema-valid trees.
- **Playwright e2e smoke** (`apps/web-e2e`, separate CI job): landing
  pricing, login → live dashboard → trace panel, builder + backtest
  simulator, webhook secret shown-once flow — against a deterministic fake
  API.
- **CI `images` job**: docker-builds all 8 Dockerfiles (rot guard).
- Migration 0007 (single, forward-only): dry-run column, fire_feed + NOTIFY,
  channels, webhook endpoints.


## [0.7.0] — 2026-07-13 — Mission M7: Web App

### Added

- `apps/web`: Next.js 15 / React 19 App Router app faithfully implementing
  the three design mockups (self-hosted fonts, mockup design tokens/CSS):
  - Landing: hero + animated event terminal, rule-anatomy pipeline, pillars,
    security section, pricing driven by `PLANS` from rule-schema.
  - Dashboard: stats strip, live activity feed (5s polling of /fires),
    click-through evaluation-trace panel (✓/✗ leaves with values and
    metric_unavailable reasons, cooldown copy), rule cards with lifecycle
    badges and suspended fix hints.
  - Rule builder: sentence bar with trigger/condition/action chips, three
    step-cards (incl. friendly cooldown row and one-level OR groups),
    syntax-highlighted live rule.json validated in-browser with the real
    `ruleInputSchema`, "would it fire now?" preview running the real
    `evaluate()` in the browser, and "Test against last 30 days" wired to
    the real backtest API with a mockup-style simulator log (FIRE/SKIP with
    cooldown annotations, totals, weekly-DCA baseline, gap flags).
  - Login/verify (magic link), rules list, accounts (trade-only key intake
    with user-facing rejection codes), activity history with trace
    drill-down.
- All data flows exclusively through `@btcmatic/api-client` (injected via
  ApiProvider; faked in tests). 33 component/unit tests (jsdom), no network.
- Ops: standalone-output Dockerfile, compose `full` profile web service,
  `.env.example` additions.


## [0.6.0] — 2026-07-13 — Mission M6: Backtester

### Added

- `packages/backtester`: pure `replay()` — synthesizes normalized envelopes
  from history and feeds the IDENTICAL `evaluate()`; event-time throttle
  windows and in-process claim buckets mirror the engine's semantics
  (documented in-source); fill model at candle close ± slippage bps + fee
  tier; deterministic every-Nth skip-trace sampling; canonical sorted-keys
  serialization → byte-identical reports (tested in-package and across
  fresh worker runs on real Postgres); purity enforced by test (no
  Date.now/Math.random in src).
- Report object: fires (ts, price, fill, amounts, trace), sampled skip
  traces, totals + average entry, baselines spending the exact same quote
  total (lump-sum at range start; Mondays-09:00-UTC weekly DCA), and a
  data-quality section (candle/fee gaps, coverage %) — gaps flagged, never
  interpolated.
- Historical loader (migration 0006: `candles`, `fee_history`,
  `backtest_results`): paginated idempotent Binance klines backfill job;
  additive bitcoin-mempool hook recording each successful poll into
  `fee_history` (own-polling path — no public archive exists).
- API: `POST /backtests` (cache hit → 200, miss → 202 + BullMQ job),
  `GET /backtests/:id`; result cache keyed sha256(rule-hash | range |
  params); OpenAPI/api-client regenerated, spectral green.
- Acceptance: dip-buyer over a committed 30-day fixture reproduces the
  hand-computed fire list exactly; the documented second dip is
  cooldown-suppressed (asserted in skip traces); an artificial 2h gap is
  flagged with exact bounds. Benchmark: 30-day/1m replay ≈ 0.1s (10s
  budget; CI multiplier 3×).
- depguard now also enforces backtester containment (no engine/adapter/
  executor imports).


## [0.5.0] — 2026-07-13 — Mission M5: Control plane — API & billing

### Added

- `apps/api`: Fastify v5 REST API — email magic-link auth + 7d session JWT;
  rule CRUD through the full pipeline (zod → capability registry → plan
  enforcement → dry-run default for exchange_order rules); accounts/key
  management proxied to executor-orders' internal key-intake endpoint
  (plaintext never in API storage, logs or Redis); fires/orders history with
  evaluation traces; watched-address CRUD; `/healthz`.
- Plan enforcement: `packages/rule-schema/plans.ts` — Free/Pro/Power
  constants + pure `enforcePlan` (rule counts, condition-leaf counts,
  throttle floors, channel & exchange_order availability) → 403
  `plan_limit_exceeded`; capability misses → 422 `capability_missing`.
- Paddle billing: `ts;h1=` HMAC signature verification (timing-safe),
  webhook-driven subscription state, downgrade auto-disables excess rules
  oldest-first with audit rows + one notify job.
- OpenAPI 3.1 generated from the zod schemas, served at `/docs`, committed
  to `packages/api-client/openapi.json`, spectral-linted in tests;
  `packages/api-client`: openapi-typescript codegen + `createBtcmaticClient`
  (openapi-fetch) with compile-time spec conformance.
- Rate limiting: atomic Lua token buckets per user (120/min), per IP
  (300/min), magic-link 5/min/IP → 429 with retry-after; audit rows
  (secret-redacted) for every mutating call.
- Migration 0005: `magic_link_tokens`, `subscriptions`.

### Fixed

- Latent M2 issue: the `candles_1m` continuous-aggregate refresh policy only
  covered 2 hours, so backfilled tick history fell out of real-time reads
  once the watermark advanced (price_change_pct → NULL). Policy widened to
  8 days, matching the largest metric window.


## [0.4.0] — 2026-07-13 — Mission M4: Executors — notify, webhook, orders

### Added

- `packages/shared/net`: SSRF-guarded fetch — resolves and vets every
  A/AAAA record (private/link-local/loopback/metadata/CGNAT/IPv6 ranges,
  IPv4-mapped unwrapping), pins the connection to the vetted IPs via a
  custom undici dialer (no check-then-connect rebinding window), manual
  same-host-only redirects (max 3, re-vetted per hop), 5s deadline, 4KB
  body cap. 17-case bypass suite (DNS rebinding, 169.254.169.254 redirect,
  decimal IPs, `::ffff:127.0.0.1`, …).
- Shared queue contract: `actionJobSchema` in event-envelope; the engine's
  `ActionJob` now derives from it and executors zod-validate every job.
- `apps/executor-notify`: Telegram + SMTP + HMAC-SHA256-signed webhooks
  (`X-BTCMatic-Delivery` = claim key) dispatched exclusively through the
  SSRF guard; exactly-once via `dispatched:{claim}:{channel}` success
  markers; 6-attempt backoff → dead-letter queue + user notification;
  `on_action_result` template rendering; audit rows per attempt.
- `apps/executor-orders`: envelope-encryption key vault (AWS KMS + local
  dev shim; plaintext never logged — test-asserted), §8 key validation
  (withdrawal scope → `key_withdrawal_scope`, unrestricted IP →
  `key_ip_unrestricted`), ccxt order placement with deterministic
  clientOrderId derived from the claim key (timeout retries reuse it),
  §7 order state machine with reconciliation poller, terminal-state-only
  `on_action_result` chaining, auto-suspension (`suspended_balance`/
  `suspended_auth`) with user notification.
- `dry_run` mode: full pipeline with simulated fill at the latest tick
  price — identical rows, audit and notifications to live.
- Rule lifecycle (§7): migration 0004 adds `rules.status`
  (active/cooling/suspended_*/disabled) with legal-transition writer +
  audit; the engine now loads only active/cooling rules.
- Ops: Dockerfiles, `/healthz`, compose `full` profile services for both
  executors; `.env.example` documents every new variable.


## [0.3.0] — 2026-07-13 — Mission M3: Bitcoin & exchange adapters

### Added

- `packages/adapter-kit` (new): zod-validated `publishEvent` (XADD to the
  `events` stream), ULID event ids, health server/tracker, full-jitter
  backoff, capability upsert, pg LISTEN helper.
- `apps/adapters/exchange-ws`: Binance + Kraken spot tickers behind one
  `ExchangeConnector` interface (new exchange = config + connector entry);
  jittered-backoff reconnect with resubscribe; REST gap-fill (klines/OHLC)
  emitting candle-close ticks after reconnect.
- `apps/adapters/bitcoin-mempool`: mempool.space polling (15s) →
  `fee_estimate` (fee_next_block, fee_30m, mempool_vsize); upstream downtime
  never fabricates values — health degraded + backoff, engine staleness does
  the rest.
- `apps/adapters/bitcoin-chain`: Esplora-backed address watching from
  `watched_addresses` (LISTEN hot-reload); `address_activity` at 0-conf,
  re-emits at 1/3/6 confirmations; reorg (hash change or vanished tx) emits
  `address_activity_revoked` carrying the original dedup key.
- `apps/adapters/scheduler`: one timezone-aware BullMQ repeatable per
  schedule rule, diff-synced with rule CRUD via LISTEN/NOTIFY; fires publish
  `schedule` envelopes.
- Capability registry: `capabilitySchema` + pure `CapabilityRegistry` in
  rule-schema (`ethereum:mainnet` rejected until an adapter claims it);
  adapters register rows at boot (migration 0003: `capabilities`,
  `watched_addresses`).
- Ops: per-adapter Dockerfiles + `/healthz`, compose `full` profile,
  `docs/SMOKE-M3.md` (Binance tick → engine ≤ 2s smoke procedure).
  All adapter tests run on recorded fixtures / in-process servers — no live
  network in CI.


## [0.2.0] — 2026-07-13 — Mission M2: Event engine core

### Added

- `packages/evaluator`: pure `evaluate(tree, snapshot, clock) → {matched, trace}`
  — full-leaf trace (no short-circuit), `metric_unavailable` / `type_mismatch`
  fail-closed semantics, injected clock; fast-check property tests against an
  independent reference implementation.
- `apps/engine` — the tick → evaluate → claim → enqueue hot path, memory-only:
  - in-memory metric store `(source, metric, window) → {value, updatedAt}`
    with per-metric-class staleness TTLs (stale = missing);
  - sliding-window rule-fire counter (local wins ∨ DB aggregate);
  - rule index keyed `(trigger.type, trigger.source)` with race-safe
    versioned rebuilds and Postgres LISTEN/NOTIFY hot reload;
  - Redis Streams consumer group (pending-drain on restart, ack-after-process);
  - per-rule throttle (`SET NX PX`);
  - claim manager: Redis SETNX + `fires` row (fired_at = claim-bucket start)
    — two-layer exactly-once, property-tested with N concurrent claimants
    against real Redis+TimescaleDB;
  - BullMQ action jobs (`actions.order` / `actions.notify`) carrying claim
    key + evaluation trace, jobId-deduped; logging stub consumer.
- Migration 0002: `ticks` hypertable, `candles_1m` and `rule_fires_hourly`
  continuous aggregates (real-time), `fires.event_ts`, NOTIFY trigger on rules.
- Aggregate refresher pulling `price_change_pct` / `rule_fire_count` windows
  into memory on interval; buffered tick persister off the hot path.
- OTel spans `engine.ingest → evaluate → claim → enqueue` (tested with an
  in-memory exporter) + Grafana dashboard JSON (`infra/grafana/`).
- Acceptance integration test: 3 seeded rules incl. the §4 dip-buyer, synthetic
  tick stream → exactly one action job with the correct trace; flapping ticks
  suppressed by the cooldown; kill/restart replay cannot double-claim.

### Changed

- BullMQ forbids `:` in queue names / job ids → queues are `actions.order` /
  `actions.notify` (brief said `actions:order` / `actions:notify`).

## [0.1.0] — 2026-07-13 — Mission M1: Monorepo skeleton & contracts

### Added

- pnpm workspaces + Turborepo monorepo per ARCHITECTURE.md §12: all apps
  (api, web, engine, executor-orders, executor-notify, 4 adapters) and
  packages (evaluator, backtester) as compilable stubs.
- `packages/rule-schema`: full zod schema for the rule document (§4) —
  recursive condition tree (depth ≤ 4, leaves ≤ 16), 5 trigger types,
  3 action types, `on_action_result`, namespaced sources; inferred TS types;
  dip-buyer/fee-window/weekly-dca fixtures; 40 schema tests with precise
  error-path assertions.
- `packages/event-envelope`: normalized event types (§5) + deterministic
  `dedup_key` builders per event type (incl. `txid:vout:conf_bucket` and
  reorg revocation keys), determinism tests.
- `packages/shared`: zod-validated config loader, pino logger with
  apiKey/secret/authorization redaction, OpenTelemetry bootstrap,
  `BtcmaticError` hierarchy.
- `infra/`: docker-compose (Postgres 16 + TimescaleDB, Redis 7); initial
  forward-only migration (users, accounts, rules with extracted index
  columns, fires hypertable, orders, audit_log) + testcontainers
  integration test.
- `tools/depguard`: dependency-direction guard (engine must not import
  adapters/executors; ccxt/KMS only in executor-orders) with its own test
  suite, wired into CI alongside build/lint/test and a CI self-test proving
  the guard fails on a violating import.
