ai-marketing-skills/sales-pipeline/trigger_prospector.py
Alfred Claw a96d0d8889 Initial commit: 6 AI marketing skill categories
- growth-engine: Autonomous experiment engine (Karpathy autoresearch for marketing)
- sales-pipeline: RB2B router, deal resurrector, trigger prospector, ICP learner
- content-ops: Expert panel, quality gate, editorial brain, quote miner
- outbound-engine: Cold outbound optimizer, lead pipeline, competitive monitor
- seo-ops: Content attack briefs, GSC optimizer, trend scout
- finance-ops: CFO briefing, cost estimate, scenario modeler

79 files, all sanitized - zero hardcoded credentials or internal references.
2026-03-27 20:14:52 -07:00

410 lines
17 KiB
Python

#!/usr/bin/env python3
"""
Trigger-Based Prospecting Engine
Monitors job postings, new hires, and funding signals to identify
companies where new marketing leaders are evaluating agency/vendor relationships.
Searches across multiple signal categories:
- New CMO/VP Marketing hires (leadership change = budget reallocation)
- Marketing leadership job postings (team building = growth mode)
- Agency search signals (active evaluation)
- Funding rounds (capital to deploy on growth)
Each signal is scored, enriched with industry/size estimates, and paired
with a personalized outreach hook and email draft.
Usage:
python3 trigger_prospector.py --days 7 --top 15 --min-score 50
Requires: BRAVE_API_KEY environment variable
"""
import argparse
import json
import os
import random
import re
import sys
from datetime import datetime, timedelta
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen
# ─── Configuration ───────────────────────────────────────────────────────────
BASE_DIR = Path(os.environ.get("BASE_DIR", Path(__file__).resolve().parent))
DATA_DIR = BASE_DIR / "data"
OUTPUT_FILE = DATA_DIR / "trigger-prospects-latest.json"
BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search"
# Your company info (for email templates)
YOUR_COMPANY_NAME = os.environ.get("YOUR_COMPANY_NAME", "Your Company")
YOUR_SENDER_NAME = os.environ.get("YOUR_SENDER_NAME", "Your Name")
# ─── Signal Search Queries ───────────────────────────────────────────────────
# Customize these queries for your target market.
# Each category maps to a list of search queries that detect buying signals.
SEARCH_QUERIES = {
"new_hire": [
'"hired head of marketing"',
'"new CMO" announced',
'"VP marketing joined"',
'"head of growth" hired',
'"VP of marketing" appointed',
'"chief marketing officer" joins',
],
"job_posting": [
'"head of marketing" job posting site:linkedin.com',
'"VP of marketing" hiring site:linkedin.com',
'"CMO" open role site:linkedin.com',
],
"agency_search": [
'"looking for marketing agency"',
'"looking for agency" marketing',
'"seeking marketing partner"',
'"RFP" "marketing agency"',
],
"funding": [
'"series A" raised marketing',
'"series B" raised marketing',
'"raised" million marketing growth',
'"funding round" marketing scale',
],
}
# ─── Service Keyword Mapping ────────────────────────────────────────────────
# Maps your service offerings to keywords found in signal text.
# Used to suggest which services to pitch to each prospect.
SERVICE_KEYWORDS = {
"SEO": ["seo", "organic", "search engine", "content marketing", "blog", "rankings"],
"Paid Media": ["paid", "ppc", "ads", "advertising", "google ads", "facebook ads",
"media buy", "paid social", "paid search"],
"Creative": ["creative", "brand", "design", "video", "content", "storytelling"],
"CRO": ["conversion", "cro", "optimization", "landing page", "funnel", "a/b test"],
"AI Marketing": ["ai", "artificial intelligence", "machine learning", "automation",
"personalization"],
}
def get_brave_api_key():
"""Get Brave Search API key from environment."""
key = os.environ.get("BRAVE_API_KEY")
if not key:
print("❌ BRAVE_API_KEY not set.", file=sys.stderr)
print(" Get one at: https://api.search.brave.com/", file=sys.stderr)
sys.exit(1)
return key
def brave_search(query: str, api_key: str, freshness: str = "pw", count: int = 10) -> list:
"""Search Brave and return results list."""
params = urlencode({"q": query, "count": count, "freshness": freshness})
url = f"{BRAVE_SEARCH_URL}?{params}"
req = Request(url, headers={
"Accept": "application/json",
"Accept-Encoding": "identity",
"X-Subscription-Token": api_key,
})
try:
with urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode())
return data.get("web", {}).get("results", [])
except Exception as e:
print(f" Warning: Search failed for '{query[:50]}...': {e}", file=sys.stderr)
return []
def freshness_for_days(days: int) -> str:
"""Map day count to Brave freshness parameter."""
if days <= 1:
return "pd"
elif days <= 7:
return "pw"
elif days <= 30:
return "pm"
return "py"
def extract_company_name(title: str, description: str) -> str:
"""Best-effort company name extraction from search result text."""
patterns = [
r"(?:at|joins?|hired by|appointed at|named .* at)\s+([A-Z][A-Za-z0-9&\.\- ]{1,40}?)"
r"(?:\s+as|\s*[,\.\-\|]|\s+to\b)",
r"([A-Z][A-Za-z0-9&\.\- ]{1,40}?)\s+(?:hires?|appoints?|names?|announces?|welcomes?)\b",
r"([A-Z][A-Za-z0-9&\.\- ]{1,40}?)\s+(?:raises?|secures?|closes?)\s+\$",
r"([A-Z][A-Za-z0-9&\.\- ]{1,40}?)\s+(?:series [A-C]|funding)",
]
text = f"{title} {description}"
for pat in patterns:
m = re.search(pat, text)
if m:
name = m.group(1).strip().rstrip(" -,.|")
if name.lower() not in {"the", "a", "new", "former", "our", "this", "why", "how", "what"}:
return name
parts = re.split(r"[\|\-–—:]", title)
if parts:
candidate = parts[0].strip()
if len(candidate) < 50 and candidate[0:1].isupper():
return candidate
return title[:60]
def estimate_company_size(text: str) -> str:
"""Estimate company size from context clues in signal text."""
text_lower = text.lower()
if any(w in text_lower for w in ["enterprise", "fortune 500", "10,000", "global"]):
return "1000+"
if any(w in text_lower for w in ["series c", "series d", "ipo", "public"]):
return "500-1000"
if any(w in text_lower for w in ["series b", "growth stage", "scale"]):
return "200-500"
if any(w in text_lower for w in ["series a", "startup", "seed"]):
return "50-200"
if any(w in text_lower for w in ["pre-seed", "bootstrapped", "early stage"]):
return "10-50"
return "50-500"
def estimate_industry(text: str) -> str:
"""Estimate industry from signal text."""
text_lower = text.lower()
industries = {
"SaaS": ["saas", "software", "platform", "cloud", "app"],
"E-commerce": ["ecommerce", "e-commerce", "retail", "shop", "store", "dtc", "d2c"],
"Fintech": ["fintech", "financial", "banking", "payments", "insurance"],
"Healthcare": ["health", "medical", "biotech", "pharma", "wellness"],
"Education": ["edtech", "education", "learning", "course"],
"AI/ML": ["artificial intelligence", "machine learning", "ai-powered", "ai company"],
"Crypto/Web3": ["crypto", "blockchain", "web3", "defi", "nft"],
"Media": ["media", "publishing", "news", "content"],
"B2B Services": ["b2b", "consulting", "services", "agency"],
}
for industry, keywords in industries.items():
if any(k in text_lower for k in keywords):
return industry
return "Technology"
def suggest_services(text: str) -> list:
"""Suggest which of your services to pitch based on signal text."""
text_lower = text.lower()
matched = []
for service, keywords in SERVICE_KEYWORDS.items():
if any(k in text_lower for k in keywords):
matched.append(service)
if not matched:
matched = ["SEO", "Paid Media"] # Sensible defaults
return matched
def score_prospect(signal_type: str, size_est: str, services: list, text: str) -> int:
"""Score a prospect 0-100 based on signal type, company fit, and context."""
score = 0
# Signal type scoring
signal_scores = {"new_hire": 35, "job_posting": 25, "funding": 30, "agency_search": 40}
score += signal_scores.get(signal_type, 20)
# Company size fit (mid-market is ideal for most agencies)
size_scores = {"10-50": 10, "50-200": 25, "200-500": 25, "500-1000": 15, "1000+": 5}
score += size_scores.get(size_est, 15)
# Service alignment
score += min(len(services) * 5, 20)
# Bonus signals in text
text_lower = text.lower()
if "cmo" in text_lower or "chief marketing" in text_lower:
score += 10
if "agency" in text_lower:
score += 5
if any(w in text_lower for w in ["review", "evaluate", "looking for", "rfp"]):
score += 5
return min(score, 100)
def generate_outreach_hook(company: str, signal_type: str) -> str:
"""Generate a casual outreach hook based on the signal type."""
hooks = {
"new_hire": [
f"New marketing leadership at {company}. The first 90 days is when the best "
f"leaders figure out what's actually working.",
f"Congrats on the new hire at {company}. Leadership changes are the best time "
f"to audit what's driving results and what's noise.",
],
"job_posting": [
f"Noticed {company} is hiring marketing roles. Usually means growth is the "
f"priority. We help companies hit targets while the team ramps up.",
f"{company} is building out the marketing team. We've been the bridge for "
f"companies in that exact phase.",
],
"funding": [
f"Congrats on the raise. Post-funding is when the pressure to scale "
f"acquisition hits. We help turn capital into pipeline efficiently.",
f"Saw the funding news for {company}. The companies that win post-raise "
f"scale acquisition without burning through runway.",
],
"agency_search": [
f"Saw {company} is evaluating marketing partners. Happy to throw our hat in.",
f"Noticed you're looking for a marketing partner at {company}.",
],
}
options = hooks.get(signal_type, [f"Noticed some movement at {company}."])
return random.choice(options)
def generate_email_draft(company, signal_type, services):
"""Generate a trigger-based cold email draft."""
services_str = ", ".join(services[:3]) if services else "growth marketing"
cta = random.choice([
"Worth exploring?", "Curious if relevant?", "Worth a conversation?",
"Make sense to chat?", "Worth 15 min?",
])
signoff = YOUR_SENDER_NAME
templates = {
"new_hire": {
"subject": f"{company}, new leadership = fresh eyes",
"body": (f"Hey,\n\nSaw the leadership change at {company}. The first 90 days "
f"are when the best marketing leaders audit what's working and cut what's not.\n\n"
f"We specialize in {services_str} and figured the timing might be right.\n\n"
f"{cta}\n\n{signoff}"),
},
"job_posting": {
"subject": f"{company} is hiring, we can help now",
"body": (f"Hey,\n\nNoticed {company} is hiring marketing roles. Hiring takes time, "
f"but growth targets don't wait.\n\n"
f"We've been the bridge for companies in that exact gap, handling "
f"{services_str} while the team ramps up.\n\n{cta}\n\n{signoff}"),
},
"funding": {
"subject": f"congrats on the raise, {company}",
"body": (f"Hey,\n\nSaw the funding news. Congrats. Post-raise is when the pressure "
f"to scale acquisition really hits.\n\n"
f"We help companies turn funding into efficient pipeline growth, "
f"specifically through {services_str}.\n\n{cta}\n\n{signoff}"),
},
"agency_search": {
"subject": f"{company} + {YOUR_COMPANY_NAME}",
"body": (f"Hey,\n\nSaw you're evaluating marketing partners at {company}.\n\n"
f"We specialize in {services_str}. Happy to share a few quick wins "
f"we'd go after in the first 30 days. No commitment.\n\n{cta}\n\n{signoff}"),
},
}
t = templates.get(signal_type, templates["agency_search"])
return f"Subject: {t['subject']}\n\n{t['body']}"
def suggest_channel(signal_type: str) -> str:
"""Suggest the best outreach channel for this signal type."""
channels = {
"new_hire": "LinkedIn (congratulate + connect)",
"agency_search": "Email (direct response)",
"funding": "LinkedIn + Email (warm congrats)",
"job_posting": "Email",
}
return channels.get(signal_type, "Email")
# ─── Main Pipeline ───────────────────────────────────────────────────────────
def run(days: int = 7, top: int = 15, min_score: int = 50):
api_key = get_brave_api_key()
freshness = freshness_for_days(days)
print(f"🔍 Trigger-Based Prospecting Engine")
print(f" Scanning last {days} days | Top {top} | Min score: {min_score}")
print(f" {'-'*50}")
all_prospects = []
seen_urls = set()
for signal_type, queries in SEARCH_QUERIES.items():
print(f"\n📡 Scanning: {signal_type.replace('_', ' ').title()}")
for query in queries:
print(f"{query[:60]}...")
results = brave_search(query, api_key, freshness=freshness, count=8)
for r in results:
url = r.get("url", "")
if url in seen_urls:
continue
seen_urls.add(url)
title = r.get("title", "")
desc = r.get("description", "")
full_text = f"{title} {desc}"
company = extract_company_name(title, desc)
size_est = estimate_company_size(full_text)
industry = estimate_industry(full_text)
services = suggest_services(full_text)
score = score_prospect(signal_type, size_est, services, full_text)
if score < min_score:
continue
prospect = {
"company": company,
"signal_type": signal_type,
"signal_detail": title,
"signal_url": url,
"signal_date": datetime.now().strftime("%Y-%m-%d"),
"prospect_score": score,
"industry": industry,
"est_company_size": size_est,
"suggested_services": services,
"suggested_channel": suggest_channel(signal_type),
"outreach_hook": generate_outreach_hook(company, signal_type),
"email_draft": generate_email_draft(company, signal_type, services),
}
all_prospects.append(prospect)
# Deduplicate by company (keep highest score)
company_best = {}
for p in all_prospects:
key = p["company"].lower().strip()
if key not in company_best or p["prospect_score"] > company_best[key]["prospect_score"]:
company_best[key] = p
prospects = sorted(company_best.values(),
key=lambda x: x["prospect_score"], reverse=True)[:top]
# Save output
DATA_DIR.mkdir(parents=True, exist_ok=True)
output = {
"generated_at": datetime.now().isoformat(),
"params": {"days": days, "top": top, "min_score": min_score},
"total_signals_found": len(all_prospects),
"prospects": prospects,
}
OUTPUT_FILE.write_text(json.dumps(output, indent=2))
# Print summary
print(f"\n{'='*60}")
print(f"🎯 TOP {len(prospects)} PROSPECTS (of {len(all_prospects)} signals found)")
print(f"{'='*60}\n")
for i, p in enumerate(prospects, 1):
print(f" {i:2d}. [{p['prospect_score']:3d}] {p['company']}")
print(f" Signal: {p['signal_type']}{p['signal_detail'][:70]}")
print(f" Size: {p['est_company_size']} | Industry: {p['industry']}")
print(f" Services: {', '.join(p['suggested_services'])}")
print(f" Channel: {p['suggested_channel']}")
print()
print(f"📁 Saved to: {OUTPUT_FILE}")
return prospects
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Trigger-Based Prospecting Engine")
parser.add_argument("--days", type=int, default=7, help="Lookback window in days (default: 7)")
parser.add_argument("--top", type=int, default=15, help="Number of top prospects (default: 15)")
parser.add_argument("--min-score", type=int, default=50, help="Minimum prospect score (default: 50)")
args = parser.parse_args()
run(days=args.days, top=args.top, min_score=args.min_score)