← All work

Funding Radar

Grant-discovery agent that ranks funding a student hardware startup can actually apply for

Jun–Jul 2026Personal

TypeScriptNode 20ExaClaude Haiku 4.5 (structured outputs)Gemini FlashSupabase / local JSON storeYAML configGitHub Actions (weekly)Vercel (static)

Problem

A pre-incorporation, student-led lab-hardware venture needs non-dilutive money, but Canadian grants, prizes and accelerators are scattered across funder sites with buried eligibility rules, and most of what a naive search returns is closed, province-locked, or equity-only. Re-checking that list every week is the same shape of work as an SDR pipeline, with funding programs as the leads. The tool had to reject what the team cannot apply for and say why, rather than pad the list.

What I built

Funding Radar is a TypeScript pipeline that runs discover → extract → dedupe → qualify → score → store → report, scheduled weekly via GitHub Actions. Exa runs sixteen hand-written queries plus sixteen seed funder pages (capped at 60 candidates per run), Claude Haiku turns each page into a schema-enforced Opportunity record, and a three-pass dedupe collapses the same program found on different pages. Hard eligibility filters run as plain code: geography that excludes Canada/BC, equity-only programs, passed deadlines, and incorporation requirements each reject or downgrade with a stated reason. Gemini Flash returns six 0–100 subscores, but the final fit score is a weighted sum computed in code from a YAML profile whose weights must total 100, so ranking is deterministic and explainable. Results persist to Supabase or a local JSON store with a sticky 'applied' status across runs, then render as a grouped Markdown report, a CSV, and a self-contained static dashboard on Vercel that ships only the generated HTML.

Highlights

  • The rank is computed in code, not by the model: the LLM only emits per-dimension subscores; fit_score is a weighted sum from config weights validated to sum to 100 at load, so a config drift fails loudly instead of skewing every score.
  • Code guards over prompt guards: geography, equity-only, closed-deadline and incorporation rules are deterministic filters that reject with a reason and keep the rejected list visible in the report for transparency.
  • Runs with zero API keys: each missing key degrades to fixtures or a deterministic mock, so plumbing, dedupe and qualify logic can be validated before spending a cent; PIPELINE_TODAY pins 'today' for reproducible runs.
  • Dedupe before scoring rather than after (the spec's order) so no LLM calls are spent on duplicates; three-pass matching (funder+name key, canonical URL, token overlap) handles same-program-different-page and acronym-vs-full-name funders.
  • Publish by whitelist: .vercelignore excludes everything except the generated dashboard folder, so source, .env, fixtures and the data store cannot be uploaded; the Gemini client sets the thinking budget to zero so a reasoning model cannot spend its output budget on thought and return empty JSON.

Numbers

  • 40 opportunities after dedup on the 2026-06-30 run: 27 rejected with a stated reason, 10 qualified, 3 on watch
  • 13 opportunities shown on the public dashboard (2 closing soon, 8 rolling intake, 3 gated on incorporation); the 27 rejections stay in the report only
  • 16 hand-written discovery queries plus 16 seed funder pages per run, capped at 60 candidates
  • Runs end-to-end with zero API keys using 8 real fixture opportunities and deterministic mocks
  • Six scoring dimensions whose weights are validated to sum to 100 at config load

Screenshots

Funding Radar: Static dashboard header: grouped counts (closing soon / rolling / needs incorporation / applied) and the top-ranked dated opportunity with its fit score and why-it-fits.
Static dashboard header: grouped counts (closing soon / rolling / needs incorporation / applied) and the top-ranked dated opportunity with its fit score and why-it-fits.
Funding Radar: Rolling-intake section: each row carries a code-computed fit score, amount, deadline, a one-line why-it-fits and a concrete next action.
Rolling-intake section: each row carries a code-computed fit score, amount, deadline, a one-line why-it-fits and a concrete next action.