Updates

Changelog

Every notable change, dated. One mission is one minor version. The same notes are available as Markdown at /changelog.md.

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_idstripe_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.
Changelog · BTCMatic