[object Object]

← back to Commercialrealestate

SFV CRE viewer + grounded control-chat

e27dd0a3b54d99e94273c7caefc60b70e0d2f12a · 2026-06-21 14:29:31 -0700 · Steve

- Correct Canby 7302-7304 to verified in-place cap 5.96% (7.8% was projected); re-ranks #1->#7
- Shared finance.js (UMD) for live 25%/30%/all-cash scenario recompute in-browser
- analyze.js: Qwen cache (instant re-ranks) + shared math
- Viewer: scenario switcher, max-price filter, verdict/DD banner, docked chat
- Chat: run-notes-chat bridge (free Claude CLI) + action-block protocol so chat drives the grid
- serve.js -> Express; smoke-tested (routes 200, chat backend=cli)

Files touched

Diff

commit e27dd0a3b54d99e94273c7caefc60b70e0d2f12a
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Jun 21 14:29:31 2026 -0700

    SFV CRE viewer + grounded control-chat
    
    - Correct Canby 7302-7304 to verified in-place cap 5.96% (7.8% was projected); re-ranks #1->#7
    - Shared finance.js (UMD) for live 25%/30%/all-cash scenario recompute in-browser
    - analyze.js: Qwen cache (instant re-ranks) + shared math
    - Viewer: scenario switcher, max-price filter, verdict/DD banner, docked chat
    - Chat: run-notes-chat bridge (free Claude CLI) + action-block protocol so chat drives the grid
    - serve.js -> Express; smoke-tested (routes 200, chat backend=cli)
---
 claude-chat-bridge.cjs    | 255 +++++++++++++
 data/listings.json        |  34 ++
 data/qwen-cache.json      | 363 ++++++++++++++++++
 data/ranked.json          | 944 ++++++++++++++++++++++++++++++++++++++++++++++
 package-lock.json         | 827 ++++++++++++++++++++++++++++++++++++++++
 package.json              |   3 +
 public/finance.js         |  62 +++
 public/index.html         | 282 ++++++++++++++
 scripts/analyze.js        |  83 ++++
 scripts/capture-schema.js |  51 +++
 scripts/discover.js       |  87 +++++
 scripts/discover2.js      |  71 ++++
 scripts/scrape-crexi.js   | 114 ++++++
 scripts/serve.js          |  38 ++
 14 files changed, 3214 insertions(+)

diff --git a/claude-chat-bridge.cjs b/claude-chat-bridge.cjs
new file mode 100644
index 0000000..d231415
--- /dev/null
+++ b/claude-chat-bridge.cjs
@@ -0,0 +1,255 @@
+#!/usr/bin/env node
+"use strict";
+// claude-chat-bridge.cjs — drop-in "chat with Claude inline" backend for any Express app.
+//
+// Mounts a single endpoint that takes a chat history + the on-screen run-notes
+// context and returns Claude's reply by calling the Anthropic Messages API.
+// Dependency-free (uses node:https), so it drops into any project without npm.
+//
+// Usage (CommonJS):
+//     const mountClaudeChat = require("./claude-chat-bridge.cjs");
+//     mountClaudeChat(app, { surface: "control-center" });
+//
+// Usage (ESM — Node imports CJS default export fine):
+//     import mountClaudeChat from "./claude-chat-bridge.cjs";
+//     mountClaudeChat(app, { surface: "morning-review" });
+//
+// Two backends:
+//   "cli" (DEFAULT) — shells out to the `claude` CLI in print mode (`claude -p`),
+//                     which authenticates with the logged-in Claude Max/Pro
+//                     subscription. No API key, no per-token billing.
+//   "api"           — calls the Anthropic Messages API with ANTHROPIC_API_KEY
+//                     (metered). Used only as a fallback if the CLI is missing,
+//                     or when forced via opts.backend / CLAUDE_CHAT_BACKEND=api.
+//
+// Adds:
+//   POST <path>            { messages:[{role,content}], context?, system?, model? }
+//                          → { reply, model, backend, usage? }
+//   GET  <path>/health     → { ok, backend, cli: <bool>, key: <bool>, model }
+//
+// API-key resolution (api backend only), at call time, first match wins:
+//   1. process.env.ANTHROPIC_API_KEY
+//   2. ~/Projects/secrets-manager/.env        (canonical — Steve's secrets fan-out)
+//   3. ~/.claude/skills/edges-agent/.env
+//   4. ~/.claude.json  mcp server env blocks
+
+const fs = require("fs");
+const path = require("path");
+const os = require("os");
+const https = require("https");
+const { spawn } = require("child_process");
+
+const HOME = os.homedir();
+const DEFAULT_MODEL = "claude-sonnet-4-6"; // fast for a notes Q&A helper
+const MAX_CONTEXT_CHARS = 16000; // keep the prompt bounded
+const ALLOWED_MODELS = new Set([
+  "claude-sonnet-4-6",
+  "claude-opus-4-8",
+  "claude-haiku-4-5-20251001",
+]);
+
+// Resolve the real `claude` binary (NOT the interactive zsh tmux wrapper).
+function resolveClaudeBin() {
+  const candidates = [
+    process.env.CLAUDE_BIN,
+    path.join(HOME, ".npm-global/bin/claude"),
+    "/opt/homebrew/bin/claude",
+    "/usr/local/bin/claude",
+    path.join(HOME, ".claude/local/claude"),
+  ].filter(Boolean);
+  for (const c of candidates) {
+    try { if (fs.existsSync(c)) return c; } catch {}
+  }
+  return null;
+}
+
+function readKeyFromEnvFile(file) {
+  try {
+    const m = fs.readFileSync(file, "utf8").match(/^ANTHROPIC_API_KEY\s*=\s*(.+)$/m);
+    if (m) return m[1].trim().replace(/^["']|["']$/g, "");
+  } catch {}
+  return null;
+}
+
+function readKeyFromClaudeJson() {
+  try {
+    const j = JSON.parse(fs.readFileSync(path.join(HOME, ".claude.json"), "utf8"));
+    const servers = j.mcpServers || {};
+    for (const name of Object.keys(servers)) {
+      const env = servers[name] && servers[name].env;
+      if (env && typeof env.ANTHROPIC_API_KEY === "string" && env.ANTHROPIC_API_KEY) {
+        return env.ANTHROPIC_API_KEY.trim();
+      }
+    }
+  } catch {}
+  return null;
+}
+
+function anthropicKey() {
+  if (process.env.ANTHROPIC_API_KEY) return process.env.ANTHROPIC_API_KEY.trim();
+  return (
+    readKeyFromEnvFile(path.join(HOME, "Projects/secrets-manager/.env")) ||
+    readKeyFromEnvFile(path.join(HOME, ".claude/skills/edges-agent/.env")) ||
+    readKeyFromClaudeJson() ||
+    null
+  );
+}
+
+function callAnthropic({ key, model, system, messages, maxTokens }) {
+  const body = JSON.stringify({
+    model,
+    max_tokens: maxTokens || 1024,
+    system,
+    messages,
+  });
+  const opts = {
+    hostname: "api.anthropic.com",
+    path: "/v1/messages",
+    method: "POST",
+    headers: {
+      "x-api-key": key,
+      "anthropic-version": "2023-06-01",
+      "content-type": "application/json",
+      "content-length": Buffer.byteLength(body),
+    },
+  };
+  return new Promise((resolve, reject) => {
+    const req = https.request(opts, (resp) => {
+      let buf = "";
+      resp.on("data", (c) => (buf += c));
+      resp.on("end", () => {
+        let json;
+        try { json = JSON.parse(buf); } catch { return reject(new Error("bad JSON from Anthropic: " + buf.slice(0, 300))); }
+        if (resp.statusCode !== 200) {
+          const msg = (json && json.error && json.error.message) || buf.slice(0, 300);
+          return reject(new Error("Anthropic " + resp.statusCode + ": " + msg));
+        }
+        const reply = (json.content || [])
+          .filter((b) => b.type === "text")
+          .map((b) => b.text)
+          .join("")
+          .trim();
+        resolve({ reply, usage: json.usage || null, model: json.model || model });
+      });
+    });
+    req.on("error", reject);
+    req.setTimeout(60000, () => req.destroy(new Error("Anthropic request timed out")));
+    req.write(body);
+    req.end();
+  });
+}
+
+// CLI backend — `claude -p` against the Max subscription. Conversation history
+// is flattened into a single prompt (each call is stateless); the run-notes
+// context + behavior live in --append-system-prompt.
+function callClaudeCLI({ bin, model, system, messages }) {
+  const prompt = messages
+    .map((m) => (m.role === "assistant" ? "Assistant: " : "User: ") + m.content)
+    .join("\n\n");
+  const args = ["-p", "--model", model, "--append-system-prompt", system];
+  return new Promise((resolve, reject) => {
+    const child = spawn(bin, args, {
+      cwd: os.tmpdir(), // neutral cwd: don't load a project CLAUDE.md
+      env: { ...process.env },
+      stdio: ["pipe", "pipe", "pipe"],
+    });
+    let out = "", err = "";
+    const killer = setTimeout(() => child.kill("SIGKILL"), 90000);
+    child.stdout.on("data", (c) => (out += c));
+    child.stderr.on("data", (c) => (err += c));
+    child.on("error", (e) => { clearTimeout(killer); reject(e); });
+    child.on("close", (code) => {
+      clearTimeout(killer);
+      if (code !== 0) return reject(new Error("claude CLI exited " + code + ": " + (err || out).slice(0, 300)));
+      const reply = out.trim();
+      if (!reply) return reject(new Error("claude CLI returned empty output" + (err ? " (stderr: " + err.slice(0, 200) + ")" : "")));
+      resolve({ reply, model, backend: "cli", usage: null });
+    });
+    child.stdin.end(prompt);
+  });
+}
+
+function buildSystem({ surface, context, extra }) {
+  const ctx = String(context || "").slice(0, MAX_CONTEXT_CHARS);
+  return [
+    `You are Claude, embedded as an inline assistant inside "${surface || "a run-notes viewer"}".`,
+    `The user is reading the run notes / process output shown below and may ask follow-up questions about it.`,
+    `Answer concisely and specifically, grounded in the notes. If something isn't in the notes, say so rather than guessing.`,
+    `Use short paragraphs and bullet lists. You can use markdown. Do NOT invent file paths, commands, or results that aren't supported by the notes.`,
+    extra ? `\nAdditional instructions:\n${extra}` : "",
+    ctx ? `\n--- RUN NOTES IN VIEW ---\n${ctx}\n--- END RUN NOTES ---` : `\n(No run-notes context was provided.)`,
+  ].filter(Boolean).join("\n");
+}
+
+/**
+ * Mount the inline-chat endpoint onto an Express app.
+ * @param {import('express').Express} app
+ * @param {object} [opts]
+ * @param {string} [opts.path="/api/claude-chat"]
+ * @param {string} [opts.surface]      Human label for the surface (used in the system prompt).
+ * @param {string} [opts.defaultModel] Override the default model.
+ * @param {string} [opts.systemExtra]  Extra system-prompt instructions for this surface.
+ */
+function mountClaudeChat(app, opts = {}) {
+  const route = opts.path || "/api/claude-chat";
+  const surface = opts.surface || "run-notes viewer";
+  const fallbackModel = opts.defaultModel || DEFAULT_MODEL;
+  // Backend: opts.backend → CLAUDE_CHAT_BACKEND env → "cli" (Max subscription).
+  const wantBackend = (opts.backend || process.env.CLAUDE_CHAT_BACKEND || "cli").toLowerCase();
+
+  function pickBackend() {
+    const bin = resolveClaudeBin();
+    if (wantBackend === "api") return { backend: "api", bin, key: anthropicKey() };
+    // default "cli": use it if the binary is present, else fall back to api.
+    if (bin) return { backend: "cli", bin, key: null };
+    return { backend: "api", bin: null, key: anthropicKey() };
+  }
+
+  app.get(route + "/health", (_req, res) => {
+    const bin = resolveClaudeBin();
+    const sel = pickBackend();
+    res.json({ ok: true, backend: sel.backend, cli: !!bin, key: !!anthropicKey(), model: fallbackModel, surface });
+  });
+
+  app.post(route, async (req, res) => {
+    const { messages, context, system, model } = req.body || {};
+    if (!Array.isArray(messages) || messages.length === 0) {
+      return res.status(400).json({ error: "messages[] required" });
+    }
+    // Sanitize incoming turns to {role, content} with role in user|assistant.
+    const clean = [];
+    for (const m of messages) {
+      const role = m && m.role === "assistant" ? "assistant" : "user";
+      const content = typeof m?.content === "string" ? m.content : String(m?.content ?? "");
+      if (content.trim()) clean.push({ role, content: content.slice(0, 8000) });
+    }
+    if (!clean.length) return res.status(400).json({ error: "no usable message content" });
+    if (clean[clean.length - 1].role !== "user") {
+      return res.status(400).json({ error: "last message must be from the user" });
+    }
+    const useModel = ALLOWED_MODELS.has(model) ? model : fallbackModel;
+    const sys = buildSystem({ surface, context, extra: typeof system === "string" ? system : opts.systemExtra });
+    const sel = pickBackend();
+    try {
+      if (sel.backend === "cli") {
+        const out = await callClaudeCLI({ bin: sel.bin, model: useModel, system: sys, messages: clean });
+        return res.json({ reply: out.reply, model: out.model, backend: "cli" });
+      }
+      if (!sel.key) {
+        return res.status(503).json({
+          error: "No Claude backend available: `claude` CLI not found and ANTHROPIC_API_KEY not set. Install Claude Code (Max login) or add a key via the secrets skill.",
+        });
+      }
+      const out = await callAnthropic({ key: sel.key, model: useModel, system: sys, messages: clean });
+      res.json({ reply: out.reply, model: out.model, backend: "api", usage: out.usage });
+    } catch (e) {
+      res.status(502).json({ error: String(e.message || e), backend: sel.backend });
+    }
+  });
+
+  return app;
+}
+
+mountClaudeChat.anthropicKey = anthropicKey;
+mountClaudeChat.DEFAULT_MODEL = DEFAULT_MODEL;
+module.exports = mountClaudeChat;
diff --git a/data/listings.json b/data/listings.json
new file mode 100644
index 0000000..18eced9
--- /dev/null
+++ b/data/listings.json
@@ -0,0 +1,34 @@
+{
+  "meta": {
+    "market": "San Fernando Valley, CA",
+    "asset_classes": ["Multifamily", "Retail/Net-lease", "Mixed-use"],
+    "budget_frame": "leveraged — $400,000 available as down payment + closing",
+    "sourced": "2026-06-21 via Exa web search (LoopNet/Compass/Crexi/broker sites)",
+    "caveats": [
+      "Cap rates are BROKER-STATED (often in-place or pro-forma optimistic); independently verify NOI, rent rolls, and expenses in due diligence.",
+      "Status reflects what the source page showed when scraped; confirm availability before acting.",
+      "Listings without a disclosed cap rate have NOI marked null — financial return is computed only where NOI is known."
+    ]
+  },
+  "listings": [
+    {"id": "victory13350", "address": "13350 Victory Blvd", "city": "Van Nuys", "zip": "91401", "type": "Multifamily", "price": 1299995, "units": 5, "sqft": 3468, "cap_rate": 4.49, "year_built": 1951, "rent_control": "Not on LA RSO (per listing)", "status": "Active", "upside_note": "~30% rent upside; 5 garage spaces add revenue; copper plumbing partial", "source": "https://www.compass.com/homedetails/13350-Victory-Blvd-Van-Nuys-CA-91401/1J7A3S_pid/"},
+    {"id": "sepulveda8144", "address": "8144 Sepulveda Pl", "city": "Panorama City", "zip": "91402", "type": "Multifamily", "price": 1400000, "units": 6, "sqft": 5418, "cap_rate": 5.00, "year_built": 1952, "rent_control": "LA RSO (pre-1978) — assume rent controlled", "status": "Active", "upside_note": "All 2bd/1ba unit mix; tremendous upside; newer roof, copper plumbing, owned laundry", "source": "https://www.compass.com/homedetails/8144-Sepulveda-Pl-Panorama-City-CA-91402/1IJRNO_pid/"},
+    {"id": "canby7260", "address": "7260 Canby Ave", "city": "Reseda", "zip": "91335", "type": "Multifamily", "price": 1425000, "units": 6, "sqft": 5108, "cap_rate": 4.75, "year_built": 1954, "rent_control": "LAR2; pre-1978 likely RSO", "status": "Active", "upside_note": "RTI plans for 2 ADUs -> projected 7.08% cap, 7.60% pre-tax CoC post-ADU; $272/sf post-ADU; Walk Score 86 near CSUN", "source": "https://www.loopnet.com/Listing/7260-Canby-Ave-Reseda-CA/34975971/"},
+    {"id": "canby7304", "address": "7302-7304 Canby Ave", "city": "Reseda", "zip": "91335", "type": "Mixed-use", "price": 1250000, "units": 5, "sqft": 5048, "cap_rate": 5.96, "cap_rate_projected": 7.80, "noi_actual": 74497, "gross_actual": 112200, "expenses": 37703, "verified": true, "year_built": 1926, "rent_control": "C2 (LAC2) mixed commercial+residential", "status": "Active", "upside_note": "VERIFIED (MLS #26654413): actual in-place cap ~5.96% (NOI $74,497 on $112,200 gross, $37,703 exp) — the 7.8% headline is PROJECTED, achieved mainly by doubling the commercial unit $2,000->$4,000/mo + bumping the 3bd. SELLER FINANCING confirmed. Price CUT $1,399,000->$1,250,000 (motivated seller). Occupancy ambiguous (listing desc says 3 occupied vs MLS 0% vacancy — verify rent roll). C2 development optionality.", "source": "https://www.compass.com/homedetails/7304-Canby-Ave-Reseda-CA-91335/1IO7AS_pid/"},
+    {"id": "schoenborn17920", "address": "17920 Schoenborn St", "city": "Northridge", "zip": "91325", "type": "Multifamily", "price": 1395000, "units": 6, "sqft": 4962, "cap_rate": null, "year_built": 1959, "rent_control": "LAR3; pre-1978 RSO", "status": "Active", "upside_note": "Cap not disclosed; live-in-one play near CSUN/Northridge Hospital; classic value-add", "source": "https://www.compass.com/homedetails/17920-Schoenborn-St-Northridge-CA-91325/1JVU5S_pid/"},
+    {"id": "amigo8357", "address": "8357 Amigo Ave", "city": "Northridge", "zip": "91324", "type": "Multifamily", "price": 1249000, "units": 4, "sqft": 3816, "cap_rate": null, "year_built": 1960, "rent_control": "LAR3", "status": "Active", "upside_note": "Cap not disclosed; reduced from $1,350,000 (price cut signals motivated seller); quad near CSUN", "source": "https://www.compass.com/homedetails/8357-Amigo-Ave-Northridge-CA-91324/1IMLRO_pid/"},
+    {"id": "wilbur9014", "address": "9014 Wilbur Ave", "city": "Northridge", "zip": "91324", "type": "Multifamily", "price": 1700000, "units": 4, "sqft": 3211, "cap_rate": null, "year_built": 1980, "rent_control": "Built 1980 -> NOT LA RSO; AB1482 only", "status": "Active", "upside_note": "Cap not disclosed; 1980 build = no LA rent control (higher increases); remodeled + new-construction units; adjacent 2-unit also available", "source": "https://www.compass.com/homedetails/9014-Wilbur-Ave-Northridge-CA-91324/1KTV77_pid/"},
+    {"id": "tilden8843", "address": "8843-8845 Tilden Ave", "city": "Panorama City", "zip": "91402", "type": "Multifamily", "price": 1050000, "units": 3, "sqft": 2076, "cap_rate": null, "year_built": 1948, "rent_control": "Pre-1978 -> LA RSO", "status": "Active", "upside_note": "Cap not disclosed; $350k/unit; individually metered; owner-user/house-hack friendly", "source": "https://www.coldwellbankerhomes.com/ca/panorama-city/8843-tilden/pid_69954470/"},
+    {"id": "tilden8837", "address": "8837 Tilden Ave", "city": "Panorama City", "zip": "91402", "type": "Multifamily", "price": 899000, "units": 2, "sqft": 1794, "cap_rate": 5.30, "year_built": 1948, "rent_control": "Pre-1978 -> LA RSO", "status": "Active", "upside_note": "Duplex; small entry point; on market 51 days", "source": "https://www.compass.com/homedetails/8837-Tilden-Ave-Panorama-City-CA-91402/1JQEEP_pid/"},
+    {"id": "sanfernando10004", "address": "10004 San Fernando Rd", "city": "Pacoima", "zip": "91331", "type": "Mixed-use", "price": 1100000, "units": 3, "sqft": 2270, "cap_rate": null, "year_built": 1933, "rent_control": "Zoned LAC2 (residential/commercial)", "status": "Active", "upside_note": "Cap not disclosed; triplex on commercial-zoned lot, 9,155sf land; SOLD AS-IS (condition risk); redevelopment optionality", "source": "https://www.compass.com/homedetails/10004-San-Fernando-Rd-Pacoima-CA-91331/1J7S8M_pid/"},
+    {"id": "vannuys12872", "address": "12872 Van Nuys Blvd", "city": "Pacoima", "zip": "91331", "type": "Multifamily", "price": 949900, "units": 3, "sqft": 2084, "cap_rate": null, "year_built": 1948, "rent_control": "Pre-1978 -> LA RSO", "status": "Active (one source showed Pending — VERIFY)", "upside_note": "Cap not disclosed; renovated triplex, new roof/windows/plumbing; STATUS UNCERTAIN", "source": "https://www.compass.com/homedetails/12872-Van-Nuys-Blvd-Pacoima-CA-91331/1L7DX2_pid/"},
+    {"id": "canby7304b_dup", "address": "21627 Sherman Way", "city": "Canoga Park", "zip": "91303", "type": "Retail", "price": 899998, "units": 1, "sqft": 1500, "cap_rate": null, "year_built": 1941, "rent_control": "Commercial — n/a", "status": "Active", "upside_note": "Owner-user retail storefront in Canoga Park Parts District; Opportunity Zone + JEDI Zone incentives; SBA 504 ~10% down; new roof/HVAC 2022. No income in place (owner-user).", "source": "https://www.coldwellbankerhomes.com/ca/canoga-park/21627-sherman-way/pid_69748281/"},
+    {"id": "owensmouth7129", "address": "7129-7131 1/2 Owensmouth Ave", "city": "Canoga Park", "zip": "91303", "type": "Retail", "price": 1050000, "units": 1, "sqft": 1482, "cap_rate": null, "year_built": 1970, "rent_control": "Commercial — n/a", "status": "Active", "upside_note": "Cap not disclosed; small freestanding retail near Warner Center", "source": "https://www.loopnet.com/Listing/7129-7131-1-2-Owensmouth-Ave-Canoga-Park-CA/35430257/"},
+    {"id": "comercio5301", "address": "5301 Comercio Lane", "city": "Woodland Hills", "zip": "91364", "type": "Mixed-use", "price": 814250, "units": 4, "sqft": 3473, "cap_rate": null, "year_built": 1956, "rent_control": "LAC4 (office/retail mixed)", "status": "Pending (probate/court-confirmation)", "upside_note": "Cap not disclosed; probate trust sale, court-confirmation required (slow/uncertain close); LAC4 flexible use near Ventura Blvd; lowest entry price", "source": "https://www.nuevo-realestate.com/real-estate/5301-comercio-lane-woodland-hills-ca-91364/25577103/183735124"},
+    {"id": "vanowen15840", "address": "15840 Vanowen St", "city": "Lake Balboa", "zip": "91406", "type": "Multifamily", "price": 970000, "units": 5, "sqft": 3349, "cap_rate": 5.25, "year_built": 1952, "rent_control": "Pre-1978 -> LA RSO", "status": "Under Contract", "upside_note": "5 units, new roof 2022, copper plumbing, tremendous upside — BUT already in escrow (likely unavailable)", "source": "https://www.loopnet.com/Listing/15840-Vanowen-St-Lake-Balboa-CA/35415601/"},
+    {"id": "canby7454", "address": "7454 Canby Ave", "city": "Reseda", "zip": "91335", "type": "Multifamily", "price": 1749000, "units": 9, "sqft": 6784, "cap_rate": null, "year_built": 1962, "rent_control": "Pre-1978 -> LA RSO", "status": "Active Under Contract", "upside_note": "9 units at $194k/unit, diverse mix — but in escrow; back-up only", "source": "https://www.remax.com/ca/reseda/home-details/7454-canby-ave-reseda-ca-91335/13235972936323110093/M00000093/SR26009655"},
+    {"id": "correnti12600", "address": "12600 Correnti St", "city": "Pacoima", "zip": "91331", "type": "Multifamily", "price": 3300000, "units": 14, "sqft": 5334, "cap_rate": 7.50, "year_built": null, "rent_control": "Renovated; verify RSO", "status": "Active (OUT OF BUDGET — context)", "upside_note": "ASSUMABLE $2.5M loan at 3.9% fixed thru 09/2027 -> only ~25% (~$825k) down but 12.2% cash-on-cash on current; plans to add 20 units -> proforma 13.9% cap. Exceptional economics but down payment EXCEEDS $400k.", "source": "https://www.compass.com/homedetails/12600-Correnti-St-Pacoima-CA-91331/27H0FQ_pid/"},
+    {"id": "burbank10701", "address": "10701 Burbank Blvd", "city": "North Hollywood", "zip": "91601", "type": "Retail/NNN", "price": 2799000, "units": 1, "sqft": 1802, "cap_rate": 7.30, "year_built": 1951, "rent_control": "Commercial NNN — n/a", "status": "Active (OUT OF BUDGET — context)", "upside_note": "Single-tenant NNN cannabis dispensary, $204k NOI, 10-yr lease 2% bumps, fully passive. High cap but cannabis-tenant risk + needs ~$700k down.", "source": "https://www.jayrubin-re.com/file/view-file/580"},
+    {"id": "canoga6020", "address": "6020 Canoga Ave", "city": "Woodland Hills", "zip": "91367", "type": "Retail/NNN", "price": 5500000, "units": 2, "sqft": 4896, "cap_rate": 6.35, "year_built": null, "rent_control": "Commercial NNN — n/a", "status": "Active (OUT OF BUDGET — context)", "upside_note": "2-tenant NNN (sushi + credit union) in Warner Center across from future Rams HQ; 18.2yr avg occupancy; trophy passive asset but far above budget.", "source": "https://www.brandonmichaelsgroup.com/6020canogaave"}
+  ]
+}
diff --git a/data/qwen-cache.json b/data/qwen-cache.json
new file mode 100644
index 0000000..1849dab
--- /dev/null
+++ b/data/qwen-cache.json
@@ -0,0 +1,363 @@
+{
+  "victory13350": {
+    "key": "e80609d29381",
+    "qwen": {
+      "thesis": "This 5-unit multifamily property in Van Nuys offers an opportunity to acquire a value-add asset with potential for rent growth and operational improvements. With a 4.49% cap rate and 30% rent upside, the property is undervalued relative to its potential, especially with the added revenue from 5 garage spaces and a solid location in the San Fernando Valley.",
+      "risks": [
+        "Low cash-on-cash return (-4.9%) indicates negative initial cash flow",
+        "Partial copper plumbing may require future upgrades",
+        "DSCR of 0.77 suggests high debt service risk"
+      ],
+      "upside": [
+        "Significant rent increase potential due to 30% upside",
+        "Garage spaces provide additional income streams",
+        "Location in a stable, growing submarket with strong demand for multifamily housing"
+      ],
+      "opportunity_score": 55,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 55
+  },
+  "sepulveda8144": {
+    "key": "0300de8b74cf",
+    "qwen": {
+      "thesis": "This 6-unit multifamily property in Panorama City offers a compelling opportunity for a leveraged buyer with $400k available, given its strong cap rate, rent-controlled status, and potential for value-add through rent increases and unit upgrades. The property's newer roof, copper plumbing, and owned laundry provide a solid foundation for long-term appreciation and cash flow improvement.",
+      "risks": [
+        "Low cash-on-cash return (-3.04%) due to high leverage",
+        "Rent control may limit immediate rent growth potential",
+        "Age of the building (1952) may lead to higher maintenance costs over time"
+      ],
+      "upside": [
+        "Significant upside through value-add strategies such as unit renovations and rent increases",
+        "Strong location in a growing area of the San Fernando Valley",
+        "Potential for long-term appreciation due to limited supply of rent-controlled multifamily units"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "canby7260": {
+    "key": "8ada99d0f1e2",
+    "qwen": {
+      "thesis": "This 6-unit multifamily property in Reseda offers a leveraged opportunity with strong location near CSUN and a high Walk Score, along with potential for value-add through planned ADUs. The current cap rate of 4.75% is competitive, and projected improvements could significantly enhance returns.",
+      "risks": [
+        "Low cash-on-cash return (-3.95%) indicates negative initial cash flow",
+        "Dependence on ADU approvals and construction timelines",
+        "Rent control (LAR2) may limit rental growth potential"
+      ],
+      "upside": [
+        "Planned ADUs could increase cap rate to 7.08% and improve cash-on-cash return",
+        "High Walk Score and proximity to CSUN suggest strong demand and long-term appreciation potential",
+        "Potential for value-add through unit upgrades and operational improvements"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "canby7304": {
+    "key": "b7e8af1a38b6",
+    "qwen": {
+      "thesis": "This mixed-use property in Reseda offers a value-add opportunity with a verified in-place cap rate of 5.96% and potential to increase to 7.8% through rent increases and commercial unit optimization. The motivated seller, seller financing, and development optionality under C2 zoning make it an attractive leveraged play for a $400k-down buyer.",
+      "risks": [
+        "Occupancy status is unclear and needs verification",
+        "Rent control restrictions may limit rent increases",
+        "Value-add projections depend on successful execution of tenant improvements and re-leasing"
+      ],
+      "upside": [
+        "Significant NOI growth potential through rent increases and commercial unit optimization",
+        "Seller financing reduces upfront cash needs",
+        "C2 zoning allows for future development or repositioning"
+      ],
+      "opportunity_score": 72,
+      "recommendation": "Buy"
+    },
+    "qwenScore": 72
+  },
+  "schoenborn17920": {
+    "key": "2b8ddd604cde",
+    "qwen": {
+      "thesis": "This 6-unit multifamily property in Northridge offers a value-add opportunity with strong location near CSUN and Northridge Hospital, and potential for rent growth due to rent control exemptions. With a $400k budget, the buyer can secure a leveraged position in a desirable area with room for operational improvements.",
+      "risks": [
+        "Rent control restrictions may limit upside potential",
+        "Age of the property (1959) may require significant capital expenditures",
+        "NOI and cap rate are unknown, increasing uncertainty in valuation"
+      ],
+      "upside": [
+        "Location proximity to major institutions increases long-term value",
+        "Potential for rent increases under LAR3 and RSO exemptions",
+        "Classic value-add opportunity with room for renovations and management improvements"
+      ],
+      "opportunity_score": 72,
+      "recommendation": "Buy"
+    },
+    "qwenScore": 72
+  },
+  "amigo8357": {
+    "key": "d01b2635a2ca",
+    "qwen": {
+      "thesis": "This multifamily property in Northridge offers a leveraged opportunity with a motivated seller and a prime location near CSUN, suggesting potential for rental income growth and value appreciation. The 25% down payment aligns with the investor's $400k budget, allowing for a purchase with room for closing costs and reserves.",
+      "risks": [
+        "LAR3 rent control may limit rental income growth potential",
+        "Age of the property (1960) may indicate higher maintenance costs",
+        "Cap rate is not disclosed, making it difficult to assess current returns"
+      ],
+      "upside": [
+        "Proximity to CSUN increases demand for rental units",
+        "Price reduction indicates seller motivation, potentially leading to a favorable purchase price",
+        "Potential for value-add through renovations or improved management"
+      ],
+      "opportunity_score": 72,
+      "recommendation": "Buy"
+    },
+    "qwenScore": 72
+  },
+  "wilbur9014": {
+    "key": "ef795442d4fb",
+    "qwen": {
+      "thesis": "This 4-unit multifamily property in Northridge offers an opportunity to purchase a remodeled and upgraded asset in a desirable area with no rent control restrictions, allowing for higher rental income potential. While the asking price exceeds the $400k budget for a 25% down purchase, the adjacent 2-unit property may provide a bundled acquisition opportunity that could enhance overall returns.",
+      "risks": [
+        "Insufficient down payment for a 25% LTV purchase without additional financing or budget adjustments",
+        "No disclosed cap rate limits ability to estimate returns and compare to market benchmarks",
+        "Adjacent 2-unit property availability may complicate due diligence and increase complexity of the deal"
+      ],
+      "upside": [
+        "No rent control in Northridge (built 1980) allows for higher rent increases and greater NOI potential",
+        "Remodeled and new-construction units may contribute to higher occupancy and rental rates",
+        "Potential for bundled acquisition with adjacent 2-unit property may offer scale and diversification"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "tilden8843": {
+    "key": "0afa37a6fc19",
+    "qwen": {
+      "thesis": "This multifamily property in Panorama City offers a leveraged opportunity for a buyer with a $400k budget, allowing for a 25% down payment and potential owner-user or house-hack strategy. The individually metered units and pre-1978 rent control status may provide long-term stability and income potential in a growing market.",
+      "risks": [
+        "Pre-1978 rent control may limit rental income growth",
+        "Age of the property (1948) may imply higher maintenance and repair costs",
+        "Cap rate is not disclosed, making it difficult to assess current returns"
+      ],
+      "upside": [
+        "Potential for owner-user or house-hack strategy to reduce expenses",
+        "Individually metered units may allow for better tenant screening and revenue management",
+        "Location in Panorama City may benefit from ongoing neighborhood improvements and demand for multifamily housing"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "tilden8837": {
+    "key": "dd3195a2a468",
+    "qwen": {
+      "thesis": "This duplex in Panorama City offers a leveraged entry point into the San Fernando Valley multifamily market with a verified 5.3% cap rate and a low asking price relative to its size. With a $400k budget, the investor can secure a fully funded purchase with room for improvement, positioning for potential value-add through rent increases and property upgrades.",
+      "risks": [
+        "Low cash-on-cash return (-1.95%) indicates poor initial profitability",
+        "DSCR of 0.91 suggests potential cash flow stress",
+        "Rent control (LA RSO) limits upside in rental income"
+      ],
+      "upside": [
+        "Potential for value-add through renovations and tenant improvements",
+        "Opportunity to capitalize on long-term appreciation in the Panorama City market",
+        "Leverage available to scale returns with future equity injections"
+      ],
+      "opportunity_score": 55,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 55
+  },
+  "sanfernando10004": {
+    "key": "9ba5e1dc1c16",
+    "qwen": {
+      "thesis": "This triplex on a large commercial-zoned lot in Pacoima offers value-add potential through redevelopment or rent growth, with a low down payment requirement and a budget-friendly entry point for a leveraged buyer. The property's size, land value, and zoning flexibility create opportunities for long-term appreciation and income diversification.",
+      "risks": [
+        "Significant condition risk due to 'as-is' sale and age of the building (1933)",
+        "Uncertain cap rate and NOI due to lack of financial disclosure",
+        "Redevelopment may require permits, approvals, and time, increasing complexity and cost"
+      ],
+      "upside": [
+        "Potential for redevelopment on the 9,155 sqft lot, which could significantly increase value",
+        "Zoning flexibility (LAC2) allows for mixed-use opportunities",
+        "Leverage-friendly purchase with 25% down and manageable cash outlay"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "vannuys12872": {
+    "key": "21389313b5cf",
+    "qwen": {
+      "thesis": "This renovated triplex in Pacoima offers a leveraged opportunity for a buyer with $400k available for down payment and closing costs. With a 25% down payment, the cash needed is within budget, and the property's recent upgrades (roof, windows, plumbing) may support competitive rental rates in a rent-controlled market.",
+      "risks": [
+        "Uncertain property status (active vs. pending) could affect transaction timing and terms",
+        "Rent control under LA RSO may limit rental income growth potential",
+        "Age of the building (1948) may imply hidden maintenance or compliance costs"
+      ],
+      "upside": [
+        "Recent renovations may allow for higher occupancy and rental rates",
+        "Strategic location in Pacoima may benefit from area revitalization",
+        "Leverage available with 25% down payment to maximize returns"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "canby7304b_dup": {
+    "key": "cf1e1540754a",
+    "qwen": {
+      "thesis": "This retail property in Canoga Park offers a unique opportunity for a leveraged buyer with $400k available for down payment and closing costs. Located in a revitalizing area with Opportunity Zone and JEDI Zone incentives, the property benefits from potential tax advantages and future growth. With recent improvements (new roof and HVAC) and a prime location in the Parts District, the asset is well-positioned for value-add strategies such as retenanting or repositioning.",
+      "risks": [
+        "No current income in place increases uncertainty around future cash flow potential",
+        "Vacant retail properties in competitive markets may face challenges in attracting quality tenants",
+        "Age of the building (1941) may imply higher maintenance costs or outdated infrastructure"
+      ],
+      "upside": [
+        "Opportunity Zone and JEDI Zone incentives may provide tax benefits and attract development interest",
+        "Potential for retenanting at higher rates or repositioning the space for a more profitable use",
+        "SBA 504 financing option offers favorable terms with ~10% down, reducing capital requirements"
+      ],
+      "opportunity_score": 72,
+      "recommendation": "Buy"
+    },
+    "qwenScore": 72
+  },
+  "owensmouth7129": {
+    "key": "8bc39f075989",
+    "qwen": {
+      "thesis": "This freestanding retail property in Canoga Park offers an opportunity to acquire a small, income-producing asset near the Warner Center, a high-traffic area. With a $400k budget covering down payment and closing costs, the investor can leverage the remaining funds to secure a position in a growing retail market with potential for rent growth and value appreciation.",
+      "risks": [
+        "Location near Warner Center may face competition from larger retail tenants",
+        "No cap rate disclosed limits ability to assess current returns",
+        "Small size may limit scalability or tenant diversity"
+      ],
+      "upside": [
+        "Proximity to Warner Center provides strong foot traffic potential",
+        "Freestanding retail may allow for flexible use or repositioning",
+        "Potential for long-term appreciation in a desirable submarket"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "comercio5301": {
+    "key": "3da352658e14",
+    "qwen": {
+      "thesis": "This mixed-use property in Woodland Hills offers a unique opportunity for a leveraged buyer with $400k available for down and closing, given its low entry price relative to comparable assets in the area. Its flexible LAC4 zoning and proximity to Ventura Blvd present potential for value-add through repositioning or multi-family conversion.",
+      "risks": [
+        "Probate sale with slow/uncertain closing timeline",
+        "No cap rate disclosed, limiting income projection analysis",
+        "Age and condition of the 1956-built structure may require significant renovation"
+      ],
+      "upside": [
+        "Flexible LAC4 zoning allows for office, retail, or mixed-use development",
+        "Lowest entry price in the area suggests potential for appreciation",
+        "Strategic location near Ventura Blvd offers strong traffic and visibility"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "vanowen15840": {
+    "key": "3622605fc9f3",
+    "qwen": {
+      "thesis": "This 5-unit multifamily property in Lake Balboa offers a compelling opportunity with recent upgrades, strong cap rate, and potential for value-add through rent increases and unit improvements. With a $400k budget, the investor can secure a leveraged position with manageable cash outlay and upside potential.",
+      "risks": [
+        "Already in escrow, reducing availability and control",
+        "Rent control restrictions may limit rental growth potential",
+        "Low cash-on-cash return and DSCR below 1.0 indicate financial stress under current assumptions"
+      ],
+      "upside": [
+        "Recent improvements (new roof, copper plumbing) provide a solid foundation for value-add",
+        "Potential for rent increases due to location and market demand",
+        "Opportunity to reposition units for higher income streams"
+      ],
+      "opportunity_score": 60,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 60
+  },
+  "canby7454": {
+    "key": "c2018bd68ab3",
+    "qwen": {
+      "thesis": "This 9-unit multifamily property in Reseda offers a potential opportunity for a value-add investor with a $400k budget, leveraging the existing escrow status and the diverse unit mix. However, the required down payment exceeds the available budget, requiring creative financing or a reduction in purchase price to make the deal viable.",
+      "risks": [
+        "Insufficient down payment to meet the $480,975 requirement, potentially limiting financing options or requiring higher interest rates.",
+        "Rent control regulations (LA RSO) may limit rental income growth and increase operational complexity.",
+        "Uncertainty around the property's current NOI and cap rate, which are critical for evaluating long-term returns."
+      ],
+      "upside": [
+        "Diverse unit mix may allow for targeted renovations and rent increases on non-rent-controlled units.",
+        "Location in Reseda offers potential for long-term appreciation and stable cash flow in a growing market.",
+        "Existing escrow status may provide a window to negotiate terms or secure a backup position."
+      ],
+      "opportunity_score": 55,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 55
+  },
+  "correnti12600": {
+    "key": "246be61dfcb2",
+    "qwen": {
+      "thesis": "This multifamily property in Pacoima offers strong long-term value with a verified 7.5% cap rate and potential for significant upside through unit expansion. While the current cash-on-cash return is lower due to the high down payment requirement, the proforma cap rate of 13.9% and assumable loan terms present a compelling opportunity for a buyer with a $400k budget if alternative financing or value-add strategies can be implemented.",
+      "risks": [
+        "Down payment requirement exceeds budget, limiting immediate purchase feasibility",
+        "Need to verify RSO status and rent control implications",
+        "Proforma projections depend on successful unit expansion and market absorption"
+      ],
+      "upside": [
+        "Assumable loan at 3.9% provides favorable financing terms",
+        "Potential for 13.9% proforma cap rate with unit addition",
+        "Strong location in growing San Fernando Valley market"
+      ],
+      "opportunity_score": 65,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 65
+  },
+  "burbank10701": {
+    "key": "e3d0e11a30ec",
+    "qwen": {
+      "thesis": "This single-tenant NNN cannabis dispensary offers a high cap rate of 7.3% with a 10-year lease and 2% annual rent bumps, providing stable cash flow and long-term income potential. However, the required down payment exceeds the $400k budget, limiting leverage and cash-on-cash return.",
+      "risks": [
+        "Cannabis industry regulatory and legal risks",
+        "Tenant-specific risk due to single-tenant exposure",
+        "Down payment shortfall may require additional financing or reduce leverage"
+      ],
+      "upside": [
+        "Attractive cap rate and long-term lease with rent bumps",
+        "Fully passive management with NNN structure",
+        "Potential for appreciation in a growing retail market"
+      ],
+      "opportunity_score": 55,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 55
+  },
+  "canoga6020": {
+    "key": "4242ef5a24ea",
+    "qwen": {
+      "thesis": "This trophy retail asset in Warner Center offers long-term stability with two strong NNN tenants and proximity to future Rams HQ, but the $400k budget is insufficient for a 25% down purchase, requiring significant additional capital or alternative financing.",
+      "risks": [
+        "Insufficient down payment to meet 25% requirement without additional financing",
+        "High leverage may increase sensitivity to interest rate changes",
+        "Tenant concentration risk with only two tenants"
+      ],
+      "upside": [
+        "Strong location with future development potential near Rams HQ",
+        "Long-term in-place cap rate of 6.35% with stable NNN tenants",
+        "Potential for value appreciation in Warner Center over time"
+      ],
+      "opportunity_score": 55,
+      "recommendation": "Watch"
+    },
+    "qwenScore": 55
+  }
+}
\ No newline at end of file
diff --git a/data/ranked.json b/data/ranked.json
new file mode 100644
index 0000000..bc28f1e
--- /dev/null
+++ b/data/ranked.json
@@ -0,0 +1,944 @@
+{
+  "meta": {
+    "market": "San Fernando Valley, CA",
+    "asset_classes": [
+      "Multifamily",
+      "Retail/Net-lease",
+      "Mixed-use"
+    ],
+    "budget_frame": "leveraged — $400,000 available as down payment + closing",
+    "sourced": "2026-06-21 via Exa web search (LoopNet/Compass/Crexi/broker sites)",
+    "caveats": [
+      "Cap rates are BROKER-STATED (often in-place or pro-forma optimistic); independently verify NOI, rent rolls, and expenses in due diligence.",
+      "Status reflects what the source page showed when scraped; confirm availability before acting.",
+      "Listings without a disclosed cap rate have NOI marked null — financial return is computed only where NOI is known."
+    ]
+  },
+  "assumptions": {
+    "budget": 400000,
+    "downPct": 25,
+    "ratePct": 6.75,
+    "amortYears": 30,
+    "closingPct": 2.5
+  },
+  "generated": "2026-06-21",
+  "ranked": [
+    {
+      "id": "schoenborn17920",
+      "address": "17920 Schoenborn St",
+      "city": "Northridge",
+      "zip": "91325",
+      "type": "Multifamily",
+      "price": 1395000,
+      "units": 6,
+      "sqft": 4962,
+      "cap_rate": null,
+      "year_built": 1959,
+      "rent_control": "LAR3; pre-1978 RSO",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; live-in-one play near CSUN/Northridge Hospital; classic value-add",
+      "source": "https://www.compass.com/homedetails/17920-Schoenborn-St-Northridge-CA-91325/1JVU5S_pid/",
+      "finance": {
+        "down": 348750,
+        "closing": 34875,
+        "cashNeeded": 383625,
+        "loan": 1046250,
+        "annualDebtService": 81431,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This 6-unit multifamily property in Northridge offers a value-add opportunity with strong location near CSUN and Northridge Hospital, and potential for rent growth due to rent control exemptions. With a $400k budget, the buyer can secure a leveraged position in a desirable area with room for operational improvements.",
+        "risks": [
+          "Rent control restrictions may limit upside potential",
+          "Age of the property (1959) may require significant capital expenditures",
+          "NOI and cap rate are unknown, increasing uncertainty in valuation"
+        ],
+        "upside": [
+          "Location proximity to major institutions increases long-term value",
+          "Potential for rent increases under LAR3 and RSO exemptions",
+          "Classic value-add opportunity with room for renovations and management improvements"
+        ],
+        "opportunity_score": 72,
+        "recommendation": "Buy"
+      },
+      "qwenScore": 72,
+      "composite": 61,
+      "rank": 1
+    },
+    {
+      "id": "amigo8357",
+      "address": "8357 Amigo Ave",
+      "city": "Northridge",
+      "zip": "91324",
+      "type": "Multifamily",
+      "price": 1249000,
+      "units": 4,
+      "sqft": 3816,
+      "cap_rate": null,
+      "year_built": 1960,
+      "rent_control": "LAR3",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; reduced from $1,350,000 (price cut signals motivated seller); quad near CSUN",
+      "source": "https://www.compass.com/homedetails/8357-Amigo-Ave-Northridge-CA-91324/1IMLRO_pid/",
+      "finance": {
+        "down": 312250,
+        "closing": 31225,
+        "cashNeeded": 343475,
+        "loan": 936750,
+        "annualDebtService": 72909,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This multifamily property in Northridge offers a leveraged opportunity with a motivated seller and a prime location near CSUN, suggesting potential for rental income growth and value appreciation. The 25% down payment aligns with the investor's $400k budget, allowing for a purchase with room for closing costs and reserves.",
+        "risks": [
+          "LAR3 rent control may limit rental income growth potential",
+          "Age of the property (1960) may indicate higher maintenance costs",
+          "Cap rate is not disclosed, making it difficult to assess current returns"
+        ],
+        "upside": [
+          "Proximity to CSUN increases demand for rental units",
+          "Price reduction indicates seller motivation, potentially leading to a favorable purchase price",
+          "Potential for value-add through renovations or improved management"
+        ],
+        "opportunity_score": 72,
+        "recommendation": "Buy"
+      },
+      "qwenScore": 72,
+      "composite": 61,
+      "rank": 2
+    },
+    {
+      "id": "canby7304b_dup",
+      "address": "21627 Sherman Way",
+      "city": "Canoga Park",
+      "zip": "91303",
+      "type": "Retail",
+      "price": 899998,
+      "units": 1,
+      "sqft": 1500,
+      "cap_rate": null,
+      "year_built": 1941,
+      "rent_control": "Commercial — n/a",
+      "status": "Active",
+      "upside_note": "Owner-user retail storefront in Canoga Park Parts District; Opportunity Zone + JEDI Zone incentives; SBA 504 ~10% down; new roof/HVAC 2022. No income in place (owner-user).",
+      "source": "https://www.coldwellbankerhomes.com/ca/canoga-park/21627-sherman-way/pid_69748281/",
+      "finance": {
+        "down": 225000,
+        "closing": 22500,
+        "cashNeeded": 247499,
+        "loan": 674999,
+        "annualDebtService": 52536,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This retail property in Canoga Park offers a unique opportunity for a leveraged buyer with $400k available for down payment and closing costs. Located in a revitalizing area with Opportunity Zone and JEDI Zone incentives, the property benefits from potential tax advantages and future growth. With recent improvements (new roof and HVAC) and a prime location in the Parts District, the asset is well-positioned for value-add strategies such as retenanting or repositioning.",
+        "risks": [
+          "No current income in place increases uncertainty around future cash flow potential",
+          "Vacant retail properties in competitive markets may face challenges in attracting quality tenants",
+          "Age of the building (1941) may imply higher maintenance costs or outdated infrastructure"
+        ],
+        "upside": [
+          "Opportunity Zone and JEDI Zone incentives may provide tax benefits and attract development interest",
+          "Potential for retenanting at higher rates or repositioning the space for a more profitable use",
+          "SBA 504 financing option offers favorable terms with ~10% down, reducing capital requirements"
+        ],
+        "opportunity_score": 72,
+        "recommendation": "Buy"
+      },
+      "qwenScore": 72,
+      "composite": 61,
+      "rank": 3
+    },
+    {
+      "id": "tilden8843",
+      "address": "8843-8845 Tilden Ave",
+      "city": "Panorama City",
+      "zip": "91402",
+      "type": "Multifamily",
+      "price": 1050000,
+      "units": 3,
+      "sqft": 2076,
+      "cap_rate": null,
+      "year_built": 1948,
+      "rent_control": "Pre-1978 -> LA RSO",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; $350k/unit; individually metered; owner-user/house-hack friendly",
+      "source": "https://www.coldwellbankerhomes.com/ca/panorama-city/8843-tilden/pid_69954470/",
+      "finance": {
+        "down": 262500,
+        "closing": 26250,
+        "cashNeeded": 288750,
+        "loan": 787500,
+        "annualDebtService": 61293,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This multifamily property in Panorama City offers a leveraged opportunity for a buyer with a $400k budget, allowing for a 25% down payment and potential owner-user or house-hack strategy. The individually metered units and pre-1978 rent control status may provide long-term stability and income potential in a growing market.",
+        "risks": [
+          "Pre-1978 rent control may limit rental income growth",
+          "Age of the property (1948) may imply higher maintenance and repair costs",
+          "Cap rate is not disclosed, making it difficult to assess current returns"
+        ],
+        "upside": [
+          "Potential for owner-user or house-hack strategy to reduce expenses",
+          "Individually metered units may allow for better tenant screening and revenue management",
+          "Location in Panorama City may benefit from ongoing neighborhood improvements and demand for multifamily housing"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 57,
+      "rank": 4
+    },
+    {
+      "id": "sanfernando10004",
+      "address": "10004 San Fernando Rd",
+      "city": "Pacoima",
+      "zip": "91331",
+      "type": "Mixed-use",
+      "price": 1100000,
+      "units": 3,
+      "sqft": 2270,
+      "cap_rate": null,
+      "year_built": 1933,
+      "rent_control": "Zoned LAC2 (residential/commercial)",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; triplex on commercial-zoned lot, 9,155sf land; SOLD AS-IS (condition risk); redevelopment optionality",
+      "source": "https://www.compass.com/homedetails/10004-San-Fernando-Rd-Pacoima-CA-91331/1J7S8M_pid/",
+      "finance": {
+        "down": 275000,
+        "closing": 27500,
+        "cashNeeded": 302500,
+        "loan": 825000,
+        "annualDebtService": 64211,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This triplex on a large commercial-zoned lot in Pacoima offers value-add potential through redevelopment or rent growth, with a low down payment requirement and a budget-friendly entry point for a leveraged buyer. The property's size, land value, and zoning flexibility create opportunities for long-term appreciation and income diversification.",
+        "risks": [
+          "Significant condition risk due to 'as-is' sale and age of the building (1933)",
+          "Uncertain cap rate and NOI due to lack of financial disclosure",
+          "Redevelopment may require permits, approvals, and time, increasing complexity and cost"
+        ],
+        "upside": [
+          "Potential for redevelopment on the 9,155 sqft lot, which could significantly increase value",
+          "Zoning flexibility (LAC2) allows for mixed-use opportunities",
+          "Leverage-friendly purchase with 25% down and manageable cash outlay"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 57,
+      "rank": 5
+    },
+    {
+      "id": "owensmouth7129",
+      "address": "7129-7131 1/2 Owensmouth Ave",
+      "city": "Canoga Park",
+      "zip": "91303",
+      "type": "Retail",
+      "price": 1050000,
+      "units": 1,
+      "sqft": 1482,
+      "cap_rate": null,
+      "year_built": 1970,
+      "rent_control": "Commercial — n/a",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; small freestanding retail near Warner Center",
+      "source": "https://www.loopnet.com/Listing/7129-7131-1-2-Owensmouth-Ave-Canoga-Park-CA/35430257/",
+      "finance": {
+        "down": 262500,
+        "closing": 26250,
+        "cashNeeded": 288750,
+        "loan": 787500,
+        "annualDebtService": 61293,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 45,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This freestanding retail property in Canoga Park offers an opportunity to acquire a small, income-producing asset near the Warner Center, a high-traffic area. With a $400k budget covering down payment and closing costs, the investor can leverage the remaining funds to secure a position in a growing retail market with potential for rent growth and value appreciation.",
+        "risks": [
+          "Location near Warner Center may face competition from larger retail tenants",
+          "No cap rate disclosed limits ability to assess current returns",
+          "Small size may limit scalability or tenant diversity"
+        ],
+        "upside": [
+          "Proximity to Warner Center provides strong foot traffic potential",
+          "Freestanding retail may allow for flexible use or repositioning",
+          "Potential for long-term appreciation in a desirable submarket"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 57,
+      "rank": 6
+    },
+    {
+      "id": "canby7304",
+      "address": "7302-7304 Canby Ave",
+      "city": "Reseda",
+      "zip": "91335",
+      "type": "Mixed-use",
+      "price": 1250000,
+      "units": 5,
+      "sqft": 5048,
+      "cap_rate": 5.96,
+      "cap_rate_projected": 7.8,
+      "noi_actual": 74497,
+      "gross_actual": 112200,
+      "expenses": 37703,
+      "verified": true,
+      "year_built": 1926,
+      "rent_control": "C2 (LAC2) mixed commercial+residential",
+      "status": "Active",
+      "upside_note": "VERIFIED (MLS #26654413): actual in-place cap ~5.96% (NOI $74,497 on $112,200 gross, $37,703 exp) — the 7.8% headline is PROJECTED, achieved mainly by doubling the commercial unit $2,000->$4,000/mo + bumping the 3bd. SELLER FINANCING confirmed. Price CUT $1,399,000->$1,250,000 (motivated seller). Occupancy ambiguous (listing desc says 3 occupied vs MLS 0% vacancy — verify rent roll). C2 development optionality.",
+      "source": "https://www.compass.com/homedetails/7304-Canby-Ave-Reseda-CA-91335/1IO7AS_pid/",
+      "finance": {
+        "down": 312500,
+        "closing": 31250,
+        "cashNeeded": 343750,
+        "loan": 937500,
+        "annualDebtService": 72967,
+        "noi": 74500,
+        "cashFlow": 1533,
+        "coc": 0.45,
+        "dscr": 1.02,
+        "affordable": true
+      },
+      "financeScore": 35,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This mixed-use property in Reseda offers a value-add opportunity with a verified in-place cap rate of 5.96% and potential to increase to 7.8% through rent increases and commercial unit optimization. The motivated seller, seller financing, and development optionality under C2 zoning make it an attractive leveraged play for a $400k-down buyer.",
+        "risks": [
+          "Occupancy status is unclear and needs verification",
+          "Rent control restrictions may limit rent increases",
+          "Value-add projections depend on successful execution of tenant improvements and re-leasing"
+        ],
+        "upside": [
+          "Significant NOI growth potential through rent increases and commercial unit optimization",
+          "Seller financing reduces upfront cash needs",
+          "C2 zoning allows for future development or repositioning"
+        ],
+        "opportunity_score": 72,
+        "recommendation": "Buy"
+      },
+      "qwenScore": 72,
+      "composite": 52,
+      "rank": 7
+    },
+    {
+      "id": "wilbur9014",
+      "address": "9014 Wilbur Ave",
+      "city": "Northridge",
+      "zip": "91324",
+      "type": "Multifamily",
+      "price": 1700000,
+      "units": 4,
+      "sqft": 3211,
+      "cap_rate": null,
+      "year_built": 1980,
+      "rent_control": "Built 1980 -> NOT LA RSO; AB1482 only",
+      "status": "Active",
+      "upside_note": "Cap not disclosed; 1980 build = no LA rent control (higher increases); remodeled + new-construction units; adjacent 2-unit also available",
+      "source": "https://www.compass.com/homedetails/9014-Wilbur-Ave-Northridge-CA-91324/1KTV77_pid/",
+      "finance": {
+        "down": 425000,
+        "closing": 42500,
+        "cashNeeded": 467500,
+        "loan": 1275000,
+        "annualDebtService": 99236,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": false
+      },
+      "financeScore": 27,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This 4-unit multifamily property in Northridge offers an opportunity to purchase a remodeled and upgraded asset in a desirable area with no rent control restrictions, allowing for higher rental income potential. While the asking price exceeds the $400k budget for a 25% down purchase, the adjacent 2-unit property may provide a bundled acquisition opportunity that could enhance overall returns.",
+        "risks": [
+          "Insufficient down payment for a 25% LTV purchase without additional financing or budget adjustments",
+          "No disclosed cap rate limits ability to estimate returns and compare to market benchmarks",
+          "Adjacent 2-unit property availability may complicate due diligence and increase complexity of the deal"
+        ],
+        "upside": [
+          "No rent control in Northridge (built 1980) allows for higher rent increases and greater NOI potential",
+          "Remodeled and new-construction units may contribute to higher occupancy and rental rates",
+          "Potential for bundled acquisition with adjacent 2-unit property may offer scale and diversification"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 43,
+      "rank": 8
+    },
+    {
+      "id": "correnti12600",
+      "address": "12600 Correnti St",
+      "city": "Pacoima",
+      "zip": "91331",
+      "type": "Multifamily",
+      "price": 3300000,
+      "units": 14,
+      "sqft": 5334,
+      "cap_rate": 7.5,
+      "year_built": null,
+      "rent_control": "Renovated; verify RSO",
+      "status": "Active (OUT OF BUDGET — context)",
+      "upside_note": "ASSUMABLE $2.5M loan at 3.9% fixed thru 09/2027 -> only ~25% (~$825k) down but 12.2% cash-on-cash on current; plans to add 20 units -> proforma 13.9% cap. Exceptional economics but down payment EXCEEDS $400k.",
+      "source": "https://www.compass.com/homedetails/12600-Correnti-St-Pacoima-CA-91331/27H0FQ_pid/",
+      "finance": {
+        "down": 825000,
+        "closing": 82500,
+        "cashNeeded": 907500,
+        "loan": 2475000,
+        "annualDebtService": 192634,
+        "noi": 247500,
+        "cashFlow": 54866,
+        "coc": 6.05,
+        "dscr": 1.28,
+        "affordable": false
+      },
+      "financeScore": 28,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This multifamily property in Pacoima offers strong long-term value with a verified 7.5% cap rate and potential for significant upside through unit expansion. While the current cash-on-cash return is lower due to the high down payment requirement, the proforma cap rate of 13.9% and assumable loan terms present a compelling opportunity for a buyer with a $400k budget if alternative financing or value-add strategies can be implemented.",
+        "risks": [
+          "Down payment requirement exceeds budget, limiting immediate purchase feasibility",
+          "Need to verify RSO status and rent control implications",
+          "Proforma projections depend on successful unit expansion and market absorption"
+        ],
+        "upside": [
+          "Assumable loan at 3.9% provides favorable financing terms",
+          "Potential for 13.9% proforma cap rate with unit addition",
+          "Strong location in growing San Fernando Valley market"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 38,
+      "rank": 9
+    },
+    {
+      "id": "burbank10701",
+      "address": "10701 Burbank Blvd",
+      "city": "North Hollywood",
+      "zip": "91601",
+      "type": "Retail/NNN",
+      "price": 2799000,
+      "units": 1,
+      "sqft": 1802,
+      "cap_rate": 7.3,
+      "year_built": 1951,
+      "rent_control": "Commercial NNN — n/a",
+      "status": "Active (OUT OF BUDGET — context)",
+      "upside_note": "Single-tenant NNN cannabis dispensary, $204k NOI, 10-yr lease 2% bumps, fully passive. High cap but cannabis-tenant risk + needs ~$700k down.",
+      "source": "https://www.jayrubin-re.com/file/view-file/580",
+      "finance": {
+        "down": 699750,
+        "closing": 69975,
+        "cashNeeded": 769725,
+        "loan": 2099250,
+        "annualDebtService": 163388,
+        "noi": 204327,
+        "cashFlow": 40939,
+        "coc": 5.32,
+        "dscr": 1.25,
+        "affordable": false
+      },
+      "financeScore": 24,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This single-tenant NNN cannabis dispensary offers a high cap rate of 7.3% with a 10-year lease and 2% annual rent bumps, providing stable cash flow and long-term income potential. However, the required down payment exceeds the $400k budget, limiting leverage and cash-on-cash return.",
+        "risks": [
+          "Cannabis industry regulatory and legal risks",
+          "Tenant-specific risk due to single-tenant exposure",
+          "Down payment shortfall may require additional financing or reduce leverage"
+        ],
+        "upside": [
+          "Attractive cap rate and long-term lease with rent bumps",
+          "Fully passive management with NNN structure",
+          "Potential for appreciation in a growing retail market"
+        ],
+        "opportunity_score": 55,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 55,
+      "composite": 32,
+      "rank": 10
+    },
+    {
+      "id": "vannuys12872",
+      "address": "12872 Van Nuys Blvd",
+      "city": "Pacoima",
+      "zip": "91331",
+      "type": "Multifamily",
+      "price": 949900,
+      "units": 3,
+      "sqft": 2084,
+      "cap_rate": null,
+      "year_built": 1948,
+      "rent_control": "Pre-1978 -> LA RSO",
+      "status": "Active (one source showed Pending — VERIFY)",
+      "upside_note": "Cap not disclosed; renovated triplex, new roof/windows/plumbing; STATUS UNCERTAIN",
+      "source": "https://www.compass.com/homedetails/12872-Van-Nuys-Blvd-Pacoima-CA-91331/1L7DX2_pid/",
+      "finance": {
+        "down": 237475,
+        "closing": 23748,
+        "cashNeeded": 261223,
+        "loan": 712425,
+        "annualDebtService": 55449,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 20,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This renovated triplex in Pacoima offers a leveraged opportunity for a buyer with $400k available for down payment and closing costs. With a 25% down payment, the cash needed is within budget, and the property's recent upgrades (roof, windows, plumbing) may support competitive rental rates in a rent-controlled market.",
+        "risks": [
+          "Uncertain property status (active vs. pending) could affect transaction timing and terms",
+          "Rent control under LA RSO may limit rental income growth potential",
+          "Age of the building (1948) may imply hidden maintenance or compliance costs"
+        ],
+        "upside": [
+          "Recent renovations may allow for higher occupancy and rental rates",
+          "Strategic location in Pacoima may benefit from area revitalization",
+          "Leverage available with 25% down payment to maximize returns"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 31,
+      "rank": 11
+    },
+    {
+      "id": "comercio5301",
+      "address": "5301 Comercio Lane",
+      "city": "Woodland Hills",
+      "zip": "91364",
+      "type": "Mixed-use",
+      "price": 814250,
+      "units": 4,
+      "sqft": 3473,
+      "cap_rate": null,
+      "year_built": 1956,
+      "rent_control": "LAC4 (office/retail mixed)",
+      "status": "Pending (probate/court-confirmation)",
+      "upside_note": "Cap not disclosed; probate trust sale, court-confirmation required (slow/uncertain close); LAC4 flexible use near Ventura Blvd; lowest entry price",
+      "source": "https://www.nuevo-realestate.com/real-estate/5301-comercio-lane-woodland-hills-ca-91364/25577103/183735124",
+      "finance": {
+        "down": 203563,
+        "closing": 20356,
+        "cashNeeded": 223919,
+        "loan": 610688,
+        "annualDebtService": 47531,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": true
+      },
+      "financeScore": 20,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This mixed-use property in Woodland Hills offers a unique opportunity for a leveraged buyer with $400k available for down and closing, given its low entry price relative to comparable assets in the area. Its flexible LAC4 zoning and proximity to Ventura Blvd present potential for value-add through repositioning or multi-family conversion.",
+        "risks": [
+          "Probate sale with slow/uncertain closing timeline",
+          "No cap rate disclosed, limiting income projection analysis",
+          "Age and condition of the 1956-built structure may require significant renovation"
+        ],
+        "upside": [
+          "Flexible LAC4 zoning allows for office, retail, or mixed-use development",
+          "Lowest entry price in the area suggests potential for appreciation",
+          "Strategic location near Ventura Blvd offers strong traffic and visibility"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 31,
+      "rank": 12
+    },
+    {
+      "id": "sepulveda8144",
+      "address": "8144 Sepulveda Pl",
+      "city": "Panorama City",
+      "zip": "91402",
+      "type": "Multifamily",
+      "price": 1400000,
+      "units": 6,
+      "sqft": 5418,
+      "cap_rate": 5,
+      "year_built": 1952,
+      "rent_control": "LA RSO (pre-1978) — assume rent controlled",
+      "status": "Active",
+      "upside_note": "All 2bd/1ba unit mix; tremendous upside; newer roof, copper plumbing, owned laundry",
+      "source": "https://www.compass.com/homedetails/8144-Sepulveda-Pl-Panorama-City-CA-91402/1IJRNO_pid/",
+      "finance": {
+        "down": 350000,
+        "closing": 35000,
+        "cashNeeded": 385000,
+        "loan": 1050000,
+        "annualDebtService": 81723,
+        "noi": 70000,
+        "cashFlow": -11723,
+        "coc": -3.04,
+        "dscr": 0.86,
+        "affordable": true
+      },
+      "financeScore": 0,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This 6-unit multifamily property in Panorama City offers a compelling opportunity for a leveraged buyer with $400k available, given its strong cap rate, rent-controlled status, and potential for value-add through rent increases and unit upgrades. The property's newer roof, copper plumbing, and owned laundry provide a solid foundation for long-term appreciation and cash flow improvement.",
+        "risks": [
+          "Low cash-on-cash return (-3.04%) due to high leverage",
+          "Rent control may limit immediate rent growth potential",
+          "Age of the building (1952) may lead to higher maintenance costs over time"
+        ],
+        "upside": [
+          "Significant upside through value-add strategies such as unit renovations and rent increases",
+          "Strong location in a growing area of the San Fernando Valley",
+          "Potential for long-term appreciation due to limited supply of rent-controlled multifamily units"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 29,
+      "rank": 13
+    },
+    {
+      "id": "canby7260",
+      "address": "7260 Canby Ave",
+      "city": "Reseda",
+      "zip": "91335",
+      "type": "Multifamily",
+      "price": 1425000,
+      "units": 6,
+      "sqft": 5108,
+      "cap_rate": 4.75,
+      "year_built": 1954,
+      "rent_control": "LAR2; pre-1978 likely RSO",
+      "status": "Active",
+      "upside_note": "RTI plans for 2 ADUs -> projected 7.08% cap, 7.60% pre-tax CoC post-ADU; $272/sf post-ADU; Walk Score 86 near CSUN",
+      "source": "https://www.loopnet.com/Listing/7260-Canby-Ave-Reseda-CA/34975971/",
+      "finance": {
+        "down": 356250,
+        "closing": 35625,
+        "cashNeeded": 391875,
+        "loan": 1068750,
+        "annualDebtService": 83183,
+        "noi": 67688,
+        "cashFlow": -15495,
+        "coc": -3.95,
+        "dscr": 0.81,
+        "affordable": true
+      },
+      "financeScore": 0,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This 6-unit multifamily property in Reseda offers a leveraged opportunity with strong location near CSUN and a high Walk Score, along with potential for value-add through planned ADUs. The current cap rate of 4.75% is competitive, and projected improvements could significantly enhance returns.",
+        "risks": [
+          "Low cash-on-cash return (-3.95%) indicates negative initial cash flow",
+          "Dependence on ADU approvals and construction timelines",
+          "Rent control (LAR2) may limit rental growth potential"
+        ],
+        "upside": [
+          "Planned ADUs could increase cap rate to 7.08% and improve cash-on-cash return",
+          "High Walk Score and proximity to CSUN suggest strong demand and long-term appreciation potential",
+          "Potential for value-add through unit upgrades and operational improvements"
+        ],
+        "opportunity_score": 65,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 65,
+      "composite": 29,
+      "rank": 14
+    },
+    {
+      "id": "tilden8837",
+      "address": "8837 Tilden Ave",
+      "city": "Panorama City",
+      "zip": "91402",
+      "type": "Multifamily",
+      "price": 899000,
+      "units": 2,
+      "sqft": 1794,
+      "cap_rate": 5.3,
+      "year_built": 1948,
+      "rent_control": "Pre-1978 -> LA RSO",
+      "status": "Active",
+      "upside_note": "Duplex; small entry point; on market 51 days",
+      "source": "https://www.compass.com/homedetails/8837-Tilden-Ave-Panorama-City-CA-91402/1JQEEP_pid/",
+      "finance": {
+        "down": 224750,
+        "closing": 22475,
+        "cashNeeded": 247225,
+        "loan": 674250,
+        "annualDebtService": 52478,
+        "noi": 47647,
+        "cashFlow": -4831,
+        "coc": -1.95,
+        "dscr": 0.91,
+        "affordable": true
+      },
+      "financeScore": 6,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This duplex in Panorama City offers a leveraged entry point into the San Fernando Valley multifamily market with a verified 5.3% cap rate and a low asking price relative to its size. With a $400k budget, the investor can secure a fully funded purchase with room for improvement, positioning for potential value-add through rent increases and property upgrades.",
+        "risks": [
+          "Low cash-on-cash return (-1.95%) indicates poor initial profitability",
+          "DSCR of 0.91 suggests potential cash flow stress",
+          "Rent control (LA RSO) limits upside in rental income"
+        ],
+        "upside": [
+          "Potential for value-add through renovations and tenant improvements",
+          "Opportunity to capitalize on long-term appreciation in the Panorama City market",
+          "Leverage available to scale returns with future equity injections"
+        ],
+        "opportunity_score": 55,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 55,
+      "composite": 28,
+      "rank": 15
+    },
+    {
+      "id": "victory13350",
+      "address": "13350 Victory Blvd",
+      "city": "Van Nuys",
+      "zip": "91401",
+      "type": "Multifamily",
+      "price": 1299995,
+      "units": 5,
+      "sqft": 3468,
+      "cap_rate": 4.49,
+      "year_built": 1951,
+      "rent_control": "Not on LA RSO (per listing)",
+      "status": "Active",
+      "upside_note": "~30% rent upside; 5 garage spaces add revenue; copper plumbing partial",
+      "source": "https://www.compass.com/homedetails/13350-Victory-Blvd-Van-Nuys-CA-91401/1J7A3S_pid/",
+      "finance": {
+        "down": 324999,
+        "closing": 32500,
+        "cashNeeded": 357499,
+        "loan": 974996,
+        "annualDebtService": 75886,
+        "noi": 58370,
+        "cashFlow": -17516,
+        "coc": -4.9,
+        "dscr": 0.77,
+        "affordable": true
+      },
+      "financeScore": 0,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This 5-unit multifamily property in Van Nuys offers an opportunity to acquire a value-add asset with potential for rent growth and operational improvements. With a 4.49% cap rate and 30% rent upside, the property is undervalued relative to its potential, especially with the added revenue from 5 garage spaces and a solid location in the San Fernando Valley.",
+        "risks": [
+          "Low cash-on-cash return (-4.9%) indicates negative initial cash flow",
+          "Partial copper plumbing may require future upgrades",
+          "DSCR of 0.77 suggests high debt service risk"
+        ],
+        "upside": [
+          "Significant rent increase potential due to 30% upside",
+          "Garage spaces provide additional income streams",
+          "Location in a stable, growing submarket with strong demand for multifamily housing"
+        ],
+        "opportunity_score": 55,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 55,
+      "composite": 25,
+      "rank": 16
+    },
+    {
+      "id": "canoga6020",
+      "address": "6020 Canoga Ave",
+      "city": "Woodland Hills",
+      "zip": "91367",
+      "type": "Retail/NNN",
+      "price": 5500000,
+      "units": 2,
+      "sqft": 4896,
+      "cap_rate": 6.35,
+      "year_built": null,
+      "rent_control": "Commercial NNN — n/a",
+      "status": "Active (OUT OF BUDGET — context)",
+      "upside_note": "2-tenant NNN (sushi + credit union) in Warner Center across from future Rams HQ; 18.2yr avg occupancy; trophy passive asset but far above budget.",
+      "source": "https://www.brandonmichaelsgroup.com/6020canogaave",
+      "finance": {
+        "down": 1375000,
+        "closing": 137500,
+        "cashNeeded": 1512500,
+        "loan": 4125000,
+        "annualDebtService": 321056,
+        "noi": 349250,
+        "cashFlow": 28194,
+        "coc": 1.86,
+        "dscr": 1.09,
+        "affordable": false
+      },
+      "financeScore": 0,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This trophy retail asset in Warner Center offers long-term stability with two strong NNN tenants and proximity to future Rams HQ, but the $400k budget is insufficient for a 25% down purchase, requiring significant additional capital or alternative financing.",
+        "risks": [
+          "Insufficient down payment to meet 25% requirement without additional financing",
+          "High leverage may increase sensitivity to interest rate changes",
+          "Tenant concentration risk with only two tenants"
+        ],
+        "upside": [
+          "Strong location with future development potential near Rams HQ",
+          "Long-term in-place cap rate of 6.35% with stable NNN tenants",
+          "Potential for value appreciation in Warner Center over time"
+        ],
+        "opportunity_score": 55,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 55,
+      "composite": 21,
+      "rank": 17
+    },
+    {
+      "id": "canby7454",
+      "address": "7454 Canby Ave",
+      "city": "Reseda",
+      "zip": "91335",
+      "type": "Multifamily",
+      "price": 1749000,
+      "units": 9,
+      "sqft": 6784,
+      "cap_rate": null,
+      "year_built": 1962,
+      "rent_control": "Pre-1978 -> LA RSO",
+      "status": "Active Under Contract",
+      "upside_note": "9 units at $194k/unit, diverse mix — but in escrow; back-up only",
+      "source": "https://www.remax.com/ca/reseda/home-details/7454-canby-ave-reseda-ca-91335/13235972936323110093/M00000093/SR26009655",
+      "finance": {
+        "down": 437250,
+        "closing": 43725,
+        "cashNeeded": 480975,
+        "loan": 1311750,
+        "annualDebtService": 102096,
+        "noi": null,
+        "cashFlow": null,
+        "coc": null,
+        "dscr": null,
+        "affordable": false
+      },
+      "financeScore": 2,
+      "financeConf": "low",
+      "qwen": {
+        "thesis": "This 9-unit multifamily property in Reseda offers a potential opportunity for a value-add investor with a $400k budget, leveraging the existing escrow status and the diverse unit mix. However, the required down payment exceeds the available budget, requiring creative financing or a reduction in purchase price to make the deal viable.",
+        "risks": [
+          "Insufficient down payment to meet the $480,975 requirement, potentially limiting financing options or requiring higher interest rates.",
+          "Rent control regulations (LA RSO) may limit rental income growth and increase operational complexity.",
+          "Uncertainty around the property's current NOI and cap rate, which are critical for evaluating long-term returns."
+        ],
+        "upside": [
+          "Diverse unit mix may allow for targeted renovations and rent increases on non-rent-controlled units.",
+          "Location in Reseda offers potential for long-term appreciation and stable cash flow in a growing market.",
+          "Existing escrow status may provide a window to negotiate terms or secure a backup position."
+        ],
+        "opportunity_score": 55,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 55,
+      "composite": 19,
+      "rank": 18
+    },
+    {
+      "id": "vanowen15840",
+      "address": "15840 Vanowen St",
+      "city": "Lake Balboa",
+      "zip": "91406",
+      "type": "Multifamily",
+      "price": 970000,
+      "units": 5,
+      "sqft": 3349,
+      "cap_rate": 5.25,
+      "year_built": 1952,
+      "rent_control": "Pre-1978 -> LA RSO",
+      "status": "Under Contract",
+      "upside_note": "5 units, new roof 2022, copper plumbing, tremendous upside — BUT already in escrow (likely unavailable)",
+      "source": "https://www.loopnet.com/Listing/15840-Vanowen-St-Lake-Balboa-CA/35415601/",
+      "finance": {
+        "down": 242500,
+        "closing": 24250,
+        "cashNeeded": 266750,
+        "loan": 727500,
+        "annualDebtService": 56623,
+        "noi": 50925,
+        "cashFlow": -5698,
+        "coc": -2.14,
+        "dscr": 0.9,
+        "affordable": true
+      },
+      "financeScore": 0,
+      "financeConf": "high",
+      "qwen": {
+        "thesis": "This 5-unit multifamily property in Lake Balboa offers a compelling opportunity with recent upgrades, strong cap rate, and potential for value-add through rent increases and unit improvements. With a $400k budget, the investor can secure a leveraged position with manageable cash outlay and upside potential.",
+        "risks": [
+          "Already in escrow, reducing availability and control",
+          "Rent control restrictions may limit rental growth potential",
+          "Low cash-on-cash return and DSCR below 1.0 indicate financial stress under current assumptions"
+        ],
+        "upside": [
+          "Recent improvements (new roof, copper plumbing) provide a solid foundation for value-add",
+          "Potential for rent increases due to location and market demand",
+          "Opportunity to reposition units for higher income streams"
+        ],
+        "opportunity_score": 60,
+        "recommendation": "Watch"
+      },
+      "qwenScore": 60,
+      "composite": 18,
+      "rank": 19
+    }
+  ]
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..041ec45
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,827 @@
+{
+  "name": "commercialrealestate",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "commercialrealestate",
+      "version": "0.1.0",
+      "dependencies": {
+        "express": "^4.22.2"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.5",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+      "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.15.1",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+      "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.5",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.15.1",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+      "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.2",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4",
+        "side-channel-list": "^1.0.1",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
index bb2291f..6334a74 100644
--- a/package.json
+++ b/package.json
@@ -7,5 +7,8 @@
     "scrape": "node scripts/scrape-crexi.js",
     "analyze": "node scripts/analyze.js",
     "serve": "node scripts/serve.js"
+  },
+  "dependencies": {
+    "express": "^4.22.2"
   }
 }
diff --git a/public/finance.js b/public/finance.js
new file mode 100644
index 0000000..6709a61
--- /dev/null
+++ b/public/finance.js
@@ -0,0 +1,62 @@
+// finance.js — shared leverage math for the SFV CRE ranker.
+// Works in Node (require) and the browser (global CREFin). Single source of truth so the
+// viewer can recompute any down-payment scenario live and match the Node ranking exactly.
+(function (root, factory) {
+  if (typeof module === 'object' && module.exports) module.exports = factory();
+  else root.CREFin = factory();
+})(typeof self !== 'undefined' ? self : this, function () {
+
+  function debtService(loan, ratePct, amortYears) {
+    if (loan <= 0) return 0;
+    const r = ratePct / 100 / 12, n = amortYears * 12;
+    if (r === 0) return loan / amortYears;
+    const m = loan * (r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1);
+    return m * 12;
+  }
+
+  // A = { budget, downPct, ratePct, amortYears, closingPct }
+  function finance(L, A) {
+    const down = L.price * A.downPct / 100;
+    const closing = L.price * A.closingPct / 100;
+    const cashNeeded = Math.round(down + closing);
+    const loan = Math.max(0, L.price - down);
+    const ads = (A.downPct >= 100 || loan <= 0) ? 0 : debtService(loan, A.ratePct, A.amortYears);
+    const noi = (L.cap_rate != null) ? Math.round(L.price * L.cap_rate / 100) : null;
+    let cashFlow = null, coc = null, dscr = null;
+    if (noi != null) {
+      cashFlow = Math.round(noi - ads);
+      coc = +(cashFlow / cashNeeded * 100).toFixed(2);
+      dscr = ads > 0 ? +(noi / ads).toFixed(2) : null; // null = all-cash (no debt)
+    }
+    const affordable = cashNeeded <= A.budget;
+    return { down: Math.round(down), closing: Math.round(closing), cashNeeded, loan: Math.round(loan),
+      annualDebtService: Math.round(ads), noi, cashFlow, coc, dscr, affordable };
+  }
+
+  // Deterministic finance score 0-100 (return-driven; honest about negative leverage)
+  function financeScore(L, f) {
+    let s = 50, conf = 'med';
+    if (f.coc != null) {
+      s = 30 + Math.max(-30, Math.min(45, f.coc * 5));
+      if (f.dscr != null) s += f.dscr >= 1.25 ? 8 : f.dscr >= 1.0 ? 0 : -15;
+      if (L.cap_rate != null) s += (L.cap_rate - 5) * 3;
+      conf = 'high';
+    } else { s = 45; conf = 'low'; } // NOI undisclosed -> can't score return
+    if (!f.affordable) s -= 18;
+    if (/Under Contract|Pending/i.test(L.status || '')) s -= 25;
+    if (/OUT OF BUDGET/i.test(L.status || '')) s -= 30;
+    return { score: Math.max(0, Math.min(100, Math.round(s))), conf };
+  }
+
+  function composite(L, f, fScore, qwenScore) {
+    let c;
+    if (f.coc != null && qwenScore != null) c = Math.round(0.55 * fScore + 0.45 * qwenScore);
+    else if (qwenScore != null) c = Math.round(0.4 * fScore + 0.6 * qwenScore);
+    else c = fScore;
+    if (/Under Contract|Pending/i.test(L.status || '')) c = Math.round(c * 0.65);
+    if (!f.affordable) c = Math.round(c * 0.85);
+    return c;
+  }
+
+  return { debtService, finance, financeScore, composite };
+});
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..8bdc232
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,282 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>SFV CRE — $400k Down Investment Ranking</title>
+<style>
+  :root { --cols: 3; --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; }
+  * { box-sizing: border-box; }
+  body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
+  header { padding:20px 28px 12px; border-bottom:1px solid var(--line); }
+  h1 { margin:0 0 4px; font-size:22px; letter-spacing:.2px; }
+  .sub { color:var(--mut); font-size:13px; }
+  details.note { margin:10px 0 2px; max-width:1040px; border:1px solid var(--line); border-radius:10px; background:var(--card); }
+  details.note summary { cursor:pointer; padding:9px 14px; font-size:13px; color:var(--blue); list-style:none; }
+  details.note summary::-webkit-details-marker { display:none; }
+  details.note .body { padding:0 14px 12px; font-size:13px; color:#cdd6e0; }
+  details.note .body b { color:var(--ink); }
+  .controls { display:flex; flex-wrap:wrap; gap:14px; align-items:center; padding:12px 28px; border-bottom:1px solid var(--line); position:sticky; top:0; background:rgba(14,17,22,.96); backdrop-filter:blur(8px); z-index:10; }
+  .controls label { color:var(--mut); font-size:12px; margin-right:6px; text-transform:uppercase; letter-spacing:.5px; }
+  select, .chip, .seg button { background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:7px 10px; font-size:13px; cursor:pointer; }
+  .chip.active { border-color:var(--blue); color:var(--blue); }
+  .seg { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
+  .seg button { border:0; border-radius:0; border-right:1px solid var(--line); }
+  .seg button:last-child { border-right:0; }
+  .seg button.active { background:#1f6feb; color:#fff; }
+  input[type=range] { width:150px; vertical-align:middle; accent-color:var(--blue); }
+  .pricecap { width:120px; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:6px 8px; font-size:13px; }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols), 1fr); gap:16px; padding:20px 28px 30px; }
+  .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; display:flex; flex-direction:column; gap:10px; position:relative; transition:box-shadow .2s,border-color .2s; }
+  .card.uc { opacity:.62; }
+  .card.flash { border-color:var(--blue); box-shadow:0 0 0 3px rgba(88,166,255,.35); }
+  .top { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; }
+  .rank { font-size:12px; color:var(--mut); }
+  .score { font-size:30px; font-weight:700; line-height:1; }
+  .score small { font-size:11px; color:var(--mut); font-weight:400; display:block; text-align:right; }
+  .addr { font-weight:600; font-size:15px; }
+  .city { color:var(--mut); font-size:13px; }
+  .badges { display:flex; flex-wrap:wrap; gap:6px; }
+  .b { font-size:11px; padding:3px 8px; border-radius:20px; border:1px solid var(--line); color:var(--mut); }
+  .b.type { color:var(--blue); border-color:#284b7a; }
+  .b.ok { color:var(--acc); border-color:#1f6f37; }
+  .b.warn { color:var(--warn); border-color:#6b5320; }
+  .b.bad { color:var(--bad); border-color:#7d2b28; }
+  .b.rec-Buy { color:var(--acc); border-color:#1f6f37; font-weight:600; }
+  .b.rec-Watch { color:var(--warn); border-color:#6b5320; font-weight:600; }
+  .b.rec-Pass { color:var(--bad); border-color:#7d2b28; font-weight:600; }
+  .metrics { display:grid; grid-template-columns:1fr 1fr; gap:6px 14px; font-size:13px; border-top:1px solid var(--line); border-bottom:1px solid var(--line); padding:10px 0; }
+  .metrics div { display:flex; justify-content:space-between; }
+  .metrics .k { color:var(--mut); }
+  .metrics .v { font-variant-numeric:tabular-nums; font-weight:600; }
+  .pos { color:var(--acc); } .neg { color:var(--bad); } .na { color:var(--mut); }
+  .thesis { font-size:13px; color:#cdd6e0; }
+  .lst { font-size:12px; color:var(--mut); margin:2px 0 0; padding-left:16px; }
+  .src { font-size:12px; } .src a { color:var(--blue); text-decoration:none; }
+  .small { font-size:11px; color:var(--mut); }
+  .foot { color:var(--mut); font-size:12px; padding:0 28px 8px; }
+
+  /* Docked chat */
+  #chatwrap { border-top:1px solid var(--line); background:#0b0e13; padding:16px 28px 40px; }
+  #chatwrap h2 { font-size:15px; margin:0 0 2px; }
+  #chatwrap .hint { color:var(--mut); font-size:12px; margin-bottom:10px; }
+  #log { display:flex; flex-direction:column; gap:10px; max-height:46vh; overflow-y:auto; padding:4px 2px; }
+  .msg { max-width:80%; padding:9px 13px; border-radius:12px; font-size:14px; line-height:1.45; white-space:pre-wrap; word-wrap:break-word; }
+  .msg.u { align-self:flex-end; background:#1f6feb; color:#fff; border-bottom-right-radius:3px; }
+  .msg.a { align-self:flex-start; background:var(--card); border:1px solid var(--line); border-bottom-left-radius:3px; }
+  .msg.a code { background:#0b0e13; padding:1px 5px; border-radius:4px; }
+  .msg.sys { align-self:center; color:var(--mut); font-size:12px; background:transparent; }
+  .actiontag { display:inline-block; margin-top:6px; font-size:11px; color:var(--acc); border:1px solid #1f6f37; border-radius:6px; padding:2px 7px; }
+  #chatform { display:flex; gap:10px; margin-top:12px; }
+  #q { flex:1; background:var(--card); color:var(--ink); border:1px solid var(--line); border-radius:10px; padding:11px 13px; font-size:14px; }
+  #send { background:#1f6feb; color:#fff; border:0; border-radius:10px; padding:0 20px; font-size:14px; font-weight:600; cursor:pointer; }
+  #send:disabled { opacity:.5; cursor:default; }
+  .examples { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
+  .ex { font-size:12px; color:var(--blue); border:1px dashed #284b7a; border-radius:16px; padding:4px 10px; cursor:pointer; background:transparent; }
+</style>
+</head>
+<body>
+<header>
+  <h1>San Fernando Valley CRE — Best Investment for $400k Down</h1>
+  <div class="sub" id="sub">loading…</div>
+  <details class="note" id="verdict"><summary>📌 Analyst verdict &amp; due-diligence notes</summary><div class="body" id="verdictBody"></div></details>
+</header>
+
+<div class="controls">
+  <div><label>Down payment</label>
+    <span class="seg" id="scenario">
+      <button data-s="25" class="active">25% down</button>
+      <button data-s="30">30% down</button>
+      <button data-s="cash">All cash</button>
+    </span>
+  </div>
+  <div><label>Sort</label>
+    <select id="sort">
+      <option value="composite">Best Investment (blended)</option>
+      <option value="coc">Cash-on-Cash ↓</option>
+      <option value="cap">Cap Rate ↓</option>
+      <option value="dscr">DSCR ↓</option>
+      <option value="priceAsc">Price ↑</option>
+      <option value="priceDesc">Price ↓</option>
+      <option value="cashNeeded">Cash Needed ↑</option>
+    </select>
+  </div>
+  <div id="filters"></div>
+  <div><label>Max $</label><input class="pricecap" id="maxPrice" type="number" placeholder="any" step="50000"></div>
+  <div style="margin-left:auto"><label>Density</label><input type="range" id="density" min="1" max="5" step="1" value="3"></div>
+</div>
+
+<div class="grid" id="grid"></div>
+<div class="foot" id="foot"></div>
+
+<div id="chatwrap">
+  <h2>💬 Ask the analyst &nbsp;<span class="small">— grounded in this ranking · free (Claude CLI)</span></h2>
+  <div class="hint">Ask questions <b>or tell it to drive the view</b>: “show only deals under $1M at 30% down sorted by cash-on-cash”, “why is Canby #1 and is its 7.8% cap real?”, “take me to the Reseda mixed-use”.</div>
+  <div id="log"></div>
+  <div class="examples" id="examples"></div>
+  <form id="chatform" autocomplete="off">
+    <input id="q" placeholder="Ask about the deals, or tell me to filter / sort / change financing…">
+    <button id="send" type="submit">Send</button>
+  </form>
+</div>
+
+<script src="finance.js"></script>
+<script>
+const $ = s => document.querySelector(s);
+const fmt = n => n==null ? '—' : '$'+Math.round(n).toLocaleString();
+const pct = n => n==null ? '—' : n+'%';
+const cls = (v, good, ok) => v==null?'na':(v>=good?'pos':(v>=ok?'':'neg'));
+
+const SCEN = { '25':{budget:400000,downPct:25,ratePct:6.75,amortYears:30,closingPct:2.5},
+               '30':{budget:400000,downPct:30,ratePct:6.75,amortYears:30,closingPct:2.5},
+               'cash':{budget:400000,downPct:100,ratePct:0,amortYears:30,closingPct:2.5} };
+let DATA=null, filter='all', scenario='25', maxPrice=null, history=[];
+
+// recompute finance + composite for every listing under the active scenario
+function recompute(){
+  const A = SCEN[scenario];
+  DATA.ranked.forEach(p=>{
+    p.finance = CREFin.finance(p, A);
+    const fsc = CREFin.financeScore(p, p.finance);
+    p.financeScore = fsc.score; p.financeConf = fsc.conf;
+    p.composite = CREFin.composite(p, p.finance, fsc.score, p.qwenScore);
+  });
+  DATA.ranked.sort((a,b)=>b.composite-a.composite);
+  DATA.ranked.forEach((p,i)=>p.rank=i+1);
+}
+
+function card(p){
+  const f=p.finance, uc=/Under Contract|Pending/i.test(p.status);
+  const aff = f.affordable ? `<span class="b ok">✓ $${(f.cashNeeded/1000).toFixed(0)}k in budget</span>`
+                           : `<span class="b bad">✗ needs $${(f.cashNeeded/1000).toFixed(0)}k</span>`;
+  const rec = p.qwen && p.qwen.recommendation ? `<span class="b rec-${p.qwen.recommendation}">${p.qwen.recommendation}</span>` : '';
+  const statusB = uc ? `<span class="b warn">${p.status}</span>` : (/OUT OF BUDGET/i.test(p.status)?`<span class="b bad">over budget</span>`:`<span class="b ok">Active</span>`);
+  const capB = p.cap_rate!=null
+     ? `<span class="b">${p.cap_rate}% cap${p.cap_rate_projected?` →${p.cap_rate_projected}%*`:''}</span>`
+     : `<span class="b warn">cap n/d</span>`;
+  return `<div class="card ${uc?'uc':''}" id="card-${p.id}">
+    <div class="top">
+      <div><div class="rank">#${p.rank} · score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
+      <div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>
+    </div>
+    <div class="badges"><span class="b type">${p.type}</span><span class="b">${p.units} unit${p.units>1?'s':''}</span><span class="b">${fmt(p.price)}</span>${capB}${statusB} ${aff} ${rec}</div>
+    <div class="metrics">
+      <div><span class="k">Cash-on-cash</span><span class="v ${cls(f.coc,6,3)}">${pct(f.coc)}</span></div>
+      <div><span class="k">DSCR</span><span class="v ${cls(f.dscr,1.25,1.0)}">${f.dscr??'—'}</span></div>
+      <div><span class="k">Est. NOI</span><span class="v">${fmt(f.noi)}</span></div>
+      <div><span class="k">${scenario==='cash'?'Debt svc':'Debt svc/yr'}</span><span class="v">${fmt(f.annualDebtService)}</span></div>
+      <div><span class="k">Cash needed</span><span class="v ${f.affordable?'pos':'neg'}">${fmt(f.cashNeeded)}</span></div>
+      <div><span class="k">Cash flow/yr</span><span class="v ${cls(f.cashFlow,1,0)}">${fmt(f.cashFlow)}</span></div>
+    </div>
+    <div class="thesis">${p.qwen?.thesis||''}</div>
+    ${p.qwen?.risks?.length?`<div><span class="small">Risks</span><ul class="lst">${p.qwen.risks.map(r=>`<li>${r}</li>`).join('')}</ul></div>`:''}
+    ${p.upside_note?`<div class="small">💡 ${p.upside_note}</div>`:''}
+    <div class="src"><a href="${p.source}" target="_blank" rel="noopener noreferrer">View listing ↗</a></div>
+  </div>`;
+}
+
+function render(){
+  let r = DATA.ranked.slice();
+  if(filter==='affordable') r=r.filter(p=>p.finance.affordable);
+  else if(filter==='active') r=r.filter(p=>!/Under Contract|Pending|OUT OF BUDGET/i.test(p.status));
+  else if(filter!=='all') r=r.filter(p=>p.type.toLowerCase().includes(filter));
+  if(maxPrice) r=r.filter(p=>p.price<=maxPrice);
+  const s=$('#sort').value, num=v=>v==null?-1e9:v;
+  const S={ composite:(a,b)=>b.composite-a.composite, coc:(a,b)=>num(b.finance.coc)-num(a.finance.coc),
+    cap:(a,b)=>num(b.cap_rate)-num(a.cap_rate), dscr:(a,b)=>num(b.finance.dscr)-num(a.finance.dscr),
+    priceAsc:(a,b)=>a.price-b.price, priceDesc:(a,b)=>b.price-a.price, cashNeeded:(a,b)=>a.finance.cashNeeded-b.finance.cashNeeded };
+  r.sort(S[s]);
+  $('#grid').innerHTML = r.map(card).join('');
+  $('#foot').textContent = `${r.length} of ${DATA.ranked.length} shown · ${scenario==='cash'?'all-cash':SCEN[scenario].downPct+'% down @ '+SCEN[scenario].ratePct+'%'} · * →X% = projected cap after value-add (not in-place).`;
+}
+
+// ---- context for the chat: the live ranking, compactly ----
+function getContext(){
+  const a=DATA.assumptions, lines=[];
+  lines.push(`MARKET: ${DATA.meta.market}. Budget: $400,000 down. Active scenario: ${scenario==='cash'?'ALL CASH':SCEN[scenario].downPct+'% down, '+SCEN[scenario].ratePct+'%/'+SCEN[scenario].amortYears+'yr, '+SCEN[scenario].closingPct+'% closing'}.`);
+  lines.push(`Filter=${filter}, sort=${$('#sort').value}, maxPrice=${maxPrice||'none'}.`);
+  lines.push(`Verdict: DTD panel 3/3 picked 7302-7304 Canby Ave Reseda (id canby7304). Its 7.8% cap is PROJECTED; actual in-place ~5.96% (NOI $74,497).`);
+  lines.push('RANKED PROPERTIES (current scenario):');
+  DATA.ranked.forEach(p=>{ const f=p.finance;
+    lines.push(`#${p.rank} id=${p.id} | ${p.address}, ${p.city} | ${p.type} | $${p.price.toLocaleString()} | ${p.units}u | cap ${p.cap_rate!=null?p.cap_rate+'%':'n/d'}${p.cap_rate_projected?` (proj ${p.cap_rate_projected}%)`:''} | NOI ${f.noi?'$'+f.noi.toLocaleString():'n/d'} | CoC ${f.coc!=null?f.coc+'%':'n/a'} | DSCR ${f.dscr??'n/a'} | cash needed $${f.cashNeeded.toLocaleString()} ${f.affordable?'(in budget)':'(OVER budget)'} | composite ${p.composite} | rec ${p.qwen?.recommendation||'n/a'} | status ${p.status}`);
+  });
+  return lines.join('\n');
+}
+
+// ---- apply a chat-issued action to the viewer ----
+const VALID = { filter:['all','multifamily','retail','mixed','affordable','active'],
+  sort:['composite','coc','cap','dscr','priceAsc','priceDesc','cashNeeded'], scenario:['25','30','cash'] };
+function applyAction(a){
+  const done=[];
+  if(a.scenario && VALID.scenario.includes(String(a.scenario))){ setScenario(String(a.scenario)); done.push('scenario→'+a.scenario); }
+  if(a.filter && VALID.filter.includes(a.filter)){ filter=a.filter; syncChips(); done.push('filter→'+a.filter); }
+  if(a.type && VALID.filter.includes(a.type)){ filter=a.type; syncChips(); done.push('filter→'+a.type); }
+  if(a.sort && VALID.sort.includes(a.sort)){ $('#sort').value=a.sort; localStorage.setItem('sfv_sort',a.sort); done.push('sort→'+a.sort); }
+  if(a.maxPrice!=null && !isNaN(+a.maxPrice)){ maxPrice=+a.maxPrice; $('#maxPrice').value=+a.maxPrice; done.push('max $'+(+a.maxPrice).toLocaleString()); }
+  if(a.minPrice!=null){ /* minPrice handled via maxPrice-style filter could be added; keep maxPrice primary */ }
+  render();
+  if(a.highlightId){ const el=$('#card-'+a.highlightId); if(el){ el.scrollIntoView({behavior:'smooth',block:'center'}); el.classList.add('flash'); setTimeout(()=>el.classList.remove('flash'),2200); done.push('→'+a.highlightId); } }
+  return done;
+}
+
+function setScenario(s){ scenario=s; localStorage.setItem('sfv_scenario',s);
+  document.querySelectorAll('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===s));
+  recompute(); }
+function syncChips(){ document.querySelectorAll('.chip').forEach(c=>c.classList.toggle('active',c.dataset.f===filter)); }
+
+// ---- chat ----
+function addMsg(role, text, actions){
+  const d=document.createElement('div'); d.className='msg '+role;
+  d.innerHTML = (text||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/```action[\s\S]*?```/g,'').trim()
+    .replace(/\*\*(.+?)\*\*/g,'<b>$1</b>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\n/g,'<br>');
+  if(actions&&actions.length) d.innerHTML += `<div class="actiontag">⚡ view updated: ${actions.join(', ')}</div>`;
+  $('#log').appendChild(d); $('#log').scrollTop=$('#log').scrollHeight; return d;
+}
+function parseAction(reply){ const m=reply.match(/```action\s*([\s\S]*?)```/); if(!m) return null; try{ return JSON.parse(m[1].trim()); }catch{ return null; } }
+
+async function ask(text){
+  addMsg('u', text); history.push({role:'user',content:text});
+  $('#send').disabled=true; const thinking=addMsg('sys','…thinking');
+  try{
+    const r = await fetch('/api/claude-chat',{ method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ messages: history, context: getContext() }) });
+    const j = await r.json(); thinking.remove();
+    if(j.error){ addMsg('sys','⚠ '+j.error); }
+    else {
+      const act = parseAction(j.reply); const applied = act ? applyAction(act) : null;
+      addMsg('a', j.reply, applied);
+      history.push({role:'assistant',content:j.reply});
+    }
+  }catch(e){ thinking.remove(); addMsg('sys','⚠ '+e.message); }
+  $('#send').disabled=false; $('#q').focus();
+}
+
+// ---- boot ----
+fetch('data/ranked.json').then(r=>r.json()).then(d=>{
+  DATA=d;
+  $('#sub').innerHTML = `${d.ranked.length} active listings · <b>${d.meta.market}</b> · budget <b>$400,000</b> down${d.generated?' · sourced '+d.generated:''}`;
+  $('#verdictBody').innerHTML = `<b>DTD panel (3/3):</b> best $400k-down pick = <b>7302-7304 Canby Ave, Reseda</b> (mixed-use, seller financing — the only deal with positive/near-breakeven leverage). <br>
+    <b>DD correction:</b> its headline <b>7.8% cap is PROJECTED</b>; actual in-place ≈ <b>5.96%</b> (NOI $74,497 on $112,200 gross). Price cut $1.399M→$1.25M. <br>
+    <b>Market reality:</b> most SFV multifamily caps (4.5–5%) sit below the ~7.8% debt constant → negative leveraged cash flow at 25% down. Cap rates are broker-stated — verify NOI in due diligence.`;
+  const fs=['all','multifamily','retail','mixed','affordable','active'];
+  $('#filters').innerHTML = fs.map(f=>`<span class="chip ${f==='all'?'active':''}" data-f="${f}">${f[0].toUpperCase()+f.slice(1)}</span>`).join('');
+  $('#filters').onclick=e=>{ if(!e.target.dataset.f) return; filter=e.target.dataset.f; syncChips(); render(); };
+  // persisted controls
+  $('#sort').value = localStorage.getItem('sfv_sort')||'composite';
+  const dv=localStorage.getItem('sfv_density')||'3'; $('#density').value=dv; document.documentElement.style.setProperty('--cols',dv);
+  scenario = localStorage.getItem('sfv_scenario')||'25';
+  document.querySelectorAll('#scenario button').forEach(b=>b.classList.toggle('active',b.dataset.s===scenario));
+  $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };
+  $('#sort').onchange=()=>{ localStorage.setItem('sfv_sort',$('#sort').value); render(); };
+  $('#density').oninput=()=>{ document.documentElement.style.setProperty('--cols',$('#density').value); localStorage.setItem('sfv_density',$('#density').value); };
+  $('#maxPrice').oninput=()=>{ maxPrice = $('#maxPrice').value?+$('#maxPrice').value:null; render(); };
+  // chat
+  const EX=['Why is Canby #1 — is its 7.8% cap real?','Show only deals under $1M at 30% down, sorted by cash-on-cash','Which deals actually cash-flow?','Take me to the best retail/NNN option'];
+  $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
+  $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
+  $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
+  recompute(); render();
+}).catch(e=>{ $('#sub').textContent='Failed to load data/ranked.json — run: npm run analyze'; });
+</script>
+</body>
+</html>
diff --git a/scripts/analyze.js b/scripts/analyze.js
new file mode 100644
index 0000000..fbc5ecb
--- /dev/null
+++ b/scripts/analyze.js
@@ -0,0 +1,83 @@
+// analyze.js — deterministic leverage math (shared finance.js) + local Qwen qualitative read
+// -> data/ranked.json. Qwen responses are cached by content hash so re-runs are instant.
+// Run: node scripts/analyze.js   (Qwen3:14b on localhost:11434, free/local)
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const crypto = require('crypto');
+const CREFin = require('../public/finance.js');
+
+const ROOT = path.join(__dirname, '..');
+const { meta, listings } = JSON.parse(fs.readFileSync(path.join(ROOT, 'data', 'listings.json'), 'utf8'));
+const CACHE_PATH = path.join(ROOT, 'data', 'qwen-cache.json');
+let cache = {};
+try { cache = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8')); } catch (_) {}
+
+// Reference assumptions for the stored finance block (viewer recomputes other scenarios live)
+const A = { budget: 400000, downPct: 25, ratePct: 6.75, amortYears: 30, closingPct: 2.5 };
+
+function qwen(prompt) {
+  return new Promise((resolve) => {
+    const body = JSON.stringify({ model: 'qwen3:14b', prompt: '/no_think ' + prompt, stream: false, think: false,
+      options: { temperature: 0.3, num_predict: 600 } });
+    const req = http.request({ host: 'localhost', port: 11434, path: '/api/generate', method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, (res) => {
+      let d = ''; res.on('data', c => d += c);
+      res.on('end', () => { try { resolve(JSON.parse(d).response || ''); } catch { resolve(''); } });
+    });
+    req.on('error', () => resolve(''));
+    req.setTimeout(60000, () => { req.destroy(); resolve(''); });
+    req.write(body); req.end();
+  });
+}
+const parseJSON = (txt) => { const m = txt.match(/\{[\s\S]*\}/); if (!m) return null; try { return JSON.parse(m[0]); } catch { return null; } };
+
+const PROMPT = (L, f) => `You are a San Fernando Valley commercial real estate investment analyst.
+An investor has $400,000 available as DOWN PAYMENT + closing (leveraged purchase). Evaluate THIS property.
+
+FACTS:
+- Address: ${L.address}, ${L.city} (${L.type})
+- Asking price: $${L.price.toLocaleString()}
+- Units: ${L.units} | SqFt: ${L.sqft || 'n/a'} | Year built: ${L.year_built || 'n/a'}
+- Cap rate: ${L.cap_rate != null ? L.cap_rate + '% (in-place/verified)' : 'NOT DISCLOSED'}${L.cap_rate_projected ? ' | projected ' + L.cap_rate_projected + '% after value-add' : ''}
+- Rent control: ${L.rent_control}
+- Status: ${L.status}
+- Notes: ${L.upside_note}
+COMPUTED (25% down, 6.75%/30yr): cash needed $${f.cashNeeded.toLocaleString()} (${f.affordable ? 'WITHIN' : 'EXCEEDS'} the $400k budget); est NOI ${f.noi ? '$' + f.noi.toLocaleString() : 'unknown'}; cash-on-cash ${f.coc != null ? f.coc + '%' : 'n/a'}; DSCR ${f.dscr != null ? f.dscr : 'n/a'}.
+
+Return ONLY a JSON object:
+{"thesis":"2-3 sentence investment thesis for a $400k-down buyer","risks":["risk1","risk2","risk3"],"upside":["lever1","lever2"],"opportunity_score":<0-100 integer, risk-adjusted for THIS budget>,"recommendation":"Buy"|"Watch"|"Pass"}`;
+
+(async () => {
+  const out = [];
+  let hits = 0, misses = 0;
+  for (let i = 0; i < listings.length; i++) {
+    const L = listings[i];
+    const f = CREFin.finance(L, A);
+    const fs2 = CREFin.financeScore(L, f);
+    // cache key = hash of the facts that feed the prompt
+    const key = crypto.createHash('sha1').update(JSON.stringify([L.id, L.price, L.cap_rate, L.cap_rate_projected, L.units, L.status, L.upside_note])).digest('hex').slice(0, 12);
+    let q, qScore;
+    if (cache[L.id] && cache[L.id].key === key) {
+      q = cache[L.id].qwen; qScore = cache[L.id].qwenScore; hits++;
+      process.stdout.write(`[${i + 1}/${listings.length}] ${L.address} … cached `);
+    } else {
+      process.stdout.write(`[${i + 1}/${listings.length}] ${L.address} … qwen `);
+      const raw = await qwen(PROMPT(L, f));
+      q = parseJSON(raw) || { thesis: '(local model unavailable — finance-only score)', risks: [], upside: [], opportunity_score: null, recommendation: null };
+      qScore = (typeof q.opportunity_score === 'number') ? q.opportunity_score : null;
+      cache[L.id] = { key, qwen: q, qwenScore: qScore };
+      misses++;
+    }
+    const composite = CREFin.composite(L, f, fs2.score, qScore);
+    console.log(`fin=${fs2.score} qwen=${qScore ?? '—'} -> ${composite} (${q.recommendation || 'n/a'})`);
+    out.push({ ...L, finance: f, financeScore: fs2.score, financeConf: fs2.conf, qwen: q, qwenScore: qScore, composite });
+  }
+  out.sort((a, b) => b.composite - a.composite);
+  out.forEach((o, i) => o.rank = i + 1);
+  fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
+  fs.writeFileSync(path.join(ROOT, 'data', 'ranked.json'),
+    JSON.stringify({ meta, assumptions: A, generated: process.env.GEN_TS || null, ranked: out }, null, 2));
+  console.log(`\n=== ranked.json written: ${out.length} properties (qwen ${hits} cached / ${misses} fresh) ===`);
+  console.log('TOP 5:'); out.slice(0, 5).forEach(o => console.log(`  #${o.rank} ${o.composite} — ${o.address}, ${o.city} ($${o.price.toLocaleString()}, ${o.units}u, ${o.cap_rate != null ? o.cap_rate + '% cap' : 'cap n/d'})`));
+})();
diff --git a/scripts/capture-schema.js b/scripts/capture-schema.js
new file mode 100644
index 0000000..702570a
--- /dev/null
+++ b/scripts/capture-schema.js
@@ -0,0 +1,51 @@
+// capture-schema.js — load Crexi UI searches and dump the exact /universal-search/search
+// request body (filter keys) + response (listing shape). One warm session, few navigations.
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default;
+
+const skillEnv = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
+const get = (txt, k) => (txt.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
+const bb = new Browserbase({ apiKey: get(skillEnv, 'BROWSERBASE_API_KEY') });
+const RAW = path.join(__dirname, '..', 'data', 'raw');
+
+(async () => {
+  const session = await bb.sessions.create({ projectId: get(skillEnv, 'BROWSERBASE_PROJECT_ID'),
+    browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 900 } } });
+  console.log('bb session', session.id);
+  const browser = await chromium.connectOverCDP(session.connectUrl);
+  const ctx = browser.contexts()[0];
+  const page = ctx.pages()[0] || await ctx.newPage();
+  page.setDefaultTimeout(60000);
+
+  const caps = [];
+  page.on('response', async (resp) => {
+    const u = resp.url();
+    if (!u.includes('api.crexi.com/universal-search/search')) return;
+    try {
+      const reqBody = resp.request().postData();
+      const body = await resp.json();
+      caps.push({ url: u, reqBody, respKeys: Object.keys(body), sample: (body.data || body.results || body.assets || [])[0] || null,
+        count: (body.data || body.results || body.assets || []).length });
+      console.log(`  captured search → ${ (body.data||body.results||body.assets||[]).length } items`);
+    } catch (_) {}
+  });
+
+  // A search URL that applies type + location text via Crexi's query params
+  const urls = [
+    'https://www.crexi.com/properties?types[]=Multifamily&placeName=Van%20Nuys,%20CA',
+    'https://www.crexi.com/properties/CA/los-angeles',
+  ];
+  for (const url of urls) {
+    console.log('→', url);
+    await page.goto(url, { waitUntil: 'domcontentloaded' }).catch(e => console.log('  nav err', e.message.split('\n')[0]));
+    await page.waitForTimeout(7000);
+    await page.mouse.wheel(0, 3000).catch(()=>{});
+    await page.waitForTimeout(3000);
+  }
+  fs.writeFileSync(path.join(RAW, 'search-capture.json'), JSON.stringify(caps, null, 2));
+  console.log(`\n=== saved ${caps.length} /universal-search/search captures ===`);
+  await browser.close().catch(()=>{});
+  process.exit(0);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/discover.js b/scripts/discover.js
new file mode 100644
index 0000000..89fefd4
--- /dev/null
+++ b/scripts/discover.js
@@ -0,0 +1,87 @@
+// discover.js — one warm Crexi session; (1) log every POST the UI makes to api.crexi.com
+// with path+item-count, (2) run in-page fetch experiments against /universal-search/search
+// to find the filter schema that returns SFV multifamily listings.
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default;
+const skillEnv = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
+const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
+const bb = new Browserbase({ apiKey: get(skillEnv, 'BROWSERBASE_API_KEY') });
+const RAW = path.join(__dirname, '..', 'data', 'raw');
+
+(async () => {
+  const session = await bb.sessions.create({ projectId: get(skillEnv, 'BROWSERBASE_PROJECT_ID'),
+    browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 900 } } });
+  console.log('bb session', session.id);
+  const browser = await chromium.connectOverCDP(session.connectUrl);
+  const ctx = browser.contexts()[0];
+  const page = ctx.pages()[0] || await ctx.newPage();
+  page.setDefaultTimeout(60000);
+
+  const posts = [];
+  page.on('response', async (resp) => {
+    const u = new URL(resp.url());
+    if (!u.hostname.includes('api.crexi.com')) return;
+    if (resp.request().method() !== 'POST') return;
+    try {
+      const j = await resp.json();
+      const arr = Array.isArray(j) ? j : (j.data || j.results || j.assets || j.items || []);
+      posts.push({ path: u.pathname, body: resp.request().postData(), count: Array.isArray(arr) ? arr.length : 0,
+        topKeys: Array.isArray(j) ? '[array]' : Object.keys(j).slice(0, 12),
+        sampleLoc: (arr[0] && (arr[0].address || (arr[0].locations && arr[0].locations[0]))) || null });
+    } catch (_) {}
+  });
+
+  console.log('→ warming crexi.com search');
+  await page.goto('https://www.crexi.com/properties', { waitUntil: 'domcontentloaded' }).catch(()=>{});
+  await page.waitForTimeout(8000);
+  await page.mouse.wheel(0, 3000).catch(()=>{}); await page.waitForTimeout(4000);
+
+  // Save what the UI itself POSTed (ground-truth schema)
+  fs.writeFileSync(path.join(RAW, 'ui-posts.json'), JSON.stringify(posts, null, 2));
+  console.log(`UI made ${posts.length} POSTs to api.crexi.com:`);
+  posts.forEach(p => console.log(`  ${p.path}  count=${p.count}  keys=${JSON.stringify(p.topKeys)}`));
+
+  // In-page experiments against /universal-search/search
+  const SFV_BBOX = { north: 34.33, south: 34.12, east: -118.28, west: -118.68 };
+  const experiments = [
+    { tag: 'empty', filters: {} },
+    { tag: 'types-Multifamily', filters: { types: ['Multifamily'] } },
+    { tag: 'assetTypes', filters: { assetTypes: ['Multifamily'] } },
+    { tag: 'propertyTypes', filters: { propertyTypes: ['Multifamily'] } },
+    { tag: 'bbox-mapBounds', filters: { mapBounds: SFV_BBOX } },
+    { tag: 'bbox-geo', filters: { boundingBox: { neLat: SFV_BBOX.north, neLon: SFV_BBOX.east, swLat: SFV_BBOX.south, swLon: SFV_BBOX.west } } },
+    { tag: 'city', filters: { 'address.city': ['Van Nuys'] } },
+    { tag: 'locations', filters: { locations: [{ city: 'Van Nuys', state: 'CA' }] } },
+  ];
+  const results = [];
+  for (const ex of experiments) {
+    const r = await page.evaluate(async ({ filters }) => {
+      const attempts = [];
+      for (const shape of [
+        { filters, searchTypes: ['Sales'], size: 5 },
+        { filters, searchTypes: ['Sales'], size: 5, from: 0 },
+        { filters, searchTypes: ['Sales'], pageSize: 5, page: 1 },
+      ]) {
+        try {
+          const res = await fetch('https://api.crexi.com/universal-search/search', {
+            method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(shape) });
+          const txt = await res.text();
+          let j; try { j = JSON.parse(txt); } catch { j = null; }
+          const arr = j ? (Array.isArray(j) ? j : (j.data || j.results || j.assets || j.items || [])) : [];
+          attempts.push({ status: res.status, count: Array.isArray(arr) ? arr.length : 0,
+            firstCity: arr[0] && (arr[0].locations?.[0]?.city || arr[0].address?.city || arr[0].city) || null,
+            err: (j && j.message) || (txt.slice(0, 120)) });
+        } catch (e) { attempts.push({ error: String(e).slice(0, 100) }); }
+      }
+      return attempts;
+    }, ex);
+    const best = r.find(a => a.count > 0) || r[0];
+    console.log(`  [${ex.tag}] ${JSON.stringify(best)}`);
+    results.push({ tag: ex.tag, filters: ex.filters, attempts: r });
+  }
+  fs.writeFileSync(path.join(RAW, 'experiments.json'), JSON.stringify(results, null, 2));
+  await browser.close().catch(()=>{});
+  process.exit(0);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/discover2.js b/scripts/discover2.js
new file mode 100644
index 0000000..ea6dbbd
--- /dev/null
+++ b/scripts/discover2.js
@@ -0,0 +1,71 @@
+// discover2.js — capture Crexi's anonymous Bearer token + the UI's exact /universal-search
+// request body, then replay POST /universal-search/search (token attached) with SFV filters.
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default;
+const skillEnv = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
+const get = (t, k) => (t.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
+const bb = new Browserbase({ apiKey: get(skillEnv, 'BROWSERBASE_API_KEY') });
+const RAW = path.join(__dirname, '..', 'data', 'raw');
+
+(async () => {
+  const session = await bb.sessions.create({ projectId: get(skillEnv, 'BROWSERBASE_PROJECT_ID'),
+    browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 900 } } });
+  console.log('bb session', session.id);
+  const browser = await chromium.connectOverCDP(session.connectUrl);
+  const ctx = browser.contexts()[0];
+  const page = ctx.pages()[0] || await ctx.newPage();
+  page.setDefaultTimeout(60000);
+
+  let bearer = null;
+  const uiBodies = [];
+  page.on('request', (req) => {
+    const u = req.url();
+    if (!u.includes('api.crexi.com')) return;
+    const auth = req.headers()['authorization'];
+    if (auth && auth.toLowerCase().startsWith('bearer') && !bearer) { bearer = auth; console.log('  got bearer token'); }
+    if (req.method() === 'POST' && u.includes('universal-search')) {
+      const pd = req.postData(); if (pd) uiBodies.push({ url: new URL(u).pathname, body: pd });
+    }
+  });
+
+  console.log('→ warm + interact');
+  await page.goto('https://www.crexi.com/properties?types[]=Multifamily', { waitUntil: 'domcontentloaded' }).catch(()=>{});
+  await page.waitForTimeout(8000);
+  for (let i = 0; i < 4; i++) { await page.mouse.wheel(0, 3000).catch(()=>{}); await page.waitForTimeout(2500); }
+  fs.writeFileSync(path.join(RAW, 'ui-bodies.json'), JSON.stringify(uiBodies, null, 2));
+  console.log(`  UI universal-search POST bodies: ${uiBodies.length}`);
+  uiBodies.slice(0,3).forEach(b => console.log('   ', b.url, b.body.slice(0,300)));
+
+  if (!bearer) { console.log('NO BEARER — aborting'); await browser.close().catch(()=>{}); process.exit(2); }
+
+  // Replay /universal-search/search with token + SFV filters. Try several filter shapes.
+  const SFV = { north: 34.33, south: 34.12, east: -118.28, west: -118.68 };
+  const shapes = [
+    { tag: 'types', filters: { types: ['Multifamily'] } },
+    { tag: 'mapBounds', filters: { mapBounds: { northEast: { lat: SFV.north, lon: SFV.east }, southWest: { lat: SFV.south, lon: SFV.west } } } },
+    { tag: 'mapBox', filters: { mapBox: SFV } },
+    { tag: 'geography', filters: { geography: { boundingBox: SFV } } },
+    { tag: 'searchText', filters: {}, searchText: 'Van Nuys, CA' },
+  ];
+  const out = [];
+  for (const s of shapes) {
+    const r = await page.evaluate(async ({ bearer, s }) => {
+      const body = Object.assign({ filters: s.filters, searchTypes: ['Sales'], size: 5, from: 0 }, s.searchText ? { searchText: s.searchText } : {});
+      const res = await fetch('https://api.crexi.com/universal-search/search', {
+        method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': bearer }, body: JSON.stringify(body) });
+      const txt = await res.text(); let j; try { j = JSON.parse(txt); } catch { j = null; }
+      const arr = j ? (Array.isArray(j) ? j : (j.data || j.results || j.assets || j.items || [])) : [];
+      return { status: res.status, total: j && (j.total || j.totalCount), count: arr.length,
+        cities: arr.slice(0,5).map(a => a.locations?.[0]?.city || a.address?.city || a.city),
+        sampleKeys: arr[0] ? Object.keys(arr[0]).slice(0,30) : null, err: (j && j.message) || txt.slice(0,150) };
+    }, { bearer, s });
+    console.log(`  [${s.tag}] status=${r.status} count=${r.count} total=${r.total} cities=${JSON.stringify(r.cities)}`);
+    out.push({ tag: s.tag, ...r });
+  }
+  fs.writeFileSync(path.join(RAW, 'replay.json'), JSON.stringify(out, null, 2));
+  fs.writeFileSync(path.join(RAW, 'bearer.txt'), bearer);
+  await browser.close().catch(()=>{});
+  process.exit(0);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/scrape-crexi.js b/scripts/scrape-crexi.js
new file mode 100644
index 0000000..2e72688
--- /dev/null
+++ b/scripts/scrape-crexi.js
@@ -0,0 +1,114 @@
+// scrape-crexi.js — Drive a real Browserbase cloud browser to Crexi, capture the
+// PROPERLY-FILTERED /assets/search XHR JSON (the public api.crexi.com feed ignores
+// our body when called raw; from a real session it returns city/type/price-scoped data).
+//
+// Strategy: navigate Crexi search for each SFV city, listen for api.crexi.com responses
+// carrying a `data[]` array, dedupe by asset id, save everything to data/raw/.
+// Also dump the request postData of /assets/search so we learn the real filter schema.
+const fs = require('fs');
+const path = require('path');
+const { chromium } = require('playwright-core');
+const Browserbase = require('@browserbasehq/sdk').default;
+
+const skillEnv = fs.readFileSync(process.env.HOME + '/.claude/skills/browserbase/.env', 'utf8');
+const get = (txt, k) => (txt.match(new RegExp('^' + k + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim();
+const PROJECT = get(skillEnv, 'BROWSERBASE_PROJECT_ID');
+const KEY = get(skillEnv, 'BROWSERBASE_API_KEY');
+
+const RAW = path.join(__dirname, '..', 'data', 'raw');
+fs.mkdirSync(RAW, { recursive: true });
+
+// SFV cities (full San Fernando Valley coverage)
+const SFV_CITIES = [
+  'Van Nuys', 'Sherman Oaks', 'North Hollywood', 'Encino', 'Reseda', 'Northridge',
+  'Canoga Park', 'Woodland Hills', 'Tarzana', 'Panorama City', 'Sun Valley', 'Pacoima',
+  'Granada Hills', 'Chatsworth', 'Winnetka', 'Sylmar', 'Mission Hills', 'Valley Village',
+  'Studio City', 'Arleta', 'Lake Balboa', 'West Hills', 'Porter Ranch', 'Sunland', 'Tujunga'
+];
+
+const TYPE_SLUGS = { Multifamily: 'multifamily', Retail: 'retail' };
+
+// CRE_LIMIT=N caps cities (probe mode); CRE_TYPES=Multifamily limits types
+const LIMIT = parseInt(process.env.CRE_LIMIT || '0', 10);
+const CITIES = LIMIT ? SFV_CITIES.slice(0, LIMIT) : SFV_CITIES;
+const TYPES = process.env.CRE_TYPES
+  ? Object.fromEntries(Object.entries(TYPE_SLUGS).filter(([k]) => process.env.CRE_TYPES.includes(k)))
+  : TYPE_SLUGS;
+
+(async () => {
+  const bb = new Browserbase({ apiKey: KEY });
+  const session = await bb.sessions.create({
+    projectId: PROJECT,
+    browserSettings: { solveCaptchas: true, viewport: { width: 1440, height: 900 } }
+  });
+  console.log('bb session', session.id, '| live:', session.debuggerFullscreenUrl || '(n/a)');
+  const browser = await chromium.connectOverCDP(session.connectUrl);
+  const ctx = browser.contexts()[0];
+  const page = ctx.pages()[0] || await ctx.newPage();
+  page.setDefaultTimeout(60000);
+
+  const assets = new Map();           // id -> asset object
+  const searchBodies = [];            // captured request postData schemas
+  let respCount = 0;
+
+  page.on('request', (req) => {
+    const u = req.url();
+    if (u.includes('api.crexi.com') && u.includes('search') && req.method() === 'POST') {
+      const pd = req.postData();
+      if (pd) searchBodies.push({ url: u, body: pd });
+    }
+  });
+
+  page.on('response', async (resp) => {
+    const u = resp.url();
+    if (!u.includes('api.crexi.com')) return;
+    if (resp.request().resourceType() === 'preflight') return;
+    try {
+      const ct = resp.headers()['content-type'] || '';
+      if (!ct.includes('json')) return;
+      const j = await resp.json();
+      const arr = Array.isArray(j) ? j : (j.data || j.assets || j.results || null);
+      if (Array.isArray(arr) && arr.length && arr[0] && (arr[0].id || arr[0].assetId)) {
+        respCount++;
+        let added = 0;
+        for (const a of arr) {
+          const id = a.id || a.assetId;
+          if (id && !assets.has(id)) { assets.set(id, a); added++; }
+        }
+        console.log(`  xhr#${respCount} ${u.split('?')[0].split('crexi.com')[1]} +${added} (total ${assets.size})`);
+      }
+    } catch (_) { /* non-json or consumed */ }
+  });
+
+  async function visit(url, label) {
+    try {
+      console.log(`\n→ ${label}: ${url}`);
+      await page.goto(url, { waitUntil: 'domcontentloaded' });
+      await page.waitForTimeout(5000);
+      // scroll to trigger lazy XHR / pagination
+      for (let i = 0; i < 3; i++) {
+        await page.mouse.wheel(0, 4000).catch(() => {});
+        await page.waitForTimeout(2500);
+      }
+    } catch (e) { console.log(`  ! ${label} error: ${e.message.split('\n')[0]}`); }
+  }
+
+  // 1) Warm a session on a search page so cookies/headers are set
+  await visit('https://www.crexi.com/properties?types[]=Multifamily', 'warm: MF search');
+
+  // 2) Per-city, per-type city landing pages (these fire city-scoped /assets/search)
+  for (const [type, slug] of Object.entries(TYPES)) {
+    for (const city of CITIES) {
+      const citySlug = city.replace(/\s+/g, '-');
+      const url = `https://www.crexi.com/properties/CA/${encodeURIComponent(citySlug)}/${slug}`;
+      await visit(url, `${type} / ${city}`);
+      fs.writeFileSync(path.join(RAW, 'assets.json'), JSON.stringify([...assets.values()], null, 2));
+    }
+  }
+
+  fs.writeFileSync(path.join(RAW, 'assets.json'), JSON.stringify([...assets.values()], null, 2));
+  fs.writeFileSync(path.join(RAW, 'search-bodies.json'), JSON.stringify(searchBodies.slice(0, 20), null, 2));
+  console.log(`\n=== DONE: ${assets.size} unique assets captured, ${searchBodies.length} search bodies ===`);
+  await browser.close().catch(() => {});
+  process.exit(0);
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/scripts/serve.js b/scripts/serve.js
new file mode 100644
index 0000000..36f1393
--- /dev/null
+++ b/scripts/serve.js
@@ -0,0 +1,38 @@
+// serve.js — Express server for the SFV CRE viewer + grounded control-chat.
+// Static viewer + /data + the run-notes-chat bridge (free Claude CLI via Max sub).
+// Run: node scripts/serve.js   ->  http://127.0.0.1:9911
+const express = require('express');
+const path = require('path');
+const mountClaudeChat = require('../claude-chat-bridge.cjs');
+
+const ROOT = path.join(__dirname, '..');
+const PORT = process.env.PORT || 9911;
+const app = express();
+app.use(express.json({ limit: '256kb' }));
+
+// The chat can DRIVE the viewer. This protocol is how it does it.
+const SYSTEM_EXTRA = `You are a commercial-real-estate investment analyst embedded in the "San Fernando Valley CRE — $400k Down" ranking viewer. The context block below is the LIVE ranked dataset the user is looking at (every property, its price, units, cap rate, cash-on-cash, DSCR, composite score, rank, recommendation, status, and notes), plus the financing assumptions and the analyst verdict.
+
+GROUNDING RULES:
+- Answer ONLY from the data in the context. Never invent properties, prices, cap rates, or returns not present.
+- Cap rates marked "projected" are NOT in-place income — always flag that distinction (e.g. the Reseda mixed-use 7302-7304 Canby: ~6% actual vs 7.8% projected).
+- Be concise: short paragraphs + bullets. Give numbers.
+
+LIVE CONTROL: the user can ask you to change what the viewer shows. When they want to filter, sort, change the down-payment scenario, cap the price, or jump to a property, append EXACTLY ONE fenced code block at the very END of your reply:
+\`\`\`action
+{ ...only the keys that should change... }
+\`\`\`
+Action schema (omit keys that don't change):
+- "scenario": "25" | "30" | "cash"   (down-payment model)
+- "filter": "all" | "multifamily" | "retail" | "mixed" | "affordable" | "active"
+- "sort": "composite" | "coc" | "cap" | "dscr" | "priceAsc" | "priceDesc" | "cashNeeded"
+- "maxPrice": <number USD>     "minPrice": <number USD>
+- "highlightId": "<property id from the context>"   (scrolls to + flashes that card)
+Rules: explain in prose ABOVE the block. Only emit an action block when the user actually wants the view changed — for pure questions, NO action block. Use the exact ids/enum values above.`;
+
+app.use('/data', express.static(path.join(ROOT, 'data')));
+app.use('/', express.static(path.join(ROOT, 'public')));
+
+mountClaudeChat(app, { path: '/api/claude-chat', surface: 'sfv-cre', defaultModel: 'claude-sonnet-4-6', systemExtra: SYSTEM_EXTRA });
+
+app.listen(PORT, '127.0.0.1', () => console.log(`SFV CRE viewer + chat -> http://127.0.0.1:${PORT}`));

← 33e1972 initial scaffold: SFV CRE investment ranker  ·  back to Commercialrealestate  ·  Ranking integrity: verify top picks vs MLS + unverified-NOI 0feadae →