This is a submission for the Hacktoberfest Open-Source AI Challenge: Slow and Steady.
"In most boroughs, you can use your main residence for tourist rentals from June 10 to September 10 of each year." That's Montreal's short-term rental page, as of this weekend. Toronto's page says its 40-day registration revocation process is now a 10-day process. British Columbia's says hosts and platforms must register and pay annual fees. Three sets of rules, three pages, and not one of them will send you a note when a date, a fee or a deadline moves. You find out when the rule already applies to you.
So I built lazywatch. Once a day it checks the pages and, most days, does nothing at all. That's the point. When a page does change, a Gemma 3 4B running on my own laptop reads only the paragraphs that changed and hands me one sentence and one quote. The quote has to match the page word for word, or the sentence gets thrown out.
Here's what the rest of this post covers: the 68 page checks it made over one weekend, the 5 times the model woke up, the moment I killed the worker mid-run to see what Temporal would do, the one summary that passed every check and was still wrong, and the one the string match threw out.
| Watches | 8 official pages: short-term rental rules for Montreal, Toronto and British Columbia, Airbnb's hosting page for Canada, the Canada Gazette, federal news releases, travel advisories, and one weather feed as the control |
| Gemma 3 4B | Runs through Ollama on the laptop's GPU, with embeddinggemma for search. No API, no account, no bill |
| Trust rule | Gemma writes the sentence. A string match decides if it stays |
| Temporal plumbing | A workflow with one activity per page, 5 retries with doubling backoff |
| Browserbase | A real browser for pages that refuse a plain fetch (190 paragraphs where urllib got nothing) |
| Tiger Cloud memory | Postgres with pgvector; keyword and meaning search fused by rank |
| Result | 68 checks, 5 wakes, 4 of 5 quotes verified, 1 thrown out by the string match, 0 pages read by me |
What I Built
Ever tried to keep up with the price of gas? It's one number on the sign in the morning and another on the way home, and nobody warned you. Short-term rental rules are the same game with a slower clock: the sign is a government web page, it moves a few times a year instead of a few times a day, and missing the change costs a lot more than a tank. So you check the page. Open the tab, scroll, squint, close the tab. Nothing new. Again. And again. Until the one week you skip, which is naturally the week the fee schedule changes.
lazywatch is a page watcher built around one idea: do as little as possible. Once a day it fetches each page and compares the text to yesterday's copy. If nothing changed, that's it. Done. No model, no tokens, no reading. If something did change, Gemma gets the changed paragraphs, nothing else, and writes one sentence plus a quote. Once a week I read one short digest. That's the whole job.
Slow and steady, on purpose. One fetch per page per day. A model that sleeps unless there's something to read. And a daily run that's a Temporal workflow, so a flaky page or a napping model doesn't take the rest of the run down with it.
Demo
Eight pages watched from a Windows laptop, Friday evening to Sunday evening: the short-term rental pages for Montreal, Toronto and British Columbia, Airbnb's responsible hosting page for Canada, the Canada Gazette Part I index, the Government of Canada news page, the federal travel advisories, and one ringer. Environment Canada's weather feed for Montreal changes every hour, so if the model never woke up for THAT, I'd know the watcher was broken rather than the rules being quiet.
Saturday night's check, exactly as it printed (British Columbia had been added that evening, hence the first snapshot):
Canada Gazette Part I, 2026 index: unchanged (111 paragraphs), model not called
British Columbia short-term rental rules (province): first snapshot (20 paragraphs, plain)
Montreal short-term tourist rentals: unchanged (103 paragraphs), model not called
Toronto short-term rental operators: unchanged (315 paragraphs), model not called
Government of Canada news releases: unchanged (190 paragraphs), model not called
Airbnb responsible hosting in Canada: unchanged (79 paragraphs), model not called
Environment Canada weather for Montreal (Atom feed): changed, verified
Government of Canada travel advisories: unchanged (232 paragraphs), model not called
Six pages unchanged, one brand-new snapshot, and the model never ran for any of them. Then the ringer did its job. Between 6 pm and 8 pm the temperature went from 12.2°C to 9.7°C (welcome to late October in Montreal), the pressure flipped from falling to rising, and the humidity jumped from 43% to 61%. Gemma got those two paragraphs and the two they replaced, nothing else, and the digest came back with:
# Weekly rules digest
52 page checks in 7 days, model woken 1 times.
## Environment Canada weather for Montreal (Atom feed) (2026-10-25)
https://weather.gc.ca/rss/weather/45.529_-73.562_e.xml
The current weather conditions for Montreal have been updated with more recent
data, including temperature, pressure, and wind speed.
> Current Conditions: Mainly Clear, 9.7°C
The quote is in the new text word for word. Verified. One sentence, one receipt, and I never opened the page.
Sunday noon brought two more wakes. The Government of Canada news page, the one only a real browser can read, had a new release at the top and an old one dropped off the bottom. Gemma wrote "The rules page was updated with a new news release announcing a visit by Ghana's Foreign Minister to Canada" and quoted the 38-word opening sentence of the release exactly. Not a rule, but a change, caught and quoted. The weather feed had been reissued at 11:30 am, ten paragraphs in and ten out, and the quote checked out: "Sunny. High 8. Forecast issued 11:30 AM EDT Sunday 25 October 2026". The sentence that came with it is the one I'll pick apart later.
Sunday at 5 pm brought the last two, and the best lesson of the weekend. The Government of Canada page "changed" because it had started serving a survey pop-up; Gemma's sentence said exactly that, the quote verified, and I ignored it in two seconds without opening the page. The weather feed changed again (17.0°C, a warm end to the day), and this time the model's quote was not on the page. The string match threw the sentence out and the digest showed the raw diff, marked NOT verified. Both get their own paragraphs below.
The line I care about most is the effort line at the top of the digest: 68 page checks in 7 days, model woken 5 times. Every other check cost one HTTP request and a text diff. The rules pages stayed quiet all weekend, which is the best possible result for a tool whose whole job is to leave me alone.
Code
lazywatch on GitHub, MIT. About 300 lines of standard-library Python for the watcher itself. Temporal, Browserbase and Tiger Data are one small file each, and the tool keeps working if any of the three is missing. Twenty tests run with no model, no account and no network.
How I Built It
The open piece first. Gemma 3 4B runs through Ollama on the laptop's GPU, and embeddinggemma builds the search index. No API, no account, no bill. The only thing leaving the machine is the page fetch itself.
Laziness is the design, not a side effect. The model runs only when the paragraph diff isn't empty. This weekend that was 5 times out of 68, and each call saw only the changed paragraphs (four on Saturday, twenty for each forecast reissue, eight for a survey banner), never the whole page. Gemma has no idea what the rest of the Toronto page says, and it doesn't need to.
Gemma writes the sentence. A string match decides if it stays.
The model has to return a quote, and the summary is accepted only if that quote appears verbatim in the new text. Otherwise the digest shows the raw diff, marked NOT verified, so a made-up sentence never reaches me dressed as a fact. Five summaries this weekend. Four passed. The fifth came back with the quote "Currently: Mainly Sunny, 17.0°C" when the page says "Current Conditions: Mainly Sunny, 17.0°C". Close, but not the page's words, so the string match threw the sentence out and the digest showed the raw diff instead, marked NOT verified. That's the rule doing its job. One of the four that passed was still a misreading of real text, and it gets its own section below.
12 seconds in, I killed the worker
The daily check is a Temporal workflow with one activity per page and a retry policy of 5 attempts, 10 seconds doubling. I did two mean things to it on purpose and kept the workflow histories.
First, a dead page (example.invalid). Five attempts over 2 minutes 31 seconds, then "failed after 5 attempts", and the workflow finished with every other page intact.
Then the real test. Twelve seconds into a run, I killed the worker process. Attempt 1 of the page it was fetching died with it. Forty seconds later, attempt 2 started on the replacement worker and finished in 10 seconds. The pages that had already finished were not fetched again, the workflow completed with every result, and the history shows every step.
That's the difference between a cron job and a workflow: one of them can tell you what happened at 3 am.
Pages that refuse a plain fetch
Some pages hollow out or flat-out refuse a plain download. The Government of Canada news page closed the connection on every plain fetch. Through a Browserbase session with Playwright over CDP, it reads 190 paragraphs. A page can be marked "render": true, or "render": "auto", which tries the plain download first, switches to the browser only if the page refuses, and remembers the choice so later checks compare like with like.
A memory that understands "colder"
Every check and every change lands in a Tiger Cloud Postgres service. Changes get an embedding from embeddinggemma, and search fuses full-text rank with pgvector cosine rank using reciprocal rank fusion. The point is to ask "has the licence fee changed before?" months from now and get both the exact words and the paraphrase. With five changes stored, search "pressure rising" put Saturday's weather change first (its text says "103.4 kPa rising"), and search "is it getting colder outside", which shares not one word with any stored text, still ranked the three weather changes above the two news items, purely on meaning:
2026-10-25 Environment Canada weather for Montreal: The current weather conditions for
Montreal have been updated with more recent data, including temperature, pressure, and
wind speed. (verified)
> Current Conditions: Mainly Clear, 9.7°C
2026-10-25 Environment Canada weather for Montreal: The weather forecast for Montreal
has been updated with a new hourly forecast ... (unverified)
> Currently: Mainly Sunny, 17.0°C
2026-10-25 Environment Canada weather for Montreal: The weather forecast for Montreal
has been updated with a new 7-day forecast ... (verified)
> Sunny. High 8. Forecast issued 11:30 AM EDT Sunday 25 October 2026
2026-10-25 Government of Canada news releases: The rules page was updated with a new
news release announcing a visit by Ghana's Foreign Minister to Canada. (verified)
> The Honourable Anita Anand, Minister of Foreign Affairs, will welcome ...
2026-10-25 Government of Canada news releases: The public rules page now includes a
call to action to complete a website survey ... (verified)
> You are one of the few visitors who have been selected to share your feedback.
What It Can't Do
Sunday's forecast summary read: "The weather forecast for Montreal has been updated with a new 7-day forecast, replacing the previous 2-day forecast." The quote was real, the check passed, and the sentence is wrong. The forecast was reissued, not extended. A 4B model misread what it saw. The quote check catches invented text, not a wrong reading of real text. So read the quote, not just the sentence. The quote is the receipt.
And a watcher is only as good as what it's looking at. I added peek, a command that prints what each snapshot actually holds, and it showed me two embarrassing things. One URL I'd added was a 404 page, and the watcher had happily snapshotted the error page and would have reported "unchanged" forever. And Ontario's e-Laws listing shows a "needs JavaScript" notice to a plain fetch, and an app error even in the real browser. Both pages got replaced. A 4B model can't tell you that your watcher is staring at a wall. You have to look.
The Sunday 5 pm run added the third limit: noise. The Government of Canada page "changed" because it started showing a survey invitation, seven paragraphs of it. Gemma described it accurately ("now includes a call to action to complete a website survey"), the quote verified, and none of it mattered. The watcher can't tell news from noise. The one-sentence summary means I can, in the time it takes to read it.
Point It at Anything
Nothing in lazywatch knows what a short-term rental is. It knows pages, paragraphs and quotes. Swap the page list and it follows building code bulletins, a city council's bylaw page, a school board's policy page, park and trail alerts, transit service changes, a product recall list, a grant program's eligibility rules, a festival lineup or the registration page for your kid's hockey league. Same loop every time: fetch, diff, wake the model only if there's something to read, quote it, keep the receipt. (The NHL rulebook is next on my list. Don't tell the refs.)
Where it could go from here: a digest a whole community shares instead of one person's inbox, a change feed where every item carries its quote as proof, and alerts within the hour for the pages that matter most. The bones are there. The model sleeps. The receipts pile up.
Why Does Open Innovation Matter?
Because I can read every line of this thing, run the model with no account and no bill, and see exactly what reaches it: the changed paragraphs, nothing more. Not the whole page, not my notes, not a browsing history. When these pages decide whether someone can rent their place out next summer, I'd rather not hand that to a black box with a monthly invoice.
And when it broke, it broke in a text file I could read. The fix took an afternoon, not a support ticket.
The Lazy Payoff
Sixty-eight checks, five wakes, four verified quotes, one thrown out, zero bylaws read by me, and the rules pages stayed quiet all weekend, which, if you ask me, is exactly what a tool like this should report most weeks. The weather feed proved the model wakes when it should. The rules pages proved it stays asleep when it should. Slow and steady, and the weekend was mine.
Which page do you wish someone watched for you?
Prize Categories
Best Use of Gemma: Gemma 3 4B runs on the laptop through Ollama and only ever sees changed paragraphs. Five summaries over the weekend, 4 of 5 quotes verified word for word, 1 paraphrased quote caught and thrown out, 63 checks where it never ran.
Best Use of Temporal: one activity per page, 5 retries with doubling backoff. A dead page failed cleanly after 2 min 31 s while the rest finished, and a worker killed 12 s into a run was replaced, with the page finishing on attempt 2 and nothing fetched twice.
Best Use of Browserbase: the Government of Canada news page refuses every plain fetch; through Browserbase it reads 190 paragraphs, and "auto" mode switches to the browser only when a page refuses and remembers the choice.
Best Use of Tiger Data: every check and change lives in Tiger Cloud; search fuses full-text and pgvector ranks, so "is it getting colder outside", sharing no words with any stored text, still ranked the weather changes first.


Top comments (0)