All posts
Workflow

PRAW 8: the production setup for buyer-intent Reddit alerts

By Bazzly Team5 min read

A production walkthrough for PRAW 8: poll subs for buyer-intent keywords, pipe matches to Slack, and dodge the rate-limit and auth traps that broke v7 scripts.

PRAW 8: the production setup for buyer-intent Reddit alerts

PRAW 8 shipped in November 2026 with a stricter auth flow, a rewritten rate-limit handler, and a few breaking changes that will silently kill any v7 script you leave running. If you use Reddit as a lead source, that's your monitoring cron dying at 3am and you finding out on Monday.

This is the setup I run in production: a small PRAW 8 poller that watches a handful of subreddits, filters submissions and comments for buying-intent phrases, and posts the matches to a Slack channel with enough context to reply in under a minute.

What changed in PRAW 8 that will break your v7 script

The PRAW 8 changelog is worth reading in full, but three things matter for a lead-monitoring workflow:

  1. Script-app auth is stricter. PRAW 8 refuses to start if your user_agent string is generic (python:bot:1.0 won't fly). Reddit's API team has been aggressive about identifying and throttling anonymous-looking traffic since the May 2026 scraper crackdown, and PRAW now enforces the format they expect at client init.
  2. The rate-limit backoff is different. v7 would sleep in short bursts. v8 sleeps once, longer, based on the x-ratelimit-reset header. If your script assumed a burst pattern, your log will now look frozen for 300+ seconds.
  3. SubredditStream yields differently on cold start. By default it now skips historical items and starts from "now". If you were relying on backfill to seed a database on first run, you have to pass skip_existing=False explicitly.

Migrating from v7 is mostly a config-file and user-agent job. If you've never touched PRAW before, install fresh and skip the migration section entirely.

The architecture

One process, three responsibilities: pull, filter, notify. Slack is the notification target because it's where founders already read things, but the same pattern works for Discord, email digests, or a database.

Rendering diagram…

Keep it one process. A queue + worker split is tempting and premature. Reddit's rate limits are per-OAuth-app, so parallelism doesn't buy you speed, it just fragments your budget.

Step 1: register the app and set up praw.ini

Go to https://www.reddit.com/prefs/apps and create a script app. You need the client ID (under the app name), the client secret, and a Reddit account that will act as the script's identity. Use a dedicated account, not your personal one.

Create praw.ini in your project directory:

[buyerbot]
client_id=YOUR_CLIENT_ID
client_secret=YOUR_CLIENT_SECRET
username=your_bot_account
password=your_bot_password
user_agent=linux:com.yourcompany.buyerbot:v1.0 (by /u/your_bot_account)

The user_agent format matters. PRAW 8 checks for a platform, a reverse-domain identifier, a version, and an operator handle. Skip any of those and the client raises on the first request.

Step 2: the poller

import praw
import re
import time
import requests

reddit = praw.Reddit("buyerbot")

SUBS = "SaaS+Entrepreneur+startups+webdev+nocode"

INTENT_PATTERNS = [
    r"\b(any|what|which|recommend|suggest)\s+(tool|app|service|product)",
    r"\b(looking for|need|searching for)\b.*\b(that|which|to)\b",
    r"\b(alternative to|instead of|replacement for)\b",
    r"\b(is there a|does anyone know a)\b",
    r"\bwilling to pay\b",
]

COMPILED = [re.compile(p, re.IGNORECASE) for p in INTENT_PATTERNS]
SLACK_WEBHOOK = "https://hooks.slack.com/services/XXX/YYY/ZZZ"
seen = set()

def matches_intent(text):
    return any(p.search(text) for p in COMPILED)

def notify(item, kind):
    if item.id in seen:
        return
    seen.add(item.id)
    text = item.title if kind == "post" else item.body
    payload = {
        "text": f"*{kind}* in r/{item.subreddit.display_name}\n"
                f"<{item.permalink and 'https://reddit.com' + item.permalink}|link>\n"
                f"> {text[:300]}"
    }
    requests.post(SLACK_WEBHOOK, json=payload, timeout=5)

def run():
    subreddit = reddit.subreddit(SUBS)
    while True:
        try:
            for submission in subreddit.stream.submissions(skip_existing=True, pause_after=0):
                if submission is None:
                    break
                if matches_intent(f"{submission.title} {submission.selftext}"):
                    notify(submission, "post")
            for comment in subreddit.stream.comments(skip_existing=True, pause_after=0):
                if comment is None:
                    break
                if matches_intent(comment.body):
                    notify(comment, "comment")
        except Exception as e:
            print(f"error: {e}, sleeping 60s")
            time.sleep(60)

if __name__ == "__main__":
    run()

A few things worth calling out:

  • pause_after=0 makes each stream yield None when it runs out of new items so you can alternate between submissions and comments in one process instead of blocking on one forever.
  • skip_existing=True matches the v7 default behavior. Turn it off only if you're doing a backfill.
  • The seen set is in-memory; for anything running longer than a day, swap it for a SQLite table keyed on item.id.

Step 3: buyer-intent filters that don't drown you

The regex list above is the starting point. Every real deployment ends up with a two-layer filter: a broad regex sieve, then a tighter contextual check. Broad regex alone matches too much. Full-text embedding search matches too little of the noise you'd actually want to catch.

What works in practice:

  • Positive patterns: "looking for a tool that…", "any alternatives to X", "willing to pay for", "does anyone use…", "switched from X to Y".
  • Negative patterns to strip: "I built", "launched", "my SaaS" (these are people announcing, not asking).
  • Score threshold: on posts older than 30 minutes, require score ≥ 2 before alerting. Filters out immediate mod removals and posts that landed with zero engagement.

If you want to go further, add a cheap LLM call as the second stage. Pass the matched text to a model with a one-shot classification prompt ("is this person actively looking for a product to buy: yes/no"). Keep the model call gated behind the regex match so you're not paying for every post in a busy sub.

Step 4: the rate-limit reality

PRAW 8 handles the 429s for you, but you still have a real budget: 100 queries per minute per OAuth app for authenticated scripts. subreddit.stream uses .new() under the hood on a fixed interval, and each .new() on a multireddit counts as one request. Five subs in a +-joined multireddit is one request, not five. That's the trick that keeps this workflow cheap.

Things that will burn your budget:

  • Iterating comments on every matched post to "enrich" the alert. Don't. Send the permalink, open it when the alert fires.
  • Fetching author.link_karma for every hit. You almost never actually use it in the Slack message.
  • Running two streams (comments + submissions) as separate processes on the same OAuth app. They fight for the same 100/min bucket.

Step 5: running it in production

systemd unit, one file, restart on failure:

[Unit]
Description=Reddit buyer-intent poller
After=network.target

[Service]
Type=simple
User=buyerbot
WorkingDirectory=/opt/buyerbot
ExecStart=/opt/buyerbot/venv/bin/python poller.py
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

Log to stdout, let journald collect it. If you want a fancier setup, ship the logs to Loki or Datadog, but for a single-founder monitor this is enough.

One thing to build in from day one: an ack workflow. When you reply to a lead, click a Slack reaction () on the alert. That's your record of what you actioned. Once a week, look at the un-acked alerts and figure out whether the filter is too loose or you're just missing days.

When rolling your own stops being worth it

A hand-rolled PRAW poller is 50 lines of code, one server, and about an hour of setup. Great for one product, one niche, a handful of subs.

It stops scaling when you want (a) multiple products with different keyword sets, (b) full comment-thread context on matches, (c) author history to filter out obvious low-quality accounts, or (d) DM-ready message drafts attached to each alert. At that point you're building a small product, not a script, and Bazzly does that watching for you with the enrichment already wired in.

Either way, PRAW 8 is the right foundation. The v7 scripts on GitHub will keep working for another few months, but the auth format is going to bite eventually. Rewrite the user-agent, pin praw>=8.0.2, and add skip_existing explicitly wherever you use a stream. That gets you through the next twelve months without a 3am pager.

Related reading