Three streams shipped this week:
- 100 document parsers with one command
- 100 finance validators with one command
- 100 monitoring pages — and this one needs a Worker with cron, KV, and webhooks
Let me show you how the monitoring one works.
What it does
Watch monitors two things daily:
- Currency rates — NBU official rates (44 currencies) + ECB via Frankfurter
- EU VAT rates — 27 member states, standard and reduced rates
When a value changes beyond the threshold, it fires a webhook + email alert. No dashboards to check.
Live landing: https://boring-saas-infra.pages.dev/watch
Example page: https://boring-saas-infra.pages.dev/watch/vat-watch-germany
Why monitoring is harder than validation
The first two streams were stateless: request → response → forget.
Monitoring is stateful:
- A cron must run daily even when nobody is using the API
- We need to remember yesterday's value to detect changes
- We need to store subscriptions (who watches what)
- We need to deliver alerts (webhook + email)
That's exactly what Cloudflare Workers + KV + Cron Triggers give you.
The architecture
One Worker, one KV namespace, one cron job.
watch-engine.boring-saas-infra.workers.dev
├── GET /currency/nbu ← live NBU rates
├── GET /currency/frankfurter ← live ECB rates
├── GET /vat/:country ← VAT snapshot for one country
├── GET /watch/list ← list all watches
├── POST /watch/create ← create a watch
├── DELETE /watch/:id ← delete a watch
└── GET /cron/run ← manual cron trigger (for testing)
Cron: 0 6 * * * ← runs every day at 06:00 UTC
The wrangler.toml
name = "watch-engine"
main = "src/index.js"
compatibility_date = "2026-10-08"
[[kv_namespaces]]
binding = "WATCH_KV"
id = "021f939116ae416bb35703daa1e2c74d"
preview_id = "021f939116ae416bb35703daa1e2c74d"
[triggers]
crons = ["0 6 * * *"]
That's the entire infrastructure. No database, no message queue, no VPS.
The data sources
For currency — three of them:
// NBU — official Ukrainian rates
async function fetchNBU() {
const r = await fetch("https://bank.gov.ua/NBUStatService/v1/statdirectory/exchange?json");
const data = await r.json();
const rates = {};
for (const row of data) rates[row.cc] = { rate: row.rate, name: row.txt };
return { source: "NBU", base: "UAH", rates };
}
// Frankfurter — free ECB wrapper
async function fetchFrankfurter(base = "USD") {
const r = await fetch(`https://api.frankfurter.app/latest?from=${base}`);
const data = await r.json();
return { source: "Frankfurter", base: data.base, rates: data.rates };
}
Both are free, no API key, stable JSON.
For VAT — a snapshot map of 27 countries:
const VAT_SNAPSHOT = {
updated: "2026-10-08",
countries: {
DE: { standard: 19, reduced: [7] },
FR: { standard: 20, reduced: [2.1, 5.5, 10] },
IT: { standard: 22, reduced: [4, 5, 10] },
// ... 24 more
}
};
The cron compares the current snapshot with yesterday's stored value. If a rate changed, it fires an alert. VAT rates change rarely (a few times per year per country), so this is cheap and effective.
Change detection
The whole point of a monitoring system is to not alert on every run. Only when something actually changes.
async function runCron(env) {
const alerts = [];
// Currency: compare with previous snapshot
const nbu = await fetchNBU();
const prev = await getSnapshot(env, "currency:NBU");
if (prev) {
for (const [cc, cur] of Object.entries(nbu.rates)) {
const old = prev.rates[cc];
if (old && Math.abs(old.rate - cur.rate) / old.rate > 0.005) {
alerts.push({
type: "CURRENCY_CHANGE",
currency: cc,
from: old.rate,
to: cur.rate,
change_pct: ((cur.rate - old.rate) / old.rate * 100).toFixed(2),
});
}
}
}
await saveSnapshot(env, "currency:NBU", nbu);
// VAT: only fires when the snapshot's `updated` date changes
// ...
return { alerts };
}
Threshold for currency: 0.5%. Anything smaller is noise.
The webhook alert format
{
"type": "VAT_CHANGE",
"country": "DE",
"from": "19%",
"to": "20%",
"effective": "2026-01-01",
"source": "EU Commission TEDB",
"ts": "2026-10-08T14:21:33.181Z"
}
POSTed to whatever URL you configured. Works with:
- Slack incoming webhooks
- Discord webhooks
- n8n / Zapier / Make.com
- Your own backend
The 100 SEO pages
Same pattern as the previous two streams.
products-watch.json (excerpt):
[
{"slug":"currency-watch-uah-usd","name":"UAH/USD Currency Watch","type":"currency","category":"Currency","pair":"UAH/USD","source":"NBU"},
{"slug":"vat-watch-germany","name":"German VAT Watch","type":"vat","category":"VAT","country":"DE","country_name":"Germany"},
{"slug":"vat-watch-eu-standard","name":"EU Standard VAT Watch","type":"vat","category":"VAT","country":null,"country_name":"EU"}
]
100 entries:
- 50 currency pairs — UAH pairs (25) + ECB/Frankfurter pairs (25)
- 50 VAT watches — 27 countries + 23 EU-wide categories
generator-watch.cjs reads the JSON, renders templates/watch-detail.html, writes 100 folders, updates sitemap.xml and llms.txt.
node generator-watch.cjs
→
Generated 100 pages in output/watch/
sitemap.xml updated — 628 URLs total
llms.txt updated — 100 monitors
Each page has a live demo hitting /currency/nbu or /vat/:country — so visitors see real data, not a mock.
Pricing
- Free — 1 active watch, daily check, email alerts
- Pro ($29.99/mo) — 100 watches, webhooks, priority checks
Different price from the previous streams because monitoring is recurring value — customers stay subscribed.
Stack
- Cloudflare Workers (API + cron)
- Cloudflare KV (state)
- Cloudflare Pages (static pages)
- Node.js generators (no deps)
- Suby (payments)
- IndexNow (search ping)
Total monthly cost so far: $0.
What's next
- Tariff Watch — US ITC / EU TARIC / UK Trade Tariff changes
- Sanctions Watch — OFAC / EU / UK OFSI lists
- HS Code Watch — WCO Harmonized System updates
Same architecture, just more data sources.
Try it
- Landing: https://boring-saas-infra.pages.dev/watch
- Example page: https://boring-saas-infra.pages.dev/watch/vat-watch-germany
- Live NBU rates: https://watch-engine.boring-saas-infra.workers.dev/currency/nbu
- Live VAT for Germany: https://watch-engine.boring-saas-infra.workers.dev/vat/DE
If you're building KYC, tax, or finance tooling and need alerts on rate/rate changes — the free tier is genuinely free.
Happy to answer questions about Cloudflare Workers + KV + cron, or the programmatic SEO side.
Top comments (2)
The VAT path has a useful distinction to make explicit: as shown here, it's watching changes to a bundled snapshot, rather than discovering changes at the source. What refreshes VAT_SNAPSHOT, and how would a subscriber know that refresh stopped?
I'd add two fixtures: bump
updatedwithout changing any rates (no VAT_CHANGE), then change one country's rate without bumpingupdated(detect the value change or reject the inconsistent snapshot). Keeping source-checked time separate from deployment/edit time would let a successful daily cron report stale source data rather than imply a fresh check. I haven't run the Worker; this is based on the snapshot and date-gated comparison described in the post.This is incredibly solid architectural feedback. Handling stale source data is one of those classic edge cases that completely breaks currency monitors if left unchecked. Separating the source-checked timestamp from the internal deployment time is a textbook best practice. Definitely worth adding those test fixtures to ensure the Cloudflare Worker fails gracefully instead of reporting stale data as fresh.