From 95344c99e764ff4fd2f6366290357542e43376ba Mon Sep 17 00:00:00 2001 From: Shub Gaur Date: Wed, 27 May 2026 22:22:47 -0700 Subject: [PATCH 1/2] founder-gtm: add plugin Co-authored-by: Cursor --- .cursor-plugin/marketplace.json | 5 + founder-gtm/.cursor-plugin/plugin.json | 39 + founder-gtm/.gitignore | 12 + founder-gtm/CHANGELOG.md | 111 +++ founder-gtm/LICENSE | 21 + founder-gtm/README.md | 115 +++ founder-gtm/assets/logo.svg | 6 + founder-gtm/automations/AUTOMATIONS.md | 62 ++ founder-gtm/automations/README.md | 43 + founder-gtm/automations/daily-followups.md | 39 + .../automations/daily-followups.workflow.json | 28 + .../automations/positive-reply-ping.md | 34 + .../automations/post-campaign-debrief.md | 39 + .../post-campaign-debrief.workflow.json | 37 + founder-gtm/automations/trial-expiry-sweep.md | 35 + founder-gtm/automations/weekly-get-better.md | 31 + .../weekly-get-better.workflow.json | 29 + .../canvases/founder-gtm-playbook.canvas.tsx | 900 ++++++++++++++++++ founder-gtm/hooks/check-voice-on-edit.sh | 96 ++ founder-gtm/hooks/hooks.json | 18 + founder-gtm/hooks/welcome-on-first-session.sh | 51 + founder-gtm/resources.md | 106 +++ founder-gtm/rules/gtm-voice-guide.mdc | 135 +++ founder-gtm/skills/gtm-cold-email/SKILL.md | 404 ++++++++ .../gtm-cold-email/scripts/gmail-auth.py | 61 ++ .../gtm-cold-email/scripts/requirements.txt | 3 + founder-gtm/skills/gtm-design-play/SKILL.md | 217 +++++ .../plays/linkedin-job-change-eng-leader.md | 88 ++ .../plays/recent-seed-fundraisers-vp-eng.md | 70 ++ .../plays/show-hn-launches-ai-infra.md | 69 ++ .../skills/gtm-find-prospects/SKILL.md | 399 ++++++++ .../data/personal-email-domains.txt | 50 + .../data/title-exclusions.txt | 31 + .../data/title-keywords.txt | 17 + .../gtm-find-prospects/scripts/README.md | 47 + .../scripts/domain-histogram.py | 119 +++ .../scripts/hn-show-scraper.py | 115 +++ .../scripts/requirements.txt | 3 + .../scripts/techcrunch-funding-rss.py | 136 +++ .../scripts/title-classifier.py | 217 +++++ .../scripts/x-topic-search.py | 132 +++ founder-gtm/skills/gtm-get-better/SKILL.md | 357 +++++++ .../skills/gtm-linkedin-outreach/SKILL.md | 293 ++++++ founder-gtm/skills/gtm-playbook/SKILL.md | 18 + founder-gtm/skills/gtm-sales-pack/SKILL.md | 324 +++++++ .../scripts/extract-voice-from-gmail.py | 342 +++++++ .../gtm-sales-pack/scripts/requirements.txt | 3 + founder-gtm/skills/gtm-setup/SKILL.md | 171 ++++ founder-gtm/skills/gtm-warm-intro/SKILL.md | 247 +++++ founder-gtm/skills/gtm-x-outreach/SKILL.md | 215 +++++ 50 files changed, 6140 insertions(+) create mode 100644 founder-gtm/.cursor-plugin/plugin.json create mode 100644 founder-gtm/.gitignore create mode 100644 founder-gtm/CHANGELOG.md create mode 100644 founder-gtm/LICENSE create mode 100644 founder-gtm/README.md create mode 100644 founder-gtm/assets/logo.svg create mode 100644 founder-gtm/automations/AUTOMATIONS.md create mode 100644 founder-gtm/automations/README.md create mode 100644 founder-gtm/automations/daily-followups.md create mode 100644 founder-gtm/automations/daily-followups.workflow.json create mode 100644 founder-gtm/automations/positive-reply-ping.md create mode 100644 founder-gtm/automations/post-campaign-debrief.md create mode 100644 founder-gtm/automations/post-campaign-debrief.workflow.json create mode 100644 founder-gtm/automations/trial-expiry-sweep.md create mode 100644 founder-gtm/automations/weekly-get-better.md create mode 100644 founder-gtm/automations/weekly-get-better.workflow.json create mode 100644 founder-gtm/canvases/founder-gtm-playbook.canvas.tsx create mode 100755 founder-gtm/hooks/check-voice-on-edit.sh create mode 100644 founder-gtm/hooks/hooks.json create mode 100755 founder-gtm/hooks/welcome-on-first-session.sh create mode 100644 founder-gtm/resources.md create mode 100644 founder-gtm/rules/gtm-voice-guide.mdc create mode 100644 founder-gtm/skills/gtm-cold-email/SKILL.md create mode 100644 founder-gtm/skills/gtm-cold-email/scripts/gmail-auth.py create mode 100644 founder-gtm/skills/gtm-cold-email/scripts/requirements.txt create mode 100644 founder-gtm/skills/gtm-design-play/SKILL.md create mode 100644 founder-gtm/skills/gtm-design-play/plays/linkedin-job-change-eng-leader.md create mode 100644 founder-gtm/skills/gtm-design-play/plays/recent-seed-fundraisers-vp-eng.md create mode 100644 founder-gtm/skills/gtm-design-play/plays/show-hn-launches-ai-infra.md create mode 100644 founder-gtm/skills/gtm-find-prospects/SKILL.md create mode 100644 founder-gtm/skills/gtm-find-prospects/data/personal-email-domains.txt create mode 100644 founder-gtm/skills/gtm-find-prospects/data/title-exclusions.txt create mode 100644 founder-gtm/skills/gtm-find-prospects/data/title-keywords.txt create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/README.md create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/domain-histogram.py create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/hn-show-scraper.py create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/requirements.txt create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/techcrunch-funding-rss.py create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/title-classifier.py create mode 100644 founder-gtm/skills/gtm-find-prospects/scripts/x-topic-search.py create mode 100644 founder-gtm/skills/gtm-get-better/SKILL.md create mode 100644 founder-gtm/skills/gtm-linkedin-outreach/SKILL.md create mode 100644 founder-gtm/skills/gtm-playbook/SKILL.md create mode 100644 founder-gtm/skills/gtm-sales-pack/SKILL.md create mode 100644 founder-gtm/skills/gtm-sales-pack/scripts/extract-voice-from-gmail.py create mode 100644 founder-gtm/skills/gtm-sales-pack/scripts/requirements.txt create mode 100644 founder-gtm/skills/gtm-setup/SKILL.md create mode 100644 founder-gtm/skills/gtm-warm-intro/SKILL.md create mode 100644 founder-gtm/skills/gtm-x-outreach/SKILL.md diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index dae8230..fb81eb6 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -63,6 +63,11 @@ "source": "orchestrate", "description": "Fan large tasks out across parallel Cursor cloud agents with planners, workers, verifiers, and structured handoffs." }, + { + "name": "founder-gtm", + "source": "founder-gtm", + "description": "Go-to-market toolkit for early-stage founders: sales pack, prospecting, outbound on X, LinkedIn, email, warm intros, and a weekly learning loop." + }, { "name": "pstack", "source": "pstack", diff --git a/founder-gtm/.cursor-plugin/plugin.json b/founder-gtm/.cursor-plugin/plugin.json new file mode 100644 index 0000000..183eb4a --- /dev/null +++ b/founder-gtm/.cursor-plugin/plugin.json @@ -0,0 +1,39 @@ +{ + "name": "founder-gtm", + "displayName": "Founder GTM", + "version": "0.5.1", + "description": "Go-to-market toolkit for early-stage founders: sales pack, prospecting, outbound on X, LinkedIn, email, warm intros, and a weekly learning loop.", + "author": { + "name": "Cursor Foundry", + "email": "foundry@cursor.com" + }, + "homepage": "https://github.com/cursor/plugins/tree/main/founder-gtm", + "repository": "https://github.com/cursor/plugins", + "license": "MIT", + "logo": "assets/logo.svg", + "category": "productivity", + "tags": [ + "gtm", + "outbound", + "founders", + "sales" + ], + "keywords": [ + "gtm", + "go-to-market", + "outbound", + "sales", + "founders", + "cold-email", + "linkedin", + "x", + "twitter", + "lemlist", + "gmail", + "prospecting", + "warm-intros" + ], + "skills": "./skills/", + "rules": "./rules/", + "hooks": "./hooks/hooks.json" +} diff --git a/founder-gtm/.gitignore b/founder-gtm/.gitignore new file mode 100644 index 0000000..2a6e341 --- /dev/null +++ b/founder-gtm/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules/ +.env +.env.* +!.env.example + +# Founder-local artifacts (each founder's own; not committed) +sales-pack.md +prospects/ +outreach-log/ +.gtm-state/ +*.local.md diff --git a/founder-gtm/CHANGELOG.md b/founder-gtm/CHANGELOG.md new file mode 100644 index 0000000..056e17b --- /dev/null +++ b/founder-gtm/CHANGELOG.md @@ -0,0 +1,111 @@ +# Changelog + +## 0.5.1 (2026-05-27) + +Marketplace readiness pass before private team rollout. + +- Added a lightweight `gtm-playbook` skill so the canvas is discoverable from the marketplace skill list. +- Added a plugin logo and shortened the manifest description for the Marketplace hero card. +- Updated README and distribution notes for the Anysphere team marketplace flow. +- Removed remaining hardcoded local plugin paths from runtime docs and scripts. Scripts now resolve the plugin root from `CURSOR_PLUGIN_ROOT` or their installed file location. +- Automation workflow JSON now defaults to the current project root, where `sales-pack.md` and `outreach-log/` live, instead of assuming a local plugin checkout. + +## 0.5.0 (2026-05-27) + +Seven upgrades shipped from a focused improvement pass. + +**New skill** +- `gtm-warm-intro`, reads the founder's LinkedIn connections CSV, ranks bridges per prospect, drafts the intro-request message plus a forwardable blurb the bridge can paste verbatim. Warm intros convert at 5 to 10x cold; this is the single highest-leverage channel skill in the plugin. Sends via Gmail when available or generates copy-paste markdown. + +**New first-run capability** +- `gtm-sales-pack` Section 6 (Voice) now offers three paths: (A) extract voice patterns from your sent Gmail via the existing OAuth token, (B) paste samples manually, or (C) use defaults. The new `scripts/extract-voice-from-gmail.py` pulls sentence-length distribution, opener capitalization, punctuation tics, recurring n-grams, sign-off style, and 3 redacted excerpts. Read-only and idempotent. Requires `google-api-python-client`, `google-auth`, `google-auth-oauthlib` (pinned in the new `scripts/requirements.txt`). + +**Self-improving learning loop** +- `gtm-get-better` Step 8.5 now proposes edits to the skill files themselves when a pattern wins consistently (N≥20, 2x positive rate, 2+ cycles). Founder approval gates every change. Provenance markers (``) track which edits came from the loop. Capped at 3 proposals per run. Excludes `rules/gtm-voice-guide.mdc`. + +**Three starter plays in `gtm-design-play/plays/`** +- `recent-seed-fundraisers-vp-eng.md`, person+account hybrid, cold email channel, full 4-touch cadence +- `show-hn-launches-ai-infra.md`, person signal, X DM primary + email fallback +- `linkedin-job-change-eng-leader.md`, person signal, LinkedIn channel +- Each is fully populated. Founders fork them instead of designing from scratch. + +**Voice enforcement at write-time** +- New `afterFileEdit` hook (`hooks/check-voice-on-edit.sh`) scans saved files under `outreach-log/`, `prospects/`, and `drafts/` for AI tells: em/en dashes, "I hope this finds you well" variants, "Excited to announce", "Thrilled to share", and the high-risk subject words from `gtm-cold-email`. Returns `additional_context` warnings; fail-open, non-blocking. + +**Lookalike targeting** +- `gtm-find-prospects` Step 3.5 now has a complete recipe: reads `existing-customers.txt`, extracts 3 attributes per customer, aggregates the dominant pattern, runs LinkedIn / Crunchbase / X bio cross-search, dedupes against existing + pipeline, and scores prospects with a +10 boost for full-triangle attribute matches. + +**Voice rule expanded** +- `rules/gtm-voice-guide.mdc` gained a 10-pattern "Anti-AI tells" section sourced from `blader/humanizer`: superficial -ing tails, copula avoidance, false ranges, persuasive authority tropes, signposting, hyphenated predicate overuse, sycophantic tone, generic positive conclusions, filler phrases, excessive hedging. Each with the fix. + +**Plumbing cleanups** +- All em/en dashes swept from prose across `skills/`, `rules/`, `hooks/`. Preserved only inside fenced code blocks and warning-subject examples (where the dash is the subject of the warning). +- `scripts/requirements.txt` added to both `gtm-sales-pack/scripts/` and `gtm-cold-email/scripts/` pinning the Gmail API dependencies. + +**Canvas refresh** +- Hero stats updated to reflect 9 skills, 6 helper scripts, 3 starter plays. Skills table adds the `/gtm-warm-intro` row. "What's in the package" footer now mentions the starter plays and the second hook. + +## 0.4.0 (2026-05-27) + +Plugin-wide rename: all skills now prefixed `gtm-` (except `gtm-setup` which already had it). + +**Renamed** +- `sales-pack` → `gtm-sales-pack` +- `find-prospects` → `gtm-find-prospects` +- `design-play` → `gtm-design-play` +- `x-outreach` → `gtm-x-outreach` +- `linkedin-outreach` → `gtm-linkedin-outreach` +- `cold-email` → `gtm-cold-email` +- `get-better` → `gtm-get-better` + +Every cross-reference updated across SKILL.md files, the canvas, README, CHANGELOG, resources.md, automations, and `plugin.json`. + +**Other v0.4.0 changes** +- Fixed `gtm-find-prospects` description rendering: `` was being parsed as an HTML tag and stripped. Replaced with `{campaign}` here and in two other skills with the same pattern. +- `rules/gtm-voice-guide.mdc` gained the "Anti-AI tells (the humanizer pass)" section. +- `gtm-linkedin-outreach` now asks tool choice + daily-limit at Step 0 (before any prereq check). Account-type table gives suggested daily caps. Per-day counter file refuses sends past the limit. +- New `hooks/welcome-on-first-session.sh` greets new founders in any project without a sales-pack yet. Gated by `.gtm-state/welcomed` marker so it only fires once per project. + +## 0.3.0 (2026-05-27) + +Added based on internal GTM-repo distillation and early founder feedback. + +**New skill** +- `design-play`, codify a working signal/persona/cadence/offer into a reusable play. Reads from working campaigns; writes to `plays/.md`. Drawn from the internal play-design framework. + +**New automations folder** +- `automations/` with five recommended Cursor Automation specs: + - `weekly-get-better` (cron, Mondays) + - `daily-followups` (cron, weekday mornings, advances cold-email sequences under the daily cap) + - `post-campaign-debrief` (cron or Slack-trigger, ingests replies the morning after a send) + - `positive-reply-ping` (cron, notifies only on positives) + - `trial-expiry-sweep` (cron, optional for PLG founders on Stripe) + +**New find-prospects scripts** +- `scripts/techcrunch-funding-rss.py`, recent rounds from TechCrunch RSS +- `scripts/hn-show-scraper.py`, Show HN launches via Algolia +- `scripts/x-topic-search.py`, bio + topic search via the local xmcp MCP +- `scripts/domain-histogram.py`, work vs personal email split for any CSV +- `scripts/title-classifier.py`, keyword + exclusion bucketing (no LLM, no API key) +- `data/title-keywords.txt`, `data/title-exclusions.txt`, `data/personal-email-domains.txt`, tunable by the founder without touching Python + +**Edits to existing skills** +- `cold-email`: subject-line framework with reasons (works / fails / why), sender identity section with the warm-domain trust multiplier. +- `sales-pack`: split high-signal vs low-signal CTAs, added "Features by buyer need" and "Signal strength cheat sheet" template sections. +- `get-better`: pos+obj rate added as secondary metric, signal-first retirement rule, "do not re-offer" rule, high-risk subject auto-flag. +- `find-prospects`: signal source catalog mapped to the new scripts, account-mode confidence scoring. +- `rules/gtm-voice-guide.mdc`: anti-patterns expanded (no Step-1 bullets, one CTA with breakup exception), mechanics block (numerals, sentence case, straight quotes), transferable technical-buyer principles. + +**Distribution** +- Added `LICENSE` (MIT) and this `CHANGELOG.md`. +- Enriched `.cursor-plugin/plugin.json` with `displayName`, `homepage`, `repository`, `license`. + +## 0.1.0 (2026-05-27) + +Initial scaffold. + +- 7 skills: `gtm-setup`, `sales-pack`, `find-prospects`, `x-outreach`, `linkedin-outreach`, `cold-email`, `get-better`. +- `rules/gtm-voice-guide.mdc` (always-applied anti-slop voice rules). +- `canvases/founder-gtm-playbook.canvas.tsx` (the visual playbook). +- `resources.md` (opinionated reading list). +- `skills/cold-email/scripts/gmail-auth.py` (one-time Gmail OAuth bootstrap). diff --git a/founder-gtm/LICENSE b/founder-gtm/LICENSE new file mode 100644 index 0000000..5a2c756 --- /dev/null +++ b/founder-gtm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cursor Foundry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/founder-gtm/README.md b/founder-gtm/README.md new file mode 100644 index 0000000..bacbefe --- /dev/null +++ b/founder-gtm/README.md @@ -0,0 +1,115 @@ +# founder-gtm + +> A scrappy go-to-market toolkit for early-stage founders. Built as a Cursor plugin. + +Most early-stage founders fall into one of two traps. They blast generic AI templates that go straight to spam. Or they do nothing because the right tool stack costs $1,500 to $3,000 a month and they're not ready for it. + +This plugin is a third path: spend an afternoon wiring up your own outbound machine using what you already have, send 25 real messages a day, and let the system get a little smarter every week. + +## What's in the package + +Three things ship together: + +1. **This plugin**, ten skills you trigger from chat, three starter plays you can fork, five recommended Cursor Automations, six helper scripts, and two hooks (welcome + voice enforcement). +2. **A Cursor canvas** at `canvases/founder-gtm-playbook.canvas.tsx`, the visual playbook. +3. **`resources.md`**, a curated reading list of free or cheap GTM resources for founders. + +## Install + +```bash +/add-plugin founder-gtm +``` + +Then reload Cursor and run **`/gtm-setup`** for the 30-minute onboarding. + +**Share link:** send people to the landing page or Marketplace listing. After install, tell them to run **`/gtm-setup`** in Cursor. + +**Development only** (contributors hacking on this repo): + +```bash +git clone https://github.com/cursor/plugins.git +cd plugins +ln -s "$PWD/founder-gtm" ~/.cursor/plugins/local/founder-gtm +``` + +Then reload Cursor and open the canvas at `canvases/founder-gtm-playbook.canvas.tsx`. + +## The skills + +| Skill | When to use | What it does | +|---|---|---| +| `/gtm-setup` | First install | Walks you through setup in the right order. Picks channels, runs prereqs. | +| `/gtm-playbook` | To open the visual guide | Opens the Founder GTM canvas and gives a short orientation before you start setup. | +| `/gtm-sales-pack` | Before any outreach | Interviews you (~25 questions, one at a time) about your company, ICP, value props, common objections, persona-specific positioning, and your writing voice. Writes a `sales-pack.md` knowledge base every other skill reads from. | +| `/gtm-find-prospects` | To build a target list | Asks what targeting tools you already have, then combines free sources (LinkedIn search, X via xmcp, GitHub, Crunchbase free, TechCrunch funding RSS, Show HN) with whatever paid tools you've connected. Outputs a ranked CSV. | +| `/gtm-design-play` | To codify what's working | Turns a signal/persona/channel/cadence combination that produced replies into a reusable play under `plays/.md`. | +| `/gtm-x-outreach` | To run X DMs | Pulls each target's last 10 to 20 posts via the local xmcp MCP, finds a real hook, drafts a personalized DM in your voice, sends or saves to drafts. | +| `/gtm-linkedin-outreach` | To run LinkedIn outreach | Drafts ≤250-char connection notes grounded in the target's profile and your sales pack. Sends via Lemlist (recommended), Amplemarket, La Growth Machine, or manual copy-paste. | +| `/gtm-cold-email` | To run cold email | Connects to Gmail via the Google Workspace CLI, checks domain warming, drafts personalized sequences, saves them as drafts or sends with a hard daily cap. Reply detection cancels pending follow-ups when someone replies. | +| `/gtm-warm-intro` | When you have mutual connections (5 to 10x cold conversion) | Reads your exported LinkedIn connections CSV, matches bridges per prospect, drafts the intro-request message plus a forwardable blurb the bridge person can paste verbatim. Sends via Gmail or generates copy-paste markdown. | +| `/gtm-get-better` | Weekly | Reads logs across all channels, classifies replies on the standard rubric (positive / objection / neutral / OOO / negative), slices metrics per-play and per-touch, retires losing plays at N≥15, and proposes edits to the skill files themselves when a pattern wins consistently (founder approval required per edit). | + +## Prerequisites at a glance + +`/gtm-setup` walks you through these. Quick reference: + +| Skill | Needs | +|---|---| +| `gtm-sales-pack` | Nothing required. Optionally: Gmail OAuth (reuses the cold-email token) so the voice section can extract patterns from your sent mail instead of asking you to paste samples. | +| `gtm-find-prospects` | Whatever tools you have. Nothing required. | +| `gtm-x-outreach` | Local xmcp MCP server running at `http://127.0.0.1:8000/mcp`. Free up to the X API free tier; pay-per-call beyond. | +| `gtm-linkedin-outreach` | Lemlist account + API key (cheapest path). Or Amplemarket / La Growth Machine if you already have them. | +| `gtm-cold-email` | Google Workspace account (not free gmail.com), gcloud CLI installed, Gmail OAuth client. Domain should be warmed (the skill recommends Instantly or Smartlead's free tier if not). | +| `gtm-warm-intro` | LinkedIn connections export CSV (Settings → Data Privacy → Get a copy of your data). Optional: Gmail OAuth for "last interaction with this bridge person" recency scoring. | +| `gtm-get-better` | One prior campaign to learn from. | + +## What's in the plugin folder + +``` +founder-gtm/ +├── README.md +├── distribution.md +├── CHANGELOG.md +├── LICENSE +├── resources.md +├── .cursor-plugin/plugin.json +├── canvases/ +│ └── founder-gtm-playbook.canvas.tsx +├── rules/ +│ └── gtm-voice-guide.mdc +├── hooks/ +│ ├── hooks.json (sessionStart + afterFileEdit) +│ ├── welcome-on-first-session.sh (first-run greeting) +│ └── check-voice-on-edit.sh (AI-tell scan on saved drafts) +├── skills/ +│ ├── gtm-setup/ +│ ├── gtm-playbook/ +│ ├── gtm-sales-pack/ +│ │ ├── SKILL.md +│ │ └── scripts/extract-voice-from-gmail.py (reads sent mail for voice patterns) +│ ├── gtm-find-prospects/ +│ │ ├── SKILL.md +│ │ ├── scripts/ (5 small scrapers + helpers) +│ │ └── data/ (title keyword lists, personal-email domains) +│ ├── gtm-design-play/ +│ │ ├── SKILL.md +│ │ └── plays/ (3 starter plays to fork) +│ ├── gtm-x-outreach/ +│ ├── gtm-linkedin-outreach/ +│ ├── gtm-cold-email/ +│ │ ├── SKILL.md +│ │ └── scripts/gmail-auth.py (one-time OAuth bootstrap) +│ ├── gtm-warm-intro/ (5 to 10x cold conversion via mutual connections) +│ └── gtm-get-better/ +└── automations/ + ├── README.md + ├── weekly-get-better.md + ├── daily-followups.md + ├── post-campaign-debrief.md + ├── positive-reply-ping.md (optional) + └── trial-expiry-sweep.md (optional) +``` + +## License + +MIT. Fork it, remix it, send PRs. diff --git a/founder-gtm/assets/logo.svg b/founder-gtm/assets/logo.svg new file mode 100644 index 0000000..fb581b4 --- /dev/null +++ b/founder-gtm/assets/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/founder-gtm/automations/AUTOMATIONS.md b/founder-gtm/automations/AUTOMATIONS.md new file mode 100644 index 0000000..4d7d6e0 --- /dev/null +++ b/founder-gtm/automations/AUTOMATIONS.md @@ -0,0 +1,62 @@ +# Automations + +Three Cursor Automation workflows ship with this plugin. They're optional — the skills work fine without them — but if you want the system to actually compound without you having to remember to run things, install these. + +| File | What it does | Schedule | +|---|---|---| +| `weekly-get-better.workflow.json` | Runs `/gtm-get-better` on Mondays at 7am PT | weekly | +| `daily-followups.workflow.json` | Runs cold-email reply check + advances follow-up queue every weekday at 9am PT | daily, M–F | +| `post-campaign-debrief.workflow.json` | Manual trigger; campaign-scoped debrief | on-demand | + +## How to install + +### Path 1: ask the agent (recommended) + +Open a chat in Cursor and say: + +> Install the founder-gtm automations from the installed Founder GTM plugin's `automations/` folder. + +The agent reads each `.workflow.json`, opens the Cursor Automations UI via the `cursor-app-control` MCP's `open_automation` tool, prefills the form from the JSON, and you click save. Repeat for each file. + +### Path 2: manual + +1. Open Cursor → Automations. +2. Click "New automation". +3. Copy the contents of the `.workflow.json` file you want to install. +4. Paste / fill in the fields: + - Name + description from the JSON + - Trigger (cron string + timezone) + - Workflow step type: `agent` + - Workspace root: your project root, where `sales-pack.md` and `outreach-log/` live. + - Agent prompt: paste the `workflow.steps[0].prompt` string verbatim +5. Save. + +## What you should expect + +- **Weekly get-better** will email or Slack you a short summary on Mondays (depending on your Automation notification settings). You can ignore it on weeks when nothing changed; reply to it when there's a positive reply you should personally handle. + +- **Daily followups** runs silently most days. You'll only get notified when a positive reply comes in that needs you to write a real response. This is the high-value notification — treat it like a Slack DM from your best customer. + +- **Post-campaign debrief** is for when you want to know *now* whether a campaign worked, instead of waiting for the next Monday. Run it manually 7 days after a campaign goes out. + +## What these don't do + +- They don't send autonomously without you in the loop on positives. The state machine sends scheduled follow-ups (which you already approved when you created the campaign), but never an inbound reply to a real human. +- They don't classify reply intent better than you can. If the LLM marks something as "positive" that you think was lukewarm, override it. The classification rubric is in `skills/gtm-get-better/SKILL.md` if you want to read or tune it. +- They don't run when Cursor isn't open or when your Mac is asleep. Cursor Automations require a running Cursor instance (cloud automations are coming; check the latest docs). + +## Editing the schedule + +Open the `.workflow.json` you want to change, edit the `cron` and `timezone` fields, and re-import. Or just edit the saved automation in the Cursor Automations UI directly — no need to round-trip through the file. + +Standard cron, 5 fields, UTC unless `timezone` is set: `minute hour day-of-month month day-of-week`. + +## When to add your own + +After a month of running these three, you'll probably want one or two more. The pattern is the same: each file is an agent prompt + a trigger. Common ones founders add: + +- **Friday wrap-up**: end-of-week summary of replies, drafts pending, and next week's outreach plan. +- **Specific play monitor**: re-run a single play's debrief automatically every Friday. +- **Inbound-from-X**: when xmcp gets reliable webhook support, ping when a prospect replies on X. + +Drop new `.workflow.json` files in this directory and re-import via Path 1. diff --git a/founder-gtm/automations/README.md b/founder-gtm/automations/README.md new file mode 100644 index 0000000..5c3e734 --- /dev/null +++ b/founder-gtm/automations/README.md @@ -0,0 +1,43 @@ +# Cursor Automations for founder-gtm + +Cursor Automations are scheduled or event-triggered agent runs, configured in Cursor's Automations editor. They turn this plugin from a set of skills you remember to run into a system that runs on its own and pings you when it matters. + +This folder is a set of **automation specs** you create once. Each spec below is fully drafted: name, trigger, tools, prompt, and the deferred settings you finish in the editor. + +## How to install + +You have two paths: + +**Easy.** Open Cursor's Automations UI, click "New", and copy the fields from the spec below into the form. + +**Easier.** Open chat in your Cursor workspace and type: + +> "Create the founder-gtm weekly-get-better automation from the installed Founder GTM plugin's `automations/weekly-get-better.md` file." + +The built-in `automate` skill will pick it up, run the integration discovery for any Slack channels or repos it needs, show you a draft table, and open the Automations editor pre-filled with the right values. + +You can do the same for the other two specs. + +## The three automations to start with + +| Automation | Trigger | Why it matters | +|---|---|---| +| `weekly-get-better` | Cron, Monday 9am local | Closes the learning loop. Skipping it is what turns this plugin into a one-shot tool. | +| `daily-followups` | Cron, weekdays 7am local | Advances the cold-email sequence one step and sends queued follow-ups under the daily cap. | +| `post-campaign-debrief` | Slack message or manual trigger | Right after a campaign finishes, ingest replies and tell you what worked. | + +Two more, optional, that founders commonly want once the first three are running: + +| Automation | Trigger | Why it might matter | +|---|---|---| +| `positive-reply-ping` | Cron, weekdays 9am local | Notifies you only when a reply was classified positive. Keeps noise low. | +| `trial-expiry-sweep` | Cron, Tuesday + Saturday 7am local | For PLG founders on Stripe. Surfaces trials ending in 7 days with enough seats to be worth a touch. | + +Each spec is its own markdown file in this folder. + +## Notes on Cursor Automations + +- They run in Cloud Agents, so they keep working when your laptop is closed. +- Each one needs to be reviewed in the Automations editor before saving (channel IDs, repo scopes, schedule, etc.). The specs below mark every field that needs to be finalized there. +- You can disable any automation from the same UI without deleting it. +- Cloud compute pricing applies; see the [Cloud Agent dashboard](https://cursor.com/dashboard?tab=cloud-agents). diff --git a/founder-gtm/automations/daily-followups.md b/founder-gtm/automations/daily-followups.md new file mode 100644 index 0000000..8a53acb --- /dev/null +++ b/founder-gtm/automations/daily-followups.md @@ -0,0 +1,39 @@ +# Automation: daily-followups + +> Advance the cold-email sequence one step every weekday morning, under the daily cap. + +## Spec + +| Field | Value | +|---|---| +| **Name** | daily-followups | +| **Description** | Every weekday at 7am, run `/gtm-cold-email --followups` so Touch 2 / 3 / 4 messages go out on cadence. Honors the daily send cap from `.env` and the per-thread reply state so anyone who already replied gets dropped from the queue. | +| **Trigger** | Cron: `0 7 * * 1-5` (weekdays 7am local) | +| **Tools** | None required. Cold-email uses the Gmail OAuth token stored at `${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json`. | +| **Optional tools** | `slack` — to ping you if the daily cap was hit or if any send failed (e.g. bounce). | +| **Prompt** | Run `/gtm-cold-email --followups`. Send all queued Touch 2 / 3 / 4 messages whose scheduled date is today or earlier, up to the configured daily cap. Stop early if you hit the cap. After the run, report the count of sent, the count of skipped-due-to-reply, the count of skipped-due-to-cap, and any errors. If anything errored, post a one-line summary to {{slack channel or DM}}. | + +## To finish in the Automations editor + +- **Schedule timezone.** Confirm 7am matches your local timezone. Earlier is better for B2B (catches the prospect at the top of their inbox). +- **Slack channel** (optional, error-only). Pick a DM with yourself so cap-hit and bounce notifications are visible without spamming a team channel. +- **Cloud compute.** Daily runs are cheap; the agent only acts when there's a queued follow-up. + +## Safeguards + +The skill itself enforces: + +- Hard daily cap (default 25; configurable in `${CURSOR_PLUGIN_ROOT}/.env`). +- Reply check via Gmail API before sending each follow-up. If the recipient replied, the rest of the sequence is canceled for that thread automatically. +- Plain-text only, no tracking pixels. +- Random spacing between sends (60–300 seconds default). + +## When to disable + +- You're on vacation and don't want auto-replies firing to people who reply. +- You've paused all cold-email campaigns. +- Your sending domain reputation just took a hit (warm again before resuming). + +## Variants + +If you want Touch 1 sends to be drafts-only and only Touch 2–4 to send automatically, change the prompt to `Run /gtm-cold-email --followups --touches 2,3,4`. New campaigns will land in your Drafts folder for manual review; the automation handles the follow-up cadence once you click send on Touch 1. diff --git a/founder-gtm/automations/daily-followups.workflow.json b/founder-gtm/automations/daily-followups.workflow.json new file mode 100644 index 0000000..209ffbb --- /dev/null +++ b/founder-gtm/automations/daily-followups.workflow.json @@ -0,0 +1,28 @@ +{ + "name": "founder-gtm — daily followups + reply check", + "description": "Runs the cold-email state machine once a day. Checks replies, cancels pending follow-ups on any thread that replied, advances the queue for everyone else (respecting the daily cap and spacing). Surfaces positive replies for the founder to handle personally.", + "trigger": { + "type": "schedule", + "cron": "0 16 * * 1-5", + "timezone": "America/Los_Angeles", + "notes": "Fires weekdays at 9am Pacific. Late enough that overnight OOO replies are in, early enough that the day's follow-ups go out at normal business hours." + }, + "workflow": { + "steps": [ + { + "type": "agent", + "workspace": { + "root": ".", + "fallbackToProjectRoot": true + }, + "prompt": "Run the gtm-cold-email reply state machine for today.\n\n1. Run /gtm-cold-email --check-replies first:\n - Pull recent inbound from Gmail (last 14 days, threads we initiated).\n - For each thread with new inbound, classify the first inbound message (positive/objection/neutral/OOO/negative) using the rubric in the gtm-get-better skill.\n - Apply OOO heuristics first (cheap pre-filter), then LLM classification for the rest.\n - Append to outreach-log/email-replies.jsonl with is_first_reply=true.\n - For any classified thread, cancel pending follow-up entries in outreach-log/email-followups-pending.jsonl.\n\n2. Then run /gtm-cold-email --followups:\n - For each pending entry where send_after <= now AND prospect state is NOT_REPLIED:\n - Verify today's send count is under COLD_EMAIL_DAILY_CAP (default 25).\n - Send via Gmail API (the draft already exists).\n - Log to outreach-log/email.jsonl.\n - Remove from pending.\n\n3. End with a one-screen summary:\n - Follow-ups sent today: N\n - New replies classified: positive=X, objection=Y, neutral=Z, OOO=W, negative=V\n - Positive replies needing my personal response:\n - {prospect_name} ({company}): \"{reply_excerpt}\"\n - (one line per positive)\n - Daily cap remaining: M of CAP\n\nIf there are positive replies, offer to draft personalized responses in my voice (from sales-pack.md) for me to review and send." + } + ] + }, + "notifications": { + "onComplete": "brief", + "onNeedsInput": "always", + "summary": "Always notify on positive replies. Brief notification otherwise." + }, + "version": "0.2.0" +} diff --git a/founder-gtm/automations/positive-reply-ping.md b/founder-gtm/automations/positive-reply-ping.md new file mode 100644 index 0000000..8d9a295 --- /dev/null +++ b/founder-gtm/automations/positive-reply-ping.md @@ -0,0 +1,34 @@ +# Automation: positive-reply-ping (optional) + +> When a prospect replies positively, ping the founder. Suppress everything else. + +## Spec + +| Field | Value | +|---|---| +| **Name** | positive-reply-ping | +| **Description** | Polls Gmail for replies to outbound threads, classifies them, and notifies only when a reply was classified positive. Cuts through the OOO + neutral noise that makes founders ignore reply notifications. | +| **Trigger** | Cron: `0 9 * * 1-5` (weekdays 9am local) | +| **Tools** | `slack` (notification destination) | +| **Prompt** | Run `/gtm-cold-email --check-replies` for the last 24 hours. If any reply was classified as positive, post one message per positive reply to {{slack channel or DM}} with: prospect name and company, the first 2 sentences of their reply, a direct link to the Gmail thread, and the campaign the prospect came from. Suppress neutral, objection, and OOO replies. If none, stay silent. | + +## To finish in the Automations editor + +- **Schedule timezone.** 9am local catches the previous evening's replies and the morning's. +- **Slack channel** (required). Your DM with yourself unless your team is on this with you. +- **Cloud compute.** Cheap when there's no signal; otherwise scales with reply volume. + +## Why "positive only" + +The internal Cursor growth team finds that: + +- ~50% of replies are OOO (no action needed) +- ~10–20% are objection (worth reviewing in batch, not one at a time) +- ~5–10% are negative (no action needed) +- ~20–30% are positive (action needed today) + +If every reply pinged you, you'd mute the channel within a week. Pinging only on positives keeps the signal-to-noise high enough that you actually act. + +## Pairing + +Don't run this without also running `weekly-get-better`. The weekly run still surfaces objection trends and OOO rates so you don't lose them; this just keeps the daily attention high-signal. diff --git a/founder-gtm/automations/post-campaign-debrief.md b/founder-gtm/automations/post-campaign-debrief.md new file mode 100644 index 0000000..39fde5d --- /dev/null +++ b/founder-gtm/automations/post-campaign-debrief.md @@ -0,0 +1,39 @@ +# Automation: post-campaign-debrief + +> The day after a campaign goes out, ingest replies and tell the founder what's already working. + +## Spec + +| Field | Value | +|---|---| +| **Name** | post-campaign-debrief | +| **Description** | Triggered the morning after a campaign send. Pulls Gmail replies via the cold-email skill's reply-check path, classifies them, and posts a quick read on the first 24 hours of response. Useful for spotting whether the subject line is dead before more of the sequence fires. | +| **Trigger** | Cron: `0 8 * * *` (every day 8am local) OR Slack message trigger: a keyword like "debrief" in a chosen channel. | +| **Tools** | `slack` (for the summary post). Optional `readSlack` if you want the agent to first check whether a debrief was already run today. | +| **Prompt** | Check `outreach-log/email.jsonl` in the project root for any campaigns whose Touch 1 was sent in the last 24 hours. If none, stop silently. If one or more, run `/gtm-cold-email --check-replies` to ingest new replies, then `/gtm-get-better` with a 24-hour lookback scoped to those campaigns. Post the result to {{slack channel or DM}} as a short read: campaign name, Touch 1 sends, replies so far, classification breakdown, the one thing to watch for. | + +## To finish in the Automations editor + +- **Trigger choice.** Pick one: + - **Cron 8am daily** — passive, won't fire if there's no recent campaign. + - **Slack message trigger** — you type "debrief" in a channel and it runs. Slower to set up; better for ad-hoc debriefs. +- **Slack channel** (required, for the summary post). Use a DM with yourself unless you want the team to see it. +- **Cloud compute.** Slightly more expensive than the other two automations because of the reply-classification LLM calls; budget ~2 minutes of compute per run. + +## What "what to watch for" usually says + +Based on the patterns the internal Cursor growth team sees: + +- "Subject is dead — no opens after 18 hours" → swap subjects before Touch 2 fires. +- "All replies are objections, no positives" → the wedge in your sales-pack needs sharpening for this signal. +- "All replies are positive but no meetings booked" → your CTA is too soft; tighten it for the next campaign. +- "Above target on positives at low N" → run another batch of the same play with 3x volume. + +## When to disable + +- You haven't run a campaign in two weeks (no signal, no debrief value). +- Your Slack notifications are out of control and you'd rather see this in the weekly `/gtm-get-better` summary instead. + +## Pairing + +This automation pairs naturally with `weekly-get-better`. Daily debriefs catch surprises early; the weekly run aggregates them into durable updates to `sales-pack.md`. diff --git a/founder-gtm/automations/post-campaign-debrief.workflow.json b/founder-gtm/automations/post-campaign-debrief.workflow.json new file mode 100644 index 0000000..dadbc17 --- /dev/null +++ b/founder-gtm/automations/post-campaign-debrief.workflow.json @@ -0,0 +1,37 @@ +{ + "name": "founder-gtm — post-campaign debrief", + "description": "Manual-trigger automation. Run 7 days after a specific campaign goes out to get a focused learning report for just that campaign, separate from the weekly cross-campaign /gtm-get-better sweep. Useful when you want to know if a single play worked before deciding whether to run it again or retire it.", + "trigger": { + "type": "manual", + "inputs": [ + { + "name": "campaign_name", + "description": "The campaign slug to debrief (e.g. seed-funded-vp-eng-2026-01). Must match the campaign field in outreach-log/*.jsonl rows.", + "required": true + }, + { + "name": "lookback_days", + "description": "How many days back to read from the log. Defaults to 14.", + "required": false, + "default": 14 + } + ] + }, + "workflow": { + "steps": [ + { + "type": "agent", + "workspace": { + "root": ".", + "fallbackToProjectRoot": true + }, + "prompt": "Run a campaign-scoped debrief of the play named {{campaign_name}}.\n\n1. Filter outreach-log/*.jsonl rows where campaign == {{campaign_name}} AND timestamp within last {{lookback_days}} days.\n2. Run /gtm-cold-email --check-replies if any email touches are in scope and gmail-token exists.\n3. Ask me to log any X/LinkedIn replies for this campaign's prospects that aren't already in manual-replies.jsonl.\n4. Compute the gtm-get-better metrics for this campaign only:\n - enrolled, reply_rate, positive_rate, OOO-filtered_rate\n - by-touch attribution (which touch produced each positive)\n - opener / subject patterns that landed vs didn't (sample N may be small; flag if N<10)\n5. Compare against the success criteria in plays/{{campaign_name slugged to play file}}.md.\n6. Recommend ONE of: double down (run another cycle as-is) / iterate copy (re-run /gtm-design-play in iteration mode) / iterate signal or persona / retire.\n7. Update plays/{{campaign_name}}.md status if I confirm a decision.\n8. Append a campaign-scoped note to sales-pack.md § Update log (do NOT replace the weekly gtm-get-better entries — both should coexist).\n\nIf N<10 enrolled, refuse to make a strong recommendation. Tell me to run another cycle before judging the play." + } + ] + }, + "notifications": { + "onComplete": "full", + "onNeedsInput": "always" + }, + "version": "0.2.0" +} diff --git a/founder-gtm/automations/trial-expiry-sweep.md b/founder-gtm/automations/trial-expiry-sweep.md new file mode 100644 index 0000000..e33c33f --- /dev/null +++ b/founder-gtm/automations/trial-expiry-sweep.md @@ -0,0 +1,35 @@ +# Automation: trial-expiry-sweep (optional, PLG founders on Stripe) + +> Twice a week, find trials ending in 7 days with enough seats to be worth a personal touch. + +## Spec + +| Field | Value | +|---|---| +| **Name** | trial-expiry-sweep | +| **Description** | Queries Stripe for subscriptions in trial status with `trial_end` within the next 7 days, filters by min seat count or MRR threshold, and outputs a CSV under `prospects/` for the founder to run an outreach campaign against. Pattern lifted from how the internal Cursor growth team surfaces expiring high-value trials. | +| **Trigger** | Cron: `0 7 * * 2,6` (Tuesdays + Saturdays 7am local) | +| **Tools** | `mcp` — pointed at your Stripe MCP server (or `gh` if you're storing trial data elsewhere). | +| **Prompt** | Using the Stripe MCP, list subscriptions with `status='trialing'` and `trial_end` between now and 7 days from now. For each, pull the customer's company, seat count, and primary contact email. Filter to trials with at least 5 seats (or whatever threshold matches the founder's PLG motion, read from `sales-pack.md` § "Buying signals"). Write the result to `prospects/trial-expiry-YYYY-MM-DD.csv` using the standard gtm-find-prospects schema. Then post a one-line summary to {{slack channel or DM}} with the count and a suggestion to run `/gtm-design-play` if there are more than 5. | + +## To finish in the Automations editor + +- **MCP server name.** Confirm the exact Stripe MCP server name as the Cloud Agent will see it. +- **Threshold tuning.** Edit the prompt to reflect your actual minimum seat count or MRR. +- **Slack channel** (required for the summary). +- **Cloud compute.** Twice-weekly runs are cheap unless you have hundreds of trials. + +## Prerequisites + +- An active Stripe MCP server (e.g. the official Stripe MCP or a custom one). +- A PLG product with self-serve trial signups. +- A sales-pack with a defined "trial expiry" signal in § Buying signals. + +## When to disable + +- You've stopped offering trials (most founders eventually move to a freemium model). +- You don't have time to act on the surfaced list. An unactioned signal is worse than no signal. + +## When to upgrade + +Once you have 3 months of expiry → conversion data, add a second filter on usage in the last 7 days. Trials with high recent usage convert at much higher rates than trials with declining usage. Use both signals together to focus your outreach on the trials most likely to convert. diff --git a/founder-gtm/automations/weekly-get-better.md b/founder-gtm/automations/weekly-get-better.md new file mode 100644 index 0000000..d6cc279 --- /dev/null +++ b/founder-gtm/automations/weekly-get-better.md @@ -0,0 +1,31 @@ +# Automation: weekly-get-better + +> Run `/gtm-get-better` every Monday morning so your sales pack and per-channel playbooks compound week over week. + +## Spec + +| Field | Value | +|---|---| +| **Name** | weekly-get-better | +| **Description** | Run the founder-gtm `/gtm-get-better` skill weekly. Reads outreach logs across X DMs, LinkedIn, and cold email. Classifies first replies. Updates `sales-pack.md` and `outreach-log/learned-*.md` with what landed and what didn't. | +| **Trigger** | Cron: `0 9 * * 1` (Mondays 9am, your local time) | +| **Tools** | None required. The skill reads local files (`outreach-log/*.jsonl`, `sales-pack.md`, `plays/*.md`) and writes back to the same. | +| **Optional tools** | `slack` — if you want a weekly summary posted to a channel or DM after the run. | +| **Prompt** | Run the `/gtm-get-better` skill with a 7-day lookback window. After it completes, post a 3-bullet summary of the top actions to {{slack channel or DM}} so I see it in the morning. If there's nothing new since last week's run, say so and stop. | + +## To finish in the Automations editor + +- **Schedule timezone.** Confirm the 9am time matches your local timezone in the editor preview. +- **Slack channel** (optional). If you enabled the `slack` tool, pick the channel or DM destination. Use your own DM with yourself if you don't want noise in a public channel. +- **Cloud compute.** Confirm your Cloud Agent plan; weekly runs are cheap (one short context window). + +## When to disable + +- You've stopped running outbound for now. +- You're in a deep build sprint and don't want the noise. +- The plugin has produced 3+ consecutive empty `/gtm-get-better` runs (means you're not generating new data fast enough — go run a campaign first). + +## When to upgrade + +- After 4 weeks of data, change the lookback to "since last run" so cycles compound on the prior week's findings. +- After 8 weeks, add `slackTrigger` + `mcp` so you can ask follow-up questions about the weekly summary in Slack and have the agent dig into specifics. diff --git a/founder-gtm/automations/weekly-get-better.workflow.json b/founder-gtm/automations/weekly-get-better.workflow.json new file mode 100644 index 0000000..143dcf5 --- /dev/null +++ b/founder-gtm/automations/weekly-get-better.workflow.json @@ -0,0 +1,29 @@ +{ + "name": "founder-gtm — weekly gtm-get-better", + "description": "Compounds learnings from the past week's outreach. Reads logs across X, LinkedIn, and email; classifies replies; updates sales-pack.md and per-channel playbooks; surfaces the top 3 actions for the next week. Runs Mondays at 7am Pacific by default.", + "trigger": { + "type": "schedule", + "cron": "0 14 * * 1", + "timezone": "America/Los_Angeles", + "notes": "Cron is UTC by default; this fires Monday 7am Pacific year-round. Change the cron/timezone if you prefer a different time." + }, + "workflow": { + "steps": [ + { + "type": "agent", + "workspace": { + "root": ".", + "fallbackToProjectRoot": true, + "notes": "Run from the project where the founder keeps sales-pack.md and outreach-log/. If those files are missing, prompt the founder for the right project root." + }, + "prompt": "Run the founder-gtm `/gtm-get-better` skill end-to-end.\n\nSpecifically:\n\n1. Read outreach-log/*.jsonl and .gtm-state/last-gtm-get-better.json.\n2. Default lookback: since the last /gtm-get-better run.\n3. If gtm-cold-email is set up (gmail-token.json exists), run /gtm-cold-email --check-replies first to ingest fresh email replies before learning.\n4. If there are X DM or LinkedIn prospects with no logged reply and no manual-replies entry since last run, walk me through them one at a time to log the reply (positive/objection/neutral/OOO/negative/no_reply).\n5. Compute per-play and per-channel metrics: enrolled, reply_rate, positive_rate, OOO-filtered_rate, by-touch attribution.\n6. Identify winners (positive_rate >= 3% AND enrolled >= 15) and retirement candidates (positive_rate = 0% AND enrolled >= 15 AND 2+ cycles).\n7. Append a timestamped update block to sales-pack.md (the format defined in the gtm-get-better skill).\n8. Update outreach-log/learned-*.md for each channel with new patterns.\n9. End with exactly 3 prioritized next actions for the coming week.\n10. Update .gtm-state/last-gtm-get-better.json.\n\nIf there have been no new sends since the last /gtm-get-better run, stop and tell me to wait until the next campaign cycle." + } + ] + }, + "notifications": { + "onComplete": "brief", + "onNeedsInput": "always", + "notes": "Brief on-complete summary in Slack/email. Always ping me when the skill needs me to log X/LinkedIn replies manually." + }, + "version": "0.2.0" +} diff --git a/founder-gtm/canvases/founder-gtm-playbook.canvas.tsx b/founder-gtm/canvases/founder-gtm-playbook.canvas.tsx new file mode 100644 index 0000000..2095b3c --- /dev/null +++ b/founder-gtm/canvases/founder-gtm-playbook.canvas.tsx @@ -0,0 +1,900 @@ +import { + Callout, + Card, + CardBody, + CardHeader, + Code, + Divider, + Grid, + H1, + H2, + H3, + Link, + Pill, + Row, + Stack, + Table, + Text, + useHostTheme, +} from "cursor/canvas"; + +export default function FounderGtmPlaybook() { + const theme = useHostTheme(); + + return ( + + + + + + + + + + + + + founder-gtm v0.4.0 · plugin + canvas + resources.md + + + ); +} + +function Hero() { + const theme = useHostTheme(); + return ( + + + v0.4.0 + Early-stage founders + +

Founder GTM best practices with Cursor

+ + Most early founders fall into one of two traps. They blast generic AI templates that + go straight to spam, or they do nothing because the standard outbound stack costs + $1,500 to $3,000 a month. This is a third path: an opinionated set of skills, + scripts, and automations that run from inside Cursor, send 25 real messages a day, + and get sharper every week. + + + + + + + + +
+ ); +} + +function StatCard({ value, label }: { value: string; label: string }) { + const theme = useHostTheme(); + return ( +
+ + + {value} + + {label} + +
+ ); +} + +function Premise() { + return ( + + + + + Reply classifier with a 5-category rubric: + positive, objection, neutral, OOO, negative. Applied only to the first inbound + message per thread, so follow-up chatter doesn't pollute the intent signal. + + + + + Per-person-enrolled denominators, not per-message. + A 4-touch sequence inflates message counts and makes plays incomparable. Every + metric divides by unique people who got at least one touch. + + + + + Signal-first retirement rule. At N ≥ 15 sends + with 0 positives across 2 cycles, the play is retired. Most weak plays don't + recover from copy tweaks; the signal or persona is wrong. + + + + + 4-touch cadence with an offer ladder matched to + signal strength. High-signal plays ask for the meeting on touch 1; low-signal + plays lead with content and never ask for a call. + + + + + Do-not-re-offer rule. If the prospect already + consumed the asset (downloaded the guide, attended the webinar), the next touch + is what comes after the asset, not the asset again. + + + + + Always-on voice rule that drafts from your + collected samples and refuses to generate filler. If a draft can't anchor to a + specific signal in the last 30 days, the skill refuses to send. + + + + + Title classifier and signal scorer shipped as + editable Python (a 200-line keyword + exclusion list), distilled from the kind + of golden-list scripts internal growth teams build for outbound enrichment. + + + + These mechanisms are non-proprietary patterns from how Cursor's growth team + operates outbound, distilled into something a single founder can run. + + + + ); +} + +function Bullet({ children }: { children: React.ReactNode }) { + const theme = useHostTheme(); + return ( + +
+
{children}
+ + ); +} + +function FrameworkTimeline() { + const theme = useHostTheme(); + const steps = [ + { + num: "01", + name: "Identify", + summary: "Find people with a real reason to listen this week.", + how: "Recency is the cheapest personalization. Someone who just raised, just shipped, just hired, or just complained about your problem is open to a message in a way they won't be next month.", + }, + { + num: "02", + name: "Resonate", + summary: "Open with one real thing about them, in your voice.", + how: "The first sentence has to do work. Reference the post, the round, the hire. Skip the part where you introduce yourself; they figure that out from your signature.", + }, + { + num: "03", + name: "Time", + summary: "Send while the moment is still warm.", + how: "A message about something from yesterday lands. The same message about something from last month gets archived. gtm-find-prospects ranks by signal recency for a reason.", + }, + { + num: "04", + name: "Follow up", + summary: "Most positives come from touches 2 to 4, not touch 1.", + how: "Pre-write the four-touch sequence the moment you send the first. Use a different angle on every touch. Never send a fifth. The breakup acknowledges they're busy and asks one binary question.", + }, + ]; + + return ( + + +

The framework

+ + Four steps. None are novel. The leverage comes from doing all four well at the + same time, which is what a one-person sales motion usually can't pull off. + +
+ + + {steps.map((step, i) => ( + + + +

{step.name}

+ + {step.summary} + + {step.how} +
+
+ ))} +
+
+ ); +} + +function TimelineHeader({ + num, + isLast, + theme, +}: { + num: string; + isLast: boolean; + theme: ReturnType; +}) { + return ( + +
+ + {num} + +
+ {!isLast && ( + +
+ + + + + )} + + ); +} + +function ChannelsSection() { + return ( + + +

The three channels worth running

+ + Pick one to start. Add a second only after the first is consistently producing + replies. Running all three on day one is how founders end up with three half-broken + campaigns and no idea what's working. + +
+ + + + + + +
+ ); +} + +function ChannelCard({ + channel, + handle, + best, + stack, + limits, + pillTone, +}: { + channel: string; + handle: string; + best: string; + stack: string; + limits: string; + pillTone: "info" | "success" | "warning"; +}) { + const theme = useHostTheme(); + return ( + + {handle}}> + {channel} + + + + + + BEST FOR + + {best} + + + + STACK + + {stack} + + + + LIMITS + + {limits} + + + + + ); +} + +function InsideTheLoop() { + return ( + + +

Inside the learning loop

+ + The plugin is opinionated about how you measure outbound. The shared rubric and + the subject-line catalog below are what /gtm-get-better classifies against every week. + +
+ + + +

The reply rubric

+ + Every first inbound message gets exactly one of these labels. OOO replies stop + the sequence but are excluded from the positive-rate denominator. + + positive, + + Real interest, follow-up question, agreed to a meeting. + , + "Sure, would love to chat.", + ], + [ + objection, + + Engaged but pushed back. Usually worth answering. + , + "We're already on a competitor.", + ], + [ + neutral, + + Replied but no clear engagement or pushback. + , + "Got it, thanks.", + ], + [ + OOO, + + Auto-reply, vacation, or role-change responder. + , + "I'm out until Monday.", + ], + [ + negative, + + Hard no, unsubscribe, angry reply, mark-as-spam. + , + "Stop emailing me.", + ], + ]} + /> + + + +

Subjects: what works, what fails

+ + Auto-flagged by /gtm-get-better even before N ≥ 15. The fail patterns are retired + with thousands of sends behind the call. + + + Works + + + + + + + + + + + Fails + + + + + + + + + +
+ + + + At N ≥ 15 sends, 0 positives, second cycle, the default recommendation is retire. + Copy tweaks rarely save a weak signal. Re-pick the signal or the persona before + rewriting the message. + + + ); +} + +function SubjectExample({ + label, + example, + tone, +}: { + label: string; + example: string; + tone?: "fail"; +}) { + const theme = useHostTheme(); + return ( + + + {label.toUpperCase()} + + + {example} + + + ); +} + +function AlternativeStackSection() { + return ( + + +

What you can replace from the standard stack

+ + The plugin is a working alternative to the SaaS outbound stack until you're past + the point of needing it. Each layer here maps to a skill or script you can read + and modify in an afternoon. + +
+ +
Contact data and enrichment, + Apollo + Clay, + LinkedIn search, Hunter.io free tier, xmcp, and an email-pattern guesser, + gtm-find-prospects, + ], + [ + LinkedIn outbound, + Amplemarket or Outreach, + Lemlist or manual copy-paste, + gtm-linkedin-outreach, + ], + [ + Cold email infrastructure, + Salesloft or Outreach, + Google Workspace + Gmail API + Smartlead's free warmup tier, + gtm-cold-email, + ], + [ + X / Twitter DMs, + No enterprise equivalent, + Local xmcp + X API Basic tier or free tier for low volume, + gtm-x-outreach, + ], + [ + Buying signals (funding, hires, ships), + Crunchbase Pro + ZoomInfo, + TechCrunch RSS, Crunchbase free, Show HN, Product Hunt, LinkedIn job-change filter, + gtm-find-prospects, + ], + [ + Title filtering, + Custom enrichment + ML scoring, + A keyword list with an exclusion regex, in 200 lines of Python you can edit, + title-classifier.py, + ], + [ + Sequencing and reply detection, + Outreach.io or Salesloft, + JSONL logs and a state machine in gtm-cold-email that cancels pending touches on reply, + gtm-cold-email, + ], + [ + Performance analytics, + HockeyStack, Gong + a data team, + gtm-get-better reads your logs, classifies replies, retires losing plays at N ≥ 15, + gtm-get-better, + ], + [ + Voice and copy quality, + An SDR team and a copywriter, + The voice samples you fed gtm-sales-pack, applied via the always-on voice rule, + gtm-sales-pack, + ], + [ + Scheduling and triggers, + Workflow automation platforms, + Five Cursor Automations: weekly gtm-get-better, daily follow-ups, post-campaign debrief, positive-reply ping, trial-expiry sweep, + automations/*, + ], + ]} + /> + + ); +} + +function SkillsSection() { + return ( + + +

The ten skills

+ + You trigger each one with a slash command in chat. The skills are loosely coupled, + but the dependencies below are real. gtm-sales-pack has to exist before any channel + skill will draft for you. + +
+ +
/gtm-setup, + First install, + Walks you through setup in the right order. Picks channels, runs prereqs., + Nothing, + ], + [ + /gtm-playbook, + To open the guide, + Opens this visual playbook and gives a short orientation before setup., + Nothing, + ], + [ + /gtm-sales-pack, + Before any outreach, + + Interviews you (about 25 questions, one at a time) about company, ICP, value + props, objections, voice. Writes sales-pack.md. + , + Nothing, + ], + [ + /gtm-find-prospects, + To build a target list, + + Asks what tools you have. Combines free and paid sources. Runs a title + classifier with exclusions. Outputs a ranked CSV in person or account mode. + , + sales-pack.md, + ], + [ + /gtm-design-play, + To codify what worked, + + Designs a single outbound play: signal, persona, channel, 4-touch cadence, + offer ladder. Writes plays/{"{name}"}.md. + , + sales-pack.md, + ], + [ + /gtm-x-outreach, + To run X DMs, + + Pulls each target's last 10 to 20 posts via xmcp, finds a hook, drafts the DM + in your voice, sends or saves to drafts. + , + gtm-sales-pack + xmcp + prospect list, + ], + [ + /gtm-linkedin-outreach, + To run LinkedIn outreach, + + Drafts connection notes under 250 characters. Sends via Lemlist, Amplemarket, + LGM, or manual. + , + gtm-sales-pack + LinkedIn tool + prospect list, + ], + [ + /gtm-cold-email, + To run cold email, + + Drafts 4-step sequences. Sends via Gmail API with a daily cap, or saves as + Gmail Drafts. Reply state machine cancels follow-ups when someone replies. + , + gtm-sales-pack + Gmail OAuth + prospect list, + ], + [ + /gtm-warm-intro, + For 5 to 10x conversion vs cold, + + Reads your LinkedIn connections CSV, matches bridges per prospect, drafts the + intro request plus a forwardable blurb for the bridge to paste. + , + gtm-sales-pack + connections.csv + prospect list, + ], + [ + /gtm-get-better, + Weekly, + + Reads logs. Classifies first replies on the rubric. Slices metrics per play + and per touch. Retires losing plays at N ≥ 15. Proposes edits to the skill + files themselves when a pattern wins consistently. + , + At least one campaign run, + ], + ]} + /> + + ); +} + +function RecommendedSequence() { + const theme = useHostTheme(); + return ( + + +

The recommended sequence

+ + If you do nothing else, do this in this order. Don't skip the sales pack. It is + the single biggest determinant of whether your messages sound like you or like a + bot. + +
+ + + + + + + + + + + + Founders who blast 200 messages on day 1 always regret it. The first batch is for + feeling the quality bar. After you've seen 5 replies (positive, negative, or none), + scale to 25 per day. Two weeks later, scale to the channel's safe ceiling. From + there the automations carry the recurring rhythm and gtm-get-better runs your + weekly learning loop. + + +
+ + The reason this is a set of markdown files instead of a hosted SaaS: when + something isn't working, you open the skill and change it. You tune the title + classifier. You add a signal source. You rewrite the voice rule. The system that + messages your prospects is one you can read and modify in an afternoon. + +
+
+ ); +} + +function SequenceRow({ + when, + run, + why, +}: { + when: string; + run: string; + why: string; +}) { + const theme = useHostTheme(); + return ( + +
+ + {when.toUpperCase()} + +
+
+ {run} +
+ {why} +
+ ); +} + +function ResourcesFooter() { + return ( + +

What to read next

+ + + What's in the package + + + + Installs from the team marketplace, with files under the Founder GTM plugin root. + + + skills/: ten gtm-prefixed skills you invoke as slash + commands. Start with /gtm-setup. + + + skills/gtm-design-play/plays/: three starter plays you fork + instead of designing from scratch (seed fundraisers, Show HN launches, + eng-leader job changes). + + + rules/gtm-voice-guide.mdc: always-on voice rule. Strips em + dashes, AI clichés, and signposting from every draft. + + + automations/: five Cursor Automation specs you create once + (Day 2 morning of the sequence above). + + + skills/gtm-find-prospects/scripts/: five small scrapers + (TechCrunch funding RSS, Show HN, xmcp topic search, domain histogram, title + classifier) plus tunable data files in data/. + + + hooks/: sessionStart hook that greets new founders in any + project without a sales-pack yet, plus an afterFileEdit hook that scans + drafts under outreach-log/ for AI tells before you send. + + + README.md, CHANGELOG.md, resources.md: + install path, version history, and the curated reading list. + + + + + + External reading + + + + Founder-Led Sales by Pete + Kazanjy. First three chapters are free. The canonical text for this stage. + + + + Lavender's Cold Email Playbook + + . The most data-driven writing guide anywhere. + + + MKT1 newsletter by Emily Kramer. + Rigor that most GTM writing lacks. + + + Smartlead Academy. Free + course on cold-email deliverability infrastructure. Read it before sending + from a new domain. + + + Lenny's Newsletter{" "} + under the outbound and sales tags. + + + blader/humanizer + {" "}is the source of the anti-AI-writing patterns baked into the voice rule. + + + + + + + + Open chat. Type /gtm-setup. The orchestrator walks you through the + first 30 minutes. If you haven't built a sales pack yet, that's where it sends you + first. + +
+ ); +} diff --git a/founder-gtm/hooks/check-voice-on-edit.sh b/founder-gtm/hooks/check-voice-on-edit.sh new file mode 100755 index 0000000..e89e118 --- /dev/null +++ b/founder-gtm/hooks/check-voice-on-edit.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# founder-gtm afterFileEdit hook: scan outbound-content edits for AI tells. +# +# Fires after Write or Edit. Reads the edit JSON from stdin. +# +# Only acts when the edited file path is inside one of the actual outbound +# content directories (outreach-log/, prospects/, drafts/) under the project +# root. Anything else returns {} silently. +# +# When in scope, scans the new file content for: +# - em dash (U+2014) and en dash (U+2013) +# - "I hope this finds you well" and common variants +# - "Excited to announce", "Thrilled to share", "In today's" +# - High-risk subject patterns from gtm-cold-email: "unlock", "10x", +# "accelerating", or a version number like "2.0:" +# +# Fail-open. If anything goes sideways, return {} so the agent is not blocked. +# The goal is surfacing AI tells, not gating saves. + +set -u + +input=$(cat 2>/dev/null || true) + +# Extract the edited file path. Cursor's afterFileEdit payload has shifted +# across versions, so try the common keys defensively. +file_path=$(printf '%s' "$input" | jq -r ' + (.tool_input.file_path // .tool_input.path + // .file_path // .path + // .arguments.file_path // .arguments.path + // empty) +' 2>/dev/null || true) + +if [ -z "${file_path:-}" ] || [ ! -f "$file_path" ]; then + printf '%s\n' '{}' + exit 0 +fi + +# Only fire for files inside the outbound content dirs. Match anywhere in the +# path because Cursor may pass absolute or project-relative paths. +case "$file_path" in + */outreach-log/*|*/prospects/*|*/drafts/*) ;; + *) printf '%s\n' '{}'; exit 0;; +esac + +# Read the saved file content. Hooks fire after the write completes, so the +# file on disk reflects the new content. +content=$(cat "$file_path" 2>/dev/null || true) +if [ -z "$content" ]; then + printf '%s\n' '{}' + exit 0 +fi + +tells=() + +if printf '%s' "$content" | LC_ALL=C grep -q $'\xe2\x80\x94'; then + tells+=("em dash (U+2014)") +fi +if printf '%s' "$content" | LC_ALL=C grep -q $'\xe2\x80\x93'; then + tells+=("en dash (U+2013)") +fi +if printf '%s' "$content" | grep -Eiq "I hope (this|you|this email|this message) (email )?finds (you|this) (well|safe)"; then + tells+=("\"I hope this finds you well\" or variant") +fi +if printf '%s' "$content" | grep -Eiq "Excited to announce"; then + tells+=("\"Excited to announce\"") +fi +if printf '%s' "$content" | grep -Eiq "Thrilled to share"; then + tells+=("\"Thrilled to share\"") +fi +if printf '%s' "$content" | grep -Eiq "In today'?s"; then + tells+=("\"In today's\"") +fi +if printf '%s' "$content" | grep -Eiq "\bunlock\b"; then + tells+=("high-risk subject word \"unlock\"") +fi +if printf '%s' "$content" | grep -Eq "\b10x\b"; then + tells+=("high-risk subject word \"10x\"") +fi +if printf '%s' "$content" | grep -Eiq "\baccelerating\b"; then + tells+=("high-risk subject word \"accelerating\"") +fi + +if [ "${#tells[@]}" -eq 0 ]; then + printf '%s\n' '{}' + exit 0 +fi + +joined="" +for t in "${tells[@]}"; do + if [ -z "$joined" ]; then joined="$t"; else joined="$joined, $t"; fi +done + +msg="[voice-guide] WARNING: ${file_path} contains AI tells (${joined}). Rewrite before sending. Apply gtm-voice-guide.mdc." + +jq -nc --arg m "$msg" '{additional_context: $m}' 2>/dev/null || printf '%s\n' '{}' +exit 0 diff --git a/founder-gtm/hooks/hooks.json b/founder-gtm/hooks/hooks.json new file mode 100644 index 0000000..5f2bf44 --- /dev/null +++ b/founder-gtm/hooks/hooks.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/welcome-on-first-session.sh", + "timeout": 5 + } + ], + "afterFileEdit": [ + { + "command": "bash ${CURSOR_PLUGIN_ROOT}/hooks/check-voice-on-edit.sh", + "matcher": "Write|Edit", + "timeout": 5 + } + ] + } +} diff --git a/founder-gtm/hooks/welcome-on-first-session.sh b/founder-gtm/hooks/welcome-on-first-session.sh new file mode 100755 index 0000000..2ff5b24 --- /dev/null +++ b/founder-gtm/hooks/welcome-on-first-session.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# founder-gtm sessionStart hook +# +# Fires on every new agent session. In each project, surfaces a one-time +# welcome message suggesting /gtm-setup if the founder hasn't already +# built a sales pack here. +# +# Gates (silent unless ALL pass): +# 1. We can identify cwd +# 2. No sales-pack.md exists in cwd (founder isn't already set up) +# 3. No .gtm-state/welcomed marker (we haven't already nagged in this project) +# +# Always sets the marker before emitting output, so even if the founder +# ignores the welcome they only see it once per project. + +set -eu + +input=$(cat 2>/dev/null || true) +cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true) + +# Fall back to PWD if cwd isn't in the hook input +if [ -z "${cwd:-}" ]; then + cwd="${PWD:-}" +fi + +# Bail if we still can't identify cwd +if [ -z "${cwd:-}" ] || [ ! -d "$cwd" ]; then + printf '%s\n' '{}' + exit 0 +fi + +state_dir="$cwd/.gtm-state" +marker="$state_dir/welcomed" + +# Already welcomed here, or founder already has a sales pack, silent +if [ -f "$marker" ] || [ -f "$cwd/sales-pack.md" ]; then + mkdir -p "$state_dir" 2>/dev/null || true + touch "$marker" 2>/dev/null || true + printf '%s\n' '{}' + exit 0 +fi + +# Set marker BEFORE emitting so a missed greeting doesn't keep re-firing +mkdir -p "$state_dir" 2>/dev/null || true +touch "$marker" 2>/dev/null || true + +cat <<'JSON' +{ + "additional_context": "[founder-gtm plugin] Welcome. This project has no sales-pack.md yet. If the user wants to set up GTM outbound (sales pack interview, prospect lists, drafting X DMs / LinkedIn notes / cold email in their voice, weekly learning loop), invoke the /gtm-setup skill to begin the 30-minute onboarding. If the user wants something else, ignore this welcome and proceed with their actual request." +} +JSON diff --git a/founder-gtm/resources.md b/founder-gtm/resources.md new file mode 100644 index 0000000..55060ff --- /dev/null +++ b/founder-gtm/resources.md @@ -0,0 +1,106 @@ +# Resources — GTM reading for early-stage founders + +A curated, opinionated reading list for early-stage founders running their own outbound. Free or cheap. Skewed toward writers who actually ship outbound themselves, not consultants selling courses. + +This list is meant to live alongside the `founder-gtm` plugin. The plugin gives you the machinery; this gives you the principles. + +--- + +## Start here (the four essays to read first) + +1. **"The Sales Learning Curve" — Mark Leslie & Charles Holloway (HBR)** — the foundational frame for why founder-led sales matters before you hire AEs. https://hbr.org/2006/07/the-sales-learning-curve +2. **"How to Start a Startup" Lecture 14: How to Operate - Keith Rabois** - founder-led sales as the only way until product-market fit. https://www.startupschool.org/library +3. **"How to Talk to Users" - Aaron Epstein** - the underlying skill that makes every outbound message land. https://www.startupschool.org/library +4. **"Founder-Led Sales" — Pete Kazanjy** — the canonical book on this exact phase. The first 3 chapters are free at https://www.foundingsales.com and worth buying the rest. + +--- + +## Outbound copy and message craft + +- **Lavender's Cold Email Playbook (free)** — the most data-driven cold-email writing guide. They publish their benchmarks. https://www.lavender.ai/learn +- **Steli Efti — "The Ultimate Startup Guide to Outbound Sales"** — concrete templates and frameworks from Close.com's CEO. https://blog.close.com +- **Sahil Mansuri — "Writing cold emails that get replies"** — Bravado's CEO; tactical, with examples. Search his X/LinkedIn archive. +- **"Cold Email Hall of Fame" — Saleshacker** — annotated real-world examples. https://www.saleshacker.com +- **Patio11's emails advice** — Patrick McKenzie wrote the canonical "boring" cold email primer. https://www.kalzumeus.com/standing-invitation/ + +--- + +## Targeting and signals + +- **MKT1 newsletter (Emily Kramer)** — the most rigorous GTM newsletter for early-stage. Especially the "PLG + outbound" series. https://www.mkt1.co +- **Lenny's Newsletter — outbound & sales tags** — interviews with founders about what actually worked at 0–10M ARR. https://www.lennysnewsletter.com +- **"Account-Based Marketing" — Sangram Vajre (free chapters)** — for when you graduate from name-based to account-based targeting. https://www.terminus.com +- **HockeyStack's outbound benchmarks** — quarterly published reply-rate benchmarks by industry and channel. https://hockeystack.com/blog + +--- + +## Channel-specific tactical + +### Cold email +- **"The 4-Email Framework that 10x'd My Reply Rate" — Jason Bay (Outbound Squad)** — short and concrete. +- **Smartlead Academy (free)** — best free course on cold email infrastructure (warming, DKIM, DMARC, dedicated subdomains). https://www.smartlead.ai/academy +- **mail-tester.com** — paste a draft, get a deliverability score. Use before every campaign. +- **MXToolbox SuperTool** — verify SPF/DKIM/DMARC on your sending domain. https://mxtoolbox.com/SuperTool.aspx + +### LinkedIn +- **Justin Welsh's LinkedIn OS** — the standard playbook for founder LinkedIn presence (which is the foundation for cold connection requests landing). The free posts on his profile cover 80% of it. +- **Daniel Disney — "LinkedIn Sales Star"** — connection-request anatomy with real teardowns. https://danieldisney.online +- **"The Anti-Sales Method" — Jordan Crawford** — challenges the standard LinkedIn outbound playbook. + +### X / Twitter +- **"How to do cold DMs on Twitter" — Jack Smith (founder, Vungle)** — short post but the principles still hold. +- **Read the founders themselves** — @dharmesh, @sahil, @paulg, @alexmaccaw — study how they write and how strangers DM them. Most replies come from short, specific, opinionated messages. +- **xmcp on GitHub** — open-source local MCP server for X API access. https://github.com/xdevplatform/xmcp + +### Multi-channel sequencing +- **"The Cadence" — Salesloft (free)** — the original cadence-design playbook. Still relevant. +- **"How to Build a Sales Cadence That Doesn't Suck" — Outreach blog** — diminishing returns on touches >4. + +--- + +## Voice and writing + +- **"On Writing Well" — William Zinsser** — the only writing book most founders need. +- **Paul Graham essays** — for tightness of language. https://www.paulgraham.com/articles.html +- **"The Elements of Style" — Strunk & White** — short, ruthless. +- **Bench Press: "The 1-Page Persona Builder"** — quick way to articulate ICP voice before drafting. https://benchpresshq.com + +--- + +## Tools mentioned in the plugin (with honest cost notes) + +| Tool | Why it's here | Cost (as of 2026) | Worth it for early-stage? | +|---|---|---|---| +| **Lemlist** | LinkedIn + email outbound primary | $59/mo solo plan | Yes if doing >50 messages/week | +| **Smartlead** | Free domain warmup; cold email at scale | Free tier + $39/mo paid | Yes — free warmup is the cheapest path | +| **Instantly** | Domain warmup + inbox rotation | $37/mo | Worth it for serious cold email scale | +| **Hunter.io** | Email finding | 25 free lookups/mo, $34/mo for 500 | Yes — free tier covers early stage | +| **NeverBounce** | Email verification | $0.008/verification, free tier 1000 | Yes — bounce <5% protects reputation | +| **Apollo** | Contact + signal data | $49/mo basic, $99/mo pro | Skip until $5K MRR — the free gtm-find-prospects path covers it | +| **Clay** | Enrichment + waterfall | $149/mo entry | Skip until $50K ARR — overkill for <100 prospect lists | +| **Amplemarket** | All-in-one outbound | $400+/mo | Skip until you have a real sales budget | +| **LinkedIn Sales Navigator** | Better LinkedIn search + 50 InMail/mo | $99/mo | Worth it once you've validated LinkedIn as your channel | +| **Crunchbase Pro** | Funding round data | $49/mo | Skip — free tier + TechCrunch RSS covers signals | + +--- + +## Anti-resources (what to ignore) + +- **Anyone selling a "10K leads in 30 days" course.** Volume without quality kills your domain reputation and gets you blocked. +- **Most LinkedIn "growth hacking" influencers.** The ones with the loudest follower counts are usually selling to other "growth hackers", not running real outbound. +- **Gartner / Forrester reports on outbound.** They're for enterprise procurement, not for you. +- **"ChatGPT for cold email" generators (the bulk template kind).** Identical to the AI slop already flooding your prospects' inboxes. The whole point of the `founder-gtm` plugin is to be the opposite of these. + +--- + +## A short philosophy + +The standard advice on outbound is to "personalize at scale". That phrase is broken — you can't actually personalize at scale, you can only template at scale. + +What you can do is **research at scale, write deliberately at low scale, and iterate fast on what works**. That's the entire idea behind this plugin: Cursor is the engine that lets you do the research and the iteration; the writing stays human. + +The founders we've seen succeed with this approach send 20–50 messages per week, get 15–25% reply rates, and book 3–8 calls per week from cold. That's a lot more leverage than 500 generic emails a day with a 0.5% reply rate. + +--- + +> Have a resource that belongs here? Open a PR. diff --git a/founder-gtm/rules/gtm-voice-guide.mdc b/founder-gtm/rules/gtm-voice-guide.mdc new file mode 100644 index 0000000..3cbe281 --- /dev/null +++ b/founder-gtm/rules/gtm-voice-guide.mdc @@ -0,0 +1,135 @@ +--- +description: Voice and anti-slop guide for any outbound message drafted via the founder-gtm plugin (cold emails, X DMs, LinkedIn notes, follow-ups). Always applied while founder-gtm skills are active. +globs: +alwaysApply: true +--- + +# Founder-GTM Voice Guide + +You are drafting outbound for an early-stage founder. The single most important thing: **the recipient must believe a human wrote this, specifically for them**. Generic AI outbound is the noise we are trying to break through. + +## The three differentiators + +Every outbound message must do at least one of these, ideally all three, better than the median message in the recipient's inbox: + +1. **Unique voice**, sounds like a specific human, not an AI or a sales playbook. Reads how the founder actually talks. +2. **Engaging hook**, references something specific and recent: a post they wrote, a feature they shipped, a podcast they were on, a hire they made. Never generic ("I saw your company is growing"). +3. **Right timing**, the message arrives at a moment the prospect cares about (just shipped, just funded, just hired, just hit a pain point). + +If none of these is true, do not send the message. Rewrite. + +## Cardinal rules + +1. **Short and direct.** 2 to 4 sentences for cold email; ≤250 chars for LinkedIn connect notes; ≤500 chars for X DMs. +2. **Lead with substance.** First sentence must do work, reference the signal, ask a real question, or share something specific. Never "I hope this finds you well." +3. **No hyperbole.** No "revolutionary", "game-changing", "world-class", "10x", "unlock". Let the facts speak. +4. **Prove it with specifics.** Use real numbers, real customer names (only ones the founder has permission to cite), real outcomes. +5. **Confident, not loud.** Avoid "I believe", "I think", "I just wanted to". State things directly. +6. **No em-dashes or semicolons in outbound copy.** Use periods or restructure. +7. **No emojis in outbound.** Ever. +8. **Warm but professional.** Write like a thoughtful peer reaching out, not a salesperson closing a quota. +9. **Exactly one clear CTA per message.** Phrased as a short question. On its own line at the end. Never "Would you like A, B, or C?" +10. **Reference the signal.** The recipient should immediately understand why they specifically are getting this message. +11. **Step 1 is never a list.** No bullets, no proof tables, no customer logos. That goes in Step 2 at the earliest. Step 1 is one human sentence into one CTA. +12. **One CTA, with one exception.** The breakup (Touch 4) may use the "no vs not now?" three-question form. Every other touch is one CTA. + +## Mechanics + +These are micro-rules that catch the AI tells reviewers learn to spot: + +- Oxford comma on lists of three or more. +- Numerals for metrics ("45%", "12 engineers"), not spelled out. +- Subject lines in sentence case, not Title Case. +- Exclamation points are rare. Cold outreach gets zero of them. +- Straight quotes ("like this"), not curly quotes. +- No em dashes or en dashes anywhere. If you find yourself reaching for one, use a comma or start a new sentence. +- "Founders" not "founders, like yourself". + +## Principles for any technical buyer + +The recipient of your outbound is usually a smart, busy technical person. The principles that work for them work for every technical buyer: + +- **First sentence delivers value or asks a real question. No warmup.** Skip "I hope this finds you well", "I work with teams like yours", "I'm reaching out because". +- **Replace adjectives with one number or one named example.** "Faster" is filler; "cuts review time from 4 days to 4 hours" is data. +- **State things directly. Drop "I believe" unless you actually are uncertain.** Hedging reads as fake humility. +- **Confident, not loud.** No caps lock. No multiple exclamation points. No "MUST READ". +- **High signal-to-noise.** Every sentence has to earn its place. If you can cut a sentence and the email still makes sense, cut it. +- **Show, don't tell.** If you say your product is fast, you've already lost. If you cite a benchmark, you might keep them. + +## Opener patterns that work (study these) + +- "Saw your post on {{specific_topic}}, {{one_sentence_of_genuine_reaction}}." +- "Caught your conversation with {{podcast_host}} about {{topic}}." +- "Congrats on the {{specific_milestone}}." +- "Noticed {{company}} just {{shipped_thing / hired_role / raised_round}}." +- "Read your {{essay_post_thread}} on {{specific_topic}}, {{the_one_part_that_landed}}." + +## Opener patterns that kill the message + +- "I hope this email finds you well." +- "I work with {{persona}} like yourself." +- "I'm reaching out because…" +- "I came across {{company}} and was impressed by…" +- "Quick question — " (only works if it actually is one) +- Anything that opens with "I" instead of "you" or a signal. + +## Voice preservation + +The founder has a voice. Read `sales-pack.md` for the section on "How I write" before drafting. If samples of their own writing are available (their tweets, their blog, prior emails they've sent), match cadence, sentence length, capitalization habits, and vocabulary. Do not flatten their voice into "generic founder tone". + +If the founder uses lowercase, you use lowercase. If they swear, you swear. If they write 8-word sentences, you write 8-word sentences. The recipient should not be able to tell which messages were drafted by AI. + +## Personalization tiers (use the highest available) + +| Tier | What it looks like | When to use | +|---|---|---| +| **High** | References a specific thing the prospect said/did in the last 30 days | Default. Always try first. | +| **Medium** | References something specific about their company (recent hire, ship, raise, product) | When the prospect has no personal public footprint | +| **Low** | Persona-based; references the role they're in and a problem common to that role | Last resort. Flag to the founder that this is low-personalization and may underperform. | + +Never send a Tier Low message without telling the founder it's Tier Low. + +## Follow-up cadence + +Most replies come from follow-ups, not first touches. The default sequence: + +- **Touch 1 (Day 0):** the personalized opener. One CTA. +- **Touch 2 (Day 3 to 4):** different angle. Add value (a relevant link, a one-line insight, a small offer). New CTA, softer. +- **Touch 3 (Day 8 to 10):** a low-commitment alternative ("if a call is too much, would a 5-minute Loom be useful?"). +- **Touch 4 (Day 14 to 21):** clean breakup. Acknowledge they're busy. Offer one final thing or ask for a referral. No guilt. + +Never more than 4 touches. Never refer to prior touches in a guilt-trippy way ("I've reached out a few times…"). Save the acknowledgment of past attempts for the breakup only. + +## Anti-AI tells (the humanizer pass) + +Patterns from `blader/humanizer` on GitHub. Each is a tell that gives away an AI draft. Scan for these alongside the cardinal rules above. + +- **Superficial -ing analyses.** "Highlighting", "underscoring", "reflecting", "showcasing" tacked on for fake depth. Cut the wrapper and state the thing directly. +- **Copula avoidance.** "Serves as", "stands as", "boasts", "features" used in place of "is" or "has". Just use "is" and "has". +- **False ranges.** "From X to Y" where X and Y aren't on a meaningful scale. Write a plain list instead. +- **Persuasive authority tropes.** "The real question is", "at its core", "what really matters", "fundamentally". Drop the wrapper and say the point. +- **Signposting and announcements.** "Let's dive in", "let's explore", "here's what you need to know", "now let's look at". Just start. +- **Hyphenated word pairs in predicate position.** "We are cross-functional", "this is data-driven", "the platform is end-to-end". Keep them as attributive adjectives only ("a cross-functional team"). +- **Sycophantic openings.** "Great question!", "You're absolutely right!", "Of course!". Cut. Get to the answer. +- **Generic positive conclusions.** "The future looks bright", "exciting times lie ahead". Replace with a concrete next step or fact. +- **Filler phrases.** "In order to" becomes "to". "Due to the fact that" becomes "because". "At this point in time" becomes "now". "It is important to note that" is cut. +- **Excessive hedging.** "Could potentially possibly", "might have some effect". Pick one hedge or drop it. + +## Anti-patterns to scan for before sending + +- [ ] No em-dashes, no semicolons +- [ ] No emojis +- [ ] No "I hope this finds you well" or any variant +- [ ] No "I work with…" or "I help…" opening +- [ ] First sentence does substantive work (signal, question, or specific value) +- [ ] Exactly one CTA, phrased as a question, on its own line +- [ ] Specific numbers / names / signals, not vague claims +- [ ] Sounds like the founder, not like an AI +- [ ] Channel-appropriate length (X DM ≤500 chars, LinkedIn ≤250 chars, cold email ≤4 sentences) +- [ ] Step 1 has no bullets, no proof tables, no customer logos +- [ ] Exactly one CTA per touch (Touch 4 breakup may use three short questions if and only if they form a single decision) +- [ ] No em dashes anywhere (—) or en dashes (–) +- [ ] No emojis, no curly quotes +- [ ] Numerals for metrics, sentence case for subjects + +If any of these fail, rewrite before sending. diff --git a/founder-gtm/skills/gtm-cold-email/SKILL.md b/founder-gtm/skills/gtm-cold-email/SKILL.md new file mode 100644 index 0000000..fc988c0 --- /dev/null +++ b/founder-gtm/skills/gtm-cold-email/SKILL.md @@ -0,0 +1,404 @@ +--- +name: gtm-cold-email +description: Run personalized cold email outreach for an early-stage founder via Gmail (Google Workspace). Reads a prospects CSV from gtm-find-prospects, drafts a 3 to 4 step email sequence per prospect grounded in sales-pack.md, and either saves them as Gmail Drafts for manual review or sends them programmatically with a hard daily cap (default 25/day, configurable) and inter-send spacing. Includes domain-warming guidance, recommends Instantly/Smartlead/Mailwarm for cold domains, and never blasts an unwarmed domain. Use when the founder wants to run cold email outreach, has a list of prospect emails, runs /gtm-cold-email, or asks how to send emails at scale safely. +--- + +# Cold Email, sending without nuking your domain + +You are running a cold email campaign for an early-stage founder. Cold email is the highest-leverage outbound channel when done right and the fastest way to permanently damage your sending domain when done wrong. The skill prioritizes **deliverability and quality over volume**. + +## Prerequisites + +```bash +test -f sales-pack.md || echo "MISSING sales-pack.md" + +# Gmail API ready +gcloud auth list --filter=status:ACTIVE --format="value(account)" || echo "GCLOUD NOT AUTHED" +gcloud services list --enabled --filter="config.name:gmail.googleapis.com" --format="value(config.name)" || echo "GMAIL API NOT ENABLED" + +# Founder-gtm Gmail token exists +test -f ${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json || echo "MISSING gmail-token.json" +``` + +If any of these fail, run the [Gmail setup](#gmail-setup-one-time) section below before proceeding. + +## Gmail setup (one-time) + +The skill uses the official Google Workspace path: gcloud CLI + Gmail API + an OAuth client whose token is stored locally and gitignored. + +### Why Google Workspace, not free gmail.com + +- Workspace domains have far better deliverability for cold outbound. +- Workspace provides the admin controls needed to safely manage cold email (DKIM, SPF, DMARC). +- Cold email from a free `@gmail.com` address dies in spam folders within ~10 sends. + +If the founder doesn't have Workspace yet: tell them to get one at workspace.google.com ($6-$18/user/month) on a dedicated outbound subdomain (e.g. `outreach.{{theirdomain}}.com`) so any sender-reputation damage stays off their primary domain. + +### Setup steps + +1. **Install gcloud CLI** (if missing): + ```bash + brew install --cask google-cloud-sdk + gcloud init + ``` + +2. **Auth as the Workspace user** who will send the emails: + ```bash + gcloud auth login + gcloud auth application-default login + ``` + +3. **Pick or create a GCP project** for this plugin (recommend a dedicated one to scope the OAuth credential): + ```bash + gcloud projects create founder-gtm-{{founder-handle}} --name="founder-gtm" + gcloud config set project founder-gtm-{{founder-handle}} + ``` + +4. **Enable the Gmail API:** + ```bash + gcloud services enable gmail.googleapis.com + ``` + +5. **Create an OAuth client** (Desktop app type, Gmail API doesn't support service accounts for individual Gmail boxes): + - Console: https://console.cloud.google.com/apis/credentials + - Create Credentials → OAuth client ID → Desktop app + - Download the client JSON to `${CURSOR_PLUGIN_ROOT}/.gtm-state/oauth-client.json` + - Also: add the founder's email as a test user under OAuth consent screen (so they don't need verification for personal use). + +6. **Grant scopes** (`gmail.send`, `gmail.compose`, `gmail.modify`): + The skill provides a one-time helper script at `scripts/gmail-auth.py` (see [Scripts](#scripts) below). Running it opens a browser, the founder approves, and a refresh token is saved to `${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json`. + +7. **Verify DKIM/SPF/DMARC** on the sending domain. Workspace docs: https://support.google.com/a/answer/33786. Without these, deliverability is near zero. + +8. **DNS warmup check:** + - Domain registered <60 days ago → DO NOT cold-email yet. Warm for 4+ weeks first. + - Domain established but never used for cold outbound → warm for 2 weeks first. + - Domain has prior healthy sending volume → safe to start at 25/day. + +## Domain warming guidance + +Ask the founder: + +``` +Question: "Has this Workspace domain ever sent cold email before? Honest answer." +Options: +- Yes, regularly (safe to start at default cap) +- It's an established domain, but I haven't done cold outbound from it (warm 2 weeks first) +- It's a new dedicated outbound subdomain (warm 4 weeks first) +- It's the same domain as my product (think hard — sender reputation damage will affect ALL email from this domain) +``` + +If warming is needed: + +| Tool | Cost | Notes | +|---|---|---| +| **Smartlead** | Free warmup tier | Recommended free option | +| **Instantly** | ~$37/mo | Best UX, includes warmup + inbox rotation | +| **Mailwarm** | ~$69/mo | Pure warmup focus | +| **Lemlist** | ~$59/mo | Has built-in warmup if using Lemlist for LinkedIn too | + +For warming periods, cap real cold sends at 5/day max. Increase by 5/day each week until reaching the target 25/day. + +## Workflow (after setup is done) + +### Step 1: Load prospect list + +``` +Question: "Which prospect list?" +Options: +- Auto-detect (most recent prospects/*.csv) +- {{list CSVs}} +- Paste a list inline +``` + +Filter rows where `email` is non-empty AND (`recommended_channel` is `email` OR `multi`). + +Show the founder the filtered count + breakdown by `email_confidence`: + +``` +Found 42 prospects with emails: + • 18 confirmed + • 14 pattern-guess + • 10 unverified + +Recommend: send to "confirmed" only on first run. Save "pattern-guess" as drafts you review. Skip "unverified". +``` + +### Step 2: Pick send mode + +``` +Question: "How do you want to send?" +Options: +- Drafts only — save to Gmail Drafts folder; you click send manually +- Send with my approval — show each draft; I approve, the skill sends +- Auto-send with cap — sends up to {{daily-cap}} today, spaced 2–5 minutes apart +``` + +Daily cap default: 25. Configurable via `.env`: +``` +COLD_EMAIL_DAILY_CAP=25 +COLD_EMAIL_MIN_INTERVAL_SECONDS=120 +COLD_EMAIL_MAX_INTERVAL_SECONDS=300 +``` + +Track today's sent count in `.gtm-state/send-counter-{{YYYY-MM-DD}}.json`. Refuse to exceed the cap. + +### Step 3: For each prospect, draft the sequence + +A campaign generates a **4-step sequence** per prospect, sent as Drafts now and scheduled (or as a single Touch 1 if the founder prefers manual cadence control). + +Apply `gtm-voice-guide` + these cold-email specifics: + +**Subject line rules:** +- 3 to 6 words. Mobile inbox; no room for hype. +- Specific, not aspirational. +- Include the prospect's company name or a specific signal where natural. +- Test the founder's instinct: would the founder open this email cold? + +**Subject framework, what works, what fails, and why:** + +| Pattern | Works because | Example | +|---|---|---| +| Pain or scaling question | Validates a struggle they're actually having; implies you've seen it before | "Scaling outbound to 100/day?" | +| "{{Company}}'s {{topic}}" | Feels 1:1; passes the "is this for me?" test in the inbox | "Acme's developer velocity" | +| Practical value, not pitch | Promises utility before claiming anything | "How we cut review time at Brex" | +| `Re:` thread on follow-ups | Familiar continuity; lower friction on touch 2+ | "Re: Acme's developer velocity" | +| Their name when you've talked before | High recognition for warm follow-ups | "Jane, quick follow-up" | + +| Pattern | Fails because | Example | +|---|---|---| +| Aspirational claims | Inbox BS detectors trigger immediately | "10x your engineers", "Unlock the full power of X" | +| Product / version launches | Reads as a marketing blast, not a peer email | "Acme 2.0 is here" | +| Generic thought leadership | No "why you, why now" connection to the prospect | "Ramping on unfamiliar codebases" | +| Vague social proof | Which teams? What does "accelerating" mean? | "Top teams are accelerating with Acme" | + +**Naming rule of thumb:** if the prospect already knows your product, your product name in the subject helps (recognition). If you're cold, lead with **their** company name or a specific pain, not your product name they don't know yet. + +**Auto-flag in gtm-get-better:** any subject containing the words "unlock", "10x", "accelerating", or a version number ("2.0:") is high-risk regardless of N. Retire before they hit the dataset. + +## Sender identity + +The same email body can land 1.5 to 2x more positive replies when sent from a recognized company domain versus a personal Gmail. Internal A/B data from a more mature outbound team showed ~12% positive vs ~7% positive on the same play. + +Apply this to founder-scale outbound: + +- **Send from your company domain** (you@yourcompany.com), not a personal Gmail alias, once the domain is warmed. +- **For early founders without warm reputation:** use a dedicated outbound subdomain (e.g. `hello.yourcompany.com`) so any reputation damage stays off your product email. +- **Display name:** "Firstname Lastname" or "Firstname @ Company". Never "Sales Team", never "Outbound", never an alias the prospect can't connect back to you. +- **Reply-to:** always your actual email. Don't use a no-reply or a CRM-managed alias. +- **Plain text only.** No HTML. No tracking pixels. No images. These all hurt deliverability at founder volume and read as spammy. +- **No unsubscribe footer needed for low-volume B2B outreach to professional emails.** CAN-SPAM requires it for higher volume; add a one-line "no problem if not relevant, happy to drop it" if you're sending >50/day or to consumer addresses. + +**Body rules:** +- 2 to 4 sentences. Hard cap 4. Long emails get archived. +- First sentence references the signal (the `hook` from gtm-find-prospects). +- One CTA, phrased as a question, on its own newline at the end. +- Plain text. No HTML, no images, no tracking pixels (kills deliverability and feels gross). +- No unsubscribe footer needed for low-volume B2B outreach to professional emails (legally, CAN-SPAM requires it only at higher volumes; check your jurisdiction). Add one if sending >100/day. + +**4-step sequence template:** + +**Step 1 (Day 0), The opener** +``` +Subject: {{Company}}'s {{specific signal area}} + +{{Hook line — references the prospect-specific signal from gtm-find-prospects}} + +{{One line connecting their context to what you do, in their language.}} + +{{Single CTA — a question, on its own line. CTA tier depends on signal_strength; see below.}} + +{{Founder signature}} +``` + +**CTA tier matched to signal_strength (read this column from the prospect CSV):** + +| signal_strength | Touch 1 CTA tier | Examples | +|---|---|---| +| `high` | direct ask | "Would a 15-min chat make sense?" / "Want me to set up an enterprise trial?" | +| `medium` | soft ask | "Would a quick Loom showing this be useful?" / "Open to swapping notes?" | +| `low` | content / peer ask | "Want me to send the playbook we wrote on this?" / "Happy to share what we've seen, interested?" | + +Asking for a meeting on touch 1 of a low-signal play gets ignored. Asking for "the playbook" on touch 1 of a high-signal play leaves leverage on the table. Match the ask to the heat. + +**Step 2 (Day 3 to 4), Value add** +``` +Subject: Re: {{Step 1 subject}} + +{{Different angle from Step 1 — share a relevant resource, customer outcome, or one-line insight.}} + +{{Softer CTA — "happy to send the Loom" or "want me to share the playbook?"}} +``` + +**Step 3 (Day 8 to 10), Alternative offer** +``` +Subject: Re: {{Step 1 subject}} + +{{A lower-commitment alternative to the original CTA. "If a call's too much, would a 5-min Loom be useful?" or "Happy to send a one-pager you can forward to the team."}} +``` + +**Step 4 (Day 14 to 18), Breakup** +``` +Subject: Re: {{Step 1 subject}} + +{{Clean acknowledgment they're busy. One final offer or a referral ask. No guilt.}} + +{{Example: "All good if not a fit right now. Anyone else at {{Company}} I should reach out to?"}} +``` + +### Step 4: Show drafts + approval + +Display Step 1 for each prospect (Steps 2 to 4 are pre-generated but only sent if no reply). + +Per prospect: name + company + signal + the 4-step preview. Founder choices: Send Step 1 now / Save all 4 as Drafts / Skip / Edit. + +### Step 5: Send via Gmail API + +Each send uses the Gmail API's `users.messages.send`. The skill's helper script (or inline tool calls) handles: + +- Encoding the message as RFC 2822 + base64url. +- Threading: Steps 2 to 4 use the same Message-ID `In-Reply-To` so they thread under Step 1 in the recipient's inbox. +- Spacing: enforce `COLD_EMAIL_MIN_INTERVAL_SECONDS` between sends in auto-send mode. +- Cap enforcement: increment `.gtm-state/send-counter-{{date}}.json`; refuse to exceed cap. + +Drafts mode: use `users.drafts.create` instead. Drafts appear in the founder's Gmail Drafts folder; threading still works on send. + +### Step 6: Schedule Steps 2 to 4 + +For each prospect with Step 1 sent: +- Steps 2 to 4 are saved as Gmail Drafts immediately so the founder can review them anytime. +- A pending-followup log entry is created in `outreach-log/email-followups-pending.jsonl` with send-after dates. +- A reply-check needs to gate sends: before sending Step N, check via Gmail API if the thread has a reply from the recipient. If yes, abort the sequence for that prospect. + +Re-run `/gtm-cold-email --followups` (or set a daily Cursor Automation) to process the pending queue. + +### Step 7: Log every send + +Append to `outreach-log/email.jsonl`: + +```json +{ + "timestamp": "2026-05-27T17:30:00Z", + "campaign": "{{campaign-name}}", + "step": 1, + "prospect": { "name": "...", "email": "...", "email_confidence": "confirmed", "company": "...", "score": 85 }, + "signal": "recent_funding", + "hook_used": "Seed $4M led by Foo VC 2026-01-15", + "subject": "Acme's developer velocity", + "body": "the full message body", + "send_mode": "auto" | "approval" | "drafts", + "gmail_message_id": "186abc...", + "thread_id": "186def...", + "founder_action": "send" | "edit_send" | "draft_only" +} +``` + +## Reply handling, the state machine + +Every cold-email sequence is a tiny state machine: the prospect either has not replied yet, has replied, or has timed out. The skill enforces this every time `/gtm-cold-email --followups` or `--check-replies` runs. + +``` +For each prospect with at least one touch sent: + state = NOT_REPLIED (default) + + If any inbound message exists on the thread (from anyone other than us): + classify the first inbound message per the rubric in /gtm-get-better + state = REPLIED + cancel all pending touches for this prospect + write to outreach-log/email-replies.jsonl with the classification + + If state is NOT_REPLIED and the next scheduled touch time has passed: + if today's send count < daily cap: + send the next touch + increment send count + reschedule the touch after that + + If state is NOT_REPLIED and all 4 touches have been sent: + state = COMPLETED_NO_REPLY +``` + +### `/gtm-cold-email --check-replies` + +Run this at the start of every `--followups` cycle, or as its own automation: + +1. Pull recent replies: `users.threads.list` with `q=in:inbox newer_than:14d`. +2. For each thread, check if it's a thread we initiated (cross-reference `thread_id` from `outreach-log/email.jsonl`). +3. For each thread with new inbound, take the **first** inbound message and classify it using the shared rubric (positive / objection / neutral / OOO / negative). See the `gtm-get-better` skill for the rubric definition. +4. Run OOO detection heuristics before LLM classification (cheap pre-filter): + - `Auto-Submitted` header is set to anything other than `no` + - Subject contains `out of office`, `OOO`, `auto-reply`, `[Auto Reply]`, `automatic reply` + - Body matches `^(I'?m|I am) (out of office|on vacation|on leave|away)` + - Body matches `I no longer work at` +5. Append one entry per thread to `outreach-log/email-replies.jsonl`: + ```json + { + "timestamp": "...", + "campaign": "...", + "thread_id": "...", + "prospect_email": "...", + "classification": "positive|objection|neutral|ooo|negative", + "is_first_reply": true, + "reply_excerpt": "first 280 chars of the inbound message", + "touch_at_reply": 2, + "auto_classified": true + } + ``` +6. For each `classification != null`, cancel any matching pending entries in `email-followups-pending.jsonl` (the state machine's "cancel all pending touches" step). +7. Surface positives to the founder in the next session summary so they can draft a personal reply. + +### `/gtm-cold-email --followups` + +Run this once a day (ideally via a Cursor Automation; see `automations/daily-followups.workflow.json`): + +1. Run `--check-replies` first to ensure no stale pending touches. +2. Read `outreach-log/email-followups-pending.jsonl`. +3. For each entry where `send_after <= now` AND prospect state is `NOT_REPLIED`: + - Verify daily cap hasn't been hit. + - Send via Gmail API (the draft already exists from the initial campaign run). + - Log the send to `outreach-log/email.jsonl`. + - Remove the entry from pending. +4. Output a summary of what was sent + what was skipped (and why). + +### Reply auto-handling, what we do NOT do + +We never auto-reply to inbound. Two reasons: + +1. Reputation: an autonomous bot replying as the founder is the fastest way to destroy trust if it gets something wrong. +2. Conversion: a real reply from the founder to a positive lead converts at orders of magnitude higher than a bot reply. + +What we DO offer: when `--check-replies` finds a positive, surface it to the founder + offer to draft a personal reply in their voice (using `sales-pack.md` for tone). The founder reads and clicks send. + +## Honest limitations + +- **You're sending from your founder's primary email**. Reputation damage compounds. Use a dedicated outbound subdomain when scaling. +- **Gmail's per-day send limits**: Workspace allows 2,000 sends/day total. Cold email volume should sit far below that. +- **Sequencing requires the founder to leave Cursor open or run a Cursor Automation**. The skill doesn't have a background daemon. The simplest model: re-run `/gtm-cold-email --followups` daily. +- **Pattern-guessed emails bounce more often.** Bounces above ~5% hurt domain reputation. Always verify (NeverBounce free tier, mail-tester.com) before sending pattern-guessed addresses. + +## Scripts + +The skill ships with one helper script, `scripts/gmail-auth.py`, that the founder runs once during setup. It does the OAuth flow against the OAuth client they created at `console.cloud.google.com/apis/credentials` and saves a refresh token to `.gtm-state/gmail-token.json` (chmod 600). All subsequent sends use that saved token directly via `google-api-python-client`. + +Run it once: + +```bash +pip install google-auth-oauthlib google-api-python-client +python scripts/gmail-auth.py +``` + +## Output after the run + +``` +Cold email campaign: {{campaign-name}} +Step 1 sent: {{N}} | Step 1 saved as drafts: {{M}} | Skipped: {{X}} +Daily cap remaining today: {{remaining}} of {{cap}} +Steps 2–4 saved as drafts (will send on cadence unless reply received). + +Top 3 hooks used: +1. Jane Doe — Acme's recent $4M seed round +2. ... + +Run /gtm-cold-email --followups daily to advance the sequence. +Run /gtm-cold-email --check-replies to ingest responses. +Run /gtm-get-better in 7 days to learn from reply patterns. +``` diff --git a/founder-gtm/skills/gtm-cold-email/scripts/gmail-auth.py b/founder-gtm/skills/gtm-cold-email/scripts/gmail-auth.py new file mode 100644 index 0000000..24b2429 --- /dev/null +++ b/founder-gtm/skills/gtm-cold-email/scripts/gmail-auth.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""One-time Gmail OAuth bootstrap for the founder-gtm cold-email skill. + +Run once after creating the OAuth client in the Google Cloud Console. + +Usage: + python scripts/gmail-auth.py + +Prerequisites: + pip install google-auth-oauthlib google-api-python-client + +Reads: + $CURSOR_PLUGIN_ROOT/.gtm-state/oauth-client.json + +Writes: + $CURSOR_PLUGIN_ROOT/.gtm-state/gmail-token.json (chmod 600) +""" + +import os +from pathlib import Path + +try: + from google_auth_oauthlib.flow import InstalledAppFlow +except ImportError as e: + raise SystemExit( + "Missing dependency. Run:\n" + " pip install google-auth-oauthlib google-api-python-client" + ) from e + +SCOPES = [ + "https://www.googleapis.com/auth/gmail.send", + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/gmail.modify", + "https://www.googleapis.com/auth/gmail.readonly", +] + +PLUGIN_ROOT = Path(os.environ.get("CURSOR_PLUGIN_ROOT", Path(__file__).resolve().parents[3])) +STATE_DIR = PLUGIN_ROOT / ".gtm-state" +CLIENT_FILE = STATE_DIR / "oauth-client.json" +TOKEN_FILE = STATE_DIR / "gmail-token.json" + + +def main() -> None: + STATE_DIR.mkdir(parents=True, exist_ok=True) + if not CLIENT_FILE.exists(): + raise SystemExit( + f"Missing {CLIENT_FILE}.\n" + "Create an OAuth client (Desktop app) at\n" + " https://console.cloud.google.com/apis/credentials\n" + f"and save the downloaded JSON to that path." + ) + + flow = InstalledAppFlow.from_client_secrets_file(str(CLIENT_FILE), SCOPES) + creds = flow.run_local_server(port=0, prompt="consent") + TOKEN_FILE.write_text(creds.to_json()) + os.chmod(TOKEN_FILE, 0o600) + print(f"Token saved to {TOKEN_FILE} (chmod 600)") + + +if __name__ == "__main__": + main() diff --git a/founder-gtm/skills/gtm-cold-email/scripts/requirements.txt b/founder-gtm/skills/gtm-cold-email/scripts/requirements.txt new file mode 100644 index 0000000..cf4bf99 --- /dev/null +++ b/founder-gtm/skills/gtm-cold-email/scripts/requirements.txt @@ -0,0 +1,3 @@ +google-auth-oauthlib>=1.2 +google-api-python-client>=2.130 +google-auth>=2.30 diff --git a/founder-gtm/skills/gtm-design-play/SKILL.md b/founder-gtm/skills/gtm-design-play/SKILL.md new file mode 100644 index 0000000..558289a --- /dev/null +++ b/founder-gtm/skills/gtm-design-play/SKILL.md @@ -0,0 +1,217 @@ +--- +name: gtm-design-play +description: Codify a working outbound motion into a reusable play. Distilled from how Cursor's growth team designs automated outbound plays internally (person vs account signals, persona match, channel choice, four-touch cadence, offer ladder), generalized for early-stage founders. Use after at least one campaign has produced replies and you want to capture the pattern, when a founder asks how to scale what worked, when /gtm-get-better surfaces a winning signal/persona combo, or when the founder says they want to systematize outreach. +--- + +# Design Play, turn a working motion into a repeatable play + +A "play" is the smallest unit of repeatable outbound: one signal, one persona, one channel, one cadence, one offer ladder. Naming and structuring it the same way every time is what turns ad-hoc outreach into a system. + +This skill borrows the framework Cursor's internal growth team uses (`generate-outbound-play-ideas`), stripped of the proprietary signals and metrics. The framework is sound. The signals are yours to define. + +## When to design a play vs run one-off outreach + +Design a play when: +- A signal has produced replies twice in a row from different people. +- You can describe the signal in one sentence without using the word "interested". +- The same persona is the buyer across both replies. + +Don't design a play when: +- You've sent fewer than 25 messages of any kind. +- The replies came from prospects you already knew. +- The signal is "I think they'd care". + +The point is to name what's already working, not to invent it. + +## The play structure + +Every play has five fields. Write them in this order. Each one constrains the next. + +### 1. Signal +What event in the world tells you this person might be open to a message right now? Two flavors: + +**Person signals**, a specific person did a specific thing. +- Example: VP Eng posted on X about evaluating AI code review tools. +- Example: Founding engineer registered for a workshop on prompt engineering. +- Example: CTO emailed support asking about SSO at a competitor. + +**Account signals**, a company-level metric or event crossed a threshold. +- Example: Company hit a Series A in the last 60 days. +- Example: Team grew by 3+ engineers in the last 90 days. +- Example: 5+ users from the same domain signed up for the product. + +Person signals get messaged directly. Account signals require enriching the account to find the right buyer first. + +### 2. Persona +The role at the company you actually message. Default: a director-or-above engineering leader if you sell to engineering teams. Otherwise: whatever the sales-pack `## Personas` section lists. + +If the signal is a person signal, the persona is usually that person. If it's an account signal, the persona is whoever can sponsor a meeting at that company. + +### 3. Channel +Pick one. Not multi-channel. Plays branch by channel for a reason: cadence, format, and acceptable length differ by an order of magnitude. + +- **X DM**, when the persona is active on X and the hook is from a recent post. +- **LinkedIn**, when the persona is a leader at an established company; safest default for cold. +- **Cold email**, when you have a verified email and the message needs more than a sentence to land. + +### 4. Cadence +Four touches. Day 0, 3 to 5, 7 to 10, 14 to 18. Each touch has a different angle. The cardinal rule: do not repeat Step 1's angle in Step 2. + +- **Touch 1:** signal-anchored opener. One soft CTA. +- **Touch 2:** new angle. Share something useful. Different CTA. +- **Touch 3:** lower-commitment alternative. (Loom instead of call, one-pager instead of demo.) +- **Touch 4:** clean breakup. Acknowledge they're busy. Offer one final thing or a referral ask. + +Do not write a fifth touch. Diminishing returns and damaged sender reputation start there. + +### 5. Offer ladder +Match the ask to the signal strength. + +| Signal strength | Default Touch 1 CTA | +|---|---| +| High (just funded, just complained about your problem, asked support a buying question) | Direct: "want me to set up a call?" or "want to start a trial?" | +| Medium (recent role change, recent product launch in your space, content engagement) | Soft: "happy to send a Loom" or "want our playbook on X?" | +| Low (matches persona, no specific behavior) | Content only: "thought you'd find this useful", no meeting ask | + +Sending a Touch 1 demo request on a Low-signal play is the fastest way to a 0% reply rate. The internal data is consistent on this. + +## The "do not re-offer" rule + +If a prospect already consumed the asset you'd otherwise offer (downloaded the guide, attended the webinar, read the case study), the next touch must be what comes *after* that asset. Not the asset again. + +Example: someone downloaded your "scaling X" guide on Tuesday. Wednesday's Touch 1 should reference the guide and offer the next step (a walkthrough, a templated implementation, a related deeper resource), not the guide. + +This is the single most common play-design mistake. + +## Segmentation: one signal, two or three plays + +Same signal often warrants different copy for different segments. Examples: + +- **Workshop registrant**, attended vs registered-but-no-show needs different Touch 1. +- **Recent fundraise**, Seed vs Series B founders care about different things; same play, different framing. +- **Existing customer expansion** vs **cold company** on the same product-usage signal, totally different message. + +Before drafting copy, list two or three segments and write Touch 1 separately for each. If you can't think of meaningful segments, you have one play, not three. + +## Workflow + +### Step 1: Identify the candidate motion + +Pull from one of these inputs: +- `outreach-log/*.jsonl`, find a signal+persona combo with replies. +- The founder's stated hypothesis: "I think X works because Y." +- A `/gtm-get-better` run flagging a high-positive-rate pattern. + +Show the founder the candidate. Confirm it's worth codifying. + +### Step 2: Draft the play + +Walk through the five fields. Ask one at a time. Use the recommended answer when the founder hesitates. + +For signal strength, ask explicitly: "Is this high, medium, or low?" Map to the offer ladder. + +### Step 3: Write the segments + +Ask: "Are there subgroups within this signal that need different framing?" If yes, list them. Write a Touch 1 opener for each segment separately. Reuse Touches 2 to 4 across segments unless the segments differ enough to need separate sequences. + +### Step 4: Validate + +Run this checklist before saving: + +- [ ] **Detectable signal.** Could you write a script (or set up a Lemlist/Smartlead trigger) that fires when this signal occurs? +- [ ] **Reachable persona.** Can you find email or LinkedIn or X handle for the people who fit this persona? +- [ ] **Offer matches signal.** High signal gets a direct ask; low signal gets content. No mismatches. +- [ ] **Not redundant.** If you have other plays, this one targets a distinct combination. +- [ ] **Helpful to recipient.** If you got this message at the timing the play targets, would you appreciate it? If no, the play needs rework. + +Any failure means revise before saving. + +### Step 5: Save to `plays/` + +Write to `plays/{slug}.md` using the template below. Tell the founder which prospect list this play applies to and suggest running the matching channel skill (`/gtm-x-outreach`, `/gtm-linkedin-outreach`, or `/gtm-cold-email`) against that list with the play's segment in mind. + +## Template + +```markdown +# Play: {{Name}} + +> Designed YYYY-MM-DD via /gtm-design-play. Update by re-running. + +## Signal +**Type:** person | account +**Signal:** {{one-sentence description of the event/threshold that triggers this play}} +**Strength:** high | medium | low +**Detection:** {{how you'll actually catch this — script, manual scan, tool alert}} +**Source data:** {{where the signal lives — X search, TechCrunch RSS, product analytics, etc.}} + +## Persona +**Title pattern:** {{e.g. "Director+ in Engineering" or "Founding engineer at 5-25 person team"}} +**Buying power:** decision-maker | influencer +**Sharpest hook for this persona:** {{one sentence}} + +## Channel +**Primary:** x | linkedin | email +**Reason:** {{why this channel, not the others}} + +## Segments +Two or three. For each, write a Touch 1 opener separately. + +### Segment A: {{name}} +**Touch 1 opener:** {{verbatim copy}} + +### Segment B: {{name}} +**Touch 1 opener:** {{verbatim copy}} + +## Cadence (shared across segments) + +| Touch | Day | Angle | CTA | +|---|---|---|---| +| 1 | 0 | Signal-anchored | {{from offer ladder}} | +| 2 | 3-5 | {{different angle — proof, content, peer reference}} | {{softer CTA}} | +| 3 | 7-10 | {{alternative offer — Loom, one-pager, async}} | {{lower-commitment CTA}} | +| 4 | 14-18 | Clean breakup | {{referral ask or "no vs not now?"}} | + +## Offer ladder applied +- **Touch 1 CTA:** {{specific ask matching signal strength}} +- **Asset offered:** {{if any — and confirm prospect hasn't already consumed it}} +- **Walk-away offer (Touch 4):** {{referral, trial link, or final binary question}} + +## Success criteria +- **Positive reply rate target:** {{realistic — 2-5% for cold; 5-15% for warm/strong-signal}} +- **When to retire:** 0% positive at N≥15 sends → retire the *signal* before rewriting copy. +- **When to scale:** above target at N≥20 → run again with 3x volume. + +## Notes +{{Anything that won't fit above — caveats, segment-specific tactics, tool-specific quirks.}} +``` + +## Starter plays to fork + +Three fully filled-in example plays live in `plays/`. They are the fastest path from zero to a running campaign: fork one, swap in the founder's own product, customer names, and signal sources, run it. + +| Play file | Signal | Persona | Channel | +|---|---|---|---| +| `plays/recent-seed-fundraisers-vp-eng.md` | Company raised seed in last 60 days, 5+ engineers on LinkedIn | VP / Head of Engineering | cold email | +| `plays/show-hn-launches-ai-infra.md` | Founder posted Show HN in last 14 days in AI infra category | The Show HN founder themselves | X DM (warmer), email fallback | +| `plays/linkedin-job-change-eng-leader.md` | Someone with VP+ Eng title started a new role in last 30 days | The new eng leader | LinkedIn connect + Touch 2 message | + +When a founder is designing their first play, recommend they read these first, pick the one closest to their motion, and fork it (`cp plays/{starter}.md plays/{my-version}.md`) rather than starting from a blank template. + +## What this skill explicitly does NOT do + +- Does not draft individual messages. That's the channel skills' job. This codifies the pattern; channel skills execute it. +- Does not run the play. Hand off to `/gtm-x-outreach`, `/gtm-linkedin-outreach`, or `/gtm-cold-email` with a prospect list. +- Does not track results. That's `/gtm-get-better`'s job, and `gtm-get-better` reads `plays/*.md` to know which plays to evaluate. + +## Hypothesis backlog + +When designing plays, you'll generate ideas that don't have data yet. Write them down in `plays/_backlog.md` (one line each) so you can come back when you have enough volume to test: + +``` +- Product-usage signal: 5+ admin actions in a single session → owner persona, X channel +- Support-ticket signal: keywords "compliance" or "audit" → security leader persona, email channel +- Hiring signal: 3+ open eng roles posted in last 30 days → VP Eng persona, LinkedIn channel +- Consolidation signal: 10+ individual seats at one company → admin persona, email channel +``` + +These are seeds. The `/gtm-get-better` skill can prompt you to promote one to a real play after you've gathered evidence. diff --git a/founder-gtm/skills/gtm-design-play/plays/linkedin-job-change-eng-leader.md b/founder-gtm/skills/gtm-design-play/plays/linkedin-job-change-eng-leader.md new file mode 100644 index 0000000..2e456f7 --- /dev/null +++ b/founder-gtm/skills/gtm-design-play/plays/linkedin-job-change-eng-leader.md @@ -0,0 +1,88 @@ +# Play: linkedin-job-change-eng-leader + +> Designed 2026-05-27 via /gtm-design-play. Starter play, fork before running. + +## Signal +**Type:** person +**Signal:** Someone with a VP+ Engineering title started a new role in the last 30 days. +**Strength:** medium +**Detection:** LinkedIn Sales Navigator alert "Posted on LinkedIn: started a new position" filtered by `title contains VP, Head, Director, CTO` AND `function: Engineering`. Without Sales Navigator: manual LinkedIn search using the "started a new job" filter (free, run weekly). +**Source data:** LinkedIn job-change alerts, or LinkedIn search results saved into a CSV per week. + +## Persona +**Title pattern:** VP Engineering, Head of Engineering, Director of Engineering, CTO. Newly in role. +**Buying power:** decision-maker, though the first 60 days of a new role they are often defaulting to existing vendors and asking for evaluations from their team. +**Sharpest hook for this persona:** They are evaluating tools for the first 90 days as they assess what their predecessor left them. New eng leaders almost always rip out 1 to 3 vendors in their first quarter. + +## Channel +**Primary:** LinkedIn (connection request + Touch 2 after acceptance) +**Reason:** Their LinkedIn is the most active in their career right now (congrats messages flooding in). A connection request lands at peak attention. Email is noisier with onboarding logistics. + +## Segments +Two segments based on previous company size. + +### Segment A: Came from a much larger company (1000+ engineers) +**Touch 1 (connection note, <= 250 chars):** +``` +Hey {{firstname}}, congrats on the {{Company}} move. Going from {{previous company}} scale to a smaller team usually surfaces a stack of tooling decisions in the first 60 days. We work with a few teams in your spot. Would love to connect. +``` + +**Touch 2 (after acceptance, day 1 to 2 post-connect):** +``` +Thanks for connecting, {{firstname}}. + +The pattern we see most often when someone moves from {{previous company}} scale to a smaller team: the smaller team's tooling looks underbuilt at first, but the rip-and-replace cost is brutal until month 4 to 6. We have helped 3 teams in your spot make those decisions less expensive. + +If you want, happy to send a 1-pager on what those 3 teams kept and what they replaced. + +Want me to send it? + +{{founder signature}} +``` + +### Segment B: Came from a similar-sized company or smaller (lateral or step-down) +**Touch 1 (connection note, <= 250 chars):** +``` +Hey {{firstname}}, congrats on joining {{Company}}. We work with a few teams running similar stacks to what you came from. Curious how the first weeks are going. Would love to connect. +``` + +**Touch 2 (after acceptance, day 1 to 2 post-connect):** +``` +Thanks for connecting, {{firstname}}. + +Quick context on what we do: {{founder one-liner from sales-pack.md}}. The reason I reached out is that {{Company}} looks like a strong fit for the kind of team we serve best, and the first 60 days in a new role is usually when the eng leader sets the bar for what tools the team uses. + +If it would help, happy to send a 2-min Loom on how {{customer with similar profile}} solved {{relevant problem}}. + +Want me to send it? + +{{founder signature}} +``` + +## Cadence (shared across segments) + +| Touch | Day | Angle | CTA | +|---|---|---|---| +| 1 | 0 | LinkedIn connection request, signal-anchored | "would love to connect" (no pitch in the connect note) | +| 2 | 1 to 2 (post-acceptance) | DM with context + one specific offer | "want me to send the {{1-pager / Loom}}?" | +| 3 | 7 to 10 (post-acceptance) | Alternative angle: ask one specific question about their first 30 days | "what is the first tooling decision you are looking at?" | +| 4 | 14 to 18 (post-acceptance) | Clean breakup, peer-to-peer offer | "totally no worries if not the right time, will be around when the dust settles" | + +## Offer ladder applied +- **Touch 1 CTA:** none, just the connect (acceptance is the goal). +- **Touch 2 CTA:** soft, a 1-pager or short Loom. Never a meeting on Touch 2. +- **Asset offered:** founder writes one "first 60 days as a new eng leader" doc, reuses across segments. +- **Walk-away offer (Touch 4):** soft, no pitch, keep the door open. + +## Success criteria +- **Connection acceptance rate target:** 35 to 45% (job-change moments are warm). +- **Positive reply rate target (Touch 2):** 5 to 10% of acceptances. +- **When to retire:** acceptance below 25% at N >= 25 → opener needs work. 0% positive Touch 2 at N >= 20 → Touch 2 framing needs work; the play likely still has signal. +- **When to scale:** above 8% positive at N >= 20, run on every weekly LinkedIn job-change scan. + +## Notes +- The 90-day window after starting a new role is the highest-leverage moment. After 90 days the prospect has already settled on vendors. +- Do not message in the first week after they started. They are in onboarding mode and inbox-flooded. Wait at least 7 days. +- If they post publicly about their first impressions of the eng stack, that is a Tier-High personalization opportunity. Reference the post directly. +- Avoid "congratulations on the new role" as the only personalization. Everyone says it. Add one specific thing from their background or the new company. +- For Sales Navigator users: the "Posted on LinkedIn" alert fires within 24 hours of the job change post. Set up the alert once; the play runs weekly off the alert output. diff --git a/founder-gtm/skills/gtm-design-play/plays/recent-seed-fundraisers-vp-eng.md b/founder-gtm/skills/gtm-design-play/plays/recent-seed-fundraisers-vp-eng.md new file mode 100644 index 0000000..da0459e --- /dev/null +++ b/founder-gtm/skills/gtm-design-play/plays/recent-seed-fundraisers-vp-eng.md @@ -0,0 +1,70 @@ +# Play: recent-seed-fundraisers-vp-eng + +> Designed 2026-05-27 via /gtm-design-play. Starter play, fork before running. + +## Signal +**Type:** account (hybrid, with person enrichment) +**Signal:** Company closed a seed round in the last 60 days AND has at least 5 engineers listed on LinkedIn. +**Strength:** medium +**Detection:** TechCrunch venture RSS + Crunchbase free tier scan, cross-filtered against LinkedIn "Engineering" headcount via the find-prospects script. Run weekly. +**Source data:** TechCrunch funding RSS (`scripts/techcrunch-funding-rss.py`), LinkedIn company search for `current_employee_count_eng >= 5`. + +## Persona +**Title pattern:** VP Engineering, Head of Engineering, Director of Engineering. At a seed-stage company this is often the first eng hire above the founders. +**Buying power:** decision-maker on tooling under $50k ARR. Influencer above that. +**Sharpest hook for this persona:** Seed-stage eng leaders are setting up the dev-tooling foundation. They are the buyer for anything that reduces the cost of going from 5 to 25 engineers. + +## Channel +**Primary:** email +**Reason:** VPs of Eng at seed companies are reachable on LinkedIn but slow to respond. Email lands in their workflow inbox where they expect peer outreach. X is hit or miss for non-founder eng leaders. + +## Segments +Two segments based on funding size. + +### Segment A: <$3M seed (capital-conscious, founder-led tech) +**Touch 1 opener:** +``` +Subject: {{Company}}'s eng setup + +Saw {{Company}} closed the seed last month, congrats. The 5-to-25-engineer stretch is usually where the early tooling decisions either pay off or have to be ripped out. We have helped {{customer_1}} and {{customer_2}} get through that without rebuilding mid-Series-A. + +Want me to share a short doc on what worked for them? + +{{founder signature}} +``` + +### Segment B: $3M to $8M seed (already hiring fast) +**Touch 1 opener:** +``` +Subject: {{Company}}'s next 10 engineers + +Saw the {{$XM}} seed close, congrats. You are probably mid-hire on the next 5 to 10 engineers right now. We have shipped specifically for the moment where eng goes from 5 to 25 (where most tooling cracks). {{customer_1}} just got through it; happy to share what they kept and what they rebuilt. + +Worth a 15-min call to compare notes? + +{{founder signature}} +``` + +## Cadence (shared across segments) + +| Touch | Day | Angle | CTA | +|---|---|---|---| +| 1 | 0 | Signal-anchored: recent seed + scaling moment | Soft (Segment A: "want a short doc?") / Direct (Segment B: "worth a 15-min call?") | +| 2 | 3 to 5 | Proof point, different angle: customer outcome with metric | "want the playbook we wrote on this?" | +| 3 | 8 to 10 | Alternative offer: 5-min Loom instead of call | "if a call is too much, would a Loom on {{X}} be useful?" | +| 4 | 14 to 18 | Clean breakup, referral ask | "all good if not a fit right now, is there someone else at {{Company}} I should reach out to?" | + +## Offer ladder applied +- **Touch 1 CTA:** Segment A: content (doc). Segment B: direct ask (15-min call). +- **Asset offered:** Founder writes a 1-page "First 10 eng hires, what tooling matters" doc once, reuses across the play. +- **Walk-away offer (Touch 4):** referral ask, not pitch. + +## Success criteria +- **Positive reply rate target:** 4 to 7% (seed-funded prospects are warmer than blanket cold). +- **When to retire:** 0% positive at N >= 15 sends, second cycle. Retire the *signal-persona pair*, not the copy first. +- **When to scale:** above 5% at N >= 20 sends, expand to 50 sends per week. + +## Notes +- Seed-funded lists are easy to over-mine. Watch for the same company getting 100+ cold emails the same week. Move fast (within 7 days of the round being announced) or wait 30+ days. +- Do not name a customer in Touch 1 if the customer is a direct competitor of {{Company}}. Check `sales-pack.md § Proof points` for permission tiers. +- If the seed round was led by a fund the founder also has a connection at, switch to `/gtm-warm-intro` instead. Warm intros from a shared investor convert dramatically better than cold. diff --git a/founder-gtm/skills/gtm-design-play/plays/show-hn-launches-ai-infra.md b/founder-gtm/skills/gtm-design-play/plays/show-hn-launches-ai-infra.md new file mode 100644 index 0000000..461ff17 --- /dev/null +++ b/founder-gtm/skills/gtm-design-play/plays/show-hn-launches-ai-infra.md @@ -0,0 +1,69 @@ +# Play: show-hn-launches-ai-infra + +> Designed 2026-05-27 via /gtm-design-play. Starter play, fork before running. + +## Signal +**Type:** person +**Signal:** A founder posted a Show HN launch in the last 14 days in the AI infra category (LLM tooling, agent frameworks, eval, observability, fine-tuning, vector DB, RAG). +**Strength:** high +**Detection:** HN Algolia search via `scripts/hn-show-scraper.py` with category keywords. Run every 2 days during active campaign. +**Source data:** Show HN posts (Algolia `https://hn.algolia.com/api/v1/search?tags=show_hn`), filtered by category keywords. + +## Persona +**Title pattern:** Founder, technical co-founder, solo builder. The person who actually wrote the post. +**Buying power:** decision-maker on their own product. Often the only person at the company. +**Sharpest hook for this persona:** They just shipped publicly and are checking HN every 5 minutes for the next 24 hours. Right time, right context. + +## Channel +**Primary:** X DM (warmer, fits the "just launched" energy) +**Fallback:** email (use the address in their HN profile if present, or pattern-guess from the company domain) +**Reason:** Founders posting Show HN are usually active on X around the launch. A DM lands while they are still in launch mode. Email works as a follow-up. + +## Segments +Two segments based on launch trajectory. + +### Segment A: Launch is climbing the HN front page (>50 points in <12 hours) +**Touch 1 opener:** +``` +hey, saw {{product}} on HN, the {{specific feature or claim}} bit landed for me. we are building {{founder one-liner}} and run into the exact {{problem area}} you are solving. + +curious what you would change about the {{X}} approach if you started over today. happy to share how we have seen {{specific pattern}} work for {{customer or design partner}}. + +would a quick swap of notes be useful? +``` + +### Segment B: Launch is quieter (<50 points, low engagement) +**Touch 1 opener:** +``` +hey, caught the {{product}} Show HN. the framing on {{specific paragraph from their post}} is sharp. most launches in this space lead with the wrong wedge. + +we are also in {{adjacent area}} and ran the same playbook 6 months ago. happy to share what got us our first 10 paying users if it is useful, no pitch. + +want me to send the writeup? +``` + +## Cadence (shared across segments) + +| Touch | Day | Angle | CTA | +|---|---|---|---| +| 1 | 0 to 1 (post launch) | Reference specific paragraph from their Show HN post | Segment A: swap notes (peer ask). Segment B: send the writeup (content). | +| 2 | 3 to 5 | New angle: ask about a real product question they raised in HN comments | "if you have 10 minutes, want to compare {{X}} approaches?" | +| 3 | 7 to 10 | Switch to email if no X reply. Same hook, slightly different framing. Alternative low-commitment offer (5-min Loom). | "wrote a quick Loom on {{X}}, want me to send?" | +| 4 | 14 to 18 | Clean breakup, peer-to-peer | "all good if it is not the right time, would love to swap notes once you are past launch" | + +## Offer ladder applied +- **Touch 1 CTA:** swap notes (Segment A) or send writeup (Segment B). Never a meeting on Touch 1. +- **Asset offered:** the founder writes one "what we learned launching {{X}}" doc, reuses it. +- **Walk-away offer (Touch 4):** keep the door open, no guilt. + +## Success criteria +- **Positive reply rate target:** 10 to 20% (Show HN launchers are unusually warm, and they care about peer feedback). +- **When to retire:** 0% positive at N >= 15 sends. Likely the founder's product is not adjacent enough to the AI infra category. Tighten the category filter. +- **When to scale:** above 12% at N >= 20 sends, run on every Show HN launch in the category, weekly. + +## Notes +- Speed matters: the first 48 hours after a Show HN post is the maximum-attention window. After 7 days the post is buried and the founder has moved on. +- Do not reference HN comments that were negative or contentious unless the founder shows resilience in their reply. Reading the comment thread is part of the personalization work. +- If the founder also tweeted the launch, reference both: "saw the Show HN post + the X thread, the {{X}} angle is sharp." +- The X DM character cap is 500. The opener above runs close, leave room. +- Pattern to avoid: pitching the product in Touch 1. The launch context creates peer-to-peer expectation; pitching breaks it. diff --git a/founder-gtm/skills/gtm-find-prospects/SKILL.md b/founder-gtm/skills/gtm-find-prospects/SKILL.md new file mode 100644 index 0000000..6be5233 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/SKILL.md @@ -0,0 +1,399 @@ +--- +name: gtm-find-prospects +description: Builds a ranked prospect list for an early-stage founder. Reads sales-pack.md for ICP and persona criteria, asks the founder what data sources they already pay for, then combines those with free sources (LinkedIn search, X via xmcp, GitHub orgs, Crunchbase free, TechCrunch funding RSS, Show HN, Product Hunt, public event attendee lists) to produce a ranked CSV at `prospects/{campaign}.csv`. Supports two modes: person-signal (one human did something interesting) and account-signal (a company hit a threshold; we find the right person inside). Includes a title classifier with exclusion list to filter out look-alike-but-wrong titles before any paid enrichment. Use when a founder needs a target list, says they want leads, asks who to message, runs /gtm-find-prospects, or before any outreach campaign. +--- + +# Find Prospects, scrappy targeting for founders + +You are building a target list for an early-stage founder. The output is one CSV per campaign that the channel skills (`gtm-x-outreach`, `gtm-linkedin-outreach`, `gtm-cold-email`) read directly. + +## Prerequisite check + +```bash +test -f sales-pack.md || echo "MISSING" +``` + +If `sales-pack.md` is missing, refuse to proceed. Tell the founder you need their sales pack first, then invoke the `gtm-sales-pack` skill. + +Parse `sales-pack.md` for the `## ICP`, `## Personas`, and `## Buying signals` sections. These become your targeting criteria. + +## How this differs from Clay or Apollo + +Both are great. They are also $800-$2000/mo before you have product-market fit. The framework this skill uses is the same one mature growth teams use, distilled into a free-first version: + +**Signal → Persona → Source → Enrich → Rank → Hand off** + +1. **Signal**: a real-world event indicating buying intent (just raised, just hired, just shipped, just complained on X). +2. **Persona**: which role at the company you would actually message, informed by `sales-pack.md`. +3. **Source**: where you find the signal-and-persona combination (free or paid). +4. **Enrich**: add contact info (LinkedIn URL, X handle, email pattern) once the prospect passes the bar. +5. **Rank**: order by signal strength, persona fit, reachability, and warm connections. +6. **Hand off**: write the CSV and tell the founder which channel skill to run next. + +## Two modes + +Founders almost always start in **person mode** and discover account mode later. Default to person unless the founder hands you a list of companies. + +### Person mode (default) + +You start from a signal that happened to a specific human. You already know who you want to message; you only need to enrich and rank. + +> "Find me people who recently posted on X about AI evaluations and look like engineering leaders at small startups." + +### Account mode + +You start from a list of companies (funding round list, conference attendee company list, lookalike of an existing customer, hand-curated target accounts). For each, you need to **find the right human inside** before any outreach. + +> "I have a list of 50 companies that raised seed rounds in the last 60 days. Find me the VP Eng or founder at each." + +Account-mode rows in the CSV have `account_signal`, `suggested_personas[]`, and `find_persona_recipe` columns instead of named individuals. The skill then either suggests a follow-up "enrich these to named contacts" run, or the founder fills in names manually. + +## Workflow + +### Step 1: Tool inventory + +Ask the founder what they have. AskQuestion, multi-select: + +``` +Question: "What targeting tools do you have access to? Pick everything you have. We'll prefer paid sources where you have them and fill gaps with free scraping/search." +Options: +- Apollo (paid) +- Clay (paid) +- Amplemarket (paid) +- ZoomInfo (paid) +- LinkedIn Sales Navigator (paid) +- A CSV/spreadsheet I already have +- Local xmcp MCP server (free X access with my OAuth) +- LinkedIn Premium or Recruiter +- Just whatever's free +``` + +Persist the answer to `${CURSOR_PLUGIN_ROOT}/.gtm-state/tools.json` so future runs don't re-ask. For paid tools, ask only for the variable name where the key lives in `.env`; never store the key yourself. + +### Step 2: Define the campaign + +``` +Question: "What's this campaign about? Pick the signal." +Options: +- Recent funding rounds (seed / A / B in last 90 days, matching ICP) +- Recent role changes (people who just started a buyer-persona role) +- Recent shipping (Show HN, Product Hunt, company changelogs) +- Recent complaining (people on X/Reddit who recently complained about a problem you solve) +- Specific company list (account mode — I have a list) +- Conference / event attendees (I have a list) +- Lookalike of an existing customer +- Other (I'll describe) +``` + +Also ask: +- **Campaign name** (slug, e.g. `seed-fundraisers-2026-01`) +- **Target count** (default 50, max 200; large lists kill personalization) +- **Mode** (auto-detect from signal: `Specific company list` → account mode; everything else → person mode by default) + +### Step 3: Run the source(s) + +Use the matrix below. Always reach for the highest-signal source first. + +| Signal | Best paid source | Free fallback | Helper script | +|---|---|---|---| +| Recent funding | Crunchbase Pro, Clay | TechCrunch venture RSS, Axios Pro Rata, Term Sheet | `scripts/techcrunch-funding-rss.py` | +| Recent role changes | Sales Navigator alerts, ZoomInfo | LinkedIn "started a new job" filter | manual LinkedIn search | +| Recent shipping |, | Show HN (Algolia API), Product Hunt | `scripts/hn-show-scraper.py` | +| Recent complaining |, | xmcp `searchPostsRecent`, Reddit search, HN search | `scripts/x-topic-search.py` | +| Specific company list | Apollo / Clay (instant enrich) | LinkedIn manual + Hunter.io + pattern guess | none, account mode handles this | +| Conference attendees |, | Conference attendee X lists, Luma event pages | bring your own CSV | +| Lookalike | Clay (similarity), Apollo | 3-attribute extraction + LinkedIn / Crunchbase / X bio cross-search; see Step 3.5 | manual + title classifier | + +For the X path, the xmcp scripts let you do bio-keyword filtering and pull each candidate's best recent post as a pre-built hook for the `gtm-x-outreach` skill. + +### Step 3.5: Lookalike enrichment (when the signal is "Lookalike of an existing customer") + +The signal-source matrix in Step 3 lists "lookalike" as a row but the recipe is below because it spans multiple data sources. Run this step only when the founder picked "Lookalike of an existing customer" in Step 2. + +**Why lookalikes work.** Your best existing customers share traits that predict fit. A 3-customer pattern is enough to start; a 5-customer pattern is reliable. The point is to mine those traits and turn them into search queries. + +#### 3.5.1: Ask the founder for 3 to 5 best existing customers + +``` +Question: "Which existing customers should we look-alike from? Pick 3 to 5 of your best (highest contract value, fastest activation, most product engagement, or whatever 'best' means for you)." +Options: +- Read from existing-customers.txt at project root (if it exists) +- Paste a list of company names inline +- I will give them to you one at a time +``` + +If `existing-customers.txt` exists, prefer it. Show the founder the list and ask them to star (`*`) the 3 to 5 they would clone if they could. If they have not maintained this file, ask them to name the 3 to 5 now. + +#### 3.5.2: For each customer, extract 3 attributes + +For each of the 3 to 5 customers, populate these attributes. Pull from public sources only (no internal CRM, no logged-in scraping). + +| Attribute | How to gather (free path) | How to gather (paid path) | +|---|---|---| +| Industry | Their LinkedIn company page "Industry" field; their About page on their site | Crunchbase free tier industry tags | +| Company size band | LinkedIn "Company size" range; About page if they publish it | Apollo, ZoomInfo size band | +| Tech stack indicators | Their public engineering blog tags, GitHub org public repos, StackShare if listed, job posts mentioning technologies | BuiltWith free tier for the marketing site | +| Recent funding round | TechCrunch search, Crunchbase free tier | Pitchbook, Crunchbase Pro | + +Save the per-customer attribute set to `.gtm-state/lookalike-source-{campaign-name}.json` so re-runs do not re-fetch. + +#### 3.5.3: Aggregate the dominant pattern + +Across the 3 to 5 source customers, identify the dominant value for each attribute. A "dominant" value is one that shows up in at least 3 of 5 (or 2 of 3) source customers. + +```json +{ + "industry_dominant": "B2B SaaS dev tools", + "size_band_dominant": "50 to 200 employees", + "tech_stack_dominant": ["TypeScript", "Postgres", "self-hosted observability"], + "recency_filter": "raised Series A or B in last 18 months" +} +``` + +If no attribute is dominant (high spread across customers), tell the founder honestly: "Your best customers do not cluster cleanly. Lookalike mode will produce noisy results. Consider going back to a signal-based campaign instead, or add 2 to 3 more best-customer examples to find the pattern." + +#### 3.5.4: Build the search queries + +Translate the dominant pattern into actual queries. Three sources to run in parallel: + +**LinkedIn company search filters:** +``` +- Industry: {{industry_dominant}} +- Company size: {{size_band_dominant}} +- Headquarters location: {{founder ICP region, e.g. United States}} +- Optional growth signal: "follower count grew 25%+ in last 90 days" (Sales Nav only) +``` + +**Crunchbase tag search (free tier):** +``` +- Industry group tag: {{closest Crunchbase tag to industry_dominant}} +- Operating status: Active +- Last funding round: {{recency_filter mapped to round type + date filter}} +``` + +**X bio keyword search via xmcp:** +For each tech_stack_dominant entry that maps to a community keyword (e.g. "Postgres", "TypeScript"), search X bios: +```python +# via xmcp's search-users-by-bio-keyword tool (or searchPostsRecent with bio filter) +queries = [ + "co-founder OR cto OR head of eng " + tech_keyword + for tech_keyword in tech_stack_dominant +] +``` + +For each query, collect candidate company names + URLs. + +#### 3.5.5: Dedupe and merge + +- Drop any company already in `existing-customers.txt`. +- Drop any company already in `pipeline.txt`. +- Drop any company already in the source 3 to 5 (do not lookalike yourself). +- Dedupe by domain (canonicalize: strip `www.`, strip `http(s)://`, lowercase). +- Merge LinkedIn / Crunchbase / X candidates into one list. If a company appears in 2+ sources, that is a stronger signal (boost score in Step 8). + +Cap the merged list at the founder's target count from Step 2. + +#### 3.5.6: Output to standard prospects CSV + +Write to `prospects/{campaign-name}.csv` using the schema in Step 9. Specifically for lookalike rows: + +- `signal` = `lookalike` +- `signal_evidence` = `Matches {{N}} of 3 attributes from best customers {{customer_1, customer_2, ...}}: {{matched_attributes}}` +- `signal_strength` = `high` when all 3 attributes match, `medium` when 2 of 3 match, `low` when only 1 matches. +- `score boost`: add +10 to the Step 8 score for rows where all 3 attributes match (this is the "lookalike triangle hit", the strongest lookalike signal we have). + +After Step 8 ranking, top of the list should be exact-attribute matches; the bottom should be partial matches the founder can review or drop. + +#### 3.5.7: Persona enrichment + +Lookalike mode is account-mode by default (you have companies, not people). After Step 3.5.6, the founder picks a persona from `sales-pack.md § Personas`, then either: + +- Runs the persona enrichment subflow against the company list (LinkedIn search for `{{persona title pattern}} at {{company}}`), or +- Marks the list as "company-level only" and hands off to a follow-up enrichment pass. + +The output CSV for lookalike mode should have both `company` and the `find_persona_recipe` filled in even if `full_name` is empty for some rows. + +### Step 4: Enrich (multi-anchor cascade) + +For each candidate row, attempt to populate these in order. Stop as soon as you have what you need for the chosen channel. + +``` +Identity anchors (from most reliable to least): + 1. LinkedIn URL + 2. Company domain + 3. Work email (verified) + 4. Personal email (gmail, etc. — see data/personal-email-domains.txt) + 5. X handle + 6. GitHub handle +``` + +**Personal-email bridge.** If the only contact you have is a personal email (gmail, icloud, etc.) and you don't know the company, search LinkedIn for that name and find their current employer separately. This is how you catch a founder who signed up to your product with their personal Gmail; their company is on LinkedIn, not in the email. + +**Email finding cascade (when you need email and don't have it):** + +1. Founder's paid tool (Apollo, Clay, Amplemarket) → enriched email. +2. Hunter.io free tier (25 lookups/mo) at `hunter.io/email-finder`. +3. Pattern guessing using a known email at the domain (`firstname.lastname@`, `flast@`, etc.). Verify with NeverBounce free tier before sending. +4. Skip the row if you can't find an email and email is the only channel. + +**LinkedIn URL** is almost always findable via Google: `"FirstName LastName" "Company" site:linkedin.com/in`. + +Mark contact-info confidence per row: `confirmed`, `pattern-guess`, `unverified`. The channel skills use this to decide whether to send or save as a draft for the founder to review. + +### Step 5: Apply the title classifier + +Once you have a `role` column populated, run the title classifier: + +```bash +python ${CURSOR_PLUGIN_ROOT}/skills/gtm-find-prospects/scripts/title-classifier.py \ + --input prospects/raw.csv \ + --output prospects/classified.csv +``` + +This adds three columns based on keyword lists in `data/title-keywords.txt` and `data/title-exclusions.txt`: + +- `title_is_tech_leader`, true if the role matches the leadership + technology rules +- `title_is_mid_tier`, true for senior IC titles (Staff, Principal, Tech Lead) +- `title_excluded_reason`, populated when an exclusion matched (e.g. "excluded:sales engineer", "excluded:chief of staff") + +Edit the data files to fit your ICP. The defaults are tuned for "VP+ engineering at startups" and exclude common look-alikes like Director of Sales Engineering, Chief of Staff, Customer Success leaders, and hardware engineers. + +### Step 6: Apply account-fit filters + +Before ranking, drop rows that fail any of these: + +- [ ] **Not already a customer** (founder maintains an `existing-customers.txt` file in the project root; skill checks each domain against it) +- [ ] **Not already in active pipeline** (similar `pipeline.txt` if the founder tracks one) +- [ ] **Geography matches ICP** (default: don't drop, but flag) +- [ ] **Company size matches ICP** (founder sets `min_employees` and `max_employees` in `sales-pack.md`; skill parses them) +- [ ] **Not a child account** of an existing customer +- [ ] **Domain isn't a personal-email domain treated as a company** (gmail.com is not a company; check against `data/personal-email-domains.txt`) +- [ ] **Industry / segment matches ICP** when known + +This is the cheap filter that prevents you from spending personalization effort on prospects who would be a hard "no" before you said hello. + +### Step 7: Run domain histogram (when you have an email list) + +If the source was an attendee list or anything with raw emails, run: + +```bash +python scripts/domain-histogram.py --input prospects/raw.csv --email-column email +``` + +You will see: + +``` +total rows: 134 + with email: 128 (96%) + empty: 6 + + business: 51 (40%) + personal: 77 (60%) + +top 20 domains (business + personal mixed): + 34 gmail.com [personal] + 18 outlook.com [personal] + 12 acme.com + 8 example.io + ... +``` + +If 60%+ of the list is personal-domain email, you have two choices: + +- Run the personal-email bridge step (LinkedIn-lookup each personal email to find the actual company). +- Drop everything that isn't business email if the channel is cold email (deliverability matters more than coverage). + +Either way: don't push a personal-email-heavy list straight to a paid enrichment tool. Costs add up and most enrichers fail on personal addresses. + +### Step 8: Rank + +Score each candidate 0 to 100: + +| Factor | Weight | How to score | +|---|---|---| +| Signal strength | 35 | High (just funded, just complained about your problem) = 35. Medium (recently changed roles, recently shipped) = 22. Low (matches persona) = 8. | +| Persona fit | 30 | `title_is_tech_leader` true and exact persona match = 30. Tech leader, adjacent persona = 18. Mid-tier IC at right company = 12. | +| Reachability | 15 | Email confirmed + LinkedIn + X = 15. Two of three = 9. One = 4. | +| Warm path | 10 | Accelerator batchmate, shared connection, mutual follow, shared work history = 10. Otherwise 0. | +| Play priors | 10 | Bump up if the signal type has worked for this founder before (read from `outreach-log/learned-*.md`). Defaults to 0 for first campaign. | + +Sort desc. Drop anything below 30. Cap at the founder's target count. + +**Honest note on play priors:** for a founder's first campaign there's no history to weight. Skip this column the first time. After 2 to 3 campaigns and a `/gtm-get-better` run, the learning files exist and this column starts paying off. + +### Step 9: Write the CSV + +Output to `prospects/{campaign-name}.csv`. Schema (channel skills depend on it): + +```csv +score,full_name,company,role,linkedin_url,x_handle,email,email_confidence,signal,signal_strength,signal_evidence,recommended_channel,hook,warm_path,notes +``` + +| Column | Description | +|---|---| +| `score` | 0 to 100 from Step 8 | +| `full_name` | First + last (empty in account mode) | +| `company` | Company name | +| `role` | Job title (empty in account mode) | +| `linkedin_url` | Full URL or empty | +| `x_handle` | `@handle` or empty | +| `email` | Email if found | +| `email_confidence` | `confirmed` / `pattern-guess` / `unverified` | +| `signal` | Category from Step 2 | +| `signal_strength` | `high` / `medium` / `low`, used by channel skills to pick CTA tier | +| `signal_evidence` | One line with the proof and a date | +| `recommended_channel` | `x` / `linkedin` / `email` / `multi` | +| `hook` | One-sentence personalization hook the channel skill will use | +| `warm_path` | Note about any mutual connection or shared context, if you found one | +| `notes` | Anything else useful | + +**Account mode** writes a parallel schema with `account_signal`, `suggested_personas`, and `find_persona_recipe` (e.g. `"LinkedIn search: 'VP Engineering' OR 'Head of Engineering' at {company} who started in last 90 days"`) instead of named individuals. + +### Step 10: Event-attendee subflow (when applicable) + +If the source is an event attendee list (Luma export, conference badge scan, webinar registrants): + +1. Run `domain-histogram.py` first to see the personal/business mix. +2. Tag each row with `signal=joined_event`, `signal_evidence=Joined {event_name} on {date}`, `signal_strength=medium` (event attendance is a real signal but not as strong as direct complaining or active shipping). +3. For business-email attendees, fast-path through enrichment (email is already verified). +4. For personal-email attendees, decide: bridge via LinkedIn (slow but worth it for the right event), or drop (cheap; OK for low-fit events). + +### Step 11: Hand off + +Tell the founder where the CSV lives, the top 5 prospects with their hooks, and which channel skill to run next. Suggest sending the first 5 by hand before batching the rest. Always. + +## Quality bar before saving + +- [ ] Every row has `signal` and `signal_evidence` (no "just a name" rows) +- [ ] Every person-mode row has `hook` written for that person specifically +- [ ] Every row has at least one of `linkedin_url`, `x_handle`, `email` +- [ ] No duplicate rows (dedupe by `full_name + company` for person mode, by `company` for account mode) +- [ ] Top 5 spot-check: ranking correlates with signal × persona fit +- [ ] If 50%+ of rows are `email_confidence=pattern-guess`, tell the founder honestly + +If quality fails, re-run the relevant step. Quality beats volume every time at this stage. + +## Honest disclosure to the founder + +When you hand off, be specific about confidence: + +> "This list has 47 prospects ranked by signal strength. Top 12 are high-confidence (strong signal, exact persona, multiple channels). Next 20 are solid (medium signal, good persona fit). Bottom 15 are pattern-guessed emails or weaker signals, send those to drafts mode and review before sending." + +## What this skill does not do + +- Send messages (channel skills do that). +- Store contact info outside the local CSV (no SaaS upload, no third-party sync). +- Scrape LinkedIn at scale or violate platform ToS. Search via your own logged-in session is fine; mass-scraping is not. +- Buy data. Every paid source the founder uses requires them to already pay for it. + +## Scripts in this folder + +See `scripts/README.md` for full details. The short version: + +- `title-classifier.py`, deterministic role filter; reads from `data/` +- `techcrunch-funding-rss.py`, recent funding rounds +- `hn-show-scraper.py`, Show HN "just shipped" posts +- `x-topic-search.py`, bio + topic search via xmcp +- `domain-histogram.py`, personal vs business email mix before enrichment + +These are intentionally small and standard-library-first so a founder can read and modify them. Anything more sophisticated lives in the channel skills, where it's needed for sending. diff --git a/founder-gtm/skills/gtm-find-prospects/data/personal-email-domains.txt b/founder-gtm/skills/gtm-find-prospects/data/personal-email-domains.txt new file mode 100644 index 0000000..2441a65 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/data/personal-email-domains.txt @@ -0,0 +1,50 @@ +# Personal email domains, used by domain-histogram.py to classify addresses +# as personal vs work. One domain per line. Lines starting with # are comments. +# Edit if you find local providers your prospects use that aren't here. + +gmail.com +googlemail.com +yahoo.com +yahoo.co.uk +yahoo.fr +yahoo.de +yahoo.es +yahoo.it +ymail.com +hotmail.com +hotmail.co.uk +hotmail.fr +outlook.com +outlook.co.uk +live.com +live.co.uk +icloud.com +me.com +mac.com +aol.com +protonmail.com +pm.me +proton.me +fastmail.com +fastmail.fm +zoho.com +msn.com +qq.com +163.com +126.com +foxmail.com +naver.com +mail.ru +yandex.com +yandex.ru +yandex.kz +duck.com +duckduckgo.com +hey.com +tutanota.com +gmx.com +gmx.de +gmx.net +web.de +t-online.de +mail.com diff --git a/founder-gtm/skills/gtm-find-prospects/data/title-exclusions.txt b/founder-gtm/skills/gtm-find-prospects/data/title-exclusions.txt new file mode 100644 index 0000000..5b01016 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/data/title-exclusions.txt @@ -0,0 +1,31 @@ +# Global exclusion patterns for title-classifier.py +# One pattern per line. Matches anywhere in the title (case-insensitive). +# Lines starting with # are comments. +# Hitting any of these drops the row regardless of which bucket would match. +# +# Defaults exclude roles that often look like engineering leaders but aren't. +# Edit to match your ICP. If you sell to sales engineers, remove that line. + +\bsales engineer\b +\bsales engineering\b +\bsolutions engineer\b +\bsolutions engineering\b +\baccount executive\b +\bcustomer success\b +\bcsm\b +\brecruiter\b +\btalent\b +\bpeople ops\b +\baccountant\b +\bparalegal\b +\battorney\b +\bmarketing manager\b +\bbrand\b +\bgrowth marketing\b +\bcommunity manager\b +\boperations manager\b +\bhr \b +\bchief of staff\b +\bproduct marketing\b +\bfield engineer\b +\bhardware engineer\b diff --git a/founder-gtm/skills/gtm-find-prospects/data/title-keywords.txt b/founder-gtm/skills/gtm-find-prospects/data/title-keywords.txt new file mode 100644 index 0000000..9ac1ac9 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/data/title-keywords.txt @@ -0,0 +1,17 @@ +# Bucket definitions for title-classifier.py +# Format: bucket_name|include_keyword_1,include_keyword_2,...|exclude_keyword_1,exclude_keyword_2,... +# Matches are case-insensitive substring matches. +# Exclusions within a bucket take precedence over its includes. +# Order matters: first matching bucket wins. +# +# Edit these to match your ICP. The defaults assume you sell to engineering +# leaders at startups. If you sell to a different persona, replace these. + +cto|cto,chief technology officer| +vp_engineering|vp engineering,vp of engineering,vice president of engineering,svp engineering,evp engineering|sales,marketing,people,finance +head_of_engineering|head of engineering,head of eng,head of platform,head of infrastructure|sales,marketing,people +director_engineering|director of engineering,director, engineering,engineering director,senior director engineering,senior director, engineering|sales,marketing +engineering_manager|engineering manager,eng manager,manager, engineering,manager of engineering|associate +founding_engineer|founding engineer,founding software engineer,engineer #1,first engineer| +staff_principal_engineer|staff engineer,staff software engineer,principal engineer,principal software engineer,senior staff engineer| +founder_ceo|founder,co-founder,ceo,chief executive|staff,engineering,engineer diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/README.md b/founder-gtm/skills/gtm-find-prospects/scripts/README.md new file mode 100644 index 0000000..a00eaa3 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/README.md @@ -0,0 +1,47 @@ +# gtm-find-prospects/scripts + +Small, real scrapers and utilities the `gtm-find-prospects` skill calls. Each one does one thing, writes CSV or JSON, and exits. Compose them. + +| Script | What it does | Outputs | +|---|---|---| +| `techcrunch-funding-rss.py` | Pulls recent funding-round announcements from TechCrunch's RSS, filters by stage/sector keywords, returns one row per round | CSV: company, round, amount, lead, date, source_url | +| `hn-show-scraper.py` | Pulls "Show HN" launches from the Hacker News Algolia API in the last N days, filtered by keywords | CSV: title, company, url, hn_url, date, points, comments | +| `x-topic-search.py` | Uses the local xmcp MCP server to find people posting about a topic, ranked by recency and engagement | CSV: handle, name, bio, last_post_excerpt, last_post_date, faves, followers | +| `domain-histogram.py` | Takes a CSV of email addresses (or any column of emails) and returns a histogram of domains, separating personal from work | CSV: domain, count, kind (work/personal), sample_addresses | +| `title-classifier.py` | Classifies a list of job titles into persona buckets using a keyword-list + exclusion-regex approach (no LLM needed, no API key). Reads keyword lists from `../data/title-keywords.txt` and `../data/title-exclusions.txt` so founders can tune their ICP without editing Python | CSV: original cols + persona_bucket, persona_confidence, matched_keywords | + +All scripts: +- Use stdlib + `requests` only where possible. No paid API dependencies. +- Print to stdout when run with `--print`; otherwise write to a path. +- Exit non-zero on real failures, zero on empty results. +- Are designed to be piped together. Output of one is valid input to the next. + +## Install + +```bash +pip install -r ${CURSOR_PLUGIN_ROOT}/skills/gtm-find-prospects/scripts/requirements.txt +``` + +## Compose example + +```bash +# 1. Get recent Seed/A rounds in AI infra +python techcrunch-funding-rss.py --stages "Seed,Series A" --keywords "AI,LLM,infrastructure" \ + --since-days 60 --out funding.csv + +# 2. Find people posting about your problem area on X +python x-topic-search.py --query '("AI evals" OR "LLM testing") -is:retweet' \ + --min-faves 5 --since-days 30 --out x_prospects.csv + +# 3. Classify titles into personas (tune ../data/title-keywords.txt first) +python title-classifier.py --input x_prospects.csv --title-col bio \ + --out x_prospects_classified.csv +``` + +## Data files + +`title-classifier.py` reads from `../data/`: + +- `title-keywords.txt`, bucket definitions. One bucket per line, format: `bucket_name|include_keyword_1,include_keyword_2|exclude_keyword_1,exclude_keyword_2`. Edit to match your ICP. +- `title-exclusions.txt`, global exclusion regex (one pattern per line). Hits these and the row is dropped no matter what bucket would otherwise match. Use for "always wrong" titles like sales engineers when you want eng leaders. +- `personal-email-domains.txt`, read by `domain-histogram.py`. One domain per line. Defaults cover gmail / yahoo / outlook / icloud / proton / ru / cn providers. Add to it if you find others. diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/domain-histogram.py b/founder-gtm/skills/gtm-find-prospects/scripts/domain-histogram.py new file mode 100644 index 0000000..fb9cac0 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/domain-histogram.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Compute a domain histogram from a CSV containing email addresses. + +Separates work domains from personal (gmail, yahoo, etc.) so you can focus +outreach on people using a company email. Useful for webinar attendee +exports, event signup lists, or any CSV with an email column. + +Usage: + python domain_histogram.py --input attendees.csv --email-col email --out domains.csv +""" +from __future__ import annotations + +import argparse +import csv +import re +import sys +from collections import Counter, defaultdict + +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +DEFAULT_PERSONAL_DOMAINS_FILE = SCRIPT_DIR.parent / "data" / "personal-email-domains.txt" + + +def load_personal_domains(path: Path) -> set[str]: + if not path.exists(): + return set() + domains: set[str] = set() + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip().lower() + if line and not line.startswith("#"): + domains.add(line) + return domains + + +EMAIL_RE = re.compile(r"[\w.+\-]+@([\w.-]+\.[A-Za-z]{2,})") + + +def domain_of(email: str) -> str | None: + m = EMAIL_RE.search(email or "") + return m.group(1).lower() if m else None + + +def kind(domain: str, personal_domains: set[str]) -> str: + return "personal" if domain in personal_domains else "work" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", required=True, help="Input CSV path") + parser.add_argument("--email-col", default="email", help="Column name containing the email") + parser.add_argument("--min-count", type=int, default=1, help="Minimum count to include in output") + parser.add_argument( + "--personal-domains-file", + default=str(DEFAULT_PERSONAL_DOMAINS_FILE), + help="Path to personal-email-domains.txt", + ) + parser.add_argument("--out", default="-") + args = parser.parse_args() + + personal_domains = load_personal_domains(Path(args.personal_domains_file)) + counter: Counter[str] = Counter() + samples: dict[str, list[str]] = defaultdict(list) + + try: + with open(args.input, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + if args.email_col not in (reader.fieldnames or []): + print( + f"error: column '{args.email_col}' not in {reader.fieldnames}", + file=sys.stderr, + ) + return 1 + for row in reader: + email = (row.get(args.email_col) or "").strip() + d = domain_of(email) + if not d: + continue + counter[d] += 1 + if len(samples[d]) < 3: + samples[d].append(email) + except FileNotFoundError: + print(f"error: input file not found: {args.input}", file=sys.stderr) + return 1 + + rows = [ + { + "domain": d, + "count": c, + "kind": kind(d, personal_domains), + "sample_addresses": "|".join(samples[d]), + } + for d, c in counter.most_common() + if c >= args.min_count + ] + + fieldnames = ["domain", "count", "kind", "sample_addresses"] + out_stream = sys.stdout if args.out == "-" else open(args.out, "w", newline="", encoding="utf-8") + try: + writer = csv.DictWriter(out_stream, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + finally: + if out_stream is not sys.stdout: + out_stream.close() + + work = sum(r["count"] for r in rows if r["kind"] == "work") + personal = sum(r["count"] for r in rows if r["kind"] == "personal") + print( + f"wrote {len(rows)} domains to {args.out}. " + f"Work: {work} addresses; personal: {personal}.", + file=sys.stderr, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/hn-show-scraper.py b/founder-gtm/skills/gtm-find-prospects/scripts/hn-show-scraper.py new file mode 100644 index 0000000..845f1b3 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/hn-show-scraper.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Pull recent "Show HN" launches from Hacker News' Algolia API. + +Free. No API key required. + +Usage: + python show_hn.py --keywords "AI,LLM,devtools" --since-days 30 --out launches.csv +""" +from __future__ import annotations + +import argparse +import csv +import re +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +try: + import requests +except ImportError: + sys.exit("Install requests: pip install requests") + +ALGOLIA = "https://hn.algolia.com/api/v1/search_by_date" + + +def fetch_show_hn(since_days: int) -> list[dict]: + since = int((datetime.now(timezone.utc) - timedelta(days=since_days)).timestamp()) + out: list[dict] = [] + page = 0 + while True: + params = { + "tags": "show_hn", + "numericFilters": f"created_at_i>={since}", + "hitsPerPage": 100, + "page": page, + } + r = requests.get(ALGOLIA, params=params, timeout=20) + r.raise_for_status() + data = r.json() + hits = data.get("hits", []) + if not hits: + break + out.extend(hits) + if page + 1 >= data.get("nbPages", 1): + break + page += 1 + return out + + +def parse_company(title: str) -> str: + t = re.sub(r"^Show HN:\s*", "", title, flags=re.IGNORECASE).strip() + m = re.match(r"^([A-Za-z0-9.\-_ &]{2,30})(?:\s+[-–—:|]|\s+is\b|\s+\(|,)", t) + if m: + return m.group(1).strip() + return t.split(" ")[0] + + +def matches(text: str, terms: list[str]) -> list[str]: + text_l = text.lower() + return [t for t in terms if t.lower() in text_l] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--keywords", default="", help="Comma-separated; any match qualifies. Empty = all Show HNs.") + parser.add_argument("--since-days", type=int, default=30) + parser.add_argument("--min-points", type=int, default=0) + parser.add_argument("--out", default="-") + args = parser.parse_args() + + keywords = [k.strip() for k in args.keywords.split(",") if k.strip()] + + try: + hits = fetch_show_hn(args.since_days) + except requests.RequestException as e: + print(f"error: {e}", file=sys.stderr) + return 1 + + rows: list[dict] = [] + for h in hits: + title = h.get("title") or "" + blob = f"{title} {h.get('story_text') or ''}" + if keywords and not matches(blob, keywords): + continue + if (h.get("points") or 0) < args.min_points: + continue + rows.append({ + "title": title, + "company": parse_company(title), + "url": h.get("url") or "", + "hn_url": f"https://news.ycombinator.com/item?id={h.get('objectID')}", + "date": datetime.fromtimestamp(h.get("created_at_i", 0), tz=timezone.utc).date().isoformat(), + "points": h.get("points") or 0, + "comments": h.get("num_comments") or 0, + "author": h.get("author") or "", + "keywords_matched": "|".join(matches(blob, keywords)) if keywords else "", + }) + + rows.sort(key=lambda r: (r["date"], r["points"]), reverse=True) + fieldnames = ["title", "company", "url", "hn_url", "date", "points", "comments", "author", "keywords_matched"] + out_stream = sys.stdout if args.out == "-" else open(args.out, "w", newline="", encoding="utf-8") + try: + writer = csv.DictWriter(out_stream, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + finally: + if out_stream is not sys.stdout: + out_stream.close() + + print(f"wrote {len(rows)} launches to {args.out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/requirements.txt b/founder-gtm/skills/gtm-find-prospects/scripts/requirements.txt new file mode 100644 index 0000000..d69b72d --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/requirements.txt @@ -0,0 +1,3 @@ +requests>=2.31 +feedparser>=6.0 +mcp>=1.0 diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/techcrunch-funding-rss.py b/founder-gtm/skills/gtm-find-prospects/scripts/techcrunch-funding-rss.py new file mode 100644 index 0000000..ddfafb0 --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/techcrunch-funding-rss.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Pull recent funding announcements from TechCrunch's venture RSS feed. + +Filters by stage keywords (e.g. "Seed", "Series A") and sector keywords found +in the title or description. Writes a CSV of qualifying rounds. + +Free. No API key required. + +Usage: + python techcrunch_funding.py \\ + --stages "Seed,Series A" \\ + --keywords "AI,LLM,infrastructure" \\ + --since-days 60 \\ + --out funding.csv +""" +from __future__ import annotations + +import argparse +import csv +import re +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path + +try: + import feedparser +except ImportError: + sys.exit("Install feedparser: pip install feedparser") + +FEEDS = [ + "https://techcrunch.com/category/venture/feed/", + "https://techcrunch.com/category/startups/feed/", +] + +AMOUNT_RE = re.compile(r"\$([\d.]+)\s*(million|billion|m|b)\b", re.IGNORECASE) +LEAD_RE = re.compile(r"led by ([A-Z][\w &'.,-]+?)(?:\.|,| with| and)", re.IGNORECASE) + + +def parse_amount(text: str) -> str: + m = AMOUNT_RE.search(text) + if not m: + return "" + value, unit = m.groups() + unit_norm = "B" if unit.lower().startswith("b") else "M" + return f"${value}{unit_norm}" + + +def parse_lead(text: str) -> str: + m = LEAD_RE.search(text) + return m.group(1).strip() if m else "" + + +def parse_company(title: str) -> str: + parts = re.split(r"\s+(?:raises|secures|closes|lands|nets)\s+", title, maxsplit=1, flags=re.IGNORECASE) + if len(parts) == 2: + return parts[0].strip() + parts = title.split(",", 1) + return parts[0].strip() + + +def matches(text: str, terms: list[str]) -> list[str]: + text_l = text.lower() + return [t for t in terms if t.lower() in text_l] + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--stages", default="Seed,Series A,Series B", help="Comma-separated stage keywords to filter by") + parser.add_argument("--keywords", default="", help="Comma-separated sector keywords (any match qualifies); leave empty to match all") + parser.add_argument("--since-days", type=int, default=90) + parser.add_argument("--out", default="-", help="Output CSV path or '-' for stdout") + args = parser.parse_args() + + stages = [s.strip() for s in args.stages.split(",") if s.strip()] + keywords = [k.strip() for k in args.keywords.split(",") if k.strip()] + since = datetime.now(timezone.utc) - timedelta(days=args.since_days) + + rows: list[dict] = [] + seen_urls: set[str] = set() + for url in FEEDS: + try: + feed = feedparser.parse(url) + except Exception as e: + print(f"warn: failed to fetch {url}: {e}", file=sys.stderr) + continue + for entry in feed.entries: + link = entry.get("link", "") + if link in seen_urls: + continue + seen_urls.add(link) + + published_raw = entry.get("published_parsed") or entry.get("updated_parsed") + if not published_raw: + continue + published = datetime(*published_raw[:6], tzinfo=timezone.utc) + if published < since: + continue + + title = entry.get("title", "") + summary = entry.get("summary", "") + blob = f"{title} {summary}" + + stage_hits = matches(blob, stages) + if not stage_hits: + continue + if keywords and not matches(blob, keywords): + continue + + rows.append({ + "company": parse_company(title), + "stage": stage_hits[0], + "amount": parse_amount(blob), + "lead_investor": parse_lead(blob), + "date": published.date().isoformat(), + "keywords_matched": "|".join(matches(blob, keywords)) if keywords else "", + "title": title, + "source_url": link, + }) + + rows.sort(key=lambda r: r["date"], reverse=True) + fieldnames = ["company", "stage", "amount", "lead_investor", "date", "keywords_matched", "title", "source_url"] + out_stream = sys.stdout if args.out == "-" else open(args.out, "w", newline="", encoding="utf-8") + try: + writer = csv.DictWriter(out_stream, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + finally: + if out_stream is not sys.stdout: + out_stream.close() + + print(f"wrote {len(rows)} rounds to {args.out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/title-classifier.py b/founder-gtm/skills/gtm-find-prospects/scripts/title-classifier.py new file mode 100644 index 0000000..3a430ea --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/title-classifier.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +"""Classify a list of job titles into persona buckets. + +Keyword-list + exclusion-regex approach. Lifted from how Cursor's growth +team classifies titles for outbound (`golden_list_scripts`), genericized. + +No API key, no LLM. Fast and deterministic. Founders tune buckets by +editing the data files, not Python: + ../data/title-keywords.txt , bucket definitions + ../data/title-exclusions.txt, global "always drop" regex list + +Usage: + python title-classifier.py --input prospects.csv --title-col role \\ + --out prospects_classified.csv + + # Inspect what's in your data with no filter: + python title-classifier.py --input prospects.csv --title-col role --print-misses +""" +from __future__ import annotations + +import argparse +import csv +import re +import sys +from pathlib import Path +from typing import Iterable + +SCRIPT_DIR = Path(__file__).resolve().parent +DATA_DIR = SCRIPT_DIR.parent / "data" +DEFAULT_KEYWORDS_FILE = DATA_DIR / "title-keywords.txt" +DEFAULT_EXCLUSIONS_FILE = DATA_DIR / "title-exclusions.txt" + + +def load_buckets(path: Path) -> list[tuple[str, list[str], list[str]]]: + """Parse title-keywords.txt into [(bucket_name, includes, excludes)].""" + buckets: list[tuple[str, list[str], list[str]]] = [] + for raw in _iter_lines(path): + parts = raw.split("|") + if len(parts) < 2: + continue + name = parts[0].strip() + includes = [k.strip() for k in parts[1].split(",") if k.strip()] + excludes = ( + [k.strip() for k in parts[2].split(",") if k.strip()] + if len(parts) > 2 + else [] + ) + if name and includes: + buckets.append((name, includes, excludes)) + return buckets + + +def load_exclusion_regex(path: Path) -> re.Pattern[str] | None: + patterns: list[str] = [] + for raw in _iter_lines(path): + patterns.append(raw) + if not patterns: + return None + combined = "|".join(f"(?:{p})" for p in patterns) + try: + return re.compile(combined, re.IGNORECASE) + except re.error as e: + print(f"warn: failed to compile exclusion regex: {e}", file=sys.stderr) + return None + + +def _iter_lines(path: Path) -> Iterable[str]: + if not path.exists(): + return + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + yield line + + +def classify( + title: str, + buckets: list[tuple[str, list[str], list[str]]], + excluder: re.Pattern[str] | None, +) -> tuple[str | None, list[str], str | None]: + """Return (bucket_or_None, matched_keywords, exclusion_reason_or_None).""" + if not title: + return None, [], None + t = title.lower() + if excluder: + m = excluder.search(t) + if m: + return None, [], f"excluded:{m.group(0).strip()}" + for bucket, includes, excludes in buckets: + if any(ex.lower() in t for ex in excludes): + continue + matched = [kw for kw in includes if kw.lower() in t] + if matched: + return bucket, matched, None + return None, [], None + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--input", required=True) + parser.add_argument("--title-col", default="role") + parser.add_argument("--out", default="-") + parser.add_argument( + "--keywords-file", + default=str(DEFAULT_KEYWORDS_FILE), + help="Path to title-keywords.txt", + ) + parser.add_argument( + "--exclusions-file", + default=str(DEFAULT_EXCLUSIONS_FILE), + help="Path to title-exclusions.txt", + ) + parser.add_argument( + "--print-misses", + action="store_true", + help="Print titles that matched no bucket to stderr (for tuning buckets).", + ) + parser.add_argument( + "--only-matched", + action="store_true", + help="Only output rows that matched a bucket.", + ) + args = parser.parse_args() + + buckets = load_buckets(Path(args.keywords_file)) + excluder = load_exclusion_regex(Path(args.exclusions_file)) + if not buckets: + print( + f"error: no buckets loaded from {args.keywords_file}. " + "Did you edit it for your ICP?", + file=sys.stderr, + ) + return 1 + + try: + with open(args.input, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + if args.title_col not in (reader.fieldnames or []): + print( + f"error: column '{args.title_col}' not in {reader.fieldnames}", + file=sys.stderr, + ) + return 1 + input_rows = list(reader) + input_fields = reader.fieldnames or [] + except FileNotFoundError: + print(f"error: input file not found: {args.input}", file=sys.stderr) + return 1 + + out_fields = list(input_fields) + [ + "persona_bucket", + "persona_confidence", + "matched_keywords", + "exclusion_reason", + ] + matched_rows: list[dict] = [] + misses: list[str] = [] + matched_count = 0 + excluded_count = 0 + + for row in input_rows: + title = row.get(args.title_col, "") or "" + bucket, kws, exclusion = classify(title, buckets, excluder) + confidence = "high" if len(kws) >= 2 else ("medium" if kws else "low") + row = dict(row) + if bucket: + row["persona_bucket"] = bucket + row["persona_confidence"] = confidence + row["matched_keywords"] = "|".join(kws) + row["exclusion_reason"] = "" + matched_count += 1 + elif exclusion: + row["persona_bucket"] = "" + row["persona_confidence"] = "" + row["matched_keywords"] = "" + row["exclusion_reason"] = exclusion + excluded_count += 1 + if args.only_matched: + continue + else: + row["persona_bucket"] = "" + row["persona_confidence"] = "" + row["matched_keywords"] = "" + row["exclusion_reason"] = "" + if title and args.print_misses: + misses.append(title) + if args.only_matched: + continue + matched_rows.append(row) + + out_stream = ( + sys.stdout if args.out == "-" else open(args.out, "w", newline="", encoding="utf-8") + ) + try: + writer = csv.DictWriter(out_stream, fieldnames=out_fields, extrasaction="ignore") + writer.writeheader() + writer.writerows(matched_rows) + finally: + if out_stream is not sys.stdout: + out_stream.close() + + print( + f"classified {len(input_rows)} titles. matched: {matched_count}. " + f"excluded: {excluded_count}. misses: {len(input_rows) - matched_count - excluded_count}.", + file=sys.stderr, + ) + if args.print_misses and misses: + print("\n--- top 25 misses (consider adding to buckets) ---", file=sys.stderr) + for t in misses[:25]: + print(t, file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/founder-gtm/skills/gtm-find-prospects/scripts/x-topic-search.py b/founder-gtm/skills/gtm-find-prospects/scripts/x-topic-search.py new file mode 100644 index 0000000..1344ecd --- /dev/null +++ b/founder-gtm/skills/gtm-find-prospects/scripts/x-topic-search.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Find people on X posting about a topic, via the local xmcp MCP server. + +Requires the xmcp MCP server running at http://127.0.0.1:8000/mcp with +OAuth1 user-context configured. See ~/dev/xmcp/CURSOR_SETUP.md. + +Uses xmcp's `searchPostsRecent` tool, ranks results by engagement + recency, +and outputs a CSV. + +Usage: + python xmcp_topic_search.py \\ + --query '("AI evals" OR "LLM testing") -is:retweet' \\ + --since-days 14 \\ + --min-faves 5 \\ + --out x_prospects.csv +""" +from __future__ import annotations + +import argparse +import asyncio +import csv +import sys +from datetime import datetime, timedelta, timezone + +try: + from mcp import ClientSession + from mcp.client.streamable_http import streamablehttp_client +except ImportError: + sys.exit("Install mcp: pip install mcp") + +XMCP_URL = "http://127.0.0.1:8000/mcp" + + +async def run_search(query: str, since_days: int, max_results: int) -> list[dict]: + start_time = ( + datetime.now(timezone.utc) - timedelta(days=since_days) + ).isoformat(timespec="seconds").replace("+00:00", "Z") + + async with streamablehttp_client(XMCP_URL) as (read, write, _): + async with ClientSession(read, write) as session: + await session.initialize() + result = await session.call_tool( + "searchPostsRecent", + { + "query": query, + "max_results": min(max_results, 100), + "start_time": start_time, + "tweet.fields": "created_at,public_metrics,author_id,lang", + "expansions": "author_id", + "user.fields": "username,name,description,public_metrics", + }, + ) + + payload = next( + (c.text for c in result.content if hasattr(c, "text") and c.text), None + ) + if not payload: + return [] + import json + data = json.loads(payload) + tweets = data.get("data", []) or [] + users_by_id = {u["id"]: u for u in (data.get("includes", {}).get("users") or [])} + + rows: list[dict] = [] + for t in tweets: + author_id = t.get("author_id") + user = users_by_id.get(author_id, {}) + metrics = t.get("public_metrics") or {} + rows.append({ + "handle": "@" + user.get("username", ""), + "name": user.get("name", ""), + "bio": (user.get("description") or "").replace("\n", " "), + "followers": (user.get("public_metrics") or {}).get("followers_count", 0), + "last_post_excerpt": (t.get("text") or "").replace("\n", " ")[:240], + "last_post_date": (t.get("created_at") or "")[:10], + "post_id": t.get("id"), + "faves": metrics.get("like_count", 0), + "replies": metrics.get("reply_count", 0), + "reposts": metrics.get("retweet_count", 0), + }) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--query", required=True, help="X search query (see https://docs.x.com/x-api/posts/search/integrate/build-a-query)") + parser.add_argument("--since-days", type=int, default=7, help="Recent search supports up to 7 days on free tier") + parser.add_argument("--min-faves", type=int, default=0) + parser.add_argument("--max-results", type=int, default=100) + parser.add_argument("--out", default="-") + args = parser.parse_args() + + try: + rows = asyncio.run(run_search(args.query, args.since_days, args.max_results)) + except ConnectionError: + print( + "error: xmcp MCP server not reachable at " + XMCP_URL + ". " + "Start it with ~/dev/xmcp/start.sh", + file=sys.stderr, + ) + return 2 + except Exception as e: + print(f"error: {e}", file=sys.stderr) + return 1 + + rows = [r for r in rows if r["faves"] >= args.min_faves] + rows.sort(key=lambda r: (r["faves"], r["last_post_date"]), reverse=True) + + seen: set[str] = set() + deduped: list[dict] = [] + for r in rows: + if r["handle"] in seen: + continue + seen.add(r["handle"]) + deduped.append(r) + + fieldnames = ["handle", "name", "bio", "followers", "last_post_excerpt", "last_post_date", "post_id", "faves", "replies", "reposts"] + out_stream = sys.stdout if args.out == "-" else open(args.out, "w", newline="", encoding="utf-8") + try: + writer = csv.DictWriter(out_stream, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(deduped) + finally: + if out_stream is not sys.stdout: + out_stream.close() + + print(f"wrote {len(deduped)} unique authors to {args.out}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/founder-gtm/skills/gtm-get-better/SKILL.md b/founder-gtm/skills/gtm-get-better/SKILL.md new file mode 100644 index 0000000..55f7f78 --- /dev/null +++ b/founder-gtm/skills/gtm-get-better/SKILL.md @@ -0,0 +1,357 @@ +--- +name: gtm-get-better +description: Weekly compound learning loop for the founder-gtm plugin. Reads outreach logs across all channels (X DMs, LinkedIn, cold email), classifies replies using the standard rubric (positive / objection / neutral / OOO / negative) on the first inbound message per thread, tallies metrics per-person-enrolled (not per-email) and sliced by play / channel / touch number, identifies winning hooks/subjects/openers, and writes timestamped updates to sales-pack.md plus per-channel learned-*.md files so the next campaign produces sharper messages. Includes a retirement rule (N>=15 sends, 0 positive replies, 2+ cycles → retire). Use weekly, after any campaign has had 7+ days to collect replies, when the founder runs /gtm-get-better, asks to learn from past outreach, or asks what's working. +--- + +# Get Better, compound learning loop + +The whole reason to run outbound from Cursor instead of a fixed SaaS tool is that the system can get sharper every cycle. This skill is what makes that real. + +## Inputs + +``` +outreach-log/x-dms.jsonl every X DM sent +outreach-log/linkedin.jsonl every LinkedIn note sent +outreach-log/email.jsonl every cold email step sent +outreach-log/email-replies.jsonl replies ingested by /gtm-cold-email --check-replies +outreach-log/manual-replies.jsonl replies the founder hand-logs (X, LinkedIn) +plays/*.md play definitions (slice metrics by these) +sales-pack.md current source of truth (we append to it) +``` + +If `outreach-log/` is empty or every file is missing, tell the founder you need a campaign + ~7 days for replies before there's anything to learn from. + +## The rubric (single source of truth) + +Every reply gets classified as exactly one of: + +| Label | Definition | Examples | +|---|---|---| +| **positive** | Prospect expressed real interest, asked a follow-up question, agreed to a meeting, or asked you to send more info | "Sure, would love to chat" / "Yes, send the Loom" / "What does pricing look like?" / "Can we do Thursday?" | +| **objection** | Prospect engaged but pushed back; usually worth replying to | "We're already on X" / "Not the right time" / "Send a one-pager and I'll look" / "What about [concern]" | +| **neutral** | Prospect replied but no clear engagement or pushback | "Got it, thanks" / "Will pass to the team" / single-emoji replies | +| **OOO** | Auto-reply, out of office, vacation responder, role-change auto-reply | "I'm out until Monday" / "I no longer work at X" / Auto-Submitted header present | +| **negative** | Hard no, unsubscribe, angry reply, mark-as-spam | "Stop emailing me" / "Remove from list" / "Don't contact me" | + +**First-reply-only.** Only the first inbound message per thread is classified for metrics. Follow-up "thanks!" or scheduling-back-and-forth doesn't get re-classified, that would let later messages overwrite the real intent signal. + +**OOO detection heuristics** (auto-classify, then the founder confirms): +- `Auto-Submitted` header set to anything other than `no` +- Subject contains "out of office", "OOO", "auto-reply", "automatic reply", "[Auto Reply]" +- Body matches `^(I'?m|I am) (out of office|on vacation|on leave|away)` +- Body matches `I no longer work at` + +OOO replies stop the sequence (don't keep messaging someone on vacation) but are excluded from the positive-rate denominator. + +## Workflow + +### Step 1: Pick the lookback window + +``` +Question: "How far back are we learning from?" +Options: +- Since last /gtm-get-better run (default) +- Last 7 days +- Last 30 days +- All time +``` + +Default: since last run. Read `.gtm-state/last-gtm-get-better.json` for the timestamp. + +### Step 2: Ingest fresh replies + +Two paths: + +**Cold email**, call `/gtm-cold-email --check-replies` first. This polls Gmail threads and writes new entries to `outreach-log/email-replies.jsonl`. Then classify each new entry per the rubric. + +**X DMs and LinkedIn**, no programmatic reply ingestion exists. Walk the founder through unreplied prospects: + +``` +Question: "Any X DM or LinkedIn replies to log since last run?" +Options: +- Yes — walk me through prospects with no logged reply +- Skip (only learn from email replies) +``` + +If yes, show each unreplied prospect one at a time and ask: positive / objection / neutral / OOO / negative / no_reply. For positives or objections, ask for a one-line note of what the prospect said. Append to `outreach-log/manual-replies.jsonl`. + +### Step 3: Compute metrics + +For the lookback window, compute per channel **and** per play (using the `campaign` field in each log row, which matches a play name): + +**The denominator is unique people enrolled, not messages sent.** A 4-touch sequence inflates message counts and makes plays incomparable. Always divide by people who got at least one message. + +``` +For each (channel, play): + enrolled = count(unique prospects with at least one outbound sent) + total_sent = sum of all outbound messages + any_reply = count(unique prospects with at least one reply of any kind) + positive_replies = count(unique prospects whose first reply was positive) + objection_replies = count(unique prospects whose first reply was objection) + neutral_replies = count(unique prospects whose first reply was neutral) + ooo_replies = count(unique prospects whose first reply was OOO) + negative_replies = count(unique prospects whose first reply was negative) + + reply_rate = any_reply / enrolled + positive_rate = positive_replies / enrolled + pos_plus_obj_rate = (positive + objection) / enrolled + ooo_filtered_rate = positive_replies / max(1, enrolled - ooo_replies) +``` + +Also compute **per touch** within the cold-email channel: +- `positives_attributable_to_touch_N` (which touch produced the first reply, by touch number) + +This tells the founder which follow-up is actually working. The common finding: touch 3 often produces more positives than touch 1. + +### Step 4: Classify what's working + +For each (channel, play) combination: + +- **Winners** (positive_rate ≥ 3% AND enrolled ≥ 15): note opener / subject / hook source / persona segment. +- **Losers** (positive_rate = 0% AND enrolled ≥ 15): candidates for retirement. +- **Surprises**: anything that contradicts the founder's stated belief from `sales-pack.md`. +- **Too early to call** (enrolled < 15): list, but flag as hypothesis-only. + +The N≥15 threshold is intentionally lower than enterprise growth teams use (N≥20 or 50). At founder volume, 15 is enough to start seeing direction without waiting forever. + +**Auto-flag high-risk subject patterns (even before N≥15).** Subjects containing any of "unlock", "10x", "accelerating", or a version number ("2.0:") are dead-on-arrival patterns the internal Cursor growth team has retired with 4,000+ sends of data behind the call. Flag these immediately in the report so the founder can rewrite before more of the sequence fires. + +**The "do not re-offer" rule.** Scan `outreach-log/*.jsonl` for prospects whose follow-up offer references an asset they already consumed (downloaded the guide, attended the webinar, read the case study). Flag these as "wasted touch" in the surprises section. The next touch should be what comes *after* the asset, not the asset again. + +### Step 5: Retirement decision for losers + +For each play that meets the retirement bar (0 positive, N≥15, ≥2 cycles run): + +``` +Question: "Play '{name}' has {N} sends, 0 positive replies, {X} OOO. This is the second cycle with these numbers. Retire?" +Options: +- Retire it (mark status: retired in plays/{name}.md) +- Iterate the messaging (run /gtm-design-play in iteration mode on this play) +- Iterate the signal or persona (start the play over from scratch) +- Keep watching for one more cycle (rare — only if there's an unusual reason) +``` + +Default recommendation: retire. Most weak plays do not get better with copy tweaks; the signal or persona is wrong. + +### Step 6: Update sales-pack.md + +Append a timestamped block to the `## Update log` section at the bottom of `sales-pack.md`: + +```markdown +### Update — 2026-05-27 (from /gtm-get-better) + +**Window:** since 2026-05-13 (14 days) +**Total enrolled across plays:** 87 +**First replies:** 14 (6 positive, 3 objections, 2 neutral, 3 OOO) +**Positive rate (people enrolled):** 6.9% +**OOO-filtered positive rate:** 7.1% + +**By play:** +| Play | Channel | Enrolled | Reply | Positive | Pos+Obj | Status | +|---|---|---|---|---|---|---| +| seed-funded-vp-eng-2026-01 | email | 38 | 18% | 10.5% | 13.2% | winner — double down | +| x-shipped-evals-2026-01 | x-dms | 24 | 16% | 8.3% | 8.3% | winner — double down | +| linkedin-ai-pms-2026-01 | li | 25 | 8% | 0% | 4% | retire after this cycle | + +**By touch (cold email):** +| Touch | Sent | Positive | % of positives | +|---|---|---|---| +| 1 | 38 | 2 | 33% | +| 2 | 32 | 3 | 50% | +| 3 | 24 | 1 | 17% | +| 4 | (not run yet) | — | — | + +**What worked:** +- Opener pattern "Saw your post on {{X}}" — 4 of 6 positives came from this. +- Signal "recent_complaining" outperformed "recent_funding" (positive_rate 9% vs 4%). +- Persona "Founding engineer at 5–25-person teams" replied at 12%; "VP Eng at 100+" at 0%. + +**What didn't:** +- Subject "{{Company}}'s developer velocity" — 0 positive on 18 sends; retiring. +- Touch 1 CTA "would love to demo" — 0 replies on plays using it. + +**Open objections heard (worth answering):** +- "We already use {competitor}" — 2 prospects; sharpen the wedge in sales-pack § Value props. +- "Tried similar things, didn't stick" — 1 prospect; consider a "small commitment" CTA option. + +**Recommended sales-pack edits:** +1. Add "Founding engineer at 5–25-person teams" to top-priority personas. +2. Strengthen the wedge vs {competitor} in § Value props. +3. Test "5-min Loom" CTA instead of "15-min call" for objection-resistant prospects. +``` + +Append-only. Don't rewrite earlier entries. + +### Step 7: Update per-channel learned files + +For any channel with new learnings, write or append `outreach-log/learned-{channel}.md`: + +```markdown +# Learned — X DMs + +Last updated: 2026-05-27 + +## Opener templates to prefer +- "Saw your post on {{X}}" — current best (positive rate 7.1%, N=24) + +## Opener templates to retire +- "Congrats on the new role" — 0 positive on N=18; retired 2026-05-27 + +## Signal source ranking (last 30 days) +1. recent_complaining (positive_rate 9%, N=24) +2. recent_funding (positive_rate 4%, N=51) +3. recent_role_change (positive_rate 0%, N=18 — retired) + +## CTAs that work +- "would a quick chat make sense?" — N=12, positive_rate 8% + +## CTAs that don't +- "would love to demo for you" — N=12, positive_rate 0%, retired +``` + +The channel skills read these files at the top of their workflow and bias drafting toward what's been working for this founder. + +### Step 8: Surface top 3 actions + +Don't drown the founder in numbers. End with three concrete next moves, in priority order: + +``` +Top 3 actions from this cycle: + +1. Double down on 'recent_complaining' as a signal source. 3x positive rate vs other signals. Re-run /gtm-find-prospects with it as the primary signal. + +2. Sharpen the wedge vs {competitor}. Two prospects raised it; you don't have a tight one-liner. Run /gtm-sales-pack and focus on § Value props § Wedge. + +3. Retire the 'linkedin-ai-pms-2026-01' play after this cycle. 0 positive on N=25, second cycle. The signal or persona is wrong; copy tweaks won't fix it. +``` + +### Step 8.5: Propose skill file edits + +The other steps update `sales-pack.md` and per-channel learned files. This step does something stronger: when a pattern is clearly winning, propose baking it into the **skill files themselves** so future runs default to it. + +**Eligibility (conservative on purpose):** + +- `N >= 20` sends backing the pattern. +- Pattern is statistically distinctive: `pattern_positive_rate >= 2x` other patterns in the same channel and persona. +- Pattern has held across at least two cycles (use `.gtm-state/skill-edit-history.jsonl` to check). +- Default is "propose, do not apply". Every change requires founder approval per edit. + +**Targets:** + +| Skill file | What gets edited | Example pattern | +|---|---|---| +| `gtm-x-outreach/SKILL.md` | Default opener template in Step 4 | "Saw your post on {{X}}" won 8 of 10 positives across 30 sends | +| `gtm-linkedin-outreach/SKILL.md` | Step 3 example block | "Hey {{name}}, your post on {{X}}" outperforms "Hey {{name}}, saw you just joined" | +| `gtm-cold-email/SKILL.md` | Subject framework table | "Re: {{Company}}'s X" converts 3x better than alternatives | +| `gtm-design-play/SKILL.md` | Offer ladder defaults | Soft Touch 1 CTA "want the playbook" beats direct ask on medium-signal plays | + +**Workflow:** + +1. Scan the metrics computed in Step 3 and the patterns surfaced in Step 4. For each pattern that meets eligibility, identify the candidate skill file edit. +2. Read the target skill file. Locate the exact section to change. Generate a unified diff proposal: + + ```diff + --- gtm-x-outreach/SKILL.md + +++ gtm-x-outreach/SKILL.md + @@ + -{{Hook line, reacts to the specific post or thread}} + +{{Hook line, reacts to the specific post or thread. Default opener template: + +"Saw your post on {{topic}}, {{one sentence of genuine reaction}}." Won 8 of 10 positives across 30 sends in this founder's history.}} + ``` + +3. Show the diff to the founder one edit at a time. AskQuestion: + + ``` + Question: "Apply this edit to gtm-x-outreach/SKILL.md? Evidence: 8 of 10 positives on N=30 across 2 cycles." + Options: + - Apply (writes the edit + adds a provenance marker) + - Skip this edit + - Apply but let me hand-tweak first (opens the file at the edit location) + - Abort all skill edits + ``` + +4. On approval, apply the edit and add a provenance marker comment right above the edited block: + + ```html + + ``` + +5. Append to `.gtm-state/skill-edit-history.jsonl`: + + ```json + { + "timestamp": "2026-05-27T17:30:00Z", + "skill_file": "skills/gtm-x-outreach/SKILL.md", + "evidence": { "pattern": "opener: Saw your post on X", "positives": 8, "sends": 30, "cycles": 2 }, + "approved": true, + "edit_summary": "Made 'Saw your post on X' the default opener template" + } + ``` + +6. If the founder skips or aborts, log the proposal anyway with `approved: false` so the next cycle does not re-propose the same edit immediately. Re-propose after one more cycle of fresh evidence. + +**Guardrails:** + +- Never edit `rules/gtm-voice-guide.mdc` from this step. The voice rule is hand-curated; copy-paste-good-patterns drift it. +- Never delete sections. Only additive edits, or replacing example text inside a section. +- Show a max of 3 edit proposals per run. More than that overwhelms the review. + +### Step 9: Persist the run + +Write `.gtm-state/last-gtm-get-better.json`: + +```json +{ + "timestamp": "2026-05-27T17:30:00Z", + "window_start": "2026-05-13T17:30:00Z", + "window_end": "2026-05-27T17:30:00Z", + "totals": { "enrolled": 87, "positive": 6, "objection": 3, "neutral": 2, "ooo": 3, "negative": 0 }, + "play_decisions": [ + { "play": "seed-funded-vp-eng-2026-01", "decision": "double_down" }, + { "play": "x-shipped-evals-2026-01", "decision": "double_down" }, + { "play": "linkedin-ai-pms-2026-01", "decision": "retire_next_cycle" } + ], + "actions": [ "...", "...", "..." ] +} +``` + +## Output format to the founder + +``` +Learning cycle complete — 14-day window. +Enrolled: 87 prospects across 3 plays. +First replies: 14 (6 positive, 3 objections, 2 neutral, 3 OOO). +Positive rate: 6.9% (up from 4.2% last cycle). + +Winners (keep running): + ✓ seed-funded-vp-eng-2026-01 — 10.5% positive + ✓ x-shipped-evals-2026-01 — 8.3% positive + +Retire candidate: + ✗ linkedin-ai-pms-2026-01 — 0% positive on N=25 + +Top 3 actions: +1. ... +2. ... +3. ... + +Detailed update appended to sales-pack.md. +Per-channel playbooks updated. +``` + +## Honest limitations + +- **X and LinkedIn replies require manual logging.** No reliable programmatic path. The skill prompts you. +- **Small samples lie.** When N<15 per play, treat findings as hypotheses, not conclusions. Flag explicitly. +- **Reply classification can drift.** Spot-check the LLM's classifications periodically. +- **Touch attribution is approximate.** "Positive after touch 3" doesn't mean touch 3 caused the positive, touches 1 and 2 set it up. + +## Frequency + +Run weekly during active outbound. Less often and the learning is stale. More often and you'll be reading noise from undersized samples. + +If the founder runs `/gtm-get-better` more than once in 24 hours without new campaign data, push back: "No new data since last run. Come back after another campaign cycle." + +## Companion skill + +After a few cycles, the founder should design new plays from the learnings (via `/gtm-design-play`) and retire old ones. This skill identifies the candidates; `/gtm-design-play` builds the next iteration. diff --git a/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md b/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md new file mode 100644 index 0000000..67a55ce --- /dev/null +++ b/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md @@ -0,0 +1,293 @@ +--- +name: gtm-linkedin-outreach +description: Run personalized LinkedIn outreach for an early-stage founder. Reads a prospects CSV from gtm-find-prospects, fetches each target's LinkedIn profile context, drafts ≤250-char connection-request notes grounded in sales-pack.md, and sends via Lemlist (primary, recommended), Amplemarket, La Growth Machine, or generates copy-paste-ready text for manual sending. Use when the founder wants to run LinkedIn outreach, wants to send connection requests at scale, runs /gtm-linkedin-outreach, or has a prospect list with linkedin_url populated. +--- + +# LinkedIn Outreach, connection notes that get accepted + +You are running a LinkedIn outreach campaign for an early-stage founder. The default channel inside LinkedIn is the **connection request with a personalized note**, capped at 300 chars by LinkedIn's UI, but the highest-performing notes are ≤250 chars. + +## Step 0: Tool and daily-limit setup (do this first) + +LinkedIn's rate limits are unforgiving and tool choice locks you in for the rest of the workflow. Settle both before any prerequisite checks. + +### 0a: Confirm the LinkedIn tool + +Read `${CURSOR_PLUGIN_ROOT}/.gtm-state/tools.json` if it exists; otherwise ask: + +``` +Question: "Which LinkedIn tool are we using? (We strongly recommend Lemlist, the cheapest at ~$59/mo and the best LinkedIn+email combo. The others work too if you already have them.)" +Options: +- Lemlist (recommended) +- Amplemarket (if you already pay for it) +- La Growth Machine (LGM) +- Manual: I'll copy-paste each note into LinkedIn myself +``` + +Persist the choice to `.gtm-state/tools.json` under key `linkedin_tool`. + +### 0b: Pick the daily connect limit + +LinkedIn caps connect requests at ~100/week before flagging; "safe" daily varies a lot by account type. Ask the founder which bucket they're in and use the table to recommend: + +| Account type | Recommended daily limit | Notes | +|---|---|---| +| New LinkedIn account (less than 6 months old) | 5 to 10 connects/day | LinkedIn aggressively rate-limits new accounts | +| Established free account | 15 to 20 connects/day | Standard. About 100/week ceiling. | +| LinkedIn Premium | 20 to 30 connects/day | Slightly higher tolerance | +| Sales Navigator | 30 to 50 connects/day plus InMail credits | Highest tolerance. Use InMails for non-1st-degree connections. | + +``` +Question: "What's your LinkedIn account type? We'll recommend a safe daily connect limit." +Options: +- New account (<6 months): recommend 8/day +- Established free account: recommend 18/day +- LinkedIn Premium: recommend 25/day +- Sales Navigator: recommend 40/day +- Override (I'll set my own number) +``` + +Save the chosen integer as `LINKEDIN_DAILY_LIMIT` in `${CURSOR_PLUGIN_ROOT}/.env`. Also write the choice to `.gtm-state/tools.json` under `linkedin_daily_limit` so other tools see the same number. + +### 0c: Per-day counter + +Maintain `${CURSOR_PLUGIN_ROOT}/.gtm-state/linkedin-connects-{YYYY-MM-DD}.json`: + +```json +{ + "date": "2026-05-27", + "limit": 18, + "sent_today": 0, + "campaign_counts": { "seed-fundraisers-2026-05": 0 } +} +``` + +This mirrors the pattern `gtm-cold-email` uses for its daily cap. Increment `sent_today` on every successful send (Lemlist add-to-campaign, Amplemarket add-to-sequence, LGM enrollment, or a manual entry the founder marks sent). **Before each send, refuse if `sent_today >= limit`** and tell the founder: "Today's LinkedIn cap of {limit} is hit. Resume tomorrow or raise the limit in `.env` if your account can handle more." + +## Prerequisites + +```bash +test -f sales-pack.md || echo "MISSING sales-pack.md" +``` + +Refuse to draft without `sales-pack.md`. + +## Tool-specific setup + +### Lemlist (recommended path) + +If first time: +1. Create account at https://app.lemlist.com (free trial). +2. Generate API key: Settings → Integrations → API → Generate. +3. Connect LinkedIn account inside Lemlist UI (Lemlist uses a cookie-based session, no LinkedIn API needed). +4. Store in `${CURSOR_PLUGIN_ROOT}/.env`: + ``` + LEMLIST_API_KEY=... + LEMLIST_TEAM_ID=... + ``` +5. Test: `curl -u :$LEMLIST_API_KEY https://api.lemlist.com/api/team` should return team JSON. + +Lemlist API reference: https://developer.lemlist.com (search for "Add lead to campaign" and "LinkedIn invitation"). + +### Amplemarket + +If first time: +1. Get API key from Settings → API. +2. Store as `AMPLEMARKET_API_KEY` in `.env`. +3. API docs: https://docs.amplemarket.com + +### La Growth Machine + +If first time: +1. Get API key from your LGM workspace settings. +2. Store as `LGM_API_KEY` in `.env`. +3. API docs: https://api-docs.lagrowthmachine.com + +### Manual + +Nothing to set up. We'll generate a markdown file with one note per prospect, formatted for fast copy-paste. + +## Step 1: Load the prospect list + +``` +Question: "Which prospect list?" +Options: +- Auto-detect (use the most recent prospects/*.csv) +- {{list discovered CSVs}} +- Paste a list inline +``` + +Filter rows where `linkedin_url` is non-empty AND (`recommended_channel` is `linkedin` OR `multi`). + +Ask for batch size, capped by the remaining daily allowance from `.gtm-state/linkedin-connects-{YYYY-MM-DD}.json` (`limit - sent_today`): + +``` +Question: "How many connection requests to draft? Today's remaining cap is {remaining} of {limit}." +Options: +- 5 (calibration) +- {remaining} (use the rest of today's allowance) +- All filtered, capped at {remaining} +``` + +If the founder asks for more than `remaining`, refuse and explain: "Your daily LinkedIn cap is {limit} (from `LINKEDIN_DAILY_LIMIT`). {sent_today} are already sent. Pick {remaining} or fewer, or re-run tomorrow." + +## Step 2: For each prospect, gather LinkedIn context + +LinkedIn has no public API. You can't programmatically fetch profile data without violating ToS or paying for enrichment tools. Workable approaches: + +- **If the founder has Amplemarket/Apollo:** they include enrichment that returns profile JSON. Use that. +- **If using Lemlist with the Lemlist Chrome extension:** Lemlist scrapes profile data when the founder adds the lead via the extension. The API can read this enriched data on the lead object. +- **If gtm-find-prospects already added profile context to the `hook` column:** use that. +- **Otherwise:** ask the founder to paste the prospect's About section + last 3 LinkedIn posts. This sounds painful but for 15 prospects it's 10 minutes and produces far better notes than scraping. + +The skill should default to the path most economical for the founder's tool stack. + +## Step 3: Draft the connection note + +Apply the `gtm-voice-guide` rule. LinkedIn-specific constraints: + +- **≤250 characters** (hard cap; LinkedIn allows 300 but tighter performs better). +- **Plain text only.** No emojis. No formatting. +- **First sentence must reference something specific**, their About, a recent post, a recent role change, a shared connection, a recent company milestone. +- **Second sentence: one-line "why you".** Connect their context to what you do, in their language. +- **Optional third: micro-CTA.** Often just "would love to connect and trade notes." + +Structure: + +``` +Hey {{firstname}}, {{hook — refers to something specific}}. + +{{One line: how that connects to what you're working on. No pitch yet — this is a connection request, not a sales email.}} + +{{Optional CTA: "Would love to connect" / "Open to swapping notes?"}} +``` + +**Examples of good ≤250 char notes:** + +> Hey Jane, your post on AI evals being the new unit tests really resonated, we're building tooling around exactly that workflow at Acme. Would love to connect and compare notes. + +> Hey Sam, saw you just joined Acme as Head of Eng, congrats. We work with a few similar Series A teams on scaling agentic coding workflows. Open to connecting? + +> Hey Pat, loved your essay on PLG-led enterprise sales. We're an AI infra co figuring out exactly that motion right now. Would be great to connect. + +**Examples of bad notes (do not produce these):** + +> Hey Jane, I came across your profile and was impressed by your background. I'd love to connect and explore opportunities. *(Generic. Says nothing specific. Sounds like a recruiter spam template.)* + +> Hi Sam, I'm building a tool that helps engineering teams 10x their velocity. Would love to demo it for you. *(Pitches in a connect request. Almost certainly declined.)* + +## Step 4: Show drafts + approval + +Per prospect, show: + +- Name, role, company, score +- The context being referenced +- The draft note + character count +- Founder choices: Send / Edit / Skip / Find a different hook + +## Step 5: Send via the chosen tool + +**Before each send**, re-check `.gtm-state/linkedin-connects-{YYYY-MM-DD}.json`. If `sent_today >= limit`, stop the loop immediately and tell the founder how many actually went out vs how many were skipped. Increment `sent_today` only after a successful Lemlist/Amplemarket/LGM API confirmation, or after the founder marks a manual entry sent. + +### Via Lemlist + +1. Add the lead to a Lemlist campaign (or create a campaign first if none exists). + ``` + POST https://api.lemlist.com/api/campaigns/{{campaignId}}/leads + { + "linkedinUrl": "...", + "firstName": "...", + "lastName": "...", + "companyName": "...", + "customFields": { "personalizedNote": "" } + } + ``` +2. The Lemlist campaign should be configured (in Lemlist UI) with a LinkedIn connection-request step that uses the `personalizedNote` custom field as the note text. + +Founders unfamiliar with Lemlist campaign setup: walk them through it once via the Lemlist UI; the skill then drives lead addition via API. + +### Via Amplemarket / LGM + +Similar pattern: add lead to a pre-built sequence; the tool handles the LinkedIn connection-send + retries. + +### Via Manual + +Generate `outreach-log/linkedin-{{campaign-name}}.md` with one entry per prospect: + +```markdown +## Jane Doe — VP Engineering at Acme +LinkedIn: https://linkedin.com/in/janedoe +Score: 85 + +> Hey Jane, your post on AI evals being the new unit tests really resonated — we're building tooling around exactly that workflow at Acme. Would love to connect and compare notes. + +[ ] Sent on: ______ +``` + +Founder opens this file, clicks each LinkedIn link, pastes the note, sends, and checks the box. Slow but free. + +## Step 6: Log the send + +Append to `outreach-log/linkedin.jsonl`: + +```json +{ + "timestamp": "2026-05-27T17:30:00Z", + "campaign": "{{campaign-name}}", + "prospect": { "name": "...", "linkedin_url": "...", "company": "...", "score": 85 }, + "context_used": "their About + their last post on AI evals", + "note": "the full note we sent", + "char_count": 234, + "tool": "lemlist", + "send_status": "queued", + "lemlist_lead_id": "..." +} +``` + +## Step 7: Schedule follow-ups (after acceptance) + +LinkedIn connection requests are just the door-opener. Real outreach is the **first message after they accept**. + +The skill should queue a Touch 2 message to be sent 1 to 2 days after acceptance. The tool (Lemlist) handles the "fires when accepted" trigger natively. For manual users, generate a follow-up draft file they can reference once a connection is accepted. + +Touch 2 template (≤500 chars, plain text): + +``` +Thanks for connecting, {{firstname}}. + +{{Brief context — one specific thing about why your work is relevant to them, building on the hook from the connect note.}} + +{{The actual offer: "Would a 15-min call make sense?" or "Want me to send a Loom showing how this works for {{similar company}}?" — pull the default CTA from sales-pack.md § "The one thing".}} + +{{Founder signature from sales-pack.md}} +``` + +## Rate limits and best practices + +- **LinkedIn caps connection requests at ~100/week.** Exceeding triggers warnings and eventual account restrictions. The Step 0 daily-limit table maps account types to safe daily numbers (5 to 10 for new accounts, up to 30 to 50 on Sales Navigator). +- **Connection acceptance rate target:** 30 to 40%. Below 25% means your notes need work (re-run with sharper hooks). +- **Lemlist's LinkedIn module limits:** ~50 connects/day per account; respect it on top of the Step 0 cap. +- **Premium account?** LinkedIn Premium / Sales Navigator gives ~150/week and InMail credits. Re-run Step 0b to raise `LINKEDIN_DAILY_LIMIT` if you upgrade mid-campaign. +- **Cap enforcement is hard, not advisory.** The per-day counter file refuses sends past the limit. If you need to raise it, edit `.env` and re-run Step 0b (don't bypass the counter). + +## Honest limitations + +- **LinkedIn has no public API for connection requests.** All tools (Lemlist, Amplemarket, LGM) work via browser automation or cookie-based session. LinkedIn can detect and ban. Use a real account, behave like a human, stay under limits. +- **Notes >300 chars get truncated by LinkedIn.** Stay under 300 always, ideally under 250. +- **Some prospects have "connection note disabled"**, you can only send a blank invite. The skill should detect this (Lemlist returns a flag) and ask the founder if they want to send a blank invite (lower acceptance rate but sometimes worth it for hot prospects). + +## Output after the run + +``` +LinkedIn campaign: {{campaign-name}} +Sent: {{N}} | Drafts saved: {{M}} | Skipped: {{X}} +Tool: {{Lemlist / Amplemarket / LGM / Manual}} +Daily cap: {{sent_today}} of {{limit}} used today. + +Top 3 hooks used: +1. Jane Doe — referenced her recent post on AI evals +2. ... + +Watch for acceptances in {{tool}} or LinkedIn. Touch 2 will auto-fire 1–2 days after acceptance. +Run /gtm-get-better in 7 days to learn from response rates. +``` diff --git a/founder-gtm/skills/gtm-playbook/SKILL.md b/founder-gtm/skills/gtm-playbook/SKILL.md new file mode 100644 index 0000000..f60b696 --- /dev/null +++ b/founder-gtm/skills/gtm-playbook/SKILL.md @@ -0,0 +1,18 @@ +--- +name: gtm-playbook +description: Opens or points to the Founder GTM canvas playbook. Use when a founder wants the visual GTM guide, asks what the plugin includes, asks to see the playbook, or wants a walkthrough before running gtm-setup. +--- + +# GTM Playbook + +Open the visual playbook at `canvases/founder-gtm-playbook.canvas.tsx`. + +If the `cursor-app-control` MCP is available, use `open_resource` to open that file in Cursor. If it is not available, tell the founder where the canvas lives and suggest running `/gtm-setup` after they skim it. + +After opening the canvas, give a short orientation: + +1. The framework is identify, resonate, time, follow up. +2. The first practical step is `/gtm-sales-pack`. +3. The plugin gets better over time through `/gtm-get-better` and the bundled automations. + +Do not summarize the whole canvas unless the founder asks. The point is to open the artifact and help them start. diff --git a/founder-gtm/skills/gtm-sales-pack/SKILL.md b/founder-gtm/skills/gtm-sales-pack/SKILL.md new file mode 100644 index 0000000..286ad8f --- /dev/null +++ b/founder-gtm/skills/gtm-sales-pack/SKILL.md @@ -0,0 +1,324 @@ +--- +name: gtm-sales-pack +description: Interviews a founder grill-me style (one question at a time, ~25 questions total) about their company, ICP, value props, common objections, persona-specific positioning, and writing voice. Produces a structured sales-pack.md knowledge base that every other founder-gtm skill (gtm-x-outreach, gtm-linkedin-outreach, gtm-cold-email, gtm-find-prospects, gtm-get-better) reads from. PREREQUISITE for all outreach skills. Use when a founder first sets up founder-gtm, when sales-pack.md is missing or stale, when the founder says they're updating their positioning, types /gtm-sales-pack, or asks for help articulating their pitch. +--- + +# Sales Pack, context collection + +You are interviewing an early-stage founder to build their **sales pack**: a single markdown file (`sales-pack.md`) that becomes the source of truth for every outbound message the `founder-gtm` plugin drafts. Without this file, every other skill produces generic AI slop. + +## What you produce + +A `sales-pack.md` file at the founder's current project root, with the exact section structure defined in the [Output template](#output-template) below. Use the template verbatim, other skills parse against these section headings. + +## How to interview + +This skill is an offshoot of `grill-me`. Same energy: ask one question at a time, walk the tree, recommend an answer when useful, but let the founder speak in their own words. Do not batch questions. Do not paraphrase their answers into bullet points without checking. + +### Three principles + +1. **One question at a time, in order.** Each question's answer informs the next. Do not show the full list up front, it overwhelms. +2. **Capture their exact words.** When they describe their product, copy their phrasing. When they describe a customer pain, write it the way they said it. The point is voice, not polish. +3. **Push for specificity.** "We help engineering teams move faster" is useless. "We help 50-engineer Series A teams ship 2x more PRs by automating code review" is usable. When you hear vague claims, ask "can you give me a specific example?" or "what number have you actually measured?". + +### Modes + +Offer the founder a choice at the start: + +- **Full mode (~25 questions, ~20 minutes)**, recommended for first run. +- **Lightning mode (~10 questions, ~7 minutes)**, for impatient founders. Produces a usable but thinner sales-pack; flag that they should re-run full mode within a week. + +## The question tree + +Walk these sections in order. Within each section, ask the questions one at a time. Skip questions if the answer is already clear from prior context. + +### Section 1: Company basics (4 questions) + +1. **One-liner.** "In one sentence, what does your company do?" If the answer is buzzwordy, push back: "Imagine you're saying this to your mom, what does it actually do?" +2. **Stage.** "Where are you, pre-seed, seed, Series A? How many engineers/employees?" +3. **Customers today.** "Name 3 real customers you have right now. (Or 3 design partners, or 3 people who actively use it.)" +4. **What problem are you solving that wasn't being solved before?** Push for the specific gap in the market, not the generic problem space. + +### Section 2: Ideal Customer Profile (5 questions) + +5. **Who is the *best* customer you have today?** Name them. Why are they your best? +6. **What did they have in common before they bought?** (Stage, team size, tech stack, role of buyer, pain point trigger.) +7. **Who is *not* a good customer for you?** What are the disqualifiers? (Just as important as who *is*, saves outreach time.) +8. **What signals indicate someone is ready to buy right now?** (Just raised, just hired a CTO, just shipped X, just hit pain Y.) +9. **Personas you sell to.** List up to 3 (e.g. "VP Eng", "Founding engineer", "Head of Growth"). For each: do they have buying power, or are they an influencer? + +### Section 3: Value props per persona (4 questions) + +10. **For your primary persona:** What are the top 3 reasons they care? Phrase each as the outcome they get, not the feature you ship. ("Cut review time from 4 days to 4 hours" ≠ "AI-powered PR review.") +11. **Proof points.** What numbers or customer outcomes can you cite with permission? List them. (Name + metric + permission level: "Brex / 45% AI-written code / OK to name publicly".) +12. **Wedge.** What's the one thing you can claim that competitors can't? +13. **Anti-pitch.** What are you not? (e.g. "We're not a Cursor replacement, we're a layer on top." This sharpens positioning.) + +### Section 4: Common objections (3 questions) + +14. **What do prospects push back on most often?** List the top 5 objections in their actual words. +15. **For each objection, what's your one-line response?** Keep it tight. +16. **What objection genuinely scares you?** The one you don't have a great answer for yet. (We'll handle this honestly in messaging.) + +### Section 5: Channels and what's worked (3 questions) + +17. **What outbound have you tried already?** (X, LinkedIn, email, in-person events, etc.) What replied? What didn't? +18. **Where do your best customers actually hang out?** (Specific subreddits, Twitter circles, Slack communities, podcasts, conferences.) +19. **What's your unfair distribution advantage?** (An accelerator batch, a VC partner's intros, a viral tweet you wrote, your background, a community you run.) Be honest if there isn't one. + +### Section 6: Voice and writing style (4 questions + 1 path picker) + +**6.0 Voice path picker (ask first).** Before the writing-sample questions, ask the founder which path they want for voice collection. The order matters: Option A is preferred when available because real sent email beats curated samples for capturing the founder's actual voice. + +``` +Question: "How do you want to capture your writing voice?" +Options: +- A) Pull from my Gmail (recommended) — reads my last 50 to 100 sent emails, extracts patterns, redacts recipients. Read-only. Requires the founder-gtm Gmail token from /gtm-cold-email setup. +- B) Paste 2 to 3 samples manually (skips Gmail) +- C) Use defaults (generic founder voice, conservative downstream) +``` + +Persist the chosen path to `.gtm-state/sales-pack-voice-path.json` so re-runs do not re-ask. + +**Option A: Pull from Gmail.** Check for the OAuth token at `${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json`. The token scopes already include `gmail.readonly` (set by `gtm-cold-email/scripts/gmail-auth.py`). If the token is missing, tell the founder one of two things: + +- If they have already run `/gtm-cold-email` setup, the token should exist; re-run `python ${CURSOR_PLUGIN_ROOT}/skills/gtm-cold-email/scripts/gmail-auth.py` to regenerate. +- If they have not run `/gtm-cold-email` setup, fall back to Option B (or set up Gmail now via the cold-email setup flow). + +Run the extractor: + +```bash +python ${CURSOR_PLUGIN_ROOT}/skills/gtm-sales-pack/scripts/extract-voice-from-gmail.py \ + --max-emails 100 --min-words 30 \ + --out .gtm-state/voice-profile.json +``` + +The script (see `scripts/extract-voice-from-gmail.py`) is read-only and idempotent. It pulls the last N sent messages, filters out auto-replies, calendar invites, and short messages (< min-words), then extracts: + +- Sentence length distribution (median, p25, p75) +- Opener capitalization habit (lowercase vs sentence case ratio) +- Punctuation tics (em-dash count, ellipsis count, exclamation count, semicolon count) +- Recurring phrases (top 10 bigrams and trigrams not in a stopword list) +- Sign-off style (the line above the signature) +- Three representative excerpts (recipient names redacted to ``) + +Write the extracted profile into the sales-pack's `## Voice` section as a structured block (template below), plus the three excerpts as quoted blocks. + +**Option B: Paste samples manually.** Ask question 20: "Show me 2 to 3 examples of writing you're proud of. (A tweet, a blog post, an email you sent that landed.) Link or paste them." Use them as voice samples in the `## Voice` section. + +**Option C: Use defaults.** Skip questions 20 to 23. Mark `voice_source: defaults` in the sales-pack frontmatter (a comment near the top) so downstream skills know to be conservative. Use this generic profile: concise, direct, lowercase casual, no corporate jargon, no em dashes, no exclamation points. + +**Then ask (regardless of path):** + +21. **How would your closest friend describe how you talk?** (Dry, intense, warm, blunt, geeky, wry.) +22. **What words or phrases do you never use?** ("Synergy", "leverage", "circle back". Get the hit list.) +23. **Email or DM signature you want at the end of every message.** + +### Section 7: The one thing (3 questions) + +24. **If a prospect remembers exactly one thing from your outreach, what should it be?** +25. **CTA for a HIGH-signal first touch** (e.g. they just complained about your problem, just raised, asked support a buying question). What do you ask them to do? (Direct call? Live demo? Free trial signup?) +26. **CTA for a LOW-signal first touch** (e.g. they just match your persona, nothing specific happened). What do you ask? (Hint: not a meeting. Usually a piece of content or a low-commitment ask.) + +Both matter. Sending a Touch 1 meeting ask to a low-signal prospect is the fastest path to a 0% reply rate. + +## Skill workflow + +### Step 0: Check for an existing sales-pack + +```bash +if [ -f sales-pack.md ]; then + cat sales-pack.md | head -3 +fi +``` + +If one exists, ask the founder whether they want to **append/update** specific sections (skip questions in sections they're not updating) or **fully redo** it. + +### Step 1: Pick mode + +Ask the founder: full vs lightning mode. Default recommendation: full. + +In lightning mode, ask only these questions: 1, 2, 3, 5, 7, 9, 10, 14, 17, 25. Ten questions, ~7 minutes. + +### Step 2: Run the interview + +One question at a time. After each answer: +- Reflect what you heard back in one sentence ("So your sharpest customer is X because of Y, got it."). +- If the answer was vague, ask one targeted follow-up before moving on. +- If the founder gives a long story, extract the 2 to 3 key facts and confirm before moving on. + +Never show the full question list. Never batch. + +### Step 3: Draft the file + +Once all questions are answered, write `sales-pack.md` using the template below. Quote the founder's exact phrasing in the "How I talk" and "Value props" sections, those are the highest-leverage parts for personalized outreach. + +### Step 4: Review with the founder + +Show them the draft. Ask: + +``` +Question: "How does this read?" +Options: +- Ship it (saves the file as-is) +- One section needs more depth (loops back to that section) +- The voice section is off (re-runs section 6) +- Start over (rare — only if positioning fundamentally shifted) +``` + +### Step 5: Save and confirm + +Save to `sales-pack.md`. Add a top-of-file note: + +```markdown +> Built with /gtm-sales-pack on YYYY-MM-DD. Re-run /gtm-sales-pack to update. +> Update notes from /gtm-get-better will append at the bottom. +``` + +Tell the founder which skill to run next (usually `gtm-find-prospects` or their first channel skill). + +## Output template + +Use this exact section structure. Other skills parse against the H2 (`##`) headings. + +```markdown +# Sales pack — {{Company Name}} + +> Built with /gtm-sales-pack on YYYY-MM-DD. Re-run /gtm-sales-pack to update. + +## One-liner + +{{Founder's one-sentence description, verbatim}} + +## Company + +- **Stage:** {{stage}} +- **Team size:** {{size}} +- **Current customers:** {{3 named}} +- **Problem we solve that wasn't being solved:** {{founder's words}} + +## ICP + +- **Best customer profile:** {{verbatim description of best customer + why}} +- **Common attributes of buyers:** {{stage, team size, tech stack, buyer role, pain trigger}} +- **Disqualifiers (who NOT to sell to):** {{founder's words}} +- **Buying signals to watch for:** {{list — these become inputs to gtm-find-prospects}} + +## Personas + +For each persona (up to 3): + +### Persona: {{Title}} + +- **Buying power:** {{yes / influencer-only}} +- **Top 3 outcomes they want:** {{list, framed as outcomes not features}} +- **Sharpest hook for this persona:** {{one sentence}} + +## Value props and proof points + +- **Wedge (one-liner we can claim that nobody else can):** {{founder's words}} +- **Top 3 outcomes overall:** {{list}} +- **Proof points (with permission tier):** + - {{Customer name}} — {{specific metric}} — {{public / private / NDA}} + +## Anti-pitch (what we're NOT) + +{{founder's words — sharpens positioning}} + +## Objections + +| Objection (in prospect's words) | One-line response | +|---|---| +| {{...}} | {{...}} | + +**Open objection (no great answer yet):** {{the scary one — flag this when it comes up in outreach so we handle honestly}} + +## Channels and prior outbound + +- **What we've tried:** {{summary}} +- **What replied:** {{patterns}} +- **What didn't:** {{patterns}} +- **Where best customers actually hang out:** {{specific communities / podcasts / subreddits / X circles}} +- **Unfair distribution advantage:** {{honest answer — leave blank if none}} + +## Voice, how I talk + +- **Source:** {{gmail | manual | defaults}} +- **Self-description:** {{dry / intense / warm / blunt / geeky / wry / etc.}} +- **Words and phrases I never use:** {{hit list}} + +### Extracted voice profile (Option A only, written by extract-voice-from-gmail.py) + +``` +sample_count: {{N}} +sentence_length: median {{X}} words, p25 {{Y}}, p75 {{Z}} +opener_capitalization: lowercase {{A}}% / sentence_case {{B}}% +punctuation_tics: em_dash {{N}}, ellipsis {{N}}, exclamation {{N}}, semicolon {{N}} +recurring_phrases: + - "{{phrase 1}}" ({{count}}) + - "{{phrase 2}}" ({{count}}) + - "{{phrase 3}}" ({{count}}) +sign_off_pattern: "{{most common sign-off line}}" +``` + +- **Voice samples (Option A pulls 3 from Gmail, recipients redacted; Option B pastes 2 to 3 manually):** + > {{sample 1}} + + > {{sample 2}} + + > {{sample 3}} +- **Signature for outbound:** {{exact signature block}} + +## The one thing + +- **What should every prospect remember:** {{single sentence}} +- **CTA for high-signal first touch:** {{verbatim, single sentence}} +- **CTA for low-signal first touch:** {{verbatim, single sentence — usually content, not a meeting}} + +## Features by buyer need + +Map each feature you'd mention to the buyer need it satisfies. When a prospect's signal indicates a specific need (cost control, security review, hiring scale, velocity), the channel skills will pull from the matching row. + +| Buyer need | What we mention | Proof / customer (with permission tier) | +|---|---|---| +| Cost control | {{features}} | {{customer + metric}} | +| Security / compliance | {{features}} | {{customer + metric}} | +| Visibility / analytics | {{features}} | {{customer + metric}} | +| Velocity / productivity | {{features}} | {{customer + metric}} | +| Onboarding / rollout | {{features}} | {{customer + metric}} | + +## Signal strength cheat sheet + +Your buying signals from § ICP, ranked by strength. The gtm-cold-email and gtm-design-play skills read this to match the right CTA to the right signal. + +| Signal | Strength | Detection method | Default Touch 1 CTA | +|---|---|---|---| +| {{e.g. SSO support request}} | High | Support tickets matching keywords | "Want me to set up a trial?" | +| {{e.g. Recent fundraise}} | Medium | TechCrunch RSS + LinkedIn check | "Happy to send a playbook" | +| {{e.g. Matches persona only}} | Low | Title + company match | "Thought you'd find this useful" — content only | + +## Update log + + +``` + +## Quality bar before you save + +Before writing the file, sanity check: + +- [ ] No buzzwords sneaked into the founder's value props (no "revolutionary", "game-changing") +- [ ] Every proof point has a real customer name + a real number + a permission level +- [ ] The voice section has at least one paste of the founder's actual writing (not just adjectives) +- [ ] The CTA section names exactly one CTA, not a menu +- [ ] The ICP section is specific enough that `gtm-find-prospects` could turn it into a query + +If any of those fail, ask one more clarifying question before saving. + +## Notes for downstream skills + +Other skills read `sales-pack.md` like this: +- `gtm-find-prospects` parses the `## ICP` and `## Personas` sections to build target criteria. +- `gtm-x-outreach`, `gtm-linkedin-outreach`, `gtm-cold-email` parse `## Value props`, `## Personas`, `## Voice`, and `## The one thing` to draft messages. +- `gtm-get-better` appends notes to the `## Update log` section based on observed reply patterns. + +Keep this contract stable. If you add new sections, add them after `## The one thing` and before `## Update log`. diff --git a/founder-gtm/skills/gtm-sales-pack/scripts/extract-voice-from-gmail.py b/founder-gtm/skills/gtm-sales-pack/scripts/extract-voice-from-gmail.py new file mode 100644 index 0000000..daf6ef0 --- /dev/null +++ b/founder-gtm/skills/gtm-sales-pack/scripts/extract-voice-from-gmail.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +"""Extract a founder's writing voice profile from their last N sent Gmail messages. + +Reuses the OAuth token created by `gtm-cold-email/scripts/gmail-auth.py` +(scopes include `gmail.readonly`). Read-only and idempotent: safe to run +repeatedly. Recipient names in excerpts are redacted to "". + +Usage: + python extract-voice-from-gmail.py [--max-emails 100] [--min-words 30] + [--out .gtm-state/voice-profile.json] + +Prerequisites: + pip install google-auth google-api-python-client + +Reads: + $CURSOR_PLUGIN_ROOT/.gtm-state/gmail-token.json + $CURSOR_PLUGIN_ROOT/.gtm-state/oauth-client.json + (used only to refresh the access token if it has expired) + +Writes: + The JSON path passed via --out (default: .gtm-state/voice-profile.json + in the current working directory). + +What the profile contains: + sample_count, sentence_length (median/p25/p75), opener_capitalization + (lowercase vs sentence case ratio), punctuation_tics (em dash, en dash, + ellipsis, exclamation, semicolon), recurring_phrases (top bigrams and + trigrams, stopword-filtered), sign_off_pattern, excerpts (3 redacted). +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import os +import re +import statistics +import sys +from collections import Counter +from pathlib import Path +from typing import Any + +try: + from google.oauth2.credentials import Credentials + from google.auth.transport.requests import Request + from googleapiclient.discovery import build + from googleapiclient.errors import HttpError +except ImportError as e: + raise SystemExit( + "Missing dependency. Run:\n" + " pip install google-auth google-api-python-client" + ) from e + +PLUGIN_ROOT = Path(os.environ.get("CURSOR_PLUGIN_ROOT", Path(__file__).resolve().parents[3])) +STATE_DIR = PLUGIN_ROOT / ".gtm-state" +TOKEN_FILE = STATE_DIR / "gmail-token.json" +CLIENT_FILE = STATE_DIR / "oauth-client.json" + +SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"] + +STOPWORDS = set( + """ + a an the and or but if so to of in on at for with from as is was were be been + being have has had do does did this that these those i you he she it we they + me him her us them my your his its our their what when where which who whom how + why all any both each few more most other some such no nor not only own same + than too very can will just don dont can't won't isn aren't shouldn wouldn + about into between through during before after above below up down out off + over under again further then once here there because while + """.split() +) + +AUTO_REPLY_SUBJECT_RE = re.compile( + r"(out of office|ooo|auto-?reply|automatic reply|\[auto reply\]|vacation)", + re.IGNORECASE, +) +CALENDAR_SUBJECT_RE = re.compile( + r"(invitation:|updated invitation:|canceled event:|declined:|accepted:|tentative:)", + re.IGNORECASE, +) + + +def load_credentials() -> Credentials: + if not TOKEN_FILE.exists(): + raise SystemExit( + f"Missing {TOKEN_FILE}.\n" + "Run the cold-email Gmail bootstrap once first:\n" + f" python {PLUGIN_ROOT / 'skills/gtm-cold-email/scripts/gmail-auth.py'}" + ) + creds = Credentials.from_authorized_user_file(str(TOKEN_FILE), SCOPES) + if creds.expired and creds.refresh_token: + creds.refresh(Request()) + TOKEN_FILE.write_text(creds.to_json()) + os.chmod(TOKEN_FILE, 0o600) + return creds + + +def fetch_sent_messages(service: Any, max_emails: int) -> list[dict[str, Any]]: + results: list[dict[str, Any]] = [] + page_token: str | None = None + while len(results) < max_emails: + page_size = min(100, max_emails - len(results)) + resp = ( + service.users() + .messages() + .list( + userId="me", + labelIds=["SENT"], + maxResults=page_size, + pageToken=page_token, + ) + .execute() + ) + for msg in resp.get("messages", []): + try: + full = ( + service.users() + .messages() + .get(userId="me", id=msg["id"], format="full") + .execute() + ) + except HttpError: + continue + results.append(full) + if len(results) >= max_emails: + break + page_token = resp.get("nextPageToken") + if not page_token: + break + return results + + +def decode_body(payload: dict[str, Any]) -> str: + if payload.get("mimeType") == "text/plain" and payload.get("body", {}).get("data"): + return base64.urlsafe_b64decode(payload["body"]["data"]).decode( + "utf-8", errors="replace" + ) + for part in payload.get("parts", []) or []: + text = decode_body(part) + if text: + return text + return "" + + +def strip_quoted(body: str) -> str: + lines: list[str] = [] + for line in body.splitlines(): + if line.startswith(">"): + break + if re.match(r"^On .* wrote:$", line.strip()): + break + if re.match(r"^From: .+$", line.strip()): + break + lines.append(line) + return "\n".join(lines).strip() + + +def get_header(headers: list[dict[str, str]], name: str) -> str: + name = name.lower() + for h in headers: + if h.get("name", "").lower() == name: + return h.get("value", "") + return "" + + +def split_sentences(text: str) -> list[str]: + parts = re.split(r"(?<=[.!?])\s+(?=[A-Za-z])", text.strip()) + return [p.strip() for p in parts if p.strip()] + + +def tokenize(text: str) -> list[str]: + return [t.lower() for t in re.findall(r"[A-Za-z']+", text)] + + +def ngrams(tokens: list[str], n: int) -> list[tuple[str, ...]]: + return [tuple(tokens[i : i + n]) for i in range(len(tokens) - n + 1)] + + +def is_stopwordy(gram: tuple[str, ...]) -> bool: + return all(t in STOPWORDS for t in gram) + + +def redact_recipient(text: str, to_header: str) -> str: + if not to_header: + return text + name_match = re.match(r"\s*\"?([^\"<]+?)\"?\s*<", to_header) + name = name_match.group(1).strip() if name_match else to_header.split("@")[0] + first = name.split()[0] if name else "" + if first and len(first) >= 2: + text = re.sub(rf"\b{re.escape(first)}\b", "", text) + return text + + +def extract_sign_off(body: str) -> str: + lines = [l.strip() for l in body.splitlines() if l.strip()] + if len(lines) < 2: + return "" + for candidate in reversed(lines[:-1][-5:]): + if 1 <= len(candidate.split()) <= 5 and candidate.endswith((",", ".", "")): + return candidate + return lines[-2] if len(lines) >= 2 else "" + + +def analyze(messages: list[dict[str, Any]], min_words: int) -> dict[str, Any]: + kept: list[dict[str, Any]] = [] + for msg in messages: + payload = msg.get("payload", {}) + headers = payload.get("headers", []) + if get_header(headers, "Auto-Submitted") and get_header( + headers, "Auto-Submitted" + ).lower() != "no": + continue + subject = get_header(headers, "Subject") + if AUTO_REPLY_SUBJECT_RE.search(subject) or CALENDAR_SUBJECT_RE.search(subject): + continue + body = strip_quoted(decode_body(payload)) + if not body or len(body.split()) < min_words: + continue + kept.append( + { + "subject": subject, + "to": get_header(headers, "To"), + "body": body, + } + ) + + sentence_lengths: list[int] = [] + lowercase_openers = 0 + sentence_case_openers = 0 + punctuation = Counter() + all_tokens: list[str] = [] + sign_offs: Counter = Counter() + + for m in kept: + for s in split_sentences(m["body"]): + words = s.split() + if not words: + continue + sentence_lengths.append(len(words)) + if words[0][:1].islower(): + lowercase_openers += 1 + else: + sentence_case_openers += 1 + punctuation["em_dash"] += m["body"].count("\u2014") + punctuation["en_dash"] += m["body"].count("\u2013") + punctuation["ellipsis"] += m["body"].count("...") + m["body"].count("\u2026") + punctuation["exclamation"] += m["body"].count("!") + punctuation["semicolon"] += m["body"].count(";") + all_tokens.extend(tokenize(m["body"])) + so = extract_sign_off(m["body"]) + if so: + sign_offs[so] += 1 + + bigrams = Counter(g for g in ngrams(all_tokens, 2) if not is_stopwordy(g)) + trigrams = Counter(g for g in ngrams(all_tokens, 3) if not is_stopwordy(g)) + + excerpts: list[str] = [] + for m in kept: + if len(excerpts) >= 3: + break + text = m["body"] + text = redact_recipient(text, m["to"]) + text = re.sub(r"\s+", " ", text).strip() + if len(text) > 400: + text = text[:397] + "..." + excerpts.append(text) + + total_openers = lowercase_openers + sentence_case_openers + lowercase_pct = round(100 * lowercase_openers / total_openers, 1) if total_openers else 0.0 + sentence_pct = round(100 * sentence_case_openers / total_openers, 1) if total_openers else 0.0 + + profile: dict[str, Any] = { + "sample_count": len(kept), + "sentence_length": { + "median": int(statistics.median(sentence_lengths)) if sentence_lengths else 0, + "p25": int(_percentile(sentence_lengths, 25)) if sentence_lengths else 0, + "p75": int(_percentile(sentence_lengths, 75)) if sentence_lengths else 0, + }, + "opener_capitalization": { + "lowercase_pct": lowercase_pct, + "sentence_case_pct": sentence_pct, + }, + "punctuation_tics": dict(punctuation), + "recurring_phrases": { + "bigrams": [ + {"phrase": " ".join(g), "count": c} + for g, c in bigrams.most_common(10) + if c >= 2 + ], + "trigrams": [ + {"phrase": " ".join(g), "count": c} + for g, c in trigrams.most_common(10) + if c >= 2 + ], + }, + "sign_off_pattern": sign_offs.most_common(1)[0][0] if sign_offs else "", + "excerpts": excerpts, + } + return profile + + +def _percentile(values: list[int], pct: float) -> float: + if not values: + return 0.0 + s = sorted(values) + k = (len(s) - 1) * pct / 100 + f = int(k) + c = min(f + 1, len(s) - 1) + if f == c: + return float(s[f]) + return s[f] + (s[c] - s[f]) * (k - f) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--max-emails", type=int, default=100) + parser.add_argument("--min-words", type=int, default=30) + parser.add_argument( + "--out", + type=str, + default=str(Path(".gtm-state") / "voice-profile.json"), + ) + args = parser.parse_args() + + creds = load_credentials() + service = build("gmail", "v1", credentials=creds, cache_discovery=False) + messages = fetch_sent_messages(service, args.max_emails) + if not messages: + print("No sent messages found.", file=sys.stderr) + sys.exit(1) + profile = analyze(messages, args.min_words) + + out_path = Path(args.out) + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(profile, indent=2)) + print(f"Voice profile written to {out_path}") + print(f"Samples kept: {profile['sample_count']}") + + +if __name__ == "__main__": + main() diff --git a/founder-gtm/skills/gtm-sales-pack/scripts/requirements.txt b/founder-gtm/skills/gtm-sales-pack/scripts/requirements.txt new file mode 100644 index 0000000..cf4bf99 --- /dev/null +++ b/founder-gtm/skills/gtm-sales-pack/scripts/requirements.txt @@ -0,0 +1,3 @@ +google-auth-oauthlib>=1.2 +google-api-python-client>=2.130 +google-auth>=2.30 diff --git a/founder-gtm/skills/gtm-setup/SKILL.md b/founder-gtm/skills/gtm-setup/SKILL.md new file mode 100644 index 0000000..72f16db --- /dev/null +++ b/founder-gtm/skills/gtm-setup/SKILL.md @@ -0,0 +1,171 @@ +--- +name: gtm-setup +description: First-run orchestrator for the founder-gtm plugin. Walks an early-stage founder through plugin setup in the right order, gtm-sales-pack first, then the channels they want to use (X DMs, LinkedIn via Lemlist, cold email via Gmail), then the targeting and learning skills. Use when a founder first installs founder-gtm, types /gtm-setup, asks how to start, asks what to do next, or seems lost about which skill to run first. +--- + +# GTM Setup, first-run orchestrator + +You are walking a brand new early-stage founder through setup of the `founder-gtm` plugin. They probably installed it 5 minutes ago. Be concise, opinionated, and ask one decision at a time. + +## Why this skill exists + +The plugin has 6 other skills and they have a real dependency order. If a founder runs `gtm-x-outreach` before `gtm-sales-pack`, the messages will be generic and the campaign will fail. Walking them through the right sequence is the difference between this plugin working and not. + +## Setup order (this is the canonical sequence) + +``` +1. gtm-sales-pack ← always first, no exceptions +2. gtm-find-prospects ← optional but recommended; sets up data sources for targeting +3. Pick channel(s): gtm-x-outreach, gtm-linkedin-outreach, gtm-cold-email +4. Run first campaign on one channel +5. gtm-get-better ← after first campaign has had ~1 week to collect replies +6. gtm-design-play ← when ready to systematize what worked into named, repeatable motions +7. (optional) install the automations under automations/ so /gtm-get-better and reply-checks run on their own +``` + +## Workflow + +### Step 1: Check whether sales-pack exists + +Before doing anything else, check whether `sales-pack.md` exists in the founder's current project root (or wherever they keep their GTM state). + +```bash +ls sales-pack.md 2>/dev/null && echo "EXISTS" || echo "MISSING" +``` + +- **If MISSING** → Tell the founder: "Before any outreach, we need 20 minutes to build your sales pack. This is a knowledge base every other skill in the plugin reads from. Without it, your messages will be generic. Running it now." Then invoke the `gtm-sales-pack` skill. +- **If EXISTS** → Skim it briefly to confirm it has the required sections (company, ICP, value props, objections, voice). If any are missing or feel thin, ask the founder if they want to re-run `gtm-sales-pack` to fill the gaps. Otherwise move on. + +### Step 2: Ask which channels the founder wants to run + +Use the AskQuestion tool. Multi-select. + +``` +Question: "Which outbound channels do you want to set up? (Pick all that apply — you can always add more later.)" +Options: +- X DMs (uses the xmcp MCP server; great for tech founders with active X presence) +- LinkedIn connection requests (uses Lemlist; cheapest and highest-volume LinkedIn path) +- Cold email (uses Gmail via Google Workspace CLI; safest for established domains) +``` + +### Step 3: For each chosen channel, run its setup section + +Each channel has its own setup checklist below. Walk through them **one at a time**, not all at once. Finish channel 1 before starting channel 2. + +After each channel's setup is complete, ask the founder if they want to **run a small test campaign on that channel right now** (5 to 10 prospects) before setting up the next channel. The fastest way to learn is to actually send messages. + +#### X DMs setup checklist + +``` +- [ ] Confirm xmcp is running: + curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/mcp + Expect 200/405/406. If connection refused, run ~/dev/xmcp/start.sh + (first run opens a browser for OAuth1 consent — founder must approve once). +- [ ] Confirm Cursor's MCP settings show `x-api` as connected. + If offline, toggle off/on in Settings → MCP. +- [ ] Note that X API DM sends are pay-per-call beyond the free tier. + Recommend the founder check https://console.x.com for their current plan + remaining credits. +- [ ] Optional: install the `xdk` Python SDK for programmatic batch sends. +``` + +If xmcp is not installed at all, tell the founder honestly: "The X channel needs the local xmcp MCP server set up first. That's a one-time install, see https://github.com/xdevplatform/xmcp. Want me to walk you through it, or skip X for now and set up another channel?" + +#### LinkedIn setup checklist + +``` +- [ ] Ask which LinkedIn tool the founder has or wants to use: + • Lemlist (recommended — ~$59/mo, best LinkedIn+email combo, our default) + • Amplemarket (more expensive but powerful if they already have it) + • La Growth Machine (alternative) + • Manual copy-paste (free, scales to ~50 connects/week) +- [ ] If Lemlist: + • Founder creates account at https://app.lemlist.com (free trial available) + • Generates API key at Settings → Integrations → API + • Stores in ${CURSOR_PLUGIN_ROOT}/.env as LEMLIST_API_KEY=... + • Connects their LinkedIn account inside Lemlist (uses cookie-based session) +- [ ] If Amplemarket or LGM: similar pattern; the gtm-linkedin-outreach skill walks them through. +- [ ] If Manual: nothing to install; skill will generate copy-paste-ready connect notes. +- [ ] LinkedIn connection request limit: 100–200/week before LinkedIn flags the account. + Set a daily cap in the skill config (default 20/day). +``` + +#### Cold email setup checklist + +``` +- [ ] Confirm Google Workspace account (Gmail must be a Workspace account, not personal gmail.com — Workspace gives you the API + better deliverability). +- [ ] Install gcloud CLI if missing: + brew install --cask google-cloud-sdk +- [ ] Authenticate: + gcloud auth login + gcloud auth application-default login +- [ ] Enable the Gmail API on a GCP project: + gcloud services enable gmail.googleapis.com +- [ ] Create an OAuth client + grant Gmail send/draft scopes: + The gtm-cold-email skill walks through this; output is a token file + stored at ${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json (gitignored). +- [ ] Domain warming check: + Ask the founder: "Has this sending domain sent >50 cold emails before?" + • Yes → safe to start at 25/day cap. + • No → strongly recommend warming the domain for 2+ weeks first. + Recommend: Instantly (~$37/mo) or Mailwarm (~$69/mo) or free option: + Smartlead's free warmup tier (https://www.smartlead.ai). + If founder skips warming, lower the daily cap to 5/day for the first 2 weeks. +- [ ] Pick send mode for first campaign: + • Drafts only (founder reviews each, clicks send manually) — safest for first run. + • Programmatic send with hard daily cap (default 25/day, configurable). +``` + +### Step 4: Recommend setting up gtm-find-prospects next + +Once at least one channel is configured, suggest running `gtm-find-prospects` to build the first target list. Don't force it, some founders already have a target list from another source (a spreadsheet, an export from a tool, a list of accelerator batchmates). Ask: + +``` +Question: "Do you already have a list of people you want to reach out to, or do you want me to help build one?" +Options: +- I have a list (CSV, spreadsheet, or names I'll paste in) +- Build one with me (run gtm-find-prospects) +- Skip targeting; I'll just message a few people I already know +``` + +### Step 5: Run the first campaign + +For whichever channel the founder picked first, hand off to that channel's skill (`gtm-x-outreach`, `gtm-linkedin-outreach`, or `gtm-cold-email`) with the prospect list. Walk them through their **first 5 messages personally**, don't auto-batch yet. The point of the first 5 is for the founder to feel the quality bar and adjust the sales pack or voice before scaling. + +### Step 6: Schedule the gtm-get-better loop + +After the first campaign goes out, tell the founder: + +> "Come back in 7 days and run `/gtm-get-better`. It'll read your responses, score what worked, and update your sales-pack and per-channel playbooks. The plugin gets sharper every cycle, that's the whole point of running it from Cursor instead of a fixed SaaS tool." + +Optionally, offer to install the three Cursor Automations shipped at `automations/`: + +- `weekly-get-better.workflow.json`, Mondays at 7am PT, runs the learning loop. +- `daily-followups.workflow.json`, weekdays at 9am PT, checks for replies and advances the cold-email queue. +- `post-campaign-debrief.workflow.json`, manual trigger; campaign-scoped learning report. + +The easy install path: ask the agent to "install the founder-gtm automations from the installed Founder GTM plugin's `automations/` folder", it uses the `cursor-app-control` MCP's `open_automation` tool to prefill each one. See `automations/AUTOMATIONS.md` for details. + +## Founder-friendly summary at the end + +When setup is complete, give the founder a single concise summary: + +``` +Setup complete. Here's your stack: + +📋 Sales pack: sales-pack.md (re-run `/gtm-sales-pack` anytime to update) +🎯 Targeting: [tools the founder connected] +📡 Channels live: [list] +🔁 Next: Run /gtm-x-outreach, /gtm-linkedin-outreach, or /gtm-cold-email with a target list +📈 Weekly: Run /gtm-get-better to compound learnings +``` + +## Common stumbling blocks + +- **Founder skips gtm-sales-pack**, refuse, gently. Explain that without it every other skill produces generic AI slop. Offer to do a 10-minute lightning version if they're impatient (the gtm-sales-pack skill has a quick mode). +- **Founder wants to send to 500 people on day 1**, talk them down. Cap first campaign at 25 to 50. The first batch is for calibration, not volume. +- **Founder has no domain warmed for cold email**, never let them blast a cold domain at 25/day on day 1. Drop the cap to 5/day or push them to warm first. +- **Founder hates the drafts the AI produces**, that's diagnostic of a thin sales pack or unclear voice. Run `gtm-sales-pack` again with focus on the "voice" and "how I talk about the product" sections. + +## Output style + +Be concise. Use checklists. Use the founder's first name once you know it. Treat this like onboarding a friend, not running a script, pause for questions, adapt to what they say. diff --git a/founder-gtm/skills/gtm-warm-intro/SKILL.md b/founder-gtm/skills/gtm-warm-intro/SKILL.md new file mode 100644 index 0000000..72bdf37 --- /dev/null +++ b/founder-gtm/skills/gtm-warm-intro/SKILL.md @@ -0,0 +1,247 @@ +--- +name: gtm-warm-intro +description: Run warm-intro outreach for an early-stage founder. Reads the founder's LinkedIn connections CSV export, cross-references against a prospects/*.csv from gtm-find-prospects, finds 1st-degree bridges at each prospect's company, ranks intro candidates by connection strength and recency, drafts a request message to the bridge person plus a forwardable blurb the bridge can paste verbatim, and either sends via Gmail (if email path) or generates copy-paste markdown (for Slack or other channels). Warm intros convert at 5 to 10x cold. Use when the founder wants to leverage their network, mentions warm intros or referrals, runs /gtm-warm-intro, or after gtm-find-prospects has produced a target list. +--- + +# Warm Intro, the highest-leverage outbound channel + +Warm intros convert at 5 to 10x the rate of cold outreach. The single highest-leverage move in early-stage outbound is asking a mutual connection for an intro, and giving that connection a forwardable blurb so they have zero work to do. + +This skill turns the founder's existing LinkedIn network into an intro pipeline. + +## Why this exists + +Cold email and DM are mass channels. Warm intros are surgical. A founder typically has 200 to 2000 LinkedIn 1st-degree connections, and any of them might bridge to a target prospect. The bottleneck is knowing **who** to ask, **how** to ask, and giving them a paragraph they can paste straight into a forward. + +Most founders skip warm intros because the manual work is painful: matching prospects to connections, drafting a request that respects the bridge's time, writing a forwardable blurb. This skill does all of that. + +## Prerequisites + +```bash +test -f sales-pack.md || echo "MISSING sales-pack.md" +ls prospects/*.csv 2>/dev/null | head -1 || echo "MISSING prospects CSV (run /gtm-find-prospects first)" +test -f ${CURSOR_PLUGIN_ROOT}/.gtm-state/linkedin-connections.csv || echo "MISSING LinkedIn connections export" +``` + +Refuse to draft without `sales-pack.md` and a prospect list. + +## Step 0: Get the LinkedIn connections export (one-time) + +LinkedIn lets the founder export their full 1st-degree connection list with one click. Walk them through it: + +``` +1. LinkedIn.com → Me (top-right avatar) → Settings & Privacy +2. Data Privacy (left sidebar) → "Get a copy of your data" +3. Pick "Want something in particular?" → check "Connections" only (not the full archive, which takes 24 hours) +4. Request archive. LinkedIn emails a download link in about 10 minutes for connections-only. +5. Download the zip, unzip, find Connections.csv inside. +6. Move it to ${CURSOR_PLUGIN_ROOT}/.gtm-state/linkedin-connections.csv +``` + +The CSV columns LinkedIn provides: `First Name, Last Name, URL, Email Address, Company, Position, Connected On`. + +If the founder already exported once, the file is reusable until they want to refresh (LinkedIn allows the export anytime). + +## Workflow + +### Step 1: Load the prospect list + +``` +Question: "Which prospect list are we running warm intros on?" +Options: +- Auto-detect (most recent prospects/*.csv) +- {{list discovered CSVs}} +- Paste a list of company + person rows inline +``` + +Load the CSV. Required columns for matching: `company` (always). Helpful columns: `full_name`, `linkedin_url`, `role`. + +### Step 2: Match each prospect to bridge candidates + +For each prospect row: + +1. Normalize the prospect's `company` (lowercase, strip "Inc"/"LLC"/"Ltd", collapse whitespace). +2. Scan the LinkedIn connections CSV. A bridge candidate is a 1st-degree connection whose `Company` normalizes to the same value. +3. Collect all bridge candidates per prospect. A prospect with 0 bridges is dropped from this run (suggest cold channels for that one instead). + +Output a tally to the founder: + +``` +Found bridges for 14 of 47 prospects: + • 6 prospects with exactly 1 bridge + • 5 prospects with 2 to 3 bridges + • 3 prospects with 4+ bridges (pick the strongest) +``` + +The 33 prospects with no bridge stay on the prospect list for cold outreach via the other channel skills. + +### Step 3: Rank bridge candidates per prospect + +For each (prospect, bridge_candidate) pair, score the bridge: + +| Factor | Weight | How to score | +|---|---|---| +| Recency of last interaction | 35 | Read from Gmail if available: search sent + received for the bridge's email, score by days since last message (≤30 = 35, ≤90 = 22, ≤365 = 12, older = 4). If no Gmail, ask the founder per bridge: "When did you last talk to {{bridge name}}?" | +| Connection strength | 30 | Heuristic: count mutual LinkedIn engagements (likes, comments) if the founder can paste a few examples. If unknown, default to 18 for "in the same circle" and 30 for "I would call them a friend" (ask the founder for the top 3 only). | +| Shared school or company | 20 | If the CSV `Position` history shows overlap with the founder's background (founder provides), or the bridge's `Company` was a prior employer of the founder. Use 20 for overlap, 0 otherwise. | +| Tenure at target company | 15 | Bridges who have been at the target company longer (look at "Connected On" as a weak proxy if no other data) score higher. Default 8. | + +Pick the **highest-scoring bridge** per prospect. Tie-break by most recent interaction. + +### Step 4: Pick the channel for the bridge ask + +Ask the founder once per bridge (or once globally if a clear preference exists): + +``` +Question: "How will you reach {{bridge name}}?" +Options: +- Email (drafts via Gmail, sent via gmail-token.json if the founder has cold-email set up) +- Slack DM (generate copy-paste markdown) +- iMessage / WhatsApp / signal (generate copy-paste text) +- LinkedIn DM (generate copy-paste text) +- Skip this bridge +``` + +Persist per-bridge channel choices to `.gtm-state/warm-intro-channel-prefs.json` (keyed by the bridge's LinkedIn URL) so re-runs do not re-ask. + +### Step 5: Draft the intro-request message to the bridge + +Apply `gtm-voice-guide`. Constraints: + +- Short: 4 to 6 sentences for email; 2 to 3 sentences for Slack/iMessage. +- Make the ask explicit: "would you be open to introducing me to {{prospect_name}} at {{company}}?" +- Acknowledge their time: offer the forwardable blurb so the bridge does no writing. +- Give them an easy out: "totally fine if not a fit or you do not know them well." + +**Email format (4 to 6 sentences):** + +``` +Subject: quick ask, intro to {{prospect first name}} at {{company}}? + +Hey {{bridge first name}}, + +{{One-sentence personal touch, reference last interaction or recent thing they did, do not fake it if there isn't one}}. + +I am trying to get in front of {{prospect_name}} at {{company}}. {{One sentence on why now: signal, fit, etc.}}. + +Any chance you would be open to an intro? I have written a forwardable blurb below so you would not have to write anything, just paste and send. + +Either way, no pressure if it is not a fit or you do not know them well. + +{{founder signature from sales-pack.md}} + +--- +Forwardable blurb (paste into a fresh email to {{prospect_name}}): + +{{blurb, see Step 6}} +``` + +**Slack-style format (2 to 3 sentences):** + +``` +hey {{bridge first name}}, random ask: do you know {{prospect_name}} at {{company}} well enough to intro me? working on {{one phrase from sales-pack one-liner}} and they look like an exact fit. happy to send you a paragraph you can just paste, no writing needed on your end. total no worries if it is awkward. +``` + +### Step 6: Write the forwardable blurb + +This is the high-leverage move. The bridge should be able to copy the blurb into a fresh email to the prospect and hit send with no edits. + +Template (3 to 4 sentences): + +``` +{{prospect first name}}, meet {{founder first name}}, founder of {{company}}. {{One sentence on what the company does, lifted verbatim from sales-pack.md § One-liner}}. {{One sentence on the specific reason they should care: the signal from the prospect row, or the persona-fit reason}}. {{founder first name}} can take it from here, leaving you two to it. +``` + +Quality bar before saving: + +- Reads like the bridge wrote it (warm, casual). +- Says exactly what the company does in one sentence (no "AI-powered platform that..."). +- Mentions one concrete reason the prospect should care, not generic. +- Ends with the polite "leaving you two to it" or similar handoff phrase. + +### Step 7: Show drafts and approve per bridge + +Per bridge, display: + +- Bridge name, current role + company, last interaction (if known), score +- Target prospect, the signal, why this bridge for this prospect +- The intro-request message (channel-formatted) +- The forwardable blurb + +Ask: + +``` +Question: "Send this intro request to {{bridge_name}}?" +Options: +- Send (Gmail) or copy-paste-ready (other channels) +- Edit the request, then send +- Edit the blurb, then send +- Skip this bridge, try the next-best one for the same prospect +- Skip this prospect entirely +``` + +### Step 8: Send or hand off + +**Email path:** if the founder has run `/gtm-cold-email` setup (Gmail token at `${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json`), send via the Gmail API using the existing token. Subject and body as drafted. No tracking pixels. Plain text. + +**Other channels:** write `outreach-log/warm-intros-pending.md` with one section per pending intro, channel-formatted for copy-paste. Tell the founder to send and then re-run `/gtm-warm-intro --mark-sent` to log them. + +### Step 9: Log to outreach-log/warm-intros.jsonl + +For every intro request the founder sends (or marks sent), append one line: + +```json +{ + "timestamp": "2026-05-27T17:30:00Z", + "campaign": "{{campaign-name}}", + "bridge": { + "name": "Alex Rivera", + "linkedin_url": "https://linkedin.com/in/alexrivera", + "company": "Acme", + "channel": "email" + }, + "prospect": { + "name": "Jane Doe", + "company": "Acme", + "role": "VP Engineering", + "linkedin_url": "https://linkedin.com/in/janedoe" + }, + "request_message": "the full request we sent", + "forwardable_blurb": "the full blurb", + "gmail_message_id": "186abc..." +} +``` + +`gtm-get-better` reads this file and credits warm-intro positives against the bridges who said yes (and the bridges who never replied), so the founder learns which bridges are real intro paths vs the polite-but-flaky ones. + +## Output to the founder + +``` +Warm intro campaign: {{campaign-name}} + +Prospects with bridges: {{14}} of {{47}} +Intro requests sent: {{N}} +Intro requests copy-paste-ready (Slack/iMessage/LinkedIn): {{M}} + +Top 3 bridges by score: +1. Alex Rivera (Acme) → Jane Doe (VP Eng): last talked 12 days ago, mutual investor connection +2. ... + +Reminder: warm intros convert 5 to 10x cold. Reply with whatever the bridge sends back (yes, no, "let me think") and run /gtm-warm-intro --mark-sent if you reached out via Slack or iMessage so the log stays in sync. + +Run /gtm-get-better next week to see which bridges actually converted. +``` + +## Honest limitations + +- **LinkedIn connection CSVs go stale.** Re-export every 60 to 90 days, or sooner if the founder has connected with many people recently. +- **Bridge ranking is a heuristic.** The founder usually knows their network better than a script. Always show the top 3 candidates per prospect and let them override. +- **Some bridges will ghost.** Track non-response in `warm-intros.jsonl`; bridges who never reply after 2 asks should be flagged as low-yield. +- **Do not abuse the same bridge.** Cap intro requests per bridge at 1 per quarter unless the bridge volunteers more. The skill enforces this by reading the log before drafting. +- **No personal-email matching.** LinkedIn's CSV only includes the email the bridge chose to share with you (often empty). Email sends require the bridge's email to be in the CSV or in the founder's Gmail history. + +## Companion skills + +- `/gtm-find-prospects` produces the prospect list this skill consumes. +- `/gtm-cold-email` handles prospects who have no bridge. +- `/gtm-get-better` reads `outreach-log/warm-intros.jsonl` for compounding the learnings. diff --git a/founder-gtm/skills/gtm-x-outreach/SKILL.md b/founder-gtm/skills/gtm-x-outreach/SKILL.md new file mode 100644 index 0000000..96ba98e --- /dev/null +++ b/founder-gtm/skills/gtm-x-outreach/SKILL.md @@ -0,0 +1,215 @@ +--- +name: gtm-x-outreach +description: Run personalized X (Twitter) DM outreach for an early-stage founder. Reads a prospects CSV produced by gtm-find-prospects, fetches each target's last 10 to 20 posts via the local xmcp MCP server, identifies a real hook in their recent thinking, drafts a personalized DM grounded in the founder's sales-pack.md, and either saves drafts for review or sends them with rate limiting. Use when the founder wants to run X DM outreach, says they want to message specific X users, runs /gtm-x-outreach, or has a prospect list with x_handle populated. +--- + +# X Outreach, personalized DMs at founder scale + +You are running an X DM outreach campaign for an early-stage founder. Every DM must be grounded in something real the target recently said. Generic "saw your work, would love to chat" gets ignored. + +## Prerequisites + +Check all three before doing anything: + +```bash +# 1. sales-pack.md exists +test -f sales-pack.md || echo "MISSING sales-pack.md" + +# 2. xmcp MCP server is up +curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/mcp +# Expect 200/405/406. If connection refused, run ~/dev/xmcp/start.sh and have the founder approve OAuth in browser. + +# 3. The x-api skill at ~/.cursor/skills/x-api/SKILL.md is loaded (it has the xmcp tool reference) +``` + +If any prereq fails, fix it before proceeding. Refuse to draft messages without `sales-pack.md`. + +**Read `~/.cursor/skills/x-api/SKILL.md`** for the xmcp tool naming convention and the `body`-wrapping pattern. This is the most common source of 400 errors. + +## Workflow + +### Step 1: Load inputs + +``` +Question: "Which prospect list are we running?" +Options: +- Auto-detect (use the most recent prospects/*.csv) +- {{list discovered CSVs}} +- Paste a list inline (founder will paste names + handles) +``` + +Filter the loaded CSV to rows where `x_handle` is non-empty AND (`recommended_channel` is `x` OR `multi`). + +Show the founder the filtered count and ask: + +``` +Question: "Found {{N}} X-reachable prospects. Run on all of them, top 5, or pick manually?" +Options: +- Top 5 (calibration — recommended for first campaign) +- Top 20 +- All {{N}} +- Pick manually (I'll show you the list) +``` + +### Step 2: Pick send mode + +``` +Question: "How do you want to handle sending?" +Options: +- Drafts only — I'll show each draft, you approve/send manually via X +- Send with my approval — I'll draft, ask for explicit "send" per message +- Auto-send with rate limit — drafts and sends with delay between messages (recommended cap: 10/day for new accounts, up to 50/day for warmed) +``` + +For first-time use, default-recommend "Drafts only" or "Send with my approval". Auto-send only after the founder has shipped 2 to 3 campaigns and is confident in quality. + +### Step 3: For each prospect, research + +Per prospect, in order of `score` descending: + +1. **Resolve user ID.** Call xmcp `getUsersByUsername` with `{"username": ""}`. If 404, mark the row as "handle invalid" and skip. + +2. **Pull recent timeline.** Call xmcp `getUsersIdTweets` (the user timeline tool) with `{"id": "", "max_results": 20, "tweet.fields": "created_at,public_metrics,referenced_tweets"}`. Take the last 10 to 20 posts. + + Fallback if the tool name differs in this xmcp version: use `searchPostsRecent` with `{"query": "from:", "max_results": 20}`. + +3. **Find the hook.** Scan the posts and pick exactly one to reference. Prefer in this order: + - A post where they articulate a problem your product solves. + - A post where they share an opinion you genuinely engage with. + - A post about a recent ship/launch/hire/raise relevant to them. + - A thread on a topic in your sales-pack's domain. + + Reject as hooks: posts older than 30 days (stale), retweets without comment, replies to others (out of context), engagement-bait threads, anything political/personal unless directly relevant. + + If no good hook exists, mark the prospect as "no recent hook" and downgrade to LinkedIn or email instead, do not send a generic DM. + +### Step 4: Draft the DM + +Apply the `gtm-voice-guide` rule (it's always-applied while this plugin is active). Specifically: + +- **Length:** ≤500 characters. Tighter is better. The X DM input box is small. +- **Open with the hook**, not yourself. +- **One CTA**, phrased as a question, on its own line at the end. +- **Voice match**: use the founder's voice samples from `sales-pack.md` § "Voice, how I talk". + +Use this structure: + +``` +{{Hook line — reacts to the specific post or thread}} + +{{One sentence connecting their thinking to what your product does or who you serve. Do NOT pitch the product in detail.}} + +{{CTA — single question. Often: "would a quick chat make sense?" or "want me to send you a Loom?" or "would it be useful to swap notes?"}} +``` + +Reference the post the hook is from at the end if natural ("the thread on X"). Don't paste URLs. + +### Step 5: Show the draft + +Display the draft alongside: + +- Target name + handle + score +- The specific post the hook references (so the founder can validate it's a real reference) +- Sales pack value prop being implied (so they can sanity-check fit) +- Character count + +Ask the founder: + +``` +Question: "Send this one?" +Options: +- Send +- Edit, then send (founder gives revised text) +- Skip — bad fit +- Skip — bad hook, find a different post for this person +- Abort campaign (stop the loop) +``` + +### Step 6: Send via xmcp (if approved) + +Call xmcp `createDirectMessagesByParticipantId` with the `body`-wrapping convention: + +```json +{ + "participant_id": "", + "body": { + "text": "" + } +} +``` + +**Common errors:** +- 400 `$.text: is missing` → you forgot to wrap under `body`. See the x-api skill. +- 403 → the recipient doesn't accept DMs from non-followers, or your account doesn't have DM permission. Mark the row and skip; suggest LinkedIn for this prospect instead. +- 429 → rate-limited. Pause; resume after the reset window. + +### Step 7: Log the send + +Append to `outreach-log/x-dms.jsonl` (one line per send): + +```json +{ + "timestamp": "2026-05-27T17:30:00Z", + "campaign": "{{campaign-name}}", + "prospect": { + "name": "Jane Doe", + "handle": "@janedoe", + "user_id": "1234567890", + "company": "Acme", + "score": 85 + }, + "hook_post_id": "1700000000000000000", + "hook_post_excerpt": "first 140 chars of the post we referenced", + "message": "the full message we sent", + "char_count": 312, + "send_status": "sent", + "send_response_id": "DM ID returned by xmcp", + "founder_action": "send" +} +``` + +The `gtm-get-better` skill reads this log to learn what hooks work. + +### Step 8: Rate limit + follow-up scheduling + +- Default delay between sends: 30 to 90 seconds (randomized). Helps avoid spam-flag heuristics. +- Default daily cap: 10 DMs/day for accounts with <1000 followers, 25/day for >1000, 50/day for >10k. +- Schedule follow-ups: after 4 days with no reply, queue a Touch 2. After 8 days, Touch 3. After 14, breakup. The `gtm-get-better` skill or a Cursor Automation can drive these. + +Tell the founder: "Follow-ups are queued in `outreach-log/x-followups-pending.jsonl`. Re-run `/gtm-x-outreach --followups` to send the next round." + +## What X DMs are great for and bad for + +**Great for:** +- Tech founders with an active X presence (matching peers DM each other naturally on X). +- Devtools, AI, infra, design tools companies whose buyer persona lives on X. +- Hot-take or recent-event-driven outreach where speed matters. +- People who explicitly say "DMs open" or have a Calendly in their bio. + +**Bad for:** +- B2B enterprise buyers (most don't check X DMs). +- People who don't follow you (your DM may go to the "Requests" inbox and get missed). +- Anything that requires formal tone or written depth, use email instead. + +If the founder's ICP is mostly enterprise procurement, gently push them toward cold email or LinkedIn instead. + +## Honest limitations + +- **You pay per DM beyond the X API free tier.** Check `console.x.com` for current pricing. As of recent pricing, expect ~$200/mo for Basic tier with thousands of DMs. +- **xmcp holds OAuth1 in memory only.** Restart = re-consent. Don't restart mid-campaign. +- **One X account per xmcp process.** Sending from a different account requires editing `~/dev/xmcp/.env` and restarting. +- **X aggressively suppresses spam-pattern accounts.** Even with good messages, a brand-new low-follower account doing 50 DMs/day will get rate-limited or shadow-banned. Warm the account by posting and replying genuinely for 2 to 4 weeks before bulk DMing. + +## Output to the founder after the run + +``` +X DM campaign: {{campaign-name}} +Sent: {{N}} | Drafts saved: {{M}} | Skipped: {{X}} + +Top 3 hooks used (founder can study these): +1. Jane Doe — referenced her post on AI evals: "evals are the new unit tests" +2. ... + +Follow-up Touch 2 queued for {{date}} ({{N}} prospects). +Replies will arrive in your X DMs inbox. Run /gtm-get-better in 7 days. +``` From 37ff000a11b2f521624eee869323c4b0a60fd0e5 Mon Sep 17 00:00:00 2001 From: Shub Gaur Date: Wed, 27 May 2026 22:41:34 -0700 Subject: [PATCH 2/2] founder-gtm: address Bugbot findings Co-authored-by: Cursor --- founder-gtm/automations/daily-followups.md | 2 +- founder-gtm/skills/gtm-cold-email/SKILL.md | 6 +++--- founder-gtm/skills/gtm-find-prospects/SKILL.md | 13 +++++++------ founder-gtm/skills/gtm-linkedin-outreach/SKILL.md | 4 ++-- founder-gtm/skills/gtm-playbook/SKILL.md | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/founder-gtm/automations/daily-followups.md b/founder-gtm/automations/daily-followups.md index 8a53acb..28f808f 100644 --- a/founder-gtm/automations/daily-followups.md +++ b/founder-gtm/automations/daily-followups.md @@ -7,7 +7,7 @@ | Field | Value | |---|---| | **Name** | daily-followups | -| **Description** | Every weekday at 7am, run `/gtm-cold-email --followups` so Touch 2 / 3 / 4 messages go out on cadence. Honors the daily send cap from `.env` and the per-thread reply state so anyone who already replied gets dropped from the queue. | +| **Description** | Every weekday at 7am, run `/gtm-cold-email --followups` so Touch 2 / 3 / 4 messages go out on cadence. Honors the daily send cap from `${CURSOR_PLUGIN_ROOT}/.env` and the per-thread reply state so anyone who already replied gets dropped from the queue. | | **Trigger** | Cron: `0 7 * * 1-5` (weekdays 7am local) | | **Tools** | None required. Cold-email uses the Gmail OAuth token stored at `${CURSOR_PLUGIN_ROOT}/.gtm-state/gmail-token.json`. | | **Optional tools** | `slack` — to ping you if the daily cap was hit or if any send failed (e.g. bounce). | diff --git a/founder-gtm/skills/gtm-cold-email/SKILL.md b/founder-gtm/skills/gtm-cold-email/SKILL.md index fc988c0..bc3f805 100644 --- a/founder-gtm/skills/gtm-cold-email/SKILL.md +++ b/founder-gtm/skills/gtm-cold-email/SKILL.md @@ -134,14 +134,14 @@ Options: - Auto-send with cap — sends up to {{daily-cap}} today, spaced 2–5 minutes apart ``` -Daily cap default: 25. Configurable via `.env`: +Daily cap default: 25. Configurable via `${CURSOR_PLUGIN_ROOT}/.env`: ``` COLD_EMAIL_DAILY_CAP=25 COLD_EMAIL_MIN_INTERVAL_SECONDS=120 COLD_EMAIL_MAX_INTERVAL_SECONDS=300 ``` -Track today's sent count in `.gtm-state/send-counter-{{YYYY-MM-DD}}.json`. Refuse to exceed the cap. +Track today's sent count in `${CURSOR_PLUGIN_ROOT}/.gtm-state/send-counter-{{YYYY-MM-DD}}.json`. Refuse to exceed the cap. ### Step 3: For each prospect, draft the sequence @@ -259,7 +259,7 @@ Each send uses the Gmail API's `users.messages.send`. The skill's helper script - Encoding the message as RFC 2822 + base64url. - Threading: Steps 2 to 4 use the same Message-ID `In-Reply-To` so they thread under Step 1 in the recipient's inbox. - Spacing: enforce `COLD_EMAIL_MIN_INTERVAL_SECONDS` between sends in auto-send mode. -- Cap enforcement: increment `.gtm-state/send-counter-{{date}}.json`; refuse to exceed cap. +- Cap enforcement: increment `${CURSOR_PLUGIN_ROOT}/.gtm-state/send-counter-{{date}}.json`; refuse to exceed cap. Drafts mode: use `users.drafts.create` instead. Drafts appear in the founder's Gmail Drafts folder; threading still works on send. diff --git a/founder-gtm/skills/gtm-find-prospects/SKILL.md b/founder-gtm/skills/gtm-find-prospects/SKILL.md index 6be5233..c4b8acd 100644 --- a/founder-gtm/skills/gtm-find-prospects/SKILL.md +++ b/founder-gtm/skills/gtm-find-prospects/SKILL.md @@ -247,14 +247,15 @@ Once you have a `role` column populated, run the title classifier: ```bash python ${CURSOR_PLUGIN_ROOT}/skills/gtm-find-prospects/scripts/title-classifier.py \ --input prospects/raw.csv \ - --output prospects/classified.csv + --out prospects/classified.csv ``` -This adds three columns based on keyword lists in `data/title-keywords.txt` and `data/title-exclusions.txt`: +This adds four columns based on keyword lists in `data/title-keywords.txt` and `data/title-exclusions.txt`: -- `title_is_tech_leader`, true if the role matches the leadership + technology rules -- `title_is_mid_tier`, true for senior IC titles (Staff, Principal, Tech Lead) -- `title_excluded_reason`, populated when an exclusion matched (e.g. "excluded:sales engineer", "excluded:chief of staff") +- `persona_bucket`, the first matching persona bucket from `data/title-keywords.txt` (e.g. `cto`, `vp_engineering`, `staff_principal_engineer`) +- `persona_confidence`, `high` when 2+ keywords matched, `medium` when 1 keyword matched, blank when no persona matched +- `matched_keywords`, the pipe-separated keywords that matched +- `exclusion_reason`, populated when an exclusion matched (e.g. `sales engineer`, `chief of staff`) Edit the data files to fit your ICP. The defaults are tuned for "VP+ engineering at startups" and exclude common look-alikes like Director of Sales Engineering, Chief of Staff, Customer Success leaders, and hardware engineers. @@ -312,7 +313,7 @@ Score each candidate 0 to 100: | Factor | Weight | How to score | |---|---|---| | Signal strength | 35 | High (just funded, just complained about your problem) = 35. Medium (recently changed roles, recently shipped) = 22. Low (matches persona) = 8. | -| Persona fit | 30 | `title_is_tech_leader` true and exact persona match = 30. Tech leader, adjacent persona = 18. Mid-tier IC at right company = 12. | +| Persona fit | 30 | `persona_bucket` matches the target persona and `persona_confidence` is high or medium = 30. Adjacent engineering leadership bucket = 18. `staff_principal_engineer` or `founding_engineer` at the right company = 12. Rows with `exclusion_reason` populated should usually be dropped before ranking. | | Reachability | 15 | Email confirmed + LinkedIn + X = 15. Two of three = 9. One = 4. | | Warm path | 10 | Accelerator batchmate, shared connection, mutual follow, shared work history = 10. Otherwise 0. | | Play priors | 10 | Bump up if the signal type has worked for this founder before (read from `outreach-log/learned-*.md`). Defaults to 0 for first campaign. | diff --git a/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md b/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md index 67a55ce..4b7b8bb 100644 --- a/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md +++ b/founder-gtm/skills/gtm-linkedin-outreach/SKILL.md @@ -24,7 +24,7 @@ Options: - Manual: I'll copy-paste each note into LinkedIn myself ``` -Persist the choice to `.gtm-state/tools.json` under key `linkedin_tool`. +Persist the choice to `${CURSOR_PLUGIN_ROOT}/.gtm-state/tools.json` under key `linkedin_tool`. ### 0b: Pick the daily connect limit @@ -47,7 +47,7 @@ Options: - Override (I'll set my own number) ``` -Save the chosen integer as `LINKEDIN_DAILY_LIMIT` in `${CURSOR_PLUGIN_ROOT}/.env`. Also write the choice to `.gtm-state/tools.json` under `linkedin_daily_limit` so other tools see the same number. +Save the chosen integer as `LINKEDIN_DAILY_LIMIT` in `${CURSOR_PLUGIN_ROOT}/.env`. Also write the choice to `${CURSOR_PLUGIN_ROOT}/.gtm-state/tools.json` under `linkedin_daily_limit` so other tools see the same number. ### 0c: Per-day counter diff --git a/founder-gtm/skills/gtm-playbook/SKILL.md b/founder-gtm/skills/gtm-playbook/SKILL.md index f60b696..31cea07 100644 --- a/founder-gtm/skills/gtm-playbook/SKILL.md +++ b/founder-gtm/skills/gtm-playbook/SKILL.md @@ -5,9 +5,9 @@ description: Opens or points to the Founder GTM canvas playbook. Use when a foun # GTM Playbook -Open the visual playbook at `canvases/founder-gtm-playbook.canvas.tsx`. +Open the visual playbook at `${CURSOR_PLUGIN_ROOT}/canvases/founder-gtm-playbook.canvas.tsx`. -If the `cursor-app-control` MCP is available, use `open_resource` to open that file in Cursor. If it is not available, tell the founder where the canvas lives and suggest running `/gtm-setup` after they skim it. +If the `cursor-app-control` MCP is available, use `open_resource` with that absolute plugin-root path. Do not open `canvases/founder-gtm-playbook.canvas.tsx` as a workspace-relative path; after Marketplace install, the canvas lives inside the installed plugin, not necessarily in the founder's current project. If `cursor-app-control` is not available, tell the founder where the canvas lives and suggest running `/gtm-setup` after they skim it. After opening the canvas, give a short orientation: