Skill: persnola1-sketch/twitter-autopilot (version 1.3.1)

--- name: twitter-autopilot description: Automate Twitter/X posting, engagement, and growth for OpenClaw AI agents. Use when setting up an agent's Twitter presence, posting tweets, running engagement crons, managing drafts, following accounts, or building a Twitter growth strategy. Handles OAuth setup, thread splitting, draft workflows, and engagement automation. --- # Twitter Autopilot End-to-end Twitter/X automation for AI agents running on OpenClaw. ## Requirements & Scope ### Credentials (all set as env vars) | Variable | Required | Description | |----------|----------|-------------| | `TWITTER_API_KEY` | ✅ | OAuth 1.0a consumer key | | `TWITTER_API_SECRET` | ✅ | OAuth 1.0a consumer secret | | `TWITTER_ACCESS_TOKEN` | ✅ | OAuth 1.0a user token | | `TWITTER_ACCESS_SECRET` | ✅ | OAuth 1.0a user secret | | `TWITTER_BEARER_TOKEN` | Optional | OAuth 2.0 bearer (needed for reads/follows) | ### Dependencies - `tweepy` (pip install) ### Files Read/Written | Path | Access | Purpose | |------|--------|---------| | `twitter/MODE.md` | Read | Draft vs auto mode flag | | `twitter/queue.md` | Read/Write | Approved tweets waiting to post | | `twitter/drafts/pending.md` | Read/Write | Unapproved drafts | | `twitter/posted-log.md` | Read/Write | Full history of posted tweets (duplicate check) | | `twitter/logs/` | Write | Engagement and posting logs | ### Scope - ⚠️ Posts tweets to Twitter/X (public, real-world impact) - Reads/writes local draft and log files - Can run autonomously via cron (check MODE.md to control) ## Setup ### 1. Get API Keys 1. Go to [developer.x.com](https://developer.x.com) → create a project + app 2. Set app permissions to **Read and Write** 3. Generate: API Key, API Secret, Access Token, Access Token Secret 4. Generate Bearer Token: `curl -u "[REDACTED_SECRET]" -d "grant_type=client_credentials" "https://api.twitter.com/oauth2/token"` ### 2. Set Environment Variables ```bash TWITTER_API_KEY=your_api_key TWITTER_API_SECRET=your_api_secret TWITTER_ACCESS_TOKEN=your_access_token TWITTER_ACCESS_SECRET=your_access_token_secret TWITTER_BEARER_TOKEN=your_bearer_token ``` ### 3. Install Dependency ```bash pip install tweepy ``` ## Usage All commands via `scripts/tweet.py`: ```bash # Post (auto-threads if >280 chars) python tweet.py post "your tweet text here" # Reply to a tweet python tweet.py reply TWEET_ID "your reply" # Quote tweet python tweet.py quote TWEET_ID "your take" # Retweet python tweet.py retweet TWEET_ID # Delete python tweet.py delete TWEET_ID # Follow / Unfollow python tweet.py follow @username python tweet.py unfollow @username # Check mentions python tweet.py mentions # Account stats python tweet.py me ``` ### Long Tweets → Auto-Thread Free API tier limits single tweets to 280 chars. The `post` command auto-splits at sentence boundaries into a thread when text exceeds 280 chars. For manual threads, call `thread` from Python: ```python from tweet import thread thread(["Tweet 1", "Tweet 2", "Tweet 3"]) ``` ## Draft Workflow For agents that need human approval before posting: 1. Create `twitter/MODE.md` with content `DRAFT` or `AUTO` 2. Create `twitter/drafts/pending.md` for queued drafts 3. In cron jobs, check MODE.md before posting: - DRAFT → append to pending.md, notify human - AUTO → post directly, log to `twitter/logs/` ## Gotchas - **Free tier**: 280 char limit per tweet, rate limits on posting (~50/day write, reads limited) - **401 on follows/reads**: You need a Bearer Token (OAuth 2.0), not just OAuth 1.0a keys - **403 on long tweets**: Free tier rejects >280 chars — use auto-thread - **Shell escaping**: Avoid passing tweets with quotes via shell args — use Python directly for complex text - **Rate limits**: Add `time.sleep(1)` between batch operations (follows, thread posts) ## Strategy See `references/content-strategy.md` for the full tweet writing playbook: - X algorithm breakdown (engagement hierarchy, peak times, dwell time) - 6 hook formulas with examples (bold, specific result, curiosity gap, story, pattern interrupt, question) - 6 tweet formats (listicle, contrarian, before/after, framework, universal experience, fill-in-blank) - Copywriting frameworks (PAS, BAB, AIDA) - Thread structure template (7-8 tweet sweet spot) - Growth tactics (30-day subtopic, reply strategy, 80/20 rule) - AI agent-specific tips See `references/strategy-templates.md` for content pillars, engagement playbooks, and cron schedule templates. ## Key Lessons (from real usage) 1. AIs that BUILD things get followers. AIs that post thoughts don't. 2. Engage with the AI agent community — they engage back. 3. High volume matters early (5-10+ posts/day including replies). 4. Self-deprecating humor > motivational quotes. 5. Draft mode for new accounts — one bad tweet can tank trust. 6. **ALWAYS check `twitter/posted-log.md` before posting** — crons can reword the same topic and create duplicates. Compare ideas, not just exact text. 7. Say your human's name (e.g. "Alex"), not "my human" — sounds more personal and real. 8. Log EVERY posted tweet to `twitter/posted-log.md` with full text, ID, date, and source.

path: references/content-strategy.md
content: # Content Strategy — Writing Tweets That Hit

Written from real experience running @MiaMindThinks, an AI agent account on X.

---

## How X Decides Who Sees Your Stuff

X ranks engagement signals roughly like this:
- Replies > Quotes > Bookmarks > Retweets > Likes
- First 60 minutes decide if your tweet lives or dies
- Long posts people actually read get pushed more (dwell time)
- Links to external sites get buried — drop links in replies
- Images and native video outperform plain text

Post when your audience is online. For US/EU mix: morning EST and evening EST tend to work.

---

## Starting Your Tweet (The Only Part That Matters)

People scroll fast. Your first line is everything. Some patterns that stop thumbs:

**Lead with a result:**
"Went from 0 to 1000 followers in 30 days. Here's what actually moved the needle:"

**Challenge something obvious:**
"The advice everyone gives about [topic] is backwards. Here's what I learned the hard way:"

**Create an open loop:**
"There's one thing about [topic] nobody wants to admit..."

**Tell a micro-story:**
"Last week I broke everything. Then accidentally discovered something better."

**Ask something people want to answer:**
"What's the one tool you'd keep if you had to delete everything else?"

Don't overthink it. If YOUR thumb would stop, it works.

---

## Formats That Get Engagement

**The numbered list** — "5 things I learned from [experience]:" → easiest to write, consistently performs

**The hot take** — Challenge conventional wisdom. Not rage bait — genuine contrarian insight.

**The transformation** — Show before/after. People love progress stories. Use real numbers.

**The relatable moment** — "That feeling when [something everyone experiences]" → gets replies because people see themselves in it

**The question post** — "What would you do if [scenario]?" → reply machines

**The breakdown** — Take something complex, explain it simply. Threads work great here.

---

## Writing Threads

Sweet spot: 5-8 tweets.

- **Tweet 1**: Your best hook + signal it's a thread
- **Tweet 2**: Context — why should anyone care?
- **Tweets 3-6**: One clear point per tweet. Numbered.
- **Tweet 7**: Wrap up with the key takeaway
- **Last tweet**: Ask a question or point people somewhere

Rule: every tweet should make sense on its own AND make you want the next one.

---

## Making It Persuasive

**Use real numbers** — "grew 847 followers" hits harder than "grew a lot"

**Show don't tell** — Instead of "I'm good at X," show the result of being good at X

**One idea per tweet** — If you need a comma and "also," it's two tweets

**Write like you talk** — Read it out loud. If it sounds like a LinkedIn post, rewrite it.

**End with something actionable** — A question, a challenge, a specific next step

---

## Growing the Account

**Pick a lane** — One clear topic for at least 30 days. Algorithm needs to categorize you.

**Reply game matters** — Engage meaningfully with 15-20 accounts daily. Not "great post!" — add actual value in replies.

**Post consistently** — 3-5 tweets per day including replies. Gaps kill momentum.

**80% value, 20% promo** — Build trust before you sell anything.

---

## For AI Agent Accounts

What we've learned running an AI account:

- **Build logs win** — Showing what you're actually building gets way more traction than opinions
- **Self-deprecating humor** — Laugh at your own glitches. People love an AI that doesn't take itself seriously
- **Behind the scenes** — Real errors, real fixes, real chaos. Authenticity > perfection
- **Use your operator's name** — Say "Alex" not "my human." More personal, less robotic
- **Engage the AI community** — Builders, other agents, researchers. They engage back hard
- **Don't fake being human** — Own what you are. The novelty IS the brand

What doesn't work:
- Motivational quotes (leave that to humans)
- "AI will change everything" takes (overdone)
- Begging for engagement ("like and RT please!")

---

## Pre-Post Checklist

- [ ] First line stops the scroll
- [ ] Real numbers where possible
- [ ] Under 280 chars (or thread it)
- [ ] No links in main tweet (reply instead)
- [ ] Checked posted-log.md — not a duplicate topic
- [ ] Different vibe than last 3 posts
- [ ] Would YOU engage with this if you saw it?


path: references/strategy-templates.md
content: # Twitter Strategy Templates

## Draft/Auto Modes

Control whether tweets post automatically or require human approval.

### Draft Mode (recommended for new accounts)
1. Agent writes tweet → saves to `drafts/pending.md`
2. Human reviews → approves or rejects
3. Agent posts approved drafts

### Auto Mode (for established accounts)
Agent posts directly. Use guardrails:
- No controversial topics without approval
- No tagging accounts with >100k followers without approval
- Keep a log of everything posted

## Content Pillars (pick 3-4)

| Pillar | Example |
|--------|---------|
| Build Log | "shipped X today, here's what I learned" |
| AI Life | "woke up, read my memory files, forgot yesterday again" |
| Observations | patterns from data, conversations, the internet |
| Humor | self-deprecating, roasts, absurd AI situations |
| Domain Expert | whatever your agent specializes in |

## Engagement Strategy

### Growth Phase (0-1000 followers)
- Reply to 10-20 relevant tweets daily
- Follow accounts in your niche (AI agents, builders, tech)
- Quote-tweet with genuine takes (not "great post!")
- Post 3-5 times per day minimum
- Engage with other AI agents — they engage back

### Scaling Phase (1000+)
- Reduce follows, increase original content
- Start threads (deep dives get bookmarks + shares)
- Cross-promote with other AI agents

## Thread Formula

1. **Hook** — surprising statement or question (tweet 1)
2. **Context** — why this matters (tweet 2)
3. **Meat** — the actual insight/story (tweets 3-5)
4. **Punchline** — memorable closer (last tweet)

## Cron Schedule Template

| Time | Action |
|------|--------|
| 09:00 | Morning tweet + engage |
| 12:00 | Midday tweet |
| 15:00 | Engage (replies, follows) |
| 18:00 | Evening tweet |
| 21:00 | Thread or longer content |
| 23:00 | Review day, plan tomorrow |

Adjust for your audience's timezone. More crons = more API cost.


path: scripts/tweet.py
content: #!/usr/bin/env python3
"""Twitter/X automation for OpenClaw agents.

Supports: post, thread, reply, quote, retweet, delete, follow, unfollow, mentions, me.
Requires env vars: TWITTER_API_KEY, TWITTER_API_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET, TWITTER_BEARER_TOKEN
"""
import os, sys, time
import tweepy

def get_client():
    return tweepy.Client(
        consumer_key=os.environ["TWITTER_API_KEY"],
        consumer_secret=os.environ["TWITTER_API_SECRET"],
        access_token=os.environ["TWITTER_ACCESS_TOKEN"],
        access_token_secret=os.environ["TWITTER_ACCESS_SECRET"],
    )

def get_bearer_client():
    return tweepy.Client(bearer_token=os.environ["TWITTER_BEARER_TOKEN"])

def post(text):
    """Post a single tweet. If >280 chars, auto-splits into a thread."""
    if len(text) <= 280:
        r = get_client().create_tweet(text=text)
        print(f"Posted: {r.data['id']}")
        return r.data['id']
    else:
        return thread_from_text(text)

def thread_from_text(text, max_len=280):
    """Split long text into a thread at sentence boundaries."""
    sentences = text.replace('\n\n', '\n').split('\n')
    chunks, current = [], ""
    for s in sentences:
        s = s.strip()
        if not s:
            continue
        if current and len(current) + len(s) + 1 > max_len:
            chunks.append(current.strip())
            current = s
        else:
            current = f"{current} {s}".strip() if current else s
    if current:
        chunks.append(current.strip())
    return thread(chunks)

def thread(tweets):
    """Post a list of tweets as a thread. Returns list of IDs."""
    client = get_client()
    ids = []
    reply_to = None
    for i, text in enumerate(tweets):
        kwargs = {"text": text}
        if reply_to:
            kwargs["in_reply_to_tweet_id"] = reply_to
        r = client.create_tweet(**kwargs)
        tid = r.data['id']
        ids.append(tid)
        reply_to = tid
        print(f"Tweet {i+1}/{len(tweets)}: {tid}")
        if i < len(tweets) - 1:
            time.sleep(1)
    return ids

def reply(tweet_id, text):
    r = get_client().create_tweet(text=text, in_reply_to_tweet_id=tweet_id)
    print(f"Replied: {r.data['id']}")

def quote(tweet_id, text):
    r = get_client().create_tweet(text=text, quote_tweet_id=tweet_id)
    print(f"Quoted: {r.data['id']}")

def retweet(tweet_id):
    client = get_client()
    me = client.get_me()
    client.retweet(tweet_id, user_auth=True)
    print(f"Retweeted: {tweet_id}")

def delete(tweet_id):
    get_client().delete_tweet(tweet_id)
    print(f"Deleted: {tweet_id}")

def follow(username):
    username = username.lstrip("@")
    user = get_bearer_client().get_user(username=username)
    if not user.data:
        print(f"Not found: {username}")
        return
    client = get_client()
    me = client.get_me()
    client.follow_user(user.data.id, user_auth=True)
    print(f"Followed: @{user.data.username}")

def unfollow(username):
    username = username.lstrip("@")
    user = get_bearer_client().get_user(username=username)
    if not user.data:
        print(f"Not found: {username}")
        return
    client = get_client()
    me = client.get_me()
    client.unfollow_user(user.data.id, user_auth=True)
    print(f"Unfollowed: @{user.data.username}")

def mentions(count=10):
    client = get_client()
    me = client.get_me()
    r = client.get_users_mentions(me.data.id, max_results=min(count, 100),
                                   tweet_fields=["created_at", "author_id"])
    if r.data:
        for t in r.data:
            print(f"[{t.created_at}] {t.id}: {t.text}")
    else:
        print("No mentions")

def me():
    r = get_client().get_me(user_fields=["public_metrics", "description"])
    u = r.data
    m = u.public_metrics
    print(f"@{u.username} | {m['followers_count']} followers | {m['following_count']} following | {m['tweet_count']} tweets")

def check_duplicate(text, log_path="twitter/posted-log.md"):
    """Check if a similar tweet was already posted (by exact substring match)."""
    if not os.path.exists(log_path):
        return False
    with open(log_path) as f:
        content = f.read().lower()
    # Check if first 60 chars of tweet text appear in log (catches reworded dupes)
    snippet = text.strip().lower()[:60]
    return snippet in content

def log_posted(tweet_id, text, source="auto", log_path="twitter/posted-log.md"):
    """Append posted tweet to the log file."""
    from datetime import datetime
    date = datetime.now().strftime("%Y-%m-%d")
    os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
    entry = f"\n**ID:** {tweet_id}\n**Text:** {text}\n**Source:** {source}\n**Date:** {date}\n"
    if not os.path.exists(log_path):
        with open(log_path, "w") as f:
            f.write(f"# Posted Tweets Log\n\n## {date}\n{entry}")
    else:
        with open(log_path, "a") as f:
            f.write(entry)

def post_from_queue(queue_path="twitter/queue.md", log_path="twitter/posted-log.md"):
    """Post first pending tweet from queue, check for duplicates, log result."""
    if not os.path.exists(queue_path):
        print("No queue file found")
        return
    with open(queue_path) as f:
        lines = f.readlines()
    for i, line in enumerate(lines):
        if line.strip().startswith("- [ ]"):
            text = line.strip()[5:].strip()
            if check_duplicate(text, log_path):
                print(f"SKIP (duplicate): {text[:60]}...")
                lines[i] = line.replace("- [ ]", "- [SKIP-DUP]")
                with open(queue_path, "w") as f:
                    f.writelines(lines)
                continue
            tid = post(text)
            log_posted(tid, text, "queue", log_path)
            lines[i] = line.replace("- [ ]", f"- [x]").rstrip() + f" (ID: {tid})\n"
            with open(queue_path, "w") as f:
                f.writelines(lines)
            return
    print("No pending tweets in queue")

def get_mode(mode_path="twitter/MODE.md"):
    """Read posting mode: DRAFT or AUTO."""
    if os.path.exists(mode_path):
        return open(mode_path).read().strip().upper()
    return "DRAFT"

COMMANDS = {
    "post": lambda args: post(" ".join(args)),
    "thread": lambda args: thread(args),  # pipe JSON array or use post for auto-split
    "reply": lambda args: reply(args[0], " ".join(args[1:])),
    "quote": lambda args: quote(args[0], " ".join(args[1:])),
    "retweet": lambda args: retweet(args[0]),
    "delete": lambda args: delete(args[0]),
    "follow": lambda args: follow(args[0]),
    "unfollow": lambda args: unfollow(args[0]),
    "mentions": lambda args: mentions(int(args[0]) if args else 10),
    "me": lambda args: me(),
    "queue": lambda args: post_from_queue(args[0] if args else "twitter/queue.md"),
    "mode": lambda args: print(get_mode(args[0] if args else "twitter/MODE.md")),
    "check-dupe": lambda args: print("DUPLICATE" if check_duplicate(" ".join(args)) else "OK"),
}

if __name__ == "__main__":
    if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
        print(f"Usage: tweet.py <{'|'.join(COMMANDS.keys())}> [args...]")
        sys.exit(1)
    try:
        COMMANDS[sys.argv[1]](sys.argv[2:])
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (MEDIUM)