<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: courselens</title>
    <description>The latest articles on DEV Community by courselens (@courselens).</description>
    <link>https://dev.to/courselens</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4212560%2Fa759b68c-c727-44bf-a118-6aad37a336a9.png</url>
      <title>DEV Community: courselens</title>
      <link>https://dev.to/courselens</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/courselens"/>
    <language>en</language>
    <item>
      <title>Udemy prices change daily — so I built a free price tracker</title>
      <dc:creator>courselens</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:47:37 +0000</pubDate>
      <link>https://dev.to/courselens/udemy-prices-change-daily-so-i-built-a-free-price-tracker-379m</link>
      <guid>https://dev.to/courselens/udemy-prices-change-daily-so-i-built-a-free-price-tracker-379m</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;&lt;a href="https://courselens.net/en/" rel="noopener noreferrer"&gt;CourseLens&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every morning, it records the real price of each listed course (no login, no cookies, the price an anonymous visitor sees).&lt;/li&gt;
&lt;li&gt;Each course page shows a price chart and the periods when it was cheaper.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;/sale/&lt;/code&gt; page shows whether a sitewide sale is on right now.&lt;/li&gt;
&lt;li&gt;Each course has an RSS feed that fires when the price drops.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No server, no database.&lt;/strong&gt; Price history is a JSON file committed to the repo. Git is the database, and &lt;code&gt;git log&lt;/code&gt; is the audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One Python script builds every page.&lt;/strong&gt; No framework. The build also runs the test suite; if a test fails, the build fails and the previous version stays live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging first.&lt;/strong&gt; Every change goes to a staging branch, gets checked, then fast-forwards to main. (I learned this the hard way, see below.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; GitHub Actions free tier + Cloudflare Pages free tier + a domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bug 1: three weeks of prices in the wrong currency
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: US prices stuck for weeks because of time zones
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building a price tracker
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Store every observation, not just the latest price.&lt;/strong&gt; You can't recompute history later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "unchanged" as data.&lt;/strong&gt; A day with no change is still an observation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the build your safety net.&lt;/strong&gt; Tests that run inside the deploy are worth more than tests you remember to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write down every incident.&lt;/strong&gt; Neither bug above was caught by an automated check. I now keep a "lessons" file that I read before every change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Is there a sale right now? → &lt;a href="https://courselens.net/en/sale/" rel="noopener noreferrer"&gt;https://courselens.net/en/sale/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Course rankings with price charts → &lt;a href="https://courselens.net/en/" rel="noopener noreferrer"&gt;https://courselens.net/en/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you watch a specific course, subscribe to its price-drop RSS feed from its page.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: course links on CourseLens are Udemy affiliate links. Prices and scores are not affected by that.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>githubactions</category>
      <category>cloudflare</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
