[object Object]

← back to Studio Zero

studio-zero: guided 7-step AI YouTube channel builder (David's verbatim prompts, Claude proxy + cost + demo fallback)

f7ce02e8f7d8c1184b67a278242d39a4a8a1d179 · 2026-07-24 10:05:31 -0700 · Steve

Files touched

Diff

commit f7ce02e8f7d8c1184b67a278242d39a4a8a1d179
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Jul 24 10:05:31 2026 -0700

    studio-zero: guided 7-step AI YouTube channel builder (David's verbatim prompts, Claude proxy + cost + demo fallback)
---
 .deploy.conf       |   4 +
 BRIEF.md           |  41 ++++-
 README.md          |  41 +++--
 data/products.json |   6 -
 data/prompts.json  |  94 +++++++++++
 public/index.html  | 455 +++++++++++++++++++++++++++++++++++++++++++++++------
 server.js          | 168 ++++++++++++++++----
 7 files changed, 704 insertions(+), 105 deletions(-)

diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..5e6d7f6
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,4 @@
+# Studio Zero — deploy config (Phase 6 is Steve-gated; this just makes /deploy one switch away)
+PROJECT_NAME=studio-zero
+DEPLOY_PATH=/root/Projects/studio-zero
+HEALTH_URL=/api/config
diff --git a/BRIEF.md b/BRIEF.md
index d9c8bc7..8aac60f 100644
--- a/BRIEF.md
+++ b/BRIEF.md
@@ -1,5 +1,36 @@
-# studio-zero — brief
-- Client:
-- Outcome / "high value":
-- Constraints (brand, domain, deadline):
-- Reference / competitor URLs:
+# Studio Zero — brief
+
+**Tagline:** 0 → monetized AI YouTube channel in 90 days — 7 Claude prompts, one guided workflow.
+
+## Seed
+- Source: David_AI💡 (@David263146) on X — https://x.com/David263146/status/2079054195941147042 (2026-07-20, 287♥ / 20 replies).
+- Hook: *"🚨 BREAKING: Claude can now build you a full AI YouTube channel like a $10,000/month creator agency. For free. Here are 7 prompts to go from 0 → monetized AI channel in 90 days."*
+- The 7 prompts live in the thread replies (X auth-walled); recovered verbatim via Exa mirror fan-out (~$0.015). Confidence: high (5+ independent mirror captures, word-for-word).
+
+## Intake (Phase 0)
+- **Client / owner:** Steve (Abrams portfolio). Internal build; a productizable consumer tool.
+- **Outcome:** Turn a viral "7 prompts" growth thread into a *working product* — a guided 7-step web app where each step runs the real Claude prompt and produces a concrete deliverable (niche decision → brand identity → 90-day calendar → scripts → SEO/thumbnails → monetization stack → repurposing engine). The user leaves with an actual channel plan, not a screenshot of prompts.
+- **"High value":** the thread frames this as a "$10,000/month creator agency" replacement. The product is the lead magnet made real — an obvious top-of-funnel / paid-tool candidate, and a showcase of Claude-as-workflow.
+- **Constraints:** brand TBD (working name "Studio Zero"); no domain committed; nothing deployed or public until Steve's explicit go (accelerator Phase 6 gate). Prefer $0/local where possible; Claude API is the core engine (matches the seed) and every call surfaces its $ cost.
+
+## Decisions (confirmed with Steve, 2026-07-24)
+1. **Build direction:** Guided Channel Builder *app* (working MVP), not a static playbook page.
+2. **Prompt source:** David's *exact* 7 prompts (extracted verbatim), not a best-practice rewrite.
+
+## The 7 steps (David's verbatim prompts — see data/prompts.json)
+1. **Niche & Positioning** — top-10 high-CPM/low-comp niches with CPM, difficulty, monetization paths, 3-word concept.
+2. **Channel Identity** — name options, tagline, audience persona, 4 content pillars, unique mechanism.
+3. **90-Day Content Roadmap** — 12 search-optimized titles, upload schedule, SEO-vs-virality priority, authority progression.
+4. **Video Script System** — hook / agitation / solution / 3 insights / CTA / end-screen tease, chosen tone.
+5. **SEO & Thumbnail Strategy** — 3 thumbnail concepts + copy, <60-char title, description w/ timestamps, 10 tags, A/B variants.
+6. **Monetization Stack** — affiliate, digital products, sponsorship pitch, lead magnet, email-list play.
+7. **Growth & Repurposing Engine** — one video → X thread, LinkedIn post, 3 Shorts hooks, newsletter, Pinterest desc.
+
+## Build shape (Phases 2–5)
+- Zero-dep Node `server.js`: static host + `POST /api/generate` proxy to the Anthropic API (model default `claude-sonnet-4-6`, cost-aware). Key read at runtime from env / master secrets `.env` — never copied or committed. **Demo-mode fallback** (canned deliverables) so the prototype is fully clickable with no key.
+- `public/index.html`: distinctive dark UI (Claude-orange → YouTube-red), 7-step stepper w/ progress, per-step inputs that carry the chosen [NICHE]/[TITLE] forward, live deliverable render, **per-call + running $ cost**, projects saved to `localStorage`.
+- House rules honored: the Step 3 content calendar renders as a **grid with sort + density slider**; saved-channel cards show **created date + time**; gitified at scaffold; `.gitignore` excludes the standard set.
+- Verify with /5x + /contrarian; preview via /viewer on a free port behind `admin / DW2024!`.
+
+## Gate
+- Phase 6 (deploy / DNS / public / spend / send-to-list) is **Steve-only** — nothing ships without an explicit go. This build stops at local preview.
diff --git a/README.md b/README.md
index 9c6f5b4..a09fe5b 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,36 @@
-# Project
+# Studio Zero
 
-Scaffolded by the Claude web-dev accelerator. Storefront starter with a
-server-side-sorted product grid + density slider (both persist in localStorage).
+**0 → monetized AI YouTube channel in 90 days.** A guided 7-step web app that
+runs the seven prompts from [@David263146](https://x.com/David263146/status/2079054195941147042)
+verbatim through Claude and hands you real deliverables — niche, brand identity,
+a 90-day content calendar, scripts, thumbnails/SEO, a monetization stack, and a
+repurposing engine.
+
+Built by the Claude web-dev accelerator. See `BRIEF.md` for the full intake.
 
 ## Run
 ```bash
-PORT=3900 node server.js                                # open http://localhost:3900
-BASIC_AUTH="admin:DW2024!" PORT=3900 node server.js     # gated
+PORT=4173 node server.js                 # open http://localhost:4173
 ```
+- **Live mode** (default when a key resolves): calls the Anthropic API. The key
+  is read at runtime from `ANTHROPIC_API_KEY`, then `./.env`, then the master
+  secrets `.env` — never copied or committed. Model default `claude-sonnet-4-6`
+  (override `SZ_MODEL`).
+- **Demo mode** (no key): every step returns a canned deliverable so the whole
+  flow is clickable at `$0`.
+
+Each generate shows its **estimated + actual $ cost**; the header keeps a running
+session total.
 
-## Fill it in
-- `data/products.json` — array of `{ title, sku, price, hex, image }`.
-- `server.js` `sortProducts()` — add sort modes as needed.
-- `public/index.html` — the grid + controls.
+## Layout
+- `server.js` — static host + `POST /api/generate` (Anthropic proxy, cost
+  accounting, demo fallback) + `/api/config` + `/api/prompts`.
+- `data/prompts.json` — David's 7 verbatim prompts (single source of truth).
+- `public/index.html` — the wizard (7-step stepper, niche carry-forward,
+  Step-3 calendar as a sort+density grid, projects saved to localStorage with
+  created date+time).
 
-## Preview publicly
-From the accelerator dir: `scripts/preview-tunnel.sh <slug> <port>` stands up
-`<slug>.agentabrams.com` (basic-auth gated). The DNS step is Steve-gated.
+## Preview / launch
+- Local preview: `/viewer` behind `admin / DW2024!` (accelerator Phase 5).
+- Deploy is **Steve-gated** (accelerator Phase 6) — `.deploy.conf` is present so
+  `/deploy` is one switch away, but DNS/publish/spend never run autonomously.
diff --git a/data/products.json b/data/products.json
deleted file mode 100644
index 8eee5f2..0000000
--- a/data/products.json
+++ /dev/null
@@ -1,6 +0,0 @@
-[
-  { "title": "Sample One", "sku": "SMP-001", "price": 129.00, "hex": "#2b3a55", "image": "" },
-  { "title": "Sample Two", "sku": "SMP-002", "price": 89.00, "hex": "#d8c3a5", "image": "" },
-  { "title": "Sample Three", "sku": "SMP-003", "price": 210.00, "hex": "#8e8d8a", "image": "" },
-  { "title": "Sample Four", "sku": "SMP-004", "price": 54.00, "hex": "#e98074", "image": "" }
-]
diff --git a/data/prompts.json b/data/prompts.json
new file mode 100644
index 0000000..621eab3
--- /dev/null
+++ b/data/prompts.json
@@ -0,0 +1,94 @@
+{
+  "meta": {
+    "title": "0 → monetized AI YouTube channel in 90 days",
+    "source": "David_AI (@David263146) on X — https://x.com/David263146/status/2079054195941147042",
+    "note": "Prompts are David's verbatim wording, recovered via Exa mirror fan-out (5+ independent captures)."
+  },
+  "steps": [
+    {
+      "id": 1,
+      "key": "niche",
+      "title": "Niche & Positioning",
+      "blurb": "A data-backed niche decision instead of guessing.",
+      "gives": "A ranked shortlist of high-CPM, low-competition niches with real monetization paths.",
+      "prompt": "Analyze the top 10 YouTube niches with high CPM, low competition, and strong evergreen demand. For each, give me: average CPM, content difficulty, monetization paths beyond AdSense, and a 3-word channel concept.",
+      "inputs": [],
+      "produces": "niche"
+    },
+    {
+      "id": 2,
+      "key": "identity",
+      "title": "Channel Identity",
+      "blurb": "A full brand you can launch with today.",
+      "gives": "Channel name options, tagline, audience persona, content pillars, and a unique mechanism.",
+      "prompt": "I'm starting a YouTube channel about [NICHE]. Create a full channel identity: channel name (5 options), tagline, target audience persona, content pillars (4), and a 'unique mechanism' that differentiates me from existing creators.",
+      "inputs": [
+        { "token": "[NICHE]", "label": "Your niche", "carriesFrom": "niche", "placeholder": "e.g. personal finance for freelancers" }
+      ],
+      "produces": null
+    },
+    {
+      "id": 3,
+      "key": "roadmap",
+      "title": "90-Day Content Roadmap",
+      "blurb": "A full quarter of content, prioritized.",
+      "gives": "12 search-optimized titles, an upload schedule, and an SEO-vs-virality priority order.",
+      "prompt": "Build a 90-day YouTube content calendar for a new channel in [NICHE]. Include: 12 video titles optimized for search, upload schedule, which videos to prioritize for SEO vs. virality, and a content progression that builds authority over time.",
+      "inputs": [
+        { "token": "[NICHE]", "label": "Your niche", "carriesFrom": "niche", "placeholder": "e.g. personal finance for freelancers" }
+      ],
+      "produces": null,
+      "renderCalendar": true
+    },
+    {
+      "id": 4,
+      "key": "script",
+      "title": "Video Script System",
+      "blurb": "A scroll-stopping script, structured.",
+      "gives": "A full script: hook, agitation, solution, 3 insights, CTA, and an end-screen tease.",
+      "prompt": "Write a full YouTube script for '[VIDEO TITLE]'. Use this structure: hook (first 30 seconds designed to stop scrolling), problem agitation, solution walkthrough, 3 key insights, CTA, and an end screen tease for the next video. Tone: [TONE].",
+      "inputs": [
+        { "token": "[VIDEO TITLE]", "label": "Video title", "placeholder": "paste a title from your Step 3 calendar" },
+        { "token": "[TONE]", "label": "Tone", "placeholder": "conversational / authoritative / energetic", "default": "conversational" }
+      ],
+      "produces": null
+    },
+    {
+      "id": 5,
+      "key": "seo",
+      "title": "SEO & Thumbnail Strategy",
+      "blurb": "The click-through package for one video.",
+      "gives": "3 thumbnail concepts, an optimized title, a full description, 10 tags, and A/B variants.",
+      "prompt": "For the video '[TITLE]', give me: 3 thumbnail concepts with text overlay copy, a YouTube SEO-optimized title (under 60 chars), description (with timestamps, keywords, links), and 10 tags. Also suggest A/B test variations for the thumbnail.",
+      "inputs": [
+        { "token": "[TITLE]", "label": "Video title", "placeholder": "the video you scripted in Step 4" }
+      ],
+      "produces": null
+    },
+    {
+      "id": 6,
+      "key": "monetization",
+      "title": "Monetization Stack",
+      "blurb": "Revenue beyond AdSense, mapped.",
+      "gives": "Affiliate plays, digital-product ideas, a sponsorship pitch template, and a lead magnet.",
+      "prompt": "I have a YouTube channel in [NICHE] with [X] subscribers. Build me a full monetization roadmap beyond AdSense: affiliate opportunities, digital product ideas, sponsorship pitch template, and a lead magnet I can offer to build an email list from my audience.",
+      "inputs": [
+        { "token": "[NICHE]", "label": "Your niche", "carriesFrom": "niche", "placeholder": "e.g. personal finance for freelancers" },
+        { "token": "[X]", "label": "Current subscribers", "placeholder": "e.g. 0, or 1,200", "default": "0" }
+      ],
+      "produces": null
+    },
+    {
+      "id": 7,
+      "key": "repurpose",
+      "title": "Growth & Repurposing Engine",
+      "blurb": "One video → 7+ pieces of content.",
+      "gives": "An X thread, LinkedIn post, 3 Shorts hooks, a newsletter, and a Pinterest description.",
+      "prompt": "Take this YouTube video script/transcript and repurpose it into: a Twitter/X thread, a LinkedIn post, 3 YouTube Shorts hooks, an email newsletter, and a Pinterest description. Also suggest which platforms to prioritize based on my niche.\n\nSCRIPT/TRANSCRIPT:\n[SCRIPT]",
+      "inputs": [
+        { "token": "[SCRIPT]", "label": "Script or transcript", "placeholder": "paste your Step 4 script (or any transcript)", "textarea": true }
+      ],
+      "produces": null
+    }
+  ]
+}
diff --git a/public/index.html b/public/index.html
index ceb7ab1..4e43d54 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1,55 +1,408 @@
-<!doctype html><html lang="en"><head><meta charset="utf-8">
-<meta name="viewport" content="width=device-width,initial-scale=1"><title>Storefront starter</title>
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Studio Zero — 0 → monetized AI YouTube channel in 90 days</title>
 <style>
-  :root{--cols:4}
-  *{box-sizing:border-box} body{margin:0;font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#fafafa}
-  header{display:flex;gap:14px;align-items:center;flex-wrap:wrap;padding:14px 20px;border-bottom:1px solid #e6e6e6;position:sticky;top:0;background:#fff;z-index:5}
-  h1{font-size:17px;margin:0;font-weight:700} .spacer{flex:1}
-  label{font-size:12px;color:#666;display:flex;gap:6px;align-items:center}
-  select,input[type=range]{font:inherit} .count{font-size:12px;color:#999}
-  .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:20px}
-  .card{background:#fff;border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}
-  .card img{width:100%;aspect-ratio:1/1;object-fit:cover;display:block;background:#f0f0f0}
-  .meta{padding:8px 10px} .t{font-size:13px;font-weight:600;margin:0 0 2px} .s{font-size:11px;color:#888}
-  .p{font-size:13px;margin-top:4px} .empty{padding:40px;color:#999;text-align:center}
-</style></head><body>
-<header>
-  <h1>Storefront starter</h1>
-  <label>Sort
-    <select id="sort">
-      <option value="newest">Newest</option>
-      <option value="title">Title A→Z</option>
-      <option value="sku">SKU A→Z</option>
-      <option value="price-asc">Price ↑</option>
-      <option value="price-desc">Price ↓</option>
-      <option value="light">Light → Dark</option>
-      <option value="dark">Dark → Light</option>
-    </select>
-  </label>
-  <label>Density <input id="density" type="range" min="2" max="8" value="4"></label>
-  <span class="spacer"></span><span class="count" id="count"></span>
+  :root{
+    --bg:#0a0a0c; --panel:#121216; --panel2:#17171d; --line:#26262e;
+    --ink:#f4f4f6; --mut:#9a9aa6; --faint:#6a6a76;
+    --orange:#e07b2f; --orange2:#f0a03a; --red:#ff2b2b; --red2:#c62828;
+    --ok:#3ecf8e; --grad:linear-gradient(100deg,var(--orange) 0%,var(--orange2) 35%,var(--red) 100%);
+    --cols:3;
+  }
+  *{box-sizing:border-box}
+  html,body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
+  a{color:var(--orange2)}
+  .wrap{max-width:1080px;margin:0 auto;padding:0 20px 80px}
+  header.top{position:sticky;top:0;z-index:20;background:rgba(10,10,12,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
+  .top .row{max-width:1080px;margin:0 auto;padding:12px 20px;display:flex;align-items:center;gap:14px}
+  .brand{font-weight:800;letter-spacing:.14em;font-size:15px;background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
+  .brand .z{font-size:19px}
+  .spacer{flex:1}
+  .chip{font-size:12px;padding:4px 10px;border:1px solid var(--line);border-radius:999px;color:var(--mut);white-space:nowrap}
+  .chip b{color:var(--ink)}
+  .chip.mode-live{border-color:#2b5a3f;color:#8ee6b6}
+  .chip.mode-demo{border-color:#5a4a2b;color:#e6c98e}
+  .chip.cost b{color:var(--orange2)}
+
+  .hero{padding:60px 0 34px;text-align:center}
+  .kicker{font-size:12px;letter-spacing:.22em;color:var(--faint);text-transform:uppercase}
+  .hero h1{font-size:clamp(30px,5vw,52px);line-height:1.05;margin:14px 0 8px;font-weight:800;letter-spacing:-.02em}
+  .hero h1 .g{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
+  .hero p.sub{color:var(--mut);font-size:18px;max-width:640px;margin:0 auto 8px}
+  .hero .src{font-size:12px;color:var(--faint);margin-top:10px}
+  .cta{display:inline-flex;gap:10px;margin-top:26px}
+  .btn{cursor:pointer;border:0;border-radius:11px;padding:13px 22px;font-weight:700;font-size:15px;color:#fff;background:var(--grad);box-shadow:0 6px 22px rgba(224,60,40,.28);transition:transform .08s ease,filter .15s}
+  .btn:hover{filter:brightness(1.06)} .btn:active{transform:translateY(1px)}
+  .btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink);box-shadow:none}
+  .btn.sm{padding:8px 14px;font-size:13px;border-radius:9px}
+  .btn:disabled{opacity:.5;cursor:not-allowed}
+
+  .saved{margin:34px auto 0;max-width:720px;text-align:left}
+  .saved h3{font-size:13px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin:0 0 12px}
+  .pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
+  .pcard{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;cursor:pointer;transition:border-color .15s}
+  .pcard:hover{border-color:var(--orange)}
+  .pcard .pn{font-weight:700}
+  .pcard .pmeta{font-size:12px;color:var(--mut);margin-top:4px}
+  .pcard .when{font-size:11px;color:var(--faint);margin-top:8px;display:flex;align-items:center;gap:5px}
+  .pcard .prog{height:4px;border-radius:999px;background:var(--line);margin-top:10px;overflow:hidden}
+  .pcard .prog i{display:block;height:100%;background:var(--grad)}
+  .pcard .del{float:right;color:var(--faint);font-size:12px}
+  .pcard .del:hover{color:var(--red)}
+
+  .stepper{display:flex;align-items:center;gap:0;margin:26px 0 26px;overflow-x:auto;padding-bottom:6px}
+  .snode{display:flex;align-items:center;flex:0 0 auto}
+  .sdot{width:30px;height:30px;border-radius:50%;border:2px solid var(--line);display:grid;place-items:center;font-size:13px;font-weight:700;color:var(--mut);background:var(--panel);cursor:pointer;transition:.15s}
+  .sdot.done{background:var(--grad);border-color:transparent;color:#fff}
+  .sdot.active{border-color:var(--orange);color:var(--orange2);box-shadow:0 0 0 4px rgba(224,123,47,.15)}
+  .sline{width:34px;height:2px;background:var(--line)}
+  .sline.done{background:var(--grad)}
+
+  .panel{background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:26px}
+  .steptag{font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--orange2);font-weight:700}
+  .panel h2{font-size:26px;margin:8px 0 4px;letter-spacing:-.01em}
+  .panel .blurb{color:var(--mut);margin:0 0 4px}
+  .gives{font-size:13px;color:var(--faint);border-left:2px solid var(--orange);padding-left:10px;margin:14px 0 18px}
+  .promptbox{background:#0d0d10;border:1px solid var(--line);border-radius:10px;padding:14px 16px;font:13px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;color:#cfcfd6;white-space:pre-wrap;position:relative}
+  .promptbox .tok{color:var(--orange2);font-weight:700}
+  .promptbox .lbl{position:absolute;top:-9px;left:12px;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);background:var(--panel);padding:0 6px}
+  .fields{display:grid;gap:12px;margin:18px 0}
+  .field label{display:block;font-size:12px;color:var(--mut);margin-bottom:5px;letter-spacing:.02em}
+  .field input,.field textarea{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:9px;color:var(--ink);padding:11px 12px;font:14px/1.5 inherit}
+  .field textarea{min-height:120px;resize:vertical;font-family:ui-monospace,Menlo,monospace;font-size:13px}
+  .field input:focus,.field textarea:focus{outline:0;border-color:var(--orange)}
+  .genrow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:6px}
+  .estcost{font-size:12px;color:var(--faint)}
+  .estcost b{color:var(--orange2)}
+
+  .out{margin-top:22px;border-top:1px dashed var(--line);padding-top:20px;display:none}
+  .out.show{display:block}
+  .out .ohead{display:flex;align-items:center;gap:10px;margin-bottom:12px}
+  .out .ohead .t{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
+  .out .badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--line);color:var(--mut)}
+  .out .badge.demo{border-color:#5a4a2b;color:#e6c98e}
+  .out .badge.live{border-color:#2b5a3f;color:#8ee6b6}
+  .md{color:#e9e9ee}
+  .md h3{font-size:17px;margin:16px 0 8px} .md h4{font-size:15px;margin:14px 0 6px;color:var(--orange2)}
+  .md p{margin:8px 0} .md ul,.md ol{margin:8px 0;padding-left:22px} .md li{margin:3px 0}
+  .md code{background:#0d0d10;border:1px solid var(--line);border-radius:5px;padding:1px 5px;font-size:12px}
+  .md strong{color:#fff}
+  .md table{border-collapse:collapse;width:100%;margin:12px 0;font-size:13px}
+  .md th,.md td{border:1px solid var(--line);padding:7px 10px;text-align:left}
+  .md th{background:var(--panel2);color:var(--orange2)}
+  .spin{width:16px;height:16px;border:2px solid var(--line);border-top-color:var(--orange);border-radius:50%;animation:sp .7s linear infinite;display:inline-block;vertical-align:-3px}
+  @keyframes sp{to{transform:rotate(360deg)}}
+
+  .calbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin:8px 0 14px}
+  .calbar label{font-size:12px;color:var(--mut);display:flex;align-items:center;gap:7px}
+  .calbar select,.calbar input[type=range]{accent-color:var(--orange)}
+  .calbar select{background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:6px 8px;font:13px inherit}
+  .calgrid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:10px}
+  .vcard{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:12px;display:flex;gap:10px;align-items:flex-start}
+  .vcard .num{font-weight:800;color:var(--faint);font-size:13px}
+  .vcard .vt{font-size:13px;line-height:1.4}
+
+  .navrow{display:flex;justify-content:space-between;margin-top:24px;gap:12px}
+  .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 16px;font-size:13px;opacity:0;pointer-events:none;transition:opacity .2s;z-index:50}
+  .toast.show{opacity:1}
+  .footnote{text-align:center;color:var(--faint);font-size:12px;margin-top:40px}
+  .hidden{display:none!important}
+</style>
+</head>
+<body>
+<header class="top">
+  <div class="row">
+    <div class="brand">STUDIO&nbsp;<span class="z">ZERO</span></div>
+    <span id="modeChip" class="chip">…</span>
+    <div class="spacer"></div>
+    <span id="costChip" class="chip cost">session <b>$0.0000</b></span>
+    <button class="btn ghost sm" id="homeBtn">+ New</button>
+  </div>
 </header>
-<div class="grid" id="grid"><div class="empty">Loading…</div></div>
-<script>
-// House rule: sort + density both persist in localStorage so the choice survives reloads.
-const $=s=>document.querySelector(s), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
-const sortSel=$('#sort'), dens=$('#density');
-sortSel.value=localStorage.getItem('sort')||'newest'; dens.value=localStorage.getItem('cols')||'4';
-function applyCols(){document.documentElement.style.setProperty('--cols',dens.value);localStorage.setItem('cols',dens.value);}
-applyCols();
-async function load(){
-  const r=await fetch('/api/products?sort='+encodeURIComponent(sortSel.value)); const d=await r.json();
-  $('#count').textContent=d.count+' items';
-  $('#grid').innerHTML = d.products.length ? d.products.map(p=>`
-    <div class="card">
-      <img src="${esc(p.image||'')}" alt="${esc(p.title||'')}" loading="lazy">
-      <div class="meta"><p class="t">${esc(p.title||'Untitled')}</p>
-        <div class="s">${esc(p.sku||p.handle||'')}</div>
-        ${p.price?`<div class="p">$${Number(p.price).toFixed(2)}</div>`:''}
+
+<div class="wrap">
+  <section id="home">
+    <div class="hero">
+      <div class="kicker">7 Claude prompts · one guided workflow</div>
+      <h1>0 → <span class="g">monetized</span> AI YouTube channel<br>in 90 days.</h1>
+      <p class="sub">Studio Zero runs the seven prompts a $10K/mo creator agency would — and hands you the actual deliverables: niche, brand, a full quarter of content, scripts, thumbnails, and a money map.</p>
+      <div class="cta">
+        <button class="btn" id="startBtn">Start my channel →</button>
+        <button class="btn ghost" id="demoBtn">See a demo run</button>
       </div>
-    </div>`).join('') : '<div class="empty">No products yet — fill data/products.json.</div>';
+      <div class="src">Method seeded by <a href="https://x.com/David263146/status/2079054195941147042" target="_blank" rel="noopener noreferrer">@David263146</a> · prompts run verbatim.</div>
+    </div>
+    <div class="saved" id="savedWrap" style="display:none">
+      <h3>Your channels</h3>
+      <div class="pgrid" id="savedGrid"></div>
+    </div>
+  </section>
+
+  <section id="builder" class="hidden">
+    <div class="stepper" id="stepper"></div>
+    <div class="panel" id="panel"></div>
+    <div class="footnote">Deliverables autosave to this browser · nothing is published. Studio Zero — accelerator prototype.</div>
+  </section>
+</div>
+
+<div class="toast" id="toast"></div>
+
+<script>
+const $=(s,r=document)=>r.querySelector(s), $$=(s,r=document)=>[...r.querySelectorAll(s)];
+let CFG={liveMode:false,model:'demo',rate:{in:3,out:15}}, PROMPTS=[], STATE=null, cur=0, sessionCost=0;
+
+const LS_PROJECTS='sz_projects', LS_CAL='sz_cal_prefs';
+function toast(m){const t=$('#toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1800);}
+function fmtCost(n){return '$'+Number(n||0).toFixed(4);}
+function fmtWhen(iso){try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return iso;}}
+function uid(){return 'ch_'+Math.random().toString(36).slice(2,9);}
+
+function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
+function mdInline(s){return esc(s).replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>').replace(/`(.+?)`/g,'<code>$1</code>').replace(/\[(.+?)\]\((https?:[^)]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');}
+function md(src){
+  const lines=String(src).replace(/\r/g,'').split('\n');let html='',i=0;
+  while(i<lines.length){
+    let l=lines[i];
+    if(/^\s*\|.*\|\s*$/.test(l)&&i+1<lines.length&&/^\s*\|[-: |]+\|\s*$/.test(lines[i+1])){
+      const rows=[];while(i<lines.length&&/^\s*\|.*\|\s*$/.test(lines[i])){rows.push(lines[i]);i++;}
+      const cells=r=>r.trim().replace(/^\||\|$/g,'').split('|').map(c=>c.trim());
+      const head=cells(rows[0]);let t='<table><thead><tr>'+head.map(h=>'<th>'+mdInline(h)+'</th>').join('')+'</tr></thead><tbody>';
+      for(let r=2;r<rows.length;r++){t+='<tr>'+cells(rows[r]).map(c=>'<td>'+mdInline(c)+'</td>').join('')+'</tr>';}
+      html+=t+'</tbody></table>';continue;
+    }
+    let m;
+    if(m=l.match(/^\s*####\s+(.*)/)){html+='<h4>'+mdInline(m[1])+'</h4>';i++;continue;}
+    if(m=l.match(/^\s*###\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
+    if(m=l.match(/^\s*##\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
+    if(m=l.match(/^\s*#\s+(.*)/)){html+='<h3>'+mdInline(m[1])+'</h3>';i++;continue;}
+    if(/^\s*[-*]\s+/.test(l)){let it='';while(i<lines.length&&/^\s*[-*]\s+/.test(lines[i])){it+='<li>'+mdInline(lines[i].replace(/^\s*[-*]\s+/,''))+'</li>';i++;}html+='<ul>'+it+'</ul>';continue;}
+    if(/^\s*\d+\.\s+/.test(l)){let it='';while(i<lines.length&&/^\s*\d+\.\s+/.test(lines[i])){it+='<li>'+mdInline(lines[i].replace(/^\s*\d+\.\s+/,''))+'</li>';i++;}html+='<ol>'+it+'</ol>';continue;}
+    if(l.trim()===''){i++;continue;}
+    html+='<p>'+mdInline(l)+'</p>';i++;
+  }
+  return html;
+}
+
+function loadProjects(){try{return JSON.parse(localStorage.getItem(LS_PROJECTS)||'[]');}catch(e){return [];}}
+function saveProjects(a){localStorage.setItem(LS_PROJECTS,JSON.stringify(a));}
+function persist(){
+  if(!STATE)return;
+  const all=loadProjects();const idx=all.findIndex(p=>p.id===STATE.id);
+  STATE.updatedAt=new Date().toISOString();
+  if(idx>=0)all[idx]=STATE;else all.push(STATE);
+  saveProjects(all);
+}
+function newProject(){return {id:uid(),name:'Untitled channel',createdAt:new Date().toISOString(),niche:'',outputs:{},costs:{}};}
+
+function renderStepper(){
+  const el=$('#stepper');el.innerHTML='';
+  PROMPTS.forEach((s,idx)=>{
+    const done=!!(STATE&&STATE.outputs[s.key]);
+    const node=document.createElement('div');node.className='snode';
+    const dot=document.createElement('div');
+    dot.className='sdot'+(idx===cur?' active':'')+(done?' done':'');
+    dot.textContent=done&&idx!==cur?'✓':s.id;dot.title=s.title;
+    dot.onclick=()=>{cur=idx;renderStep();renderStepper();};
+    node.appendChild(dot);
+    if(idx<PROMPTS.length-1){const ln=document.createElement('div');ln.className='sline'+(done?' done':'');node.appendChild(ln);}
+    el.appendChild(node);
+  });
+}
+
+function fillPrompt(step,vals){
+  let p=step.prompt;
+  (step.inputs||[]).forEach(inp=>{p=p.split(inp.token).join(vals[inp.token]||inp.token);});
+  return p;
+}
+function promptHTML(step,vals){
+  let p=esc(step.prompt);
+  (step.inputs||[]).forEach(inp=>{
+    const v=vals[inp.token];
+    const rep=v?('<span class="tok">'+esc(v)+'</span>'):('<span class="tok">'+esc(inp.token)+'</span>');
+    p=p.split(esc(inp.token)).join(rep);
+  });
+  return p;
+}
+function fid(token){return 'f_'+token.replace(/[^a-z]/gi,'');}
+
+function currentVals(step){
+  const vals={};
+  (step.inputs||[]).forEach(inp=>{
+    const f=$('#'+fid(inp.token));
+    let v=f?f.value.trim():'';
+    if(!v&&inp.carriesFrom==='niche'&&STATE.niche)v=STATE.niche;
+    if(!v&&inp.default)v=inp.default;
+    vals[inp.token]=v;
+  });
+  return vals;
+}
+
+function renderStep(){
+  const step=PROMPTS[cur];if(!step)return;
+  const saved=STATE.outputs[step.key];
+  const prefill={};
+  (step.inputs||[]).forEach(inp=>{if(inp.carriesFrom==='niche'&&STATE.niche)prefill[inp.token]=STATE.niche;if(inp.default&&!prefill[inp.token])prefill[inp.token]=inp.default;});
+  const p=$('#panel');
+  p.innerHTML=`
+    <div class="steptag">Step ${step.id} of 7 · ${esc(step.title)}</div>
+    <h2>${esc(step.title)}</h2>
+    <p class="blurb">${esc(step.blurb)}</p>
+    <div class="gives"><b>What this gives you:</b> ${esc(step.gives)}</div>
+    <div class="promptbox"><span class="lbl">the prompt · run verbatim</span><span id="pbtext">${promptHTML(step,prefill)}</span></div>
+    <div class="fields" id="fields"></div>
+    <div class="genrow">
+      <button class="btn" id="genBtn">${CFG.liveMode?'Generate with Claude':'Generate (demo)'} →</button>
+      <span class="estcost" id="estCost"></span>
+    </div>
+    <div class="out ${saved?'show':''}" id="out">
+      <div class="ohead"><span class="t">Deliverable</span><span class="badge" id="outBadge"></span><span style="flex:1"></span><button class="btn ghost sm" id="copyBtn">Copy</button></div>
+      <div id="calWrap"></div>
+      <div class="md" id="mdOut"></div>
+    </div>
+    <div class="navrow">
+      <button class="btn ghost" id="prevBtn" ${cur===0?'disabled':''}>← Back</button>
+      <button class="btn ghost" id="nextBtn" ${cur===6?'disabled':''}>Next step →</button>
+    </div>`;
+  const ff=$('#fields');
+  (step.inputs||[]).forEach(inp=>{
+    const id=fid(inp.token);
+    const val=prefill[inp.token]||'';
+    const wrap=document.createElement('div');wrap.className='field';
+    wrap.innerHTML=`<label>${esc(inp.label)} <span style="color:var(--faint)">${esc(inp.token)}</span></label>`+
+      (inp.textarea?`<textarea id="${id}" placeholder="${esc(inp.placeholder||'')}"></textarea>`:`<input id="${id}" value="${esc(val)}" placeholder="${esc(inp.placeholder||'')}">`);
+    ff.appendChild(wrap);
+    const fld=$('#'+id);
+    fld.addEventListener('input',()=>{$('#pbtext').innerHTML=promptHTML(step,currentVals(step));updateEst();});
+  });
+  updateEst();
+  if(saved){renderOutput(step,saved.text,saved.demo,saved.cost);}
+  $('#genBtn').onclick=doGenerate;
+  $('#prevBtn').onclick=()=>{if(cur>0){cur--;renderStep();renderStepper();}};
+  $('#nextBtn').onclick=()=>{if(cur<6){cur++;renderStep();renderStepper();}};
+  $('#copyBtn').onclick=()=>{navigator.clipboard.writeText(STATE.outputs[step.key].text);toast('Copied deliverable');};
+}
+
+function updateEst(){
+  const step=PROMPTS[cur];const p=fillPrompt(step,currentVals(step));
+  const est=((p.length/4)/1e6*CFG.rate.in)+(1600/1e6*CFG.rate.out);
+  const e=$('#estCost');if(e)e.innerHTML=CFG.liveMode?`est. <b>${fmtCost(est)}</b> · ${CFG.model}`:'demo mode · <b>$0 (local)</b>';
+}
+
+async function doGenerate(){
+  const step=PROMPTS[cur];const vals=currentVals(step);
+  for(const inp of (step.inputs||[])){if(!vals[inp.token]){toast('Fill in '+inp.label);return;}}
+  const prompt=fillPrompt(step,vals);
+  const btn=$('#genBtn');btn.disabled=true;btn.innerHTML='<span class="spin"></span> Generating…';
+  $('#out').classList.add('show');$('#mdOut').innerHTML='<p style="color:var(--mut)">Claude is working…</p>';$('#calWrap').innerHTML='';
+  try{
+    const res=await fetch('/api/generate',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,stepKey:step.key})});
+    const j=await res.json();
+    const nicheVal=vals['[NICHE]'];if(nicheVal)STATE.niche=nicheVal;
+    if(STATE.name==='Untitled channel'&&STATE.niche)STATE.name=STATE.niche.slice(0,42);
+    STATE.outputs[step.key]={text:j.text,demo:j.demo,cost:j.cost||0,at:new Date().toISOString()};
+    STATE.costs[step.key]=j.cost||0;
+    if(j.cost){sessionCost+=j.cost;updateCostChip();}
+    if(j.warning)toast('Live call failed — showing demo');
+    persist();
+    renderOutput(step,j.text,j.demo,j.cost);
+    renderStepper();
+  }catch(e){
+    $('#mdOut').innerHTML='<p style="color:var(--red2)">Error: '+esc(e.message)+'</p>';
+  }finally{
+    btn.disabled=false;btn.innerHTML=(CFG.liveMode?'Regenerate':'Regenerate (demo)')+' ↻';
+  }
+}
+
+function renderOutput(step,text,demo,cost){
+  $('#out').classList.add('show');
+  const b=$('#outBadge');b.textContent=demo?'demo':(CFG.model+' · '+fmtCost(cost||0));b.className='badge '+(demo?'demo':'live');
+  if(step.renderCalendar){renderCalendar(text);}else{$('#calWrap').innerHTML='';}
+  $('#mdOut').innerHTML=md(text);
+}
+
+function parseTitles(text){
+  const out=[];String(text).split('\n').forEach(l=>{const m=l.match(/^\s*(?:\d+[\.\)]|[-*])\s+(.+?)\s*$/);if(m){let t=m[1].replace(/\*\*/g,'').replace(/^["“]|["”]$/g,'').trim();if(t.length>3&&t.length<160)out.push(t);}});
+  return out.slice(0,12);
+}
+function renderCalendar(text){
+  const titles=parseTitles(text);const wrap=$('#calWrap');
+  if(titles.length<3){wrap.innerHTML='';return;}
+  let prefs={};try{prefs=JSON.parse(localStorage.getItem(LS_CAL)||'{}');}catch(e){}
+  const sort=prefs.sort||'newest', cols=prefs.cols||3;
+  wrap.innerHTML=`
+    <div class="calbar">
+      <label>Sort
+        <select id="calSort">
+          <option value="newest">Planned order</option>
+          <option value="az">Title A→Z</option>
+          <option value="za">Title Z→A</option>
+          <option value="short">Shortest first</option>
+          <option value="long">Longest first</option>
+        </select>
+      </label>
+      <label>Density <input type="range" id="calDens" min="1" max="4" value="${cols}"></label>
+      <span style="font-size:12px;color:var(--faint)">${titles.length} videos · your first quarter</span>
+    </div>
+    <div class="calgrid" id="calGrid"></div>`;
+  document.documentElement.style.setProperty('--cols',cols);
+  const draw=()=>{
+    const s=$('#calSort').value;const arr=titles.map((t,i)=>({t,i}));
+    if(s==='az')arr.sort((a,b)=>a.t.localeCompare(b.t));
+    else if(s==='za')arr.sort((a,b)=>b.t.localeCompare(a.t));
+    else if(s==='short')arr.sort((a,b)=>a.t.length-b.t.length);
+    else if(s==='long')arr.sort((a,b)=>b.t.length-a.t.length);
+    $('#calGrid').innerHTML=arr.map(o=>`<div class="vcard"><span class="num">${o.i+1}</span><span class="vt">${esc(o.t)}</span></div>`).join('');
+  };
+  $('#calSort').value=sort;draw();
+  $('#calSort').onchange=()=>{prefs.sort=$('#calSort').value;localStorage.setItem(LS_CAL,JSON.stringify(prefs));draw();};
+  $('#calDens').oninput=()=>{const c=$('#calDens').value;document.documentElement.style.setProperty('--cols',c);prefs.cols=+c;localStorage.setItem(LS_CAL,JSON.stringify(prefs));};
+}
+
+function updateCostChip(){$('#costChip').innerHTML='session <b>'+fmtCost(sessionCost)+'</b>';}
+
+function renderSaved(){
+  const all=loadProjects().sort((a,b)=>(b.updatedAt||b.createdAt).localeCompare(a.updatedAt||a.createdAt));
+  const wrap=$('#savedWrap'),grid=$('#savedGrid');
+  if(!all.length){wrap.style.display='none';return;}
+  wrap.style.display='block';grid.innerHTML='';
+  all.forEach(p=>{
+    const done=Object.keys(p.outputs||{}).length;const pct=Math.round(done/7*100);
+    const c=document.createElement('div');c.className='pcard';
+    c.innerHTML=`<span class="del" data-id="${p.id}" title="Delete">✕</span>
+      <div class="pn">${esc(p.name||'Untitled channel')}</div>
+      <div class="pmeta">${done}/7 steps${p.niche?' · '+esc(p.niche.slice(0,28)):''}</div>
+      <div class="prog"><i style="width:${pct}%"></i></div>
+      <div class="when" title="${esc(p.createdAt)}">🕓 ${esc(fmtWhen(p.createdAt))}</div>`;
+    c.onclick=(e)=>{if(!e.target.classList.contains('del'))openProject(p);};
+    c.querySelector('.del').onclick=(e)=>{e.stopPropagation();if(confirm('Delete "'+(p.name||'this channel')+'"?')){saveProjects(loadProjects().filter(x=>x.id!==p.id));renderSaved();}};
+    grid.appendChild(c);
+  });
+}
+function openProject(p){STATE=p;cur=0;show('builder');renderStepper();renderStep();}
+function startNew(){STATE=newProject();persist();cur=0;show('builder');renderStepper();renderStep();}
+
+function show(which){
+  $('#home').classList.toggle('hidden',which!=='home');
+  $('#builder').classList.toggle('hidden',which==='home');
+  if(which==='home')renderSaved();
+  window.scrollTo(0,0);
 }
-sortSel.onchange=()=>{localStorage.setItem('sort',sortSel.value);load();};
-dens.oninput=applyCols;
-load();
-</script></body></html>
+
+(async function(){
+  try{CFG=await (await fetch('/api/config')).json();}catch(e){}
+  const mc=$('#modeChip');
+  if(CFG.liveMode){mc.className='chip mode-live';mc.innerHTML='● LIVE · <b>'+CFG.model+'</b>';}
+  else{mc.className='chip mode-demo';mc.innerHTML='● DEMO · <b>no key</b>';}
+  try{const pj=await (await fetch('/api/prompts')).json();PROMPTS=pj.steps||[];}catch(e){PROMPTS=[];}
+  updateCostChip();
+  $('#startBtn').onclick=startNew;
+  $('#demoBtn').onclick=startNew;
+  $('#homeBtn').onclick=()=>show('home');
+  show('home');
+})();
+</script>
+</body>
+</html>
diff --git a/server.js b/server.js
index 57f5df5..124590c 100644
--- a/server.js
+++ b/server.js
@@ -1,41 +1,147 @@
-// Storefront starter — zero-dep Node http. Serves data/products.json with SERVER-SIDE sort
-// (house rule: every product grid gets sort + a density slider). Optional basic auth via BASIC_AUTH.
-const http = require('http'), fs = require('fs'), path = require('path');
-const PORT = parseInt(process.env.PORT || '3900', 10);
-const BASIC_AUTH = process.env.BASIC_AUTH || ''; // "user:pass" to gate; empty = open
+// Studio Zero — guided AI YouTube channel builder.
+// Zero-dep Node http server: static host + POST /api/generate proxy to the Anthropic API,
+// with per-call $ cost accounting and a demo-mode fallback (no key required to click through).
+const http = require('http'), fs = require('fs'), path = require('path'), https = require('https');
+const os = require('os');
+
+const PORT = parseInt(process.env.PORT || '0', 10);
 const DIR = __dirname;
+const MODEL = process.env.SZ_MODEL || 'claude-sonnet-4-6';
+const MAX_TOKENS = parseInt(process.env.SZ_MAX_TOKENS || '1600', 10);
 
-function load() { try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'products.json'), 'utf8')); } catch { return []; } }
-function authed(req) {
-  if (!BASIC_AUTH) return true;
-  const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
-  if (!m) return false; try { return Buffer.from(m[1], 'base64').toString() === BASIC_AUTH; } catch { return false; }
+// Anthropic per-Mtoken rates (USD) — keep in sync with the cost-tracker skill.
+const RATES = {
+  'claude-sonnet-4-6':   { in: 3,  out: 15 },
+  'claude-opus-4-8':     { in: 15, out: 75 },
+  'claude-haiku-4-5':    { in: 1,  out: 5 },
+};
+function rate(m) { return RATES[m] || RATES['claude-sonnet-4-6']; }
+function costOf(model, usage) {
+  const r = rate(model);
+  const i = (usage && usage.input_tokens || 0) / 1e6 * r.in;
+  const o = (usage && usage.output_tokens || 0) / 1e6 * r.out;
+  return +(i + o).toFixed(5);
 }
-// Sort modes mirror the DW sort-skill: newest, title/sku A→Z, price ↑↓, light→dark (by dominant hex).
-function luminance(hex) { if (!/^#?[0-9a-f]{6}$/i.test(hex || '')) return 999; const h = hex.replace('#', '');
-  const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
-  return 0.2126 * r + 0.7152 * g + 0.0722 * b; }
-function sortProducts(items, mode) {
-  const a = items.slice();
-  switch (mode) {
-    case 'title': return a.sort((x, y) => (x.title || '').localeCompare(y.title || ''));
-    case 'sku': return a.sort((x, y) => (x.sku || x.handle || '').localeCompare(y.sku || y.handle || ''));
-    case 'price-asc': return a.sort((x, y) => (x.price || 0) - (y.price || 0));
-    case 'price-desc': return a.sort((x, y) => (y.price || 0) - (x.price || 0));
-    case 'light': return a.sort((x, y) => luminance(y.hex) - luminance(x.hex));
-    case 'dark': return a.sort((x, y) => luminance(x.hex) - luminance(y.hex));
-    default: return a; // 'newest' = natural order
+
+// Resolve the Anthropic key at runtime WITHOUT copying or committing it:
+// env first, then a project-local .env, then the master secrets .env.
+function resolveKey() {
+  if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY;
+  const candidates = [
+    path.join(DIR, '.env'),
+    path.join(os.homedir(), 'Projects', 'secrets-manager', '.env'),
+  ];
+  for (const f of candidates) {
+    try {
+      const m = fs.readFileSync(f, 'utf8').match(/^ANTHROPIC_API_KEY=(.+)$/m);
+      if (m) return m[1].trim().replace(/^["']|["']$/g, '');
+    } catch (_) {}
   }
+  return '';
 }
-http.createServer((req, res) => {
-  if (!authed(req)) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="preview"' }); return res.end('auth required'); }
+const HAS_KEY = !!resolveKey();
+
+function loadPrompts() {
+  try { return JSON.parse(fs.readFileSync(path.join(DIR, 'data', 'prompts.json'), 'utf8')); }
+  catch { return { steps: [] }; }
+}
+
+function callAnthropic(prompt) {
+  return new Promise((resolve, reject) => {
+    const key = resolveKey();
+    if (!key) return reject(new Error('no-key'));
+    const body = JSON.stringify({
+      model: MODEL,
+      max_tokens: MAX_TOKENS,
+      messages: [{ role: 'user', content: prompt }],
+    });
+    const req = https.request({
+      hostname: 'api.anthropic.com', path: '/v1/messages', method: 'POST',
+      headers: {
+        'content-type': 'application/json',
+        'x-api-key': key,
+        'anthropic-version': '2023-06-01',
+        'content-length': Buffer.byteLength(body),
+      },
+    }, (r) => {
+      let d = ''; r.on('data', c => d += c);
+      r.on('end', () => {
+        try {
+          const j = JSON.parse(d);
+          if (r.statusCode >= 400) return reject(new Error(j.error && j.error.message || ('HTTP ' + r.statusCode)));
+          const text = (j.content || []).filter(b => b.type === 'text').map(b => b.text).join('\n');
+          resolve({ text, usage: j.usage, model: j.model || MODEL });
+        } catch (e) { reject(e); }
+      });
+    });
+    req.on('error', reject);
+    req.setTimeout(120000, () => req.destroy(new Error('timeout')));
+    req.write(body); req.end();
+  });
+}
+
+// Demo-mode deliverable so the prototype is fully clickable with no key.
+function demoOutput(stepKey) {
+  const canned = {
+    niche: "### Top 10 niches (demo)\n\n| Niche | Avg CPM | Difficulty | Beyond AdSense | 3-word concept |\n|---|---|---|---|---|\n| AI tools for solopreneurs | $18–28 | Medium | Affiliate, courses, SaaS | \"Automate. Ship. Profit.\" |\n| Personal finance / FIRE | $22–35 | Medium | Affiliate, coaching, books | \"Money. Freedom. Calm.\" |\n| B2B software reviews | $25–40 | High | Sponsorships, affiliate | \"Software. Reviewed. Honestly.\" |\n| Home lab / self-hosting | $12–20 | Medium | Affiliate, Patreon | \"Own. Your. Data.\" |\n| Career / interview prep | $15–24 | Low | Courses, resume services | \"Land. The. Offer.\" |\n\n*(Demo output — set an ANTHROPIC_API_KEY to generate the live, full 10-niche analysis.)*",
+    identity: "### Channel identity (demo)\n\n**Names:** Zero-to-Scale · The Quiet Engine · Solo Signal · Compound Studio · Nightshift Labs\n\n**Tagline:** *Build the machine that builds your channel.*\n\n**Persona:** 28–40, time-poor operator who wants leverage, not another hobby.\n\n**Pillars:** (1) Systems teardown (2) Tool-of-the-week (3) Case-study builds (4) Mindset / operating cadence.\n\n**Unique mechanism:** the \"one-input, seven-outputs\" content engine — every build becomes a week of content.\n\n*(Demo output — set a key for the live version.)*",
+    roadmap: "### 90-day roadmap (demo)\n\n1. How I Built an AI YouTube Channel in 90 Days\n2. The $0 Faceless Channel Stack\n3. 7 Prompts That Replace a Content Team\n4. Why Posting Daily Is Killing Your Channel\n5. The One-Input, Seven-Output System\n6. I Let Claude Plan 90 Days of Content\n7. Faceless Channel Mistakes That Cost Me Months\n8. The Thumbnail Formula That Doubled My CTR\n9. How to Monetize Before 1,000 Subs\n10. My Exact Upload Schedule (Copy It)\n11. Turning One Video Into a Week of Content\n12. The 90-Day Authority Ladder\n\n*(Demo output — set a key for the full calendar with schedule + SEO/virality priority.)*",
+    script: "### Script (demo)\n\n**[0:00 HOOK]** \"Everyone told you to post daily. That advice is quietly killing new channels — here's the math.\"\n\n**[Agitation]** …\n**[Solution]** …\n**[3 insights]** …\n**[CTA]** …\n**[End-screen tease]** \"Next video: the 7-output content engine.\"\n\n*(Demo output — set a key for the full script.)*",
+    seo: "### SEO + thumbnail package (demo)\n\n**Thumbnails:** (A) shocked-face + \"$0 → $10K\" (B) split before/after (C) big red arrow on a dashboard.\n\n**Title (<60):** I Built an AI YouTube Channel in 90 Days (Real Numbers)\n\n**Tags:** ai youtube, faceless channel, youtube automation, … *(10 total)*\n\n*(Demo output — set a key for the full package.)*",
+    monetization: "### Monetization stack (demo)\n\n**Affiliate:** tool stack you already use · **Digital product:** the exact template pack · **Sponsorship pitch:** 6-line cold template · **Lead magnet:** \"The 7-Prompt Channel Kit\" → email list.\n\n*(Demo output — set a key for the full roadmap.)*",
+    repurpose: "### Repurposing engine (demo)\n\n**X thread** · **LinkedIn post** · **3 Shorts hooks** · **Newsletter** · **Pinterest desc** — all generated from your script.\n\n*(Demo output — set a key for the full multi-platform pack.)*",
+  };
+  return canned[stepKey] || "*(Demo output — set an ANTHROPIC_API_KEY to generate live deliverables.)*";
+}
+
+function send(res, code, obj) {
+  res.writeHead(code, { 'Content-Type': 'application/json' });
+  res.end(JSON.stringify(obj));
+}
+
+const server = http.createServer((req, res) => {
   const u = new URL(req.url, 'http://x');
-  if (u.pathname === '/api/products') {
-    const out = sortProducts(load(), u.searchParams.get('sort') || 'newest');
-    res.writeHead(200, { 'Content-Type': 'application/json' }); return res.end(JSON.stringify({ count: out.length, products: out }));
+
+  if (u.pathname === '/api/config') {
+    return send(res, 200, { liveMode: HAS_KEY, model: MODEL, maxTokens: MAX_TOKENS, rate: rate(MODEL) });
+  }
+  if (u.pathname === '/api/prompts') {
+    return send(res, 200, loadPrompts());
   }
+  if (u.pathname === '/api/generate' && req.method === 'POST') {
+    let raw = ''; req.on('data', c => raw += c);
+    req.on('end', async () => {
+      let body; try { body = JSON.parse(raw || '{}'); } catch { return send(res, 400, { error: 'bad json' }); }
+      const prompt = (body.prompt || '').toString().slice(0, 20000);
+      const stepKey = (body.stepKey || '').toString();
+      if (!prompt) return send(res, 400, { error: 'missing prompt' });
+      const r = rate(MODEL);
+      const est = +(((prompt.length / 4) / 1e6 * r.in) + (MAX_TOKENS / 1e6 * r.out)).toFixed(5);
+      if (!HAS_KEY) {
+        return send(res, 200, { text: demoOutput(stepKey), demo: true, cost: 0, estCost: 0, model: 'demo' });
+      }
+      try {
+        const out = await callAnthropic(prompt);
+        return send(res, 200, { text: out.text, demo: false, usage: out.usage, cost: costOf(out.model, out.usage), estCost: est, model: out.model });
+      } catch (e) {
+        // Graceful fallback to demo so the prototype never hard-fails mid-preview.
+        return send(res, 200, { text: demoOutput(stepKey), demo: true, cost: 0, estCost: est, model: 'demo', warning: 'live call failed: ' + e.message });
+      }
+    });
+    return;
+  }
+
+  // static
   const f = u.pathname === '/' ? 'index.html' : u.pathname.replace(/^\//, '');
   const fp = path.join(DIR, 'public', path.basename(f));
-  if (fs.existsSync(fp)) { res.writeHead(200, { 'Content-Type': f.endsWith('.html') ? 'text/html' : 'text/plain' }); return res.end(fs.readFileSync(fp)); }
+  if (fs.existsSync(fp)) {
+    const ext = path.extname(fp).toLowerCase();
+    const ct = ext === '.html' ? 'text/html' : ext === '.js' ? 'text/javascript' : ext === '.css' ? 'text/css' : ext === '.json' ? 'application/json' : 'text/plain';
+    res.writeHead(200, { 'Content-Type': ct }); return res.end(fs.readFileSync(fp));
+  }
   res.writeHead(404); res.end('not found');
-}).listen(PORT, function () { console.log('[' + path.basename(DIR) + '] http://localhost:' + this.address().port); });
+});
+
+server.listen(PORT, function () {
+  console.log('[studio-zero] http://localhost:' + this.address().port + '  (' + (HAS_KEY ? 'LIVE ' + MODEL : 'DEMO mode — no key') + ')');
+});

← 74d1fd4 initial scaffold (studio-zero) via web-dev accelerator  ·  back to Studio Zero  ·  studio-zero: route live generation through Claude Max plan ( 98efd98 →