From d295d88e565c6f0973f506eb8c1ab5ec0621302c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Apr 2026 19:31:50 +0000 Subject: [PATCH] docs: rewrite root README with hackathon-grade overview, tech stack, and Mermaid flow diagrams Co-authored-by: Mr T --- README.md | 405 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 364 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index dfe09f8..3328ea6 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,289 @@ +
+ # UnderWriter -Autonomous VC-underwriting demo: a Cursor-driven agent that fans out six -diligence desks, streams findings to the UI over SSE, synthesises a verdict -(`PROCEED` / `REVIEW` / `HOLD`) and either queues a wire or holds and proposes -a mandate amendment. +### Autonomous VC Diligence, in 60 Seconds + +*A Cursor-driven agent that fans out six diligence desks, streams findings to a Next.js UI in real time, synthesises an IC-grade verdict, and queues a wire — or holds it and proposes a mandate amendment as a pull request.* + +[![Cursor SDK](https://img.shields.io/badge/Cursor_SDK-Composer_2-black?style=for-the-badge)](https://cursor.com) +[![Next.js 16](https://img.shields.io/badge/Next.js-16-000?style=for-the-badge&logo=nextdotjs)](https://nextjs.org) +[![React 19](https://img.shields.io/badge/React-19-149eca?style=for-the-badge&logo=react)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?style=for-the-badge&logo=typescript)](https://www.typescriptlang.org) +[![OpenAI](https://img.shields.io/badge/OpenAI-GPT--5.5-412991?style=for-the-badge&logo=openai)](https://openai.com) +[![Specter](https://img.shields.io/badge/Specter-MCP-6e56cf?style=for-the-badge)](https://tryspecter.com) + +
+ +--- + +## The pitch in one paragraph + +A GP types: *"Wire $2M to Acme Robotics for their Series A. Lead is Sequoia. 50% pro-rata of our $4M allocation."* +UnderWriter parses the prompt and SPA, fans **six concurrent diligence desks** out across Specter, Companies House, OpenSanctions, WHOIS and the fund's own `MANDATE.md`, streams every citation to the UI as it lands, and either **queues the wire** or **fires a BLOCK** — catching, for example, a one-letter typo (`acrne.co` vs `acme.co`) on a forged wire-instruction email registered six days ago with a failing DKIM signature. When the verdict is HOLD, a Cursor SDK Composer-2 agent drafts a pull request that amends the mandate so the same pattern can never slip through again. **The fund's playbook compounds in git.** + +> **The demo catches a $2M Business Email Compromise in 60 seconds** that a junior associate would have wired. + +--- + +## Why this matters + +| Problem | UnderWriter's answer | +| --- | --- | +| BEC scams cost the industry **$2.7B/year** ([FBI IC3 2023](https://www.ic3.gov/AnnualReport/Reports/2023_IC3Report.pdf)) | A dedicated **Wire Safety desk** runs WHOIS + Levenshtein + DKIM + sanctions screening on every wire | +| Diligence is a 2-week analyst slog of 30 browser tabs | **Six desks in parallel**, joined at a single verdict, in under 30 seconds | +| Every fund has a mandate; nobody reads it | The mandate **is the agent's source code** — pure rule evaluation, deterministic, auditable | +| Policy drifts; institutional knowledge evaporates | Every override drafts a **PR against `MANDATE.md`** with rationale and run reference | + +--- + +## Technology stack + + + + + + + + + + + + + + + + + + + + + + +
LayerTechnologyWhy
Agent runtimeCursor SDK · Composer 2Sandboxed cloud VMs, parallel subagents, MCP tooling, file-editing for amendment PRs
OpenAI GPT-5.5Structured prompt parsing, SPA/wire extraction, memo editorial summary (Zod-validated)
MCP (Model Context Protocol)Specter, Companies House, OpenSanctions, WHOIS exposed as tools
BackendNext.js 16 Route HandlersSingle runtime for HTTP + SSE; zero ops surface area
React 19 · TypeScript 5End-to-end type safety from RunEvent at the seam
Server-Sent Events (SSE)Live streaming of desk.start / desk.citation / desk.resolved / verdict
Data sourcesSpecter (companies, people, transactions, interest signals)The unique data — who's actually leading the round in the last 60 days
Companies House · OpenSanctions · WHOISRegistry truth, sanctions/PEP screening, domain provenance
PDF / EML parsing (pdf-parse, mailauth)SPA extraction; SPF / DKIM / DMARC verification on inbound wire emails
FrontendNext.js 16 App Router · React 19Mandate / Run / Memo screens; SSE consumer renders tiles in real time
CSS variables + dark/light themeFixed-width memo template that looks like a real fund document
ValidationZodEvery LLM output is schema-checked before it touches the verdict layer
Mandategray-matter (YAML frontmatter) + MarkdownThe policy file is the source of truth — readable by LPs, executable by the agent
ResilienceIn-memory cache + fixture fallback per sourceDEMO_FORCE_FIXTURES=true = on-stage panic button
Mock railsIn-process wire ledgerNo real money moves; every run is a queued / held entry
+ +--- + +## System architecture + +```mermaid +flowchart TB + subgraph User["GP — Browser"] + UI["Next.js 16 UI
Mandate · Run · Memo"] + end + + subgraph Backend["Next.js 16 Route Handlers · port 3001"] + RUN["POST /api/run
SSE stream"] + MEMO["GET /api/memo/:runId"] + AMEND["POST /api/amend"] + ORCH(["Orchestrator
agents/orchestrator.ts"]) + SYN(["Synthesise
findings → verdict"]) + MEMOGEN(["Memo generator"]) + LEDGER[("Mock wire ledger")] + end + + subgraph Desks["Six diligence desks (parallel)"] + D1["01 Company"] + D2["02 Founder"] + D3["03 Lead investor"] + D4["04 Round dynamics"] + D5["05 Mandate"] + D6["06 Wire safety"] + end + + subgraph Sources["Data sources (each w/ fixture fallback)"] + SPEC["Specter MCP"] + CH["Companies House"] + OS["OpenSanctions"] + WH["WHOIS / DNS / DKIM"] + MD[("MANDATE.md
YAML + prose")] + OAI["OpenAI GPT-5.5"] + end + + subgraph Cursor["Cursor SDK · Composer 2"] + AMENDER["Amendment drafter"] + PR[("GitHub PR
against MANDATE.md")] + end + + UI -- "POST /api/run" --> RUN + RUN --> ORCH + ORCH --> D1 & D2 & D3 & D4 & D5 & D6 + D1 -. "tools" .-> SPEC + D1 -. "tools" .-> CH + D2 -. "tools" .-> SPEC + D2 -. "tools" .-> OS + D3 -. "tools" .-> SPEC + D4 -. "tools" .-> SPEC + D4 -. "GPT-5.5 SPA parse" .-> OAI + D5 -. "rules" .-> MD + D6 -. "tools" .-> WH + D6 -. "tools" .-> OS + D6 -. "tools" .-> CH + + D1 & D2 & D3 & D4 & D5 & D6 -- "DeskFinding" --> SYN + SYN --> MEMOGEN + MEMOGEN -. "editorial summary" .-> OAI + SYN -- "PROCEED" --> LEDGER + SYN -- "HOLD" --> AMEND + + AMEND --> AMENDER + AMENDER --> PR + + ORCH -. "RunEvent SSE" .-> UI + UI -- "GET /api/memo/:runId" --> MEMO + UI -- "POST /api/amend" --> AMEND + + classDef tile fill:#0b0b0b,stroke:#666,stroke-width:1px,color:#eee + class D1,D2,D3,D4,D5,D6 tile +``` + +--- + +## The diligence run, end to end + +```mermaid +sequenceDiagram + autonumber + participant GP as GP + participant UI as Next.js UI + participant API as /api/run + participant Orch as Orchestrator + participant Desks as 6× Desks (parallel) + participant Ext as Specter / CH / OS / WHOIS + participant LLM as OpenAI GPT-5.5 + participant Mem as Memo store + + GP->>UI: prompt + SPA + wire instructions + UI->>API: POST /api/run (RunRequest) + API-->>UI: 200 OK · text/event-stream + API->>Orch: runOrchestrator(req, send) + Orch->>LLM: parsePrompt(prompt) → ParsedDeal + Orch->>Orch: loadMandate() (gray-matter) + Orch-->>UI: run.init { runId, mandateVersion } + + par Six desks fan out (Promise.allSettled) + Orch->>Desks: company + Desks->>Ext: Specter + Companies House + Desks-->>UI: desk.start · desk.citation… · desk.resolved + and + Orch->>Desks: founder + Desks->>Ext: Specter + OpenSanctions PEP + Desks-->>UI: desk.start · desk.citation… · desk.resolved + and + Orch->>Desks: investor + Desks->>Ext: Specter Interest Signals + Desks-->>UI: desk.start · desk.citation… · desk.resolved + and + Orch->>Desks: round + Desks->>LLM: parseSPA() (Zod-validated) + Desks->>Ext: Specter Transactions (comparables) + Desks-->>UI: desk.start · desk.citation… · desk.resolved + and + Orch->>Desks: mandate (pure code, no LLM) + Desks-->>UI: desk.start · desk.citation… · desk.resolved + and + Orch->>Desks: wire-safety + Desks->>Ext: WHOIS + DKIM + Levenshtein + sanctions + Desks-->>UI: desk.start · desk.citation… · desk.resolved + end + + Orch->>Orch: synthesise(findings) → Verdict + Orch-->>UI: verdict { action, confidence, blockingDesk? } + Orch->>LLM: editorial summary (memo lede) + Orch->>Mem: saveMemo(runId, MemoData) + Orch-->>UI: memo.ready { memoId } + + alt verdict.action = proceed + UI->>GP: green PROCEED bar · "Generate IC Memo" + else verdict.action = hold + UI->>GP: red BLOCK modal with reason + clause + GP->>UI: click "Override & amend" + UI->>API: POST /api/amend (OverrideContext) + API->>LLM: Composer 2 drafts MANDATE.md diff + API-->>UI: AmendmentDraft { branch, diff, prTitle, prBody } + UI->>GP: PR preview + rationale + end +``` + +--- + +## The six desks at a glance -This repo is a two-app workspace: +```mermaid +flowchart LR + subgraph Inputs + P["Prompt"] + SPA["SPA PDF"] + WI["Wire .pdf / .eml"] + end -- [`backend/`](./backend) — Next.js 16 / React 19 Route Handlers. Six desks, - SSE streaming, fixture fallbacks. The brief is - [`backend/docs/Backend.md`](./backend/docs/Backend.md). Entrypoint: - `backend/app/api/run/route.ts`. -- [`front-end/`](./front-end) — Next.js 16 / React 19 UI (mandate / run / - memo screens). + subgraph D["Six desks · concurrent · 30s timeout each"] + D1["01 Company
Entity exists,
active, on-script
Specter · CH"] + D2["02 Founder
Real people,
no PEP / sanctions
Specter · OS"] + D3["03 Lead investor
Lead is actually leading
in last 60 days
Specter signals"] + D4["04 Round dynamics
Size, valuation,
pro-rata math
Specter · GPT-5.5"] + D5["05 Mandate
LPA · IC · signing
matrix · pure code
MANDATE.md"] + D6["06 Wire safety
BEC · shell · sanctions
WHOIS · DKIM · OS · CH"] + end + + V{{Verdict layer
any block ⇒ HOLD
any flag ⇒ REVIEW
else PROCEED}} + + P --> D1 & D2 & D3 & D4 & D5 + SPA --> D4 + WI --> D6 + D1 & D2 & D3 & D4 & D5 & D6 --> V + V --> M["IC Memo
(deterministic + LLM lede)"] + V -- proceed --> L["Mock wire ledger"] + V -- hold --> A["Amendment PR
(Cursor Composer 2)"] +``` + +Each desk emits its own `desk.start` → `desk.citation*` → `desk.resolved` event sequence. The orchestrator never blocks on the slowest desk — `Promise.allSettled` plus a 30-second per-desk hard timeout guarantees the verdict layer always runs. + +--- + +## The amendment loop — the agent edits the agent + +```mermaid +flowchart LR + BLOCK["Wire-safety BLOCK
e.g. lookalike acrne.co"] + OVR["GP clicks
'Override & amend'"] + OCTX[("OverrideContext
+ runId + clause")] + COMP["Cursor SDK
Composer 2"] + MD[("MANDATE.md")] + DIFF["Unified diff
+ rationale paragraph"] + PR["Draft PR
(Octokit · optional)"] + REVIEW["Managing partner
reviews like code"] + MERGE[("Mandate v + 1
future runs enforce
the new clause")] + + BLOCK --> OVR --> OCTX --> COMP + MD --> COMP + COMP --> DIFF --> PR --> REVIEW --> MERGE + MERGE -. enforced by Desk 05 .-> BLOCK +``` + +> Every override is a learning event. The mandate becomes the fund's compounding moat — versioned, reviewed, merged. + +--- ## Quick start +The backend runs end-to-end against fixtures with **zero API keys** required. + ```bash -# Backend (port 3001) — runs end-to-end against fixtures, no API keys required +# Backend (port 3001) cd backend npm install npm run dev ``` -In a second terminal: +In a second terminal — the smoke driver POSTs both seeded scenarios to `/api/run`, prints the streaming desk events, fetches the memo, and draws the amendment-PR draft for the BEC run: ```bash cd backend npm run smoke ``` -The smoke driver POSTs the seeded `clean-acme` and `bec-acme` scenarios to -`/api/run`, prints the streaming desk events, fetches the resulting memo, and -draws the amendment-PR draft for the BEC run. - -For the UI: +Frontend (port 3000): ```bash cd front-end @@ -42,6 +291,10 @@ npm install npm run dev ``` +Then open . + +--- + ## Backend API All endpoints are versionless and consumed by the UI over HTTP. @@ -53,17 +306,16 @@ All endpoints are versionless and consumed by the UI over HTTP. | `GET` | `/api/memo/{runId}` | Returns `MemoData` for a completed run | | `POST` | `/api/amend` | Drafts an amendment PR from an `OverrideContext` | -The full SSE event schema lives in -[`backend/lib/contract.ts`](./backend/lib/contract.ts). Both the backend and -the UI import from that file. +The full SSE event schema lives in [`backend/lib/contract.ts`](./backend/lib/contract.ts) — a single source of truth shared by backend and UI. -### Curl smoke +
+Curl smoke (click to expand) ```bash # Health curl -s http://localhost:3001/api/health -# Streaming run (clean Acme — expected verdict: proceed, 6/6 desks pass) +# Streaming run — clean Acme (expected verdict: PROCEED, 6/6 desks pass) curl -N -X POST http://localhost:3001/api/run \ -H 'Content-Type: application/json' \ -d '{ @@ -90,30 +342,101 @@ curl -s -X POST http://localhost:3001/api/amend \ }' ``` -## Demo behaviour +
-- `DEMO_FORCE_FIXTURES=true` makes every source skip live calls and serve from - `backend/fixtures/`. Use this on stage if the network goes sideways. -- The clean Acme deal resolves all six desks PASS and produces a `proceed` - verdict. -- The BEC Acme deal lands a wire-safety BLOCK with four signals: lookalike - domain (edit distance 2 from `acme.co`), domain age 6 days, DKIM fail, and - beneficial-owner mismatch — driving a `hold` verdict. +--- -## Configuration +## Demo scenarios + +The demo ships with two seeded buttons — one contrast, one story. + +| Scenario | Verdict | What the audience sees | +| --- | --- | --- | +| 🟢 **Clean Acme deal** | `PROCEED` · 6/6 desks pass | Tiles light up green over ~30 s · IC memo renders · wire queues | +| 🔴 **BEC Acme deal** | `HOLD` · Wire desk BLOCK | Five tiles green; **Wire safety** lands red with: lookalike domain (`acrne.co` ↔ `acme.co`, edit distance 1), domain age 6 days, DKIM fail, beneficial-owner mismatch · BLOCK modal cites `wire_safety §6.2` · GP clicks **Override & amend** → Cursor Composer 2 drafts a `MANDATE.md` PR | + +Set `DEMO_FORCE_FIXTURES=true` to skip every live API and serve from `backend/fixtures/`. **The on-stage panic button.** -All external APIs (Specter, Companies House, OpenSanctions, WHOIS, OpenAI) are -optional — the backend defaults to fixtures and runs cleanly with no keys set. -See [`backend/.env.example`](./backend/.env.example) for the full list. +--- -## Build / typecheck +## Project layout + +``` +underwriter-cursorhack/ +├── backend/ # Next.js 16 Route Handlers (port 3001) +│ ├── app/api/ +│ │ ├── run/route.ts # POST · SSE stream of RunEvent +│ │ ├── memo/[runId]/route.ts # GET · MemoData +│ │ ├── amend/route.ts # POST · AmendmentDraft (Composer 2) +│ │ └── health/route.ts +│ ├── agents/ +│ │ ├── orchestrator.ts # fans out 6 desks, joins at verdict +│ │ ├── parse-prompt.ts # GPT-5.5 → ParsedDeal (Zod) +│ │ ├── synthesise.ts # findings → verdict (pure) +│ │ ├── memo.ts # findings + verdict → MemoData +│ │ ├── amend.ts # override → AmendmentDraft +│ │ └── desks/ # company · founder · investor +│ │ # round · mandate · wire-safety +│ ├── lib/ +│ │ ├── contract.ts # ⭐ THE SEAM — shared types +│ │ ├── mandate-loader.ts # gray-matter on MANDATE.md +│ │ ├── mandate-evaluator.ts # pure rule evaluation (no LLM) +│ │ ├── ledger.ts # mock wire ledger +│ │ ├── cache.ts # in-memory + fixture fallback +│ │ └── sources/ # specter · companies-house +│ │ # opensanctions · whois +│ │ # pdf-parse · llm (OpenAI wrapper) +│ ├── fixtures/ # full data fallback set +│ ├── MANDATE.md # the policy file the agent runs against +│ └── docs/ # Backend.md · ARCHITECTURE.md · DEMO.md +└── front-end/ # Next.js 16 UI (port 3000) + └── app/ + ├── components/ # MandateScreen · RunScreen · MemoScreen + │ # DeskTile · VerdictBar · BlockModal + │ # AmendmentPR · CreatePRModal + └── state/ # types · initial · fixtures +``` + +--- + +## Configuration + +All external APIs are **optional** — the backend defaults to fixtures and runs cleanly with zero keys. See [`backend/.env.example`](./backend/.env.example) for the complete list (Cursor SDK, OpenAI, Specter, Companies House, OpenSanctions, WHOIS, GitHub). ```bash -cd backend -npm run typecheck -npm run build +DEMO_FORCE_FIXTURES=true # bypass every live API — on-stage panic button +DEMO_GITHUB_REPO= # if set, amendments open real PRs via Octokit ``` -## License +--- + +## Design principles + +1. **The mandate is the spine.** Every decision is grounded in `MANDATE.md`. No agent has authority outside what the mandate grants. Overrides become amendments via PR. +2. **Six desks, parallel by default.** Each desk is a single-purpose subagent with one job, one data-source family, one output shape. They don't talk to each other — they join at the verdict step. +3. **Load-bearing data, not decorative.** Every desk has a primary source it cannot function without. If the source is down, the desk reports degraded confidence rather than fabricating. +4. **Calibrated escalation.** Desks don't say "looks fine." They say *PASS, confidence 0.94, basis: [Specter ID, Companies House filing, comparable round]*. The verdict layer treats confidence as input, not noise. +5. **The agent edits the agent.** Every override drafts an amendment PR. The fund's playbook compounds in git. +6. **Honest failure.** Three tiers (graceful degradation → desk-level flag → orchestrator error). Citations carry `cached: true` when fixtures fired. **We never fabricate a finding. We never silently omit a desk.** + +--- + +## What this deliberately does *not* do + +No real money movement (mock ledger only) · no email sending (drafts only) · no authentication (single-tenant demo) · no run persistence beyond process memory · no automatic retries (one shot, then fixture) · no PII logging. + +--- + +## Credits + +Built for the **Cursor Hackathon**. Thanks to: + +- **[Cursor](https://cursor.com)** — the SDK, Composer 2, and the cloud sandboxes that make subagents real +- **[Specter](https://tryspecter.com)** — the load-bearing dataset for company, founder, investor, and round-dynamics desks +- **OpenAI · Companies House · OpenSanctions** — the rest of the data spine + +--- -Internal hackathon project. +
+UnderWriter — your fund's policy, executed. +