This project began as a deep dive into the generative AI hype cycle. The goal was simple: can we use emerging LLM and image generation technologies to fully automate social media presence for brands? Designed to run 24/7 on a Raspberry Pi, this bot serves as an autonomous digital agent that handles everything from content ideation to publication.

See more on the github repository.

Motivation & Experimentation

The primary driver was to explore the practical applications of generative AI. Beyond simple text generation, the project experimented with grounding AI outputs using Retrieval-Augmented Generation (RAG). By feeding local source materials into a vector database, the bot’s generated content became more contextualized and aligned with a specific brand voice.

Another experimental feature was social graph optimization. We looked into building internal maps of follower relationships to generate more meaningful, dynamic interactions rather than just broadcasting one-way updates.

Core Features

  • Human-in-the-loop Approval: To prevent AI “hallucinations” or off-brand content, we integrated a proactive authorization flow via Discord. The bot drafts content and asks for a thumbs-up before anything goes live.
  • RAG-Powered Content: A local SQLite and vector database setup ensures that generated content is grounded in specific, curated knowledge bases.
  • Autonomous Scheduling: The system handles its own queue, ensuring a consistent posting cadence without manual intervention.

Twitter Web Scraping

One of the project’s most practical challenges was navigating the constraints of social media platforms. With the move toward highly restricted/paid APIs, we implemented a robust web scraping module for X (Twitter) as a workaround.

The scraper is built on Selenium and Chrome, designed to behave like a human visitor. It handles:

  • Automated Authentication: Securely logging in and managing session states.
  • Dynamic Content Loading: Navigating “For You” and “Following” tabs, handling infinite scroll to gather context, and even clicking away cookie consent banners.
  • Deduplication: Ensuring the bot doesn’t ingest or react to the same content multiple times by tracking unique tweet IDs in a local database.

System Architecture

The bot’s architecture is modular, separating content logic from platform-specific API details.

The Scheduler: Core vs. Custom Tasks

At the heart of the orchestrator is a custom Scheduler class built on APScheduler. It manages two distinct types of tasks:

  1. Core Tasks: These are the “meta-jobs” that keep the system running. For example, _update_database runs every 10 minutes to check if the content queue is running low and triggers new generations. _update_scheduler then moves authorized items from the database into the active execution memory.
  2. Custom Tasks: These are the actual posting events. They are dynamically generated from the database and scheduled according to the cron strings defined in the configuration.

Generators and Content Objects

  • Generators: These are the “creative” functions. They handle the heavy lifting of calling OpenAI, manipulating images, and formatting text.
  • Content Objects: We use an abstraction layer (e.g., TwitterContentObject) that bundles the generator, the authorization logic (Discord), and the final posting function into a single object. This makes adding new platforms as simple as defining a new class.

Configuration

Everything is controlled via a config.yaml file. Here, you define which generators to use, how often to post (cron schedules), and where to find the necessary API keys and database paths.

An example extract of this configuration file is shown below:

paths:
    main_logfile: "../logs/main.log"
    sql_database: "../databases/sqlite/sqlite.db"
 
scheduler:
    content_object_params:
      - gen_func: generators.TwitterBot.image_with_quote
        post_func: modules.twitter_api.create_tweet
        auth_func: modules.discord_api.authorize_content
        cron: "45 11 * * *"  # minute, hour, day, month, day of week
        is_authorized: False  # if True, will not request authorization
        keys_path: "../keys/thewisestoic.json"