Files
Hermes Bot a440160249 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
2026-08-06 23:33:21 +08:00

368 lines
13 KiB
Python
Executable File

#!/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()