- 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)
295 lines
11 KiB
Python
295 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
X/Twitter Profile Auditor — Audit any X profile for growth readiness.
|
|
|
|
Checks bio quality, pinned tweet, posting patterns, and provides
|
|
actionable recommendations. Works without API access by analyzing
|
|
profile data you provide or scraping public info via web search.
|
|
|
|
Usage:
|
|
python3 profile_auditor.py --handle @username
|
|
python3 profile_auditor.py --handle @username --json
|
|
python3 profile_auditor.py --bio "current bio text" --followers 5000 --posts-per-week 10
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field, asdict
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class ProfileData:
|
|
handle: str = ""
|
|
bio: str = ""
|
|
followers: int = 0
|
|
following: int = 0
|
|
posts_per_week: float = 0
|
|
reply_ratio: float = 0 # % of posts that are replies
|
|
thread_ratio: float = 0 # % of posts that are threads
|
|
has_pinned: bool = False
|
|
pinned_age_days: int = 0
|
|
has_link: bool = False
|
|
has_newsletter: bool = False
|
|
avg_engagement_rate: float = 0 # likes+replies+rts / followers
|
|
|
|
|
|
@dataclass
|
|
class AuditFinding:
|
|
area: str
|
|
status: str # GOOD, WARN, CRITICAL
|
|
message: str
|
|
fix: str = ""
|
|
|
|
|
|
@dataclass
|
|
class AuditReport:
|
|
handle: str
|
|
score: int = 0
|
|
max_score: int = 100
|
|
grade: str = ""
|
|
findings: list = field(default_factory=list)
|
|
recommendations: list = field(default_factory=list)
|
|
|
|
|
|
def audit_bio(profile: ProfileData) -> list:
|
|
findings = []
|
|
bio = profile.bio.strip()
|
|
|
|
if not bio:
|
|
findings.append(AuditFinding("Bio", "CRITICAL", "No bio provided for audit",
|
|
"Provide bio text with --bio flag"))
|
|
return findings
|
|
|
|
# Length check
|
|
if len(bio) < 30:
|
|
findings.append(AuditFinding("Bio", "WARN", f"Bio too short ({len(bio)} chars)",
|
|
"Aim for 100-160 characters with clear value prop"))
|
|
elif len(bio) > 160:
|
|
findings.append(AuditFinding("Bio", "WARN", f"Bio may be too long ({len(bio)} chars)",
|
|
"Keep under 160 chars for readability"))
|
|
else:
|
|
findings.append(AuditFinding("Bio", "GOOD", f"Bio length OK ({len(bio)} chars)"))
|
|
|
|
# Hashtag check
|
|
hashtags = re.findall(r'#\w+', bio)
|
|
if hashtags:
|
|
findings.append(AuditFinding("Bio", "WARN", f"Hashtags in bio ({', '.join(hashtags)})",
|
|
"Remove hashtags — signals amateur. Use plain text."))
|
|
else:
|
|
findings.append(AuditFinding("Bio", "GOOD", "No hashtags in bio"))
|
|
|
|
# Buzzword check
|
|
buzzwords = ['entrepreneur', 'guru', 'ninja', 'rockstar', 'visionary', 'hustler',
|
|
'thought leader', 'serial entrepreneur', 'dreamer', 'doer']
|
|
found = [bw for bw in buzzwords if bw.lower() in bio.lower()]
|
|
if found:
|
|
findings.append(AuditFinding("Bio", "WARN", f"Buzzwords detected: {', '.join(found)}",
|
|
"Replace with specific, concrete descriptions of what you do"))
|
|
|
|
# Specificity check — pipes and slashes often signal unfocused bios
|
|
if bio.count('|') >= 3 or bio.count('/') >= 3:
|
|
findings.append(AuditFinding("Bio", "WARN", "Bio may lack focus (too many roles/identities)",
|
|
"Lead with ONE clear identity. What's the #1 thing you want to be known for?"))
|
|
|
|
# Social proof check
|
|
proof_patterns = [r'\d+[kKmM]?\+?\s*(followers|subscribers|readers|users|customers)',
|
|
r'(founder|ceo|cto|vp|head|director|lead)\s+(of|at|@)',
|
|
r'(author|writer)\s+of', r'featured\s+in', r'ex-\w+']
|
|
has_proof = any(re.search(p, bio, re.IGNORECASE) for p in proof_patterns)
|
|
if has_proof:
|
|
findings.append(AuditFinding("Bio", "GOOD", "Social proof detected"))
|
|
else:
|
|
findings.append(AuditFinding("Bio", "WARN", "No obvious social proof in bio",
|
|
"Add a credential: title, metric, brand association, or achievement"))
|
|
|
|
# CTA/Link check
|
|
if profile.has_link:
|
|
findings.append(AuditFinding("Bio", "GOOD", "Profile has a link"))
|
|
else:
|
|
findings.append(AuditFinding("Bio", "WARN", "No link in profile",
|
|
"Add a link to newsletter, product, or portfolio"))
|
|
|
|
return findings
|
|
|
|
|
|
def audit_activity(profile: ProfileData) -> list:
|
|
findings = []
|
|
|
|
# Posting frequency
|
|
if profile.posts_per_week <= 0:
|
|
findings.append(AuditFinding("Activity", "CRITICAL", "No posting data provided",
|
|
"Provide --posts-per-week estimate"))
|
|
elif profile.posts_per_week < 3:
|
|
findings.append(AuditFinding("Activity", "CRITICAL",
|
|
f"Very low posting ({profile.posts_per_week:.0f}/week)",
|
|
"Minimum 7 posts/week (1/day). Aim for 14-21."))
|
|
elif profile.posts_per_week < 7:
|
|
findings.append(AuditFinding("Activity", "WARN",
|
|
f"Low posting ({profile.posts_per_week:.0f}/week)",
|
|
"Aim for 2-3 posts per day for consistent growth"))
|
|
elif profile.posts_per_week < 21:
|
|
findings.append(AuditFinding("Activity", "GOOD",
|
|
f"Good posting cadence ({profile.posts_per_week:.0f}/week)"))
|
|
else:
|
|
findings.append(AuditFinding("Activity", "GOOD",
|
|
f"High posting cadence ({profile.posts_per_week:.0f}/week)"))
|
|
|
|
# Reply ratio
|
|
if profile.reply_ratio > 0:
|
|
if profile.reply_ratio < 0.2:
|
|
findings.append(AuditFinding("Activity", "WARN",
|
|
f"Low reply ratio ({profile.reply_ratio:.0%})",
|
|
"Aim for 30%+ replies. Engage with others, don't just broadcast."))
|
|
elif profile.reply_ratio >= 0.3:
|
|
findings.append(AuditFinding("Activity", "GOOD",
|
|
f"Healthy reply ratio ({profile.reply_ratio:.0%})"))
|
|
|
|
# Follower/following ratio
|
|
if profile.followers > 0 and profile.following > 0:
|
|
ratio = profile.followers / profile.following
|
|
if ratio < 0.5:
|
|
findings.append(AuditFinding("Profile", "WARN",
|
|
f"Low follower/following ratio ({ratio:.1f}x)",
|
|
"Unfollow inactive accounts. Ratio should trend toward 2:1+"))
|
|
elif ratio >= 2:
|
|
findings.append(AuditFinding("Profile", "GOOD",
|
|
f"Healthy follower/following ratio ({ratio:.1f}x)"))
|
|
|
|
# Pinned tweet
|
|
if profile.has_pinned:
|
|
if profile.pinned_age_days > 30:
|
|
findings.append(AuditFinding("Profile", "WARN",
|
|
f"Pinned tweet is {profile.pinned_age_days} days old",
|
|
"Update pinned tweet monthly with your latest best content"))
|
|
else:
|
|
findings.append(AuditFinding("Profile", "GOOD", "Pinned tweet is recent"))
|
|
else:
|
|
findings.append(AuditFinding("Profile", "WARN", "No pinned tweet",
|
|
"Pin your best-performing tweet or thread. It's your landing page."))
|
|
|
|
return findings
|
|
|
|
|
|
def calculate_score(findings: list) -> tuple:
|
|
total = len(findings)
|
|
if total == 0:
|
|
return 0, "F"
|
|
|
|
good = sum(1 for f in findings if f.status == "GOOD")
|
|
score = int((good / total) * 100)
|
|
|
|
if score >= 90:
|
|
grade = "A"
|
|
elif score >= 75:
|
|
grade = "B"
|
|
elif score >= 60:
|
|
grade = "C"
|
|
elif score >= 40:
|
|
grade = "D"
|
|
else:
|
|
grade = "F"
|
|
|
|
return score, grade
|
|
|
|
|
|
def generate_recommendations(findings: list, profile: ProfileData) -> list:
|
|
recs = []
|
|
criticals = [f for f in findings if f.status == "CRITICAL"]
|
|
warns = [f for f in findings if f.status == "WARN"]
|
|
|
|
for f in criticals:
|
|
if f.fix:
|
|
recs.append(f"🔴 {f.fix}")
|
|
|
|
for f in warns[:3]: # Top 3 warnings
|
|
if f.fix:
|
|
recs.append(f"🟡 {f.fix}")
|
|
|
|
# Stage-specific advice
|
|
if profile.followers < 1000:
|
|
recs.append("📈 Growth phase: Focus 70% on replies to larger accounts, 30% on your own posts")
|
|
elif profile.followers < 10000:
|
|
recs.append("📈 Momentum phase: 2-3 threads/week + daily engagement. Start a recurring series.")
|
|
else:
|
|
recs.append("📈 Scale phase: Leverage audience with cross-platform repurposing + newsletter growth")
|
|
|
|
return recs
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Audit an X/Twitter profile for growth readiness",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
%(prog)s --handle @rezarezvani --bio "CTO building AI products" --followers 5000
|
|
%(prog)s --bio "Entrepreneur | Dreamer | Hustle" --followers 200 --posts-per-week 3
|
|
%(prog)s --handle @example --followers 50000 --posts-per-week 21 --reply-ratio 0.4 --json
|
|
""")
|
|
|
|
parser.add_argument("--handle", default="@unknown", help="X handle")
|
|
parser.add_argument("--bio", default="", help="Current bio text")
|
|
parser.add_argument("--followers", type=int, default=0, help="Follower count")
|
|
parser.add_argument("--following", type=int, default=0, help="Following count")
|
|
parser.add_argument("--posts-per-week", type=float, default=0, help="Average posts per week")
|
|
parser.add_argument("--reply-ratio", type=float, default=0, help="Fraction of posts that are replies (0-1)")
|
|
parser.add_argument("--has-pinned", action="store_true", help="Has a pinned tweet")
|
|
parser.add_argument("--pinned-age-days", type=int, default=0, help="Age of pinned tweet in days")
|
|
parser.add_argument("--has-link", action="store_true", help="Has link in profile")
|
|
parser.add_argument("--json", action="store_true", help="Output JSON")
|
|
|
|
args = parser.parse_args()
|
|
|
|
profile = ProfileData(
|
|
handle=args.handle,
|
|
bio=args.bio,
|
|
followers=args.followers,
|
|
following=args.following,
|
|
posts_per_week=args.posts_per_week,
|
|
reply_ratio=args.reply_ratio,
|
|
has_pinned=args.has_pinned,
|
|
pinned_age_days=args.pinned_age_days,
|
|
has_link=args.has_link,
|
|
)
|
|
|
|
findings = audit_bio(profile) + audit_activity(profile)
|
|
score, grade = calculate_score(findings)
|
|
recs = generate_recommendations(findings, profile)
|
|
|
|
report = AuditReport(
|
|
handle=profile.handle,
|
|
score=score,
|
|
grade=grade,
|
|
findings=[asdict(f) for f in findings],
|
|
recommendations=recs,
|
|
)
|
|
|
|
if args.json:
|
|
print(json.dumps(asdict(report), indent=2))
|
|
else:
|
|
print(f"\n{'='*60}")
|
|
print(f" X PROFILE AUDIT — {report.handle}")
|
|
print(f"{'='*60}")
|
|
print(f"\n Score: {report.score}/100 (Grade: {report.grade})\n")
|
|
|
|
for f in findings:
|
|
icon = {"GOOD": "✅", "WARN": "⚠️", "CRITICAL": "🔴"}.get(f.status, "❓")
|
|
print(f" {icon} [{f.area}] {f.message}")
|
|
if f.fix and f.status != "GOOD":
|
|
print(f" → {f.fix}")
|
|
|
|
if recs:
|
|
print(f"\n {'─'*56}")
|
|
print(f" TOP RECOMMENDATIONS\n")
|
|
for i, r in enumerate(recs, 1):
|
|
print(f" {i}. {r}")
|
|
|
|
print(f"\n{'='*60}\n")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|