<?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: Subhashree Ayyappan</title>
    <description>The latest articles on DEV Community by Subhashree Ayyappan (@subhashreeayyappan).</description>
    <link>https://dev.to/subhashreeayyappan</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3965431%2Fe091d47c-34b3-4e3e-abb2-e16bffe053f4.png</url>
      <title>DEV Community: Subhashree Ayyappan</title>
      <link>https://dev.to/subhashreeayyappan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/subhashreeayyappan"/>
    <language>en</language>
    <item>
      <title>I Built a Live UK Electricity Price Dashboard. Here's What Went Wrong (and Right)</title>
      <dc:creator>Subhashree Ayyappan</dc:creator>
      <pubDate>Tue, 02 Jun 2026 23:16:15 +0000</pubDate>
      <link>https://dev.to/subhashreeayyappan/i-built-a-live-uk-electricity-price-dashboard-heres-what-went-wrong-and-right-19l2</link>
      <guid>https://dev.to/subhashreeayyappan/i-built-a-live-uk-electricity-price-dashboard-heres-what-went-wrong-and-right-19l2</guid>
      <description>

&lt;p&gt;I'm on Agile Octopus, where electricity prices change every 30 minutes. Every evening I'd open the Octopus app trying to figure out when to run the dishwasher. The app shows you the rates, but not "the cheapest 2-hour window tonight", which is the thing you actually need to know when you're standing in the kitchen at 8pm.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://ukelectricityprices.co.uk" rel="noopener noreferrer"&gt;ukelectricityprices.co.uk&lt;/a&gt;. A dashboard that shows live half-hourly prices, finds the cheapest slots for appliances, and compares tariffs across all 14 UK regions. Next.js 16, TypeScript, Tailwind, deployed on Vercel. No database. It pulls live from the Octopus API on each request.&lt;/p&gt;

&lt;p&gt;This is the story of building it, including the bugs that cost me the most time.&lt;/p&gt;

&lt;h2&gt;
  
  
  "The API is free? Seriously?"
&lt;/h2&gt;

&lt;p&gt;The Octopus Energy API is public, well-documented, and doesn't need an API key for tariff data. You just hit &lt;code&gt;https://api.octopus.energy/v1/products/&lt;/code&gt; and you're off. I had half-hourly Agile rates rendering in a table within an hour of starting.&lt;/p&gt;

&lt;p&gt;Then two weeks later, the dashboard silently broke. No errors. Just empty data.&lt;/p&gt;

&lt;p&gt;Turns out Octopus periodically releases new Agile product versions. The product code I'd hardcoded (&lt;code&gt;AGILE-FLEX-22-11-25&lt;/code&gt;) was no longer active. The API didn't return an error. It just returned nothing.&lt;/p&gt;

&lt;p&gt;The fix: query the products endpoint first, find the currently active Agile product, then use that code for the rates call. One extra API call, but I'll never have to manually update a config again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson: if an API has versioned product codes, don't hardcode them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bug That Only Appeared at 12:40am
&lt;/h2&gt;

&lt;p&gt;Everything worked perfectly for weeks. Then one night I noticed the dashboard was showing yesterday's prices at 00:40.&lt;/p&gt;

&lt;p&gt;The Octopus API returns timestamps in UTC. "Today" on the dashboard means today in London time. Most of the year that's fine. But during British Summer Time, 00:40 BST is actually 23:40 UTC the previous day. My code was asking the API for "today in UTC" and getting yesterday's London prices.&lt;/p&gt;

&lt;p&gt;I burned a full evening on this. The fix uses &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; to figure out London's UTC offset dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// sv-SE locale reliably gives YYYY-MM-DD format&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;londonToday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleDateString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sv-SE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/London&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Check what hour London shows at noon UTC&lt;/span&gt;
&lt;span class="c1"&gt;// (noon is safe, never hits a DST boundary)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noonUTC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dateStr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;T12:00:00Z`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;londonHour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-GB&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Europe/London&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hour12&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;noonUTC&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offsetHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;londonHour&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 1 in BST, 0 in GMT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sv-SE&lt;/code&gt; locale trick for getting ISO date strings looks weird but works everywhere. Found it on Stack Overflow after three other approaches all had edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Cheapest Window
&lt;/h2&gt;

&lt;p&gt;The feature people actually use most is "when's the cheapest time to run my washing machine?" A washing machine cycle is about 2 hours, so I need the cheapest 4 consecutive half-hour slots.&lt;/p&gt;

&lt;p&gt;It's a sliding window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findBestWindowN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgileSlot&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bestAvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bestAvg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bestAvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;bestStart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bestStart&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;validFrom&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;formatTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bestStart&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;validTo&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;avgPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bestAvg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also shows the most expensive window so you can see what you'd pay if you ran the dishwasher at 5pm like a normal person. The difference is usually 3-5x.&lt;/p&gt;

&lt;h2&gt;
  
  
  14 Regions, Wildly Different Prices
&lt;/h2&gt;

&lt;p&gt;I didn't realise UK electricity regions varied so much until I built the comparison page. Sometimes the cheapest region is half the price of the most expensive at the same time of day. The region code is just a letter (A through P, skipping I and O). I handle it through URL search params rather than cookies, so when someone shares a link the region comes with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SEO Mistake That Wasted Weeks
&lt;/h2&gt;

&lt;p&gt;This one still annoys me. I set a canonical URL in the root layout pointing to the homepage. Every page on the site (&lt;code&gt;/deals&lt;/code&gt;, &lt;code&gt;/compare&lt;/code&gt;, &lt;code&gt;/tips/...&lt;/code&gt;) was telling Google "the real version of this page is the homepage." Google treated all 28 pages as duplicates and only indexed one.&lt;/p&gt;

&lt;p&gt;I spent weeks wondering why Search Console showed "Alternate page with proper canonical tag" on everything. The fix was two lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;metadataBase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ukelectricityprices.co.uk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="nx"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each page gets its own canonical. Indexing started improving within days. I could have saved a month of SEO progress if I'd gotten this right from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Costs to Run
&lt;/h2&gt;

&lt;p&gt;Nothing. Vercel free tier. Octopus API is free. Carbon Intensity API is free. The domain is about ten quid a year. That's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache API responses from day one.&lt;/strong&gt; I added Redis caching late. A 5-minute cache cuts response times and is polite to their servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hardcode anything a provider might change.&lt;/strong&gt; Product codes, tariff names, rate structures. Fetch them dynamically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test during BST.&lt;/strong&gt; Timezone bugs only show up when you're not looking for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get canonical URLs right immediately.&lt;/strong&gt; Self-inflicted SEO wound that took weeks to notice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ukelectricityprices.co.uk" rel="noopener noreferrer"&gt;ukelectricityprices.co.uk&lt;/a&gt;. Free, no sign-up. If you're on Agile Octopus, the appliance timer and EV charging optimiser are worth a look.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>api</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
