I Built a Local AI Discount Monitor with n8n, Ollama, and Telegram

If you’re training your skills like I am, Google Cloud, you know the frustration: exam discounts appear without warning, last a few days, and vanish before you even find out they existed.

I got tired of manually checking the Google Cloud blog every day, so I automated it, using n8n, a local Ollama model, and Telegram. The whole thing runs on my Predator laptop, costs nothing, and now pings me automatically every morning.


What I Built and Why

The goal: Monitor the Google Cloud training and certifications blog daily. If a discount, promo code, or voucher appears, send a Telegram message instantly. If not, send a clean “NO DISCOUNT FOUND” confirmation so I know the system is running.

Why n8n? It’s the automation backbone of my AI Builder Roadmap, open source, self-hostable, and powerful enough to handle complex multi-node pipelines.

Why Ollama locally? I run a full local AI lab on the Predator (RTX 5070 Ti, 32GB DDR5). Using a local model means zero API costs, full privacy, and no rate limits. The model never leaves my machine.

Why Telegram? It’s instant, reliable, and I already have a bot set up. One message, one glance, done.


The Final Workflow Architecture

Schedule Trigger (9am daily)
    ↓
HTTP Request → GET https://cloud.google.com/blog/topics/training-certifications
    ↓
Code in JavaScript → Strip HTML, truncate to 3000 characters
    ↓
Message a Model → Ollama (qgemma3:12b) — classify: discount or not
    ↓
Send a Text Message → Telegram @bot

Five nodes. Runs in under 7 seconds. Fully local except for the HTTP request.


Building It: Step by Step

Step 1 — The Schedule Trigger

Set to fire daily at 9am. Straightforward — n8n’s Schedule Trigger node handles this natively.

Step 2 — HTTP Request Node

Fetches the raw HTML of the Google Cloud certifications blog page. No authentication needed, it’s a public page. This node worked first try.

Step 3 — The Code Node (Critical Fix)

This is where the first major problem appeared.

The mistake: Originally, the raw HTML from the HTTP Request went directly into Ollama. A full webpage is 50,000–100,000+ characters of HTML tags, scripts, navigation menus, and noise. Ollama was receiving all of it and timing out after 5 minutes trying to process it.

The fix: I added a JavaScript Code node between HTTP Request and Ollama. It strips all HTML tags using a regex replace and truncates the result to 3,000 characters — just the readable text content Ollama actually needs.

javascript

const html = $input.first().json.data;
const text = html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().substring(0, 3000);
return [{ json: { text } }];

This single addition dropped execution time from 5+ minutes (timeout) to under 30 seconds. Night and day.

Step 4 — Ollama Node

Connects to local Ollama at http://localhost:11434. This node sends the stripped text to the model with a classification prompt.

Model selection journey — and the mistakes:

I started with qwen36-tuned:latest, a large 35b model. Overkill for a task this simple. After the Code node fix brought time down to ~30 seconds, I switched to qgemma3:12b, the lightest model in my local library. Result: 6.8 seconds. A 10x improvement over the post-fix qwen36 time.

Lesson: Match model size to task complexity. A binary classifier that outputs one line does not need a 35b parameter model.

The prompt mistake: My first prompt was too open-ended — something like “analyze this page for certification discounts.” The model ignored the scraped content entirely and generated a long generic essay about how to find GCP discounts, sourced from its training data. Classic hallucination when the instruction doesn’t constrain the output format.

The fix: A strict, format-enforcing prompt:

You are a strict text classifier. Your ONLY job: check if the text below contains 
certification exam discounts, promo codes, or vouchers.

Reply with EXACTLY one of these two responses and nothing else:
- DISCOUNT FOUND: [copy the exact discount text]
- NO DISCOUNT FOUND

Do not explain. Do not add context. Do not use markdown. One line only.

---
{{ $json.text }}

The --- separator between instruction and content helps the model distinguish the two. After this fix, the output was clean and deterministic.

The System role mistake: I tried to add a separate System message in the Ollama node (following standard LLM best practices). The n8n Ollama node only supports User and Assistant roles — no System role. The workaround is to prepend the system instruction directly into the User message, which works just as well for local models.

Step 5 — Telegram Node

The field name mistake: I set the Text field to {{ $json.response }} — which seemed logical. But Ollama’s n8n node outputs the result under content, not response. The Telegram messages were showing “undefined” until I corrected it to {{ $json.content }}.

Lesson: Always check the actual output JSON of a node before wiring it to the next one. Click the Output tab, look at the field names, then write your expressions.

The wrong node mistake: I also tried to put a formatted Telegram message template (with date and source URL) inside the Ollama node’s Content field. $json.content resolved as [undefined] there because the Ollama node doesn’t have access to its own output yet — it’s still generating it. The template belongs in the Telegram node, which receives Ollama’s output as its input.

Lesson: Expressions like $json.fieldName reference the incoming data to that node, not the current node’s own output.


Making n8n Survive Reboots – NSSM

n8n was running in a PowerShell window. That means closing the window or rebooting the laptop kills the workflow. Not acceptable for a daily monitor.

The fix: NSSM (Non-Sucking Service Manager) — a Windows utility that wraps any executable as a proper Windows service that starts automatically on boot.

powershell

# NSSM was already installed
nssm install n8n
# GUI opens — fill in node.exe path, startup directory, and n8n arguments
nssm start n8n
nssm status n8n
# Returns: SERVICE_RUNNING

After this, the PowerShell window was closed and n8n continued running. The workflow will now fire at 9am even after a reboot.


Results

MetricBeforeAfter
Execution time5+ min (timeout)6.8 seconds
Telegram output“undefined” / hallucinated essayClean one-line result
Uptime dependencyPowerShell window openWindows service (always on)
Model usedqwen36-tuned:latest (35b)qgemma3:12b

What’s Next

This is v1.0. Planned improvements:

  • Multiple sources: Add HTTP Request nodes for the GEAR program page and Google Cloud Next announcements — same Ollama classifier, more coverage
  • Conditional alerting: Only send a Telegram message when DISCOUNT FOUND is detected — silence on clean days
  • n8n to Claude/Gemini memory sync

Key Takeaways

  1. Strip HTML before sending to a local model. Raw HTML will time out even fast models. A simple regex Code node is all you need.
  2. Match model size to task complexity. A 12b model outperforms a 35b model when the task is simple and speed matters.
  3. Constrain your prompts strictly. Vague instructions produce hallucinated essays. Format-enforcing prompts produce one clean line.
  4. Check field names in node output before writing expressions. $json.response vs $json.content — one works, one returns undefined.
  5. n8n expressions reference incoming data, not current node output. Wire your templates to the right node.
  6. Install n8n as a Windows service from day one. NSSM takes 5 minutes and saves you from broken workflows after every reboot.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.