Changelog
Last updated: 2026-08-09
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) feedsmetadataBase, per-route<link rel="canonical">, site-wide OpenGraph / Twitter cards (/og.png, satori-rendered) and atitle.template. The Time Machine share page drops its hand-built host detection. - Host split:
app.<domain>301s every public route to the apex and stampsX-Robots-Tag: noindex+ aDisallow: /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 withNEXT_PUBLIC_SITE_NOINDEX=1disallow 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/integrationsindex (was a 404). copyguardnow 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 viastrict-dynamicpropagation;style-srcretains'unsafe-inline'(Next inline styles, not the audit finding). Caddy'sweb_cspsnippet 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
nodeuser; compose addsread_only: true,cap_drop: ALL,no-new-privilegesand amode=1777tmpfs/tmpto 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 --prodpattern — 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_IMAGEenv override so each host pins the exact digest it already runs (RUNBOOK §3) —latest-pg16can 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_sessioncookie set by the verify endpoints (host-only on the API origin,SameSite=Lax,Securein 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. NewPOST /auth/logoutclears the cookie; the provider probesGET /meto learn session state. CORS switched to credentials mode over the same explicit origin allowlist, and cookie-authenticated mutating requests must present an allowlistedOriginheader (403cross_origin_rejectedotherwise) — 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 fromPOST /events/stream-tokenper (re)connect; stream tokens carryaud: "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):
clientOrderIdis nowbm+ SHA-256 of the claim key (36 chars). The old prefix-truncation kept only the rule id plus the bucket's YEAR — with the UNIQUEorders.client_order_idcolumn, 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/accountson executor-orders now requires a shared bearer secret (INTERNAL_API_TOKEN, constant-time compare); the API sends it asKEY_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=productionnow refusesKMS_PROVIDER=localunlessALLOW_LOCAL_KMS=true(staging-only escape hatch). Prod verified runningawsbefore the guard shipped. - Nostr login context binding (P2): the signed kind-22242 event now carries
domain/origin/purposetags 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 notbtcmatic-login. A signature obtained under another site's flow can no longer mint a BTCMatic session. - Dependency audit (P1):
pnpm audit --prodis 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 scopedpnpm.overridesfloors 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_sharesdata plane with plan-gated backtest sharing behind arule_hashintegrity 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.atomfeeds (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 profileshadow): 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 newsecurity_feed_itemstable (migration 0025). Deliberately invisible to the product: no capability rows, no events-stream emission. Builds the z-score baseline and keyword-precision corpus the futuresecurity_eventtrigger 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
--paperbackground. - 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. getInvoiceBolt11matches theBTC-LNpayment method exactly (a loose 'LN' substring could have pickedBTC-LNURL, whose destination is not a payable BOLT11).
Changed
backup.shincludes the phoenixd data volume (wallet seed) wherever it exists; RUNBOOK §8.1 corrected to the NATIVE phoenixd Lightning connection (no plugin) and the PUBLICBTCPAY_URLrequirement, plus new §8.6 (treasury sweep policy) and §8.7 (R2 tokenSignatureDoesNotMatchDR note)..env.exampledocuments 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 +pinnedWsLookupat connect time — the vetting resolution IS the connection resolution, so DNS rebinding has no window. Closes the gapguardedFetch(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 underCHANNEL_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), oneln_paymentsrow per (user, period) via the uniquenwc:{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 → 503price_unavailable, never a stale-rate invoice),POST /billing/lightning/checkout(BTCPay invoice in SATS with the rate persisted onto theln_paymentsledger; idempotent — an unexpired pending invoice is returned as-is),POST /billing/btcpay/webhook(raw-body HMACBTCPay-Sig, internal-network delivery, replay-safe status-guarded transitions). Settlement extendscurrent_period_endby 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, movesactive → past_duewith agrace_until(default 7d —past_duekeeps the paid plan), and lapsespast_due → expiredthrough the shared downgrade sweep. New user-facing templateslightning_expiring/lightning_grace. - `subscriptions.provider` (
stripe/lightning/comp, migration 0024; hand-inserted comp rows keep working via thecompdefault) +ln_paymentsledger +nwc_connectionsschema (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_USDsource in rule-schema. - Deploy:
lightningcompose profile (btcpayserver 2.4.1 + phoenixd 0.9.0, prod-only), top-level Caddy*.sitedrop-in mechanism for thepay.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. ActionJobgains optionaluser_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/subscriptionexposesprovider,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.emailinto one row per(kind, identifier)—emailornostr(hex x-only pubkey; npub is display-only). Existing accounts backfilled;users.emailis 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-pinnednostr-tools, atomic challenge burn, create-on-verify with no email, same 7-day session JWT). Strict per-IP bucketRATE_LIMIT_NOSTR_PER_MIN(default 10). - Identity management:
POST /me/identities/email(link via apurpose='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 /menow returnsemail(nullable),npuband 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
identitiesfirst — 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_uriandconnection_stringfields.
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 asplan_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/staticassets cache-first, navigations network-first with a precached/offlinefallback, 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.jsships withCache-Control: no-cacheso 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
BottomSheetcomponent (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
detailssections 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-wrapshell 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 7mobileproject 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/risklink. - `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.mdfor 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 /submissionssanitizes the rule (only price_tick/fee_estimate/ schedule/macro_event triggers; exchange_orderaccount→ theacc_gallery_templateplaceholder; address_activity/escalate/webhook rejected 422submission_not_shareable), runs both non-overlapping periods through the existing backtest pipeline, and stores itverifiedonly when a cache-bypassing re-run reproduces both reports byte-identically (automaticjuryVerified). - Snapshots: named frozen contest datasets (
snapshotstable + seed2026-H1); submissions reference one and both periods must lie inside it. - Public gallery:
GET /submissions(newest-first + jury-selected lane split; sentence viaruleSentenceText, honest per-period metrics, like count, author handle) andGET /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 (409handle_required). - Signals:
POST /submissions/:id/like+DELETE(community lane; liker must own ≥1 rule);POST /submissions/:id/jury(envJURY_USER_IDSgate);DELETE /submissions/:id(owner or juror). Optionalforked_fromattribution. - OpenAPI regenerated;
packages/sharedgainsSubmissionNotShareableError,HandleRequiredError,GalleryForbiddenError. See DEVIATIONS-M15-API.md.
0.13.0 — 2026-07-15 — Mission M14: Treasury Guard
Added
- Schema v3 (additive):
escalateaction type — ordered Telegram/email chain (max 4 steps, per-step ack timeout, one claim per chain);schema_version3 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.controlqueue 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-fulltemplate (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_eventtrigger (fomc_decision|cpi_release|btc_halvingonmacro:us, signed minute-resolution offsets like-1h/+30m) and aschema_versionfield — 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_halvingevery minute from the persisted calendar. Zero provider cost. - adapters/sentiment: hourly alternative.me Fear & Greed poller →
fear_greed_index(0–100) onsentiment: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_eventandmetric_updatenormalized 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-catcherunlocked; badge stays honestly null until history exists). Migration 0017 re-seeds the template to valid v2. - Plan gating:
macro_eventrequires themacroTriggerentitlement (free plan → 422),fear_greed_indexgated 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/draftturns 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 sharedruleSentenceText(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.dispatchqueue 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.tsreplacespaddle.ts:Stripe-Signatureverification (HMAC-SHA256 overt.body, constant-time compare, 5-min tolerance via injected clock), zod-validatedcustomer.subscription.*events; checkout creates a real Stripe Checkout Session (client injected viaApiDeps, faked in tests). Downgrade handling unchanged. Migration 0008 renamessubscriptions.paddle_subscription_id→stripe_subscription_id. Env:STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRICE_ID_PRO/POWER,WEB_APP_URLreplacePADDLE_*.
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-timet.me/<bot>?start=<code>deep link (256-bit code, SHA-256 hash stored, 15-min expiry; re-issue viaPOST /channels/:id/verification); executor-notify long-pollsgetUpdatesand binds + verifies the chat on/start <code>; unverified telegram channels are refused at test-send and skipped by routing. Migration 0009; envTELEGRAM_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) +/securitypage publishing the prod egress IP; shared site footer.
Fixed
docs/LAUNCH.mdL0 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/channelspage 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 byPLANSfrom 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 realruleInputSchema, "would it fire now?" preview running the realevaluate()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
fullprofile web service,.env.exampleadditions.
0.6.0 — 2026-07-13 — Mission M6: Backtester
Added
packages/backtester: purereplay()— synthesizes normalized envelopes from history and feeds the IDENTICALevaluate(); 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 intofee_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 + pureenforcePlan(rule counts, condition-leaf counts, throttle floors, channel & exchange_order availability) → 403plan_limit_exceeded; capability misses → 422capability_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 topackages/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_1mcontinuous-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:
actionJobSchemain event-envelope; the engine'sActionJobnow 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 viadispatched:{claim}:{channel}success markers; 6-attempt backoff → dead-letter queue + user notification;on_action_resulttemplate 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-onlyon_action_resultchaining, auto-suspension (suspended_balance/suspended_auth) with user notification.dry_runmode: 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, composefullprofile services for both executors;.env.exampledocuments every new variable.
0.3.0 — 2026-07-13 — Mission M3: Bitcoin & exchange adapters
Added
packages/adapter-kit(new): zod-validatedpublishEvent(XADD to theeventsstream), ULID event ids, health server/tracker, full-jitter backoff, capability upsert, pg LISTEN helper.apps/adapters/exchange-ws: Binance + Kraken spot tickers behind oneExchangeConnectorinterface (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 fromwatched_addresses(LISTEN hot-reload);address_activityat 0-conf, re-emits at 1/3/6 confirmations; reorg (hash change or vanished tx) emitsaddress_activity_revokedcarrying the original dedup key.apps/adapters/scheduler: one timezone-aware BullMQ repeatable per schedule rule, diff-synced with rule CRUD via LISTEN/NOTIFY; fires publishscheduleenvelopes.- Capability registry:
capabilitySchema+ pureCapabilityRegistryin rule-schema (ethereum:mainnetrejected until an adapter claims it); adapters register rows at boot (migration 0003:capabilities,watched_addresses). - Ops: per-adapter Dockerfiles +
/healthz, composefullprofile,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: pureevaluate(tree, snapshot, clock) → {matched, trace}— full-leaf trace (no short-circuit),metric_unavailable/type_mismatchfail-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 +firesrow (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:
tickshypertable,candles_1mandrule_fires_hourlycontinuous aggregates (real-time),fires.event_ts, NOTIFY trigger on rules. - Aggregate refresher pulling
price_change_pct/rule_fire_countwindows 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 areactions.order/actions.notify(brief saidactions: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) + deterministicdedup_keybuilders per event type (incl.txid:vout:conf_bucketand reorg revocation keys), determinism tests.packages/shared: zod-validated config loader, pino logger with apiKey/secret/authorization redaction, OpenTelemetry bootstrap,BtcmaticErrorhierarchy.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.