[object Object]

← back to Ventura Claw

Snapshot: ElevenLabs + Shopify connectors + coming-soon page

45dc5f63535d247b8ab49dd6eba95721cb9275f3 · 2026-05-06 12:53:12 -0700 · Steve Abrams

server/connectors/elevenlabs.js — full impl that was missed in the
earlier commit (file was untracked)
server/connectors/shopify.js — DW prod store connector (read shop info,
products, orders; write order.fulfill, order.refund, product.update)
server/connectors/index.js — register both with proper write/read action
sets; Shopify writes are sensitive (gated)
server/public/coming-soon.html — placeholder
server/server.js — adjacent updates

Files touched

Diff

commit 45dc5f63535d247b8ab49dd6eba95721cb9275f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 6 12:53:12 2026 -0700

    Snapshot: ElevenLabs + Shopify connectors + coming-soon page
    
    server/connectors/elevenlabs.js — full impl that was missed in the
    earlier commit (file was untracked)
    server/connectors/shopify.js — DW prod store connector (read shop info,
    products, orders; write order.fulfill, order.refund, product.update)
    server/connectors/index.js — register both with proper write/read action
    sets; Shopify writes are sensitive (gated)
    server/public/coming-soon.html — placeholder
    server/server.js — adjacent updates
---
 server/connectors/elevenlabs.js |  87 +++++++++++
 server/connectors/index.js      |   5 +-
 server/connectors/shopify.js    |  78 ++++++++++
 server/public/coming-soon.html  | 325 ++++++++++++++++++++++++++++++++++++++++
 server/server.js                |   2 +
 5 files changed, 496 insertions(+), 1 deletion(-)

diff --git a/server/connectors/elevenlabs.js b/server/connectors/elevenlabs.js
new file mode 100644
index 0000000..0fe949b
--- /dev/null
+++ b/server/connectors/elevenlabs.js
@@ -0,0 +1,87 @@
+// ElevenLabs — REAL. Text-to-speech generation + voice library + user account.
+// Free tier: 10K characters/month. Voice cloning available on paid tiers.
+function key(creds) {
+  const k = creds?.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY;
+  if (!k) throw new Error("ELEVENLABS_API_KEY not set");
+  return k;
+}
+
+async function call(method, path, body, creds, accept = "application/json") {
+  const r = await fetch("https://api.elevenlabs.io/v1" + path, {
+    method,
+    headers: {
+      "xi-api-key": key(creds),
+      "Content-Type": "application/json",
+      "Accept": accept,
+    },
+    body: body ? JSON.stringify(body) : undefined,
+  });
+  if (accept === "audio/mpeg") {
+    if (!r.ok) {
+      const txt = await r.text();
+      throw new Error(`elevenlabs ${path} ${r.status}: ${txt.slice(0, 200)}`);
+    }
+    const buf = Buffer.from(await r.arrayBuffer());
+    return { audio_mp3_base64: buf.toString("base64"), bytes: buf.length };
+  }
+  const d = r.status === 204 ? {} : await r.json();
+  if (!r.ok) throw new Error(`elevenlabs ${path} ${r.status}: ${d.detail?.message || d.message || JSON.stringify(d).slice(0, 200)}`);
+  return d;
+}
+
+module.exports = {
+  meta: {
+    id: "elevenlabs",
+    name: "ElevenLabs",
+    category: "ai-voice",
+    docsUrl: "https://elevenlabs.io/docs/api-reference/introduction",
+    auth: "api_key",
+    realImpl: true,
+  },
+  fields: [
+    { key: "ELEVENLABS_API_KEY", label: "API Key", type: "password", required: true, hint: "elevenlabs.io → Profile → API Keys (free tier: 10K chars/month)" },
+  ],
+  configured(c) { return !!(c?.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY); },
+
+  async health(c) {
+    if (!this.configured(c)) return { ok: false, reason: "no_token" };
+    try {
+      const u = await call("GET", "/user", null, c);
+      return {
+        ok: true,
+        subscription: u.subscription?.tier || "free",
+        chars_used: u.subscription?.character_count || 0,
+        chars_limit: u.subscription?.character_limit || 0,
+      };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+
+  // Read actions
+  async voicesList(c) {
+    const d = await call("GET", "/voices", null, c);
+    return { voices: (d.voices || []).map(v => ({
+      id: v.voice_id, name: v.name, category: v.category,
+      labels: v.labels || {}, description: v.description || "",
+      preview_url: v.preview_url || "",
+    })) };
+  },
+
+  async user(c) { return call("GET", "/user", null, c); },
+
+  async modelsList(c) { return call("GET", "/models", null, c); },
+
+  // Write actions (TTS generation counts against free-tier quota)
+  async textToSpeech(c, args) {
+    const { voice_id, text, model_id, stability, similarity_boost } = args || {};
+    if (!voice_id) throw new Error("voice_id required");
+    if (!text)     throw new Error("text required");
+    return call("POST", `/text-to-speech/${voice_id}`, {
+      text: String(text).slice(0, 5000),
+      model_id: model_id || "eleven_turbo_v2_5",
+      voice_settings: {
+        stability: Number.isFinite(stability) ? stability : 0.5,
+        similarity_boost: Number.isFinite(similarity_boost) ? similarity_boost : 0.75,
+      },
+    }, c, "audio/mpeg");
+  },
+};
diff --git a/server/connectors/index.js b/server/connectors/index.js
index a013593..2a36560 100644
--- a/server/connectors/index.js
+++ b/server/connectors/index.js
@@ -14,8 +14,9 @@ const gmail      = require("./gmail");
 const purelymail = require("./purelymail");
 const archive    = require("./archive");
 const elevenlabs = require("./elevenlabs");
+const shopify    = require("./shopify");
 
-const REAL = { slack, stripe, cloudflare, mailchimp, hubspot, notion, airtable, twilio, figma, canva, etsy, gmail, purelymail, archive, elevenlabs };
+const REAL = { slack, stripe, cloudflare, mailchimp, hubspot, notion, airtable, twilio, figma, canva, etsy, gmail, purelymail, archive, elevenlabs, shopify };
 
 // Phase 8 — central sensitivity registry. Source of truth for "this action mutates state."
 // Anything in WRITE_ACTIONS[id] requires the approval gate. Anything NOT listed is treated as read-only.
@@ -36,6 +37,7 @@ const WRITE_ACTIONS = {
   stripe:     new Set(["charge.create", "customer.create", "payment_link.create", "refund.create", "subscription.cancel"]),
   twilio:     new Set(["sms.send"]),
   elevenlabs: new Set(["textToSpeech"]),
+  shopify:    new Set(["order.fulfill", "order.refund", "product.update"]),
 };
 
 // Read actions enumerated per connector — explicit allowlist for fail-safe behavior.
@@ -55,6 +57,7 @@ const READ_ACTIONS = {
   stripe:     new Set(["balance.get", "charges.list"]),
   twilio:     new Set(["messages.list", "phone_numbers.list"]),
   elevenlabs: new Set(["voicesList", "user", "modelsList"]),
+  shopify:    new Set(["shop.info", "products.list", "products.count", "orders.list", "orders.recent", "collections.list"]),
 };
 
 function isWrite(id, action) {
diff --git a/server/connectors/shopify.js b/server/connectors/shopify.js
new file mode 100644
index 0000000..49f9384
--- /dev/null
+++ b/server/connectors/shopify.js
@@ -0,0 +1,78 @@
+// Shopify (DW prod store) — REAL.
+// Reads SHOPIFY_ADMIN_TOKEN + SHOPIFY_STORE from env. Routed via secrets-manager.
+// Docs: https://shopify.dev/docs/api/admin-rest
+
+function token(c) {
+  const t = c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN;
+  if (!t) throw new Error("SHOPIFY_ADMIN_TOKEN not set");
+  return t;
+}
+function store(c) {
+  return c?.SHOPIFY_STORE || process.env.SHOPIFY_STORE || "designer-laboratory-sandbox.myshopify.com";
+}
+async function call(method, path, body, c) {
+  const url = `https://${store(c)}/admin/api/2024-10${path}`;
+  const r = await fetch(url, {
+    method,
+    headers: { "X-Shopify-Access-Token": token(c), "Content-Type": "application/json" },
+    body: body ? JSON.stringify(body) : undefined,
+    signal: AbortSignal.timeout(20_000)
+  });
+  const text = await r.text();
+  if (!r.ok) throw new Error(`shopify ${method} ${path} ${r.status}: ${text.slice(0, 200)}`);
+  return text ? JSON.parse(text) : {};
+}
+
+module.exports = {
+  meta: { id: "shopify", name: "Shopify", category: "commerce", docsUrl: "https://shopify.dev/docs/api/admin-rest", auth: "api_key", realImpl: true },
+  fields: [
+    { key: "SHOPIFY_ADMIN_TOKEN", label: "Admin Access Token (shpat_...)", type: "password", required: true, hint: "Settings → Apps → Develop apps → reveal Admin API access token" },
+    { key: "SHOPIFY_STORE",       label: "Store domain",                    type: "text",     required: false, hint: "e.g. mystore.myshopify.com (optional, defaults to env)" },
+  ],
+  configured(c) { return !!(c?.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN || process.env.SHOPIFY_ACCESS_TOKEN); },
+  async health(c) {
+    if (!this.configured(c)) return { ok: false, reason: "no_token" };
+    try {
+      const d = await call("GET", "/shop.json", null, c);
+      return { ok: true, store: d.shop?.name, domain: d.shop?.myshopify_domain, plan: d.shop?.plan_name };
+    } catch (e) { return { ok: false, reason: e.message }; }
+  },
+  actions: {
+    async "shop.info"(_, c) { return call("GET", "/shop.json", null, c); },
+    async "products.list"({ limit }, c) { return call("GET", `/products.json?limit=${Math.min(limit||10, 250)}`, null, c); },
+    async "products.count"(_, c) { return call("GET", "/products/count.json", null, c); },
+    async "orders.list"({ status, limit }, c) {
+      const s = status || "any";
+      return call("GET", `/orders.json?status=${s}&limit=${Math.min(limit||10, 250)}`, null, c);
+    },
+    async "orders.recent"(_, c) {
+      return call("GET", `/orders.json?status=any&limit=20&order=created_at desc`, null, c);
+    },
+    async "order.fulfill"({ order_id, tracking_number, tracking_company }, c) {
+      if (!order_id) throw new Error("order_id required");
+      return call("POST", `/orders/${order_id}/fulfillments.json`, {
+        fulfillment: { tracking_number, tracking_company, notify_customer: true }
+      }, c);
+    },
+    async "order.refund"({ order_id, amount, reason }, c) {
+      if (!order_id) throw new Error("order_id required");
+      return call("POST", `/orders/${order_id}/refunds.json`, {
+        refund: { note: reason || "automated refund", shipping: { full_refund: true }, transactions: amount ? [{ amount, kind: "refund" }] : undefined }
+      }, c);
+    },
+    async "product.update"({ product_id, title, tags, body_html, status }, c) {
+      if (!product_id) throw new Error("product_id required");
+      const fields = { id: product_id };
+      if (title !== undefined)     fields.title = title;
+      if (tags !== undefined)      fields.tags = tags;
+      if (body_html !== undefined) fields.body_html = body_html;
+      if (status !== undefined)    fields.status = status;
+      return call("PUT", `/products/${product_id}.json`, { product: fields }, c);
+    },
+    async "collections.list"(_, c) {
+      const cust = await call("GET", "/custom_collections.json?limit=250", null, c);
+      const smart = await call("GET", "/smart_collections.json?limit=250", null, c);
+      return { custom: cust.custom_collections || [], smart: smart.smart_collections || [] };
+    },
+  }
+};
diff --git a/server/public/coming-soon.html b/server/public/coming-soon.html
new file mode 100644
index 0000000..4bb9473
--- /dev/null
+++ b/server/public/coming-soon.html
@@ -0,0 +1,325 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width,initial-scale=1" />
+<meta name="theme-color" content="#0e0e10" />
+<title>VenturaClaw — Coming soon · One AI command for 56 SaaS tools</title>
+<meta name="description" content="VenturaClaw is launching soon. Type one sentence — the AI picks the right SaaS tool from 56 wired-in connectors and runs the action. Drop your email for first-access." />
+<meta name="robots" content="index,follow" />
+<link rel="canonical" href="https://venturaclaw.com/" />
+<meta property="og:type" content="website" />
+<meta property="og:title" content="VenturaClaw — Coming soon" />
+<meta property="og:description" content="One AI command, 56 connectors, zero workflow building. Launching soon — get on the early-access list." />
+<meta property="og:url" content="https://venturaclaw.com/" />
+<meta property="og:site_name" content="VenturaClaw" />
+<meta name="twitter:card" content="summary_large_image" />
+<style>
+  :root {
+    --bg: #0e0e10;
+    --bg-2: #15151a;
+    --rule: #2a2a32;
+    --ink: #f4f1ea;
+    --ink-soft: #d8d2c5;
+    --ink-mute: #8b857a;
+    --gold: #d4a04a;
+    --gold-glow: #e6b96a;
+    --good: #6ec081;
+    --serif: "Cormorant Garamond", Georgia, serif;
+    --mono: "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, monospace;
+    --sans: -apple-system, "SF Pro Text", "Inter", system-ui, sans-serif;
+    --max: 1100px;
+  }
+  *, *::before, *::after { box-sizing: border-box; }
+  html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: var(--sans); font-weight: 300; }
+  a { color: var(--gold); }
+  ::selection { background: var(--gold); color: var(--bg); }
+
+  /* topbar */
+  .topbar {
+    max-width: var(--max);
+    margin: 0 auto;
+    padding: 22px 28px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+  .brand { display: flex; gap: 12px; align-items: center; }
+  .logo-dot {
+    width: 14px; height: 14px; border-radius: 50%;
+    background: radial-gradient(circle at 30% 30%, var(--gold-glow), var(--gold) 60%, #7a5a26);
+    box-shadow: 0 0 18px rgba(212, 160, 74, 0.45);
+  }
+  .brand-name { font-family: var(--serif); font-size: 22px; font-weight: 500; letter-spacing: -0.005em; }
+  .brand-name em { font-style: italic; color: var(--gold); }
+  .brand-sub { font-family: var(--mono); font-size: 9px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--ink-mute); margin-top: 2px; }
+  .status-pill {
+    font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
+    color: var(--gold); border: 1px solid var(--gold); padding: 8px 14px; border-radius: 999px;
+    display: inline-flex; align-items: center; gap: 8px;
+  }
+  .status-pill::before {
+    content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
+    background: var(--gold-glow); box-shadow: 0 0 10px var(--gold-glow);
+    animation: pulse 1.8s infinite;
+  }
+  @keyframes pulse {
+    0%, 100% { opacity: 1; transform: scale(1); }
+    50%      { opacity: 0.55; transform: scale(0.85); }
+  }
+
+  /* hero */
+  .hero { max-width: var(--max); margin: 56px auto 0; padding: 0 28px; }
+  .eyebrow {
+    font-family: var(--mono); font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
+    color: var(--ink-mute); margin-bottom: 22px; display: flex; align-items: center; gap: 10px;
+  }
+  .eyebrow .dot {
+    display: inline-block; width: 6px; height: 6px; border-radius: 50%;
+    background: var(--good); box-shadow: 0 0 10px var(--good);
+  }
+  .headline {
+    font-family: var(--serif); font-weight: 400; line-height: 1.05;
+    font-size: clamp(46px, 7.5vw, 92px); margin: 0 0 24px; letter-spacing: -0.012em;
+  }
+  .headline em { font-style: italic; color: var(--gold); }
+  .subhead {
+    font-size: 19px; line-height: 1.6; color: var(--ink-soft);
+    max-width: 720px; margin: 0 0 40px;
+  }
+
+  /* soon card */
+  .soon-card {
+    background: linear-gradient(180deg, var(--bg-2), rgba(21, 21, 26, 0.4));
+    border: 1px solid var(--rule);
+    border-left: 3px solid var(--gold);
+    padding: 32px 36px;
+    max-width: 640px;
+    margin: 0 0 56px;
+  }
+  .soon-card h2 {
+    font-family: var(--serif); font-weight: 400; font-size: 28px; margin: 0 0 14px; letter-spacing: -0.005em;
+  }
+  .soon-card p { font-size: 15px; line-height: 1.65; color: var(--ink-soft); margin: 0 0 20px; }
+  .soon-card .ml { margin-bottom: 8px; }
+  .soon-card form { display: flex; gap: 8px; flex-wrap: wrap; }
+  .soon-card input[type=email] {
+    flex: 1; min-width: 240px;
+    padding: 14px 18px;
+    background: var(--bg);
+    border: 1px solid var(--rule);
+    color: var(--ink);
+    font-family: var(--sans); font-size: 14px;
+    outline: none;
+    transition: border-color 200ms ease;
+  }
+  .soon-card input[type=email]:focus { border-color: var(--gold); box-shadow: 0 0 0 3px rgba(212, 160, 74, 0.12); }
+  .soon-card button {
+    background: var(--gold); color: var(--bg);
+    border: 0; padding: 14px 22px;
+    font-family: var(--mono); font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; font-weight: 600;
+    cursor: pointer;
+    transition: background 200ms ease;
+  }
+  .soon-card button:hover { background: var(--gold-glow); }
+  .soon-card .ack {
+    display: none;
+    margin-top: 14px; padding: 12px 14px;
+    background: rgba(110, 192, 129, 0.08); border: 1px solid var(--good);
+    color: var(--good); font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em;
+  }
+  .soon-card .ack.show { display: block; }
+  .soon-card .alt {
+    margin-top: 18px; font-family: var(--mono); font-size: 11px; color: var(--ink-mute); letter-spacing: 0.04em;
+  }
+
+  /* connectors strip — purely visual, no backend */
+  .connectors {
+    max-width: var(--max); margin: 0 auto 80px; padding: 0 28px;
+  }
+  .connectors-label {
+    font-family: var(--mono); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;
+    color: var(--ink-mute); margin-bottom: 18px;
+  }
+  .connectors-label b { color: var(--gold); font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 14px; letter-spacing: 0; text-transform: none; margin-right: 6px; }
+  .connector-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+    gap: 1px;
+    background: var(--rule);
+    border: 1px solid var(--rule);
+  }
+  .connector {
+    background: var(--bg);
+    padding: 14px 16px;
+    font-family: var(--mono); font-size: 11px; letter-spacing: 0.05em;
+    color: var(--ink-soft);
+    transition: background 220ms ease, color 220ms ease;
+  }
+  .connector b { display: block; font-family: var(--serif); font-weight: 400; font-size: 14px; color: var(--ink); letter-spacing: -0.005em; margin-bottom: 2px; }
+  .connector .cat { font-size: 9px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-mute); }
+  .connector:hover { background: var(--bg-2); color: var(--gold); }
+
+  /* moats — keep three only */
+  .moats { max-width: var(--max); margin: 0 auto 80px; padding: 0 28px; }
+  .moats h2 { font-family: var(--serif); font-weight: 400; font-size: 38px; margin: 0 0 36px; letter-spacing: -0.005em; }
+  .moats h2 em { font-style: italic; color: var(--gold); }
+  .moat-grid {
+    display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px;
+  }
+  .moat { background: var(--bg-2); border: 1px solid var(--rule); padding: 28px 26px; }
+  .moat-num { font-family: var(--mono); font-size: 11px; color: var(--gold); letter-spacing: 0.18em; margin-bottom: 14px; }
+  .moat-title { font-family: var(--serif); font-weight: 500; font-size: 20px; line-height: 1.3; margin-bottom: 12px; letter-spacing: -0.005em; }
+  .moat-body { font-size: 14px; line-height: 1.6; color: var(--ink-soft); }
+
+  /* footer */
+  footer {
+    max-width: var(--max); margin: 0 auto;
+    padding: 40px 28px 60px;
+    border-top: 1px solid var(--rule);
+    font-family: var(--mono); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+    color: var(--ink-mute);
+    display: flex; justify-content: space-between; flex-wrap: wrap; gap: 14px;
+  }
+  footer a { color: var(--ink-mute); text-decoration: none; }
+  footer a:hover { color: var(--gold); }
+</style>
+</head>
+<body>
+
+<header class="topbar">
+  <div class="brand">
+    <div class="logo-dot"></div>
+    <div>
+      <div class="brand-name">Ventura<em>Claw</em></div>
+      <div class="brand-sub">Connected Operations</div>
+    </div>
+  </div>
+  <div class="status-pill">Coming soon</div>
+</header>
+
+<main>
+  <section class="hero">
+    <div class="eyebrow"><span class="dot"></span><span>Public launch in development · early-access list open</span></div>
+    <h1 class="headline">Don't build the workflow.<br>Just <em>ask for the result.</em></h1>
+    <p class="subhead">
+      Every other automation tool sells the canvas — the boxes, the arrows, the if/then. VenturaClaw sells the outcome. Type what you want — <em>"refund order 1234 on Shopify," "post in #launch on Slack," "purge the homepage cache"</em> — and the AI picks the right connector and runs it. 56 wired in. Encrypted per-user keys. Approval queue on the dangerous stuff. Audit trail on everything.
+    </p>
+
+    <div class="soon-card">
+      <h2>Drop your email — we'll ping you on launch.</h2>
+      <p class="ml">Limited early-access spots. No spam, no newsletter, one email when the door opens.</p>
+      <form id="ws" action="mailto:info@venturaclaw.com" method="GET" enctype="text/plain">
+        <input type="email" name="subject" placeholder="you@example.com" required>
+        <button type="submit">Get notified →</button>
+      </form>
+      <div class="ack" id="ack" role="status" aria-live="polite">Thanks — we'll be in touch.</div>
+      <p class="alt">Or email <a href="mailto:info@venturaclaw.com">info@venturaclaw.com</a> with "early access" in the subject.</p>
+    </div>
+  </section>
+
+  <section class="connectors">
+    <div class="connectors-label"><b>56</b> connectors pre-wired · ready on day one</div>
+    <div class="connector-grid">
+      <div class="connector"><b>Stripe</b><span class="cat">Payments</span></div>
+      <div class="connector"><b>Shopify</b><span class="cat">Commerce</span></div>
+      <div class="connector"><b>Slack</b><span class="cat">Messaging</span></div>
+      <div class="connector"><b>Cloudflare</b><span class="cat">Infra</span></div>
+      <div class="connector"><b>HubSpot</b><span class="cat">CRM</span></div>
+      <div class="connector"><b>Notion</b><span class="cat">Docs</span></div>
+      <div class="connector"><b>Discord</b><span class="cat">Community</span></div>
+      <div class="connector"><b>Google Workspace</b><span class="cat">Productivity</span></div>
+      <div class="connector"><b>Mailchimp</b><span class="cat">Email</span></div>
+      <div class="connector"><b>SendGrid</b><span class="cat">Email</span></div>
+      <div class="connector"><b>Twilio</b><span class="cat">SMS / voice</span></div>
+      <div class="connector"><b>Zendesk</b><span class="cat">Support</span></div>
+      <div class="connector"><b>Intercom</b><span class="cat">Support</span></div>
+      <div class="connector"><b>Linear</b><span class="cat">Project mgmt</span></div>
+      <div class="connector"><b>Jira</b><span class="cat">Project mgmt</span></div>
+      <div class="connector"><b>GitHub</b><span class="cat">Code</span></div>
+      <div class="connector"><b>GitLab</b><span class="cat">Code</span></div>
+      <div class="connector"><b>Vercel</b><span class="cat">Deploy</span></div>
+      <div class="connector"><b>Netlify</b><span class="cat">Deploy</span></div>
+      <div class="connector"><b>AWS</b><span class="cat">Infra</span></div>
+      <div class="connector"><b>Google Cloud</b><span class="cat">Infra</span></div>
+      <div class="connector"><b>Azure</b><span class="cat">Infra</span></div>
+      <div class="connector"><b>DigitalOcean</b><span class="cat">Infra</span></div>
+      <div class="connector"><b>Datadog</b><span class="cat">Observability</span></div>
+      <div class="connector"><b>Sentry</b><span class="cat">Errors</span></div>
+      <div class="connector"><b>PagerDuty</b><span class="cat">On-call</span></div>
+      <div class="connector"><b>Asana</b><span class="cat">Project mgmt</span></div>
+      <div class="connector"><b>Trello</b><span class="cat">Project mgmt</span></div>
+      <div class="connector"><b>Monday</b><span class="cat">Project mgmt</span></div>
+      <div class="connector"><b>Salesforce</b><span class="cat">CRM</span></div>
+      <div class="connector"><b>Pipedrive</b><span class="cat">CRM</span></div>
+      <div class="connector"><b>Zoho CRM</b><span class="cat">CRM</span></div>
+      <div class="connector"><b>QuickBooks</b><span class="cat">Accounting</span></div>
+      <div class="connector"><b>Xero</b><span class="cat">Accounting</span></div>
+      <div class="connector"><b>FreshBooks</b><span class="cat">Accounting</span></div>
+      <div class="connector"><b>Square</b><span class="cat">Payments</span></div>
+      <div class="connector"><b>PayPal</b><span class="cat">Payments</span></div>
+      <div class="connector"><b>Plaid</b><span class="cat">Banking</span></div>
+      <div class="connector"><b>Airtable</b><span class="cat">Database</span></div>
+      <div class="connector"><b>Coda</b><span class="cat">Docs</span></div>
+      <div class="connector"><b>Calendly</b><span class="cat">Scheduling</span></div>
+      <div class="connector"><b>Zoom</b><span class="cat">Meetings</span></div>
+      <div class="connector"><b>Google Meet</b><span class="cat">Meetings</span></div>
+      <div class="connector"><b>Microsoft Teams</b><span class="cat">Meetings</span></div>
+      <div class="connector"><b>Dropbox</b><span class="cat">Files</span></div>
+      <div class="connector"><b>Box</b><span class="cat">Files</span></div>
+      <div class="connector"><b>OneDrive</b><span class="cat">Files</span></div>
+      <div class="connector"><b>Figma</b><span class="cat">Design</span></div>
+      <div class="connector"><b>Webflow</b><span class="cat">Web</span></div>
+      <div class="connector"><b>WordPress</b><span class="cat">CMS</span></div>
+      <div class="connector"><b>Ghost</b><span class="cat">Publishing</span></div>
+      <div class="connector"><b>Substack</b><span class="cat">Newsletter</span></div>
+      <div class="connector"><b>Buffer</b><span class="cat">Social</span></div>
+      <div class="connector"><b>Hootsuite</b><span class="cat">Social</span></div>
+      <div class="connector"><b>ElevenLabs</b><span class="cat">Voice AI</span></div>
+      <div class="connector"><b>OpenAI</b><span class="cat">LLM</span></div>
+      <div class="connector"><b>Anthropic</b><span class="cat">LLM</span></div>
+    </div>
+  </section>
+
+  <section class="moats">
+    <h2>What makes it <em>different.</em></h2>
+    <div class="moat-grid">
+      <div class="moat">
+        <div class="moat-num">01</div>
+        <div class="moat-title">Encrypted at rest, by default.</div>
+        <div class="moat-body">Every API key and OAuth token is wrapped in AES-256-GCM with a versioned key envelope. Rotate the master key and old data still decrypts via dual-read until the next save sweep. Plaintext never touches disk.</div>
+      </div>
+      <div class="moat">
+        <div class="moat-num">02</div>
+        <div class="moat-title">Sensitive actions wait for approval.</div>
+        <div class="moat-body">Refunds over a threshold, mass deletes, DNS edits — these don't fire until a second human signs off in the approval queue. The AI proposes; a person disposes. Audit-logged either way.</div>
+      </div>
+      <div class="moat">
+        <div class="moat-num">03</div>
+        <div class="moat-title">Audit trail on every fired action.</div>
+        <div class="moat-body">Who asked, what model routed it, which connector ran, what arguments fired, what came back. Append-only. Exportable. The "tell me everything that happened on Stripe last week" dashboard is the same query the auditor runs.</div>
+      </div>
+    </div>
+  </section>
+</main>
+
+<footer>
+  <span>© 2026 VenturaClaw · Steve Abrams</span>
+  <span>
+    <a href="mailto:info@venturaclaw.com">info@venturaclaw.com</a>
+  </span>
+</footer>
+
+<script>
+  // Soft mailto fallback — if the user hits "Get notified" on a device with
+  // no mail client, the form action still does *something*. The ack box only
+  // shows if mailto returns control to the page (i.e. handler exists).
+  document.getElementById('ws').addEventListener('submit', function() {
+    setTimeout(function(){
+      var ack = document.getElementById('ack');
+      if (ack) ack.classList.add('show');
+    }, 200);
+  });
+</script>
+</body>
+</html>
diff --git a/server/server.js b/server/server.js
index 9f78c1f..29d3e43 100644
--- a/server/server.js
+++ b/server/server.js
@@ -23,6 +23,8 @@ const rateLimit = require("express-rate-limit");
     "FIGMA_TOKEN","CANVA_TOKEN","ETSY_KEYSTRING","ETSY_ACCESS_TOKEN","ETSY_SHOP_ID",
     "GEORGE_URL","GEORGE_USER","GEORGE_PASS","GEORGE_BASIC_AUTH",
     "PURELYMAIL_API_TOKEN","BROWSERBASE_API_KEY","BROWSERBASE_PROJECT_ID",
+    "SHOPIFY_ADMIN_TOKEN","SHOPIFY_ACCESS_TOKEN","SHOPIFY_STORE",
+    "ANTHROPIC_API_KEY","ELEVENLABS_API_KEY",
     // LLM
     "OLLAMA_URL","OLLAMA_MODEL","OLLAMA_FALLBACK_URL","OLLAMA_FALLBACK_MODEL","CC_KEY_ID"
   ]);

← 5b582d2 chat: surface Admin link in topbar for admin users  ·  back to Ventura Claw  ·  competitor matrix: 8 platforms crawled, 10 differentiation v b15fb3e →