Skip to content

Repository files navigation

VC Scout

CI

A partner runs one command and gets fifteen startups triaged against a written investment thesis - each with a one-page memo in which every claim traces back to a public URL.

The design goal is not "an LLM writes memos". It is auditability: every analytical claim cites an evidence ID, every evidence claim cites a source URL and carries a verbatim excerpt, the final recommendation is made by deterministic policy rather than by a model, and a whole run replays from persisted artifacts with no network and no API key.

Start here

Browse the live run demo/site/index.html - 15 companies, scored and written up. Preview: python3 -m http.server 8000 --directory demo/site
Read the shortlist demo/ranking.md - thesis, thresholds, the triage queue
Read one memo demo/memos/n8n-io.md - the best-evidenced candidate in the run
See one AI call end to end demo/ai-trace/ - request, response, and what validation decided
How AI was used docs/AI_WORKFLOW.md - decisions, constraints, failures
How it was built worklog/ - nine chronological stage entries, written as it happened
Five-minute walkthrough docs/WALKTHROUGH.md

The pipeline

flowchart LR
    Q[Seed query] --> S[Source]
    S --> E[Enrich]
    E --> V[Evidence]
    V --> A[Analysis]
    A --> P[Policy]
    P --> M[Memos + UI]

    classDef llm fill:#e9f0ec,stroke:#1f4d3a,stroke-width:2px,color:#16181a;
    classDef det fill:#f4f1ea,stroke:#8a8579,color:#16181a;
    class V,A llm;
    class Q,S,E,P,M det;
Loading

Green is the only place a model runs. Evidence extracts source-backed claims; Analysis scores seven rubric dimensions and writes the narrative. Everything else - discovery, fetching, the total, the confidence, the recommendation, the memos and the site - is deterministic Python. The model's own suggested recommendation is recorded next to the binding call and never consulted; in the live run the two disagreed on 7 of the 15.

Quick start

uv sync
export ANTHROPIC_API_KEY=...      # required only for a live run

uv run vc-scout run \
  --query "AI customer support and back-office automation for small businesses" \
  --limit 15 \
  --run-id ai-smb-ops-demo \
  --provider anthropic \
  --model claude-sonnet-5 \
  --effort low

One command runs the whole pipeline - source → enrich → evidence → analysis → memos → site - and prints a stage timeline, the recommendation counts and where everything landed. Roughly 15 candidates costs about 45 HTTP requests and 30 model calls.

Try it with no API key

uv run vc-scout demo --run-id offline-demo

The same orchestrator, the same stages, the same HTTP and Algolia clients - only the transport underneath them serves committed fixtures instead of the internet, and the provider is deterministic. It produces real memos and a real site in about a second.

Resume, rather than repeat

Running run again on a finished run makes no network and no provider call: every stage whose artifacts are still current is resumed. Rebuild one stage and everything derived from it with --force-stage, which never touches anything upstream:

uv run vc-scout run ... --force-stage analysis   # re-analyse, re-render, re-publish
uv run vc-scout run ... --stop-after evidence    # stop early, then continue later

Repair a run that partly failed

If a run left a few analyses failed - a model returning the wrong shape, a transient provider error - retry only those, rather than paying for all fifteen again:

uv run vc-scout recover-analysis --run-id ai-smb-ops-demo --provider anthropic

It reads the full report, retries only the candidates recorded as failed, merges the results back into that report with its candidate order and every total recomputed, and rebuilds the memos and the site offline. Every analysis that already succeeded is left byte for byte as it was.

Look at the result

python3 -m http.server 8000 --directory outputs/runs/ai-smb-ops-demo/site
# then open http://127.0.0.1:8000/

Hand it to a reviewer

uv run vc-scout export-demo --run-id ai-smb-ops-demo

Writes a self-contained demo/ directory - the site, the memos, the ranking, every validated artifact, and one AI call end to end with what was sent, what came back and what was checked. No raw HTML, no credentials, no absolute paths: the export refuses to write rather than ship any of them.

Individual stages

Every stage is also its own command, which is how the pipeline was built and how it is debugged:

uv run vc-scout --help              # full command surface
uv run vc-scout config              # active rubric, thresholds and confidence policy
uv run vc-scout source   --query "..." --run-id my-run
uv run vc-scout enrich   --run-id my-run
uv run vc-scout analyze  --run-id my-run --evidence-only
uv run vc-scout analyze  --run-id my-run
uv run vc-scout recommend --run-id my-run
uv run vc-scout build-ui --run-id my-run

A stage run this way carries no provenance fingerprint, so a later run reruns it rather than trusting it. That is deliberate: an artifact whose input cannot be established is not an artifact a memo should be built on.

Commands

Command Purpose Status
source Discover candidates from Hacker News available
enrich Fetch and extract public company pages available
analyze --evidence-only Extract source-grounded evidence available
analyze Score and apply the recommendation policy available
recover-analysis Retry only the candidates a run failed on available
recommend Write partner-ready memos and the ranking available
render Deprecated alias for recommend available
build-ui Generate the static research site available
build-site Deprecated alias for build-ui available
serve Serve a generated report locally planned
run Full pipeline end to end, with resume available
demo The whole pipeline offline, from committed fixtures available
export-demo Assemble a reviewer-ready demo/ directory available
config Show the live rubric and thresholds available

Discovery

vc-scout source searches the public Hacker News Algolia API with a bounded, deterministic family of twelve queries:

Variant Tag Words Weight
query-show-hn show_hn all required 1.00
query-launch-hn launch_hn optional 0.85
query-story story optional 0.50
intent-smbintent-ecommerce-retail (9) (show_hn,launch_hn) optional 0.90–0.95

The nine intent-* variants pair the query's own AI-automation wording with a specific buyer or workflow — SMB, small business, business operations, customer support, sales, finance and accounting, scheduling, back office, ecommerce and retail. They exist because a genuinely relevant product often describes its workflow without ever using the operator's phrasing: a scheduling agent for salons will never say "SMB operations".

Only the first variant requires every query word. The rest relax word matching, and the recall that buys is paid for by the relevance gate below — not by hoping the query happens to match.

Relevance

Every story is classified from its title, one-liner, hostname and URL path against three concept groups:

Group Signal Weight
A AI automation — ai, agent, agentic, automation, copilot, assistant, … 0.40
B Business buyer — smb, small business, merchant, retailer, contractor, … 0.25
C Operational workflow — invoicing, scheduling, customer support, payroll, back office, … 0.35
relevance score = 0.40 x A + 0.25 x B + 0.35 x C     (each group: min(matches, 2) / 2)

direct      A and (B or C)   an AI product with an identifiable buyer or workflow
adjacent    A only           an AI product naming neither
irrelevant  no A             not an AI product at all

A business tool with no AI signal is irrelevant, not direct: groups B and C qualify an AI product, they do not substitute for one. Candidates classified irrelevant, or scoring below the minimum relevance of 0.20, are discarded before anything is ranked.

Ordering

Ranking is lexicographic, not a single weighted score:

1. relevance class      direct before adjacent
2. relevance score
3. quality score        0.55 x engagement + 0.25 x recency + 0.20 x variant weight
   engagement = log10(1 + points + 2 x comments) / log10(1 + 500), capped at 1.0
   recency    = 1.0 up to 30 days old, decaying linearly to 0.0 at 720 days

Engagement therefore orders candidates that are already equally relevant and can never lift a generic agent-infrastructure launch above an on-topic workflow product. A single composite score previously allowed exactly that; see D14 in docs/DECISIONS.md.

The shortlist is filled from directly relevant candidates first. Adjacent candidates are held to 30% of the shortlist while direct supply lasts, and may fill the remainder only once it runs out. The run never pads: if fewer defensible candidates exist than were requested, it returns what it found and reports the shortfall.

Every component, every matched term and every rejection is recorded in source-report.json, so the shortlist can be recomputed and argued with by hand.

Discovery ranking is not investment scoring. It runs before any page is fetched, knows nothing about the thesis rubric, and is never read by the recommendation policy. A top-ranked candidate can still be recommended pass.

Enrichment

vc-scout enrich reads a bounded set of pages from each candidate's own website: the homepage, the exact URL posted to Hacker News when it differs, and up to three internal pages chosen deterministically by role (product, pricing, customers, about, team, changelog, blog - at most one per role, same-origin only). Pages are deduplicated by final URL and by content hash, reduced to readable text, and persisted with their fetch metadata for replay.

No candidate is ever removed for having a thin or unreachable site. A company whose pages could not be read keeps an empty bundle with categorised failures, so the gap stays visible to the stages that judge it. Missing information is missing, not negative.

Fetch safety

Every URL this stage touches came from third-party text, so all fetching goes through one hardened client:

  • http and https only, on their default ports
  • hostnames resolved before connecting; loopback, private, link-local, multicast, reserved and unspecified addresses are refused, including IPv4-mapped IPv6 forms
  • redirects followed manually so every hop is revalidated, capped at 3
  • explicit 5s connect and 15s read timeouts
  • responses abandoned mid-stream at 2 MB rather than downloaded then measured
  • only text/html is parsed; text is capped at 20,000 characters per page
  • robots.txt honoured; 401 and 403 respected, never worked around
  • a descriptive User-Agent, and no credential, cookie or authorization header ever sent

Persisted fetch metadata contains response facts only - requested URL, final URL, redirect chain, status, content type, content hash, byte count and timestamp. No request headers, cookies, environment values or credentials are logged or stored.

Evidence extraction

vc-scout analyze --evidence-only hands each company's own material to a language model under a versioned prompt and writes back only what can be verified against that material.

The model is treated as an untrusted witness. It sees a bounded, per-candidate view - that company's Hacker News record and its extracted pages, nothing else. It never sees another candidate, a discovery rank, the rubric or the thesis. Structured output is obtained with forced tool use against a fixed JSON schema.

Nothing it returns is written until it has been checked:

Check Rejection
Every cited source_id was supplied for this candidate unknown_source_reference
Every claim carries a supporting excerpt schema_validation_failed
Every excerpt appears in the text of the source it is attached to excerpt_not_found
independently_supported is backed by two or more separate sources schema_validation_failed
No duplicate claims schema_validation_failed

Claim identifiers are derivedev-<sha256(company_id, claim, sources)[:12]> — never supplied by the model, so a claim cannot be given an identity it did not earn. Invalid output earns exactly one retry carrying the validation errors back; a second failure is recorded and the run continues.

Each claim carries two independent labels: verification_status (company_claim / community_signal / independently_supported) and inference_status (explicit / inferred). Absence is first-class: unknowns records what the sources did not establish and conflicts retains sources that disagree. A company with no readable website still gets a dossier — the gap becomes unknowns, never a negative claim.

Prompt-injection defence

Source pages are arbitrary text that any founder can edit. Two defences:

  1. System instructions live in a separate channel and contain nothing about any company. Source text appears only in the user message, fenced in explicit BEGIN/END UNTRUSTED SOURCE <id> markers and introduced as data, never instructions.
  2. Validation makes compliance irrelevant. A model that obeyed an injected instruction to invent revenue would still have to produce an excerpt, and there is no excerpt — so nothing is written. This is the defence that is actually tested.

Replay

Every attempt persists a request and a response artifact under llm/, carrying the exact bounded source content supplied, the prompt version and hash, the structured payload, the validation result and errors, token usage, stop reason and latency. No credential, header, cookie or absolute path is ever written — asserted by a test that greps every artifact. A stored response can be re-validated without calling the provider.

Analysis, scoring and the recommendation

vc-scout analyze reads the evidence dossiers - and only the dossiers. Raw pages, raw Hacker News responses and the web are unreachable from this stage: what counts as evidence was already decided and verified upstream.

What the model does: the narrative, a per-dimension assessment against the rubric, a thesis-fit verdict, risks, open questions, and an advisory recommendation.

What the model does not do: the total (recomputed in Python from its own components), the research confidence (computed from coverage facts), or the binding recommendation (made by deterministic policy). Its suggestion is recorded and compared, never obeyed.

Assessment status caps the score

The score measures the strength of the evidence-backed investment case, not the company's objective worth. Every dimension carries a status, and the status caps it:

Status Score ceiling Meaning
supported 100% of maximum The evidence backs this
partially_supported 70% Some evidence, not enough
contradicted 100% The evidence shows a problem — and must say what
not_assessable 50% Nothing was found. Not a finding against the company

not_assessable is deliberately neither forced to zero nor to the midpoint — the model must choose and explain how the score reflects the uncertainty. scored_out_of reports how many points were assessable, so a low total can be read correctly.

Research confidence

Computed deterministically after the model answers, from six coverage components with bounded penalties for identity warnings, conflicts and unknowns. A zero-claim dossier scores 0.0. The full formula and its thresholds are in D30 of docs/DECISIONS.md and are reproduced in vc_scout.policy.compute_confidence.

The independently_supported label earns nothing on its own — only findings the analysis explicitly names as corroborated count (D31).

Recommendation guardrails

Bands are 80-100 take a meeting, 65-79 watch, 0-64 pass. Then:

  • a meeting also needs medium confidence, an identifiable product and buyer, no identity warning, and evidence in four dimensions;
  • a pass band with more than three unassessable dimensions and low confidence becomes watch for insufficient evidence — unless the evidence positively shows a thesis mismatch, which may still pass;
  • a zero-claim dossier becomes watch, never a fabricated score narrative;
  • an unresolved cross-domain identity mismatch caps at watch;
  • a missing website never forces a pass on its own.

Every guardrail that fires is named in the artifact, next to the band it moved from, the model's suggestion, and whether the two disagreed.

Memos and the ranking

vc-scout recommend turns the stored artifacts into a partner-ready read. It makes no provider call, needs no API key and touches no network — it renders what earlier stages already validated.

uv run vc-scout recommend --run-id source-test
uv run vc-scout recommend --run-id source-test --force   # re-render over existing output
outputs/runs/source-test/
├── memos/<company_id>.md          # one ~700-900 word memo per candidate
├── ranking.md                     # the reviewer's entry point
└── recommendation-report.json     # what was rendered, and what could not be

Each memo opens with the call, the score and the confidence, then a snapshot table, why this call (the policy's own rationale, verbatim, plus every guardrail in plain language), a Team/Product/Market view, the seven-dimension scorecard, risks and open questions, the two-or-three things that would change the call, and a numbered source list:

# rulemesh.com

**Pass** · 40/100 · high confidence

**One-sentence call:** Pass: rulemesh.com scored 40/100 against the thesis rubric on
high-confidence research, with thesis fit recorded as adjacent - short of what a meeting needs.
...
| Pain and measurable ROI | 8 / 20 | Partially supported | Compliance is plausibly recurring… | [S1] [S2] |
...
**[S1]** Compliance Requirements Engineers Can Actually Implement | RuleMesh · company homepage · <https://rulemesh.com/> · observed 2026-08-23

Three properties are worth knowing:

  • Deterministic. Same artifacts, same bytes. Nothing here carries a generated timestamp, and every collection is sorted, so re-rendering is a check rather than a new opinion.
  • Every citation resolves. [S1] markers are numbered in reading order, each resolves to exactly one source entry, and every listed source is cited somewhere above. A source URL is rendered as its own link text, so a memo cannot show a label that points somewhere else. Internal ev-/unk- identifiers never appear as a reader-facing citation.
  • Untrusted text stays text. Company names, page titles, model narrative and page excerpts all came off third-party pages. They are neutralised before rendering, so they can contribute words but never headings, tables, code blocks, HTML, images or links.

ranking.md carries the thesis, the rubric, the thresholds, the run summary and a table ordered by call, then score, then confidence, then name. That ordering is a triage queue, not a quality ranking, and the document says so: a watch that exists only because the research came up short is not a claim that the company is better than one that was passed on evidence. When no candidate reaches the meeting band, the ranking explains why from the run's own counts rather than talking a candidate up to fill it.

The static site

vc-scout build-ui turns the same artifacts into a read-only site: a portfolio page and one page per company. No provider call, no API key, no network, no build step, and no runtime dependency of any kind - two files of hand-written CSS and JavaScript sit beside the HTML and nothing else is fetched.

uv run vc-scout build-ui --run-id source-test
uv run vc-scout build-ui --run-id source-test --force   # rebuild over an existing site

# preview it
python3 -m http.server 8000 --directory outputs/runs/source-test/site
# then open http://127.0.0.1:8000/
outputs/runs/source-test/site/
├── index.html                     # the portfolio: summary, thesis, filters, table
├── companies/<company_id>.html    # one page per company
├── assets/styles.css              # one stylesheet, system fonts, no imports
├── assets/app.js                  # search, filter, sort. No framework, no network
└── ui-report.json                 # what was generated, and what could not be

The portfolio page carries the run summary, the thesis and thresholds, live search and filters (recommendation, confidence, thesis fit) with sorting, and a table that becomes stacked cards on a phone. Every row links to a company page carrying the decision header, the snapshot, why this call with the policy's rationale verbatim, the seven-dimension scorecard, the investment view, risks and open questions, what would change the call, the numbered sources, and a provenance panel. Company pages print cleanly.

Security posture. Every page declares default-src 'none'; style-src 'self'; script-src 'self'; img-src 'self'; font-src 'self'; base-uri 'none'; form-action 'none'. Jinja autoescaping is on and no untrusted string is ever marked safe. Every href is validated - absolute http/https only, or a relative path this generator built - so a javascript: or data: URL cannot become a link. There are no inline event handlers, no inline styles, no remote images, no external scripts, stylesheets or fonts. The embedded filter data is escaped so that no value can close the <script> element it sits in, and the JavaScript writes only through textContent.

Accessibility. One <h1> per page and no skipped heading levels, labelled form controls, scoped table headers, visible focus rings, external links marked with an icon and rel="noopener noreferrer nofollow", prefers-reduced-motion respected, no horizontal overflow at 375px, and a recommendation that is never carried by colour alone - each badge pairs its colour with the word and a distinct glyph.

Investment thesis

We invest in seed-stage, AI-native software companies that automate recurring, revenue-critical workflows for SMBs. The product should produce measurable value within 30 days, integrate into an existing system of record, and develop defensibility through proprietary workflow data, distribution, integrations or operational depth rather than relying only on model access.

Scoring

Dimension Points
Pain and measurable ROI 20
Product wedge 15
Distribution 15
Defensibility 15
Team 15
Traction and freshness 10
Market and timing 10
Total 100

80-100 take a meeting, 65-79 watch, 0-64 pass.

Research confidence is separate from the investment score. The score answers "how well does this fit the thesis?"; confidence answers "how much did we actually find out?". Missing information is recorded as unknown, never as a negative judgment - it leaves a dimension unscored and lowers confidence, and low confidence caps the recommendation at watch.

Architecture

src/vc_scout/
├── pipeline.py          one-command orchestration: order, resume, stop/continue
├── stages/              source · enrich · evidence · analysis · recommend · ui · recover · export
├── llm/                 provider (raw httpx), compact schemas, the real validators, fake provider
├── models/              Pydantic artifact contracts, frozen, extra="forbid"
├── render/              Markdown memos and the HTML site, from shared view models
├── policy.py            confidence and the binding recommendation. No model, no network
├── rubric.py            the seven dimensions and their weights
├── thesis.py            the thesis, versioned and content-hashed
├── assessment_policy.py what a source may be used to support, per dimension
└── prompts/             versioned prompt files, hashed into every artifact

Every artifact is a validated Pydantic document written atomically with sorted keys, so a run diffs cleanly. Every path is built by RunStore and asserted to live inside the run directory. Derived reports carry a fingerprint of the artifacts they were derived from, so a resume can tell "already done" from "done against different inputs".

Output structure

outputs/runs/<run-id>/
├── candidates.json          source-report.json
├── extracted/<id>.json      enrichment-report.json
├── evidence/<id>.json       evidence-report.json
├── analyses/<id>.json       analysis-report.json
├── memos/<id>.md            ranking.md   recommendation-report.json
├── site/                    index.html · companies/<id>.html · assets/ · ui-report.json
├── llm/                     every request and response, per attempt
├── raw/                     fetched page bodies
└── run-report.json          the whole run: stages, timings, tokens, versions

Trace a claim back to its source

This is the property the whole pipeline exists to give you. Open demo/memos/n8n-io.md and pick a sentence carrying a marker:

  1. The scorecard row for Product wedge cites [S2].
  2. Sources at the foot of the memo resolves [S2] to one entry: the page title, its role, the public URL and the date it was read - with the excerpt quoted beneath it.
  3. demo/artifacts/analyses/n8n-io.json shows which ev- claim IDs that row cited.
  4. demo/artifacts/evidence/n8n-io.json shows each of those claims with its verbatim excerpt and the src- source it came from.

Every marker in a memo resolves to exactly one source, and every listed source is cited somewhere above it. A statement resting only on a recorded gap is labelled Open question rather than left unattributed.

Design decisions and limitations

56 numbered decisions with their costs are in docs/DECISIONS.md. The ones that shape the output most:

  • The model never makes the call. The total is recomputed in Python from its own components; confidence comes from coverage; the recommendation comes from policy.py.
  • Absence of evidence is not evidence of weakness. A dimension the sources cannot reach is not_assessable, which caps what it may score and lowers confidence - it never becomes a negative finding, and the memos say so in those words.
  • Provenance caps what a source may conclude, not whether it may. A company's own page is good evidence of what its product does; it is not evidence of a result, an advantage or a scale.
  • A conflict blocks the dimension it is about, not every dimension sharing a page with it.

Known limitations: the evidence in this run is thin and mostly company-authored, so no candidate reached the meeting band; the rubric is uncalibrated against real outcomes; prompt injection is mitigated rather than solved; sourcing depends on what Hacker News happened to contain that week. docs/AI_WORKFLOW.md states these in full.

Testing

uv run pytest                  # 950+ tests, offline, no API key required
uv run ruff check .
uv run ruff format --check .
uv run mypy src                # strict on src/

The suite cannot reach the network: an autouse fixture blocks sockets and DNS and strips every *_API_KEY from the environment, so a regression that reintroduced a live call fails loudly rather than quietly costing money. Network stages are exercised through the production clients over httpx.MockTransport; the LLM is exercised through a deterministic fake that quotes only what it was given. CI runs the same four commands on Python 3.12.

Five-minute walkthrough

docs/WALKTHROUGH.md is a timed outline:

0:00-0:30 The problem, and the thesis being applied
0:30-1:10 One command, and where the models actually run
1:10-2:30 One startup from a Hacker News post to verified evidence
2:30-3:30 Analysis, the score, and the deterministic call
3:30-4:15 The dashboard and one memo
4:15-5:00 AI workflow, what broke, trade-offs

Documentation

AI assistance

Built with AI assistance, disclosed in full in docs/AI_WORKFLOW.md. Every commit is authored by the repository owner; no commit carries an AI trailer.

License

No license provided. All rights reserved by the repository owner.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages