If you've ever bought a Udemy course, you've probably seen this: the same course costs one price on Monday and a fraction of it on Thursday. One course I watch was listed at $110 and sold for under $20 a few days later.
Udemy doesn't show price history. The "sale ends in 6 hours!" banner tells you nothing about whether the price will be even lower next week. So I started checking prices every morning and publishing what I saw: CourseLens.
This post covers how it's built for $0/month, and the two bugs that made it show wrong prices for weeks without me noticing.
What it does
- Every morning, it records the real price of each listed course (no login, no cookies, the price an anonymous visitor sees).
- Each course page shows a price chart and the periods when it was cheaper.
- A
/sale/page shows whether a sitewide sale is on right now. - Each course has an RSS feed that fires when the price drops.
The stack
GitHub Actions (daily cron)
└─ Python: fetch prices → append to JSON in the repo → commit
Cloudflare Pages (watches the repo)
└─ build.py: JSON → static HTML (no framework, no database)
-
No server, no database. Price history is a JSON file committed to the repo. Git is the database, and
git logis the audit trail. - One Python script builds every page. No framework. The build also runs the test suite; if a test fails, the build fails and the previous version stays live.
- Staging first. Every change goes to a staging branch, gets checked, then fast-forwards to main. (I learned this the hard way, see below.)
- Cost: GitHub Actions free tier + Cloudflare Pages free tier + a domain.
Bug 1: three weeks of prices in the wrong currency
CourseLens has a second regional edition that shows prices in a local currency. For three weeks, that edition showed prices about a hundred times too low.
What happened: I was traveling when I wrote the fetcher, and the network I was on happened to get local-currency prices back. I assumed that was the default and didn't ask for a currency. GitHub Actions runners sit in US data centers. They got dollar amounts, and my code put the local currency symbol in front of them. A $15 course suddenly looked like it cost pocket change.
Lesson: never rely on an implicit default that depends on where the code runs. Set the region and currency explicitly, for every market, and add a test that fails if the currency comes back wrong.
Bug 2: US prices stuck for weeks because of time zones
To save requests, I only refreshed the US edition's prices when the other regional edition detected that a sale had started or ended. It seemed reasonable: Udemy sales are sitewide, so one market should tell you about the other.
They aren't in sync. Sales start and end on local time boundaries, so two markets can be a day apart. When the other edition saw "no change", the US edition never refreshed, and some of its pages showed prices up to 85% below the real one.
Lesson: don't make one market's data depend on another market's events. The fix was boring: refresh every US course every day, unconditionally. It costs a few extra requests and removes a whole class of bugs.
What I'd tell anyone building a price tracker
- Store every observation, not just the latest price. You can't recompute history later.
- Treat "unchanged" as data. A day with no change is still an observation.
- Make the build your safety net. Tests that run inside the deploy are worth more than tests you remember to run.
- Write down every incident. Neither bug above was caught by an automated check. I now keep a "lessons" file that I read before every change.
Try it
- Is there a sale right now? → https://courselens.net/en/sale/
- Course rankings with price charts → https://courselens.net/en/
If you watch a specific course, subscribe to its price-drop RSS feed from its page.
Disclosure: course links on CourseLens are Udemy affiliate links. Prices and scores are not affected by that.
Top comments (0)