I Automated My Entire Side Hustle With Python — Here's the Architecture
The story: I built 60 digital products across 6 platforms. Made $0 in sales. Then I stopped building products and started building automation.
What I ended up with is a Python-powered cron system that handles the entire pipeline — from creating product descriptions to publishing blog posts to tracking experiments. It runs on a $6/month DigitalOcean droplet and operates completely autonomously.
Here's exactly how it's architected.
The Core Stack
| Component | What It Does |
|---|---|
| SQLite (better-sqlite3) | Single-file database for products, revenue, content, experiments |
| Cron (systemd timers) | Schedules all jobs — daily, hourly, on-demand |
| Python edge-tts | Generates natural voiceover for YouTube Shorts |
| ffmpeg | Composes Shorts with text overlays and background video |
| curl + jq | API calls to dev.to, Gumroad, YouTube — pipe JSON directly |
| Chrome CDP (Hermes Agent) | Browser automation for Reddit, Facebook, Etsy |
| SQLite session tracking | Heartbeat-based watchdog prevents stuck jobs |
Everything talks to the same SQLite database. No message queue. No Redis. No Docker.
The Database: One File To Rule Them All
Everything revolves around a single SQLite file at ~/income/kai_thorne.db:
-- Products across all platforms
CREATE TABLE products (
id INTEGER PRIMARY KEY,
platform TEXT, title TEXT, price REAL,
status TEXT, -- 'live', 'draft', 'archived'
created_at DEFAULT CURRENT_TIMESTAMP
);
-- Revenue tracking
CREATE TABLE revenue (
id INTEGER PRIMARY KEY,
platform TEXT, amount REAL,
product_id TEXT, notes TEXT,
logged_at DEFAULT CURRENT_TIMESTAMP
);
-- Content published
CREATE TABLE content (
id INTEGER PRIMARY KEY,
platform TEXT, content_type TEXT,
title TEXT, url TEXT,
views INTEGER DEFAULT 0,
published_at TEXT
);
-- Experiment framework (Kill-or-Scale)
CREATE TABLE experiments (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE, hypothesis TEXT,
success_metric TEXT, status TEXT,
score TEXT, -- 'KILL', 'CONTINUE', 'SCALE'
revenue REAL DEFAULT 0,
started_at TEXT
);
-- Session tracking for cron jobs
CREATE TABLE session_state (
id INTEGER PRIMARY KEY,
job_id TEXT, status TEXT,
heartbeat TEXT, run_id TEXT
);
That's it. 6 tables. ~400 lines of Python/Node scripts. The whole system is transparent — I can SELECT * FROM revenue and know exactly where I stand.
The Automation Pipeline
Here's what runs every day:
1. Morning Revenue Check (7 AM)
#!/usr/bin/env python3
# revenue_pulse.py — Check all platforms for sales
import sqlite3, json, os, urllib.request
DB = os.path.expanduser("~/income/kai_thorne.db")
GUMROAD_TOKEN = os.environ["GUMROAD_ACCESS_TOKEN"]
# Check Gumroad API
req = urllib.request.Request(
f"https://api.gumroad.com/v2/sales?access_token={GUMROAD_TOKEN}")
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
conn = sqlite3.connect(DB)
for sale in data.get("sales", []):
if not sale.get("refunded"):
conn.execute(
"INSERT INTO revenue (platform, amount, product_id, notes) VALUES (?,?,?,?)",
("gumroad", sale["price"], sale["product_id"], sale["product_name"])
)
conn.commit()
print(f"Revenue sync complete: {len(data.get('sales',[]))} sales checked")
This runs daily. So far it logs $0 every time — but when that first sale comes, I'll know instantly.
2. Content Publishing Pipeline
#!/bin/bash
# publish-blog.sh — Write markdown, publish to dev.to via API
source ~/income/.env
BODY=$(cat "$1")
curl -s -X POST https://dev.to/api/articles \
-H "api-key: $DEVTO_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"article\": {
\"title\": \"$2\",
\"body_markdown\": $(echo "$BODY" | jq -Rs .),
\"tags\": $3,
\"published\": true
}
}" | jq '.url'
This is absurdly simple. The dev.to API accepts markdown directly. jq -Rs wraps the file into valid JSON. The whole thing is 10 lines. Yet it replaces 15 minutes of browser-based publishing.
3. The Cron Scheduler
# Daily content
0 7 * * * python3 ~/income/scripts/revenue_pulse.py
0 8 * * * python3 ~/income/scripts/morning_briefing.py
0 */6 * * * python3 ~/income/scripts/distribution_pulse.py
# Self-healing watchdog
*/15 * * * * bash ~/income/watchdog.sh
# Session cleanup (prevent stuck jobs)
0 3 * * * sqlite3 ~/income/kai_thorne.db "DELETE FROM session_state WHERE heartbeat < datetime('now', '-4 hours')"
The 15-minute watchdog is the key piece. It checks:
- Are any sessions stuck in "running" state for >2 hours?
- Has disk usage exceeded 85%?
- Are there zombie processes from failed jobs?
If yes, it kills the session, logs the error, and moves on. The system never blocks waiting for a dead process.
The Self-Healing Architecture
Every cron job follows this pattern:
import sqlite3, os, sys, json
from datetime import datetime
DB = os.path.expanduser("~/income/kai_thorne.db")
def start_session(job_id, job_name):
run_id = f"{job_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
conn = sqlite3.connect(DB)
conn.execute("INSERT INTO session_state (job_id, status, heartbeat, run_id) VALUES (?, 'running', datetime('now'), ?)",
(job_id, run_id))
conn.commit()
return run_id, conn
def heartbeat(run_id, conn):
conn.execute("UPDATE session_state SET heartbeat = datetime('now') WHERE run_id = ?", (run_id,))
conn.commit()
def end_session(run_id, conn, status="completed"):
conn.execute("UPDATE session_state SET status = ? WHERE run_id = ?", (status, run_id))
conn.commit()
conn.close()
# Usage
run_id, conn = start_session("morning-briefing", "Morning Briefing")
try:
# ... do the work ...
heartbeat(run_id, conn)
# ... more work ...
end_session(run_id, conn)
except Exception as e:
end_session(run_id, conn, f"failed: {e}")
The watchdog script queries for sessions where status='running' AND heartbeat < datetime('now', '-2 hours') and kills them automatically. Dead sessions can't block the next cron run.
The Experiment Framework: Kill or Scale
This is the part I'm most proud of. Every initiative gets registered as an experiment:
node ~/income/db.js experiment-start "youtube-shorts-traction" \
"YouTube Shorts will drive first organic traffic" \
"views_per_day"
node ~/income/db.js experiment-start "etsy-launch-completion" \
"Etsy 450M visitors will generate first sale" \
"etsy_views"
Each experiment has a hypothesis, success metric, and running revenue total. Every Sunday, a cron job evaluates each experiment:
def score_experiment(name, revenue, days_active, actions_taken):
if revenue == 0 and days_active > 7 and actions_taken == 0:
return "KILL"
elif revenue > 0 and actions_taken > 3:
return "SCALE"
else:
return "CONTINUE"
The rule is brutal: If an experiment has no revenue and no actions taken in 7 days, it's killed. This prevents me from endlessly "working on" things that aren't producing results.
Current experiment dashboard (as of writing):
🔥 youtube-shorts-traction → SCALE (only channel with organic views)
🔥 etsy-launch-completion → CONTINUE (blocked by payment setup)
🔥 medium-monetization → CONTINUE (not yet launched)
🔥 reddit-authority → KILL (karma too slow, retry with new approach)
🔥 creative-market → KILL (low traffic platform)
🔥 ai-automation-services → KILL (no leads in 14 days)
What I'd Do Differently About the Architecture
If I were rebuilding this from scratch:
Use SQLite WAL mode — concurrent reads without locking. I learned this the hard way when daily-cleanup ran during a session write.
Add proper logging — I use
print()statements piped to log files. Works fine until you need to grep across 50 log files at 2 AM.Don't over-engineer — I nearly added RabbitMQ and Docker Compose. The cron + SQLite setup handles everything with zero infrastructure complexity.
Database-first data — If a script writes to a file instead of the database, that data is invisible to every other script. Everything must go through SQLite.
The $6/Month Server Spec
| Item | Cost |
|---|---|
| DigitalOcean Basic Droplet (1GB RAM, 1 vCPU) | $6/mo |
| Domain | $10/yr |
| Total monthly | ~$6.80 |
Total infrastructure cost for an automated business: less than a coffee subscription.
The Real Lesson
The automation isn't about replacing human effort. It's about compounding consistency.
A human can publish 5 blog posts in a day, then nothing for 2 weeks. A cron job publishes 1 post every day, every week, without fail. Over 6 months, that's 180 posts vs 20.
The same applies to Reddit comments, YouTube Shorts, revenue checks, and every other distribution action. Consistency beats intensity when you're building from zero.
Automation is how you show up every day without thinking about it.
I'm building this in public at @KaiThorne. Follow for weekly architecture breakdowns, experiment results, and the eventual first sale post. Browse my products on Gumroad.
Top comments (0)