From 91cb007ac5f0e1cbd35d9b27beb97214365fc507 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 01:25:13 -0400 Subject: [PATCH 01/11] docs: investor & VC lead funnel design spec for /investors --- .../2026-05-29-investors-funnel-design.md | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-29-investors-funnel-design.md diff --git a/docs/superpowers/specs/2026-05-29-investors-funnel-design.md b/docs/superpowers/specs/2026-05-29-investors-funnel-design.md new file mode 100644 index 0000000..a0e0482 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-investors-funnel-design.md @@ -0,0 +1,248 @@ +# Design Spec — `suedeai.org/investors` Investor & VC Lead Funnel + +- **Date:** 2026-05-29 +- **Status:** Approved (design), ready for implementation plan +- **Repo:** `Suede-AI/suedeai-org` (static site + Vercel serverless `/api`) +- **Owner:** Jason Colapietro (Johnny Suede), Founder & CEO, Suede Labs AI + +## 1. Goal + +Build a conversion-focused investor/VC lead funnel at `https://suedeai.org/investors/` that: + +1. Tells the Suede Labs investment thesis using only claims already published on the site. +2. Captures qualified investor leads (name, firm, role, check size, timeline, intent). +3. Routes each lead to Supabase **and** an instant email alert (reusing the live `api/contact.js` pipeline). +4. Delivers a tiered post-submit experience: interest captured → deck/data-room link unlocked → intro call bookable. + +This is a lead-generation funnel, not a data room with authentication. Hard gating is explicitly out of scope for v1 (see §13). + +## 2. Confirmed decisions + +| Decision | Choice | +|---|---| +| Conversion goal | Tiered: express interest → unlock deck → book call | +| Funnel structure | Single landing page + form | +| Lead routing | Supabase row + Resend email alert to founder | +| Host | `suedeai.org/investors` (indexable / listed) | +| Tier mechanic | One capture form; intent expressed as a field; deck + scheduler revealed on `/investors/thanks/` and in the autoresponder email | + +## 3. Scope + +**In scope (v1):** +- New page `investors/index.html` +- New thank-you page `investors/thanks/index.html` +- New serverless handler `api/investors.js` (reuses `api/_shared.js` unchanged) +- New serverless redirect helper `api/investor-link.js` (env-driven deck/call URLs) +- New Supabase table `investor_leads` + RLS in `supabase/schema.sql` +- Scoped CSS additions for the premium investor sections (in `assets/css/site.css` or a dedicated `assets/css/investors.css`) +- `sitemap.xml` entry for `/investors/` +- `tests/verify_site.py` coverage for the two new pages +- New env vars (documented in `.env.example`) + +**Out of scope (v1):** +- Authenticated/token-gated data room +- CRM sync beyond Supabase + email +- Editing `index.html` nav (held until open SEO PRs #3/#5/#6 merge — see §12) +- Any change to `api/_shared.js`, `api/contact.js`, or `api/book.js` + +## 4. Architecture overview + +``` +Visitor → /investors/ (static HTML, assets/js/site.js progressive enhancement) + │ submit form (fetch POST → /api/investors/, native-POST fallback) + ▼ +api/investors.js + ├─ validate (name, email, firm required; email regex; honeypot) + ├─ insertRow("investor_leads", {...}) via Supabase REST (_shared.insertRow) + ├─ sendEmail → INVESTOR_NOTIFY_TO (_shared.sendEmail / Resend), reply_to = lead + ├─ optional autoresponder → lead email (deck + call links from env) + └─ 303 redirect → /investors/thanks/ (or JSON {ok, redirectTo} for fetch) + ▼ +/investors/thanks/ (static, noindex) + ├─ "Book an intro call" → /api/investor-link?target=call → 302 INVESTOR_CALENDAR_URL + └─ "View the materials" → /api/investor-link?target=deck → 302 INVESTOR_DECK_URL + (both fall back to /contact/ if env unset) +``` + +The page mirrors the existing `contact/` form contract exactly: +`data-api-endpoint="/api/investors/"`, `data-fallback-action="/api/investors/"`, +`data-success-redirect="/investors/thanks/"`, plus a `[data-form-status]` note element. + +## 5. Page narrative — `investors/index.html` + +All copy is sourced from the site's published thesis (`llms.txt`, `llms-full.txt`, topical pages). No invented metrics. + +1. **Hero** — H1 thesis line ("The ownership layer for the AI media era"), lede, primary CTA *Request investor materials* (anchors to form), secondary *Read the thesis* (anchors to thesis section / links `/proof-of-creation/`). Credibility strip: "Live on Base mainnet · 24 agent-payable x402 endpoints · iOS apps shipped · Featured in TechBullion." Each strip item links to its source. +2. **Why now** — verbatim-aligned framing: creation became abundant; the scarce layer is proof, identity, ownership, distribution, payment, licensing, repeatable income. "The question is who owns the rails." +3. **The four stages** — CREATE → PROVE → LAUNCH → EARN, terminal/ledger styling. +4. **Proof of execution** — the real ecosystem table (Suede AI, Suede App, Strumly + iOS, Launchpad, Vaults + x402, Distribution) with live URLs. Registry-Cyan ledger treatment. +5. **Traction signals** — verifiable only: ERC-8004 contracts live on Base mainnet; 24 x402 paid endpoints (`app.suedeai.ai/.well-known/x402.json`); Producer ACP agent (Virtuals, ID `019e3991-374d-75f3-a6b8-17ff309b4cd2`); iOS line (Suede Studio Inspiration / Guitar / Voice); TechBullion coverage. Every figure links to its source endpoint/article. +6. **Founder** — concise Jason Colapietro / Johnny Suede block; links `/jason-colapietro/`, `/book/`, LinkedIn, X, GitHub. +7. **The ask (teaser)** — one soft line that materials/deck cover the round; no raise terms on the public page. +8. **Capture block** — the form (§6). +9. **Footer** — reuse `.site-footer`; cross-link thesis pages + `/contact/`. Legal: "© 2026 JC Investment Group LLC." + +**Banned/required language:** lead with creator ownership, programmable IP, rights, provenance, registry-backed media, royalty routing, agent commerce. Never reduce to "AI music app." Never use the phrase "Story Protocol." Describe the ERC-8004 stack in its own terms. Organization schema name stays **"Suede Labs"**; body copy may use **"Suede Labs AI"** for the company. + +## 6. Form spec + +Posts to `/api/investors/`. Fields: + +| Field | Name | Required | Notes | +|---|---|---|---| +| Full name | `name` | yes | | +| Email | `email` | yes | regex `^[^@\s]+@[^@\s]+\.[^@\s]+$` | +| Firm / organization | `firm` | yes | | +| Role / title | `role` | no | | +| Investor type | `investor_type` | no | select: Angel / Pre-seed–Seed / Multi-stage / Family office / Strategic / Other | +| Check size | `check_size` | no | select: <$25k / $25–100k / $100–250k / $250k–1M / $1M+ | +| Timeline | `timeline` | no | select: Active now / 1–3 months / Exploratory | +| Intent | `intent` | no | checkboxes: Intro / Send deck / Book a call (serialized comma-joined) | +| Website or LinkedIn | `website` | no | | +| Note | `message` | no | textarea | +| Consent | `consent` | no | checkbox, marketing follow-up | +| Honeypot | `company_url` | n/a | hidden; if filled, silently 200 without insert | +| Source | `source` | hidden | server sets `suedeai.org/investors` | +| UTM | `utm_source`, `utm_campaign` | hidden | populated from query string by `site.js` if present | + +Accessibility: every field labeled; `[data-form-status]` is `aria-live="polite"`; visible focus states; submit disabled→re-enabled around fetch. + +## 7. Data model — `supabase/schema.sql` (append) + +```sql +create table if not exists public.investor_leads ( + id bigint generated always as identity primary key, + name text not null, + email text not null, + firm text not null, + role text, + investor_type text, + check_size text, + timeline text, + intent text, + website text, + message text, + consent_marketing boolean default false, + source text default 'suedeai.org/investors', + utm_source text, + utm_campaign text, + status text not null default 'new', + submitted_at timestamptz not null default now() +); + +create index if not exists investor_leads_submitted_at_idx + on public.investor_leads (submitted_at desc); + +alter table public.investor_leads enable row level security; + +drop policy if exists "investor_leads_insert_anon" on public.investor_leads; +create policy "investor_leads_insert_anon" + on public.investor_leads + for insert + to anon, authenticated + with check ( + source = 'suedeai.org/investors' -- MUST equal the value api/investors.js writes + and name is not null + and email is not null + and firm is not null + and submitted_at is not null + ); + +revoke all on public.investor_leads from anon, authenticated; +grant insert on public.investor_leads to anon, authenticated; +``` + +`status` workflow (manual, in Supabase): `new → contacted → meeting → diligence → committed → passed`. + +**Critical:** the RLS `with check (source = 'suedeai.org/investors')` must match the exact `source` string `api/investors.js` writes, or inserts 403. Do not write `'suedeai.org'` from the API for this table. + +## 8. API — `api/investors.js` + +Mirror `api/contact.js`; reuse `_shared.js` (`allowPostOnly`, `getRequestFields`, `normalizeText`, `insertRow`, `sendEmail`, `getEnv`, `sendJson`, `redirect`, `wantsJson`) **unchanged**. + +Behavior: +1. `allowPostOnly`. +2. Read + normalize fields. Honeypot `company_url`: if non-empty → return success (`{ok:true, redirectTo:"/investors/thanks/"}` / 303) without DB write. +3. Validate `name`, `email` (regex), `firm`. On failure → 400 (JSON or text per `wantsJson`). +4. `insertRow(process.env.SUPABASE_INVESTOR_TABLE || "investor_leads", { ...fields, source: "suedeai.org/investors", submitted_at: new Date().toISOString() })`. +5. On insert failure → propagate status/payload like `contact.js`. +6. Notify email: if `INVESTOR_NOTIFY_TO` and `INVESTOR_EMAIL_FROM` set → `sendEmail` with subject `New investor lead: []`, body listing all fields + intent, `reply_to` = lead email. +7. Optional autoresponder: if `INVESTOR_AUTORESPONDER=true` and `INVESTOR_EMAIL_FROM` set → `sendEmail` to lead with thesis + deck (`INVESTOR_DECK_URL`) and call (`INVESTOR_CALENDAR_URL`) links. Best-effort; never block success on email. +8. Success → `{ok:true, redirectTo:"/investors/thanks/"}` (fetch) or `303 → /investors/thanks/`. + +Failures in email are non-fatal (lead already stored). Never log PII to stdout beyond what `contact.js` does. + +## 9. Redirect helper — `api/investor-link.js` + +GET handler. Reads `?target=`: +- `deck` → 302 to `INVESTOR_DECK_URL` +- `call` → 302 to `INVESTOR_CALENDAR_URL` +- unset/unknown env → 302 to `/contact/` + +Keeps deck/scheduler URLs in env (static HTML can't read env at runtime), so no placeholder links ship in HTML. Cache-Control: `no-store`. + +## 10. Thank-you page — `investors/thanks/index.html` + +`meta robots: noindex, follow` (matches `contact/thanks/`). Confirmation copy. Two buttons: +- `.button--primary` → `/api/investor-link?target=call` ("Book an intro call") +- `.button--secondary` → `/api/investor-link?target=deck` ("View the materials") +Plus links to `/proof-of-creation/`, `/programmable-ip/`, `/book/`. Reuse `.success-hero`, `.panel`, `.button-row`. + +## 11. Design, a11y, SEO + +- **Visual:** Suede palette layered onto `site.css` idiom — Deep Ink sections, **Registry Cyan** accents, **Rights Red** primary CTA, **Verified Emerald** "live" states. Editorial + terminal/ledger hybrid; hairlines; 4–6px radius; uppercase command/eyebrow labels; sentence-case body. Reuse existing classes (`.site-header`, `.page-hero`, `.eyebrow`, `.lede`, `.panel`, `.form-card`, `.button*`, `.site-footer`) and add scoped `.investor-*` classes for hero strip, stages, and ledger table. +- **Responsive:** 320 / 375 / 768 / 1024 / 1440; no overflow; touch targets ≥44px. +- **Motion:** compositor-only (transform/opacity); honor `prefers-reduced-motion`. +- **SEO:** indexable. `` "Invest in Suede Labs AI | Ownership Infrastructure for the AI Media Era" (or similar), description, canonical `https://suedeai.org/investors/`, OG/Twitter tags (reuse `og-suede.png`), `WebPage` JSON-LD referencing the existing Organization (name "Suede Labs"). Add `<url><loc>https://suedeai.org/investors/</loc></url>` to `sitemap.xml`. Thanks page stays `noindex`. + +## 12. Conflict-avoidance with open PRs + +Open PRs #3/#5/#6 touch `index.html` and `jason-colapietro/index.html`. This work creates **only new files** plus an append to `supabase/schema.sql` and one `<url>` entry in `sitemap.xml`. **Do not edit `index.html`** in this change — the homepage nav link to `/investors/` is deferred until those PRs merge (tracked as a follow-up). If `sitemap.xml` is also touched by PR #6, add the `/investors/` entry after merge to avoid a conflict; otherwise add it here. + +## 13. Env vars (add to `.env.example`) + +``` +SUPABASE_INVESTOR_TABLE=investor_leads +INVESTOR_EMAIL_FROM=info@suedeai.org +INVESTOR_NOTIFY_TO=info@suedeai.org +INVESTOR_AUTORESPONDER=false +INVESTOR_DECK_URL= +INVESTOR_CALENDAR_URL= +``` + +Defaults chosen so the funnel ships and captures leads even with deck/calendar URLs unset (buttons fall back to `/contact/`). `SUPABASE_URL` / `SUPABASE_PUBLISHABLE_KEY` / `RESEND_API_KEY` already exist for the contact flow. + +## 14. Testing + +- Extend `tests/verify_site.py`: assert `investors/index.html` and `investors/thanks/index.html` exist; assert required markup (canonical, title, form `action="/api/investors/"`, thanks `noindex`). +- Add a focused Node test for `api/investors.js`: missing required field → 400; honeypot filled → success-without-insert; valid payload → insert call shape + `source === 'suedeai.org/investors'`. Mock `_shared` insert/email. +- Manual E2E (in plan): `curl -X POST /api/investors/` happy path + validation; load `/investors/` at 320/768/1440; submit → `/investors/thanks/`; verify Supabase row + Resend email in a staging/preview env. + +## 15. Deployment steps (do not deploy without explicit approval) + +1. Run the `investor_leads` migration against the Supabase project (append to `schema.sql`, apply). +2. Set new env vars in the Vercel `suedeai-org` project (`vercel whoami` must be `suede-ai`). +3. Deploy preview, verify capture end-to-end, then promote. + +## 16. Future (not v1) + +- Token/signed-link gating for the deck (hard data room). +- Per-intent routing (e.g., auto-send Cal.com invite when "Book a call" checked). +- CRM sync; analytics events on form view/submit. +- Homepage nav link once SEO PRs merge. + +## 17. File manifest + +| Action | Path | +|---|---| +| add | `investors/index.html` | +| add | `investors/thanks/index.html` | +| add | `api/investors.js` | +| add | `api/investor-link.js` | +| edit | `supabase/schema.sql` (append `investor_leads`) | +| edit | `sitemap.xml` (add `/investors/`) | +| edit | `.env.example` (add investor vars) | +| edit | `assets/css/site.css` or add `assets/css/investors.css` (scoped styles) | +| edit | `tests/verify_site.py` (coverage) | +| add | `tests/test_api_investors.*` (handler validation test) | +| not touched | `index.html`, `api/_shared.js`, `api/contact.js`, `api/book.js` | From ec2657c7991f8608e87fd564c666a7133dc8a7ec Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 01:43:40 -0400 Subject: [PATCH 02/11] docs: investor & VC lead funnel implementation plan --- .../plans/2026-05-29-investors-funnel.md | 1087 +++++++++++++++++ 1 file changed, 1087 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-29-investors-funnel.md diff --git a/docs/superpowers/plans/2026-05-29-investors-funnel.md b/docs/superpowers/plans/2026-05-29-investors-funnel.md new file mode 100644 index 0000000..34c5716 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-investors-funnel.md @@ -0,0 +1,1087 @@ +# Investor & VC Lead Funnel — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship a conversion-focused investor/VC lead funnel at `https://suedeai.org/investors/` that captures qualified leads into Supabase, emails the founder instantly, and reveals deck + call options after submit. + +**Architecture:** Static HTML page + Vercel serverless handlers, reusing the live `api/_shared.js` (Supabase REST insert + Resend email) pipeline already powering `api/contact.js`/`api/book.js`. One capture form expresses tiered intent (intro / deck / call); `/investors/thanks/` and an optional autoresponder deliver the deck/scheduler links from env. All-new files plus three small additive edits (`supabase/schema.sql`, `sitemap.xml`, `.env.example`) — no edits to `index.html` or existing handlers. + +**Tech Stack:** Static HTML/CSS, Node CommonJS serverless functions (Vercel `/api`), Supabase REST, Resend, Python `tests/verify_site.py`, Node built-in `node:test` (Node ≥18). + +**Spec:** `docs/superpowers/specs/2026-05-29-investors-funnel-design.md` + +**Branch assumption:** Work on `feat/investors-funnel` (already created off `main`, holds the spec commit). Do NOT touch `index.html`, `jason-colapietro/index.html`, `api/_shared.js`, `api/contact.js`, `api/book.js`. + +**Repo root for all paths below:** `/Users/jason/Documents/Ramboed/suedeai-org` + +--- + +### Task 1: Supabase `investor_leads` table + RLS + +SQL DDL doesn't fit RED/GREEN; verify by grep after appending. The RLS check string MUST equal the `source` the API writes (`suedeai.org/investors`) or inserts 403 silently. + +**Files:** +- Modify: `supabase/schema.sql` (append at end) + +- [ ] **Step 1: Append the table + RLS to `supabase/schema.sql`** + +```sql + +create table if not exists public.investor_leads ( + id bigint generated always as identity primary key, + name text not null, + email text not null, + firm text not null, + role text, + investor_type text, + check_size text, + timeline text, + intent text, + website text, + message text, + consent_marketing boolean default false, + source text default 'suedeai.org/investors', + utm_source text, + utm_campaign text, + status text not null default 'new', + submitted_at timestamptz not null default now() +); + +create index if not exists investor_leads_submitted_at_idx + on public.investor_leads (submitted_at desc); + +alter table public.investor_leads enable row level security; + +drop policy if exists "investor_leads_insert_anon" on public.investor_leads; +create policy "investor_leads_insert_anon" + on public.investor_leads + for insert + to anon, authenticated + with check ( + source = 'suedeai.org/investors' + and name is not null + and email is not null + and firm is not null + and submitted_at is not null + ); + +revoke all on public.investor_leads from anon, authenticated; +grant insert on public.investor_leads to anon, authenticated; +``` + +- [ ] **Step 2: Verify the critical fragments are present** + +Run: `grep -n "investor_leads" supabase/schema.sql && grep -n "source = 'suedeai.org/investors'" supabase/schema.sql` +Expected: at least one match for `create table ... public.investor_leads`, the index, the policy, and exactly the RLS `source = 'suedeai.org/investors'` line. + +- [ ] **Step 3: Commit** + +```bash +git add supabase/schema.sql +git commit -m "feat(investors): add investor_leads table + RLS" +``` + +--- + +### Task 2: API handler `api/investors.js` + +**Files:** +- Create: `api/investors.js` +- Test: `tests/api_investors.test.js` + +- [ ] **Step 1: Write the failing test** + +Create `tests/api_investors.test.js`: + +```js +const test = require("node:test"); +const assert = require("node:assert"); + +// Supabase must look configured so insertRow proceeds. +process.env.SUPABASE_URL = "https://example.supabase.co"; +process.env.SUPABASE_PUBLISHABLE_KEY = "test-key"; +// Leave INVESTOR_NOTIFY_TO / INVESTOR_EMAIL_FROM unset so no email fetch fires. + +const handler = require("../api/investors.js"); + +function makeReq(body) { + return { method: "POST", headers: { accept: "application/json" }, body }; +} + +function makeRes() { + return { + statusCode: 0, + headers: {}, + body: "", + setHeader(k, v) { this.headers[k] = v; }, + end(payload) { this.body = payload || ""; }, + }; +} + +function stubFetch() { + const calls = []; + global.fetch = async (url, opts) => { + calls.push({ url, opts }); + return { ok: true, status: 200, text: async () => "" }; + }; + return calls; +} + +test("400 when firm is missing", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler(makeReq({ name: "Pat", email: "pat@fund.com" }), res); + assert.strictEqual(res.statusCode, 400); + assert.strictEqual(calls.length, 0, "should not insert when invalid"); +}); + +test("honeypot fill returns success without inserting", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler( + makeReq({ name: "Bot", email: "bot@spam.com", firm: "X", company_url: "filled" }), + res + ); + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(calls.length, 0, "honeypot should skip insert"); +}); + +test("valid lead inserts into investor_leads with correct source", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler( + makeReq({ + name: "Pat Investor", + email: "pat@fund.com", + firm: "Fund Capital", + intent_deck: "yes", + intent_call: "yes", + }), + res + ); + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(calls.length, 1, "exactly one insert call"); + assert.match(calls[0].url, /\/rest\/v1\/investor_leads$/); + const sent = JSON.parse(calls[0].opts.body); + assert.strictEqual(sent.source, "suedeai.org/investors"); + assert.strictEqual(sent.intent, "deck,call"); + const result = JSON.parse(res.body); + assert.strictEqual(result.ok, true); + assert.strictEqual(result.redirectTo, "/investors/thanks/"); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node --test tests/api_investors.test.js` +Expected: FAIL — `Cannot find module '../api/investors.js'`. + +- [ ] **Step 3: Write the handler** + +Create `api/investors.js`: + +```js +const { + allowPostOnly, + getEnv, + getRequestFields, + insertRow, + normalizeText, + redirect, + sendEmail, + sendJson, + wantsJson, +} = require("./_shared"); + +const SOURCE = "suedeai.org/investors"; +const SUCCESS_REDIRECT = "/investors/thanks/"; + +function buildIntent(fields) { + const parts = []; + if (normalizeText(fields.intent_intro)) parts.push("intro"); + if (normalizeText(fields.intent_deck)) parts.push("deck"); + if (normalizeText(fields.intent_call)) parts.push("call"); + return parts.join(","); +} + +function buildAutoresponder({ name, deckUrl, calendarUrl }) { + const hi = name ? ` ${name}` : ""; + const lines = [ + `Hi${hi},`, + "", + "Thank you for your interest in Suede Labs AI. We build the ownership and settlement layer for the AI media era: proof of creation, programmable IP, provenance, royalty routing, and agent commerce.", + "", + ]; + if (deckUrl) lines.push(`Investor materials: ${deckUrl}`); + if (calendarUrl) lines.push(`Book an intro call: ${calendarUrl}`); + if (!deckUrl && !calendarUrl) { + lines.push("Our team will follow up shortly with materials and next steps."); + } + lines.push("", "Suede Labs AI", "https://suedeai.org/"); + return { subject: "Suede Labs AI — investor materials", text: lines.join("\n") }; +} + +module.exports = async (req, res) => { + if (!allowPostOnly(req, res)) { + return; + } + + const fields = getRequestFields(req); + + // Honeypot: a hidden field humans never fill. If present, succeed without storing. + if (normalizeText(fields.company_url)) { + if (wantsJson(req)) { + sendJson(res, 200, { ok: true, redirectTo: SUCCESS_REDIRECT }); + return; + } + redirect(res, SUCCESS_REDIRECT); + return; + } + + const name = normalizeText(fields.name); + const email = normalizeText(fields.email); + const firm = normalizeText(fields.firm); + const role = normalizeText(fields.role); + const investorType = normalizeText(fields.investor_type); + const checkSize = normalizeText(fields.check_size); + const timeline = normalizeText(fields.timeline); + const website = normalizeText(fields.website); + const message = normalizeText(fields.message); + const intent = buildIntent(fields); + const consentMarketing = Boolean(normalizeText(fields.consent)); + const utmSource = normalizeText(fields.utm_source); + const utmCampaign = normalizeText(fields.utm_campaign); + + if (!name || !firm || !email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { + const errorMessage = "Name, email, and firm are required."; + if (wantsJson(req)) { + sendJson(res, 400, { error: errorMessage }); + return; + } + res.statusCode = 400; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end(errorMessage); + return; + } + + const table = process.env.SUPABASE_INVESTOR_TABLE || "investor_leads"; + const result = await insertRow(table, { + name, + email, + firm, + role, + investor_type: investorType, + check_size: checkSize, + timeline, + intent, + website, + message, + consent_marketing: consentMarketing, + source: SOURCE, + utm_source: utmSource, + utm_campaign: utmCampaign, + submitted_at: new Date().toISOString(), + }); + + if (!result.ok) { + if (wantsJson(req)) { + sendJson(res, result.status, result.payload); + return; + } + res.statusCode = result.status; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end(result.payload.error || "Submission failed."); + return; + } + + const sender = getEnv("INVESTOR_EMAIL_FROM"); + const notifyTo = getEnv("INVESTOR_NOTIFY_TO"); + + if (sender && notifyTo) { + const summary = [ + `Name: ${name}`, + `Email: ${email}`, + `Firm: ${firm}`, + `Role: ${role || "(none)"}`, + `Investor type: ${investorType || "(none)"}`, + `Check size: ${checkSize || "(none)"}`, + `Timeline: ${timeline || "(none)"}`, + `Intent: ${intent || "(none)"}`, + `Website: ${website || "(none)"}`, + `UTM: ${utmSource || "-"} / ${utmCampaign || "-"}`, + `Consent: ${consentMarketing ? "yes" : "no"}`, + "", + message || "(no message)", + ].join("\n"); + + await sendEmail({ + from: sender, + to: [notifyTo], + subject: `New investor lead: ${firm}${checkSize ? ` [${checkSize}]` : ""}`, + text: summary, + reply_to: email, + }); + } + + if (sender && getEnv("INVESTOR_AUTORESPONDER") === "true") { + const auto = buildAutoresponder({ + name, + deckUrl: getEnv("INVESTOR_DECK_URL"), + calendarUrl: getEnv("INVESTOR_CALENDAR_URL"), + }); + await sendEmail({ + from: sender, + to: [email], + subject: auto.subject, + text: auto.text, + reply_to: notifyTo || sender, + }); + } + + if (wantsJson(req)) { + sendJson(res, 200, { ok: true, redirectTo: SUCCESS_REDIRECT }); + return; + } + + redirect(res, SUCCESS_REDIRECT); +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node --test tests/api_investors.test.js` +Expected: PASS — 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add api/investors.js tests/api_investors.test.js +git commit -m "feat(investors): add /api/investors lead handler (Supabase + Resend)" +``` + +--- + +### Task 3: API redirect helper `api/investor-link.js` + +Keeps deck/scheduler URLs in env (static HTML can't read env), reached at `/api/investor-link/?target=deck|call`. Falls back to `/contact/`. + +**Files:** +- Create: `api/investor-link.js` +- Test: `tests/api_investor_link.test.js` + +- [ ] **Step 1: Write the failing test** + +Create `tests/api_investor_link.test.js`: + +```js +const test = require("node:test"); +const assert = require("node:assert"); + +const handler = require("../api/investor-link.js"); + +function makeRes() { + return { + statusCode: 0, + headers: {}, + setHeader(k, v) { this.headers[k] = v; }, + end() {}, + }; +} + +test("target=deck redirects to INVESTOR_DECK_URL when set", async () => { + process.env.INVESTOR_DECK_URL = "https://deck.example/suede"; + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=deck" }, res); + assert.strictEqual(res.statusCode, 302); + assert.strictEqual(res.headers.Location, "https://deck.example/suede"); +}); + +test("target=call falls back to /contact/ when env unset", async () => { + delete process.env.INVESTOR_CALENDAR_URL; + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=call" }, res); + assert.strictEqual(res.statusCode, 302); + assert.strictEqual(res.headers.Location, "/contact/"); +}); + +test("unknown target falls back to /contact/", async () => { + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=bogus" }, res); + assert.strictEqual(res.headers.Location, "/contact/"); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node --test tests/api_investor_link.test.js` +Expected: FAIL — `Cannot find module '../api/investor-link.js'`. + +- [ ] **Step 3: Write the handler** + +Create `api/investor-link.js`: + +```js +const TARGET_ENV = { + deck: "INVESTOR_DECK_URL", + call: "INVESTOR_CALENDAR_URL", +}; +const FALLBACK = "/contact/"; + +module.exports = async (req, res) => { + const parsed = new URL(req.url, "https://suedeai.org"); + const target = parsed.searchParams.get("target") || ""; + const envName = TARGET_ENV[target]; + const configured = envName ? String(process.env[envName] || "").trim() : ""; + const destination = configured || FALLBACK; + + res.statusCode = 302; + res.setHeader("Location", destination); + res.setHeader("Cache-Control", "no-store"); + res.end(""); +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node --test tests/api_investor_link.test.js` +Expected: PASS — 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add api/investor-link.js tests/api_investor_link.test.js +git commit -m "feat(investors): add /api/investor-link env-driven redirect helper" +``` + +--- + +### Task 4: Scoped stylesheet `assets/css/investors.css` + +**Files:** +- Create: `assets/css/investors.css` +- Modify: `tests/verify_site.py` (add the file to the asset-existence list) + +- [ ] **Step 1: Add the asset to `tests/verify_site.py` (failing assertion)** + +In `tests/verify_site.py`, after the line `css_asset = ROOT / "assets" / "css" / "site.css"` (≈ line 211), add: + +```python + investors_css = ROOT / "assets" / "css" / "investors.css" +``` + +Then inside the `for asset in [ ... ]:` existence-check list (≈ lines 223-242), add `investors_css,` after `css_asset,`. + +- [ ] **Step 2: Run to verify it fails** + +Run: `python3 tests/verify_site.py` +Expected: FAIL — `assets/css/investors.css: file does not exist`. + +- [ ] **Step 3: Create `assets/css/investors.css`** + +```css +/* Investor funnel — scoped styles. Suede Institutional IP Terminal palette. */ +.inv { + --inv-ink: #050b16; + --inv-panel: #09101b; + --inv-control: #0d1726; + --inv-line: rgba(34, 211, 238, 0.18); + --inv-cyan: #22d3ee; + --inv-red: #9f101a; + --inv-emerald: #34d399; + --inv-sky: #38bdf8; + --inv-text: #eef2f7; + --inv-muted: rgba(238, 242, 247, 0.66); + --inv-radius: 6px; + background: var(--inv-ink); + color: var(--inv-text); +} +.inv__band { + width: 100%; + padding: clamp(3rem, 2rem + 5vw, 7rem) clamp(1rem, 0.5rem + 3vw, 4rem); + border-bottom: 1px solid var(--inv-line); +} +.inv__inner { max-width: 1080px; margin: 0 auto; } +.inv__eyebrow { + font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase; + color: var(--inv-cyan); margin: 0 0 0.75rem; +} +.inv__h1 { + font-size: clamp(2.4rem, 1.2rem + 4.6vw, 4.6rem); line-height: 1.02; + letter-spacing: -0.02em; margin: 0 0 1rem; font-weight: 820; +} +.inv__lede { font-size: clamp(1.05rem, 0.98rem + 0.5vw, 1.3rem); color: var(--inv-muted); max-width: 48ch; } +.inv__cta-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; } +.inv__btn { + display: inline-flex; align-items: center; justify-content: center; + min-height: 44px; padding: 0 1.4rem; border-radius: var(--inv-radius); + font-weight: 600; text-decoration: none; border: 1px solid transparent; + transition: transform 150ms cubic-bezier(0.16, 1, 0.3, 1), background 150ms, border-color 150ms; +} +.inv__btn--primary { background: var(--inv-red); color: #fff; } +.inv__btn--primary:hover { transform: translateY(-1px); background: #b51420; } +.inv__btn--ghost { border-color: var(--inv-line); color: var(--inv-text); } +.inv__btn--ghost:hover { border-color: var(--inv-cyan); } +.inv__strip { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 2rem; font-size: 0.82rem; } +.inv__strip a { color: var(--inv-muted); text-decoration: none; border-bottom: 1px dotted var(--inv-line); } +.inv__strip a:hover { color: var(--inv-emerald); } +.inv__h2 { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.4rem); letter-spacing: -0.01em; margin: 0 0 1rem; } +.inv__body { color: var(--inv-muted); max-width: 64ch; } +.inv__grid { display: grid; gap: 1px; background: var(--inv-line); border: 1px solid var(--inv-line); border-radius: var(--inv-radius); overflow: hidden; margin-top: 1.5rem; } +.inv__grid--4 { grid-template-columns: repeat(4, 1fr); } +.inv__cell { background: var(--inv-panel); padding: 1.5rem; } +.inv__cell h3 { margin: 0 0 0.4rem; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--inv-cyan); } +.inv__cell p { margin: 0; color: var(--inv-muted); font-size: 0.95rem; } +.inv__ledger { width: 100%; border-collapse: collapse; font-size: 0.92rem; margin-top: 1.5rem; } +.inv__ledger th, .inv__ledger td { text-align: left; padding: 0.85rem 1rem; border-bottom: 1px solid var(--inv-line); vertical-align: top; } +.inv__ledger th { color: var(--inv-cyan); text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.12em; } +.inv__ledger a { color: var(--inv-sky); } +.inv__live { color: var(--inv-emerald); font-weight: 600; } +.inv__signals { list-style: none; padding: 0; margin: 1.5rem 0 0; display: grid; gap: 0.75rem; } +.inv__signals li { padding-left: 1.5rem; position: relative; color: var(--inv-muted); } +.inv__signals li::before { content: "▸"; position: absolute; left: 0; color: var(--inv-cyan); } +.inv__form { display: grid; gap: 1rem; max-width: 640px; margin-top: 1.5rem; } +.inv__field { display: grid; gap: 0.35rem; } +.inv__field label, .inv__form legend { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--inv-muted); } +.inv__form input[type="text"], .inv__form input[type="email"], .inv__form input[type="url"], .inv__form select, .inv__form textarea { + min-height: 44px; padding: 0.65rem 0.8rem; border-radius: var(--inv-radius); + background: var(--inv-control); border: 1px solid var(--inv-line); color: var(--inv-text); font: inherit; width: 100%; +} +.inv__form textarea { min-height: 120px; resize: vertical; } +.inv__form input:focus, .inv__form select:focus, .inv__form textarea:focus { outline: 2px solid var(--inv-cyan); outline-offset: 1px; } +.inv__checks { display: grid; gap: 0.5rem; border: 0; padding: 0; margin: 0; } +.inv__checks label { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); } +.inv__checks input { accent-color: var(--inv-cyan); width: 18px; height: 18px; } +.inv__consent { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); font-size: 0.9rem; } +.inv__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } +.inv__status { color: var(--inv-emerald); font-size: 0.9rem; } +.inv__cols { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; } +@media (max-width: 720px) { + .inv__grid--4 { grid-template-columns: 1fr 1fr; } + .inv__cols { grid-template-columns: 1fr; } + .inv__ledger thead { display: none; } + .inv__ledger td { display: block; border: 0; padding: 0.3rem 0; } + .inv__ledger tr { display: block; padding: 0.9rem 0; border-bottom: 1px solid var(--inv-line); } +} +@media (prefers-reduced-motion: reduce) { + .inv__btn { transition: none; } +} +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: `python3 tests/verify_site.py` +Expected: PASS (the css existence failure is gone; pages-not-yet-created failures appear only once those pages are added to PAGES in later tasks — at this point PAGES is unchanged, so it PASSES). + +- [ ] **Step 5: Commit** + +```bash +git add assets/css/investors.css tests/verify_site.py +git commit -m "feat(investors): add scoped investor funnel stylesheet" +``` + +--- + +### Task 5: Landing page `investors/index.html` + +**Files:** +- Create: `investors/index.html` +- Modify: `tests/verify_site.py` (register page in PAGES + form_expectations) + +- [ ] **Step 1: Register the page in `tests/verify_site.py` (failing assertions)** + +In the `PAGES` dict (≈ lines 22-38), add after the `contact/index.html` entry: + +```python + "investors/index.html": "/investors/", +``` + +In the `form_expectations` dict (≈ lines 168-195), add: + +```python + "investors/index.html": [ + 'action="/api/investors/"', + 'data-api-endpoint="/api/investors/"', + "who owns the rails", + ], +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `python3 tests/verify_site.py` +Expected: FAIL — `investors/index.html: file does not exist`. + +- [ ] **Step 3: Create `investors/index.html`** + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <link rel="icon" href="/favicon.ico?v=3" sizes="any"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=3"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=3"> + <link rel="apple-touch-icon" href="/apple-touch-icon.png?v=3"> + <link rel="manifest" href="/site.webmanifest"> + <title>Invest in Suede Labs AI | The Ownership Layer for the AI Media Era + + + + + + + + + + + + + + + + + + + + +
+
+
+

Investor Brief — Suede Labs AI

+

The ownership layer for the AI media era.

+

AI made creative output abundant. The scarce layer is now proof, identity, ownership, distribution, payment, and repeatable income. Suede Labs builds the rails for that layer.

+ + +
+
+ +
+
+

Creation got cheap. Ownership didn't scale.

+

AI lowered the cost of making songs, images, video, campaigns, lessons, and synthetic media to near zero. That shifts value away from production and toward the layer creators, agents, platforms, and fans need to turn work into durable value: authorship, voice, likeness, consent, provenance, rights, payment, and royalties.

+

The investor question is not whether another tool can generate media. The question is who owns the rails when creative work has to be proven, licensed, distributed, and paid out across humans and AI agents.

+
+
+ +
+
+

Four stages Suede addresses

+
+

Create

AI lowers the cost of making media of every kind.

+

Prove

Authorship, voice, likeness, consent, provenance, and rights travel with the work.

+

Launch

Projects get routes to funding, audience, and ownership participation.

+

Earn

Payments, vaults, royalties, licensing, distribution, and agent commerce turn output into income.

+
+
+
+ +
+
+

Proof of execution — shipped surfaces, not slideware

+ + + + + + + + + + + + +
SurfaceWhat it does
Suede AIOwnership infrastructure: proof-of-creation, programmable IP, creator rights, provenance.
Suede AppWorking product: rights passports, licensing, royalties, vaults, workflows.
Strumly + iOSArtist-growth and empowerment products; the iOS Suede Studio line.
LaunchpadDemand formation, activation, and fundability for creative projects.
Vaults + x402Royalty participation and agent-native, per-call USDC payments on Base.
DistributionBridge from registered, owned work to audience reach and revenue.
+
+
+ +
+
+

Traction signals

+
    +
  • ERC-8004 identity, reputation, and validation contracts live on Base mainnet.
  • +
  • 24 production x402 paid endpoints — agents pay per call in USDC, no account or API key.
  • +
  • Producer by Suede Labs is a hireable Virtuals ACP agent for music, video, and ACP/x402 consulting.
  • +
  • iOS app line shipped: Suede Studio Inspiration, Suede Studio Guitar, Suede Studio Voice.
  • +
  • Third-party press coverage in TechBullion (May 2026).
  • +
+
+
+ +
+
+

Founder

+

Jason Colapietro (Johnny Suede) is the Founder and CEO of Suede Labs AI, a published author and Forbes contributor. The internet upgraded access; AI upgraded creation; the next layer has to upgrade ownership. Read the founder profile and the thesis book Stake Your Claim.

+
+
+ +
+
+

The ask

+

We're raising to deepen the ownership and settlement rails and expand the surfaces that already ship. Round details, metrics, and use of funds are in the investor materials — request access below.

+
+
+ +
+
+

Request investor materials

+

Tell us a little about your firm. We'll send the materials and a link to book an intro call.

+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ What would you like? + + + +
+
+
+ + + + + +
+
+
+
+ + + + +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: `python3 tests/verify_site.py` +Expected: PASS — investor landing page assertions satisfied (title, description, canonical `https://suedeai.org/investors/`, favicons, og/twitter, an `

`, JSON-LD, a `https://suedeai.ai/` link, the form action/endpoint, and "who owns the rails"). + +- [ ] **Step 5: Commit** + +```bash +git add investors/index.html tests/verify_site.py +git commit -m "feat(investors): add /investors landing page" +``` + +--- + +### Task 6: Thank-you page `investors/thanks/index.html` + +**Files:** +- Create: `investors/thanks/index.html` +- Modify: `tests/verify_site.py` (add to NOINDEX_PAGES + form_expectations) + +- [ ] **Step 1: Register the page in `tests/verify_site.py` (failing assertions)** + +In `NOINDEX_PAGES` (≈ lines 17-20), add: + +```python + "investors/thanks/index.html", +``` + +In `form_expectations` (≈ lines 168-195), add: + +```python + "investors/thanks/index.html": [ + "Your request is in.", + ], +``` + +- [ ] **Step 2: Run to verify it fails** + +Note: `verify_site.py` only checks NOINDEX_PAGES / form_expectations entries `if path.exists()`, so an absent file is skipped rather than failed. The deterministic RED for this task is a file-existence check. + +Run: `test -f investors/thanks/index.html && echo EXISTS || echo MISSING` +Expected: `MISSING`. + +- [ ] **Step 3: Create `investors/thanks/index.html`** + +```html + + + + + + + + + + + Request Received | Suede Labs AI Investors + + + + + + + + + + + + +
+
+

Suede Labs AI — Investors

+

Thank you. Your request is in.

+

We've recorded your request and the Suede team will follow up directly. You can book an intro call or open the investor materials now.

+ + +
+
+ + +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: `python3 tests/verify_site.py` +Expected: PASS — the NOINDEX assertion finds the robots meta and form_expectations finds "Your request is in." + +- [ ] **Step 5: Commit** + +```bash +git add investors/thanks/index.html tests/verify_site.py +git commit -m "feat(investors): add /investors/thanks confirmation page" +``` + +--- + +### Task 7: Sitemap entry, `.env.example`, and `api/investors.js` content guard + +**Files:** +- Modify: `sitemap.xml` +- Modify: `.env.example` +- Modify: `tests/verify_site.py` (sitemap assertion + api content guard) + +- [ ] **Step 1: Add failing assertions to `tests/verify_site.py`** + +In the sitemap block (≈ lines 303-308), after the `full-preview` assertion add: + +```python + assert_contains("sitemap.xml", sitemap_text, "https://suedeai.org/investors/", failures) +``` + +After the `api/book.js` content block (≈ lines 246-256), add: + +```python + investors_api = ROOT / "api" / "investors.js" + if investors_api.exists(): + investors_api_text = read_text(investors_api) + for fragment in [ + "investor_leads", + "suedeai.org/investors", + "INVESTOR_NOTIFY_TO", + ]: + assert_contains("api/investors.js", investors_api_text, fragment, failures) +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `python3 tests/verify_site.py` +Expected: FAIL — `sitemap.xml: missing 'https://suedeai.org/investors/'`. (The `api/investors.js` guard already passes because that file exists from Task 2.) + +- [ ] **Step 3: Add the sitemap entry** + +In `sitemap.xml`, add this line after the `https://suedeai.org/contact/` entry: + +```xml + https://suedeai.org/investors/ +``` + +- [ ] **Step 4: Append the env vars to `.env.example`** + +Append to `.env.example`: + +``` +SUPABASE_INVESTOR_TABLE=investor_leads +INVESTOR_EMAIL_FROM=info@suedeai.org +INVESTOR_NOTIFY_TO=info@suedeai.org +INVESTOR_AUTORESPONDER=false +INVESTOR_DECK_URL= +INVESTOR_CALENDAR_URL= +``` + +- [ ] **Step 5: Run to verify it passes** + +Run: `python3 tests/verify_site.py` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add sitemap.xml .env.example tests/verify_site.py +git commit -m "feat(investors): list /investors in sitemap + document env vars" +``` + +--- + +### Task 8: Full verification + manual E2E + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full Python site verification** + +Run: `python3 tests/verify_site.py` +Expected: `PASS: verified N HTML pages and core assets` (N increased by 1 for the investors page). + +- [ ] **Step 2: Run all Node handler tests** + +Run: `node --test tests/api_investors.test.js tests/api_investor_link.test.js` +Expected: PASS — 6 tests pass total. + +- [ ] **Step 3: Static visual check at key breakpoints** + +Run: `python3 -m http.server 8000` +Then open `http://localhost:8000/investors/` and verify at widths 320 / 375 / 768 / 1024 / 1440: no horizontal overflow, hero readable, the four-stage grid collapses to 2-up then 1-up, the ledger table reflows on mobile, form inputs are ≥44px and reachable by keyboard with visible focus rings. Open `http://localhost:8000/investors/thanks/` and confirm the two CTAs render. (Form POST and `/api/investor-link` redirects require `vercel dev`; static server only validates layout.) + +- [ ] **Step 4: API E2E with `vercel dev` (requires local env)** + +Run: +```bash +# In one shell, with SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY (+ optional RESEND_API_KEY, INVESTOR_* ) exported: +vercel dev --listen 3000 +``` +Then in another shell: +```bash +# Happy path: +curl -i -X POST http://localhost:3000/api/investors/ \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{"name":"Pat Investor","email":"pat@fund.com","firm":"Fund Capital","intent_deck":"yes"}' +# Expected: HTTP 200, body {"ok":true,"redirectTo":"/investors/thanks/"} + +# Validation: +curl -i -X POST http://localhost:3000/api/investors/ \ + -H "Content-Type: application/json" -H "Accept: application/json" \ + -d '{"name":"Pat","email":"pat@fund.com"}' +# Expected: HTTP 400, {"error":"Name, email, and firm are required."} + +# Redirect helper (env unset → /contact/): +curl -i "http://localhost:3000/api/investor-link/?target=deck" +# Expected: HTTP 302, Location: /contact/ (or INVESTOR_DECK_URL if set) +``` +Then confirm a row appears in the Supabase `investor_leads` table and (if RESEND + INVESTOR_* set) the notification email arrives. + +- [ ] **Step 5: Final review against the spec** + +Confirm: no edits to `index.html`, `api/_shared.js`, `api/contact.js`, `api/book.js`; only new files + additive edits to `supabase/schema.sql`, `sitemap.xml`, `.env.example`, `tests/verify_site.py`. The `source` written by the handler (`suedeai.org/investors`) matches the RLS `with check`. Branch is `feat/investors-funnel`. + +--- + +## Post-implementation (require explicit user approval — do NOT auto-run) + +1. **Supabase migration:** apply the `investor_leads` block from `supabase/schema.sql` to the live Supabase project. +2. **Vercel env vars:** set `INVESTOR_EMAIL_FROM`, `INVESTOR_NOTIFY_TO`, and (when available) `INVESTOR_DECK_URL`, `INVESTOR_CALENDAR_URL`, `INVESTOR_AUTORESPONDER=true` on the `suedeai-org` Vercel project (`vercel whoami` must return `suede-ai`). +3. **Deploy preview → verify end-to-end → promote.** +4. **Open PR** for `feat/investors-funnel` → `main`. +5. **Deferred:** add a homepage nav link to `/investors/` only after open SEO PRs #3/#5/#6 merge (they touch `index.html`). + +## Spec coverage self-review + +- Tiered intent (intro/deck/call) → form checkboxes + `buildIntent` + thanks-page reveal + autoresponder (Tasks 2, 5, 6). ✓ +- Single landing page → Task 5. ✓ +- Supabase + Resend routing → `api/investors.js` reusing `_shared` (Task 2) + `investor_leads` table (Task 1). ✓ +- Host `suedeai.org/investors`, indexable → Task 5 page + sitemap (Task 7). ✓ +- RLS source-match gotcha → Task 1 SQL + Task 2 `SOURCE` constant + Step 5 review. ✓ +- Env-driven deck/scheduler, no placeholders → `api/investor-link.js` (Task 3) + `.env.example` (Task 7). ✓ +- Honeypot, validation → Task 2 tests + handler. ✓ +- Design language / a11y / responsive → Task 4 CSS + Task 8 Step 3. ✓ +- Conflict-free with open PRs → all-new files; nav link deferred (post-impl §5). ✓ +- Testing (verify_site + node tests + manual E2E) → Tasks 4–8. ✓ From 0cffdc4ba4252a0e70e0b7fa374b9ccd499d5363 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 01:46:20 -0400 Subject: [PATCH 03/11] feat(investors): add investor_leads table + RLS --- supabase/schema.sql | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/supabase/schema.sql b/supabase/schema.sql index fabc6cd..2e8aca9 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -57,3 +57,44 @@ grant insert on public.book_leads to anon, authenticated; grant insert on public.contact_inquiries to anon, authenticated; grant usage on schema public to anon, authenticated; + +create table if not exists public.investor_leads ( + id bigint generated always as identity primary key, + name text not null, + email text not null, + firm text not null, + role text, + investor_type text, + check_size text, + timeline text, + intent text, + website text, + message text, + consent_marketing boolean default false, + source text default 'suedeai.org/investors', + utm_source text, + utm_campaign text, + status text not null default 'new', + submitted_at timestamptz not null default now() +); + +create index if not exists investor_leads_submitted_at_idx + on public.investor_leads (submitted_at desc); + +alter table public.investor_leads enable row level security; + +drop policy if exists "investor_leads_insert_anon" on public.investor_leads; +create policy "investor_leads_insert_anon" + on public.investor_leads + for insert + to anon, authenticated + with check ( + source = 'suedeai.org/investors' + and name is not null + and email is not null + and firm is not null + and submitted_at is not null + ); + +revoke all on public.investor_leads from anon, authenticated; +grant insert on public.investor_leads to anon, authenticated; From ebfd71c37bd6898b650b75b43a49e3459a366bf5 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 01:51:16 -0400 Subject: [PATCH 04/11] feat(investors): add /api/investors lead handler (Supabase + Resend) --- api/investors.js | 164 ++++++++++++++++++++++++++++++++++++ tests/api_investors.test.js | 75 +++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 api/investors.js create mode 100644 tests/api_investors.test.js diff --git a/api/investors.js b/api/investors.js new file mode 100644 index 0000000..90a8758 --- /dev/null +++ b/api/investors.js @@ -0,0 +1,164 @@ +const { + allowPostOnly, + getEnv, + getRequestFields, + insertRow, + normalizeText, + redirect, + sendEmail, + sendJson, + wantsJson, +} = require("./_shared"); + +const SOURCE = "suedeai.org/investors"; +const SUCCESS_REDIRECT = "/investors/thanks/"; + +function buildIntent(fields) { + const parts = []; + if (normalizeText(fields.intent_intro)) parts.push("intro"); + if (normalizeText(fields.intent_deck)) parts.push("deck"); + if (normalizeText(fields.intent_call)) parts.push("call"); + return parts.join(","); +} + +function buildAutoresponder({ name, deckUrl, calendarUrl }) { + const hi = name ? ` ${name}` : ""; + const lines = [ + `Hi${hi},`, + "", + "Thank you for your interest in Suede Labs AI. We build the ownership and settlement layer for the AI media era: proof of creation, programmable IP, provenance, royalty routing, and agent commerce.", + "", + ]; + if (deckUrl) lines.push(`Investor materials: ${deckUrl}`); + if (calendarUrl) lines.push(`Book an intro call: ${calendarUrl}`); + if (!deckUrl && !calendarUrl) { + lines.push("Our team will follow up shortly with materials and next steps."); + } + lines.push("", "Suede Labs AI", "https://suedeai.org/"); + return { subject: "Suede Labs AI — investor materials", text: lines.join("\n") }; +} + +module.exports = async (req, res) => { + if (!allowPostOnly(req, res)) { + return; + } + + const fields = getRequestFields(req); + + // Honeypot: a hidden field humans never fill. If present, succeed without storing. + if (normalizeText(fields.company_url)) { + if (wantsJson(req)) { + sendJson(res, 200, { ok: true, redirectTo: SUCCESS_REDIRECT }); + return; + } + redirect(res, SUCCESS_REDIRECT); + return; + } + + const name = normalizeText(fields.name); + const email = normalizeText(fields.email); + const firm = normalizeText(fields.firm); + const role = normalizeText(fields.role); + const investorType = normalizeText(fields.investor_type); + const checkSize = normalizeText(fields.check_size); + const timeline = normalizeText(fields.timeline); + const website = normalizeText(fields.website); + const message = normalizeText(fields.message); + const intent = buildIntent(fields); + const consentMarketing = Boolean(normalizeText(fields.consent)); + const utmSource = normalizeText(fields.utm_source); + const utmCampaign = normalizeText(fields.utm_campaign); + + if (!name || !firm || !email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { + const errorMessage = "Name, email, and firm are required."; + if (wantsJson(req)) { + sendJson(res, 400, { error: errorMessage }); + return; + } + res.statusCode = 400; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end(errorMessage); + return; + } + + const table = process.env.SUPABASE_INVESTOR_TABLE || "investor_leads"; + const result = await insertRow(table, { + name, + email, + firm, + role, + investor_type: investorType, + check_size: checkSize, + timeline, + intent, + website, + message, + consent_marketing: consentMarketing, + source: SOURCE, + utm_source: utmSource, + utm_campaign: utmCampaign, + submitted_at: new Date().toISOString(), + }); + + if (!result.ok) { + if (wantsJson(req)) { + sendJson(res, result.status, result.payload); + return; + } + res.statusCode = result.status; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end(result.payload.error || "Submission failed."); + return; + } + + const sender = getEnv("INVESTOR_EMAIL_FROM"); + const notifyTo = getEnv("INVESTOR_NOTIFY_TO"); + + if (sender && notifyTo) { + const summary = [ + `Name: ${name}`, + `Email: ${email}`, + `Firm: ${firm}`, + `Role: ${role || "(none)"}`, + `Investor type: ${investorType || "(none)"}`, + `Check size: ${checkSize || "(none)"}`, + `Timeline: ${timeline || "(none)"}`, + `Intent: ${intent || "(none)"}`, + `Website: ${website || "(none)"}`, + `UTM: ${utmSource || "-"} / ${utmCampaign || "-"}`, + `Consent: ${consentMarketing ? "yes" : "no"}`, + "", + message || "(no message)", + ].join("\n"); + + await sendEmail({ + from: sender, + to: [notifyTo], + subject: `New investor lead: ${firm}${checkSize ? ` [${checkSize}]` : ""}`, + text: summary, + reply_to: email, + }); + } + + if (sender && getEnv("INVESTOR_AUTORESPONDER") === "true") { + const auto = buildAutoresponder({ + name, + deckUrl: getEnv("INVESTOR_DECK_URL"), + calendarUrl: getEnv("INVESTOR_CALENDAR_URL"), + }); + await sendEmail({ + from: sender, + to: [email], + subject: auto.subject, + text: auto.text, + reply_to: notifyTo || sender, + }); + } + + if (wantsJson(req)) { + sendJson(res, 200, { ok: true, redirectTo: SUCCESS_REDIRECT }); + return; + } + + redirect(res, SUCCESS_REDIRECT); +}; diff --git a/tests/api_investors.test.js b/tests/api_investors.test.js new file mode 100644 index 0000000..833d7ae --- /dev/null +++ b/tests/api_investors.test.js @@ -0,0 +1,75 @@ +const test = require("node:test"); +const assert = require("node:assert"); + +// Supabase must look configured so insertRow proceeds. +process.env.SUPABASE_URL = "https://example.supabase.co"; +process.env.SUPABASE_PUBLISHABLE_KEY = "test-key"; +// Leave INVESTOR_NOTIFY_TO / INVESTOR_EMAIL_FROM unset so no email fetch fires. + +const handler = require("../api/investors.js"); + +function makeReq(body) { + return { method: "POST", headers: { accept: "application/json" }, body }; +} + +function makeRes() { + return { + statusCode: 0, + headers: {}, + body: "", + setHeader(k, v) { this.headers[k] = v; }, + end(payload) { this.body = payload || ""; }, + }; +} + +function stubFetch() { + const calls = []; + global.fetch = async (url, opts) => { + calls.push({ url, opts }); + return { ok: true, status: 200, text: async () => "" }; + }; + return calls; +} + +test("400 when firm is missing", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler(makeReq({ name: "Pat", email: "pat@fund.com" }), res); + assert.strictEqual(res.statusCode, 400); + assert.strictEqual(calls.length, 0, "should not insert when invalid"); +}); + +test("honeypot fill returns success without inserting", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler( + makeReq({ name: "Bot", email: "bot@spam.com", firm: "X", company_url: "filled" }), + res + ); + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(calls.length, 0, "honeypot should skip insert"); +}); + +test("valid lead inserts into investor_leads with correct source", async () => { + const calls = stubFetch(); + const res = makeRes(); + await handler( + makeReq({ + name: "Pat Investor", + email: "pat@fund.com", + firm: "Fund Capital", + intent_deck: "yes", + intent_call: "yes", + }), + res + ); + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(calls.length, 1, "exactly one insert call"); + assert.match(calls[0].url, /\/rest\/v1\/investor_leads$/); + const sent = JSON.parse(calls[0].opts.body); + assert.strictEqual(sent.source, "suedeai.org/investors"); + assert.strictEqual(sent.intent, "deck,call"); + const result = JSON.parse(res.body); + assert.strictEqual(result.ok, true); + assert.strictEqual(result.redirectTo, "/investors/thanks/"); +}); From 2c2aa53258b581182f81d04797023d49810a5b22 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 01:55:44 -0400 Subject: [PATCH 05/11] feat(investors): add /api/investor-link env-driven redirect helper --- api/investor-link.js | 18 +++++++++++++++++ tests/api_investor_link.test.js | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 api/investor-link.js create mode 100644 tests/api_investor_link.test.js diff --git a/api/investor-link.js b/api/investor-link.js new file mode 100644 index 0000000..9b2f78f --- /dev/null +++ b/api/investor-link.js @@ -0,0 +1,18 @@ +const TARGET_ENV = { + deck: "INVESTOR_DECK_URL", + call: "INVESTOR_CALENDAR_URL", +}; +const FALLBACK = "/contact/"; + +module.exports = async (req, res) => { + const parsed = new URL(req.url, "https://suedeai.org"); + const target = parsed.searchParams.get("target") || ""; + const envName = TARGET_ENV[target]; + const configured = envName ? String(process.env[envName] || "").trim() : ""; + const destination = configured || FALLBACK; + + res.statusCode = 302; + res.setHeader("Location", destination); + res.setHeader("Cache-Control", "no-store"); + res.end(""); +}; diff --git a/tests/api_investor_link.test.js b/tests/api_investor_link.test.js new file mode 100644 index 0000000..8c956e2 --- /dev/null +++ b/tests/api_investor_link.test.js @@ -0,0 +1,35 @@ +const test = require("node:test"); +const assert = require("node:assert"); + +const handler = require("../api/investor-link.js"); + +function makeRes() { + return { + statusCode: 0, + headers: {}, + setHeader(k, v) { this.headers[k] = v; }, + end() {}, + }; +} + +test("target=deck redirects to INVESTOR_DECK_URL when set", async () => { + process.env.INVESTOR_DECK_URL = "https://deck.example/suede"; + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=deck" }, res); + assert.strictEqual(res.statusCode, 302); + assert.strictEqual(res.headers.Location, "https://deck.example/suede"); +}); + +test("target=call falls back to /contact/ when env unset", async () => { + delete process.env.INVESTOR_CALENDAR_URL; + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=call" }, res); + assert.strictEqual(res.statusCode, 302); + assert.strictEqual(res.headers.Location, "/contact/"); +}); + +test("unknown target falls back to /contact/", async () => { + const res = makeRes(); + await handler({ method: "GET", url: "/api/investor-link?target=bogus" }, res); + assert.strictEqual(res.headers.Location, "/contact/"); +}); From 3ff3c74bba3dde7d3029f770885e61237351cebf Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:01:27 -0400 Subject: [PATCH 06/11] feat(investors): add scoped investor funnel stylesheet --- assets/css/investors.css | 86 ++++++++++++++++++++++++++++++++++++++++ tests/verify_site.py | 2 + 2 files changed, 88 insertions(+) create mode 100644 assets/css/investors.css diff --git a/assets/css/investors.css b/assets/css/investors.css new file mode 100644 index 0000000..9c1aae8 --- /dev/null +++ b/assets/css/investors.css @@ -0,0 +1,86 @@ +/* Investor funnel — scoped styles. Suede Institutional IP Terminal palette. */ +.inv { + --inv-ink: #050b16; + --inv-panel: #09101b; + --inv-control: #0d1726; + --inv-line: rgba(34, 211, 238, 0.18); + --inv-cyan: #22d3ee; + --inv-red: #9f101a; + --inv-emerald: #34d399; + --inv-sky: #38bdf8; + --inv-text: #eef2f7; + --inv-muted: rgba(238, 242, 247, 0.66); + --inv-radius: 6px; + background: var(--inv-ink); + color: var(--inv-text); +} +.inv__band { + width: 100%; + padding: clamp(3rem, 2rem + 5vw, 7rem) clamp(1rem, 0.5rem + 3vw, 4rem); + border-bottom: 1px solid var(--inv-line); +} +.inv__inner { max-width: 1080px; margin: 0 auto; } +.inv__eyebrow { + font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase; + color: var(--inv-cyan); margin: 0 0 0.75rem; +} +.inv__h1 { + font-size: clamp(2.4rem, 1.2rem + 4.6vw, 4.6rem); line-height: 1.02; + letter-spacing: -0.02em; margin: 0 0 1rem; font-weight: 820; +} +.inv__lede { font-size: clamp(1.05rem, 0.98rem + 0.5vw, 1.3rem); color: var(--inv-muted); max-width: 48ch; } +.inv__cta-row { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; } +.inv__btn { + display: inline-flex; align-items: center; justify-content: center; + min-height: 44px; padding: 0 1.4rem; border-radius: var(--inv-radius); + font-weight: 600; text-decoration: none; border: 1px solid transparent; + transition: transform 150ms cubic-bezier(0.16, 1, 0.3, 1), background 150ms, border-color 150ms; +} +.inv__btn--primary { background: var(--inv-red); color: #fff; } +.inv__btn--primary:hover { transform: translateY(-1px); background: #b51420; } +.inv__btn--ghost { border-color: var(--inv-line); color: var(--inv-text); } +.inv__btn--ghost:hover { border-color: var(--inv-cyan); } +.inv__strip { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 2rem; font-size: 0.82rem; } +.inv__strip a { color: var(--inv-muted); text-decoration: none; border-bottom: 1px dotted var(--inv-line); } +.inv__strip a:hover { color: var(--inv-emerald); } +.inv__h2 { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.4rem); letter-spacing: -0.01em; margin: 0 0 1rem; } +.inv__body { color: var(--inv-muted); max-width: 64ch; } +.inv__grid { display: grid; gap: 1px; background: var(--inv-line); border: 1px solid var(--inv-line); border-radius: var(--inv-radius); overflow: hidden; margin-top: 1.5rem; } +.inv__grid--4 { grid-template-columns: repeat(4, 1fr); } +.inv__cell { background: var(--inv-panel); padding: 1.5rem; } +.inv__cell h3 { margin: 0 0 0.4rem; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--inv-cyan); } +.inv__cell p { margin: 0; color: var(--inv-muted); font-size: 0.95rem; } +.inv__ledger { width: 100%; border-collapse: collapse; font-size: 0.92rem; margin-top: 1.5rem; } +.inv__ledger th, .inv__ledger td { text-align: left; padding: 0.85rem 1rem; border-bottom: 1px solid var(--inv-line); vertical-align: top; } +.inv__ledger th { color: var(--inv-cyan); text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.12em; } +.inv__ledger a { color: var(--inv-sky); } +.inv__live { color: var(--inv-emerald); font-weight: 600; } +.inv__signals { list-style: none; padding: 0; margin: 1.5rem 0 0; display: grid; gap: 0.75rem; } +.inv__signals li { padding-left: 1.5rem; position: relative; color: var(--inv-muted); } +.inv__signals li::before { content: "▸"; position: absolute; left: 0; color: var(--inv-cyan); } +.inv__form { display: grid; gap: 1rem; max-width: 640px; margin-top: 1.5rem; } +.inv__field { display: grid; gap: 0.35rem; } +.inv__field label, .inv__form legend { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--inv-muted); } +.inv__form input[type="text"], .inv__form input[type="email"], .inv__form input[type="url"], .inv__form select, .inv__form textarea { + min-height: 44px; padding: 0.65rem 0.8rem; border-radius: var(--inv-radius); + background: var(--inv-control); border: 1px solid var(--inv-line); color: var(--inv-text); font: inherit; width: 100%; +} +.inv__form textarea { min-height: 120px; resize: vertical; } +.inv__form input:focus, .inv__form select:focus, .inv__form textarea:focus { outline: 2px solid var(--inv-cyan); outline-offset: 1px; } +.inv__checks { display: grid; gap: 0.5rem; border: 0; padding: 0; margin: 0; } +.inv__checks label { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); } +.inv__checks input { accent-color: var(--inv-cyan); width: 18px; height: 18px; } +.inv__consent { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); font-size: 0.9rem; } +.inv__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } +.inv__status { color: var(--inv-emerald); font-size: 0.9rem; } +.inv__cols { display: grid; gap: 1rem; grid-template-columns: 1fr 1fr; } +@media (max-width: 720px) { + .inv__grid--4 { grid-template-columns: 1fr 1fr; } + .inv__cols { grid-template-columns: 1fr; } + .inv__ledger thead { display: none; } + .inv__ledger td { display: block; border: 0; padding: 0.3rem 0; } + .inv__ledger tr { display: block; padding: 0.9rem 0; border-bottom: 1px solid var(--inv-line); } +} +@media (prefers-reduced-motion: reduce) { + .inv__btn { transition: none; } +} diff --git a/tests/verify_site.py b/tests/verify_site.py index 6bea08c..bf6602d 100644 --- a/tests/verify_site.py +++ b/tests/verify_site.py @@ -209,6 +209,7 @@ def main() -> int: cover_asset = ROOT / "assets" / "img" / "stake-your-claim-cover.jpg" pdf_asset = ROOT / "assets" / "files" / "stake-your-claim-condensed-preview.pdf" css_asset = ROOT / "assets" / "css" / "site.css" + investors_css = ROOT / "assets" / "css" / "investors.css" js_asset = ROOT / "assets" / "js" / "site.js" favicon_ico = ROOT / "favicon.ico" favicon_svg = ROOT / "favicon.svg" @@ -229,6 +230,7 @@ def main() -> int: cover_asset, pdf_asset, css_asset, + investors_css, js_asset, favicon_ico, favicon_svg, From 5a419a9d99fab5de44858c3db9158b5057e8b3f2 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:05:35 -0400 Subject: [PATCH 07/11] fix(investors): add :focus-visible states for keyboard a11y --- assets/css/investors.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/css/investors.css b/assets/css/investors.css index 9c1aae8..3d253bc 100644 --- a/assets/css/investors.css +++ b/assets/css/investors.css @@ -40,9 +40,11 @@ .inv__btn--primary:hover { transform: translateY(-1px); background: #b51420; } .inv__btn--ghost { border-color: var(--inv-line); color: var(--inv-text); } .inv__btn--ghost:hover { border-color: var(--inv-cyan); } +.inv__btn:focus-visible { outline: 2px solid var(--inv-cyan); outline-offset: 2px; } .inv__strip { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 2rem; font-size: 0.82rem; } .inv__strip a { color: var(--inv-muted); text-decoration: none; border-bottom: 1px dotted var(--inv-line); } .inv__strip a:hover { color: var(--inv-emerald); } +.inv__strip a:focus-visible { color: var(--inv-emerald); outline: 2px solid var(--inv-cyan); outline-offset: 2px; } .inv__h2 { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.4rem); letter-spacing: -0.01em; margin: 0 0 1rem; } .inv__body { color: var(--inv-muted); max-width: 64ch; } .inv__grid { display: grid; gap: 1px; background: var(--inv-line); border: 1px solid var(--inv-line); border-radius: var(--inv-radius); overflow: hidden; margin-top: 1.5rem; } @@ -70,6 +72,7 @@ .inv__checks { display: grid; gap: 0.5rem; border: 0; padding: 0; margin: 0; } .inv__checks label { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); } .inv__checks input { accent-color: var(--inv-cyan); width: 18px; height: 18px; } +.inv__checks input:focus-visible, .inv__consent input:focus-visible { outline: 2px solid var(--inv-cyan); outline-offset: 1px; } .inv__consent { display: flex; gap: 0.5rem; align-items: center; text-transform: none; letter-spacing: 0; color: var(--inv-text); font-size: 0.9rem; } .inv__hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } .inv__status { color: var(--inv-emerald); font-size: 0.9rem; } From 544a62b5daa1c0aaad6a3d458ae5540f6c8c144a Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:08:46 -0400 Subject: [PATCH 08/11] feat(investors): add /investors landing page --- investors/index.html | 223 +++++++++++++++++++++++++++++++++++++++++++ tests/verify_site.py | 6 ++ 2 files changed, 229 insertions(+) create mode 100644 investors/index.html diff --git a/investors/index.html b/investors/index.html new file mode 100644 index 0000000..e5a53c3 --- /dev/null +++ b/investors/index.html @@ -0,0 +1,223 @@ + + + + + + + + + + + Invest in Suede Labs AI | The Ownership Layer for the AI Media Era + + + + + + + + + + + + + + + + + + + + +
+
+
+

Investor Brief — Suede Labs AI

+

The ownership layer for the AI media era.

+

AI made creative output abundant. The scarce layer is now proof, identity, ownership, distribution, payment, and repeatable income. Suede Labs builds the rails for that layer.

+ + +
+
+ +
+
+

Creation got cheap. Ownership didn't scale.

+

AI lowered the cost of making songs, images, video, campaigns, lessons, and synthetic media to near zero. That shifts value away from production and toward the layer creators, agents, platforms, and fans need to turn work into durable value: authorship, voice, likeness, consent, provenance, rights, payment, and royalties.

+

The investor question is not whether another tool can generate media. The question is who owns the rails when creative work has to be proven, licensed, distributed, and paid out across humans and AI agents.

+
+
+ +
+
+

Four stages Suede addresses

+
+

Create

AI lowers the cost of making media of every kind.

+

Prove

Authorship, voice, likeness, consent, provenance, and rights travel with the work.

+

Launch

Projects get routes to funding, audience, and ownership participation.

+

Earn

Payments, vaults, royalties, licensing, distribution, and agent commerce turn output into income.

+
+
+
+ +
+
+

Proof of execution — shipped surfaces, not slideware

+ + + + + + + + + + + + +
SurfaceWhat it does
Suede AIOwnership infrastructure: proof-of-creation, programmable IP, creator rights, provenance.
Suede AppWorking product: rights passports, licensing, royalties, vaults, workflows.
Strumly + iOSArtist-growth and empowerment products; the iOS Suede Studio line.
LaunchpadDemand formation, activation, and fundability for creative projects.
Vaults + x402Royalty participation and agent-native, per-call USDC payments on Base.
DistributionBridge from registered, owned work to audience reach and revenue.
+
+
+ +
+
+

Traction signals

+
    +
  • ERC-8004 identity, reputation, and validation contracts live on Base mainnet.
  • +
  • 24 production x402 paid endpoints — agents pay per call in USDC, no account or API key.
  • +
  • Producer by Suede Labs is a hireable Virtuals ACP agent for music, video, and ACP/x402 consulting.
  • +
  • iOS app line shipped: Suede Studio Inspiration, Suede Studio Guitar, Suede Studio Voice.
  • +
  • Third-party press coverage in TechBullion (May 2026).
  • +
+
+
+ +
+
+

Founder

+

Jason Colapietro (Johnny Suede) is the Founder and CEO of Suede Labs AI, a published author and Forbes contributor. The internet upgraded access; AI upgraded creation; the next layer has to upgrade ownership. Read the founder profile and the thesis book Stake Your Claim.

+
+
+ +
+
+

The ask

+

We're raising to deepen the ownership and settlement rails and expand the surfaces that already ship. Round details, metrics, and use of funds are in the investor materials — request access below.

+
+
+ +
+
+

Request investor materials

+

Tell us a little about your firm. We'll send the materials and a link to book an intro call.

+
+ +
+
+
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ What would you like? + + + +
+
+
+ + + + + +
+
+
+
+ + + + diff --git a/tests/verify_site.py b/tests/verify_site.py index bf6602d..a3e403b 100644 --- a/tests/verify_site.py +++ b/tests/verify_site.py @@ -35,6 +35,7 @@ "sharp-excerpt/index.html": "/sharp-excerpt/", "full-preview/index.html": "/full-preview/", "contact/index.html": "/contact/", + "investors/index.html": "/investors/", } PREVIEW_PDF_PATH = "/assets/files/stake-your-claim-condensed-preview.pdf" @@ -192,6 +193,11 @@ def main() -> int: "contact/thanks/index.html": [ "Thanks. Your note is in.", ], + "investors/index.html": [ + 'action="/api/investors/"', + 'data-api-endpoint="/api/investors/"', + "who owns the rails", + ], } for file_name, fragments in form_expectations.items(): From 4c2ef6588ede0bbc4bad44637eb5a4d68ca9aff9 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:14:05 -0400 Subject: [PATCH 09/11] fix(investors): give form a positioning context for the honeypot --- assets/css/investors.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/investors.css b/assets/css/investors.css index 3d253bc..f7d0802 100644 --- a/assets/css/investors.css +++ b/assets/css/investors.css @@ -60,7 +60,7 @@ .inv__signals { list-style: none; padding: 0; margin: 1.5rem 0 0; display: grid; gap: 0.75rem; } .inv__signals li { padding-left: 1.5rem; position: relative; color: var(--inv-muted); } .inv__signals li::before { content: "▸"; position: absolute; left: 0; color: var(--inv-cyan); } -.inv__form { display: grid; gap: 1rem; max-width: 640px; margin-top: 1.5rem; } +.inv__form { display: grid; gap: 1rem; max-width: 640px; margin-top: 1.5rem; position: relative; } .inv__field { display: grid; gap: 0.35rem; } .inv__field label, .inv__form legend { font-size: 0.78rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--inv-muted); } .inv__form input[type="text"], .inv__form input[type="email"], .inv__form input[type="url"], .inv__form select, .inv__form textarea { From 23c693434dfeda423c0a41072913044b8015ab3b Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:15:19 -0400 Subject: [PATCH 10/11] feat(investors): add /investors/thanks confirmation page --- investors/thanks/index.html | 44 +++++++++++++++++++++++++++++++++++++ tests/verify_site.py | 4 ++++ 2 files changed, 48 insertions(+) create mode 100644 investors/thanks/index.html diff --git a/investors/thanks/index.html b/investors/thanks/index.html new file mode 100644 index 0000000..9cc9ff7 --- /dev/null +++ b/investors/thanks/index.html @@ -0,0 +1,44 @@ + + + + + + + + + + + Request Received | Suede Labs AI Investors + + + + + + + + + + + + +
+
+

Suede Labs AI — Investors

+

Thank you. Your request is in.

+

We've recorded your request and the Suede team will follow up directly. You can book an intro call or open the investor materials now.

+ + +
+
+ + diff --git a/tests/verify_site.py b/tests/verify_site.py index a3e403b..f1d7749 100644 --- a/tests/verify_site.py +++ b/tests/verify_site.py @@ -17,6 +17,7 @@ NOINDEX_PAGES = [ "book/thanks/index.html", "contact/thanks/index.html", + "investors/thanks/index.html", ] PAGES = { @@ -198,6 +199,9 @@ def main() -> int: 'data-api-endpoint="/api/investors/"', "who owns the rails", ], + "investors/thanks/index.html": [ + "Your request is in.", + ], } for file_name, fragments in form_expectations.items(): From 566f28fbc935d2314ec910d9ba844c6a5d776de0 Mon Sep 17 00:00:00 2001 From: Jason Colapietro <55137770+JasonColapietro@users.noreply.github.com> Date: Fri, 29 May 2026 02:19:24 -0400 Subject: [PATCH 11/11] feat(investors): list /investors in sitemap + document env vars --- .env.example | 6 ++++++ sitemap.xml | 1 + tests/verify_site.py | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/.env.example b/.env.example index 89ea910..355a7dd 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,9 @@ RESEND_API_KEY=your-resend-api-key BOOK_EMAIL_FROM=info@suedeai.org CONTACT_EMAIL_FROM=info@suedeai.org CONTACT_NOTIFY_TO=info@suedeai.org +SUPABASE_INVESTOR_TABLE=investor_leads +INVESTOR_EMAIL_FROM=info@suedeai.org +INVESTOR_NOTIFY_TO=info@suedeai.org +INVESTOR_AUTORESPONDER=false +INVESTOR_DECK_URL= +INVESTOR_CALENDAR_URL= diff --git a/sitemap.xml b/sitemap.xml index 5102fe7..baf21d0 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -16,6 +16,7 @@ https://suedeai.org/sharp-excerpt/ https://suedeai.org/full-preview/ https://suedeai.org/contact/ + https://suedeai.org/investors/ https://suedeai.org/terms/ https://suedeai.org/privacy/ https://suedeai.org/voice/terms/ diff --git a/tests/verify_site.py b/tests/verify_site.py index f1d7749..ae6bab5 100644 --- a/tests/verify_site.py +++ b/tests/verify_site.py @@ -267,6 +267,16 @@ def main() -> int: ]: assert_contains("api/book.js", book_api_text, fragment, failures) + investors_api = ROOT / "api" / "investors.js" + if investors_api.exists(): + investors_api_text = read_text(investors_api) + for fragment in [ + "investor_leads", + "suedeai.org/investors", + "INVESTOR_NOTIFY_TO", + ]: + assert_contains("api/investors.js", investors_api_text, fragment, failures) + if vercel_config.exists(): config = json.loads(read_text(vercel_config)) redirect_map = { @@ -318,6 +328,7 @@ def main() -> int: assert_contains("sitemap.xml", sitemap_text, "https://suedeai.org/book/", failures) assert_contains("sitemap.xml", sitemap_text, "https://suedeai.org/sharp-excerpt/", failures) assert_contains("sitemap.xml", sitemap_text, "https://suedeai.org/full-preview/", failures) + assert_contains("sitemap.xml", sitemap_text, "https://suedeai.org/investors/", failures) if failures: print("FAIL: site verification failed")