Back to blog
resiliencehermesproxytailscaleohanasmartgmailcronjobs
·6 min read

YouTube Banned My VPS

On the morning of April 17, the noticias-ilustradas cronjob sent me an unusual message:

I found today's NOTICIASILUSTRADAS video but couldn't extract the transcript. All routes failed — Hermes skill, yt-dlp with cookies, direct curl. The VPS IP is banned by YouTube for transcriptions.

Until that message, I thought my morning digest was resilient. It had been working for weeks. But a silent change in YouTube's policy (rate limit? datacenter heuristic?) had taken it offline.

That day I ended up doing three fixes in parallel. They all boil down to the same lesson: a system that "works" and a system that "is resilient" are different things.

Fix 1: From YouTube Transcript to Brave Search API

The old route

Cron → find today's NOTICIASILUSTRADAS video
     → yt-dlp download + extract captions
     → LLM summarize
     → TTS → voice message to Telegram

It depended on YouTube in two places: listing channel videos and extracting transcript. Both steps sensitive to IP.

The question I asked myself

Before building anything, I paused: do I really need YouTube? The actual goal is getting a summary of the day's news in audio at 06:57. YouTube was the how, not the what.

The new route

Cron → Brave Search API with current events query
     → LLM summarizes headlines
     → TTS → voice message to Telegram

Brave Search API doesn't ban datacenter IPs. Doesn't need cookies. Doesn't need transcripts. And at digest time, fresh headlines are more useful than a 15-minute video from last night.

I changed audio_matutino.py, ran it manually, got a 407KB voice message, reinstalled the cron (57 6 * * *). 20 minutes.

The question "do I really need this?" saves more code than any refactor.

Fix 2: The Residential RPi as a Safety Net

Though Brave solved the immediate problem, the root issue remains: any service that bans Hostinger IPs leaves that cron offline. And it will happen again.

I already had the residential RPi proxy (set up April 12, 100.78.170.78:8888 via Tailscale). That day I left it documented as "option B for when needed". Well, it was needed already.

I added the proxy as an optional fallback in sensitive scripts: if I detect a block, pivot to residential output. I didn't rewrite everything — just marked the points where it could re-engage easily.

Lesson: having infrastructure ready isn't enough. You have to explicitly mark where it will be used when needed, or when failure arrives you'll be reinventing the wheel.

I'd set up the RPi proxy 5 days before "just in case". That "just in case" arrived at 72 hours. "Just in case" always comes sooner than you think.

Fix 3: Drafts-Only Policy for Email Sender

That same day I hardened something I'd been carrying: the send_via_gmail.py script that Diana Ferrer (OhanaSmart's business development) uses for outreach.

The Fear

I have a daily cronjob that prepares a batch of prospects to send. If that cron, due to a bug or mis-tuned prompt, decided to send 50 emails to hotel sector decision-makers with crude tone — there's no taking it back. That burns the whole business.

The Solution

send_via_gmail.py no longer sends. It creates a draft in Gmail.

# Before:
service.users().messages().send(userId='me', body=message)

# Now:
service.users().drafts().create(userId='me', body={'message': message})

A different Gmail API call. Zero change to my workflow: every morning I open Gmail, check the "Drafts" column, and send the ones that make sense. The ones that don't, I edit or delete.

That line of code is more resilient than any governance toolkit. It doesn't depend on security runtime, can't be bypassed, it's architectural. If my agent goes crazy tomorrow, the worst it can do is leave 50 weird drafts. Nothing goes out without my click.

Rule: for irreversible decisions (email to a client, commit to main, payment, actual send), putting a human in the middle isn't friction — it's the only defense that can't be hacked.

Fix 4 (bonus): Diana's Templates in Divs, Not Tables

That same day I discovered Diana's email HTML templates were breaking in Gmail when sent as drafts. The <table> turned into random columns when opening the draft from Gmail's UI.

I rewrote all templates to <div> with max-width: 80% and inline styles. Signature at 14px, no centering, inline image via Content-ID.

Not glamorous. But the difference between an email that looks professional in Gmail and one that looks like a 2003 form is that <div> vs <table> detail.

The Common Thread

The four changes that day (YouTube → Brave, RPi as fallback, drafts-only, divs instead of tables) answer the same question:

"When this breaks — because it will — what happens?"

  • When YouTube bans → digest switches to Brave Search, zero intervention.
  • When another service bans → RPi is there as residential output.
  • When my agent goes haywire → worst case is a draft, not a send.
  • When Gmail breaks a <table> → the <div> holds.

Each one closes a failure mode. None is sophisticated. None needs new architecture. All four are small decisions with large consequences.

What I Learned

  1. "Works" is not "resilient". A system that works until something external changes (IP policy, rate limit, HTML format) is fragile by design. Resilient is what survives the change.

  2. Ask if you need it before fixing it. The YouTube fix wasn't fixing YouTube. It was realizing YouTube was the implementation, not the requirement.

  3. Ready infrastructure is worth zero until you use it. The RPi had been set up 5 days as "just in case". Until I plugged it into a real script, it was a decoration.

  4. Architectural controls beat runtime controls. Drafts-only is a single line of code that does what no governance toolkit can: make catastrophic failure impossible.

  5. Sloppy details kill credibility. An email to a residence director with a broken template is worse than not sending it. Spending an afternoon on templates that don't break in Gmail beats a day of prospecting.

What's Next

  • Wire the RPi proxy as automatic fallback when a request fails by IP (today it's manual)
  • Monitor for the noticias-ilustradas cron to alert if output drops below X KB (proxy for "something broke")
  • Replicate the drafts-only policy to any script that sends externally — Telegram should also be reviewable before sending in sensitive cases

No big changes. Many small ones. That's the difference between a system that works for a month and one that lasts a year.


— I, Johnny — configured agent: Harvie. Resilience isn't added at the end. It's discovered each time something breaks.