Tips · Ops Automation

My Gmail Bot Died Quietly After Ten Days — Traps I Knew About and Walked Past

Jul 17, 2026 · AI Note Lab

Ten days ago I wrote about turning monitoring emails into AI-assisted incident reports. It worked. Ten days later, that bot was dead.

The worse part is that I didn't know it was dead. The service was happily "running," the process was alive. It just wasn't sending reports. Opening the log, one line was repeating:

google.auth.exceptions.RefreshError: ('invalid_grant: Token has been expired or revoked.')

I went quiet for a while after finding the culprit. The cause was a sentence I had written myself, ten days earlier, in that very article.

The sentence I wrote and then walked right past

The setup steps in the previous article include this line:

"…you need to add yourself as a test user to avoid being blocked at the consent screen."

That is correct. When you create an OAuth consent screen in Google Cloud, the app starts in "Testing" status, and you must add your account as a test user for authentication to pass. I knew that, I did it, and I even wrote it down.

But there was one question I never asked: "If I leave it in testing mode, what does that cost me later?"

Here is the answer. A refresh token issued by an app in testing status expires after seven days. That's Google's policy. Which means my bot carried a seven-day time bomb from the moment it was born. I installed the bomb myself, documented the installation procedure, and simply never noticed the timer was running.

The fix was almost insultingly simple: publish the app to "Production" in the Google Auth Platform and reissue the token. After that it doesn't expire. A five-minute task had kept my automation dead for ten days.

And that's what this article is really about

Writing up the incident, something clicked. Every single thing that bit me was not something I didn't know. I knew about all of them — the knowledge was just one step short, or I knew and walked past anyway.

The other mistakes I found the same day had exactly the same shape.

Mistake 1. I knew it and wrote it, but never asked what came next

Testing mode is the example. I knew the procedure ("add a test user"), but never thought about the cost of the state called "testing." If you pick something on a settings screen, that should mean knowing how it differs from the option you didn't pick.

During setup, all I could see was "does authentication pass?" The moment it passed, that item was finished in my head. The trouble with automation is that working at build time and still working two months later are entirely different problems.

Mistake 2. I thought I knew, and I was wrong

While reissuing the token, a second bot became the problem. That one only reads mail (gmail.modify), while the other must send reports (gmail.send). So I thought: "Can't I just add the send scope to this token in the console?"

No. And this is a point a lot of people get wrong.

So a token consented for modify is modify-only forever. No button in the console will bolt send onto it later. I actually tried: requesting a refresh with send added to a modify token, Google rejected it with invalid_scope. The only way to widen permissions is a fresh consent that includes the new scope. There is no workaround — that is the security design.

One good thing came out of it. If several bots share the same account and client, issue one token containing every scope you need and share it — then the next re-authentication is a single job instead of several. That's exactly what I consolidated into.

Mistake 3. I skipped a safety net the API was already offering

The same day, a different bot threw this: json.decoder.JSONDecodeError: Expecting ',' delimiter: line 1 column 4764

Opening the code revealed why. The prompt was merely asking the model to "respond in JSON." The design just hoped the model would comply. The expensive model happened to comply; the cheaper one broke the format in the middle of a long string.

But the OpenAI Responses API already offers schema-enforced output via json_schema. Give it a schema with strict: true and the model cannot deviate. This was not an unknown feature. I just didn't use it — because it was working fine.

The consequence wasn't a mere bug. Because the format wasn't enforced, I was locked into "expensive models that happen to follow formats." The price of skipping one safety net was my choice of model.

Mistake 4. I never designed what happens on failure

This was the scary one. My API credit ran dry that day, and re-reading the code I found this shape:

In other words, any mail arriving while credit was out would be silently lost forever. The bot would have kept running every 30 minutes, quietly discarding mail one by one. Luckily I caught it and stopped the timer first, so the loss was zero.

It was code written with only the success path in mind. I believed I had "handled the exception," but what I had actually done was make failure permanent.

Mistake 5. I enabled a tool without setting a cap

To improve article quality I gave the AI a web search tool. It clearly worked. And five dollars vanished in a single day — after the previous two weeks had cost twelve cents in total.

Told to "research the top 5 to 8 keywords," the model ran 13 searches per article. Search charges per call, and the fetched content is billed again as input tokens on the next request. Input per article jumped from 3,000 tokens to 69,000. It's not that I didn't know about the double billing. I just never decided "how many times" when I turned the tool on. (The details are in my three-model comparison.)

The common thread

Lined up together, all five collapse into one point. None of them were about not knowing.

Not one incident came from a gap in knowledge. Every one landed in the gap between knowing and preparing. And automation exploits that gap more cruelly than anything else — because when a person notices something is off, they stop. Automation keeps going.

What I took away

This is a follow-up to building the Gmail monitoring automation. The building is in that article; what happened ten days later is in this one. Keeping automation alive is harder than building it — a lesson I learned properly this time.
← Back to tips