- Profile auditor (bio quality, posting patterns, growth readiness) - Tweet composer (hooks, threads, validation, 30+ proven patterns) - Content planner (weekly calendars with format mix) - Competitor analyzer (competitive intel via data import) - Growth tracker (snapshot-based progress tracking + milestone projection) - Algorithm reference doc (ranking signals, timing, format performance) - 226-line SKILL.md with practical playbook (no fluff) - Security audit: PASS (0 findings)
260 lines
9.7 KiB
Python
260 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
X/Twitter Growth Tracker — Track and analyze account growth over time.
|
|
|
|
Stores periodic snapshots of account metrics and calculates growth trends,
|
|
engagement patterns, and milestone projections.
|
|
|
|
Usage:
|
|
python3 growth_tracker.py --record --handle @user --followers 5200 --eng-rate 2.1
|
|
python3 growth_tracker.py --report --handle @user
|
|
python3 growth_tracker.py --report --handle @user --period 30d --json
|
|
python3 growth_tracker.py --milestone --handle @user --target 10000
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".growth-data")
|
|
|
|
|
|
def get_data_file(handle: str) -> str:
|
|
clean = handle.lstrip("@").lower()
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
return os.path.join(DATA_DIR, f"{clean}.jsonl")
|
|
|
|
|
|
def record_snapshot(handle: str, followers: int, following: int = 0,
|
|
eng_rate: float = 0, posts_week: float = 0, notes: str = ""):
|
|
entry = {
|
|
"timestamp": datetime.now().isoformat(),
|
|
"handle": handle,
|
|
"followers": followers,
|
|
"following": following,
|
|
"engagement_rate": eng_rate,
|
|
"posts_per_week": posts_week,
|
|
"notes": notes,
|
|
}
|
|
|
|
filepath = get_data_file(handle)
|
|
with open(filepath, "a") as f:
|
|
f.write(json.dumps(entry) + "\n")
|
|
|
|
return entry
|
|
|
|
|
|
def load_snapshots(handle: str, period_days: int = 0) -> list:
|
|
filepath = get_data_file(handle)
|
|
if not os.path.exists(filepath):
|
|
return []
|
|
|
|
entries = []
|
|
cutoff = None
|
|
if period_days > 0:
|
|
cutoff = datetime.now() - timedelta(days=period_days)
|
|
|
|
with open(filepath) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
entry = json.loads(line)
|
|
if cutoff:
|
|
ts = datetime.fromisoformat(entry["timestamp"])
|
|
if ts < cutoff:
|
|
continue
|
|
entries.append(entry)
|
|
|
|
return entries
|
|
|
|
|
|
def generate_report(handle: str, entries: list) -> dict:
|
|
if not entries:
|
|
return {"handle": handle, "error": "No data found"}
|
|
|
|
report = {
|
|
"handle": handle,
|
|
"data_points": len(entries),
|
|
"first_record": entries[0]["timestamp"],
|
|
"last_record": entries[-1]["timestamp"],
|
|
"current_followers": entries[-1]["followers"],
|
|
}
|
|
|
|
if len(entries) >= 2:
|
|
first = entries[0]
|
|
last = entries[-1]
|
|
|
|
follower_change = last["followers"] - first["followers"]
|
|
days_span = (datetime.fromisoformat(last["timestamp"]) -
|
|
datetime.fromisoformat(first["timestamp"])).days
|
|
days_span = max(days_span, 1)
|
|
|
|
report["follower_change"] = follower_change
|
|
report["days_tracked"] = days_span
|
|
report["daily_growth"] = round(follower_change / days_span, 1)
|
|
report["weekly_growth"] = round((follower_change / days_span) * 7, 1)
|
|
report["monthly_projection"] = round((follower_change / days_span) * 30)
|
|
|
|
if first["followers"] > 0:
|
|
pct_change = ((last["followers"] - first["followers"]) / first["followers"]) * 100
|
|
report["growth_percent"] = round(pct_change, 1)
|
|
|
|
# Engagement trend
|
|
eng_rates = [e["engagement_rate"] for e in entries if e.get("engagement_rate", 0) > 0]
|
|
if len(eng_rates) >= 2:
|
|
mid = len(eng_rates) // 2
|
|
first_half_avg = sum(eng_rates[:mid]) / mid
|
|
second_half_avg = sum(eng_rates[mid:]) / (len(eng_rates) - mid)
|
|
report["engagement_trend"] = "improving" if second_half_avg > first_half_avg else "declining"
|
|
report["avg_engagement_rate"] = round(sum(eng_rates) / len(eng_rates), 2)
|
|
|
|
return report
|
|
|
|
|
|
def project_milestone(handle: str, entries: list, target: int) -> dict:
|
|
if len(entries) < 2:
|
|
return {"error": "Need at least 2 data points for projection"}
|
|
|
|
current = entries[-1]["followers"]
|
|
if current >= target:
|
|
return {"handle": handle, "target": target, "status": "Already reached!"}
|
|
|
|
first = entries[0]
|
|
last = entries[-1]
|
|
days_span = (datetime.fromisoformat(last["timestamp"]) -
|
|
datetime.fromisoformat(first["timestamp"])).days
|
|
days_span = max(days_span, 1)
|
|
|
|
daily_growth = (last["followers"] - first["followers"]) / days_span
|
|
|
|
if daily_growth <= 0:
|
|
return {"handle": handle, "target": target, "status": "Not growing — can't project",
|
|
"daily_growth": round(daily_growth, 1)}
|
|
|
|
remaining = target - current
|
|
days_needed = remaining / daily_growth
|
|
target_date = datetime.now() + timedelta(days=days_needed)
|
|
|
|
return {
|
|
"handle": handle,
|
|
"current": current,
|
|
"target": target,
|
|
"remaining": remaining,
|
|
"daily_growth": round(daily_growth, 1),
|
|
"days_needed": round(days_needed),
|
|
"projected_date": target_date.strftime("%Y-%m-%d"),
|
|
}
|
|
|
|
|
|
def print_report(report: dict):
|
|
print(f"\n{'='*60}")
|
|
print(f" GROWTH REPORT — {report['handle']}")
|
|
print(f"{'='*60}")
|
|
|
|
if "error" in report:
|
|
print(f"\n ⚠️ {report['error']}")
|
|
print(f" Record data first: python3 growth_tracker.py --record --handle {report['handle']} --followers N")
|
|
print()
|
|
return
|
|
|
|
print(f"\n Current followers: {report['current_followers']:,}")
|
|
print(f" Data points: {report['data_points']}")
|
|
print(f" Tracking since: {report['first_record'][:10]}")
|
|
|
|
if "follower_change" in report:
|
|
change_icon = "📈" if report["follower_change"] > 0 else "📉" if report["follower_change"] < 0 else "➡️"
|
|
print(f"\n {change_icon} Change: {report['follower_change']:+,} followers over {report['days_tracked']} days")
|
|
print(f" Daily avg: {report.get('daily_growth', 0):+.1f}/day")
|
|
print(f" Weekly avg: {report.get('weekly_growth', 0):+.1f}/week")
|
|
print(f" 30-day projection: {report.get('monthly_projection', 0):+,}")
|
|
|
|
if "growth_percent" in report:
|
|
print(f" Growth rate: {report['growth_percent']:+.1f}%")
|
|
|
|
if "engagement_trend" in report:
|
|
trend_icon = "📈" if report["engagement_trend"] == "improving" else "📉"
|
|
print(f" Engagement: {trend_icon} {report['engagement_trend']} (avg {report['avg_engagement_rate']}%)")
|
|
|
|
print(f"\n{'='*60}\n")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Track X/Twitter account growth over time",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
parser.add_argument("--record", action="store_true", help="Record a new snapshot")
|
|
parser.add_argument("--report", action="store_true", help="Generate growth report")
|
|
parser.add_argument("--milestone", action="store_true", help="Project when target will be reached")
|
|
|
|
parser.add_argument("--handle", required=True, help="X handle")
|
|
parser.add_argument("--followers", type=int, default=0, help="Current follower count")
|
|
parser.add_argument("--following", type=int, default=0, help="Current following count")
|
|
parser.add_argument("--eng-rate", type=float, default=0, help="Current engagement rate (pct)")
|
|
parser.add_argument("--posts-week", type=float, default=0, help="Posts per week")
|
|
parser.add_argument("--notes", default="", help="Notes for this snapshot")
|
|
parser.add_argument("--period", default="all", help="Report period: 7d, 30d, 90d, all")
|
|
parser.add_argument("--target", type=int, default=0, help="Follower milestone target")
|
|
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.handle.startswith("@"):
|
|
args.handle = f"@{args.handle}"
|
|
|
|
if args.record:
|
|
if args.followers <= 0:
|
|
print("Error: --followers required for recording", file=sys.stderr)
|
|
sys.exit(1)
|
|
entry = record_snapshot(args.handle, args.followers, args.following,
|
|
args.eng_rate, args.posts_week, args.notes)
|
|
if args.json:
|
|
print(json.dumps(entry, indent=2))
|
|
else:
|
|
print(f" ✅ Recorded: {args.handle} — {args.followers:,} followers")
|
|
print(f" File: {get_data_file(args.handle)}")
|
|
|
|
elif args.report:
|
|
period_days = 0
|
|
if args.period != "all":
|
|
period_days = int(args.period.rstrip("d"))
|
|
entries = load_snapshots(args.handle, period_days)
|
|
report = generate_report(args.handle, entries)
|
|
if args.json:
|
|
print(json.dumps(report, indent=2))
|
|
else:
|
|
print_report(report)
|
|
|
|
elif args.milestone:
|
|
if args.target <= 0:
|
|
print("Error: --target required for milestone projection", file=sys.stderr)
|
|
sys.exit(1)
|
|
entries = load_snapshots(args.handle)
|
|
result = project_milestone(args.handle, entries, args.target)
|
|
if args.json:
|
|
print(json.dumps(result, indent=2))
|
|
else:
|
|
if "error" in result:
|
|
print(f" ⚠️ {result['error']}")
|
|
elif "status" in result and "days_needed" not in result:
|
|
print(f" 🎉 {result['status']}")
|
|
else:
|
|
print(f"\n 🎯 Milestone Projection: {result['handle']}")
|
|
print(f" Current: {result['current']:,}")
|
|
print(f" Target: {result['target']:,}")
|
|
print(f" Gap: {result['remaining']:,}")
|
|
print(f" Growth: {result['daily_growth']:+.1f}/day")
|
|
print(f" ETA: {result['projected_date']} (~{result['days_needed']} days)")
|
|
print()
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|