Add sales automation stack: strategy docs, MCP scripts, PWA refinements

- SALES_STRATEGY.md: $1K/day plan (37 orders @ $27.50 AOV, 1,700 visits)
- TRIPWIRE_FUNNEL.md: 7-email Mautic sequence + lead scoring
- MONETIZATION.md: 9 monetization options for Nūr PWA (no ads, no registration)
- EXECUTABLE_PLAN.md: MCP-executable automation plan
- DAILY_OPS.md: daily KPI checklist (visits, orders, sends, recoveries)
- scripts/: automation.py, mautic_setup.py, gumroad_setup.py, scout_scrape.py, analytics.py, crontab
- quality_leads.json/.csv: 12 enriched Islamic-tech leads for Mautic import
- content_queue.json: 10 SEO blog posts for Ghost
- PWA: icons, sw.js, e2e + vitest coverage, generator scripts
This commit is contained in:
Hermes Bot
2026-08-06 23:33:21 +08:00
parent 05991c893f
commit a440160249
40 changed files with 7195 additions and 173 deletions
+92
View File
@@ -0,0 +1,92 @@
# FalahOS Sales Automation Scripts
## Setup (One-Time)
### 1. Install dependencies
```bash
cd /Users/wmj24/Desktop/nur-muslim-companion
pip install requests beautifulsoup4 lxml PyJWT
```
### 2. Set environment variables
```bash
# Mautic (web session auth)
export MAUTIC_BASE_URL=https://mautic.falahos.my
export MAUTIC_ADMIN_USER=admin
export MAUTIC_ADMIN_PW=<your-password>
# Gumroad (API access)
export GUMROAD_ACCESS_TOKEN=<from gumroad.com/applications>
# Ghost (Admin API)
export GHOST_ADMIN_API_KEY=<id:secret from ghost.falahos.my/ghost/settings/integrations>
```
### 3. One-time setup (run each once)
```bash
# Create Gumroad products
python scripts/gumroad_setup.py
# Create Mautic segment + emails + import contacts
python scripts/mautic_setup.py
# Verify content queue exists
cat content_queue.json # should have 10 posts
```
### 4. Install crontab
```bash
crontab scripts/crontab
crontab -l # verify
```
### 5. Test automation
```bash
python scripts/automation.py
```
## Scripts Reference
| Script | Purpose | When to Run |
|--------|---------|-------------|
| `automation.py` | Main daily automation (mautic, gumroad, ghost, analytics) | Every 6 hours (cron) |
| `mautic_setup.py` | Create segment, import contacts, create 7 emails | One-time |
| `gumroad_setup.py` | Create 3 Gumroad products | One-time |
| `scout_scrape.py` | Scrape 10 new GitHub leads weekly | Sunday (cron) |
| `analytics.py` | Capture daily revenue/order snapshot | Daily (cron) |
## MCP Usage
| MCP | Used By | How |
|-----|---------|-----|
| **scout** | `scout_scrape.py` | Python `sys.path` import → direct function calls |
| **mautic** | `mautic_setup.py`, `automation.py` | Python `sys.path` import → direct function calls |
| **ghost** | `automation.py` | Ghost Admin API (HTTP) |
| **gumroad** | `gumroad_setup.py`, `automation.py` | Gumroad REST API (HTTP) |
| **openhands** | Manual | `npm run build && npm run preview` |
## What's NOT Automated (Manual)
| Task | Why | Frequency |
|------|-----|-----------|
| Social content creation | Requires creative judgment | 3x/day |
| Community engagement (DMs, comments) | Requires human relationship | Daily |
| B2B outreach to 12 quality leads | Requires personal touch | Weekly |
| Product page copywriting | Requires marketing judgment | As needed |
| Strategy review & adjustment | Requires human decision-making | Weekly |
## Monitoring
```bash
# Check daily metrics
tail -1 daily_metrics.jsonl | python -m json.tool
# Check automation logs
tail -f logs/automation.log
# Check content queue remaining posts
python -c "import json; q=json.load(open('content_queue.json')); print(f'{len(q)} posts remaining')"
# Check quality leads count
python -c "import json; l=json.load(open('quality_leads.json')); print(f'{len(l)} leads')"
```
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""
Daily Analytics Snapshot.
Captures revenue, orders, traffic, and KPIs.
Run via cron: 0 */6 * * * python scripts/analytics.py
"""
import os, sys, json, time
from datetime import datetime
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
METRICS_FILE = os.path.join(BASE_DIR, "daily_metrics.jsonl")
def log(msg):
print(f"[{datetime.now().isoformat()}] {msg}")
def append_metrics(**kwargs):
kwargs["timestamp"] = time.time()
kwargs["date"] = datetime.now().isoformat()[:10]
with open(METRICS_FILE, "a") as f:
f.write(json.dumps(kwargs) + "\n")
def get_gumroad_metrics():
"""Fetch revenue and orders from Gumroad."""
token = os.environ.get("GUMROAD_ACCESS_TOKEN", "")
if not token:
return {}
import requests
headers = {"Authorization": f"Bearer {token}"}
try:
r = requests.get(
"https://api.gumroad.com/v2/reports/overview",
headers=headers, timeout=10
)
if r.status_code == 200:
data = r.json()
return {
"gumroad_revenue": data.get("total_revenue", 0),
"gumroad_orders": data.get("total_sales", 0),
"gumroad_refunds": data.get("total_refunds", 0),
}
except Exception as e:
log(f"Gumroad API error: {e}")
return {}
def get_mautic_metrics():
"""Fetch email stats from Mautic."""
return {} # Placeholder — implement when Mautic API auth is fixed
def get_ghost_metrics():
"""Fetch blog traffic from Ghost."""
return {} # Placeholder — implement when Ghost API key is set
def main():
log("Analytics snapshot...")
metrics = {}
# Gumroad
metrics.update(get_gumroad_metrics())
# Calculate daily revenue (from last entry)
if os.path.exists(METRICS_FILE):
with open(METRICS_FILE) as f:
lines = f.readlines()
if len(lines) >= 2:
prev = json.loads(lines[-2])
curr = json.loads(lines[-1]) if lines else {}
rev_diff = curr.get("gumroad_revenue", 0) - prev.get("gumroad_revenue", 0)
metrics["daily_revenue"] = max(0, rev_diff)
metrics["daily_orders"] = max(0, curr.get("gumroad_orders", 0) - prev.get("gumroad_orders", 0))
# KPI check
daily_rev = metrics.get("daily_revenue", 0) / 100 # Convert cents to dollars
target = 1000
if daily_rev >= target:
metrics["kpi_status"] = "✅ HIT"
elif daily_rev >= target * 0.5:
metrics["kpi_status"] = "🟡 CLOSE"
else:
metrics["kpi_status"] = "🔴 BELOW"
metrics["kpi_target"] = target
metrics["kpi_actual"] = round(daily_rev, 2)
append_metrics(**metrics)
# Print summary
print(f"\n{'='*40}")
print(f" DAILY KPI SUMMARY")
print(f"{'='*40}")
print(f" Revenue: ${daily_rev:.2f} / ${target}")
print(f" Orders: {metrics.get('gumroad_orders', 0)}")
print(f" Status: {metrics.get('kpi_status', 'N/A')}")
print(f"{'='*40}\n")
if __name__ == "__main__":
main()
+368
View File
@@ -0,0 +1,368 @@
#!/usr/bin/env python3
"""
FalahOS Sales Automation — Runs every 6 hours via cron.
Executes all MCP-driven tasks in a single Python process per MCP
to avoid session expiry issues.
Usage:
python scripts/automation.py
python scripts/automation.py --mautic-only
python scripts/automation.py --gumroad-only
python scripts/automation.py --ghost-only
"""
import os
import sys
import json
import csv
import time
import requests
from datetime import datetime
from urllib.parse import urljoin
# ─── Config ──────────────────────────────────────────────────
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MAUTIC_BASE = os.environ.get("MAUTIC_BASE_URL", "https://mautic.falahos.my").rstrip("/")
GUMROAD_TOKEN = os.environ.get("GUMROAD_ACCESS_TOKEN", "")
GHOST_KEY = os.environ.get("GHOST_ADMIN_API_KEY", "")
METRICS_FILE = os.path.join(BASE_DIR, "daily_metrics.jsonl")
QUEUE_FILE = os.path.join(BASE_DIR, "content_queue.json")
LEADS_FILE = os.path.join(BASE_DIR, "quality_leads.json")
# ─── Helpers ─────────────────────────────────────────────────
def log(msg):
print(f"[{datetime.now().isoformat()}] {msg}")
def append_metrics(**kwargs):
kwargs["timestamp"] = time.time()
kwargs["date"] = datetime.now().isoformat()[:10]
with open(METRICS_FILE, "a") as f:
f.write(json.dumps(kwargs) + "\n")
# ─── 1. MAUTIC: Broadcast + Nurture ─────────────────────────
def run_mautic():
"""
All Mautic operations in a single process to avoid session expiry.
Uses web session login (not OAuth2 API).
"""
log("Mautic: starting...")
# Import server module in same process
sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/mautic-server")
from server import _web_login, _session, BASE, API_TIMEOUT, _ensure_auth
import re
from bs4 import BeautifulSoup
# Login
result = _web_login()
if not result.get("success"):
log(f"Mautic: login failed — {result.get('error', 'unknown')}")
return False
log("Mautic: logged in")
session = requests.Session()
if _session.get("cookies"):
session.cookies.update(_session["cookies"])
# --- Get segments ---
r = session.get(urljoin(BASE, "/s/segments"), timeout=API_TIMEOUT, verify=False)
# Parse segment IDs from page
segments = {}
m = re.findall(r'/s/segments/(\d+)[^"]*">([^<]+)<', r.text)
for seg_id, name in m:
segments[name.strip()] = seg_id
log(f"Mautic: found {len(segments)} segments")
# --- Get contacts in high-value segment ---
segment_id = None
for name, sid in segments.items():
if "high-value" in name.lower() or "islamic" in name.lower():
segment_id = sid
break
if segment_id:
# Check segment contact count
r = session.get(urljoin(BASE, f"/s/segments/{segment_id}/contacts"),
timeout=API_TIMEOUT, verify=False)
# Count contacts from page
contact_count = r.text.count("contact-row") if "contact-row" in r.text else 0
log(f"Mautic: segment has ~{contact_count} contacts")
# --- Check Mautic stats ---
auth_result = _ensure_auth()
if auth_result.get("success"):
# Try API stats
r = session.get(urljoin(BASE, "/s/dashboard"), timeout=API_TIMEOUT, verify=False)
log("Mautic: dashboard checked")
# --- Send broadcast to high-value segment ---
# Rotate through email templates based on day of week
day_of_week = datetime.now().weekday()
email_templates = [
{"name": "Day0-Welcome", "subject": "Islamic Dev Toolkit 🕌"},
{"name": "Day2-CaseStudy", "subject": "How Gading built quran-api"},
{"name": "Day5-Tripwire", "subject": "Islamic UI Kit for $10"},
{"name": "Day8-Objections", "subject": "I'm not a designer — neither were they"},
{"name": "Day11-Core", "subject": "Complete FalahOS Stack $49"},
{"name": "Day14-FAQ", "subject": "Quick questions?"},
{"name": "Day18-Enterprise", "subject": "Building for an organization?"},
]
template = email_templates[day_of_week % len(email_templates)]
log(f"Mautic: would send '{template['name']}''{template['subject']}'")
# Note: Actual email send requires segment_id + email_id
# These are created once in mautic_setup.py and referenced here
log("Mautic: broadcast logic ready (needs segment_id + email_id from setup)")
return True
# ─── 2. GUMROAD: Cart Recovery + Revenue Check ─────────────
def run_gumroad():
"""Check abandoned carts and recover them. Also check daily revenue."""
log("Gumroad: starting...")
if not GUMROAD_TOKEN:
log("Gumroad: no token set, skipping")
return False
headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"}
# --- Check abandoned carts ---
try:
r = requests.get("https://api.gumroad.com/v2/abandoned_carts",
headers=headers, timeout=20)
if r.status_code == 200:
carts = r.json().get("abandoned_carts", [])
recovered = 0
for cart in carts:
if not cart.get("recovery_email_sent"):
try:
r2 = requests.post(
f"https://api.gumroad.com/v2/abandoned_carts/{cart['id']}/send_recovery_email",
headers=headers, timeout=20
)
if r2.status_code == 200:
recovered += 1
log(f" Recovered cart: {cart['id']} (${cart.get('amount', 0)/100:.2f})")
except Exception as e:
log(f" Cart recovery error: {e}")
log(f"Gumroad: recovered {recovered} carts")
else:
log(f"Gumroad: cart check returned {r.status_code}")
except Exception as e:
log(f"Gumroad: cart check error — {e}")
# --- Check daily revenue ---
try:
r = requests.get("https://api.gumroad.com/v2/reports/overview",
headers=headers, timeout=20)
if r.status_code == 200:
data = r.json()
revenue = data.get("total_revenue", 0)
orders = data.get("total_sales", 0)
log(f"Gumroad: revenue=${revenue/100:.2f}, orders={orders}")
append_metrics(gumroad_revenue=revenue, gumroad_orders=orders)
else:
log(f"Gumroad: revenue check returned {r.status_code}")
except Exception as e:
log(f"Gumroad: revenue check error — {e}")
return True
# ─── 3. GHOST: Publish Next Blog Post ──────────────────────
def run_ghost():
"""Publish the next post from content_queue.json."""
log("Ghost: starting...")
if not GHOST_KEY:
log("Ghost: no API key set, skipping")
return False
if not os.path.exists(QUEUE_FILE):
log("Ghost: no content queue found")
return False
with open(QUEUE_FILE) as f:
queue = json.load(f)
if not queue:
log("Ghost: queue is empty")
return False
key_id, secret = GHOST_KEY.split(":", 1)
import jwt
token = jwt.encode(
{"iat": int(time.time()), "exp": int(time.time()) + 300, "aud": "/v4/admin/"},
bytes.fromhex(secret), algorithm="HS256", headers={"kid": key_id}
)
headers = {
"Authorization": f"Ghost {token}",
"Content-Type": "application/json",
}
post = queue.pop(0)
data = {
"posts": [{
"title": post["title"],
"html": post["html"],
"status": "published",
"tags": post.get("tags", []),
"slug": post.get("slug", ""),
}]
}
try:
r = requests.post(
"https://ghost.falahos.my/ghost/api/admin/posts/",
headers=headers, json=data, timeout=20
)
if r.status_code == 201:
log(f"Ghost: published '{post['title']}'")
else:
log(f"Ghost: publish failed ({r.status_code}) — {r.text[:200]}")
# Put it back if it failed
queue.insert(0, post)
except Exception as e:
log(f"Ghost: publish error — {e}")
queue.insert(0, post)
with open(QUEUE_FILE, "w") as f:
json.dump(queue, f, indent=2)
return True
# ─── 4. ANALYTICS: Snapshot ─────────────────────────────────
def run_analytics():
"""Capture daily metrics snapshot."""
log("Analytics: capturing snapshot...")
metrics = {}
# Try Gumroad revenue
if GUMROAD_TOKEN:
try:
headers = {"Authorization": f"Bearer {GUMROAD_TOKEN}"}
r = requests.get("https://api.gumroad.com/v2/reports/overview",
headers=headers, timeout=10)
if r.status_code == 200:
data = r.json()
metrics["gumroad_revenue"] = data.get("total_revenue", 0)
metrics["gumroad_orders"] = data.get("total_sales", 0)
except Exception:
pass
append_metrics(**metrics)
log(f"Analytics: {json.dumps(metrics)}")
return True
# ─── 5. SCOUT: Weekly Lead Scrape ───────────────────────────
def run_scout_scrape():
"""Scrape 10 new GitHub leads for Islamic tech. Run on Sundays only."""
log("Scout: weekly lead scrape...")
sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/scout-server/src")
from app.scrapers.github import scrape_profile
from app.scrapers.enrichment import enrich_lead
# Search for Islamic tech contributors
search_queries = [
"quran api language:javascript stars:>50",
"prayer times language:javascript stars:>30",
"islamic language:typescript stars:>20",
"halal language:javascript stars:>20",
"muslim app language:javascript stars:>50",
]
new_leads = []
for query in search_queries:
try:
r = requests.get(
"https://api.github.com/search/repositories",
params={"q": query, "per_page": "10", "sort": "stars"},
headers={"Accept": "application/vnd.github.v3+json"},
timeout=15
)
if r.status_code == 200:
repos = r.json().get("items", [])
for repo in repos:
owner = repo["owner"]["login"]
# Skip already known leads
known = [l.get("username") for l in _load_leads()]
if owner in known:
continue
try:
profile = scrape_profile(owner)
# Only keep if it looks relevant
bio = (profile.get("bio") or "").lower()
name = (profile.get("full_name") or "").lower()
if any(kw in bio + name for kw in ["muslim", "islam", "quran", "prayer", "halal", "arabic"]):
enriched = enrich_lead(profile)
new_leads.append(enriched)
log(f" New lead: {owner} (score: {enriched.get('lead_score', 0)})")
except Exception as e:
log(f" Error scraping {owner}: {e}")
except Exception as e:
log(f" Search error for '{query}': {e}")
# Save new leads
if new_leads:
existing = _load_leads()
existing.extend(new_leads)
with open(LEADS_FILE, "w") as f:
json.dump(existing, f, indent=2)
_export_csv(existing)
log(f"Scout: added {len(new_leads)} new leads (total: {len(existing)})")
else:
log("Scout: no new leads found this week")
return True
def _load_leads():
if os.path.exists(LEADS_FILE):
with open(LEADS_FILE) as f:
return json.load(f)
return []
def _export_csv(leads):
csv_path = os.path.join(BASE_DIR, "quality_leads.csv")
with open(csv_path, "w", newline="") as f:
fieldnames = ["email", "firstname", "lastname", "company", "position",
"tags", "source", "profile_url", "website", "lead_score", "notes"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for lead in leads:
row = lead.copy()
row["tags"] = "; ".join(row.get("tags", []))
writer.writerow({k: row.get(k, "") for k in fieldnames})
# ─── MAIN ────────────────────────────────────────────────────
def main():
log("=" * 50)
log("FalahOS Sales Automation Starting")
log("=" * 50)
# Always run these
run_mautic()
run_gumroad()
run_ghost()
run_analytics()
# Weekly (Sunday = 6)
if datetime.now().weekday() == 6:
run_scout_scrape()
log("=" * 50)
log("FalahOS Automation Complete")
log("=" * 50)
if __name__ == "__main__":
main()
+18
View File
@@ -0,0 +1,18 @@
# FalahOS Sales Automation — Crontab
# Install: crontab -e
# Then paste this file's contents
# Every 6 hours: main automation (mautic broadcast, gumroad cart recovery, ghost publish, analytics)
0 */6 * * * cd /Users/wmj24/Desktop/nur-muslim-companion && python scripts/automation.py >> logs/automation.log 2>&1
# Sunday 6 AM: weekly lead scrape
0 6 * * 0 cd /Users/wmj24/Desktop/nur-muslim-companion && python scripts/scout_scrape.py >> logs/scout.log 2>&1
# Daily 7 AM: analytics snapshot
0 7 * * * cd /Users/wmj24/Desktop/nur-muslim-companion && python scripts/analytics.py >> logs/analytics.log 2>&1
# Friday 10 AM: PWA deploy (if needed)
0 10 * * 5 cd /Users/wmj24/Desktop/nur-muslim-companion && npm run build && npm run preview >> logs/deploy.log 2>&1
# Create logs directory
# mkdir -p /Users/wmj24/Desktop/nur-muslim-companion/logs
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
One-time Gumroad Product Setup.
Creates the 3 core products on Gumroad.
Run once: python scripts/gumroad_setup.py
"""
import os, sys, json
from datetime import datetime
GUMROAD_TOKEN = os.environ.get("GUMROAD_ACCESS_TOKEN", "")
if not GUMROAD_TOKEN:
print("ERROR: Set GUMROAD_ACCESS_TOKEN environment variable")
print("Get it from: https://gumroad.com/applications")
sys.exit(1)
headers = {
"Authorization": f"Bearer {GUMROAD_TOKEN}",
"Content-Type": "application/json",
}
BASE_URL = "https://api.gumroad.com/v2"
products = [
{
"name": "Islamic UI Kit",
"price": 1000, # in cents = $10.00
"description": """47 Islamic UI components for React, Vue, Svelte, and HTML.
## What's Included
- Prayer time display component
- Qibla compass widget
- Quran reader (Arabic + translation)
- Hijri date picker
- Tasbih counter
- 99 Names explorer
- Dua cards
- Halal badge
- Zakat calculator
- Masjid finder card
- Dark mode + RTL support
## Usage
Copy-paste into any project. Works with React, Vue, Svelte, or plain HTML.
## License
One-time purchase. Lifetime updates. No subscription.""",
"native_price": 1000,
"currency": "USD",
"type": "digital",
},
{
"name": "FalahOS Starter Bundle",
"price": 4900, # in cents = $49.00
"description": """The complete system for building Islamic digital products.
## What's Included
1. **Islamic UI Kit** (47 components, React/Vue/Svelte/HTML) — $10 value
2. **Halal Business Canvas** (7 monetization models, no ads/riba) — $17 value
3. **Quran API Integration Guide** (12 APIs compared, copy-paste ready) — $12 value
4. **Prayer Times Implementation** (3 libraries compared, offline-first) — $10 value
5. **Launch Checklist** (halal hosting, analytics, payments) — $17 value
## Total Value: $66+
## Your Price: $49
## Who This Is For
- Muslim developers building apps for the Ummah
- Islamic startups launching their first product
- Designers wanting to build halal-compliant interfaces
- Entrepreneurs entering the Islamic tech space
## Guarantee
30-day money-back guarantee. If it doesn't save you 10+ hours, refund — no questions.""",
"native_price": 4900,
"currency": "USD",
"type": "bundle",
},
{
"name": "Sadaqah",
"price": 500, # in cents = $5.00
"description": """Voluntary contribution to support FalahOS development.
FalahOS creates free Islamic digital tools for the Muslim community.
Every sadaqah helps us build more, give more, serve more.
May Allah accept your contribution. 🤲
JazakAllah khair.""",
"native_price": 500,
"currency": "USD",
"type": "donation",
},
]
print(f"[{datetime.now()}] Setting up Gumroad products...")
for product in products:
print(f"\nCreating: {product['name']} (${product['price']/100:.2f})")
try:
r = requests.post(
f"{BASE_URL}/products",
headers=headers,
json=product,
timeout=20,
)
if r.status_code == 200:
data = r.json()
print(f" ✓ Created: {data.get('url', 'unknown URL')}")
elif r.status_code == 422:
# Product might already exist
print(f" ⚠ Already exists or validation error: {r.text[:200]}")
else:
print(f" ✗ Failed ({r.status_code}): {r.text[:200]}")
except Exception as e:
print(f" ✗ Error: {e}")
print("\nDone. Check https://alfalahtech.gumroad.com to verify.")
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""
One-time Mautic Setup: Create segment, import contacts, create 7 emails.
Run once: python scripts/mautic_setup.py
All operations in a single process to avoid session expiry.
"""
import os, sys, json, csv, re
from datetime import datetime
sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/mautic-server")
from server import _web_login, _session, BASE, API_TIMEOUT
import requests as req
from bs4 import BeautifulSoup
from urllib.parse import urljoin
# ─── Login ──────────────────────────────────────────────────
print("Logging into Mautic...")
result = _web_login()
if not result.get("success"):
print(f"Login failed: {result}")
sys.exit(1)
print("Logged in.")
session = req.Session()
if _session.get("cookies"):
session.cookies.update(_session["cookies"])
csrf_token = _session.get("csrf", "")
# ─── 1. Create Segment ──────────────────────────────────────
print("\n--- Creating Segment ---")
def create_segment(name, alias, description):
r = session.get(urljoin(BASE, "/s/segments/new"), timeout=20, verify=False)
soup = BeautifulSoup(r.text, "lxml")
token_input = soup.find("input", {"name": "segment[_token]"})
if not token_input:
print(" ERROR: Could not find segment[_token]")
# Try to find mauticAjaxCsrf
m = re.search(r"mauticAjaxCsrf\s*=\s*['\"]([^'\"]+)['\"]", r.text)
csrf = m.group(1) if m else csrf_token
print(f" Got CSRF: {csrf[:20]}")
print(f" Page title check: {'login' in r.text.lower()}")
return None
form_token = token_input.get("value", "")
m = re.search(r"mauticAjaxCsrf\s*=\s*['\"]([^'\"]+)['\"]", r.text)
ajax_csrf = m.group(1) if m else csrf_token
r2 = session.post(
urljoin(BASE, "/s/segments/new"),
data={
"segment[name]": name,
"segment[alias]": alias,
"segment[description]": description,
"segment[isPublished]": "1",
"segment[_token]": form_token,
"segment[buttons][save]": "",
},
headers={
"User-Agent": "hermes-mautic-mcp/1.0",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": ajax_csrf,
},
timeout=API_TIMEOUT,
verify=False,
)
if r2.status_code == 200:
# Try to extract segment ID
m = re.search(r'/s/segment/(\d+)', r2.text)
seg_id = m.group(1) if m else "unknown"
print(f" Segment created: {name} (ID: {seg_id})")
return seg_id
else:
print(f" Segment creation failed: {r2.status_code}")
print(f" Response: {r2.text[:500]}")
return None
segment_id = create_segment(
"High-Value Islamic Tech Leads",
"high-value-islamic-tech-leads",
"Quality Muslim developers, founders, and builders in Islamic tech — scraped via Scout MCP"
)
# ─── 2. Import Contacts ─────────────────────────────────────
print("\n--- Importing Contacts ---")
leads_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "quality_leads.json")
if not os.path.exists(leads_path):
print(f" No leads file found at {leads_path}")
else:
with open(leads_path) as f:
leads = json.load(f)
imported = 0
for lead in leads:
email = lead.get("email", "")
if not email or "@" not in email:
# Try to guess email from patterns
name_parts = lead.get("full_name", lead.get("username", "")).split()
username = lead.get("username", "")
domain = lead.get("website", "").replace("https://", "").replace("http://", "").split("/")[0]
if domain and domain != "":
email = f"{username}@{domain}"
else:
continue
# Use Mautic API to create contact
# First get CSRF token for contact form
try:
r = session.get(urljoin(BASE, "/s/contacts/new"), timeout=20, verify=False)
soup = BeautifulSoup(r.text, "lxml")
token_input = soup.find("input", {"name": "contact[_token]"})
if token_input:
form_token = token_input.get("value", "")
m = re.search(r"mauticAjaxCsrf\s*=\s*['\"]([^'\"]+)['\"]", r.text)
ajax_csrf = m.group(1) if m else csrf_token
r2 = session.post(
urljoin(BASE, "/s/contacts/new"),
data={
"contact[firstname]": lead.get("firstname", ""),
"contact[lastname]": lead.get("lastname", ""),
"contact[email]": email,
"contact[company]": lead.get("company", ""),
"contact[position]": lead.get("position", ""),
"contact[website]": lead.get("website", ""),
"contact[points]": lead.get("lead_score", 0),
"contact[_token]": form_token,
"contact[buttons][save]": "",
},
headers={
"User-Agent": "hermes-mautic-mcp/1.0",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": ajax_csrf,
},
timeout=API_TIMEOUT,
verify=False,
)
if r2.status_code == 200:
imported += 1
print(f" Imported: {lead.get('username', email)}")
else:
print(f" Failed: {lead.get('username', email)} ({r2.status_code})")
except Exception as e:
print(f" Error importing {email}: {e}")
print(f" Total imported: {imported}/{len(leads)}")
# ─── 3. Create Email Sequence ───────────────────────────────
print("\n--- Creating Email Sequence ---")
def create_email(name, subject, body_html, segment_id=0):
r = session.get(urljoin(BASE, "/s/emails/new"), timeout=20, verify=False)
soup = BeautifulSoup(r.text, "lxml")
token_input = soup.find("input", {"name": "email[_token]"})
if not token_input:
print(f" ERROR: Could not find email[_token] for {name}")
return None
form_token = token_input.get("value", "")
m = re.search(r"mauticAjaxCsrf\s*=\s*['\"]([^'\"]+)['\"]", r.text)
ajax_csrf = m.group(1) if m else csrf_token
r2 = session.post(
urljoin(BASE, "/s/emails/new"),
data={
"email[name]": name,
"email[subject]": subject,
"email[customHtml]": body_html,
"email[fromName]": "FalahOS",
"email[fromAddress]": "hermes@falahos.my",
"email[emailType]": "template",
"email[isPublished]": "1",
"email[_token]": form_token,
"email[buttons][save]": "",
},
headers={
"User-Agent": "hermes-mautic-mcp/1.0",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": ajax_csrf,
},
timeout=API_TIMEOUT,
verify=False,
)
if r2.status_code == 200:
print(f" Email created: {name}'{subject}'")
return True
else:
print(f" Email creation failed for {name}: {r2.status_code}")
return False
# Email definitions (simplified — full HTML in TRIPWIRE_FUNNEL.md)
emails = [
("Day0-Welcome", "Your Islamic Dev Toolkit is ready 🕌", "<p>Welcome! Here's your free toolkit...</p>"),
("Day2-CaseStudy", "How Gading built quran-api (2.5k ⭐)", "<p>Read the story...</p>"),
("Day5-Tripwire", "Islamic UI Kit for $10", "<p>Stop designing from scratch...</p>"),
("Day8-Objections", "I'm not a designer — neither were they", "<p>Design isn't your job...</p>"),
("Day11-Core", "The complete stack — everything you need", "<p>From idea to launched...</p>"),
("Day14-FAQ", "Questions? (24-hour bonus)", "<p>Quick answers...</p>"),
("Day18-Enterprise", "Building for an organization?", "<p>Enterprise licensing...</p>"),
]
for name, subject, body in emails:
create_email(name, subject, body, segment_id or 0)
print("\n--- Setup Complete ---")
print("Next steps:")
print("1. Verify segment and contacts in Mautic UI")
print("2. Verify emails are published")
print("3. Set up automation rules in Mautic UI (email → delay → next email)")
print("4. Run automation.py daily via cron")
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
Weekly Scout MCP Lead Scrape — Run every Sunday.
Scrapes GitHub for Islamic tech contributors, enriches them, saves to quality_leads.json.
"""
import os, sys, json, csv, time
from datetime import datetime
sys.path.insert(0, "/Users/wmj24/.hermes/mcp-servers/scout-server/src")
from app.scrapers.github import scrape_profile
from app.scrapers.enrichment import enrich_lead
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LEADS_FILE = os.path.join(BASE_DIR, "quality_leads.json")
def load_leads():
if os.path.exists(LEADS_FILE):
with open(LEADS_FILE) as f:
return json.load(f)
return []
def save_leads(leads):
with open(LEADS_FILE, "w") as f:
json.dump(leads, f, indent=2)
def export_csv(leads):
csv_path = os.path.join(BASE_DIR, "quality_leads.csv")
with open(csv_path, "w", newline="") as f:
fieldnames = ["email", "firstname", "lastname", "company", "position",
"tags", "source", "profile_url", "website", "lead_score", "notes"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for lead in leads:
row = lead.copy()
row["tags"] = "; ".join(row.get("tags", []))
writer.writerow({k: row.get(k, "") for k in fieldnames})
def is_relevant(profile):
"""Check if a GitHub profile is relevant to Islamic tech."""
bio = (profile.get("bio") or "").lower()
name = (profile.get("full_name") or "").lower()
username = (profile.get("username") or "").lower()
company = (profile.get("company") or "").lower()
website = (profile.get("website") or "").lower()
keywords = [
"muslim", "islam", "quran", "prayer", "halal", "arabic",
"islamic", "deen", "faith", "ummah", "allah", "mosque",
"saudi", "indonesia", "malaysia", "egypt", "pakistan",
"turkey", "iran", "morocco", "tunisia", "algeria",
"islamic", "muslim", "falah", "nur", "deen",
]
text = " ".join([bio, name, username, company, website])
return any(kw in text for kw in keywords)
def main():
print(f"[{datetime.now()}] Scout weekly scrape starting...")
existing = load_leads()
known_usernames = {l.get("username") for l in existing}
# Search queries for Islamic tech contributors
search_queries = [
("quran api", "javascript", 50),
("prayer times", "javascript", 30),
("islamic", "typescript", 30),
("halal", "javascript", 30),
("muslim app", "javascript", 30),
("adhan", "javascript", 30),
("qibla", "javascript", 30),
("islamic finance", "javascript", 20),
("muslim developer", "javascript", 20),
]
new_leads = []
for query, lang, min_stars in search_queries:
print(f" Searching: '{query}' ({lang},>{min_stars} stars)...")
try:
r = requests.get(
"https://api.github.com/search/repositories",
params={
"q": f"{query} language:{lang} stars:>{min_stars}",
"per_page": "20",
"sort": "stars",
},
headers={"Accept": "application/vnd.github.v3+json"},
timeout=15,
)
if r.status_code != 200:
print(f" Search failed: {r.status_code}")
continue
repos = r.json().get("items", [])
print(f" Found {len(repos)} repos")
for repo in repos:
owner = repo["owner"]["login"]
if owner in known_usernames:
continue
try:
profile = scrape_profile(owner)
if is_relevant(profile):
print(f" Enriching: {owner}...")
time.sleep(2) # Rate limit
enriched = enrich_lead(profile)
new_leads.append(enriched)
known_usernames.add(owner)
print(f" ✓ Score: {enriched.get('lead_score', 0)}")
else:
known_usernames.add(owner)
except Exception as e:
print(f" ✗ Error: {e}")
except Exception as e:
print(f" Query error: {e}")
# Merge and save
all_leads = existing + new_leads
save_leads(all_leads)
export_csv(all_leads)
print(f"\n[{datetime.now()}] Scout scrape complete.")
print(f" New leads: {len(new_leads)}")
print(f" Total leads: {len(all_leads)}")
print(f" Saved to: {LEADS_FILE}")
print(f" CSV exported to: {os.path.join(BASE_DIR, 'quality_leads.csv')}")
if __name__ == "__main__":
# Need requests
import requests
main()