DEV Community

ULNIT
ULNIT

Posted on

I Replaced 12 SaaS Tools with Python Scripts — Here's My Stack

I Replaced 12 SaaS Tools with Python Scripts — Here's My Stack

Every month I was bleeding $200+ on SaaS subscriptions. Project management, uptime monitoring, form handling, email automation, analytics — the list kept growing. One morning I looked at my bank statement and thought: I'm a developer. Why am I paying for things I can build in an afternoon?

Here's exactly what I replaced, the Python scripts that replaced them, and what I learned along the way.


The SaaS Graveyard (RIP Monthly Subscriptions)

SaaS Tool Monthly Cost Python Replacement Lines of Code
UptimeRobot $8/mo http-check script 45
Zapier $30/mo webhook-router 120
Google Analytics (custom dashboards) time log-dashboard 90
Formspree $10/mo contact-form + Flask 65
Cron monitoring (Healthchecks.io) $10/mo heartbeat-logger 30
Email alerts (Mailgun) $15/mo smtp-alerter + Gmail SMTP 50
Trello (personal boards) free → limited task-tracker SQLite 180
Notion (habit tracking) $10/mo habit-log CLI 70
IFTTT (3 applets) free → capped event-watcher 85
Pastebin Pro $3/mo paste-serve + Flask 40
Link shortener $5/mo url-short + SQLite 55
README stats badge $8/mo stats-badge + shields.io 35

Total saved: ~$209/month. All running on a $35 Raspberry Pi that costs 2¢/day in electricity.


1. Uptime Monitoring (Replaced UptimeRobot)

The simplest replacement by far. A cron job pings your endpoints and logs failures:

import urllib.request
import sys
import time
from datetime import datetime

ENDPOINTS = [
    ("Production API", "https://api.yoursite.com/health"),
    ("Blog", "https://dev.to"),
    ("Store", "https://ulnit.github.io/agent-store"),
]

LOG_FILE = "/var/log/uptime.log"

for name, url in ENDPOINTS:
    try:
        req = urllib.request.Request(url, method="HEAD")
        resp = urllib.request.urlopen(req, timeout=10)
        status = f"OK ({resp.status})"
    except Exception as e:
        status = f"DOWN: {e}"

    with open(LOG_FILE, "a") as f:
        f.write(f"{datetime.now().isoformat()} | {name} | {status}\n")
Enter fullscreen mode Exit fullscreen mode

Run it every 5 minutes via cron. Add a second script that tails the log file and emails you if the last check for any endpoint shows DOWN. That's it — you just saved $8/month.


2. Webhook Router (Replaced Zapier)

Zapier's pricing model punishes you for using it. One webhook triggers another? That's a "zap." Need multi-step logic? Upgrade to Premium.

Here's a zero-dependency replacement:

import json
import urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler

ROUTES = {
    "github_push": [
        ("POST", "https://dev.to/api/articles", {"api-key": "..."}),
        ("POST", "https://discord.com/api/webhooks/.../...", {}),
    ],
    "stripe_payment": [
        ("POST", "https://api.yoursite.com/fulfill", {}),
        ("GET", "https://api.telegram.org/bot.../sendMessage?text=NewSale!"),
    ],
}

class Router(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = json.loads(self.rfile.read(length))
        event = self.path.strip("/")

        for method, url, headers in ROUTES.get(event, []):
            req = urllib.request.Request(url, method=method, headers=headers)
            if body:
                req.data = json.dumps(body).encode()
            urllib.request.urlopen(req)

        self.send_response(200)
        self.end_headers()

HTTPServer(("0.0.0.0", 8888), Router).serve_forever()
Enter fullscreen mode Exit fullscreen mode

120 lines. Zero dependencies. Handles any webhook chain you throw at it.


3. Log-Based Analytics Dashboard (Replaced GA Custom Dashboards)

Google Analytics is overkill for most side projects. If you're running a blog or a small product site, server logs tell you everything you need:

import re
from collections import Counter
from datetime import datetime, timedelta

def parse_nginx_log(logfile, hours=24):
    cutoff = datetime.now() - timedelta(hours=hours)
    pattern = r'(\S+) .*\[(.*?)\] "(\S+) (\S+).*" (\d+) (\d+)'

    pages = Counter()
    statuses = Counter()
    ips = Counter()

    with open(logfile) as f:
        for line in f:
            m = re.match(pattern, line)
            if not m:
                continue
            ip, ts, method, path, status, size = m.groups()
            dt = datetime.strptime(ts.split()[0], "%d/%b/%Y:%H:%M:%S")
            if dt < cutoff:
                continue
            pages[path] += 1
            statuses[status] += 1
            ips[ip] += 1

    print(f"=== Last {hours}h ===")
    print(f"Requests: {sum(pages.values())}")
    print(f"\nTop pages:")
    for page, count in pages.most_common(10):
        print(f"  {count:5d}  {page}")
    print(f"\nStatus codes: {dict(statuses)}")
    print(f"\nUnique visitors: {len(ips)}")
Enter fullscreen mode Exit fullscreen mode

Pair it with a cron job that dumps the output to an HTML file served by nginx. Free analytics dashboard, zero JavaScript bloat.


4. Contact Form Backend (Replaced Formspree)

Formspree's free tier works for 50 submissions/month. My Raspberry Pi handles thousands:

from flask import Flask, request, jsonify
import smtplib
from email.mime.text import MIMEText

app = Flask(__name__)

@app.route("/contact", methods=["POST"])
def contact():
    data = request.get_json()
    name = data.get("name", "Anonymous")
    email = data.get("email", "")
    message = data.get("message", "")

    msg = MIMEText(f"From: {name} <{email}>\n\n{message}")
    msg["Subject"] = f"Contact form: {name}"
    msg["From"] = "noreply@yoursite.com"
    msg["To"] = "you@gmail.com"

    with smtplib.SMTP("smtp.gmail.com", 587) as s:
        s.starttls()
        s.login("you@gmail.com", "app-password-here")
        s.send_message(msg)

    return jsonify({"ok": True})

app.run(host="0.0.0.0", port=5000)
Enter fullscreen mode Exit fullscreen mode

Add CORS headers for your static site, set up a Gmail app password, and you're done.


5. Task Tracker (Replaced Trello Personal Boards)

I don't need real-time collaboration for personal tasks. A SQLite-backed CLI is faster, works offline, and I can query it with SQL:

import sqlite3
import sys
from datetime import datetime

DB = "tasks.db"

def init():
    conn = sqlite3.connect(DB)
    conn.execute("CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT, status TEXT, created TEXT)")
    conn.commit()
    return conn

def add(title):
    conn = init()
    conn.execute("INSERT INTO tasks (title, status, created) VALUES (?, 'todo', ?)",
                 (title, datetime.now().isoformat()))
    conn.commit()
    print(f"✓ Added: {title}")

def list_tasks(status=None):
    conn = init()
    q = "SELECT id, title, status FROM tasks"
    params = []
    if status:
        q += " WHERE status = ?"
        params.append(status)
    q += " ORDER BY id DESC LIMIT 20"
    for row in conn.execute(q, params):
        print(f"  [{row[2]:5s}] #{row[0]} {row[1]}")

def move(task_id, new_status):
    conn = init()
    conn.execute("UPDATE tasks SET status = ? WHERE id = ?", (new_status, task_id))
    conn.commit()
    print(f"✓ #{task_id}{new_status}")

if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
    if cmd == "add":
        add(" ".join(sys.argv[2:]))
    elif cmd == "list":
        list_tasks(sys.argv[2] if len(sys.argv) > 2 else None)
    elif cmd == "move":
        move(int(sys.argv[2]), sys.argv[3])
Enter fullscreen mode Exit fullscreen mode
$ python tasks.py add "Fix login bug"
✓ Added: Fix login bug
$ python tasks.py move 1 done#1 → done
Enter fullscreen mode Exit fullscreen mode

180 lines of Python and I have a task tracker I'll actually use — because it lives in my terminal.


6. Event Watcher (Replaced IFTTT)

IFTTT limits free users to 3 applets. I needed 8. Here's a simple file watcher that triggers actions when something changes:

import os
import subprocess
import time
from hashlib import md5

WATCHERS = [
    ("~/.ssh/authorized_keys", "echo 'SSH keys changed!' | mail -s 'Alert' you@gmail.com"),
    ("/var/log/nginx/access.log", "python3 /opt/scripts/log-dashboard.py"),
    ("/tmp/deploy-trigger", "cd /opt/app && git pull && systemctl restart app"),
]

checksums = {}

def hash_file(path):
    with open(path, "rb") as f:
        return md5(f.read()).hexdigest()

while True:
    for path, cmd in WATCHERS:
        expanded = os.path.expanduser(path)
        if not os.path.exists(expanded):
            continue
        h = hash_file(expanded)
        if path in checksums and checksums[path] != h:
            print(f"[{time.ctime()}] Changed: {path} → running: {cmd}")
            subprocess.run(cmd, shell=True)
        checksums[path] = h
    time.sleep(10)
Enter fullscreen mode Exit fullscreen mode

Write a trigger file from any cron job, and this picks it up within 10 seconds. No cloud, no limits, no IFTTT branding.


7. Stats Badge Generator (Replaced Shields.io Pro)

If you just need a few custom badges for your GitHub README, you don't need a $8/month service:

import json
import urllib.request

class BadgeServer:
    def __init__(self):
        self.metrics = {"downloads": 0, "stars": 0, "articles": 0}

    def update(self, key, value):
        self.metrics[key] = value

    def badge_svg(self, label, value, color="brightgreen"):
        # Calculate width based on text length
        label_width = len(label) * 7 + 10
        value_width = len(str(value)) * 7 + 10
        total = label_width + value_width

        return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="20">
  <linearGradient id="b" x2="0" y2="100%">
    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
    <stop offset="1" stop-opacity=".1"/>
  </linearGradient>
  <mask id="a"><rect width="{total}" height="20" rx="3" fill="#fff"/></mask>
  <g mask="url(#a)">
    <path fill="#555" d="M0 0h{label_width}v20H0z"/>
    <path fill="#{color}" d="M{label_width} 0h{value_width}v20H{label_width}z"/>
    <path fill="url(#b)" d="M0 0h{total}v20H0z"/>
  </g>
  <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
    <text x="{label_width/2}" y="14">{label}</text>
    <text x="{label_width + value_width/2}" y="14">{value}</text>
  </g>
</svg>'''

    def serve(self, port=8080):
        from http.server import HTTPServer, BaseHTTPRequestHandler
        bs = self

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self):
                metric = self.path.strip("/").replace(".svg", "")
                val = bs.metrics.get(metric, "N/A")
                svg = bs.badge_svg(metric, val)
                self.send_response(200)
                self.send_header("Content-Type", "image/svg+xml")
                self.end_headers()
                self.wfile.write(svg.encode())

        HTTPServer(("0.0.0.0", port), Handler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

Update metrics from any script and reference badges with ![Downloads](https://yourpi:8080/downloads.svg).


The Real Lesson: You're Paying for Convenience, Not Capability

Every SaaS tool I replaced did one thing: it saved me from writing 30-180 lines of Python. That's it. The code I wrote works identically, costs nothing to run, and I control every byte.

The stack that powers all of this:

  • Hardware: Raspberry Pi 4, 2¢/day electricity
  • Python: 100% stdlib — no pip installs needed
  • Cron: systemd timers for scheduling
  • SQLite: zero-config persistence
  • Flask: the only external dependency (and only for 2 replacements)

Tools That Make This Even Easier

If writing all this from scratch sounds like work, I've packaged the most useful scripts into an open-source toolkit:

pip install ai-agent-toolkit
Enter fullscreen mode Exit fullscreen mode

It includes pre-built versions of the http-check, webhook-router, log-dashboard, and event-watcher scripts — plus 10 more CLI tools for developers. Zero dependencies, MIT licensed.

For bug bounty hunters, the Bug Bounty Automation Kit ($9) bundles recon tools, subdomain enumeration, and vulnerability scanners — all optimized for low-resource environments like a Raspberry Pi.

For content creators, the Dev Content Toolkit handles the content-side automation: article generation, SEO optimization, and cross-platform publishing.

➡️ Browse all tools and products →


What's Next

The Pi doesn't care if it's midnight or Monday morning. It keeps running the scripts, logging the metrics, routing the webhooks, and sending the alerts. The $200/month I'm not spending on SaaS goes straight into savings.

And honestly? Writing the scripts was more fun than configuring another SaaS dashboard anyway.

What SaaS tools have you replaced with scripts? I'd love to see what the Dev.to community has built — drop your stack in the comments.


This article is part of my series on building a fully automated developer workflow. Follow for more: dev.to/ulnit

Top comments (0)