Files
Croissant Le Doux 14200edb60 feat: scaffold outreach engine monorepo on the novelpad-desktop stack
Workspaces: config (copied), outreach-core (schema + actions/queries +
hard gates), outreach-ai (Gemini client + embeddings copies, profiler and
mission-fit-judge agent stubs), outreach-worker (DBOS executor with
nightly ingest + hourly expiry workflows), outreach-review (RR7 review
queue v0). Initial drizzle migration incl. pgvector extension.

Stack contract: Yarn 4.5.0 + Turbo, Node 22.16, Drizzle 0.44.6 +
pgvector, DBOS 4.17.6, @google/genai on Vertex, gemini-embedding-001
@1536, React Router v7. Files copied from novelpad-desktop carry
provenance headers @ 62c56b87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:08:24 -04:00

257 lines
29 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HelmDocs Grant-Match Outreach Engine — Solution Architecture & Campaign Plan
**Goal:** An automated pipeline that matches open grants to NH nonprofits, then runs a personalized email drip whose success metric is a booked demo call with high purchase intent.
**Core thesis of the funnel:** The grant match *is* the lead magnet. You are not cold-emailing a pitch — you are cold-emailing a found asset ("here is $480K you're well positioned for"). The software sale rides on the credibility of that match. This means the single most important engineering requirement in the whole system is **match accuracy**. One email claiming a grant the org isn't eligible for burns the sender domain's reputation *and* the brand's. Every architectural decision below flows from that constraint.
---
## System Overview
```
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Grant Ingestion │ │ Nonprofit Intake │ │ Contact Sourcing │
│ (Grants.gov API,│ │ (NHDOJ PDF + │ │ (Apollo API + │
│ 990-PFs, RFP │ │ IRS BMF/990 │ │ email verifier) │
│ feeds, state) │ │ enrichment) │ └────────┬─────────┘
└────────┬────────┘ └────────┬─────────┘ │
│ │ │
▼ ▼ │
┌─────────────────┐ ┌──────────────────┐ │
│ Grant Store │ │ Org Profiler │ │
│ Postgres + │ │ (web research → │ │
│ pgvector │ │ structured │ │
│ (hybrid: SQL │ │ profile w/ │ │
│ facts + vector │ │ citations) │ │
│ mission text) │ └────────┬─────────┘ │
└────────┬────────┘ │ │
│ │ │
└──────────┬────────────┘ │
▼ │
┌──────────────────────┐ │
│ Scoring Engine │ │
│ (deterministic gates │ │
│ + LLM fit subscores)│ │
└──────────┬───────────┘ │
▼ │
┌──────────────────────┐ │
│ Human Review Queue │◄────────────────────────┘
│ (approve match + │
│ contact before send)│
└──────────┬───────────┘
┌──────────────────────┐ ┌─────────────────┐
│ Apollo Sync Worker │─────►│ Google Calendar │
│ → Apollo sequences │ │ booking + events │
│ (send via our GWS │ │ table + feedback │
│ lookalike inboxes) │ │ loop to scoring │
└──────────────────────┘ └─────────────────┘
```
---
## Stage 1 — Grant Corpus Ingestion
**What it does:** Continuously pulls open grant opportunities from structured sources, normalizes them into one schema, and expires them the moment they close.
**Sources, in priority order:**
| Source | Access | Why it's in scope |
|---|---|---|
| Grants.gov Search2 API | Free, official REST API | Canonical federal source; structured JSON with eligibility codes, CFDA/ALN numbers, open/close dates. Zero scraping risk. |
| NH state agency portals (DHHS, DOE, DNCR, CDFA, Charitable Foundation of NH) | Mostly scrape/RSS; some post to Grants.gov | Geographic fit is your strongest scoring signal — a NH-only grant pool is dramatically less competitive than federal, which is exactly the "easy win" profile. |
| IRS 990-PF filings (ProPublica Nonprofit Explorer API / IRS bulk data) | Free | Private foundations must disclose every grant they made. This gives you *historical funder behavior*: a foundation that gave $40K to three NH youth-services orgs last year is a near-certain match for a fourth. Most foundation grants have no public RFP — this is how you find them anyway. |
| Candid Foundation Directory or Instrumentl | Paid (~$200$400/mo) | Pre-structured private-foundation RFPs. Buy this instead of scraping GrantWatch-style aggregators — their ToS prohibit it and their data is secondhand anyway. |
| Philanthropy News Digest RFP feed | Free RSS | Cheap incremental coverage of open private RFPs. |
**Normalized grant schema (the contract everything downstream depends on):** `funder`, `title`, `synopsis`, `eligibility_entity_types`, `geographic_scope`, `program_areas` (mapped to NTEE codes), `award_floor/ceiling`, `expected_awards_count`, `open_date`, `close_date`, `match_requirement`, `application_effort_estimate` (LLM-classified: LOI-only / short form / full federal), `source_url`, `last_verified_at`.
**Storage: Postgres with pgvector — not a pure RAG store.** Reasoning: eligibility, geography, and deadlines are *hard constraints*, and hard constraints must be enforced by deterministic SQL filters, not by semantic retrieval that might surface a closed or ineligible grant because the text "felt similar." Vector embeddings are used only for the one genuinely fuzzy dimension — mission/program fit between grant synopsis and org profile. Hybrid retrieval: SQL WHERE clauses gate, vectors rank.
**Freshness:** Nightly ingestion cron; a separate hourly sweep marks anything past `close_date` as expired; every grant that reaches the outreach stage gets a `last_verified_at` re-check within 48 hours of send. Justification: the entire pitch is time-sensitive money. Emailing a dead deadline is worse than not emailing.
---
## Stage 2 — Scoring Engine
**What it does:** For every (org, grant) pair that survives the hard gates, produces a 0100 score plus a machine-readable rationale, and flags the "easy win" subset.
**Two-layer design — deterministic gates, then weighted subscores:**
*Layer 1, hard gates (SQL, non-negotiable):* entity type eligible (501(c)(3) in good standing), geography matches, deadline ≥ 21 days out, award ceiling ≥ a floor that makes the email non-trivial (e.g., $10K+).
*Layer 2, weighted subscores (0100):*
| Signal | Weight | How it's computed |
|---|---|---|
| Mission/program fit | 30 | Embedding similarity between grant synopsis and org profile, *validated* by an LLM judge that must cite which org program matches which grant priority. Similarity alone false-positives on vocabulary overlap. |
| Funder precedent | 25 | From 990-PF history: has this funder given to orgs of this NTEE code / size / geography before? Strongest single predictor of foundation success. |
| Capacity fit | 15 | Award size vs. org revenue (from 990). A $480K federal grant to a $150K/yr org is a red flag, not a hook — federal funders screen for capacity, and savvy grant writers know it. Sweet spot: award is 1075% of annual revenue. |
| Competition level | 15 | expected_awards ÷ estimated applicant pool; state/local and NH-restricted pools score high. |
| Effort-to-award ratio | 10 | LOI or short-form apps score high. This is also your product tie-in: "20 minutes in HelmDocs" must be plausible for the specific grant you cite. |
| Deadline runway | 5 | 310 weeks is ideal: urgent enough to act, long enough to realistically apply. |
**"Easy win" flag** = total ≥ 75 AND funder-precedent ≥ threshold AND effort subscore ≥ threshold. These are the only matches that go into Email 1.
**Why deterministic weights wrapped around LLM subscores, instead of "ask the agent to score it":** reproducibility (you can re-run and audit any score), tunability (when demo-booking data comes back, you adjust weights, not prompts), and defensibility (every claim in an outbound email traces to a scored fact with a source URL). The LLM contributes judgment inside narrow, checkable subtasks; it never gets to invent the final number.
---
## Stage 3 — NH Nonprofit Intake
**What it does:** Parses the NHDOJ Charitable Trusts registry PDF into the org master table, then enriches every org with IRS data.
The NHDOJ PDF gives you name, registration status, and city — enough to establish "good standing," which matters because it's a hard eligibility gate and because emailing lapsed orgs wastes sends. Parse with pdfplumber or Camelot (it's a tabular layout); expect to handle multi-line org names and re-run when NHDOJ republishes (roughly monthly — diff against the prior run so new registrants enter the pipeline automatically; new registrants are often actively seeking first-time funding, which is a great segment).
**Enrichment via ProPublica Nonprofit Explorer API (free) keyed on name+city fuzzy match → EIN:** pulls NTEE code (your program-area taxonomy for matching), total revenue (capacity scoring), fiscal year end (budget-cycle timing for outreach), and officer names (contact sourcing seed). Orgs that don't resolve to an EIN or have no 990 on file get down-prioritized rather than dropped — many small orgs file 990-N postcards and are still viable prospects, just with a lower-confidence profile.
**Segmentation:** revenue $100K$5M is the primary ICP band. Below that, orgs rarely have anyone in a grant-writing seat; above it, they have professional development staff with entrenched tools (still reachable, but a different message — segment them out rather than mixing).
---
## Stage 4 — Org Profiling (Research Agent)
**What it does:** For each org in the ICP band, an agent runs web research (search API + site scrape) and produces a *structured, citation-backed* profile.
**Profile schema:** `mission_statement`, `programs[]` (name, description, population served), `service_geography`, `recent_news[]`, `known_funders[]` (from annual reports, website funder walls, and other funders' 990 disclosures), `staff[]` (ED, development/grants roles), `budget_band`, plus `sources[]` — every field carries the URL it came from.
**Why citations are mandatory, not nice-to-have:** two downstream consumers depend on facts about the org being true. The scoring engine's mission-fit judgment is only as good as the profile, and Email 1's personalization line ("given your after-school STEM program in Nashua…") is the credibility hook of the whole campaign. A wrong personalization detail reads as spam-with-extra-steps and gets you marked as such. The profiler should emit a confidence field, and low-confidence profiles route to the human review queue rather than to the sequencer.
**Cost control:** run profiling *after* a cheap pre-match pass. Don't deep-research all ~10K registered NH charities; first do a coarse match on NTEE code + geography against the grant corpus, and only profile the ~5001,500 orgs that have at least one plausible grant. Research is the expensive stage (LLM tokens + search API calls); matching gates it.
---
## Stage 5 — Contact Sourcing
**What it does:** Attaches a verified decision-maker email to each org.
**Recommendation: Apollo API (or Clay orchestrating Apollo + Hunter) — not direct scraping.** Three reasons. First, deliverability: scraped addresses have bounce rates that will torch a new sending domain within a week; licensed providers pre-verify. Second, legal: CAN-SPAM explicitly aggravates penalties for addresses harvested by automated means from websites, and many nonprofit sites' ToS prohibit it — for a product selling *to* compliance-minded grant professionals, that's a bad foundation. Third, titles: you need role targeting (Executive Director, Development Director, Grants Manager), which scrapers can't reliably give you.
Waterfall: Apollo person-search by org domain + title → fall back to the officer names from the 990 → last resort, the org's published contact address (generic `info@` goes to a separate low-priority sequence with softer copy, since you don't know who's reading). Every address passes through a verifier (NeverBounce/ZeroBounce); anything below "valid" is excluded. Target: hard-bounce rate under 2%.
---
## Stage 6 — Match Pipeline & Human Review
**What it does:** Joins scored matches to verified contacts, and gates every send behind a lightweight approval step.
Each org gets exactly one hero grant (its top-scoring easy win) and up to three runner-up grants held in reserve for Email 2. The review queue (a simple Retool/Next.js table is enough) shows: org profile summary, hero grant, score breakdown with the LLM's cited rationale, the rendered Email 1, and approve/reject/edit buttons. At launch, review 100%; once approval rate exceeds ~95% for two weeks, drop to sampling 20% plus auto-flagged cases (low profile confidence, deadline <30 days, award >$250K).
**Why a human gate on an "automated" pipeline:** the failure mode isn't average quality, it's tail risk — one confidently wrong match forwarded around the tight-knit NH nonprofit community (and it is tight-knit) does more damage than 50 good emails do good. The gate costs ~20 seconds per lead and caps that risk. It's also your labeling function: every reject reason becomes training signal for scoring weights.
---
## Stage 7 — The Drip Campaign
**Sending infrastructure first, because copy doesn't matter if you land in spam:** buy 23 lookalike domains (e.g., helmdocs-app.com, tryhelmdocs.com) — never the primary domain. Full SPF/DKIM/DMARC on each, 23 Google Workspace inboxes across them, connected to Apollo via OAuth (Apollo sequences send through the connected mailbox, so reputation stays ours). Run a dedicated warmup tool on each inbox for 34 weeks before the first cold send, then ramp from ~20 to a ceiling of ~50 sends/inbox/day and enroll in weekly cohorts of 50100. Email 1 should be plain text, no images, no tracking-heavy footers, and at most one link. NH's prospect pool is small enough (~1,0001,500 ICP orgs) that this modest throughput covers the whole state in weeks — a feature, not a limitation, because it forces quality.
**One compliance-critical copy change to your draft:** "You can secure $480,000" is a claim of outcome you can't guarantee, and it pattern-matches to advance-fee grant scams that nonprofits are specifically warned about. Reframe every instance as *fit*, not *promise*: "a $480K grant your org appears unusually well-positioned for." Same dopamine, none of the deception exposure (FTC/state UDAP), and it reads as more credible to a professional audience, not less. Also table stakes: physical mailing address in the footer, working one-click unsubscribe, honest subject lines, suppression list honored across all domains.
**The sequence (4 touches, days 0 / 3 / 8 / 14):**
| # | Day | Job of the email | Skeleton |
|---|---|---|---|
| 1 | 0 | Deliver the found asset. Sell nothing. | Subject: `{grant_name} — deadline {date}` · "Hi {first} — we track open grant funding for NH nonprofits. Based on {specific program, cited from their site}, {org} looks unusually well-positioned for {funder}'s {grant_name} — up to {amount}, {effort_level} application, closes {date}. Want the one-page brief on why it's a fit? Just reply 'send it.'" |
| 2 | 3 | Prove it's a system, not a fluke. | "That grant wasn't a one-off — our engine found {n} more open opportunities matching {org}'s programs, worth {total} combined. Here are the top two: … The full list takes 15 min to walk through: {calendar link}." |
| 3 | 8 | Bridge from *finding* money to *winning* it. Introduce the product via the free pilot. | "Finding the money is half the battle — {grant_name}'s application is {n} sections, and most teams lose 20+ hours to it. HelmDocs drafts it from documents you already have. {Beta user quote}. We're running free pilots for a handful of NH nonprofits: bring one grant — I'd suggest {grant_name} — and we'll put the full application through HelmDocs together. You keep the draft either way. 15 min to set it up: {link}" |
| 4 | 14 | Deadline-anchored breakup. Real urgency only. | "{grant_name} closes {close_date} — after that I'll stop emailing about it. The pilot offer stands until then: one working session, you leave with a full working draft, no commitment after. {link}. If this quarter's full, here's the one-page brief to file for later: {link}. Either way, good luck with it." |
**Why this order works for the "high intent" success criterion:** Email 1's CTA is a *reply*, not a meeting — replies train inbox providers that you're wanted (deliverability) and self-select engaged leads. The demo ask only appears once the prospect has received real value twice, so the people who book in Emails 24 arrive pre-sold on the premise (there's money on the table, applying is the bottleneck, this tool removes it). That's what makes the demo high-intent rather than curiosity-driven. The urgency in Email 4 is real — the deadline is the funder's, not yours — which matters because grant professionals are professionally skeptical of manufactured scarcity.
**The pilot offer (the campaign's commitment mechanism):** the post-demo motion is a free pilot — one grant application put through HelmDocs end to end. Frame the call itself around it: not "book a demo" but "bring {grant_name}, leave with a draft." This does four things. It converts the call from a vendor pitch into a working session the prospect has a selfish reason to attend; it aligns urgency with a real external deadline instead of manufactured scarcity; "you keep the draft either way" removes all downside and reads as confidence in the product; and the conversion moment becomes the draft materializing on screen — the product sells itself at the exact second the prospect's 20-hour problem visibly shrinks to 20 minutes. Cap the pilot scope explicitly (one application, one working session plus async review) so it doesn't turn into free consulting.
**Proof posture with beta-stage evidence:** you have beta user quotes, not case studies — use them honestly and specifically. One concrete quote with a real detail ("it drafted our LOI narrative in one sitting — Development Director, youth services org") outperforms an inflated aggregate claim, and this audience fact-checks. Never invent metrics; once the first pilots convert, their before/after time-to-submit becomes Email 3's case study and the quotes move to the landing page.
**A new hard gate this creates upstream:** because Emails 34 promise HelmDocs can draft *the specific grant being dangled*, the scoring engine's Layer 1 gates gain a fourth condition: `application_form_supported = true` for the hero grant. Only dangle grants the product can genuinely draft today — a pilot session that stalls on an unsupported form converts the highest-intent moment in the funnel into a refund of trust. Runner-up grants in Email 2 don't need this gate; the hero does.
**Behavioral branches:** reply → Apollo auto-pauses the enrollment and a human takes over; the one-page grant brief goes out within the hour via SendGrid from the primary domain (speed-to-brief is your conversion moment). Booked call → Calendar webhook logs it, contact is removed from the sequence and suppressed. (Apollo's sequencer doesn't support click-triggered branching — e.g., sending Email 3 early on a link click — which is an acceptable loss at this volume; revisit only if the funnel stalls between Emails 2 and 3.)
---
## Final Email Copy (v1, ready for merge fields)
**Email 1 — Day 0 · Subject: `{grant_name} — closes {close_date}`**
> Hi {first_name},
>
> I run HelmDocs — we track every open grant available to New Hampshire nonprofits. Based on {org_short}'s {specific_program, cited from their site}, you look unusually well-positioned for {funder}'s **{grant_name}**: up to {amount}, {effort_descriptor} application, closes {close_date}.
>
> Want the one-page brief on why it's a fit? Reply "send it" and I'll get it over today.
>
> {sender_name}
> HelmDocs · {physical address} · {unsubscribe}
**Email 2 — Day 3 · Subject: `{n} more for {org_short}`**
> The {grant_name} match wasn't a one-off — our engine found {n} more open grants that fit {org_short}'s work, worth {combined_total} combined. The top two:
>
> {grant_2} — up to {amount_2}, closes {date_2}
> {grant_3} — up to {amount_3}, closes {date_3}
>
> Happy to walk you through the full list in 15 minutes: {calendar_link}
**Email 3 — Day 8 · Subject: `the {grant_name} application, drafted`**
> Finding the money is half the battle. {grant_name}'s application runs {n} sections, and most teams lose 20+ hours to it.
>
> That's the part HelmDocs handles: you upload documents you already have — your 990, program descriptions, past proposals — and it drafts the application. One of our early users put it this way: "{beta_quote}" ({role}, {org_type}).
>
> We're running free pilots for a handful of NH nonprofits right now. Bring one grant — I'd suggest {grant_name} — and we'll put the full application through HelmDocs together. You keep the draft either way.
>
> 15 minutes to set it up: {calendar_link}
**Email 4 — Day 14 · Subject: `{grant_name} closes {close_date}`**
> {grant_name} closes {close_date} — after that I'll stop emailing about it.
>
> If you want the application drafted before the deadline, the pilot offer stands: one working session, you leave with a full working draft, no commitment after. {calendar_link}
>
> If this quarter's already full, here's the one-page brief to file for later: {brief_link}
>
> Either way — good luck with it.
Copy rules baked in: plain text; one link in Email 1 (none until the CTA); fit-language, never funding promises; every personalization token traces to a cited profile field; all merge fields are static values precomputed at enrollment (Apollo can't compute at send time — hence dates, not day counts); the only urgency is the funder's real deadline.
---
## Stage 8 — Measurement & Feedback Loop
Instrument the funnel end to end: deliverability (bounce <2%, spam complaints <0.1%), engagement (reply rate on Email 1 is the campaign's single most diagnostic number target 58% for this level of personalization; below 3% means the match quality or the hook is off), conversion (brief requested demo booked show rate), and revenue (demo trial paid).
The strategic loop: log the hero grant's score components for every lead, then regress demo bookings against them monthly. If funder-precedent-heavy matches book demos at 2x the rate of mission-fit-heavy matches, reweight the engine. The scoring engine isn't a static tool it's the thing the campaign is continuously teaching.
Second-order asset: every "reply 'send it'" response and every demo transcript tells you which grants NH nonprofits actually care about. That's product intelligence for HelmDocs itself (which application forms to template first) feed it back.
---
## Build Plan & Stack
| Phase | Weeks | Deliverable |
|---|---|---|
| 1. Data spine | 12 | Postgres+pgvector schema; Grants.gov + PND ingestion; NHDOJ PDF parser; ProPublica enrichment |
| 2. Intelligence | 34 | Org profiler agent; scoring engine v1 with hard gates + weighted subscores; 990-PF funder-precedent index |
| 3. Pipeline | 5 | Apollo contact integration (API or CSV adapter); review queue UI (Next.js); Apollo sequence templates + sync worker (push enrollments, pull engagement events) |
| 4. Launch | 6 | Lookalike domains + Workspace inboxes warmed via dedicated warmup tool (start this in week 1!); connect inboxes to Apollo via OAuth; 100-lead pilot cohort, 100% human-reviewed; instrument dashboards from `pipeline_events` |
| 5. Scale | 7+ | Ramp volume; monthly weight tuning; add Candid/Instrumentl feed; expand beyond NH by swapping the Stage 3 source |
**Stack (revised for TS + existing subscriptions):**
| Layer | Choice | Notes |
|---|---|---|
| Language/runtime | TypeScript, Node 20+, pnpm monorepo | Packages: `ingest`, `profiler`, `scoring`, `sequencer`, `review-ui`, `shared` (schema + types) |
| Database | Postgres + pgvector, Drizzle ORM | Drizzle has first-class pgvector column support; one DB holds grants, orgs, matches, contacts, and pipeline events |
| Job orchestration | pg-boss | Postgres-backed job queue cron schedules, retries, and dead-letter handling with zero extra infra (no Redis) |
| LLM access | Vercel AI SDK as the multi-provider connector | One `generateText`/`generateObject` interface; providers swap per task. LM Studio exposes an OpenAI-compatible endpoint, so `@ai-sdk/openai-compatible` pointed at `localhost:1234/v1` covers local models; `@ai-sdk/anthropic` (API key) covers frontier calls |
| Model routing | Tiered by task | Local (LM Studio): bulk classification, effort estimates, first-pass profile extraction, embeddings via `/v1/embeddings`. Frontier API: mission-fit judge, email-personalization QA the two places where a wrong answer reaches a prospect. Note: Claude *subscriptions* are for interactive use; a production pipeline like this should authenticate with a Claude Platform API key (pay-as-you-go) |
| Embeddings | One local embedding model via LM Studio, dimension fixed in the pgvector column | Never mix embedding models vectors from different models aren't comparable; re-embed everything if you ever switch |
| Contacts | Apollo (existing sub) | If the plan includes API access (Settings Integrations API), use it; otherwise the pipeline ingests Apollo CSV exports from a watched folder same schema either way, API becomes a drop-in upgrade. Verification deferred (NeverBounce slot reserved in the contact module interface) |
| Sending | Apollo sequences sending through our own Google Workspace inboxes (lookalike domains) connected via OAuth | Apollo is the scheduler, not the infrastructure: it sends through whatever mailbox is connected, so domain reputation stays ours and portable. **Never** use Apollo's native/purchased mailboxes or domains shared infrastructure, shared reputation. Built-in reply detection auto-pauses a contact's enrollment (our replyhuman branch), plus per-mailbox rolling 24h throttling and opt-out handling. Warmup is *not* covered use a dedicated warmup tool (~$25/mo) for the first month per inbox and hold the 3050/day cap after. SendGrid stays for transactional mail only (brief delivery after a reply, pilot confirmations) from the primary domain |
| Sequencer package | Shrinks to an Apollo sync worker | Outbound: push approved matches as contacts with precomputed custom fields (hero grant name, amount, close date, personalization line) into the right sequence, enrolled in weekly cohorts of 50100 (large enrollment spikes trigger spam filters). Inbound: pull engagement events (opens/replies/bounces per contact) into `pipeline_events` API if the plan has it, else weekly CSV export through the same watched-folder adapter as contacts. Constraint: Apollo merge fields are simple substitution with no conditional logic or send-time computation, so every field must be precomputed at enrollment |
| Scheduling | Google Calendar appointment schedules | Booking link as the sequence CTA; a Calendar webhook (or daily poll) writes bookings into pipeline events |
| Tracking | `pipeline_events` table in Postgres + a CSV export script | Same effort as raw CSV, but joinable against scores for the feedback loop; export keeps the CSV workflow |
| Review queue | Self-built Next.js app on the same Drizzle schema | One screen: match card (org profile, hero grant, score breakdown w/ cited rationale, rendered Email 1), approve / edit / reject-with-reason. Reject reasons are an enum they become the labels for tuning scoring weights |
**SendGrid caution (important):** SendGrid's acceptable-use policy prohibits unsolicited email and purchased/sourced lists, and cold outreach on its shared IPs both risks account suspension and inherits the reputation of every other sender on the pool. It's built for transactional and opted-in marketing mail, which is the opposite of this workload. Cold sends therefore run through Apollo sequences our own Google Workspace inboxes on lookalike domains (23 inboxes, 50 sends/day each ample for ~1,500 prospects statewide). SendGrid keeps the job it's good at: transactional sends from the primary domain (brief-delivery emails after a reply, pilot confirmations).
**Deliberate scope exclusions:** no LinkedIn automation at launch (adds channel risk before email is proven), no fully-autonomous sending (the human gate stays until the data earns its removal), and no expansion past NH until reply rate 5% the model must work in a market small enough to hand-inspect before it's pointed at one that isn't.