Back to Blog

Build a Twitter AI Agent in Python: Complete Tutorial with OpenTweet API

OpenTweet Team12 min read
Build a Twitter AI Agent in Python: Complete Tutorial with OpenTweet API

Build a Twitter AI Agent in Python: Complete Tutorial with OpenTweet API

You don't need a Twitter API key to build a Twitter bot anymore.

That might sound wrong, but hear me out. The official Twitter/X API costs $100/month for the Basic tier, requires OAuth setup, and has rate limits that'll make you question your life choices. For a simple bot that generates and posts tweets, that's massive overkill.

Instead, we're going to build an AI Twitter agent in Python that uses Claude (or GPT-4) for content generation and the OpenTweet API for posting. Total cost: a few cents per tweet for the LLM, plus $5.99/month for OpenTweet.

The agent will be able to:

  • Generate tweet content from any topic using AI
  • Publish tweets immediately
  • Schedule tweets for future dates
  • Create threads
  • Bulk-schedule a week of content in one call

By the end of this tutorial, you'll have a working ~60-line Python script that does all of this. Let's build it.


What We're Building

A Python script that works like this:

# Generate and publish a tweet right now
python agent.py publish "developer productivity tips"

# Generate and schedule a tweet
python agent.py schedule "startup lessons" "2026-02-24T09:00:00Z"

# Generate and schedule a full week of content
python agent.py week "AI and machine learning"

The agent calls Claude to generate the tweet text, then calls the OpenTweet API to create and publish (or schedule) the post. Simple, no magic.


Prerequisites

You need three things:

  1. Python 3.8+ (you probably already have this)
  2. An OpenTweet account -- sign up at opentweet.io (7-day free trial). Connect your X account and generate an API key from Settings > API.
  3. An Anthropic API key (for Claude) or an OpenAI API key (for GPT-4). We'll show both.

Step 1: Install Dependencies

Two packages. That's it.

If using Claude (Anthropic):

pip install requests anthropic

If using GPT-4 (OpenAI):

pip install requests openai

Step 2: Set Up Environment Variables

Don't hardcode API keys. Set them as environment variables:

# OpenTweet API key (get it from opentweet.io Settings > API)
export OPENTWEET_API_KEY="ot_your_key_here"

# Pick one:
export ANTHROPIC_API_KEY="sk-ant-..."  # For Claude
export OPENAI_API_KEY="sk-..."         # For GPT-4

On macOS/Linux, add these to your ~/.bashrc or ~/.zshrc so they persist.


Step 3: Write the Content Generator

This function takes a topic and returns tweet-length text. Here's the Claude version:

import anthropic
import os

def generate_tweet(topic: str) -> str:
    """Generate a tweet about the given topic using Claude."""
    client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=280,
        messages=[
            {
                "role": "user",
                "content": f"""Write a single tweet about: {topic}

Rules:
- Under 280 characters
- Conversational, direct tone
- No hashtags, no emojis
- Start with a bold statement or specific detail
- Sound like a real person, not a brand

Return ONLY the tweet text, nothing else.""",
            }
        ],
    )
    return message.content[0].text.strip()

And here's the OpenAI version if you prefer GPT-4:

from openai import OpenAI
import os

def generate_tweet(topic: str) -> str:
    """Generate a tweet about the given topic using GPT-4."""
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

    response = client.chat.completions.create(
        model="gpt-4o",
        max_tokens=280,
        messages=[
            {
                "role": "user",
                "content": f"""Write a single tweet about: {topic}

Rules:
- Under 280 characters
- Conversational, direct tone
- No hashtags, no emojis
- Start with a bold statement or specific detail
- Sound like a real person, not a brand

Return ONLY the tweet text, nothing else.""",
            }
        ],
    )
    return response.choices[0].message.content.strip()

Pick one. They both work. The prompt is what matters -- you can tune it to match your voice later.


Step 4: Write the OpenTweet Poster

Now the function that takes tweet text and pushes it to X through the OpenTweet API. Two API calls: create the post, then publish it.

import requests
import os

OPENTWEET_BASE = "https://opentweet.io"
OPENTWEET_KEY = os.environ["OPENTWEET_API_KEY"]

def get_headers() -> dict:
    """Return auth headers for OpenTweet API."""
    return {
        "Authorization": f"Bearer {OPENTWEET_KEY}",
        "Content-Type": "application/json",
    }

def create_post(text: str) -> dict:
    """Create a draft post on OpenTweet."""
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={"text": text},
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

def publish_post(post_id: str) -> dict:
    """Publish a draft post to X immediately."""
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts/{post_id}/publish",
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

def schedule_post(post_id: str, scheduled_date: str) -> dict:
    """Schedule a post for a future date (ISO 8601 format)."""
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts/{post_id}/schedule",
        json={"scheduled_date": scheduled_date},
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

Clean separation: create_post creates a draft, publish_post pushes it live, schedule_post queues it for later. The OpenTweet API handles the actual Twitter authentication and posting -- you never touch OAuth.


Step 5: Combine Into the Agent

Time to wire it together. Generate content, create a post, and publish or schedule it:

def tweet_now(topic: str) -> None:
    """Generate a tweet about a topic and publish it immediately."""
    print(f"Generating tweet about: {topic}")
    text = generate_tweet(topic)
    print(f"Generated: {text}")

    post = create_post(text)
    post_id = post["data"]["id"]
    print(f"Created post {post_id}")

    publish_post(post_id)
    print("Published to X!")

def tweet_later(topic: str, scheduled_date: str) -> None:
    """Generate a tweet and schedule it for later."""
    print(f"Generating tweet about: {topic}")
    text = generate_tweet(topic)
    print(f"Generated: {text}")

    post = create_post(text)
    post_id = post["data"]["id"]
    print(f"Created post {post_id}")

    schedule_post(post_id, scheduled_date)
    print(f"Scheduled for {scheduled_date}")

Two functions, each does exactly one thing. tweet_now generates and publishes immediately. tweet_later generates and schedules for a specific time.


Step 6: Add Bulk Scheduling

This is where it gets interesting. Instead of creating one tweet at a time, you can generate a full week of content and schedule it all at once using the OpenTweet bulk endpoint.

from datetime import datetime, timedelta

def generate_week(topic: str) -> None:
    """Generate and schedule 7 tweets (one per day for the next week)."""
    print(f"Generating a week of content about: {topic}")

    # Generate 7 tweets
    posts = []
    base_date = datetime.utcnow() + timedelta(days=1)

    for i in range(7):
        text = generate_tweet(topic)
        scheduled = (base_date + timedelta(days=i)).replace(
            hour=14, minute=0, second=0
        )
        posts.append({
            "text": text,
            "scheduled_date": scheduled.strftime("%Y-%m-%dT%H:%M:%SZ"),
        })
        print(f"  Day {i + 1}: {text[:60]}...")

    # Bulk create all 7 posts in one API call
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={"posts": posts},
        headers=get_headers(),
    )
    resp.raise_for_status()
    print(f"Scheduled {len(posts)} tweets for the next 7 days!")

The OpenTweet bulk endpoint accepts up to 50 posts in a single call. Each post gets its own scheduled_date, so you can spread them across the week. One API call, seven tweets, done.


Step 7: Add Thread Support

Threads are big on X. Here's how to generate and post a multi-tweet thread:

def generate_thread(topic: str, thread_length: int = 4) -> list[str]:
    """Generate a thread of tweets about a topic."""
    client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1200,
        messages=[
            {
                "role": "user",
                "content": f"""Write a Twitter thread about: {topic}

The thread should have exactly {thread_length} tweets.
Rules for each tweet:
- Under 280 characters each
- First tweet is the hook (make people want to read the rest)
- Last tweet is a summary or call to action
- Conversational, direct tone
- No hashtags, no emojis
- No numbering (no "1/", "2/", etc.)

Return each tweet on a separate line, separated by ---""",
            }
        ],
    )

    tweets = [
        t.strip()
        for t in message.content[0].text.strip().split("---")
        if t.strip()
    ]
    return tweets[:thread_length]

def post_thread(topic: str) -> None:
    """Generate and publish a thread about a topic."""
    tweets = generate_thread(topic)
    print(f"Generated thread with {len(tweets)} tweets")

    for i, tweet in enumerate(tweets):
        print(f"  [{i + 1}] {tweet[:60]}...")

    # Create the thread using OpenTweet's thread support
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={
            "text": tweets[0],
            "is_thread": True,
            "thread_tweets": tweets[1:],
        },
        headers=get_headers(),
    )
    resp.raise_for_status()
    post_id = resp.json()["data"]["id"]

    # Publish the thread
    publish_post(post_id)
    print("Thread published to X!")

The OpenTweet API handles thread creation natively. You pass the first tweet as text, set is_thread to True, and include the rest in thread_tweets. One API call creates the entire thread, and one more publishes it.


Full Working Script

Here's the complete agent. Save this as agent.py:

#!/usr/bin/env python3
"""
Twitter AI Agent - Generate and post tweets using Claude + OpenTweet API.

Usage:
    python agent.py publish "topic here"
    python agent.py schedule "topic" "2026-02-24T09:00:00Z"
    python agent.py week "topic"
    python agent.py thread "topic"
    python agent.py check
"""

import os
import sys
import requests
import anthropic
from datetime import datetime, timedelta

# --- Config ---
OPENTWEET_BASE = "https://opentweet.io"
OPENTWEET_KEY = os.environ["OPENTWEET_API_KEY"]
ANTHROPIC_KEY = os.environ["ANTHROPIC_API_KEY"]

def get_headers():
    return {
        "Authorization": f"Bearer {OPENTWEET_KEY}",
        "Content-Type": "application/json",
    }

# --- AI Content Generation ---
def generate_tweet(topic):
    client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=280,
        messages=[{
            "role": "user",
            "content": f"""Write a single tweet about: {topic}

Rules:
- Under 280 characters
- Conversational, direct tone
- No hashtags, no emojis
- Start with a bold statement or specific detail
- Sound like a real person, not a brand

Return ONLY the tweet text, nothing else.""",
        }],
    )
    return message.content[0].text.strip()

def generate_thread(topic, length=4):
    client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
    message = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1200,
        messages=[{
            "role": "user",
            "content": f"""Write a Twitter thread about: {topic}

Exactly {length} tweets. Rules:
- Under 280 characters each
- First tweet is the hook
- Last tweet is a summary or CTA
- Conversational tone, no hashtags, no emojis, no numbering

Separate each tweet with ---""",
        }],
    )
    tweets = [t.strip() for t in message.content[0].text.strip().split("---") if t.strip()]
    return tweets[:length]

# --- OpenTweet API ---
def check_connection():
    resp = requests.get(f"{OPENTWEET_BASE}/api/v1/me", headers=get_headers())
    resp.raise_for_status()
    data = resp.json()
    print(f"Connected! Account: {data.get('data', {}).get('username', 'unknown')}")
    return data

def create_post(text):
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={"text": text},
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

def publish_post(post_id):
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts/{post_id}/publish",
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

def schedule_post(post_id, scheduled_date):
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts/{post_id}/schedule",
        json={"scheduled_date": scheduled_date},
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

def bulk_create(posts):
    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={"posts": posts},
        headers=get_headers(),
    )
    resp.raise_for_status()
    return resp.json()

# --- Agent Commands ---
def cmd_publish(topic):
    print(f"Generating tweet about: {topic}")
    text = generate_tweet(topic)
    print(f"Generated: {text}\n")

    post = create_post(text)
    post_id = post["data"]["id"]

    publish_post(post_id)
    print("Published to X!")

def cmd_schedule(topic, scheduled_date):
    print(f"Generating tweet about: {topic}")
    text = generate_tweet(topic)
    print(f"Generated: {text}\n")

    post = create_post(text)
    post_id = post["data"]["id"]

    schedule_post(post_id, scheduled_date)
    print(f"Scheduled for {scheduled_date}")

def cmd_week(topic):
    print(f"Generating 7 tweets about: {topic}\n")

    posts = []
    base_date = datetime.utcnow() + timedelta(days=1)

    for i in range(7):
        text = generate_tweet(topic)
        scheduled = (base_date + timedelta(days=i)).replace(hour=14, minute=0, second=0)
        posts.append({
            "text": text,
            "scheduled_date": scheduled.strftime("%Y-%m-%dT%H:%M:%SZ"),
        })
        print(f"  Day {i + 1}: {text[:70]}...")

    bulk_create(posts)
    print(f"\nScheduled {len(posts)} tweets for the next 7 days!")

def cmd_thread(topic):
    print(f"Generating thread about: {topic}\n")
    tweets = generate_thread(topic)

    for i, tweet in enumerate(tweets):
        print(f"  [{i + 1}] {tweet}")
    print()

    resp = requests.post(
        f"{OPENTWEET_BASE}/api/v1/posts",
        json={
            "text": tweets[0],
            "is_thread": True,
            "thread_tweets": tweets[1:],
        },
        headers=get_headers(),
    )
    resp.raise_for_status()
    post_id = resp.json()["data"]["id"]

    publish_post(post_id)
    print("Thread published to X!")

# --- CLI ---
def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(1)

    command = sys.argv[1]

    if command == "check":
        check_connection()
    elif command == "publish" and len(sys.argv) >= 3:
        cmd_publish(sys.argv[2])
    elif command == "schedule" and len(sys.argv) >= 4:
        cmd_schedule(sys.argv[2], sys.argv[3])
    elif command == "week" and len(sys.argv) >= 3:
        cmd_week(sys.argv[2])
    elif command == "thread" and len(sys.argv) >= 3:
        cmd_thread(sys.argv[2])
    else:
        print(__doc__)
        sys.exit(1)

if __name__ == "__main__":
    main()

Running It

First, make sure your environment variables are set:

export OPENTWEET_API_KEY="ot_your_key_here"
export ANTHROPIC_API_KEY="sk-ant-your_key_here"

Check that everything is connected:

python agent.py check
# Output: Connected! Account: your_x_username

Publish a tweet right now:

python agent.py publish "why most developers should blog more"
# Output:
# Generating tweet about: why most developers should blog more
# Generated: Writing code is thinking out loud. Blogging is thinking out loud where other people can find it. Most developers have years of hard-won knowledge sitting in their heads doing nothing.
#
# Published to X!

Schedule one for tomorrow morning:

python agent.py schedule "remote work productivity" "2026-02-20T09:00:00Z"

Schedule a full week of content:

python agent.py week "building side projects"
# Output:
# Generating 7 tweets about: building side projects
#
#   Day 1: The best side project is the one you actually finish. Scope it sm...
#   Day 2: Nobody cares about your tech stack. They care if your thing solve...
#   Day 3: I've shipped 12 side projects in 3 years. 10 of them failed. The...
#   Day 4: Stop adding features. Ship what you have. You can always add more...
#   Day 5: The hardest part of a side project isn't coding. It's marketing i...
#   Day 6: Your side project doesn't need users to be valuable. The skills y...
#   Day 7: Every successful product I know started as someone's side project...
#
# Scheduled 7 tweets for the next 7 days!

Post a thread:

python agent.py thread "lessons from launching 5 SaaS products"

Making It Better

The script above works. Here are some ways to extend it.

Add a cron job for daily posting

Run the agent automatically every morning:

# Edit your crontab
crontab -e

# Add this line (runs at 9 AM UTC every day)
0 9 * * * OPENTWEET_API_KEY=ot_... ANTHROPIC_API_KEY=sk-ant-... /usr/bin/python3 /path/to/agent.py publish "developer tips"

Pull topics from an RSS feed

Instead of hardcoding topics, fetch them from a blog or news feed:

import feedparser

def get_topics_from_rss(feed_url, count=5):
    feed = feedparser.parse(feed_url)
    return [entry.title for entry in feed.entries[:count]]

# Usage:
topics = get_topics_from_rss("https://yourblog.com/feed.xml")
for topic in topics:
    cmd_publish(topic)

Install feedparser with pip install feedparser.

Vary the posting times

Instead of always posting at 2 PM, randomize within a window:

import random

def random_time(base_date, hour_start=9, hour_end=17):
    hour = random.randint(hour_start, hour_end)
    minute = random.choice([0, 15, 30, 45])
    return base_date.replace(hour=hour, minute=minute, second=0)

Add error handling for rate limits

The OpenTweet API allows 60 requests per minute and 1,000 per day. For bulk operations, add a simple retry:

import time

def api_call_with_retry(func, *args, retries=3, **kwargs):
    for attempt in range(retries):
        try:
            return func(*args, **kwargs)
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 429 and attempt < retries - 1:
                wait = 2 ** attempt * 10  # 10s, 20s, 40s
                print(f"Rate limited. Waiting {wait}s...")
                time.sleep(wait)
            else:
                raise

Use OpenAI instead of Claude

Swap out the generate_tweet function with the OpenAI version from Step 3. Everything else stays the same -- the OpenTweet API doesn't care how you generated the text.


How It Works Under the Hood

Here's the flow for every tweet:

  1. You run the script with a topic
  2. Claude generates tweet text based on your prompt and rules
  3. OpenTweet API creates a draft post (POST /api/v1/posts)
  4. OpenTweet API publishes or schedules it (POST /api/v1/posts/:id/publish or /schedule)
  5. OpenTweet handles the Twitter OAuth and actually posts it to X

Your script never touches Twitter directly. No OAuth tokens, no developer account, no $100/month API fee. The OpenTweet API is the bridge between your AI agent and the Twitter platform.

The API key (ot_...) is scoped to posting only. It can't read your DMs, access your followers, or change your profile. If the key leaks, you revoke it from the OpenTweet dashboard and generate a new one. Your X account stays safe.


Cost Breakdown

Let's say you post 2 tweets per day (60/month):

Item Cost
OpenTweet subscription $5.99/month
Claude API (60 tweets x ~100 tokens each) ~$0.12/month
Total ~$6.11/month

For comparison, just the Twitter API Basic tier (which you'd need for direct API access) costs $100/month. And that doesn't include the LLM costs or the hours you'd spend building OAuth and rate limit handling.


What's Next

You now have a working AI Twitter agent in Python. It generates content, posts tweets, schedules content, creates threads, and bulk-schedules a week at a time.

Some ideas for where to take it:

  • Add personality profiles -- different prompts for different tones (educational, casual, provocative)
  • Build a content calendar -- read scheduled posts with GET /api/v1/posts?status=scheduled and display them
  • Add analytics checking -- review what's performing and feed that back into the prompt
  • Multi-account support -- use different API keys for different X accounts
  • Wrap it in a web UI -- build a simple Flask app with a text input and "Generate + Post" button

The OpenTweet API supports everything you need. The full API documentation is available in a plain-text format that's designed to be easy for both humans and AI agents to read.


Sign up for OpenTweet and start building. 7-day free trial, $5.99/month, full API access included. Your AI agent can be posting to X in the next 10 minutes.

Start Scheduling Your X Posts Today

Join hundreds of creators using OpenTweet to stay consistent, save time, and grow their audience.

7-day free trial
Only $11.99/mo
Cancel anytime