Tutorialthe whole build, step by step

Four bots that run your content: a topic finder, a critic, a comment answerer and a score keeper

Starter versions of the four bots that run my content: one finds topics, one fails bad cuts, one answers comments, one keeps score. You still approve every post.

By Eric Snyder, founder 8 min readChecked on Oct 6, 2026
Save this guidereceiptsgroup.com/guides/four-content-bots
SetupAbout an hour per bot; the comment bot is mostly clicking
Running costClaude Pro $20 a month (checked 2026-10-04); the YouTube API is free; the comment and score bots use tools you may already pay for
You needA paid Claude plan, a terminal, and a channel you post short videos to
Works onMac, Linux, or Windows with WSL (its built-in Linux)
Safe by defaultNo bot here can post. You approve every video.
In this guide
Start here
  1. The easy way: let Claude build them with you
The manual way
  1. How the four fit together
  2. Before you start
  3. Bot 1: the topic finder
  4. Bot 2: the critic
  5. Bot 3: the comment answerer
  6. Bot 4: the score keeper
  7. Real use: prompts for real jobs
  8. What it costs
  9. What my version adds (and this guide doesn't)
  10. When it breaks
  11. Sources, checked Oct 6, 2026

Four bots run my content. One finds what's breaking out in my lane. One watches every cut and fails it when it's not phone-ready. One answers comments with the guide. One pulls every post's numbers every three hours. And I still say yes to every post: none of them can publish anything.

This guide builds the starter version of each, with Claude Code doing the typing. You don't need to code. Each bot is one small script or one setting, and each one is useful on its own, so build them in any order.

Checked on Oct 6, 2026

Start herepart 1

The easy way: let Claude build them with you

Paste one prompt into Claude Code and it builds the bots one at a time, from the code boxes in this guide, testing each before the next. You do the parts that need your accounts (API keys, connecting Instagram) yourself.

bashMake the folder and start Claude in it
mkdir -p ~/content-bots
cd ~/content-bots
claude
PromptThe setup prompt (copy all of it)
Hi Claude. Please be my patient setup helper. I don't code: one step at a time, tell me exactly what to type or click, and wait for me.

We're building four content bots in this folder (~/content-bots), following this guide:
https://receiptsgroup.com/guides/four-content-bots
Its manual-way sections are the source of truth. Copy every file exactly from its code box; only fill in my details where it shows <angle brackets>.

Ask me which bots I want, then build them in this order, testing each one with the guide's test before moving on:
1. The topic finder (bots/scout.py, topics/keywords.txt). Help me pick 5-10 keywords for my lane.
2. The critic (prompts/critic.md and the frame command).
3. The comment answerer (Zernio, Instagram and Facebook). Walk me through the clicks; I connect the accounts myself.
4. The score keeper (bots/score.py) and its schedule.

Rules:
- Nothing you build may post, publish, DM or comment by itself, except the comment answerer I set up myself in Zernio.
- Never read, print or ask for API keys or tokens. Tell me which line of .env to fill in and let me do it.
- Before any command that changes something outside this folder (like crontab), explain it and wait for my yes.

Start by asking which bots I want.
The manual wayparts 2 to 11

How the four fit together

This is the manual way: every file and every setting. Use it by hand or keep it open to check what Claude is doing.

  1. 1
    The topic finderbots/scout.py: YouTube search for your keywords, last 7 days, ranked against each channel's usual→ topics/today.md
  2. 2
    You make the videoYour camera, your avatar, your editor (my content engine guide)→ a finished cut
  3. 3
    The criticffmpeg pulls frames; Claude checks them against prompts/critic.md, read-only→ PASS, or FAIL with times
  4. 4
    Your yesYou watch it and post it→ a live post asking people to comment a keyword
  5. 5
    The comment answererZernio: comment the keyword, get the link by DM (Instagram checks they follow you)→ DMs and comment replies
  6. 6
    The score keeperbots/score.py every 3 hours: Upload-Post's per-post analytics→ scores.csv
The loop: the score keeper tells you which topics and formats worked, so next week's keywords and picks aren't a guess.

Words you'll see

  • API key: a long password that lets a script use one service. Lives in .env, never in a chat or a screen recording.
  • Breaking out: a video getting far more views than its channel usually gets. A big channel's normal video isn't a trend; a small channel's 20x video is.
  • Comment-to-DM: someone comments a word on your post, and a tool sends them a direct message with a link.
  • cron: the clock built into Mac and Linux; one line says when to run what.

Before you start

  • Claude Code (Claude Pro, Max, Team or Enterprise). Install: curl -fsSL https://claude.ai/install.sh | bash, then a new terminal and claude --version.
  • Python 3.9+ and FFmpeg (python3 --version, ffmpeg -version).
  • A Google account for a free YouTube Data API key (bot 1).
  • An Instagram Business or Creator account and a Facebook Page, connected in Zernio (bot 3). Check Zernio's pricing page for the plan that includes comment automations.
  • Posts sent through Upload-Post for bot 4 (it reads their analytics). My content engine guide sets that up.
bashThe folder
mkdir -p ~/content-bots/{bots,prompts,topics,frames}
cd ~/content-bots
touch .env && chmod 600 .env
echo '.env' >> .gitignore

Bot 1: the topic finder

Mine scans YouTube and TikTok for short how-to and result videos in my lane that are beating their channel's usual by a mile, and throws out ads and brand posts. The starter version does the YouTube half with Google's official API.

  1. Get a YouTube Data API key

    Whereconsole.cloud.google.com

    Create a project, open APIs & Services, then Library, search for YouTube Data API v3 and click Enable. Then Credentials, Create credentials, API key. Restrict the key to the YouTube Data API v3. Paste it into .env as YOUTUBE_API_KEY=... yourself.

    PermissionsThe key only reads public YouTube data. It can't see or change your channel.
    What you'll seeGoogle Cloud console with the YouTube Data API v3 enabled and the key restricted.
  2. List your keywords

    5 to 10 phrases people in your lane search for. Specific beats broad: “roof leak repair” finds more you can use than “roofing”.

    topics/keywords.txt
    # one search per line
    <keyword 1>
    <keyword 2>
    <keyword 3>
  3. Save the scout

    pythonbots/scout.py
    #!/usr/bin/env python3
    """Bot 1, the topic finder: short videos in your lane from the last 7 days, ranked by how far they beat their channel's
    average. Read-only: it searches YouTube and writes topics/today.md. Nothing is downloaded.
    
        python3 bots/scout.py
    
    Needs YOUTUBE_API_KEY in .env. Keywords come from topics/keywords.txt (one per line, # for comments).
    """
    import datetime
    import json
    import os
    import urllib.parse
    import urllib.request
    from pathlib import Path
    
    HOME = Path(__file__).resolve().parent.parent
    API = "https://www.googleapis.com/youtube/v3"
    DAYS, MIN_VIEWS = 7, 1000
    
    
    def env(name):
        f = HOME / ".env"
        for line in (f.read_text().splitlines() if f.exists() else []):
            line = line.removeprefix("export ").strip()
            if line.startswith(name + "="):
                return line.split("=", 1)[1].strip().strip("'\"")
        return os.environ.get(name, "")
    
    
    def get(path, **params):
        params["key"] = env("YOUTUBE_API_KEY")
        with urllib.request.urlopen(f"{API}/{path}?{urllib.parse.urlencode(params)}", timeout=30) as r:
            return json.load(r)
    
    
    def main():
        now = datetime.datetime.now(datetime.timezone.utc)
        since = (now - datetime.timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ")
        words = [w.strip() for w in (HOME / "topics" / "keywords.txt").read_text().splitlines()
                 if w.strip() and not w.startswith("#")]
        found = {}
        for q in words:  # short videos (under 4 minutes) from the last week, most viewed first
            r = get("search", part="snippet", q=q, type="video", order="viewCount", videoDuration="short",
                    publishedAfter=since, maxResults=25, relevanceLanguage="en")
            for it in r.get("items", []):
                found.setdefault(it["id"]["videoId"], q)
        ids, videos = list(found), []
        for i in range(0, len(ids), 50):
            videos += get("videos", part="snippet,statistics", id=",".join(ids[i:i + 50])).get("items", [])
        chans = list({v["snippet"]["channelId"] for v in videos})
        avg = {}
        for i in range(0, len(chans), 50):  # a channel's average views per video = its usual
            for c in get("channels", part="statistics", id=",".join(chans[i:i + 50])).get("items", []):
                s = c["statistics"]
                n = int(s.get("videoCount") or 0)
                avg[c["id"]] = int(s.get("viewCount") or 0) / n if n else 0
        rows = []
        for v in videos:
            s, sn = v["statistics"], v["snippet"]
            views, likes = int(s.get("viewCount") or 0), int(s.get("likeCount") or 0)
            usual = avg.get(sn["channelId"]) or 0
            if views < MIN_VIEWS or not usual:
                continue
            if views >= 5000 and likes / views < 0.004:  # huge views, almost no likes: usually a paid promotion
                continue
            posted = datetime.datetime.fromisoformat(sn["publishedAt"].replace("Z", "+00:00"))
            days = max(1, (now - posted).days)
            rows.append((round(views / usual, 1), views, days, sn["channelTitle"], sn["title"], v["id"], found[v["id"]]))
        rows.sort(reverse=True)
        out = [f"# Breaking out in my lane, {now:%Y-%m-%d}", "",
               "| vs their usual | views | days old | channel | video | keyword |", "|---|---|---|---|---|---|"]
        out += [f"| {r[0]}x | {r[1]:,} | {r[2]} | {r[3]} | [{r[4]}](https://www.youtube.com/watch?v={r[5]}) | {r[6]} |"
                for r in rows[:20]]
        (HOME / "topics").mkdir(exist_ok=True)
        (HOME / "topics" / "today.md").write_text("\n".join(out) + "\n")
        print("\n".join(out[:9]))
    
    
    if __name__ == "__main__":
        main()
  4. Run it and read the list

    Good: a table of recent short videos with a “vs their usual” number; the top ones are the breakouts. Empty: your keywords are too narrow or too new; add broader ones.

    bashTest 1
    python3 bots/scout.py
    open topics/today.md 2>/dev/null || cat topics/today.md
  5. Have Claude pick the ones you can do better

    PromptPrompt
    Read topics/today.md. Pick the three topics I could make a better version of with my own setup and my own results. For each: the topic in one line, what I'd show on screen that's mine, and what I'd say differently. Don't reuse anyone's script, list, steps or wording. Read-only.

Bot 2: the critic

Mine measures every scene of a cut and fails it for text too small to read on a phone, a head too tiny in the frame, big empty areas, and the same layout twice in a row. Rude. Accurate. The starter version uses Claude's eyes on a frame every two seconds. It can't fix anything; it can only fail the cut and say where.

markdownprompts/critic.md
You are a critic with one job: say what's wrong with a vertical video, frame by frame. You never fix anything.

You get a folder of frames, one every 2 seconds (frame 1 = 0 s, frame 2 = 2 s, ...). Check each one:
1. Text: any word too small to read on a phone (under about 1/40 of the frame's height) fails.
2. Head: when a person is the subject, a face smaller than about 1/8 of the frame's height fails.
3. Empty: about a third of the frame or more with nothing in it fails.
4. Edges: text cut off, or in the top 7% or bottom 15% where the apps put their buttons, fails.
5. Repeats: two frames in a row that look the same for 6+ seconds fails.
6. Label: if it's made with an AI avatar, it's labelled as AI, on screen or with the platform's AI-content label.

Answer exactly:
PASS
or
FAIL
- <time>: <check number> <what's wrong> -> <the fix, one line>
If you're not sure, it passes.
bashTest 2: run it on a finished video
rm -rf frames/latest && mkdir -p frames/latest
ffmpeg -v error -i <path to your video>.mp4 -vf fps=1/2 frames/latest/f%03d.png
claude -p "Follow prompts/critic.md. Frames: frames/latest/" --permission-mode dontAsk --allowedTools "Read,Glob"

Good: PASS, or FAIL lines with times you can check yourself. Fix them in your editor, or have Claude fix them if Claude made the edit, then run the critic again. Then watch the final one yourself, with sound. The critic is a filter, not a judge.

Bot 3: the comment answerer

Mine works like this: comment one word on Instagram, it checks you follow me, then DMs you the guide. On Facebook it DMs straight away. Everywhere else it replies with where to find it. The starter version is the Instagram and Facebook part, in Zernio. Only Instagram exposes whether someone follows you, so the follow check is Instagram-only.

  1. Connect your accounts

    WhereZernio

    Sign up, create a profile, and connect your Instagram (Business or Creator) and your Facebook Page. Instagram must be a professional account for DM automations.

    PermissionsMeta asks you to let Zernio read comments and send messages for the accounts you connect. Connect only the accounts you post from.
  2. Make one automation per post

    WhereZernio, Comment automations

    Pick the post, then set: the keyword (guide, matched as a whole word), the DM text with a button that opens your link, an optional public reply (“Sent it to your DMs”), and on Instagram, followers only, with a follow-first message for people who don't follow you yet. Zernio's docs give per-post automations priority over account-wide ones on the same keyword.

  3. Or make it with one API call

    Same thing from the terminal. Your Zernio API key goes in .env as ZERNIO_API_KEY; the account and post ids come from Zernio. Drop the audience and followGate lines for a Facebook post.

    bashCreate an Instagram comment-to-DM automation
    set -a; . ./.env; set +a
    curl -s -X POST https://zernio.com/api/v1/comment-automations \
      -H "Authorization: Bearer $ZERNIO_API_KEY" -H "Content-Type: application/json" \
      -d '{
        "profileId": "<your Zernio profile id>",
        "accountId": "<your Instagram account id in Zernio>",
        "platformPostId": "<the post id>",
        "name": "GUIDE on <post name>",
        "trigger": "comment",
        "keywords": ["guide"],
        "matchMode": "word",
        "dmMessage": "Here you go! The full guide is below.",
        "buttons": [{"type": "url", "title": "Open the guide", "url": "<your guide link>"}],
        "commentReply": "Sent it to your DMs",
        "audience": {"followerStatus": "follower", "whenUnknown": "verify"},
        "followGate": {"message": "Follow me first, then tap the button and it is yours.", "buttonLabel": "I am following", "notFollowingMessage": "Not following yet. Follow, then tap again."}
      }'
  4. Test it with a second account

    From another Instagram account, comment “guide” on the post. You should get the follow-first message, then the DM after following. Then ask your caption to say it: “Comment GUIDE and I'll send you...”. A keyword nobody's told about gets no comments.

Bot 4: the score keeper

Mine pulls every post's numbers every three hours and compares posts at the same age, because a five-day-old post always beats a one-day-old one on totals. The starter version appends every post's latest numbers to a spreadsheet file every three hours. It needs posts sent through Upload-Post (the upload answer includes a request_id per job; this bot reads them).

pythonbots/score.py
#!/usr/bin/env python3
"""Bot 4, the score keeper: every posted video's numbers, per platform, appended to scores.csv. Read-only on your
accounts: it only reads analytics from Upload-Post.

    python3 bots/score.py

Needs UPLOAD_POST_API_KEY in .env. It finds your posts' request ids in the upload responses your posting step saved
(runs/*/sent.json from the content engine guide) or in posted.txt (one request id per line).
"""
import csv
import datetime
import json
import os
import re
import time
import urllib.request
from pathlib import Path

HOME = Path(__file__).resolve().parent.parent
API = "https://api.upload-post.com/api/uploadposts/post-analytics/"
FIELDS = ["checked_at", "request_id", "platform", "views", "likes", "comments", "shares", "saves", "post_url"]


def env(name):
    f = HOME / ".env"
    for line in (f.read_text().splitlines() if f.exists() else []):
        line = line.removeprefix("export ").strip()
        if line.startswith(name + "="):
            return line.split("=", 1)[1].strip().strip("'\"")
    return os.environ.get(name, "")


def request_ids():
    ids = set()
    for f in HOME.glob("runs/*/sent.json"):
        ids |= set(re.findall(r'request_id\W+([0-9a-f]{16,})', f.read_text()))
    p = HOME / "posted.txt"
    if p.exists():
        ids |= {l.strip() for l in p.read_text().splitlines() if l.strip()}
    return sorted(ids)


def main():
    key, now = env("UPLOAD_POST_API_KEY"), datetime.datetime.now().isoformat(timespec="minutes")
    path = HOME / "scores.csv"
    new = not path.exists()
    with path.open("a", newline="") as f:
        w = csv.DictWriter(f, fieldnames=FIELDS)
        if new:
            w.writeheader()
        for rid in request_ids():
            req = urllib.request.Request(API + rid, headers={"Authorization": f"Apikey {key}"})
            try:
                with urllib.request.urlopen(req, timeout=90) as r:
                    data = json.load(r)
            except Exception as e:  # one bad post never stops the rest
                print(f"{rid}: {e}")
                continue
            for platform, v in (data.get("platforms") or {}).items():
                m = v.get("post_metrics") or {}
                if not v.get("success") or not isinstance(m, dict):
                    continue
                w.writerow({"checked_at": now, "request_id": rid, "platform": platform, "views": m.get("views"),
                            "likes": m.get("likes"), "comments": m.get("comments"), "shares": m.get("shares"),
                            "saves": m.get("saves"), "post_url": v.get("post_url")})
            time.sleep(4)  # stay well under the live analytics limit (100 requests per 5 minutes)
    print(f"scores.csv updated at {now}")


if __name__ == "__main__":
    main()
bashTest 4
python3 bots/score.py
tail -5 scores.csv

Then put it on a clock. crontab -e opens your schedule; add this line (fix the folder from which python3 if yours is elsewhere), save, close. 15 */3 * * * means quarter past every third hour.

bashThe crontab line
15 */3 * * * cd $HOME/content-bots && /usr/bin/python3 bots/score.py >> logs.txt 2>&1

Real use: prompts for real jobs

Prompt1. What's working
Read scores.csv. For each post, take its latest row per platform. Which three posts did best on views and which on saves or shares? What do they have in common (topic, hook, length)? Read-only; say what you'd test next week.
Prompt2. Better keywords
Read topics/keywords.txt and the last week of topics/today.md. Which keywords never find anything over 3x? Suggest replacements in my lane, show me before and after, wait for my yes.
Prompt3. A meaner critic
Here's what bugged me in my last video: <your notes>. Turn each into one checkable line in prompts/critic.md. Show me the change before saving.
Prompt4. Check the comment bot
Using the Zernio API with my key from .env (never print it), list my comment automations with their triggered and sent counts. Read-only. Flag any post asking for GUIDE that has no automation.

What it costs

Claude and Upload-Post prices checked 2026-10-04. Prices change: check before you buy.
BotToolCost
All fourClaude Pro (Claude Code)$20 a month at list price
Topic finderYouTube Data APIFree within Google's daily quota
CriticFFmpeg + ClaudeFree (uses your Claude plan)
Comment answererZernioCheck Zernio's pricing page
Score keeperUpload-PostOn the plan you post with (Basic $24 a month at list price)

Every Claude run counts toward your plan's usage limits; the critic reads images, which is heavier than text, so one frame every two seconds is plenty for a short.

What my version adds (and this guide doesn't)

  • Topic finder: YouTube and TikTok, each video against its channel's recent median (not the lifetime average), promoted videos and brand channels thrown out, and a score for how remakeable it is.
  • Critic: measured, not eyeballed: text heights and faces from Apple's Vision framework, empty-space maps, layout repeats; it writes a contact sheet with every scene bordered green, amber or red.
  • Comment answerer: Zernio on Instagram and Facebook, plus my own replies on TikTok, YouTube and Threads pointing to the guide.
  • Score keeper: snapshots every 3 hours into a database, so posts compare at about 24 hours old, grouped by format, opening and length in a dashboard.

When it breaks

scout.py: HTTP Error 403
The API isn't enabled for the key's project, the key is restricted to a different API, or you hit the daily quota. Check the Google Cloud console.
scout.py: the top results are brand ads
Raise the like-rate filter, or add -brandname to that keyword (search supports NOT with a minus sign).
The critic passes something you can see is wrong
Its list doesn't cover it. Add a line describing it in a way you could check with a ruler.
Nobody gets a DM
Check the automation is on the right post, the account is Instagram Business or Creator, and the caption actually asks for the keyword.
score.py finds no posts
It needs request ids: runs/*/sent.json from the content engine, or add them to posted.txt one per line.
score.py: HTTP Error 429
Too many requests at once. Wait five minutes; the next scheduled run picks up where this one stopped.

That's the four

A bot that finds the topic, one that fails the bad cut, one that answers the comments, one that keeps score. And you, saying yes to every post. Build the one that saves you the most annoyance first.

Checked on Oct 6, 2026 against

Every claim about a third-party tool in this guide (plans, prices, menu paths, commands, limits) was checked against these official pages on Oct 6, 2026. These screens change often: if something looks different, trust the page over this guide.

Rather skip the setup?

Want help setting this up?

Receipts Group builds these systems. Thirty minutes with Eric tells you which piece is worth building first, or that none of them are. The guides stay free either way.

Keep going

more free guides