<?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: Sifat Ahmed</title>
    <description>The latest articles on DEV Community by Sifat Ahmed (@sifat_ahmed_3cc666203e7b8).</description>
    <link>https://dev.to/sifat_ahmed_3cc666203e7b8</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%2F4090356%2F4ae60a96-0c30-4bc9-82f3-5b62d886acec.png</url>
      <title>DEV Community: Sifat Ahmed</title>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sifat_ahmed_3cc666203e7b8"/>
    <language>en</language>
    <item>
      <title>You Don't Need a 12-Flag FFmpeg Script (or a Video API) to Generate Video From Code</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:36:21 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/you-dont-need-a-12-flag-ffmpeg-script-or-a-video-api-to-generate-video-from-code-710</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/you-dont-need-a-12-flag-ffmpeg-script-or-a-video-api-to-generate-video-from-code-710</guid>
      <description>&lt;p&gt;If you've ever tried to generate video programmatically, you've probably ended up in one of two places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Place one: FFmpeg scripting hell.&lt;/strong&gt; You're hand-building a 12-flag command line, fighting codec mismatches, re-learning filter graph syntax every six months, and building your own queue/retry infrastructure around a binary that was never designed to be your rendering backend. It works, until it doesn't, and when it doesn't the error messages are unreadable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Place two: a commercial video-generation API.&lt;/strong&gt; Shotstack, FastPix, and similar services genuinely solve the rendering problem, but you pay for it in a different currency - usage-based pricing that scales with your output, and a template/API contract you don't control. Fine for some products, a real constraint for others.&lt;/p&gt;

&lt;p&gt;There's a third option that's less talked about: treat the video as a normal webpage with a clock, and let a browser do the rendering.&lt;/p&gt;

&lt;p&gt;That's the model behind &lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;HyperFrames&lt;/a&gt;, an open-source framework from HeyGen. A composition is HTML, CSS, and GSAP - a single paused timeline that gets seeked to exact timestamps instead of played in real time. A render pipeline (headless Chrome plus FFmpeg under the hood, but you never touch either directly) captures each timestamp as a frame and strings them into an MP4.&lt;/p&gt;

&lt;p&gt;What that looks like in practice - no timeline editor, just markup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"clip"&lt;/span&gt; &lt;span class="na"&gt;data-duration=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Q3 Revenue Up 34%&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;data-duration&lt;/code&gt; attribute is read by the render pipeline to know how long to hold each clip when seeking; GSAP animates whatever you want inside it, and because the timeline is paused and seeked rather than played, the render is deterministic - the same input always produces the same frames, no race conditions, no dropped frames from a slow machine.&lt;/p&gt;

&lt;p&gt;This doesn't replace Remotion or Motion Canvas if you're already happy with React-based or TypeScript-based compositions - it's a different lane, closer to plain web dev than to a video framework. Where it fits is exactly the gap this post opened with: you want exportable video, you know HTML/CSS/JS, and you'd rather not learn FFmpeg's filter graph syntax or hand a vendor your rendering pipeline.&lt;/p&gt;

&lt;p&gt;Disclosure: I wrote a short paid guide covering six full animation recipes built on HyperFrames (title cards, stat counters, crossfades, and so on) plus the "five pitfalls" that eat the most debugging time when you're starting out - &lt;a href="https://sidheart.gumroad.com/l/lyudd?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=ffmpeg-gap-post" rel="noopener noreferrer"&gt;Animate With Code&lt;/a&gt;. The framework itself is free and open-source either way.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Export a GSAP Animation to MP4 (No After Effects, No Screen Recording)</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:19:18 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/how-to-export-a-gsap-animation-to-mp4-no-after-effects-no-screen-recording-g47</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/how-to-export-a-gsap-animation-to-mp4-no-after-effects-no-screen-recording-g47</guid>
      <description>&lt;p&gt;If you've searched "export gsap animation to mp4" or "gsap timeline to video," you've probably landed on a GSAP forum thread from years ago that never got a real answer. Here's the actual answer, with working code.&lt;/p&gt;

&lt;p&gt;The short version: GSAP animates the DOM. A DOM doesn't export to video on its own - you need something to seek the animation to exact timestamps and screenshot each one. That's the whole trick. No After Effects, no screen recording, no proprietary timeline tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Instead of playing your GSAP timeline in real time, you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the timeline paused, not playing&lt;/li&gt;
&lt;li&gt;Seek it to a specific time (e.g. tl.seek(1.2))&lt;/li&gt;
&lt;li&gt;Screenshot the page at that exact moment&lt;/li&gt;
&lt;li&gt;Repeat for every frame (e.g. 30 times per second)&lt;/li&gt;
&lt;li&gt;Stitch the frames into an MP4 with ffmpeg&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because step 2 is deterministic - the same seek time always produces the same visual state - step 3 can happen in a headless browser, completely decoupled from real-time playback. That's what makes this exportable at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working example: a counting stat animation
&lt;/h2&gt;

&lt;p&gt;Here's a real, complete composition - a number counting up, the kind of stat card you see in product demos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0b0f14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;#root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1920px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1080px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;#stat&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;220px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4fd1c5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;#label&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#8b98a9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text-transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;uppercase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"stat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Customers Served&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statEl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stat&lt;/span&gt;&lt;span class="dl"&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;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gsap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;power1.inOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;statEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&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;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&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="c1"&gt;// This is what makes it exportable: a paused, seekable timeline&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Capturing the frames
&lt;/h2&gt;

&lt;p&gt;Once you have a seekable timeline like &lt;code&gt;window.tl&lt;/code&gt; above, exporting is mechanical: open the page in a headless browser (Puppeteer/Playwright both work), then for every frame number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&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;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seconds&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalFrames&lt;/span&gt; &lt;span class="o"&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;duration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fps&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;totalFrames&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;t&lt;/span&gt; &lt;span class="o"&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;fps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`frame_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&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="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.png`&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;Then stitch the PNG sequence into an MP4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-framerate&lt;/span&gt; 30 &lt;span class="nt"&gt;-i&lt;/span&gt; frame_%04d.png &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuv420p out.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole pipeline. The output is a real MP4, frame-accurate to the GSAP timeline, with zero screen-recording artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five ways this silently breaks
&lt;/h2&gt;

&lt;p&gt;None of these throw an error. You just get a blank render or a frozen frame and have to guess why, which is the actual reason this question keeps showing up unanswered in forums.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CSS transitions racing the seek.&lt;/strong&gt; If a property is animated by a plain CSS &lt;code&gt;transition&lt;/code&gt; instead of by GSAP, seeking the timeline doesn't touch it - it just keeps drifting toward its own target on its own schedule. The frame you capture can be mid-transition even though your timeline thinks it's settled. Fix: drive every timed property through GSAP, and set &lt;code&gt;transition: none&lt;/code&gt; on anything time-driven.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web fonts loading async.&lt;/strong&gt; If a frame gets captured before a &lt;code&gt;@font-face&lt;/code&gt; finishes loading, you get a silent fallback-font frame with no error thrown. Gate render start on &lt;code&gt;document.fonts.ready&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anything animating outside GSAP's ticker.&lt;/strong&gt; Some third-party widgets run their own &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop internally. It won't respect your paused timeline's seek - it keeps advancing on its own and drifts out of sync with the frame you think you're capturing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-deterministic loops.&lt;/strong&gt; &lt;code&gt;repeat: -1&lt;/code&gt; never finishes, so a fixed-length export has no natural end. Compute a finite count instead: &lt;code&gt;Math.max(0, Math.floor(duration / cycleDuration) - 1)&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-deterministic ordering.&lt;/strong&gt; &lt;code&gt;Object.keys()&lt;/code&gt; or &lt;code&gt;Map&lt;/code&gt; iteration order on data built at runtime, feeding into dynamically generated positions - fine on screen, but can render slightly differently frame to frame if anything touches randomness or unordered iteration. Fix everything at composition-author time, not render time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The seek-and-screenshot loop above is the whole mechanism, but hand-rolling it for anything more than a single stat card gets tedious fast: multiple timed elements, audio tracks, scene transitions, sub-compositions. HyperFrames (HeyGen's open-source framework, Apache-2.0) wraps exactly this pipeline into a CLI - &lt;code&gt;npx hyperframes init&lt;/code&gt;, author the composition, &lt;code&gt;npx hyperframes render&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want the mental model written out in more depth, plus five more working recipes (title cards, logo intros, scene crossfades, CTA morphs, Ken Burns drift) and the full pitfalls list, I wrote it up here: &lt;a href="https://sidheart.gumroad.com/l/lyudd" rel="noopener noreferrer"&gt;https://sidheart.gumroad.com/l/lyudd&lt;/a&gt; (pay what you want, starting at $1).&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the render pipeline or any of the pitfalls above in the comments.&lt;/p&gt;

</description>
      <category>gsap</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Claude Desktop Won't Open on Windows? Here's the Real Cause (and the Fix That Actually Sticks)</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Fri, 18 Sep 2026 06:37:34 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/claude-desktop-wont-open-on-windows-heres-the-real-cause-and-the-fix-that-actually-sticks-1d2i</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/claude-desktop-wont-open-on-windows-heres-the-real-cause-and-the-fix-that-actually-sticks-1d2i</guid>
      <description>&lt;p&gt;If Claude Desktop on Windows just started throwing "This app can't open" out of nowhere, and reinstalling either doesn't fix it or fixes it for exactly one session before it breaks again - you're not imagining it, and it's not your machine. It's a real, documented Anthropic bug, and the standard "just reinstall" advice actively makes the underlying problem worse if you're using Claude Code, because it can silently destroy your session history in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual root cause
&lt;/h2&gt;

&lt;p&gt;Recent Claude Desktop builds on Windows ship as MSIX packages (Microsoft Store-style installers), and they bundle &lt;code&gt;vk_swiftshader.dll&lt;/code&gt;. On any machine with Memory Integrity (HVCI) turned on - which is on by default on a lot of newer Windows installs - Windows Code Integrity blocks that DLL outright (you'll see event 3033 in the Event Viewer if you check). The GPU process crashes, and the whole MSIX package gets flagged &lt;code&gt;Modified, NeedsRemediation&lt;/code&gt; by Windows' own package system. The next launch fails with "This app can't open."&lt;/p&gt;

&lt;p&gt;Repairing or reinstalling only resets the clock - the same package self-corrupts again within a session, because the underlying trigger (HVCI blocking that DLL) is still there. It's been independently reproduced on NVIDIA, AMD, Intel-integrated, and Windows-on-ARM machines, and it's tracked in Anthropic's own bug tracker: &lt;a href="https://github.com/anthropics/claude-code/issues/88323" rel="noopener noreferrer"&gt;anthropics/claude-code#88323&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just reinstall" is actually risky if you use Claude Code
&lt;/h2&gt;

&lt;p&gt;Reinstalling can silently destroy Claude Code's chat history while everything else (regular Chat, project files, memory) survives intact - a separate, confirmed bug: &lt;a href="https://github.com/anthropics/claude-code/issues/62997" rel="noopener noreferrer"&gt;anthropics/claude-code#62997&lt;/a&gt;. Worse, the lost sessions are often unrecoverable from the UI even though the transcript files are still sitting on disk, so you frequently can't even tell what you lost without checking the filesystem directly: &lt;a href="https://github.com/anthropics/claude-code/issues/81907" rel="noopener noreferrer"&gt;anthropics/claude-code#81907&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that actually sticks
&lt;/h2&gt;

&lt;p&gt;The permanent fix isn't repairing the MSIX build - it's switching to the older, non-MSIX Squirrel build, which has no package-integrity status for Code Integrity to flag in the first place. Short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the current version from &lt;code&gt;https://downloads.claude.ai/releases/win32/x64/RELEASES&lt;/code&gt;, then build the download URL yourself (&lt;code&gt;AnthropicClaude-&amp;lt;version&amp;gt;-full.nupkg&lt;/code&gt;) - the version number changes, so a hardcoded URL 404s.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.nupkg&lt;/code&gt; is a zip file wearing a different extension. Rename it to &lt;code&gt;.zip&lt;/code&gt; and extract normally.&lt;/li&gt;
&lt;li&gt;Copy everything inside &lt;code&gt;lib\net45\&lt;/code&gt; (not the folder itself) into &lt;code&gt;%LOCALAPPDATA%\AnthropicClaude\app-&amp;lt;version&amp;gt;\&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make a Desktop shortcut to &lt;code&gt;claude.exe&lt;/code&gt; in that new folder.&lt;/li&gt;
&lt;li&gt;Fully quit the old MSIX build first (tray icon → Quit, or Task Manager) - Electron's single-instance lock means a still-running old build silently hijacks the new one's launch.&lt;/li&gt;
&lt;li&gt;Uninstall the old MSIX package for real once the new one's confirmed working, so Windows doesn't keep quietly auto-updating it back over your fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Full step-by-step with every gotcha I actually hit (nupkg extraction, the single-instance-lock trap, when it's safe to delete the download folder) is in the repo linked below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you touch any of this, check if you're actually at risk
&lt;/h2&gt;

&lt;p&gt;Not everyone on HVCI-enabled Windows is broken yet - some machines just haven't hit the trigger. I built a free, zero-setup diagnostic script called &lt;a href="https://github.com/sifatahmedsrk52-cyber/claudeguard" rel="noopener noreferrer"&gt;ClaudeGuard&lt;/a&gt; after this happened to me for real: it checks your package status, Code Integrity event log, and Memory Integrity state (read-only, changes nothing), and separately backs up your &lt;code&gt;~/.claude&lt;/code&gt; data with a manifest so if you do reinstall, you have a real answer to "did I lose anything" instead of guessing. Single PowerShell script, no dependencies, no account, no cost.&lt;/p&gt;

&lt;p&gt;If you're already comfortable with git/bash/jq and want comprehensive cross-machine backup of Claude Code's entire data model, &lt;a href="https://github.com/jtklinger/claude-code-backup-guide" rel="noopener noreferrer"&gt;jtklinger/claude-code-backup-guide&lt;/a&gt; does that well and is actively maintained - ClaudeGuard is narrower on purpose, built specifically for the Windows MSIX corruption itself, not as a general backup workflow.&lt;/p&gt;

</description>
      <category>windows</category>
      <category>claude</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Is Your OpenAI API Bill So High? The Real Causes (and What Actually Stops It)</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Fri, 18 Sep 2026 05:53:08 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/why-is-your-openai-api-bill-so-high-the-real-causes-and-what-actually-stops-it-3bdp</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/why-is-your-openai-api-bill-so-high-the-real-causes-and-what-actually-stops-it-3bdp</guid>
      <description>&lt;p&gt;"Why is my OpenAI bill so high" is one of the most-searched questions about the API, and most answers stop at "use a cheaper model." That's true but incomplete. Here's the actual list of causes, roughly in order of how much money each one has cost real people, plus what genuinely stops each one (not just warns you about it).&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Retry loops without backoff
&lt;/h2&gt;

&lt;p&gt;The single most common cause. A request fails (rate limit, timeout, transient error), your code retries immediately, fails again, retries again - in a tight loop, that's hundreds of billed calls in minutes. Fix: exponential backoff with a hard retry ceiling (3-5 attempts, not infinite), and a circuit breaker that stops the whole loop after N consecutive failures rather than retrying forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A leaked or shared key
&lt;/h2&gt;

&lt;p&gt;A key committed to a public repo, embedded in client-side code, or shared across a team with no per-key limit gets used by more than just you - sometimes within hours of being indexed by a scraper. Fix: never ship a key to the client, rotate immediately if a repo goes public with one in history, and if multiple people/services need access, give each its own key so a leak is isolated and traceable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Defaulting to the expensive model for everything
&lt;/h2&gt;

&lt;p&gt;Classification, extraction, routing, and simple formatting tasks almost never need the flagship model - a smaller/cheaper model handles them at a fraction of the cost with no meaningful quality loss. Fix: route by task complexity, not habit; reserve the expensive model for the calls that actually need its reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Resending the full conversation history every call
&lt;/h2&gt;

&lt;p&gt;If your app passes the entire chat transcript as input on every turn, a 50-message conversation means message 50 pays for messages 1-49 all over again, every time. Fix: persist durable facts and pass a short summary plus relevant recent context instead of the whole history, and turn on prompt caching if your provider supports it - the repeated prefix (system prompt, tool definitions) drops to a fraction of normal input price.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Streaming and agent loops with no stop condition
&lt;/h2&gt;

&lt;p&gt;An agentic loop that calls itself, evaluates, and calls again with no cap on iterations can burn through a budget in minutes if it gets stuck re-trying a task it can't complete. This is the fastest way to turn a $12 side project into a $300 bill overnight. Fix: hard iteration ceilings on any agent loop, and - this is the one most people skip - a check against actual spend so far, not just call count, since calls vary wildly in cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting that looks like a cap but isn't
&lt;/h2&gt;

&lt;p&gt;OpenAI's dashboard "usage limit" is a notification threshold, not a hard stop. Cross it and you get an email; your requests keep going through. If you genuinely cannot afford to go past a number, that setting alone will not save you - you need something that actively checks spend before or during requests, not an email after the fact.&lt;/p&gt;

&lt;p&gt;The real fix, for anyone building this themselves: don't do "read balance, compare, then decrement" as two separate steps - that has a race condition where two concurrent calls can both read "under budget" before either commits. Make the reservation itself atomic (a single compare-and-swap or a real ledger hold against one authoritative counter), so a second call's decrement simply fails once the first has already taken the balance below the threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you just want the email, not the engineering
&lt;/h2&gt;

&lt;p&gt;Building the atomic-reservation version yourself is the right answer if you're already running agents in production at real volume. If you're a solo developer who just wants to know before a number gets crossed, that's a smaller problem, and it's the one I built &lt;a href="https://fusebox.sifatsrk.workers.dev" rel="noopener noreferrer"&gt;Fusebox&lt;/a&gt; for: set a monthly ceiling, connect a read-only usage key (scoped so it can only ever see spend, never touch billing or make requests), get emailed at 50/80/100%. Free tier alerts once you've already crossed 100%; paid tier ($7/mo) adds the earlier 50/80% warnings that actually give you time to react before the bill is final. It's not a replacement for the atomic-reservation pattern above if you're at scale - it's the "email me" version for everyone else.&lt;/p&gt;

&lt;p&gt;Either way, the actual lesson underneath all five causes above is the same one: OpenAI's per-token pricing means cost tracks behavior, not intent. Code that's &lt;em&gt;correct&lt;/em&gt; can still be &lt;em&gt;expensive&lt;/em&gt; if nothing is watching what it actually spends while it runs.&lt;/p&gt;

</description>
      <category>openai</category>
      <category>api</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>A counting-stat animation in HyperFrames, full working code (free recipe)</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Sun, 06 Sep 2026 05:33:01 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/a-counting-stat-animation-in-hyperframes-full-working-code-free-recipe-3on4</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/a-counting-stat-animation-in-hyperframes-full-working-code-free-recipe-3on4</guid>
      <description>&lt;p&gt;Following up on the guide I posted about a couple days ago (HyperFrames — renders video from plain HTML/CSS/GSAP instead of a timeline editor). Here's one full recipe from it, in full, free — no teaser cut-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Turning a plain number into a small performance reads as far more "designed" than just displaying a static figure — genuinely effective for anything results/data-driven: a metric, a price, a countdown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"clip"&lt;/span&gt; &lt;span class="na"&gt;data-start=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;data-duration=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt; &lt;span class="na"&gt;data-track-index=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"stat"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;0&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;customers served&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&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="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;power1.inOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onUpdate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&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;counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&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="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why it's built this way
&lt;/h2&gt;

&lt;p&gt;Tweening a plain JS object (not the DOM element itself) and writing the formatted number into the element on every tick is the reliable way to do this in a render pipeline that has to seek to arbitrary timestamps and always get the same pixels. It stays perfectly deterministic — the exact displayed number at any timestamp is a pure function of that timestamp — and it avoids animating a text property GSAP doesn't natively tween.&lt;/p&gt;

&lt;p&gt;This is one of six recipes in the full guide (title reveal, logo intro, scene crossfade, CTA morph, Ken Burns background drift are the other five), plus a "five pitfalls" section on the mistakes that produce a blank render or frozen frame instead of a helpful error message. If the rest is useful: &lt;a href="https://sidheart.gumroad.com/l/lyudd" rel="noopener noreferrer"&gt;https://sidheart.gumroad.com/l/lyudd&lt;/a&gt; ($19).&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the pattern itself here for free either way.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Made a paid guide from an open-source Claude skill (without reselling it)</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Sun, 06 Sep 2026 05:10:26 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/made-a-paid-guide-from-an-open-source-claude-skill-without-reselling-it-5b3n</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/made-a-paid-guide-from-an-open-source-claude-skill-without-reselling-it-5b3n</guid>
      <description>&lt;p&gt;Quick build-in-public post, same vein as my earlier n8n automation service posts.&lt;/p&gt;

&lt;p&gt;I wanted to package something adjacent to that work: a guide to HyperFrames, an open-source framework (from HeyGen) that renders video from plain HTML/CSS/GSAP instead of a timeline editor. There's a Claude Code skill that teaches an AI agent this framework - genuinely useful, but not something I built, so selling a copy of it would've been selling someone else's open-source work as my own.&lt;/p&gt;

&lt;p&gt;What I did instead: wrote an original guide teaching the technique - the actual mental model (why the render has to be deterministic and seekable, and what that means for how you write animation code), plus six complete working recipes (title reveals, counting stats, crossfades, that kind of thing) with real code, checked against the framework's actual contract rather than simplified examples that fall apart the moment you try to render them for real. Same rule paid Docker/React courses run on: the tool itself is free, the technique you package around it is legitimately yours to sell.&lt;/p&gt;

&lt;p&gt;$19 on Gumroad if it's useful to anyone building video-from-code pipelines: &lt;a href="https://sidheart.gumroad.com/l/lyudd" rel="noopener noreferrer"&gt;https://sidheart.gumroad.com/l/lyudd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer anything about HyperFrames or the deterministic-rendering mental model if useful - not trying to oversell it, just sharing what I built.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>opensource</category>
      <category>buildinpublic</category>
      <category>video</category>
    </item>
    <item>
      <title>OpenAI Shipped a Hard Spend Limit - Here's the Gap It Still Leaves Open</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Fri, 04 Sep 2026 04:32:03 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/openai-still-wont-let-you-set-a-hard-spend-limit-heres-what-actually-works-19d2</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/openai-still-wont-let-you-set-a-hard-spend-limit-heres-what-actually-works-19d2</guid>
      <description>&lt;p&gt;For most of 2026, this was true: OpenAI's budget field was alert-only. Cross your number, get an email, keep getting billed. That changed on July 23, when OpenAI rolled out real hard spend limits to every API account, free, no enterprise tier required. Set a cap at the org or project level, and once tracked spend reaches it, further requests fail with a 429 until the next billing cycle.&lt;/p&gt;

&lt;p&gt;That's a genuine fix for the worst version of this problem - a leaked key or a runaway retry loop can no longer bill you into the ground forever. If you haven't turned it on, it's worth doing today: project settings, Limits, Edit spend limit, done in two minutes.&lt;/p&gt;

&lt;p&gt;But a few weeks of watching this land tells me it doesn't close the gap most solo devs actually have. Two things stayed true even after the feature shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's a kill switch, not a warning
&lt;/h2&gt;

&lt;p&gt;OpenAI's own guidance is to only use hard limits on dev/CI keys, not production - because the cutoff is account-wide and immediate. It doesn't stop the one customer or workflow that's spending; it stops everything behind that key. For a side project that's fine. For anything with real users hitting it, flipping that switch on means a runaway loop doesn't just cost you money anymore, it takes your app down completely, with no warning first.&lt;/p&gt;

&lt;p&gt;So the same developers this feature was built for mostly end up leaving it off in production, for the same reason they left the old alert-only budget too high to matter: the downside of it firing is worse than the upside of it existing. That's not a flaw in OpenAI's implementation - a blunt, request-blocking cap has to work that way. It just means "OpenAI now has a hard limit" and "I can safely turn it on" are two different sentences.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cap tells you after, not before
&lt;/h2&gt;

&lt;p&gt;Even set up correctly, a hard limit is silent until it fires. You don't get a "hey, you're at 80%, might want to look" - you get a working app, then a 429. The dashboard shows you the number if you go check it, but nothing pushes a signal to you before the wall.&lt;/p&gt;

&lt;p&gt;That's the part I built Fusebox around originally, and it's still true regardless of whether the hard limit exists: &lt;a href="https://fusebox.sifatsrk.workers.dev?ref=devto" rel="noopener noreferrer"&gt;Fusebox&lt;/a&gt; watches your usage against a ceiling you set and emails you at 50%, 80%, and 100% of it, using a read-only Admin key scoped to &lt;code&gt;api.usage.read&lt;/code&gt; - it can see your numbers, it can't spend or touch your account. Free tier emails you once you're over. Paid ($7/mo) adds the earlier warnings, which is the part that actually lets you catch a loop while it's still cheap to fix.&lt;/p&gt;

&lt;p&gt;The honest way to put it: OpenAI's hard limit is the backstop you hope you never need. Fusebox is the thing that tells you early enough that you never do.&lt;/p&gt;

&lt;p&gt;If you've turned on OpenAI's hard limit in production, I'd like to know - did you actually leave it on, or did the account-wide blast radius talk you out of it? That's the real open question this feature raised for me.&lt;/p&gt;

</description>
      <category>openai</category>
      <category>api</category>
      <category>ai</category>
      <category>saas</category>
    </item>
    <item>
      <title>Bot protection and unsubscribe: the two features I built before Fusebox had a single user</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:02:07 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/bot-protection-and-unsubscribe-the-two-features-i-built-before-fusebox-had-a-single-user-eja</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/bot-protection-and-unsubscribe-the-two-features-i-built-before-fusebox-had-a-single-user-eja</guid>
      <description>&lt;p&gt;Fusebox emails you before your OpenAI bill blows past a number you set. Before writing that pitch anywhere, I built two things nobody asks for in a demo: a bot check on signup, and a real unsubscribe link. Neither was optional, and neither was hard - but skipping them is exactly the kind of thing a solo project quietly does when there's no user yet to complain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bot protection on day one
&lt;/h2&gt;

&lt;p&gt;The signup form asks for an OpenAI Admin key. That's the one part of Fusebox that can't be undone if it goes wrong - a scraped or brute-forced signup endpoint means someone else's key sitting encrypted in your database, or a flood of junk rows with no real owner. reCAPTCHA was the obvious default, but it ships a tracking script and a visible "I'm not a robot" checkbox that doesn't fit a tool whose whole pitch is "we ask for less than you'd expect." Cloudflare Turnstile does the same job - a managed challenge that mostly resolves invisibly - without the tracking baggage, and it's free.&lt;/p&gt;

&lt;p&gt;The actual wiring is small: a widget on the client that calls back with a token once it resolves, a submit button that stays disabled until that token exists, and a server-side check against Cloudflare's own siteverify endpoint before the signup handler does anything else:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyTurnstile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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://challenges.cloudflare.com/turnstile/v0/siteverify&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&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;If that check fails, the request never reaches the part that touches an API key or the database. One rule worth calling out: on any submit attempt - success or failure - the client resets the token and re-disables the button. A stale token from a previous attempt is exactly the kind of thing that gets missed until someone abuses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the unsubscribe link isn't a preference toggle
&lt;/h2&gt;

&lt;p&gt;The simplest version of "let people leave" is a settings page with a checkbox. Fusebox doesn't have an account system at all - just an email, an encrypted key, and a ceiling - so there's no login to put a checkbox behind. The unsubscribe link that goes out with every threshold email carries a random token generated once at signup and stored next to that subscriber's row. Clicking it is a single GET that flips one column:&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;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`UPDATE subscribers SET active = 0 WHERE unsubscribe_token = ? AND active = 1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AND active = 1 matters more than it looks. Without it, a link that's clicked twice - or forwarded, or hit by an email client's link-prefetcher - just re-runs the same update harmlessly. With it, the second click tells the truth: "already used," not a fake success. Small difference, but it's the kind of thing that's invisible right up until someone checks it, and getting it wrong is exactly the kind of bug that erodes trust in a tool whose entire pitch is trustworthiness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why write this down
&lt;/h2&gt;

&lt;p&gt;Neither of these features is interesting on its own, and neither shows up in a screenshot or a pitch. They're the parts of a "trust us with your API key" tool that only matter when something goes wrong - a bot, a forwarded email, a link clicked twice - and by then it's too late to have skipped them. Building them before the first real signup, instead of after the first incident, was the point.&lt;/p&gt;

&lt;p&gt;Fusebox is free, early access, live now: &lt;a href="https://fusebox.sifatsrk.workers.dev" rel="noopener noreferrer"&gt;https://fusebox.sifatsrk.workers.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've built something similar and handled either of these differently, I'd like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>cloudflare</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I'm running a done-for-you automation service on self-hosted n8n — looking for a first pilot client</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Sun, 23 Aug 2026 11:08:03 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/im-running-a-done-for-you-automation-service-on-self-hosted-n8n-looking-for-a-first-pilot-client-4c1n</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/im-running-a-done-for-you-automation-service-on-self-hosted-n8n-looking-for-a-first-pilot-client-4c1n</guid>
      <description>&lt;p&gt;Quick build-in-public post. I've been setting up a small service: I build and run n8n automations for people — the repetitive stuff that eats an hour a day (copying data between tools, replying to the same kind of message, chasing follow-ups) — self-hosted on my own infrastructure, so you never touch the software or pay for a seat. You just get the automation working.&lt;/p&gt;

&lt;p&gt;Stack is self-hosted n8n + Postgres, monitored (I run my own uptime checks on it so I know before you do if something's down), fronted through Cloudflare. Zero infra cost on my end at this stage, so pricing stays honest rather than padded to cover a cloud bill.&lt;/p&gt;

&lt;p&gt;I'm looking for a first real pilot — something small and concrete (a workflow you're doing by hand right now that shouldn't need to be). I'll build it free or heavily discounted in exchange for honest feedback and, if it goes well, a testimonial. Not trying to oversell this: it's early, I have zero client track record yet, and I'd rather say that upfront than pretend otherwise.&lt;/p&gt;

&lt;p&gt;If you've got something repetitive eating your time, or you know someone who does, I'd genuinely like to hear about it — reply here or DM.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Fusebox doesn't support Claude yet</title>
      <dc:creator>Sifat Ahmed</dc:creator>
      <pubDate>Sun, 23 Aug 2026 02:50:06 +0000</pubDate>
      <link>https://dev.to/sifat_ahmed_3cc666203e7b8/why-fusebox-doesnt-support-claude-yet-196f</link>
      <guid>https://dev.to/sifat_ahmed_3cc666203e7b8/why-fusebox-doesnt-support-claude-yet-196f</guid>
      <description>&lt;p&gt;Fusebox emails you before your OpenAI bill blows past a number you set. Before writing any of that code, I spent an afternoon checking one thing: what kind of API key would I actually need to ask people for?&lt;/p&gt;

&lt;p&gt;That question mattered more than it sounds like it should. A tool that reads your spend needs some credential from your account. The honest version of that credential can &lt;em&gt;only&lt;/em&gt; read - it can't touch billing, can't create or revoke other keys, can't add or remove people from your org. The dishonest version asks for more than it needs and hopes you don't check.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI: genuinely scoped
&lt;/h2&gt;

&lt;p&gt;OpenAI lets you create an Admin key and restrict it to specific permissions per resource - None, Read, or Write. For usage and cost data specifically, that's a scope called &lt;code&gt;api.usage.read&lt;/code&gt;. Grant only that, and the key can pull your spend numbers and do nothing else. That's exactly the shape of trust a tool like this should ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anthropic: not yet, for individual accounts
&lt;/h2&gt;

&lt;p&gt;I checked the same thing for Claude, expecting a similar scoped option. Anthropic's own docs say otherwise, for Claude Console (the product individual developers actually use):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Claude Console keys do not have selectable scopes; every key carries full access to all endpoints that accept Admin API keys."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scoped read-only keys do exist on Anthropic's side (&lt;code&gt;read:spend_limits&lt;/code&gt;, &lt;code&gt;read:analytics&lt;/code&gt;) but only for &lt;strong&gt;Claude Enterprise&lt;/strong&gt; organizations, not individual Console accounts. For everyone else, the only key that can read usage data can also manage your organization's members and revoke other people's API keys.&lt;/p&gt;

&lt;p&gt;Asking a solo developer to hand that over, in service of a feature as small as "email me at 80% of my budget," is a bad trade. So Fusebox doesn't do it yet. Claude support is built and waiting - it's a small amount of code - but it stays off until Anthropic ships the same kind of scoping for individual accounts that OpenAI already has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth writing down
&lt;/h2&gt;

&lt;p&gt;Most tools that ask for API access don't explain what they're actually asking for, and most users don't check. I'd rather the constraint be visible than quietly worked around.&lt;/p&gt;

&lt;p&gt;Fusebox is free, early access, OpenAI-only for now: &lt;a href="https://fusebox.sifatsrk.workers.dev" rel="noopener noreferrer"&gt;https://fusebox.sifatsrk.workers.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've found a cleaner way to do the Claude side of this safely, I'd genuinely like to hear about it in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>security</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
