Deploying My First AI Agent: Six Disasters and the Safety Patterns That Fixed Them
Table of Contents
- I went live feeling confident
- Disaster one: a US$127.80 Gemini bill
- Lesson
- Disaster two: the same post published five times
- Lesson
- Disaster three: comment replies turned into an infinite loop
- Lesson
- Disaster four: five accounts blocked at the same time
- Lesson
- Disaster five: every old post treated as new on a fresh deploy
- Lesson
- Disaster six: the token expired and the system did not know
- Lesson
- The automation philosophy that came out of the wreckage
- All Safety Patterns in one place
- Next post
Disclosure: Ultra Lab, MindThread and Ultra Advisor are products of the same group, Ultra Creation (傲創實業).
I went live feeling confident
In the last post I explained why I decided to build an AI team. This one is about what happened after that decision.
One weekend in early March 2026, I spent a whole day setting up my first agent. I had a Gemini API key, the systemd timer was configured, and I thought the prompt was pretty good.
I ran systemctl --user enable openclaw-autopost.timer and thought: "I'll wake up tomorrow to new posts."
Then I went to bed.
That was my first mistake.
Disaster one: a US$127.80 Gemini bill
I woke up, opened my inbox, and saw a billing notice from Google Cloud.
US$127.80.
I thought I was on the free tier. In fact, the API key I had created in the GCP Console belonged to a project with billing enabled. The Gemini free tier only applies to keys created in AI Studio.
Overnight my agent made hundreds of API calls (including retries and debugging), and every one of them cost money.
Lesson
❌ Gemini API key created in GCP Console → billed
✅ Created in AI Studio (aistudio.google.com) → free 1,500 RPD
That lesson cost US$127.80. Expensive, but I have never gotten it wrong since.
Update, 27 September 2026. A key created in AI Studio is not automatically free: if the Google Cloud project behind it is linked to a billing account, usage is charged all the same. See the Gemini billing trap. The "free 1,500 RPD" in the lesson above is the figure from the time of writing (2026-04-14); check Google's current limits before relying on it.
Safety pattern: set a hard cap at the gateway layer. At the time, our OpenClaw Gateway enforced 1,500 RPD + 15 RPM, and anything over that got a 429 straight away. The point of a hard cap is that even a badly written script cannot run up an unbounded bill.
Disaster two: the same post published five times
In the morning I checked Moltbook and found five nearly identical posts on my agent's account.
The cause was dumb: the timer was set to run every hour (I thought it was once a day), and the prompt had no deduplication logic, so every time it fired it generated roughly the same thing.
Readers do not think "wow, this account is hard-working." They think "this is a spam bot."
Lesson
An auto-posting system needs at least these three lines of defence:
1. Cooldown
COOLDOWN_FILE="$WORKSPACE/last-post-time"
if [ -f "$COOLDOWN_FILE" ]; then
last=$(cat "$COOLDOWN_FILE")
now=$(date +%s)
elapsed=$(( now - last ))
if [ $elapsed -lt 21600 ]; then # 6 hours
echo "冷卻中,跳過"
exit 0
fi
fi
2. Title deduplication
# Get the 10 most recent titles
recent_titles=$(cat "$WORKSPACE/post-history.json" | \
jq -r '.[-10:][].title')
# Put them into the prompt
DEDUP_INSTRUCTION="以下標題最近已經發過,不要重複:
$recent_titles"
3. Content fingerprint
Compute a hash before posting, and if it is too similar to any of the last 10 posts, discard it and generate again. Not a perfect solution, but good enough.
Disaster three: comment replies turned into an infinite loop
I added auto-reply: when someone commented, the agent replied.
The problem: after the agent replied, the system detected a "new comment" (the agent's own reply) and triggered another reply.
The result was an ever-growing thread of the agent talking to itself.
Lesson
Conversation depth tracking (Conversation Threading):
# conversation-threads.json format:
# { "post_id": { "depth": 2, "lastReply": "2026-03-05T10:00:00Z" } }
MAX_DEPTH=2 # reply at most 2 rounds
current_depth=$(jq -r ".\"$post_id\".depth // 0" \
"$DATA_DIR/conversation-threads.json")
if [ "$current_depth" -ge "$MAX_DEPTH" ]; then
echo "已達最大對話深度,跳過"
exit 0
fi
Plus a batch limit: at most 5 replies per scan. No matter how many comments are waiting, it handles only 5 at a time. The rest wait for the next round.
This prevents two problems:
- Infinite loops
- Replying to too many at once and getting flagged as spam by the platform
Disaster four: five accounts blocked at the same time
MindThread runs auto-posting on multiple Threads accounts. One morning, five accounts were blocked from posting by Threads at the same time.
The cause: every account's schedule was too close together (all triggered on the hour), the behaviour patterns were identical, and the platform's anti-spam system caught it.
Lesson
Schedules should look like a person, not a machine.
# Don't do this:
OnCalendar=*-*-* 09:00:00
OnCalendar=*-*-* 12:00:00
# Do this:
OnCalendar=*-*-* 09:00:00
RandomizedDelaySec=300 # random delay of 0-5 minutes
We also added an Account Health Monitor:
# Scan all accounts every 6 hours
# Use the container creation API to test whether each account is working
# If blocked → send an instant Telegram alert to the boss
if [ "$status" = "blocked" ]; then
curl -s -X POST "https://api.telegram.org/bot$TG_TOKEN/sendMessage" \
-d "chat_id=$BOSS_CHAT_ID" \
-d "text=⚠️ $account_name 被封鎖發文!"
fi
Getting blocked is not the scary part. The scary part is getting blocked without knowing it, while the account just sits there idling.
Disaster five: every old post treated as new on a fresh deploy
I have a blog-to-social.sh script that detects new blog posts and automatically publishes them to Moltbook.
The logic: scan the content/blog/ directory, and if a file is newer than the last scan time, treat it as a new post and publish it.
The problem: on the first deploy there is no "last scan time". So it treated all 60+ historical posts as new and published them all at once.
Moltbook received 60 posts from the same account in a single morning.
Lesson
First-Run Safety Pattern:
LAST_RUN_FILE="$DATA_DIR/blog-to-social-lastrun"
if [ ! -f "$LAST_RUN_FILE" ]; then
echo "首次執行,建立基準線(不處理歷史文章)"
date +%s > "$LAST_RUN_FILE"
exit 0
fi
On the first run, it only records a timestamp and does nothing else. Normal operation starts from the second run.
It could not be simpler, but if you do not think of it, things go badly wrong. I now add this pattern to every new script.
Disaster six: the token expired and the system did not know
We only fixed this bug recently (the day before I wrote this post).
Threads API tokens are valid for 60 days. We had an automatic refresh mechanism, but it only checked "how many days are left in theory" and never actually verified whether the token still worked.
Then Facebook invalidated some tokens early for security reasons, and our system calculated "17 days left, no need to refresh" and skipped them. Three accounts sat idle for four days before I noticed.
Lesson
Do not just count days. Actually verify:
def validate_threads_token(access_token):
"""Check whether the token is actually valid"""
r = requests.get(
"https://graph.threads.net/v1.0/me",
params={"fields": "id", "access_token": access_token},
timeout=10,
)
if r.status_code == 200 and "id" in r.json():
return True, "ok"
return False, r.json().get("error", {}).get("message", "unknown")
Our token refresh worker now:
- Checks the day count first (refreshes when expiry is close)
- Then checks health data (refreshes if the API has reported errors)
- Sends a Telegram alert if the refresh fails, so I can handle it manually
Two lines of defence, no more blind spots.
The automation philosophy that came out of the wreckage
These disasters taught me one thing:
Automation is not "set it and forget it". It is "set it, then build monitoring".
Every automated process should have matching monitoring:
| Process | Monitoring |
|---|---|
| Auto-posting | Cooldown + deduplication + account health checks |
| Auto-replies | Depth limit + batch limit |
| Token refresh | Day count + actual verification + failure alerts |
| Content sync | First-run safety + baseline setup |
| The whole system | Health monitor + Telegram alerts |
My rule now: after writing an automation script, spend the same amount of time writing its safety mechanisms. If you think "it won't go wrong", it definitely will.
All Safety Patterns in one place
| Pattern | What it solves | Core idea |
|---|---|---|
| Gateway hard cap | Runaway bills | Block at the entry point, do not trust downstream |
| Cooldown File | Over-posting | Time lock that enforces a minimum interval |
| Title deduplication | Duplicate content | Feed history into the prompt |
| Max Depth Threading | Reply loops | Track conversation depth |
| RandomizedDelay | Getting blocked | Make behaviour look human |
| First-Run Safety | Historical flood | First run only sets a baseline |
| Atomic Lock (mkdir) | Race conditions | Atomic operation guaranteed by POSIX |
| Health Monitor | Silent failures | Active detection + instant alerts |
| Token double verification | Blind spots | Check both the theory and the reality |
All of these patterns are open source in free-tier-agent-fleet. Use them directly.
Next post
After hitting all these problems and building the safety mechanisms, I started thinking seriously about one question: how do I make an agent do more than repeat what I taught it, and actually produce content with a "brain"?
The answer is to give it intelligence. Let it do market research automatically every day, read the latest trends, and analyse competitors, all completely free.
Next: A Zero-Cost Intelligence System: Let AI Do Your Market Research Every Day
This is part 2 of the "An AI Team for a One-Person Company" series. Part 1: Why I Needed an AI Team
All source code: GitHub: free-tier-agent-fleet