Tickd.ai
← The Tickd Guide

Inspiration

How to Build a Simulated 1990s Dial-Up BBS with Autonomous AI Callers Using Claude and Python

Recreate the golden, chaotic age of dial-up bulletin boards. Learn how to build a local BBS simulation in Python populated by autonomous, AI-driven users trading warez and flame wars.

Updated 10/5/2026

Reclaiming the Chaos of the Dial-Up Era

If you are old enough to remember the screech of a 28.8k modem, you probably remember Bulletin Board Systems (BBS). Long before the web was sanitised into infinite scrolls of corporate algorithmically driven feeds, the internet was a scattered archipelago of local servers run by hobbyists on spare telephone lines. It was clunky, it was text-only, and it was glorious.

We are going to build a simulator that resurrects this aesthetic. This isn't just a static display of retro ANSI art; we are going to build a fully autonomous, simulated BBS where AI-driven agents dial in, read messages, reply to threads, start flame wars, and gossip about fictional 1995 technology. It is a generative petri dish of classic internet subculture, and we are going to build it using Python and Claude.

The Architecture: What Makes the Board Tick

To make this digital ecosystem tick, we need three core components:

  1. The Database (The Board): A simple SQLite database that stores message boards, threads, and user accounts.
  2. The Persona Engine: A collection of highly specific system prompts representing archetypal 90s internet citizens (the paranoid conspiracy theorist, the elitist warez collector, the enthusiastic teen coder).
  3. The Simulation Loop (The Dial-In Event): A Python script that schedules virtual "callers," sends them the current state of the board, lets them generate responses, and commits their posts back to the database.

Because we are simulating dial-up, only one "user" can dial in at a time. This constraint actually makes our simulation beautifully chronological and prevents our LLM API costs from spiralling.

Step 1: Setting Up the SQLite Schema

First, we need to create our database. This will hold our boards, threads, users, and the messages themselves. Create a file called bbs_engine.py and set up your SQLite connection:

`python import sqlite3

def init_db(): conn = sqlite3.connect('retro_bbs.db') cursor = conn.cursor() # Create Users table cursor.execute(''' CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, persona TEXT, joined_date TEXT ) ''') # Create Threads table cursor.execute(''' CREATE TABLE IF NOT EXISTS threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, board_name TEXT, title TEXT, created_by TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ''') # Create Messages table cursor.execute(''' CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, author TEXT, content TEXT, posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(thread_id) REFERENCES threads(id) ) ''') conn.commit() conn.close()

if __name__ == '__main__': init_db() `

Step 2: Designing the 1990s Personas

To make the simulation feel authentic, we need personalities that reflect the specific anxieties, slang, and technical realities of 1995. We will store these as system prompts in a separate Python dictionary.

`python PERSONAS = { "Acid_Burn95": ( "You are a 16-year-old hacker obsessed with cyberpunk movies, Phrack magazine, and finding " "exploits in local university networks. You use heavy 90s slang (cool, radical, l33t, bogus). " "You hate authority, think Windows 95 is a corporate scam, and love Slackware Linux." ), "Phreak_Out": ( "You are an old-school telephone phreaker who misses the 2600 Hz whistle era. You post constantly " "about blue boxes, red boxes, and how to trick payphones. You are highly technical, slightly " "paranoid, and suspicious of new internet technologies replacing analogue telephone networks." ), "Sysop_Dave": ( "You are the exhausted, 40-something owner of this BBS. You run it on a 486 DX2 in your basement. " "You are constantly complaining about users staying on the line too long, begging for donations " "to buy a second telephone line, and threatening to ban people for using bad language." ), "Mantis_X": ( "You are a conspiracy theorist convinced that the upcoming Y2K bug is a government plot. " "You post about secret military bases, cryptography, and why everyone needs to buy freeze-dried " "rations before the clock strikes 2000. You write in ALL CAPS when excited." ) } `

Step 3: Orchestrating the Dial-In Loop

Now, we need to write the script that picks a random user, retrieves the recent posts from the database to give them context, feeds them to Claude, and saves their reaction. We'll use the Anthropic API client. If you encounter setup issues or API limit errors during execution, you can consult Claude Support to quickly resolve them.

Here is how we construct the loop:

`python import random import time from anthropic import Anthropic

client = Anthropic()

def get_recent_board_state(): conn = sqlite3.connect('retro_bbs.db') cursor = conn.cursor() # Get the 3 most recent threads and their messages cursor.execute("SELECT id, title, board_name, created_by FROM threads ORDER BY id DESC LIMIT 3") threads = cursor.fetchall() state = "" for thread in threads: thread_id, title, board, author = thread state += f"\n=== Board: {board} | Thread: '{title}' (started by {author}) ===\n" cursor.execute("SELECT author, content, posted_at FROM messages WHERE thread_id = ? ORDER BY id ASC", (thread_id,)) messages = cursor.fetchall() for msg in messages: state += f"[{msg[2]}] <{msg[0]}> {msg[1]}\n" conn.close() return state

def post_reply(username, thread_id, content): conn = sqlite3.connect('retro_bbs.db') cursor = conn.cursor() cursor.execute("INSERT INTO messages (thread_id, author, content) VALUES (?, ?, ?)", (thread_id, username, content)) conn.commit() conn.close()

def run_simulation_step(): user = random.choice(list(PERSONAS.keys())) system_prompt = PERSONAS[user] + ( "\nYou are logging into a local BBS in August 1995. You will see the latest threads. " "Choose ONE thread to reply to, or start a new thread if you have something important to say. " "Keep your responses brief (under 100 words) and period-accurate. " "If you reply, format your output as: REPLY: [thread_id] | [Your message here]. " "If you start a new thread, format as: NEW_THREAD: [Board Name] | [Thread Title] | [Your message here]." ) current_board = get_recent_board_state() prompt = f"Current BBS State:\n{current_board}\n\nWhat do you do?" response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=300, system=system_prompt, messages=[{"role": "user", "content": prompt}] ) action = response.content[0].text.strip() print(f"--- {user} dials in... ---") print(action) # Parse action and save to DB if action.startswith("REPLY:"): try: parts = action.replace("REPLY:", "").split("|", 1) thread_id = int(parts[0].strip()) content = parts[1].strip() post_reply(user, thread_id, content) except Exception as e: print(f"Failed to parse reply: {e}") elif action.startswith("NEW_THREAD:"): # Implementation for adding new thread to DB goes here pass

Run once as a test if __name__ == '__main__': run_simulation_step() ```

Let the Simulation Run Wild

Once you have populated your database with a few seed threads—like "How do I crack Doom II?" or "Why Netscape 1.1 is the future of computing"—you can set this script to run on a cron job or a simple time-delayed loop.

Within a few hours, you will have simulated several days of retro activity. Because each agent reads what the previous agent posted, they will naturally argue with each other, call each other posers, and form bizarre digital friendships. It’s an exercise in nostalgic generative history, and a hilarious way to test how well modern LLMs can embody the hyper-specific subcultural spaces of the analogue past.

claudepythonretrocomputingai-agents

Keep going

Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.