# Signal-Based LinkedIn Outreach — Full Playbook

## What This Skill Does

Replaces $200+/mo outreach stacks (Apollo, Instantly, Lemlist) with a signal-based pipeline that scrapes commenters from viral LinkedIn posts, auto-scores them against your ICP, enriches the best, and runs personalized outreach with 15-20% reply rates.

The core insight: people who comment on competitor posts about AI tools have **already raised their hand**. They're not cold — they self-selected as interested. You just need to find them, filter them, and reach them with more value than they asked for.

## Prerequisites

- Node.js 18+
- An Apify account (free tier works for small runs, ~$1.50/1K comments)
- Claude Code (for scoring, enrichment, and outreach orchestration)
- LinkedIn account (Premium recommended for connection notes at scale)
- Optional: Relevance AI account (for the comment scraper tool wrapper)

## The 5-Step Pipeline

### Step 1: Find Farm Posts

Look for LinkedIn creators who comment-gate resources ("Comment X and I'll DM you"). Their commenters are self-selecting as interested buyers.

**What to look for:**
- Posts with 500+ comments
- Comment-gating patterns ("comment GUIDE", "type INTERESTED", "drop a 🔥")
- Topics that match your ICP's pain points (AI tools, marketing automation, agency growth)

**How to find them:**
```
Search Google: "[creator name]" site:linkedin.com/posts "[topic]" 2026
Example: "Ira Bodnar" site:linkedin.com/posts "AI ads" OR "Google Ads"
```

**Extract the Activity ID from any LinkedIn post URL:**
```
https://www.linkedin.com/posts/bodnarira_some-slug-activity-7416777474472267776-Kh0n
                                                              ↑ This number
```

### Step 2: Scrape Every Commenter

```typescript
// scrape-comments.ts — Scrape all commenters from a LinkedIn post via Apify
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: process.env.APIFY_API_KEY });

interface Commenter {
  name: string;
  headline: string;
  linkedin_url: string;
  comment_text: string;
}

async function scrapePostComments(activityId: string): Promise<Commenter[]> {
  const postUrl = `https://www.linkedin.com/feed/update/urn:li:activity:${activityId}/`;

  console.log(`Scraping comments from: ${postUrl}`);

  const run = await client.actor("apify/linkedin-post-comments-scraper").call({
    postUrls: [postUrl],
    maxComments: 1000,
  });

  const { items } = await client.dataset(run.defaultDatasetId).listItems();

  const commenters: Commenter[] = items.map((item: any) => ({
    name: item.authorName || item.commenterName || "",
    headline: item.authorHeadline || item.commenterHeadline || "",
    linkedin_url: item.authorProfileUrl || item.commenterProfileUrl || "",
    comment_text: item.commentText || item.text || "",
  }));

  // Deduplicate by LinkedIn URL
  const seen = new Set<string>();
  const unique = commenters.filter((c) => {
    if (!c.linkedin_url || seen.has(c.linkedin_url)) return false;
    seen.add(c.linkedin_url);
    return true;
  });

  console.log(`✓ ${items.length} comments → ${unique.length} unique leads`);
  return unique;
}

// Usage
const leads = await scrapePostComments("7416777474472267776");
```

### Step 3: Auto-Score by Headline

Zero-cost ICP filtering using pure keyword matching. No AI calls needed.

```typescript
// score-leads.ts — Auto-score leads by headline keywords

interface ScoredLead {
  name: string;
  headline: string;
  linkedin_url: string;
  comment_text: string;
  auto_score: number;
  score_reasons: string[];
}

const SERVICE_SIGNALS = [
  "agency", "consulting", "founder", "ceo", "owner", "director",
  "co-founder", "managing director", "principal", "partner",
  "coaching", "recruitment", "real estate", "studio", "firm",
];

const NON_TECH_SIGNALS = [
  "marketing", "sales", "growth", "brand", "creative", "design",
  "content", "ads", "media", "pr", "seo", "social",
];

const TECH_SIGNALS = [
  "developer", "engineer", "cto", "devops", "data scientist",
  "programmer", "full stack", "backend", "frontend", "software engineer",
  "machine learning",
];

const DISQUALIFIERS = [
  "student", "looking for", "seeking", "open to work", "intern",
  "aspiring", "fresher", "graduate",
];

function autoScore(headline: string): { score: number; reasons: string[] } {
  const h = headline.toLowerCase();
  let score = 0;
  const reasons: string[] = [];

  for (const kw of SERVICE_SIGNALS) {
    if (h.includes(kw)) {
      score += 2;
      reasons.push(`+2 service:${kw}`);
    }
  }
  for (const kw of NON_TECH_SIGNALS) {
    if (h.includes(kw)) {
      score += 1;
      reasons.push(`+1 nontech:${kw}`);
    }
  }
  for (const kw of TECH_SIGNALS) {
    if (h.includes(kw)) {
      score -= 3;
      reasons.push(`-3 tech:${kw}`);
    }
  }
  for (const kw of DISQUALIFIERS) {
    if (h.includes(kw)) {
      score -= 5;
      reasons.push(`-5 disqualify:${kw}`);
    }
  }

  return { score, reasons };
}

function scoreLeads(leads: Commenter[]): ScoredLead[] {
  return leads
    .map((lead) => {
      const { score, reasons } = autoScore(lead.headline);
      return { ...lead, auto_score: score, score_reasons: reasons };
    })
    .sort((a, b) => b.auto_score - a.auto_score);
}

// Triage tiers
// 5+  → Likely ICP — web search to confirm
// 2-4 → Maybe ICP — enrich if capacity allows
// <2  → Not ICP — skip
```

### Step 4: Web Enrich the Best

For each lead scoring 5+, search the web to confirm they're actually ICP.

```typescript
// enrich-leads.ts — Web enrichment for high-scoring leads

interface EnrichedLead extends ScoredLead {
  company: string;
  company_url: string;
  location: string;
  industry: string;
  team_size: string;
  icp_score: number;
  score_breakdown: string;
  enrichment: string;
  why: string;
}

// ICP Scoring Rubric (out of 100)
// Title match (Founder, CEO, Owner)     +20
// Industry match (agency, consulting)   +20
// Company size (2-50 people)            +15
// Target location (US, UK, CA, AU, NZ)  +10
// Non-technical bonus                   +15
// SaaS spend signal (HubSpot, etc.)     +10
// Engagement bonus (has email/phone)    +10

// Hard Disqualifiers (→ score 0):
// - Enterprise employees (not decision makers)
// - SaaS/product companies (not service businesses)
// - Companies >50 people (slow procurement)
// - Too technical (would clone and DIY)
// - DevOps/infra engineers

async function enrichLead(
  lead: ScoredLead,
  webSearch: (query: string) => Promise<string>
): Promise<EnrichedLead | null> {
  const query = `"${lead.name}" ${lead.headline.split(" at ").pop() || ""} founder`;
  const searchResult = await webSearch(query);

  // Parse search results to extract company info
  // Claude Code handles this naturally — just describe what you want:
  // "Search for this person, find their company, confirm they're a founder,
  //  check company size and location, score against our ICP criteria"

  // Return null for hard disqualifiers
  // Return enriched lead with icp_score for confirmed leads
  return null; // Implement with your preferred web search tool
}
```

### Step 5: Signal-Based Outreach

The key insight: **never reference the post they commented on**. Speak to their intent.

```typescript
// outreach.ts — Generate personalized connection requests

interface OutreachDraft {
  linkedin_url: string;
  slug: string;
  note: string; // 300 char max for LinkedIn
  lead: EnrichedLead;
}

function generateConnectionNote(lead: EnrichedLead): string {
  // The 10x hook: they wanted 1 resource → you offer 10
  // If they commented on an AI ads post → offer 10 AI ad skills
  // If they commented on a marketing post → offer 10 marketing tools

  const templates = [
    `Hi ${lead.name.split(" ")[0]}, I built a guide for ${lead.company} on replacing your agency stack with Claude Code — CRM, ads, SEO, proposals, lead gen, content, outreach. Would love your take as a ${lead.industry} founder.`,
    `Hi ${lead.name.split(" ")[0]}, I built 10 AI marketing skills for agencies like ${lead.company} — ads, SEO, content, analytics, outreach. They're free and open source. Want me to send them over?`,
    `Hi ${lead.name.split(" ")[0]}, noticed you're running ${lead.company} — I mapped out how agencies your size can replace $500/mo in SaaS with one repo. Happy to share if useful.`,
  ];

  // Pick template and ensure under 300 chars
  const note = templates[0];
  return note.length <= 300 ? note : note.substring(0, 297) + "...";
}

// Outreach sequence (after connection accepted):
// Day 0: Connection request with hook
// Day 1 (on accept): Send lead magnet / skill file
// Day 7: Value drop — relevant case study or resource
// Day 14: Value drop — tip relevant to their niche
// Day 21: Soft CTA — "Want me to build this for {{company}}? 15 min call."
// On reply: Pause sequence, notify human
```

## Putting It All Together

```typescript
// pipeline.ts — Full signal-based outreach pipeline
import { writeFileSync, readFileSync, existsSync } from "fs";

async function runPipeline(activityId: string, options: {
  minAutoScore?: number;
  maxEnrich?: number;
  dryRun?: boolean;
} = {}) {
  const { minAutoScore = 5, maxEnrich = 50, dryRun = true } = options;

  console.log("═══ Signal-Based Outreach Pipeline ═══\n");

  // Step 1: Scrape
  console.log("→ Step 1: Scraping comments...");
  const leads = await scrapePostComments(activityId);
  console.log(`  ✓ ${leads.length} unique leads\n`);

  // Step 2: Score
  console.log("→ Step 2: Auto-scoring...");
  const scored = scoreLeads(leads);
  const tier1 = scored.filter((l) => l.auto_score >= minAutoScore);
  const tier2 = scored.filter((l) => l.auto_score >= 2 && l.auto_score < minAutoScore);
  const notIcp = scored.filter((l) => l.auto_score < 2);
  console.log(`  ✓ ${tier1.length} likely ICP (${minAutoScore}+)`);
  console.log(`  ✓ ${tier2.length} maybe ICP (2-${minAutoScore - 1})`);
  console.log(`  ✓ ${notIcp.length} not ICP (<2)\n`);

  // Step 3: Enrich top leads
  console.log(`→ Step 3: Enriching top ${Math.min(tier1.length, maxEnrich)}...`);
  // const enriched = await enrichBatch(tier1.slice(0, maxEnrich));
  // console.log(`  ✓ ${enriched.confirmed.length} confirmed ICP`);
  // console.log(`  ✓ ${enriched.disqualified.length} disqualified\n`);

  // Step 4: Generate outreach
  console.log("→ Step 4: Drafting connection requests...");
  // const drafts = enriched.confirmed.map(generateConnectionNote);
  // console.log(`  ✓ ${drafts.length} drafts ready\n`);

  // Save results
  const outputFile = `signal-outreach-${activityId}.json`;
  writeFileSync(outputFile, JSON.stringify({
    activityId,
    scraped: leads.length,
    scored: { tier1: tier1.length, tier2: tier2.length, notIcp: notIcp.length },
    leads: scored,
  }, null, 2));
  console.log(`✓ Results saved to ${outputFile}`);

  if (dryRun) {
    console.log("\n⚠ DRY RUN — no connection requests sent");
    console.log("  Remove --dry-run to send for real");
  }
}

// Run: npx tsx pipeline.ts 7416777474472267776 --dry-run
const activityId = process.argv[2];
if (!activityId) {
  console.error("Usage: npx tsx pipeline.ts <activity-id> [--dry-run]");
  process.exit(1);
}
const dryRun = process.argv.includes("--dry-run");
runPipeline(activityId, { dryRun });
```

## Environment Variables

```bash
# Required
APIFY_API_KEY=your_apify_api_key_here

# Optional (for CRM integration)
DATABASE_URL=postgresql://user:pass@host:5432/dbname

# Optional (for Relevance AI comment scraper wrapper)
RELEVANCE_API_KEY=your_relevance_api_key_here
RELEVANCE_PROJECT=your_project_id
```

## ICP Scoring Reference

### Auto-Score (Headline Keywords — Zero Cost)

| Signal Type | Keywords | Points |
|------------|----------|--------|
| Service business | agency, founder, CEO, owner, director, consulting, partner | +2 each |
| Non-technical | marketing, sales, growth, brand, creative, ads, SEO | +1 each |
| Technical (negative) | developer, engineer, CTO, devops, data scientist | -3 each |
| Disqualifier | student, intern, seeking, open to work, aspiring | -5 each |

### ICP Score (After Web Enrichment — /100)

| Signal | Points |
|--------|--------|
| Title: Founder, CEO, Owner | +20 |
| Industry: Agency, consulting, services | +20 |
| Company size: 2-50 people | +15 |
| Location: US, UK, CA, AU, NZ | +10 |
| Non-technical (can't DIY) | +15 |
| SaaS spend signals (HubSpot, etc.) | +10 |
| Reachable (email, phone visible) | +10 |

## Farm Poster Yield Data (Real Results)

| Poster | Audience Type | ICP Yield | Best For |
|--------|--------------|-----------|----------|
| Lara Acosta | Personal branders, coaches | ~10.5% | Content/brand agencies |
| Ruben Hassid | AI content creators | ~10.7% | Content/marketing agencies |
| Alex Vacca | B2B sales/GTM founders | ~6.4% | Outreach/lead gen agencies |
| Pietro Montaldo | B2B founders, growth ops | ~5.3% | GTM consultants |
| Ira Bodnar | Agency owners, media buyers | ~5% | Paid ads agencies |
| Jacob Bank | SaaS founders, marketers | ~4.7% | Marketing agency founders |

## Results From Real Campaign

| Metric | Count |
|--------|-------|
| Total comments scraped | 7,870 |
| Unique leads | ~6,935 |
| Auto-scored as likely ICP | 557 |
| Web-enriched | 776 |
| **Confirmed 8/10+ leads** | **200** |
| Enrichment confirmation rate | 26% |
| **Outreach reply rate** | **15-20%** |
| **Total cost in outreach tools** | **$0** |

## Key Rules

1. **Never say "saw you commented on X's post"** — that's creepy
2. **Speak to intent, not behavior** — the resource they wanted IS the signal
3. **Always dry-run first** before sending real connection requests
4. **20-30 connection requests/day max** — LinkedIn rate limit
5. **Log everything to CRM** — source attribution on every contact
6. **The 10x hook** — they wanted 1 resource, offer them 10
