<?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: Will</title>
    <description>The latest articles on DEV Community by Will (@willylam2222).</description>
    <link>https://dev.to/willylam2222</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%2F4010397%2F62ab87aa-e179-4927-b527-7d28b29be7a4.jpg</url>
      <title>DEV Community: Will</title>
      <link>https://dev.to/willylam2222</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/willylam2222"/>
    <language>en</language>
    <item>
      <title>I checked which tools AI search recommends across 9 SaaS categories — most brands are invisible</title>
      <dc:creator>Will</dc:creator>
      <pubDate>Wed, 15 Jul 2026 21:11:10 +0000</pubDate>
      <link>https://dev.to/willylam2222/i-checked-which-tools-ai-search-recommends-across-9-saas-categories-most-brands-are-invisible-d2n</link>
      <guid>https://dev.to/willylam2222/i-checked-which-tools-ai-search-recommends-across-9-saas-categories-most-brands-are-invisible-d2n</guid>
      <description>&lt;p&gt;When someone asks ChatGPT, Perplexity, or Gemini &lt;em&gt;"what's the best tool for X?"&lt;/em&gt;, they don't get ten blue links. They get &lt;strong&gt;one&lt;/strong&gt; answer — a short list of names. If your product isn't in it, that buyer never learns you exist. It's the new "not ranking on Google," except there's no page 2 to scroll to.&lt;/p&gt;

&lt;p&gt;I wanted to know how bad it actually is, so I ran the real buyer questions for &lt;strong&gt;9 SaaS categories&lt;/strong&gt; against AI search and tallied which brands actually get named.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did
&lt;/h2&gt;

&lt;p&gt;For each category I generated the questions a real buyer would ask ("best CRM for a small team", "top project management tool for agencies", and so on), sent them through AI search (Perplexity as the first engine), and counted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every brand named across all the answers,&lt;/li&gt;
&lt;li&gt;which brand was the &lt;strong&gt;single top pick&lt;/strong&gt;, and&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;source domains&lt;/strong&gt; the AI pulled its answer from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Categories: CRM, project management, email marketing, AI writing, help desk, website builders, password managers, video conferencing, and note-taking apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Most brands are simply invisible.&lt;/strong&gt; In nearly every category the same 3-4 incumbents soak up almost all the recommendations. If you are not one of them, you are not in the answer at all — not ranked low, &lt;em&gt;absent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A few small tools quietly win their niche.&lt;/strong&gt; In some categories a smaller name grabs the top-pick slot — and when you look at why, it is almost always because a handful of specific sources (a popular Reddit thread, one widely-cited listicle, their own comparison page) get pulled again and again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It is not fixed — it moves.&lt;/strong&gt; Re-run the same questions a week later and the ranking shifts. That means it is trackable, and it means it is influenceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The leaderboards
&lt;/h2&gt;

&lt;p&gt;I put the per-category rankings up here (who AI names most, plus the sources it reads from):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://willylam2222-bot.github.io/cited/leaderboards.html" rel="noopener noreferrer"&gt;https://willylam2222-bot.github.io/cited/leaderboards.html&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to sanity-check your own category, look it up — and if your product is not on the board, that gap is exactly the thing worth fixing.&lt;/p&gt;




&lt;p&gt;Curious whether folks here have checked where their product stands in AI answers. Drop your brand + category in the comments and I will run it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>seo</category>
      <category>saas</category>
      <category>marketing</category>
    </item>
    <item>
      <title>I scraped 30 DTC stores and built a free tool to check where your product price ranks</title>
      <dc:creator>Will</dc:creator>
      <pubDate>Sun, 12 Jul 2026 07:40:40 +0000</pubDate>
      <link>https://dev.to/willylam2222/i-scraped-30-dtc-stores-and-built-a-free-tool-to-check-where-your-product-price-ranks-3lfa</link>
      <guid>https://dev.to/willylam2222/i-scraped-30-dtc-stores-and-built-a-free-tool-to-check-where-your-product-price-ranks-3lfa</guid>
      <description>&lt;p&gt;DTC founders constantly ask "am I priced right?" — so I scraped ~30 real Shopify stores across 7 categories, pulled 10,000+ live prices, and turned it into a free tool: type in your price, see your percentile vs the actual market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://willylam2222-bot.github.io/priceprobe/price-rank-checker.html" rel="noopener noreferrer"&gt;https://willylam2222-bot.github.io/priceprobe/price-rank-checker.html&lt;/a&gt;&lt;br&gt;
(no signup, runs entirely in your browser)&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding that surprised me
&lt;/h2&gt;

&lt;p&gt;The share of products on sale &lt;em&gt;right now&lt;/em&gt; varies wildly by category:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🍵 Loose-leaf tea: &lt;strong&gt;1.8%&lt;/strong&gt; on sale&lt;/li&gt;
&lt;li&gt;☕ Specialty coffee: 5.8%&lt;/li&gt;
&lt;li&gt;💊 Supplements: &lt;strong&gt;22.4%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Supplements discount ~12x more often than tea — and cost 2.5x more. Prices there move constantly.&lt;/p&gt;

&lt;p&gt;Full data + methodology: &lt;a href="https://willylam2222-bot.github.io/priceprobe/dtc-pricing-index-2026.html" rel="noopener noreferrer"&gt;https://willylam2222-bot.github.io/priceprobe/dtc-pricing-index-2026.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built (all free, no backend)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Zero-dependency Python (stdlib &lt;code&gt;urllib&lt;/code&gt; only) hits each store's public &lt;code&gt;/products.json&lt;/code&gt; endpoint. Shopify serves the entire catalog + prices as JSON — no API key, no scraping library.&lt;/li&gt;
&lt;li&gt;The percentile math + the whole tool is client-side JS on GitHub Pages. Nothing stored, no server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code + the scraping write-up: &lt;a href="https://github.com/willylam2222-bot/priceprobe" rel="noopener noreferrer"&gt;https://github.com/willylam2222-bot/priceprobe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback — which categories should I add next?&lt;/p&gt;

</description>
      <category>data</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Every Shopify store exposes these 4 public URLs — here's what a competitor sees in 30 seconds</title>
      <dc:creator>Will</dc:creator>
      <pubDate>Tue, 07 Jul 2026 16:13:59 +0000</pubDate>
      <link>https://dev.to/willylam2222/every-shopify-store-exposes-these-4-public-urls-heres-what-a-competitor-sees-in-30-seconds-47go</link>
      <guid>https://dev.to/willylam2222/every-shopify-store-exposes-these-4-public-urls-heres-what-a-competitor-sees-in-30-seconds-47go</guid>
      <description>&lt;p&gt;Most Shopify merchants assume their catalog, pricing logic, and store structure are only visible through their storefront. They're not. Shopify serves several JSON and XML endpoints publicly &lt;em&gt;by design&lt;/em&gt; — the same data its own theme uses — and anyone can read them without a login, an API key, or a scraping tool.&lt;/p&gt;

&lt;p&gt;This isn't a hack or a vulnerability. It's public, intentional, and useful (it's how apps and themes work). But most store owners have never looked at what it reveals. Here are the four endpoints, what each one shows, and how to check your own store in 30 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;/products.json&lt;/code&gt; — your entire catalog and live prices
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://any-store.com/products.json?limit=250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns every product, every variant, every price, in clean JSON. Titles, options, inventory-ish signals, images, and the exact price a competitor would need to undercut you. Paginate with &lt;code&gt;?page=2&lt;/code&gt; and you get the whole catalog, however big.&lt;/p&gt;

&lt;p&gt;I covered the pricing angle in detail &lt;a href="https://dev.to/willylam2222/i-pulled-800-competitor-prices-from-4-shopify-stores-in-8-seconds-with-20-lines-of-python-no-2kfi"&gt;in a previous post&lt;/a&gt; — the short version: a competitor can snapshot your full price list in seconds and diff it daily.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;/collections.json&lt;/code&gt; — how you merchandise
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://any-store.com/collections.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every collection you've built: "Best Sellers," "Sale," "New Arrivals," seasonal drops. This is your merchandising strategy laid out — which products you group, what you're pushing, how you segment. A competitor learns what you &lt;em&gt;think&lt;/em&gt; sells, not just what you stock.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;/meta.json&lt;/code&gt; — your store's identity
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://any-store.com/meta.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns the store's name, ID, and location fields (city, province, country). Usually harmless — it's business info that's often on your contact page anyway — but worth knowing it's queryable programmatically, which is how tools bucket and geo-tag stores at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;/sitemap.xml&lt;/code&gt; — every URL, including things you forgot were live
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://any-store.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shopify auto-generates a sitemap of every product, collection, and page. It's meant for Google, but it's also a complete map of your store for anyone. Occasionally it surfaces pages a merchant assumed were "hidden" (unlinked but still published). If it's not truly unpublished, it's in here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own store right now
&lt;/h2&gt;

&lt;p&gt;Open each URL with your own domain. Whatever you see is exactly what every competitor, analyst, and bot can see too. That's the point — these power legitimate integrations — but you should know what's on the table.&lt;/p&gt;

&lt;p&gt;If you want to see it programmatically, this reads all four with no dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_create_unverified_context&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://onyxcoffeelab.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- put any Shopify store here
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/products.json?limit=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/collections.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/meta.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to actually do with this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you sell:&lt;/strong&gt; you can't wall these off (Shopify serves them), so the play isn't hiding — it's &lt;em&gt;watching&lt;/em&gt;. Since your competitors' data is equally open, monitor it. Know when a rival drops a price or sells out, because that's your window to react.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're technical:&lt;/strong&gt; the free, open-source version that snapshots and diffs a competitor over time is here — &lt;a href="https://github.com/willylam2222-bot/priceprobe" rel="noopener noreferrer"&gt;github.com/willylam2222-bot/priceprobe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you'd rather not touch code:&lt;/strong&gt; send me a few competitor URLs and I'll send back a clean report of their current prices, stock, and recent moves — &lt;a href="https://willyverse188.gumroad.com/l/kphhe" rel="noopener noreferrer"&gt;willyverse188.gumroad.com/l/kphhe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The data is already public on both sides. The only question is who's actually looking.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I pulled 800+ competitor prices from 4 Shopify stores in 8 seconds — with 20 lines of Python (no libraries)</title>
      <dc:creator>Will</dc:creator>
      <pubDate>Tue, 07 Jul 2026 14:57:39 +0000</pubDate>
      <link>https://dev.to/willylam2222/i-pulled-800-competitor-prices-from-4-shopify-stores-in-8-seconds-with-20-lines-of-python-no-2kfi</link>
      <guid>https://dev.to/willylam2222/i-pulled-800-competitor-prices-from-4-shopify-stores-in-8-seconds-with-20-lines-of-python-no-2kfi</guid>
      <description>&lt;p&gt;If a competitor drops a price on Tuesday and you notice on Friday, you've lost three days of sales. Most small store owners find out by accident. It turns out you don't need a $99/month tool or a scraping framework to fix that — almost every Shopify store hands you its entire catalog and live prices for free, if you know the one URL.&lt;/p&gt;

&lt;p&gt;I ran it across four real coffee roasters this morning. Here are the actual numbers, the 20-line script, and what it means if you sell anything online.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one URL nobody tells you about
&lt;/h2&gt;

&lt;p&gt;Every Shopify storefront exposes a public JSON feed of its products at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;any-shopify-store&amp;gt;/products.json?limit=250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key. No login. No headless browser. It's the same data the store's own theme uses to render pages — titles, variants, and &lt;strong&gt;live prices&lt;/strong&gt; — returned as clean JSON. Open one in your browser right now and you'll see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result (real data, pulled today)
&lt;/h2&gt;

&lt;p&gt;I pointed a tiny script at four well-known roasters and pulled every product + price in about 8 seconds total:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Products&lt;/th&gt;
&lt;th&gt;Priced bags&lt;/th&gt;
&lt;th&gt;Bag price range&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Onyx Coffee Lab&lt;/td&gt;
&lt;td&gt;248&lt;/td&gt;
&lt;td&gt;171&lt;/td&gt;
&lt;td&gt;$10–40&lt;/td&gt;
&lt;td&gt;$26.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Counter Culture&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;$10–39&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verve Coffee&lt;/td&gt;
&lt;td&gt;168&lt;/td&gt;
&lt;td&gt;460&lt;/td&gt;
&lt;td&gt;$10–40&lt;/td&gt;
&lt;td&gt;$26.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby Coffee&lt;/td&gt;
&lt;td&gt;213&lt;/td&gt;
&lt;td&gt;494&lt;/td&gt;
&lt;td&gt;$10–40&lt;/td&gt;
&lt;td&gt;$24.25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's &lt;strong&gt;798 products&lt;/strong&gt; and their live prices, for free, in less time than it took to read this paragraph. The median bag sits in a tight $24.25–$26.50 band across all four — which is itself the insight: in a mature niche your price anchor is set by the pack, and being 15% off it (either way) is a decision you want to make on purpose, not by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entire script
&lt;/h2&gt;

&lt;p&gt;Zero dependencies — Python standard library only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;

&lt;span class="n"&gt;STORES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Onyx Coffee Lab&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://onyxcoffeelab.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter Culture&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://counterculturecoffee.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Verve Coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.vervecoffee.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ruby Coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://rubycoffeeroasters.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c1"&gt;# 2nd context is a fallback for macOS machines missing root certs
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_create_unverified_context&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch failed: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/products.json?limit=250&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variants&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# single-bag band
&lt;/span&gt;                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STORES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bags  $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  median $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. Swap in your own competitors' domains and you have a competitor price monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it into an actual monitor
&lt;/h2&gt;

&lt;p&gt;The snippet above is a one-shot snapshot. To catch &lt;em&gt;changes&lt;/em&gt;, you save each run and diff against the last one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On each run, store &lt;code&gt;{product_title: price}&lt;/code&gt; to a small JSON file.&lt;/li&gt;
&lt;li&gt;Next run, compare: flag anything where the price moved, or a product that appeared/disappeared (a competitor selling out is your window to hold or raise price).&lt;/li&gt;
&lt;li&gt;Schedule it once a day with &lt;code&gt;cron&lt;/code&gt; (mac/Linux) or Task Scheduler (Windows).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Daily is plenty — be polite, one request per store.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only works on Shopify stores.&lt;/strong&gt; Roughly a quarter of the stores you compete with run Shopify; check by opening the &lt;code&gt;/products.json&lt;/code&gt; URL. Other platforms (VTEX, BigCommerce, WooCommerce) have their own public endpoints; marketplaces like Amazon actively block this and need a different approach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices only, not promotions.&lt;/strong&gt; Cart-level discounts won't show. The list price still tells you 90% of what you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be respectful.&lt;/strong&gt; This is public data the store already serves to every browser, but hammering it is rude and pointless. Once a day covers real pricing moves.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you don't want to touch a terminal
&lt;/h2&gt;

&lt;p&gt;I packaged this into a tiny open-source tool that does the save-and-diff for you: &lt;strong&gt;&lt;a href="https://github.com/willylam2222-bot/priceprobe" rel="noopener noreferrer"&gt;github.com/willylam2222-bot/priceprobe&lt;/a&gt;&lt;/strong&gt; — free, run it on your own laptop.&lt;/p&gt;

&lt;p&gt;And if you'd rather just get the answer without setting anything up, I'll run it for you: send me your competitors' URLs and I'll send back a clean report of their current prices, stock, and any recent moves. Details here: &lt;a href="https://willyverse188.gumroad.com/l/kphhe" rel="noopener noreferrer"&gt;willyverse188.gumroad.com/l/kphhe&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Either way — the URL at the top of this post is free and yours to use. Go look at what your competitors are charging right now.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I built a zero-dependency competitor price tracker in pure Python (no pip, no API keys)</title>
      <dc:creator>Will</dc:creator>
      <pubDate>Wed, 01 Jul 2026 06:26:27 +0000</pubDate>
      <link>https://dev.to/willylam2222/i-built-a-zero-dependency-competitor-price-tracker-in-pure-python-no-pip-no-api-keys-3bl7</link>
      <guid>https://dev.to/willylam2222/i-built-a-zero-dependency-competitor-price-tracker-in-pure-python-no-pip-no-api-keys-3bl7</guid>
      <description>&lt;p&gt;Most competitor price-monitoring tools charge &lt;strong&gt;$40–100/month&lt;/strong&gt; to watch a handful of URLs. I only wanted to track a few competitors — so I built a small tool I own and run myself. One Python file, no &lt;code&gt;pip install&lt;/code&gt;, no API keys, no account.&lt;/p&gt;

&lt;p&gt;It's open source (MIT): &lt;strong&gt;&lt;a href="https://github.com/willylam2222-bot/priceprobe" rel="noopener noreferrer"&gt;github.com/willylam2222-bot/priceprobe&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's how it works and the couple of things that turned out to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Point it at competitor product URLs&lt;/li&gt;
&lt;li&gt;Get &lt;strong&gt;price&lt;/strong&gt;, &lt;strong&gt;stock&lt;/strong&gt;, and &lt;strong&gt;what changed since last run&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — runs anywhere Python 3.8+ runs&lt;/li&gt;
&lt;li&gt;Own it, run it locally, no monthly bill&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Fetching (the boring part that bites you)
&lt;/h2&gt;

&lt;p&gt;Standard library only — &lt;code&gt;urllib&lt;/code&gt; with polite headers, gzip, and retries. The gotcha that cost me time: &lt;strong&gt;macOS Python.org builds ship without root certificates&lt;/strong&gt;, so &lt;code&gt;urlopen&lt;/code&gt; throws &lt;code&gt;CERTIFICATE_VERIFY_FAILED&lt;/code&gt; on plenty of machines. Rather than make users run "Install Certificates.command", I fall back to an unverified context &lt;em&gt;only&lt;/em&gt; after a verified attempt fails — we are reading public product pages, not sending secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;contexts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_default_context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_create_unverified_context&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contexts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SSLError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;  &lt;span class="c1"&gt;# non-TLS error: unverified will not help
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one fallback is the difference between "works on my machine" and "works on the buyer's machine".&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Extracting price / stock
&lt;/h2&gt;

&lt;p&gt;A regex that handles the currencies real stores use plus title and stock keywords:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PRICE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:S\$|US\$|£|€|\$|USD|SGD|GBP|EUR)\s?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OOS_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out[\s-]?of[\s-]?stock|sold[\s-]?out|currently unavailable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-target custom regex is supported for sites that hide the price somewhere weird.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Diffing against the last run
&lt;/h2&gt;

&lt;p&gt;Each run saves a JSON snapshot. The next run compares and flags &lt;strong&gt;▲ price up / ▼ price down (with %) / ✕ out-of-stock / ✓ restock&lt;/strong&gt; — normalising numbers so &lt;code&gt;£1,299.00&lt;/code&gt; and &lt;code&gt;£1.299,00&lt;/code&gt; compare correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A report you would actually send someone
&lt;/h2&gt;

&lt;p&gt;It renders a clean HTML report with changes highlighted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvhkqtc618hg03vicbde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvhkqtc618hg03vicbde.png" alt="report" width="800" height="978"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-dependency is underrated&lt;/strong&gt; for tools you want strangers to actually run. No env, no version hell.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;TLS-verify fallback&lt;/strong&gt; removed the #1 support question before it happened.&lt;/li&gt;
&lt;li&gt;Regex price extraction is "good enough" for a huge share of stores; JS-rendered prices are the long tail.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Free &amp;amp; open source: &lt;strong&gt;&lt;a href="https://github.com/willylam2222-bot/priceprobe" rel="noopener noreferrer"&gt;github.com/willylam2222-bot/priceprobe&lt;/a&gt;&lt;/strong&gt; — stars welcome ⭐&lt;/p&gt;

&lt;p&gt;Not into the terminal? There is a one-time &lt;strong&gt;$19&lt;/strong&gt; packaged version (double-click runner + step-by-step guide) for non-devs who just want the report: &lt;a href="https://willyverse188.gumroad.com/l/ptzxo" rel="noopener noreferrer"&gt;gumroad.com/l/ptzxo&lt;/a&gt;. Same engine — you pay for zero setup, not a subscription.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the extraction or diffing logic in the comments.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
