<?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>Forem: Sleeyax</title>
    <description>The latest articles on Forem by Sleeyax (@sleeyax).</description>
    <link>https://forem.com/sleeyax</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%2F2073280%2F960c9c9b-67bf-49a7-89d3-16096830c49a.png</url>
      <title>Forem: Sleeyax</title>
      <link>https://forem.com/sleeyax</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sleeyax"/>
    <language>en</language>
    <item>
      <title>AI is changing what I build</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Thu, 02 Apr 2026 19:29:21 +0000</pubDate>
      <link>https://forem.com/sleeyax/ai-is-changing-what-i-build-2j6h</link>
      <guid>https://forem.com/sleeyax/ai-is-changing-what-i-build-2j6h</guid>
      <description>&lt;p&gt;&lt;em&gt;How AI is changing not just how fast I build, but what I build, and why that matters for everyone who’s ever had a small idea.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;AI coding tools don’t just make developers faster. They lower the bar for which ideas are worth building. That means more niche, personal, and practical software gets made. Here’s how that shift looks in practice, and why it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;The biggest change AI has brought to how I build software is not that it makes me faster at writing code.&lt;/p&gt;

&lt;p&gt;It changes which ideas are worth building at all.&lt;/p&gt;

&lt;p&gt;I’m a professional software developer, so turning an idea into code was never the hard part. The real constraint was whether I cared enough to spend the time. Countless small ideas never made it past that filter. Not because they were bad, but because they weren’t important enough to justify the setup cost, implementation work, and mental overhead of yet another side project.&lt;/p&gt;

&lt;p&gt;That’s what feels different now.&lt;/p&gt;

&lt;p&gt;There’s a whole category of apps I used to mentally discard: useful, narrow, personal tools that solve one annoying problem, but not enough to earn a weekend of focused work. Those ideas died in the gap between “this would be nice to have” and “I’m actually willing to build it.”&lt;/p&gt;

&lt;p&gt;With AI coding tools, that gap is much smaller, and shrinking.&lt;/p&gt;

&lt;p&gt;Now, I can take an idea that would have stayed a note in my head, bootstrap it in a couple of hours, iterate quickly, and decide after seeing a working version whether it deserves more attention. The economics of building have changed. The threshold for trying something new has dropped.&lt;/p&gt;

&lt;p&gt;That doesn’t mean the work disappears. I still need to decide what I want, review what gets produced, test it, and cut scope aggressively. But the cost of getting from vague idea to usable prototype is now low enough that many more ideas make it through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed for me
&lt;/h2&gt;

&lt;p&gt;The biggest shift is how quickly I can get real feedback on an idea. Instead of investing hours just to see if something is worth building, I can reach a working prototype fast and judge its value right away. This means I spend less time on setup and boilerplate, and more on deciding what’s actually useful, trimming scope, and testing if the result feels right.&lt;/p&gt;

&lt;p&gt;It’s still engineering work, just more focused on product judgment than manual construction. And because the cost of trying is so low, I’m more willing to treat ideas as experiments. If something’s useful, I keep refining it. If not, I’ve only lost a couple of hours supervising the AI, not a whole weekend of coding (or worse).&lt;/p&gt;

&lt;h2&gt;
  
  
  One recent example
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sleeyax/taplog-app" rel="noopener noreferrer"&gt;TapLog&lt;/a&gt; is a simple Android app for logging recurring events with almost no friction. You create custom event types and place them on the main screen as large buttons. Tap once, and the app records the event with a timestamp. Long-press, and you can add a note. Later, you can review everything in a logbook, edit entries, export the data, or back it up locally.&lt;/p&gt;

&lt;p&gt;It solves a very ordinary problem: I wanted to track missed bus rides during my commute. That’s exactly the kind of problem I mean. Real enough to be annoying, useful enough that data would help, but not obviously important enough to justify building a custom mobile app the old way.&lt;/p&gt;

&lt;p&gt;With AI in the loop, it became easy to just make the thing.&lt;/p&gt;

&lt;p&gt;What matters here isn’t that TapLog is technically impressive. It’s that it crossed the threshold from “I wish I had a tool for this” to “I have a tool for this” fast enough that the idea survived.&lt;/p&gt;

&lt;p&gt;This app was built with Claude Code (Opus 4.6 1M context in plan mode), and that’s exactly why it exists. Without that workflow, I probably wouldn’t have given it the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;This shift changes more than just developer velocity.&lt;/p&gt;

&lt;p&gt;It changes what gets built.&lt;/p&gt;

&lt;p&gt;When the cost of trying an idea drops, more niche, personal, and practical software gets made. More small tools exist because someone can justify building them now. That seems good to me. A lot of useful software doesn’t need to become a startup or a platform. Sometimes it just needs to solve one problem well enough to be worth keeping around.&lt;/p&gt;

&lt;p&gt;And this matters beyond professional developers. I have the advantage of knowing how to evaluate tradeoffs, inspect code, and recognize when something is wrong. That still helps a lot. But the broader point stands: it’s much easier now for people to build software for problems they care about, even if they aren’t strong coders.&lt;/p&gt;

&lt;p&gt;They still need judgment. They still need persistence. They still need to test what they build. But the barrier between having an idea and having a first working version is lower than it used to be. The hard part shifts away from typing every line yourself and toward knowing what to ask for, what to keep, and what to throw away.&lt;/p&gt;

&lt;p&gt;Of course, there are tradeoffs. Lowering the bar means more experiments, but also more half-baked or disposable tools. Not every AI-generated app will be good, secure, or maintainable. And it’s easy to get caught up in building for the sake of building. But on balance, I think the benefits outweigh the risks, especially for personal projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI coding tools are changing not just how fast I build, but what I build. They make it easier to turn small, personal ideas into real software. That’s a big deal for anyone who’s ever had a useful idea that wasn’t quite worth the effort of building before. Now, more of those ideas can see the light of day, and that’s exciting to me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop wasting hours on Claude Code Pro's session cooldown</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Tue, 27 Jan 2026 21:47:14 +0000</pubDate>
      <link>https://forem.com/sleeyax/stop-wasting-hours-on-claude-code-pros-session-cooldown-4mak</link>
      <guid>https://forem.com/sleeyax/stop-wasting-hours-on-claude-code-pros-session-cooldown-4mak</guid>
      <description>&lt;p&gt;If you're a Claude Code Pro subscriber, you know the pain: you burn through your session at 3pm, and then you're locked out until 8pm. That's a 5-hour cooldown with zero usage. Half your workday, gone.&lt;/p&gt;

&lt;p&gt;I built a Telegram bot to fix this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Claude Code Pro gives you a 5-hour session window. Once it expires, you wait 5 hours before you can start a new one. There's no way to queue up your next session in advance, so you either waste prime working hours waiting, or you carefully plan your usage around the clock.&lt;/p&gt;

&lt;p&gt;Neither option is great.&lt;/p&gt;

&lt;h2&gt;
  
  
  My solution: automated warmups
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sleeyax/claude-code-session-bot" rel="noopener noreferrer"&gt;Claude Code Session Bot&lt;/a&gt; is a self-hosted Telegram bot that starts Claude Code sessions on a schedule. The idea is simple: start the session while you sleep, work out or are away from your keyboard so it's partially used up but still active when you need it.&lt;/p&gt;

&lt;p&gt;Example usage scenario: schedule &lt;code&gt;/schedule tomorrow 9am&lt;/code&gt; before bed. The bot calculates that it needs to warm up at 6am. By the time 9am rolls around, you still have 2 hours of usage left. That's enough for a productive morning. By 11am the session expires, the cooldown starts, and after lunch you have a fresh session ready to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The bot runs on a VPS (or any always-on machine) with the Claude CLI installed and authenticated. When a scheduled warmup fires, it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"ready"&lt;/span&gt; &lt;span class="nt"&gt;--output-format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends a minimal prompt to Claude, which starts the 5-hour session timer. The bot records the session in a local SQLite database and tracks time remaining.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/warmup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start a session immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/session&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Check active session status (time left, expiry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/schedule &amp;lt;datetime&amp;gt; [hours]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Schedule a warmup so you have &lt;code&gt;[hours]&lt;/code&gt; remaining at &lt;code&gt;&amp;lt;datetime&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/schedules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List pending scheduled warmups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/cancel &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancel a scheduled warmup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;View recent session history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;See the &lt;a href="https://github.com/sleeyax/claude-code-session-bot" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; for full instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scheduling examples
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/schedule tomorrow 9am        # 2h remaining at 9am (warmup fires at 6am)
/schedule monday 14:00 3      # 3h remaining at 14:00 (warmup fires at 12:00)
/schedule jan 30 8:00 4h      # 4h remaining at 8:00 (warmup fires at 7:00)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math behind this is simple: &lt;code&gt;warmup_time = target_time - (5h - desired_hours_remaining)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Date parsing is handled by &lt;a href="https://github.com/wanasit/chrono" rel="noopener noreferrer"&gt;chrono-node&lt;/a&gt;, so natural language like "tomorrow 9am" or "next monday 14:00" just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The stack is intentionally minimal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; with strict mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; (via better-sqlite3, WAL mode) for persistence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;node-telegram-bot-api&lt;/strong&gt; for the Telegram interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-memory &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; timers restored from DB on restart&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No external services, no Redis, no message queues. Schedules survive restarts because they're persisted to SQLite and restored when the bot boots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The bot only tracks sessions &lt;strong&gt;it&lt;/strong&gt; starts. There's no Anthropic API to query your current session status, and reverse-engineering internal endpoints risks account bans. So if you start a session manually from your dev machine, the bot won't know about it.&lt;/p&gt;

&lt;p&gt;For best results, use the bot as your sole session starter from a dedicated VPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This is a simple tool for a specific annoyance. If you're a Claude Code Pro user who's tired of session cooldowns eating into your productive hours, give it a try.&lt;/p&gt;

&lt;p&gt;The project is &lt;a href="https://github.com/sleeyax/claude-code-session-bot" rel="noopener noreferrer"&gt;open source&lt;/a&gt; (MIT license) - contributions and feedback welcome.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>telegram</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Write Dynamic GitLab pipelines in TypeScript</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Fri, 23 Jan 2026 21:06:16 +0000</pubDate>
      <link>https://forem.com/sleeyax/write-dynamic-gitlab-pipelines-in-typescript-3ake</link>
      <guid>https://forem.com/sleeyax/write-dynamic-gitlab-pipelines-in-typescript-3ake</guid>
      <description>&lt;p&gt;As CI/CD pipelines evolve, they tend to grow in size, complexity, and inevitably... frustration. If you’ve ever maintained a large &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file, you’ve probably felt the pain: copy-paste everywhere, no type safety, and mistakes that only surface when a runner fails at runtime.&lt;/p&gt;

&lt;p&gt;This article introduces an alternative approach: &lt;strong&gt;defining GitLab pipelines in TypeScript&lt;/strong&gt;, while still producing a perfectly valid &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file that GitLab can run as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why TypeScript for GitLab pipelines?
&lt;/h2&gt;

&lt;p&gt;GitLab CI configuration is written in YAML. YAML is fine for small pipelines, but once pipelines become complex, YAML starts to feel limiting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No type checking&lt;/li&gt;
&lt;li&gt;No refactoring support&lt;/li&gt;
&lt;li&gt;No real abstraction or reuse&lt;/li&gt;
&lt;li&gt;Errors show up late, during pipeline execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile, modern CI/CD platforms like &lt;a href="https://pandaci.com/" rel="noopener noreferrer"&gt;PandaCI&lt;/a&gt; already allow pipelines to be defined in a real programming language out of the box.&lt;/p&gt;

&lt;p&gt;Unfortunately, GitLab doesn’t support this natively.&lt;/p&gt;

&lt;p&gt;I explored existing solutions, but none quite hit the mark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/devowlio/node-gitlab-ci" rel="noopener noreferrer"&gt;node-gitlab-ci&lt;/a&gt; - no longer actively maintained&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/onlywei/gitlab-dynamic-pipelines" rel="noopener noreferrer"&gt;gitlab-dynamic-pipelines&lt;/a&gt; - a manually coded wrapper rather than a schema-accurate representation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I wanted was something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple and lightweight&lt;/li&gt;
&lt;li&gt;Easy to update when GitLab’s schema changes&lt;/li&gt;
&lt;li&gt;Mapped &lt;strong&gt;1:1&lt;/strong&gt; to the official GitLab CI schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a small library to do exactly that: &lt;a href="https://gitlab.com/sleeyax/gitlab-ci-ts" rel="noopener noreferrer"&gt;gitlab-ci-ts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The core logic is only ~30 lines of code. The rest is auto-generated TypeScript types from GitLab’s schema.&lt;/p&gt;

&lt;p&gt;If you're wondering why I don't just use &lt;code&gt;includes&lt;/code&gt;, &lt;code&gt;extends&lt;/code&gt;, anchors/references and &lt;code&gt;gitlab-local-ci&lt;/code&gt; instead, see &lt;a href="https://dev.to/sleeyax/comment/33opk"&gt;this comment&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;Instead of writing YAML by hand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define your pipeline entirely in TypeScript&lt;/li&gt;
&lt;li&gt;Compile it to a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Commit both the TypeScript source and the generated YAML&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GitLab still only sees YAML, but &lt;em&gt;you&lt;/em&gt; get the benefits of TypeScript.&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;// gitlab-ci.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GitLabCI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transformToFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sleeyax/gitlab-ci-ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Reusable cache.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;files&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;package-lock.json&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;paths&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;.npm/&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="c1"&gt;// General-purpose pipeline.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitLabCI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:20&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeCache&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;GIT_DEPTH&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="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;stages&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;test&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;build&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;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;

  &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Hidden job to share install steps.&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.install&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;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;before_script&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;npm ci --prefer-offline --no-audit --if-present&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;interruptible&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="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeCache&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="c1"&gt;// Individual jobs.&lt;/span&gt;
    &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&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;npm run lint --if-present&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$CI&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;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&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;npm test --if-present&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;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.install&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&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;npm run build --if-present&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeCache&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;paths&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;dist/&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="p"&gt;},&lt;/span&gt;

    &lt;span class="na"&gt;docker_push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;docker:28&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;services&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;docker:28-dind&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;before_script&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="s1"&gt;echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin&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;script&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;docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .&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;docker push $CI_REGISTRY_IMAGE --all-tags&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;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$CI&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="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Write ".gitlab-ci.yml" file.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transformToFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.gitlab-ci.yml&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;Ready to play around with it yourself? 👇&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @sleeyax/gitlab-ci-ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://gitlab.com/sleeyax/gitlab-ci-ts" rel="noopener noreferrer"&gt;gitlab-ci-ts repository&lt;/a&gt; for more examples and documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pros of TypeScript-driven pipelines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type safety.&lt;/strong&gt; Configuration mistakes get caught &lt;strong&gt;at compile time&lt;/strong&gt;, not during a failing pipeline run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse and abstraction.&lt;/strong&gt; You can share constants, extract helpers, and centralize common job definitions without copy-pasting YAML blocks across files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience.&lt;/strong&gt; You get autocomplete, inline documentation (TSDoc), safe refactoring, and earlier detection of invalid or unused variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structure it however you want
&lt;/h3&gt;

&lt;p&gt;Because the pipeline is just code, you’re free to organize it in a way that actually makes sense.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apps/pipeline
├── package.json
├── src
│   ├── cache.ts
│   ├── gitlab-ci.ts
│   ├── &lt;span class="nb"&gt;jobs&lt;/span&gt;
│   │   ├── environment
│   │   │   ├── development.ts
│   │   │   ├── production.ts
│   │   │   └── staging.ts
│   │   └── stages
│   │       ├── build
│   │       ├── deploy
│   │       ├── &lt;span class="nb"&gt;test&lt;/span&gt;
│   ├── main.ts
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Future possibilities
&lt;/h3&gt;

&lt;p&gt;Once your pipeline is defined as code, new doors open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unit tests for pipeline configuration&lt;/li&gt;
&lt;li&gt;Easier large-scale refactors&lt;/li&gt;
&lt;li&gt;Programmatic validation of rules, variables, and stages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Trade-offs and limitations
&lt;/h2&gt;

&lt;p&gt;Obviously, this approach isn’t without downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extra abstraction layer.&lt;/strong&gt; If GitLab introduces a feature that isn’t represented in the generated types yet, you may need to update the library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extra build step.&lt;/strong&gt; You must compile the TypeScript to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; before committing.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Defining GitLab pipelines in TypeScript brings modern software engineering practices to CI/CD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type safety&lt;/li&gt;
&lt;li&gt;Reusability&lt;/li&gt;
&lt;li&gt;Testability&lt;/li&gt;
&lt;li&gt;Maintainability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitLab still runs YAML, but you don’t have to &lt;em&gt;write&lt;/em&gt; it anymore.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>typescript</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>Flexispot E7 Flow: A detailed review and installation tips</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Sat, 17 Jan 2026 20:10:20 +0000</pubDate>
      <link>https://forem.com/sleeyax/flexispot-e7-flow-a-detailed-review-and-installation-tips-48af</link>
      <guid>https://forem.com/sleeyax/flexispot-e7-flow-a-detailed-review-and-installation-tips-48af</guid>
      <description>&lt;p&gt;This is my detailed review of the E7 Flow standing desk from Flexispot, including my experience with the ordering process, shipment, installation, and daily use. I'll also provide some tips and insights to help others who are considering this desk.&lt;/p&gt;

&lt;p&gt;If you're looking for a TL;DR, see the conclusion at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desk specifications
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt;: Flexispot E7 Flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size&lt;/strong&gt;: 160 × 80 cm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color&lt;/strong&gt;: Volcano Grey&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessories&lt;/strong&gt;: No Extras&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site&lt;/strong&gt;: flexispot.nl&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order date&lt;/strong&gt;: 2025-11-28&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assembly date&lt;/strong&gt;: 2025-12-06&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;My previous desk was a simple rectangular, solid wood block-style desk with a 3-level drawer on the left and sitting area on the right. It was only 130 × 75 cm in size. I've been using that desk all my life as a student and later as a remote worker. It was boring, but extremely stable.&lt;/p&gt;

&lt;p&gt;At work, I tried a standing desk for the first time and really liked it, so I decided to finally upgrade my home office with one as well. Finding a solid standing desk can be challenging, especially with so many options available and most of the well-known companies being based in the US.&lt;/p&gt;

&lt;p&gt;After a couple of days of research, I found Flexispot to be a good option given its desk features, price/quality ratio, and positive reviews.&lt;/p&gt;

&lt;p&gt;Initially, I wanted to go for the E7 Plus (four-legged standing desk) as I was afraid to experience the infamous wobble. But after reading further into it and considering my simple setup (desktop PC, monitor and dual speakers, no heavy equipment), I convinced myself it was overkill for my use case and placed my bet on a two-legged desk instead. The E7 Flow was just released around that time, so I decided to order it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering process
&lt;/h2&gt;

&lt;p&gt;Black Friday was only a few months away, so I decided to wait for the sale. Flexispot had an "enter your email here and get a discount code" pop-up on their website for the E7 Flow, so I signed up. On the day, I noticed they had another discount going, and my code only saved me about 20 bucks more compared to that one - but hey, every bit counts.&lt;/p&gt;

&lt;p&gt;The ordering process itself was smooth and I got a confirmation email right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipment
&lt;/h2&gt;

&lt;p&gt;The package shipped in 3 separate packages, as mentioned on the website. Delivery was fast and professional, handled by local carriers.&lt;/p&gt;

&lt;p&gt;Some minor annoyances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The tracking numbers were not provided on the dashboard; it just said "shipping". I had to check my email for the tracking numbers received from the shipping companies.&lt;/li&gt;
&lt;li&gt;The packages arrived on different days, one of which was sent by a different shipping company, so I had to keep track of multiple shipments and make sure I was home on those days.&lt;/li&gt;
&lt;li&gt;Two of the packages were processed on the same day and shipped by the same company, so it would've been nice if they were aware they belonged together so both could be delivered on the same day by the same courier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not sure how much control Flexispot has over these issues, if any, though. So no big deal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The package
&lt;/h3&gt;

&lt;p&gt;The packages arrived in good condition, with no visible damage. The boxes were sturdy and well-sealed. This became even more apparent when I opened them and found the contents wrapped in multiple layers of protective material, including foam and plastic wrappers. No scratches or dents were visible on any of the parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation process
&lt;/h2&gt;

&lt;p&gt;In the instruction video, they promised it would take about 20 minutes to install... Yeah well.&lt;/p&gt;

&lt;p&gt;It took me - a complete newbie at assembling furniture - about 10 hours for the desk assembly itself, and 20+ hours total including cable management and organizing my workspace. If you're a complete newbie like me, take your time and don't rush it. It can be a fun project if you approach it with the right mindset.&lt;/p&gt;

&lt;p&gt;It probably would've been easier if the instructions were more detailed. More on that below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The instructions
&lt;/h3&gt;

&lt;p&gt;I'll be honest: the instructions are a nightmare, especially for beginners. They're very minimal and lack detail. Some steps are not explained well, and some of the parts were scattered around the different boxes (for example the instruction manual says a part can be found in box A but it was actually in box C). I had to watch the installation video + read the manual multiple times to put the complete puzzle together and understand what to do.&lt;/p&gt;

&lt;p&gt;Below I'll go over each step of the installation process and share my experience, including any issues I encountered and how I resolved them.&lt;/p&gt;

&lt;h4&gt;
  
  
  Preparation before installation
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Install on foam / carpet to prevent scratches: I used the cardboard from the desktop box, which worked out alright. I had a minor scratch in the middle after assembly, but that's entirely my fault as I wasn't careful enough when moving the desk around.&lt;/li&gt;
&lt;li&gt;Screwdriver / drill: not needed; I only used the tools that came with the package.&lt;/li&gt;
&lt;li&gt;Get a friend to help: I managed to do the assembly and desk flip alone (I'm not exactly strong), but I did ask for help to move the desk into its final position. If you must do it solo from start to finish then it should be possible with some furniture sliders and patience.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  01 - Attach the side brackets to the beam
&lt;/h4&gt;

&lt;p&gt;This step was already pretty difficult for me. In the instruction video and manual, they only mention which side of the beam should be up, but they don't explain how to &lt;em&gt;rotate&lt;/em&gt; the beam into the correct orientation. If you skip through the video, it is briefly shown that the "blue part" of the beam is on the instructor's right side, but it's easy to miss. Basically, if you have a power plug on the left of your desk (as I do), it's best to have the blue part on the other side of the beam than what is shown, so the power cable is closest to the plug. It would've been nice if this was mentioned in the instructions.&lt;/p&gt;

&lt;p&gt;You also have to pay close attention to the side brackets, as one side of the bracket is longer than the other. Remember the E7 Flow has C-shaped legs, not T-shaped. This is easy to miss if you go over the instructions too fast. Also would've been nice if there was some kind of warning about this in the manual.&lt;/p&gt;

&lt;p&gt;Furthermore, the manual mentions to "fit the shock-absorbing washers". But I couldn't for the life of me find these in the box. The video didn't even mention it and someone on the subreddit (a mod, if I recall correctly) said that they are not necessary. So I decided to skip this step. As I briefly mentioned above, the rubbers turned out to be placed in a completely random box at the bottom, in a different location than what the manual said. So be sure to check all boxes for any small parts you think you're missing.&lt;/p&gt;

&lt;h4&gt;
  
  
  02 - Adjust the length of the frame
&lt;/h4&gt;

&lt;p&gt;A crucial piece of information is missing for this step: how to correctly position the desktop. The manual only shows a square desktop, but the E7 Flow has an ergonomic edge, so there is a clear "front" and "back" to the desk. The video doesn't clarify this either. If you look closely, you'll see that the "back" of the desk has two white "dots" on the underside, which are not present on the front. Make sure to align the frame with the correct side facing forward.&lt;/p&gt;

&lt;p&gt;This positioning step itself was pretty straightforward, but demanded patience. I needed a flashlight to properly "look under" the beam as I positioned it to see if the holes aligned.&lt;/p&gt;

&lt;h4&gt;
  
  
  03 - Affix the desktop
&lt;/h4&gt;

&lt;p&gt;This step should've been easy but caused another major confusion. There are unused screw holes on the beam in the middle of the frame. The manual mentions that "you may have extra screws depending on how far the frame is extended", but it doesn't clarify. In the video, they don't even mention it and skip over it. Looking this up on the subreddit, some people said they skipped it and other people said they just drilled the holes themselves. Given this is supposed to be a "pre-drilled holes" desk, I decided to just leave the holes unused as well.&lt;/p&gt;

&lt;p&gt;The manual also warns to "keep the centerline label centered when adjusting the frame". This is not mentioned in the video, and I didn't notice it until after I had already positioned the frame. Luckily I had some room to adjust it a bit further inwards to center the label. Why wasn't this mentioned in the previous step where you position the frame?&lt;/p&gt;

&lt;p&gt;Furthermore, the screws closest to the center of the beam didn't actually screw in as there was no real hole to screw them into. The pointy screw was basically pressing against the metal beam. Looking this up on the subreddit, people said this is a normal thing and you're just supposed to tighten it this way, not screw it in. This was so strange to me and it's not mentioned anywhere in the manual or video. More on that later.&lt;/p&gt;

&lt;h4&gt;
  
  
  04 - Install the leg columns
&lt;/h4&gt;

&lt;p&gt;This step was (finally) easy.&lt;/p&gt;

&lt;p&gt;According to a comment on the subreddit, this is the point where you could test the motors if you want to see if they work. It would've been nice if this was mentioned in the manual or video as well. I imagine this could have saved some people a lot of trouble if they discovered a faulty motor before fully assembling the desk.&lt;/p&gt;

&lt;h4&gt;
  
  
  05 - Attach the feet
&lt;/h4&gt;

&lt;p&gt;This step required some extra attention again because the feet part has a long and a short side, similar to the side brackets. It's difficult to see on the picture in the manual, but the video shows it briefly. Make sure to attach the feet with the long side facing outwards. You can check out pictures of the assembled desk online to figure this out if you're unsure.&lt;/p&gt;

&lt;h4&gt;
  
  
  06 - Fix the keypad
&lt;/h4&gt;

&lt;p&gt;This step was easy as well. Just make sure you attach it on the side you prefer. Remember that we still have to turn the desk around later, so choose accordingly.&lt;/p&gt;

&lt;p&gt;I assembled the desk in a small corner in my room. If you're in a similar spot like me, make sure you don't accidentally break the keypad either when turning the desk around or moving it to its final position.&lt;/p&gt;

&lt;h4&gt;
  
  
  07 - Connect the wires
&lt;/h4&gt;

&lt;p&gt;Finally another easy step. Just make sure to connect the wires properly according to the labels. The power cable didn't "click" into place for me like the other cables, but I just pushed it in firmly and it has worked fine so far.&lt;/p&gt;

&lt;h4&gt;
  
  
  08 - Adjust the foot pads for balance
&lt;/h4&gt;

&lt;p&gt;This is where the video diverges from the manual a bit. The video goes over the cable tray first. For consistency, I'll document my experience in the same order as the manual.&lt;/p&gt;

&lt;p&gt;The foot pads adjustment was pretty straightforward. Just make sure to adjust them evenly on both sides to keep the desk level. I heard some people skipped over this step and had issues with wobbling later on, so don't skip it unless you have a good reason to.&lt;/p&gt;

&lt;h4&gt;
  
  
  09 - Pre-installation preparation for the cable tray
&lt;/h4&gt;

&lt;p&gt;This step was the worst one of them all. It says to "check if there are any protruding screws on the beam, if so replace them with set screws". So in other words, remember those screws in step 3 that didn't actually screw in? Yeah, unscrew those again and use these other screws instead! What?! This is so incredibly counter-intuitive. Why wasn't this mentioned in step 3 where you actually put in the screws? This would've saved me so much confusion.&lt;/p&gt;

&lt;p&gt;Also, I kind of messed up one of the screws because I tightened it too much and it got stuck. I can't unscrew it anymore, so now I have a "set screw" that I can't replace. I'm not completely sure it affects the desk's stability or function, but so far it seems alright. But still, what a pain.&lt;/p&gt;

&lt;h4&gt;
  
  
  10 - Adjust the cable tray assembly
&lt;/h4&gt;

&lt;p&gt;This step was easy. The tray itself felt a bit more flimsy than I expected, but it does its job.&lt;/p&gt;

&lt;h4&gt;
  
  
  11 - Attach the cable tray
&lt;/h4&gt;

&lt;p&gt;This step was also straightforward, though I wasn't sure how tight the screws should be. Too tight and it feels like the tray might crack, too loose and it wobbles a bit. I just tightened them until they felt secure enough (though there's a bit of wobble still).&lt;/p&gt;

&lt;h4&gt;
  
  
  12 - Cable management
&lt;/h4&gt;

&lt;p&gt;The included velcro straps are a nice touch. They are sturdy and reusable, which is great for cable management. I used them to bundle my cables together and keep them organized under the desk. Personally I had plenty of strap left over after managing all my cables, but that depends on your setup of course.&lt;/p&gt;

&lt;h4&gt;
  
  
  13 - Install the cable management U-bracket
&lt;/h4&gt;

&lt;p&gt;Another nice touch to help with cable management. The included bracket feels sturdy. Sometimes it shifts a bit when I open the cable tray, but it's easy to reposition it. It doesn't fall off or anything, so no big deal.&lt;/p&gt;

&lt;h4&gt;
  
  
  14 - Install the cable organizer post
&lt;/h4&gt;

&lt;p&gt;The next set of parts to improve cable management even further. The cable clips are easy to attach and move around. It does its job to keep the cables organized. I used it to route my monitor, dual speakers and desktop cables, which helps keep them from tangling.&lt;/p&gt;

&lt;p&gt;The only minor gripe I have with this system is that the clips can't interfere with the beam (as mentioned in the manual). Thus, there's less "real" space in the tray than it looks initially. Depending on the type of cable (thick head or flat head, thick or thin cable), you might not be able to fit everything in there. For my setup, after fiddling around with it for a couple of hours and attaching the power strip at the perfect angle, it was just enough room to fit everything.&lt;/p&gt;

&lt;p&gt;Out of the box you get 5 clips, which was more than enough for me. I only ended up using 3 of them. Again, your usage will vary depending on your setup and cable types.&lt;/p&gt;

&lt;h4&gt;
  
  
  15 - Install the power distribution unit
&lt;/h4&gt;

&lt;p&gt;You can tell that the included power strip is of high quality. It has just the right amount of outlets (6) for my (present and future) needs. I had to apply a lot of pressure to plug my cables in though, so I recommend plugging them in before installing the power strip into the cable tray if possible.&lt;/p&gt;

&lt;p&gt;At first, I was worried that the sockets were not compatible with my Belgian plugs. I contacted Flexispot support and they offered to refund me the cost of the power strip if it turned out to be incompatible. Very professional of them to offer that. After learning a bit more about international plug standards, I realized it was indeed compatible. The power strip has a universal design that accepts various plug types, including the Netherlands Type C, modern Belgian/French Type E and German Type F plugs.&lt;/p&gt;

&lt;p&gt;One small complaint about the instructions: it would have been helpful if they mentioned that you can remove and rotate the mounting brackets on the sides of the power strip. Apparently, this is common for "server rack" style power strips, but I only discovered it after doing some research. Knowing this earlier would have saved me a lot of time fitting the power strip into the cable tray, since rotating the brackets allows for a better fit. In the video, the mounting brackets were also oriented differently than on my unit, which made me worry at first that I had received a faulty product.&lt;/p&gt;

&lt;h4&gt;
  
  
  16 - Install the velcro strap
&lt;/h4&gt;

&lt;p&gt;A nice addition I didn't expect. The tray already closes magnetically, so it's not absolutely necessary but offers an additional layer of security to keep it closed when not in use.&lt;/p&gt;

&lt;h4&gt;
  
  
  17 - Install the magnetic cable channel
&lt;/h4&gt;

&lt;p&gt;Another nice touch to keep the power strip cable and desk cable organized. The magnetic channel is easy to attach and remove. At first I felt like I didn't need it because my cable management was already managed so well thanks to all the other features, but it's a nice finishing touch nonetheless.&lt;/p&gt;

&lt;h4&gt;
  
  
  18 - Attach the cable clip
&lt;/h4&gt;

&lt;p&gt;Personally I skipped this step because my cables were already well managed without it. But if you have extra cables that need organizing, I can see this extra cable clip being useful. You can peel off the sticker and glue it wherever you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My experience using the desk
&lt;/h2&gt;

&lt;p&gt;Below I'll go over my experience using the desk after having it for a couple of weeks now. I don't like to do 'scores' so I'll just list the best and worst experiences using the desk so far.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best experiences
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stable in standing position. Wobble isn't as bad as I feared. With my light setup, I can type and work without any issues. I did notice some slight wobble when pushing against the desk, but it's not a big deal for normal use.&lt;/li&gt;
&lt;li&gt;Smooth and quiet height adjustment. The dual motors work well together to provide a stable lift. This is my first standing desk, so I don't have much to compare it to, but I'm very satisfied with the performance. It's exactly as advertised.&lt;/li&gt;
&lt;li&gt;Cable management system. The included cable management features are a big plus for me. The cable tray, clips, and power strip help keep my workspace tidy and organized. I appreciate the thought put into these features.&lt;/li&gt;
&lt;li&gt;The USB-C port on the keypad is a nice addition for charging (mobile) devices. It's conveniently located and I use it more than I would have expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Worst experiences
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Unstable in sitting position. My monitor wobbles when my desk is lowered to sitting position and I type on the keyboard. This was the most unexpected issue for me. I thought issues like this would be more pronounced when standing, but it's actually worse when sitting. My current theory is that it's actually my monitor (Gigabyte M28U) stand that is causing the wobble. When I touch my monitor lightly by hand, it already wobbles quite a bit. So when I type on the keyboard, the vibrations from the keystrokes transfer through the table to the monitor stand and cause the screen to wobble. I'm considering getting a more stable monitor or monitor arm in the future to see if that helps. This is close to being a deal-breaker for me, and made me question whether I should return the desk. My old desk is extremely stable so I never experienced this before. For now, I manage using a DIY solution by placing foam pads (from the desk's packaging) under the left and right sides of the monitor to stabilize it a bit more.&lt;/li&gt;
&lt;li&gt;I'm not a fan of the "ergonomic" edge. I've been sitting on a desk with straight edges my whole life, so this took some getting used to. At first it felt like my arms were constantly slipping off the edge. After a couple of weeks, I've adapted to it, but at this point in time I would still prefer straight edges. Another thing I didn't consider enough when I purchased this desk is the fact that it's impossible to "clip" things to the front of the desk. For example I have a racing wheel (for video games like Forza Horizon 4/5) that I can no longer use on this desk. I use my desk more for work than gaming, so it's not a big deal, but something to consider if you plan to use accessories that need to be clamped to the desk.&lt;/li&gt;
&lt;li&gt;The tabletop collects a lot of dust. Or maybe my room has always been dusty and it's just more visible now due to the grey color of the tabletop. Either way, I find myself cleaning the desk more often than I expected. Probably not a deal-breaker, but something to keep in mind.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, I'm satisfied with my Flexispot E7 Flow desk. It has its quirks and challenges, especially during assembly, but once set up, it provides a functional and enjoyable workspace. The standing feature is a great addition to my home office, and the cable management system helps keep things tidy.&lt;/p&gt;

&lt;p&gt;For me, it's a great standing desk, but the sitting experience is subpar because of my monitor wobble issue described above. If you're primarily looking for a standing desk and don't mind the ergonomic edge, I would recommend it. However, if you plan to use it mainly for sitting or have the exact same monitor as I do, I would advise caution and consider testing it out first if possible.&lt;/p&gt;

</description>
      <category>flexispot</category>
      <category>standingdesk</category>
      <category>review</category>
      <category>ergonomics</category>
    </item>
    <item>
      <title>I Built a Better World Time API</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Wed, 13 Aug 2025 19:56:34 +0000</pubDate>
      <link>https://forem.com/sleeyax/i-built-a-better-world-time-api-14mh</link>
      <guid>https://forem.com/sleeyax/i-built-a-better-world-time-api-14mh</guid>
      <description>&lt;p&gt;Have you ever needed to get the current time or timezone for a specific timezone or IP address, only to find that the available APIs are unreliable, outdated, or closed source? I did - and I decided to build a better solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build a New World Time API?
&lt;/h2&gt;

&lt;p&gt;The original &lt;a href="http://worldtimeapi.org/" rel="noopener noreferrer"&gt;worldtimeapi.org&lt;/a&gt; is a great idea, but it’s had its share of downtime and data issues. Other commercial APIs are often closed source, limited in features, or don’t offer plain text responses. I wanted something that was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fast and reliable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open development&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Up-to-date&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to use&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compatible with the World Time API&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introducing: &lt;a href="https://timeapi.world" rel="noopener noreferrer"&gt;timeapi.world&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This project is a drop-in replacement for &lt;code&gt;worldtimeapi.org&lt;/code&gt;, built with TypeScript and Cloudflare Workers for speed and scalability. It’s source-available under the Business Source License (BSL) and will automatically become open source after the BSL change date. It's free to use (currently up to 20,000 requests a month, commercial plans are available if you need more).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;High performance&lt;/strong&gt; (Cloudflare Workers)&lt;/li&gt;
&lt;li&gt;🟢 &lt;strong&gt;Reliable&lt;/strong&gt; (serverless, global edge network)&lt;/li&gt;
&lt;li&gt;📅 &lt;strong&gt;Frequently updated&lt;/strong&gt; (IANA timezone + MaxMind GeoIP with automated updates)&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;JSON &amp;amp; plain text responses&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🌍 &lt;strong&gt;Geo IP support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Backwards compatible with worldtimeapi.org&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;👐 &lt;strong&gt;Source-available (BSL) -&amp;gt; open source after BSL change date.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Swagger/OpenAPI docs&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  API Endpoints
&lt;/h2&gt;

&lt;p&gt;The API follows the World Time API spec:&lt;/p&gt;

&lt;h3&gt;
  
  
  Timezone Endpoints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/timezone&lt;/code&gt; – List all available timezones (JSON)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/timezone.txt&lt;/code&gt; – List all available timezones (plain text)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/timezone/{area}&lt;/code&gt; – List timezones for a specific area&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/timezone/{area}/{location}&lt;/code&gt; – Get current time for a location&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/timezone/{area}/{location}/{region}&lt;/code&gt; – Get current time for a region&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IP-based Endpoints
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/ip&lt;/code&gt; – Get time based on client IP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/ip/{ipv4}&lt;/code&gt; – Get time for a specific IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All endpoints support both JSON and plain text (just add &lt;code&gt;.txt&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Responses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSON:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"utc_offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-04:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"America/New_York"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"day_of_week"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"day_of_year"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;214&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"datetime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-02T13:02:11.703-04:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"utc_datetime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-02T17:02:11.703+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unixtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1754154131&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw_offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-18000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"week_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"abbreviation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dst_offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dst_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-03-09T07:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dst_until"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-02T06:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client_ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plain Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;utc_offset: -04:00
timezone: America/New_York
day_of_week: 6
day_of_year: 214
datetime: 2025-08-02T13:02:51.390-04:00
utc_datetime: 2025-08-02T17:02:51.390+00:00
unixtime: 1754154171
raw_offset: -18000
week_number: 31
dst: true
abbreviation: EDT
dst_offset: 3600
dst_from: 2025-03-09T07:00:00+00:00
dst_until: 2025-11-02T06:00:00+00:00
client_ip: 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Data Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timezone data:&lt;/strong&gt; &lt;a href="https://www.iana.org/time-zones" rel="noopener noreferrer"&gt;IANA timezone database&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo IP data:&lt;/strong&gt; &lt;a href="https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/" rel="noopener noreferrer"&gt;MaxMind GeoLite2&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are industry standards and updated regularly using automation scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The API is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; for type safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hono&lt;/strong&gt; web framework for performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Workers&lt;/strong&gt; for global edge deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare D1&lt;/strong&gt; for database storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All endpoints handle both JSON and plain text, with proper error handling and CORS support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Check it out at &lt;a href="https://timeapi.world/" rel="noopener noreferrer"&gt;https://timeapi.world/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s make time APIs better for everyone.&lt;/p&gt;




&lt;p&gt;Thanks for reading! If you enjoyed reading my content, &lt;a href="https://x.com/sleeyax" rel="noopener noreferrer"&gt;follow me on X&lt;/a&gt; to stay in the loop ❤️.&lt;/p&gt;

</description>
      <category>time</category>
      <category>api</category>
      <category>rapidapi</category>
      <category>timezone</category>
    </item>
    <item>
      <title>Why I stopped using NixOS and went back to Arch Linux</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Fri, 07 Mar 2025 10:36:00 +0000</pubDate>
      <link>https://forem.com/sleeyax/why-i-stopped-using-nixos-and-went-back-to-arch-4070</link>
      <guid>https://forem.com/sleeyax/why-i-stopped-using-nixos-and-went-back-to-arch-4070</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is a bit of a rant. Read at your own discretion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;About a year ago I switched from Manjaro linux to NixOS as a daily driver on my laptop. I run Arch linux on my desktop PC. &lt;/p&gt;

&lt;p&gt;My setup in a nutshell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NixOS installed on &lt;code&gt;2024-05-17&lt;/code&gt; (&lt;code&gt;stat / | grep "Birth"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;nixos-unstable&lt;/code&gt; branch + flakes + home-manager + cachix&lt;/li&gt;
&lt;li&gt;My &lt;a href="https://github.com/sleeyax/nixos-config" rel="noopener noreferrer"&gt;nixos-config&lt;/a&gt; (public  GitHub repo)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I fell in love with the idea of NixOS: as input you (basically) define your whole system configuration in a special config file syntax (Nix) and as output it gives you a reproducable build of your whole operating system and keeps track of changes so you can always revert back to an older generation if necessary. While this is nice on paper, it didn't end up working out for me. &lt;/p&gt;

&lt;p&gt;After using NixOS for almost 1 year I decided to switch back to Arch linux on my lapop. Here's why:&lt;/p&gt;

&lt;h2&gt;
  
  
  NixOS breaks. All. the. time.
&lt;/h2&gt;

&lt;p&gt;Ironically I broke Arch only once in 5 years whereas NixOS already breaks before updating. Something always requires a change in my Nix config before the &lt;code&gt;nixos-rebuild&lt;/code&gt; command finally succeeds. It's a good thing this check is in place, but the constant &lt;code&gt;rebuild &amp;gt; fix &amp;gt; rebuild &amp;gt; fix &amp;gt; rebuild &amp;gt; ...&lt;/code&gt; pattern becomes quite annoying after a while. &lt;/p&gt;

&lt;p&gt;When the new configuration finally builds, more ofen than not some component randomly stops working after reboot. If it isn't broken copy paste between electron apps it is audio or bluetooth that randomly stopped working. I'm left alone to fix it with 0 clue as to why it broke. &lt;/p&gt;

&lt;h2&gt;
  
  
  Error messages are cryptic
&lt;/h2&gt;

&lt;p&gt;This is related to my point above but it deserves its own section.&lt;/p&gt;

&lt;p&gt;Look at this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nixos-rebuild &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--flake&lt;/span&gt; .#laptop
warning: Git tree &lt;span class="s1"&gt;'/home/sleeyax/Programming/nix/nixos-config'&lt;/span&gt; is dirty
building the system configuration...
warning: Git tree &lt;span class="s1"&gt;'/home/sleeyax/Programming/nix/nixos-config'&lt;/span&gt; is dirty
error:
       … &lt;span class="k"&gt;while &lt;/span&gt;calling the &lt;span class="s1"&gt;'head'&lt;/span&gt; &lt;span class="nb"&gt;builtin
         &lt;/span&gt;at /nix/store/xq5rfjj1z2r8yx338arajg5vwsxh1fri-source/lib/attrsets.nix:1574:11:
         1573|         &lt;span class="o"&gt;||&lt;/span&gt; pred here &lt;span class="o"&gt;(&lt;/span&gt;elemAt values 1&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;head &lt;/span&gt;values&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;then
         &lt;/span&gt;1574|           &lt;span class="nb"&gt;head &lt;/span&gt;values
             |           ^
         1575|         &lt;span class="k"&gt;else&lt;/span&gt;

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating the attribute &lt;span class="s1"&gt;'value'&lt;/span&gt;
         at /nix/store/xq5rfjj1z2r8yx338arajg5vwsxh1fri-source/lib/modules.nix:927:9:
          926|     &lt;span class="k"&gt;in &lt;/span&gt;warnDeprecation opt //
          927|       &lt;span class="o"&gt;{&lt;/span&gt; value &lt;span class="o"&gt;=&lt;/span&gt; addErrorContext &lt;span class="s2"&gt;"while evaluating the option &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;showOption&lt;/span&gt;&lt;span class="p"&gt; loc&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;':" value;
             |         ^
          928|         inherit (res.defsFinal'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; highestPrio&lt;span class="p"&gt;;&lt;/span&gt;

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating the option &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;system.build.toplevel':

       … while evaluating definitions from &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;/nix/store/xq5rfjj1z2r8yx338arajg5vwsxh1fri-source/nixos/modules/system/activation/top-level.nix&lt;span class="s1"&gt;':

       … while evaluating the option `warnings'&lt;/span&gt;:

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating definitions from &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;/nix/store/xq5rfjj1z2r8yx338arajg5vwsxh1fri-source/nixos/modules/system/boot/systemd.nix':

       … while evaluating the option &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;systemd.services.home-manager-sleeyax.serviceConfig&lt;span class="s1"&gt;':

       … while evaluating definitions from `/nix/store/7dv77a007yd3z9w5gkvvn919nnrg2hf2-source/nixos'&lt;/span&gt;:

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating the option &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;home-manager.users.sleeyax.home.file."&lt;/span&gt;/home/sleeyax/.config/fontconfig/conf.d/10-hm-fonts.conf&lt;span class="s2"&gt;".source':

       … while evaluating definitions from &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;/nix/store/7dv77a007yd3z9w5gkvvn919nnrg2hf2-source/modules/files.nix&lt;span class="s1"&gt;':

       … while evaluating the option `home-manager.users.sleeyax.home.file."/home/sleeyax/.config/fontconfig/conf.d/10-hm-fonts.conf".text'&lt;/span&gt;:

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating definitions from &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;/nix/store/7dv77a007yd3z9w5gkvvn919nnrg2hf2-source/modules/misc/xdg.nix':

       … while evaluating the option &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;home-manager.users.sleeyax.xdg.configFile.&lt;span class="s2"&gt;"fontconfig/conf.d/10-hm-fonts.conf"&lt;/span&gt;.text&lt;span class="s1"&gt;':

       … while evaluating definitions from `/nix/store/7dv77a007yd3z9w5gkvvn919nnrg2hf2-source/modules/misc/fontconfig.nix'&lt;/span&gt;:

       … &lt;span class="k"&gt;while &lt;/span&gt;evaluating the option &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="s2"&gt;home-manager.users.sleeyax.home.packages':

       … while evaluating definitions from &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;/nix/store/9cmg9v44pmw1fdz0bzn357djc8g2fn1g-source/modules/home&lt;span class="s1"&gt;':

       (stack trace truncated; use '&lt;/span&gt;&lt;span class="nt"&gt;--show-trace&lt;/span&gt;&lt;span class="s1"&gt;' to show the full, detailed trace)

       error: logseq has been removed, due to lack of maintenance and blocking the Electron 27 removal.

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you spot the issue? Right! After spitting out a bunch of unreadable nonsense it says at the very bottom that the &lt;code&gt;logseq&lt;/code&gt; package has been removed. &lt;/p&gt;

&lt;p&gt;First of all, why bother with the verbosity if it's just a bunch of unreadable nonsense no one except package maintainers or core Nix devs can make sense of? Second - a bit off topic but... - removing a package I use daily without even suggesting an alternative or workaround. Seriously?&lt;/p&gt;

&lt;h2&gt;
  
  
  Huge update sizes
&lt;/h2&gt;

&lt;p&gt;NixOS handles dependencies very differently compared to Arch Linux, leading to much larger update sizes. Instead of replacing packages in place, NixOS installs new versions alongside old ones, keeping multiple system generations.&lt;/p&gt;

&lt;p&gt;For example, updating &lt;code&gt;glibc&lt;/code&gt; in Arch Linux simply replaces the old version. In NixOS, every package depending on glibc is rebuilt or redownloaded, massively increasing disk usage. While this ensures rollback safety, it also means your system can quickly bloat up (unless you run &lt;code&gt;nix-collect-garbage&lt;/code&gt; periodically).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# An example for the 'glibc-locales' package:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /nix/store | &lt;span class="nb"&gt;grep &lt;/span&gt;glibc-locales
0j3cpwmwab04jmvyzcqh538jzrnm60hn-glibc-locales-2.40-36.drv
0k4azawc10ygacxyqjz36cqghxsicqpr-glibc-locales-2.40-66.drv
2i4wm7zsm2nkn7sn2aj7ng4x1k3p71gz-glibc-locales-2.39-52.drv
2sk3c2n6p3aavhd7659ll2w63mv0i3aw-glibc-locales-2.40-66.drv
33alnvvkrp1s13vg5frn1zca34xbz424-glibc-locales-2.39-52.drv
51ssq420b3wqvcxmgmcw00hk8zkh6gvw-glibc-locales-2.39-52.drv
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By contrast, Arch Linux updates are simpler to reason about. Old files are removed automatically and shared libraries don’t cause redundant downloads. If you care about minimizing disk usage, or your internet/network is capped, NixOS can be a nightmare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compilation takes forever (and binary caches are unreliable)
&lt;/h2&gt;

&lt;p&gt;NixOS builds packages in an isolated environment, which often means compiling everything from source - even for minor updates. While &lt;a href="https://nixos.wiki/wiki/Binary_Cache" rel="noopener noreferrer"&gt;binary caches&lt;/a&gt; like &lt;a href="https://www.cachix.org/" rel="noopener noreferrer"&gt;cachix&lt;/a&gt; theoretically speed up installations, they frequently miss packages due to system differences, forcing unnecessary compilation.&lt;/p&gt;

&lt;p&gt;For example: if a dependency isn't cached exactly as required, NixOS will still rebuild it locally even if it's a common package. This can take hours, especially on slower hardware. On my machine regular maintenance updates without (or with bad caching) easily take 4-5+ hours to complete.&lt;/p&gt;

&lt;p&gt;Meanwhile, Arch Linux simply downloads prebuilt binaries with pacman or an AUR helper, making updates take a couple of minutes instead.&lt;/p&gt;

&lt;p&gt;If you hate waiting for your system to rebuild itself constantly, NixOS will drive you insane. If you love to recompile everything from scratch why aren't you using &lt;a href="https://www.gentoo.org/" rel="noopener noreferrer"&gt;gentoo&lt;/a&gt; instead?&lt;/p&gt;

&lt;h2&gt;
  
  
  Poor documentation
&lt;/h2&gt;

&lt;p&gt;Ironically, I tend to find better answers on the Arch Wiki than on the NixOS Wiki itself. The NixOS documentation is often vague, outdated, or overly abstract, assuming deep familiarity with Nix's unique concepts.&lt;/p&gt;

&lt;p&gt;Practical examples are scarce and official guides frequently leave out crucial details forcing users to dig through forum posts or even GitHub issues to figure out basic tasks.&lt;/p&gt;

&lt;p&gt;For a system as complex as NixOS, good documentation is essential. Unfortunately, it just isn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I wish NixOS a bright future ahead to address all of its shortcomings. But until then it's no longer my favorite distro and I wouldn't recommend daily driving it as your desktop linux distro unless you have a very specific use case.&lt;/p&gt;

&lt;p&gt;Alternatives already exist for its 'killer features':&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generations&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://wiki.archlinux.org/title/Btrfs" rel="noopener noreferrer"&gt;BTRFS&lt;/a&gt; snapshot volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declarative package management&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/CyberShadow/aconfmgr" rel="noopener noreferrer"&gt;aconfmgr&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declarative configuration management&lt;/td&gt;
&lt;td&gt;Dotfiles synced in a git repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nix flakes for development&lt;/td&gt;
&lt;td&gt;Docker (or podman etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nix-shell&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://wiki.archlinux.org/title/Nix" rel="noopener noreferrer"&gt;nix package manager&lt;/a&gt; on Arch (or another supported distro)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Also keep an eye on &lt;a href="https://github.com/numtide/system-manager" rel="noopener noreferrer"&gt;system-manager&lt;/a&gt; (manage system config using nix on any distro) - which is still in early development at the time of writing.&lt;/p&gt;

</description>
      <category>nixos</category>
      <category>archlinux</category>
      <category>linux</category>
      <category>hyprland</category>
    </item>
    <item>
      <title>Dynamic string validation using go's text/template package</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Mon, 23 Dec 2024 22:06:08 +0000</pubDate>
      <link>https://forem.com/sleeyax/dynamic-string-validation-using-gos-texttemplate-package-2b65</link>
      <guid>https://forem.com/sleeyax/dynamic-string-validation-using-gos-texttemplate-package-2b65</guid>
      <description>&lt;p&gt;Imagine you could validate the following string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id: "d416e1b0-97b2-4a49-8ad5-2e6b2b46eae0"
static-string: "abc"
invalid-string: def
random-number: 150
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using go template syntax like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id: "{{isUUID}}"
static-string: "abc"
invalid-string: def
random-number: {{inRange 100 200}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, that would be cool wouldn't it? Unfortunately this isn't supported by go's &lt;a href="https://pkg.go.dev/text/template" rel="noopener noreferrer"&gt;text/template&lt;/a&gt; package. &lt;/p&gt;

&lt;p&gt;I've built a library which uses a subset of the template syntax to cover this specific use-case: &lt;a href="https://github.com/sleeyax/templatex-go" rel="noopener noreferrer"&gt;github.com/sleeyax/templatex-go&lt;/a&gt;. Check it out for a more detailed example!&lt;/p&gt;




&lt;p&gt;Hi 👋 thanks for reading! If you enjoyed reading my content, consider following me on &lt;a href="https://x.com/sleeyax" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; to stay in the loop ❤️.&lt;/p&gt;

</description>
      <category>go</category>
      <category>programming</category>
      <category>opensource</category>
      <category>validation</category>
    </item>
    <item>
      <title>Turn any website into a type-safe API using AI (part 2)</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Sat, 16 Nov 2024 17:24:44 +0000</pubDate>
      <link>https://forem.com/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-2-3cag</link>
      <guid>https://forem.com/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-2-3cag</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-1-2n0e"&gt;part 1&lt;/a&gt; I went over a basic solution on how to scrape data from any website into any desired format using AI. This part covers the next steps to improve performance and reduce costs.&lt;/p&gt;

&lt;p&gt;This part is a bit shorter because I no longer have the time to delve too deep into these subjects, but if you're on a similar path and enjoyed reading part 1 then I hope these 'field notes' can serve as inspiration in your own research. If you got any more tips, or questions, feel free to leave a comment below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reduce costs &amp;amp; optimize performance
&lt;/h2&gt;

&lt;p&gt;As mentioned in part 1, using OpenAI's models as-is can get quite expensive. This section covers the different strategies I've found to reduce costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convert HTML to LLM-friendly text
&lt;/h3&gt;

&lt;p&gt;HTML elements contain quite a lot of 'bloat' by design. All of that extra markup isn't all that useful to our model, but it does count towards our token spend limit. In order to avoid that, we could try and convert that HTML into a different format (like markdown) that is not only smaller but also easier to parse by the model. &lt;/p&gt;

&lt;h4&gt;
  
  
  Jina Reader
&lt;/h4&gt;

&lt;p&gt;Using the &lt;a href="https://jina.ai/reader/" rel="noopener noreferrer"&gt;Jina Reader API&lt;/a&gt; or by self-hosting its &lt;a href="https://huggingface.co/jinaai/reader-lm-1.5b" rel="noopener noreferrer"&gt;models&lt;/a&gt;, we can convert the HTML to Markdown:&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.amazonaws.com%2Fuploads%2Farticles%2F699vppl6t92qgea7768p.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.amazonaws.com%2Fuploads%2Farticles%2F699vppl6t92qgea7768p.png" alt="Jina reader API showcase" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, there's way less bloat in there compared to the raw HTML output. Pass this to the model and it should still work while costing you less tokens.&lt;/p&gt;

&lt;h4&gt;
  
  
  Firecrawl
&lt;/h4&gt;

&lt;p&gt;Another solution is &lt;a href="https://www.firecrawl.dev/" rel="noopener noreferrer"&gt;Firecrawl&lt;/a&gt;, an open source project to crawl, scrape and clean your data. They offer a hosted paid version but its core features are free and &lt;a href="https://github.com/mendableai/firecrawl" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;. So you could set up your own instance for free.&lt;/p&gt;

&lt;h4&gt;
  
  
  Crawl4ai
&lt;/h4&gt;

&lt;p&gt;If speed is your uppermost priority or you simply don't like firecrawl, the completely free and open source &lt;a href="https://github.com/unclecode/crawl4ai" rel="noopener noreferrer"&gt;crawl4ai&lt;/a&gt; project might be a better option to look into.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different models
&lt;/h3&gt;

&lt;p&gt;OpenAI isn't the only player in the AI game. There are more affordable options available. To name a few:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://groq.com/products/" rel="noopener noreferrer"&gt;groq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.fireworks.ai/structured-responses/structured-response-formatting#using-json-mode" rel="noopener noreferrer"&gt;fireworks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's plenty of options to choose from, but not all of them work well with structured/JSON output mode (yet). Let me know in the comments which provider ended up working well for you!&lt;/p&gt;

&lt;h2&gt;
  
  
  Other challenges
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;real challenges&lt;/em&gt; that comes with a project like this are the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scraping websites that are protected by a WAF (Web Application Firewall), dealing with "anti bot" challenges, captchas etc.&lt;/li&gt;
&lt;li&gt;Avoiding bans by rotation proxies&lt;/li&gt;
&lt;li&gt;Keeping AI/LLM costs low&lt;/li&gt;
&lt;li&gt;Handling bad data or AI hallucinations&lt;/li&gt;
&lt;li&gt;Scaling the infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So please think twice before your start yet another business around this (very saturated) idea ;)&lt;/p&gt;




&lt;p&gt;Hi 👋 thanks for reading! If you enjoyed reading my content, consider following me on &lt;a href="https://x.com/sleeyax" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; to stay in the loop ❤️.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to get rid of fake followers on X (Twitter)</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Sun, 20 Oct 2024 12:36:16 +0000</pubDate>
      <link>https://forem.com/sleeyax/how-to-get-rid-of-fake-followers-on-x-twitter-4o1g</link>
      <guid>https://forem.com/sleeyax/how-to-get-rid-of-fake-followers-on-x-twitter-4o1g</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a &lt;strong&gt;free&lt;/strong&gt;, &lt;strong&gt;fully configurable&lt;/strong&gt; and &lt;strong&gt;open source&lt;/strong&gt; browser extension called X Bot Sweeper to remove fake followers and bots on X for good. Jump straight to the solution or read ahead for more information.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'll cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;The negative impact on your X account&lt;/li&gt;
&lt;li&gt;Why are bots following me on X?&lt;/li&gt;
&lt;li&gt;Spotting a fake account&lt;/li&gt;
&lt;li&gt;
Solutions

&lt;ul&gt;
&lt;li&gt;Manual removal&lt;/li&gt;
&lt;li&gt;
Browser extension

&lt;ul&gt;
&lt;li&gt;Configuration options&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;Conclusion&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Almost every day I get notified of obviously fake followers (bots) who started following me on X (formerly Twitter):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms4sv0xkhf5ux4wovmoa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fms4sv0xkhf5ux4wovmoa.png" alt="bot followers example 1" width="737" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some of these bots also send DMs (Direct Messages) asking seemingly benign questions. It's super obvious these messages are AI-generated though:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrvptiu7yh4i4wk5bhbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyrvptiu7yh4i4wk5bhbr.png" alt="bot followers example 2" width="716" height="790"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In another DM I even tried a couple of basic &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;prompt injection techniques&lt;/a&gt; to get the LLM to drop the act, but alas:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0c45to1n26dtfo26j3rv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0c45to1n26dtfo26j3rv.png" alt="bot followers example 3" width="710" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The negative impact on your X account
&lt;/h2&gt;

&lt;p&gt;You might be thinking "Why even bother? Free followers don't hurt right?". Well, in reality the exact opposite is observed to be true.&lt;/p&gt;

&lt;p&gt;Even though we don’t have full insight into the X engagement algorithm, it's safe to assume that bot followers negatively impact your account’s reach and engagement. Here’s why it’s a good idea to take precautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm Sensitivity&lt;/strong&gt;: X’s algorithm may evaluate the quality of your followers when deciding how widely to share your content. Bot followers could lower your visibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement Levels&lt;/strong&gt;: Bots don’t usually interact with your posts, leading to lower engagement rates. This could cause the algorithm to see your account as less valuable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account Reputation&lt;/strong&gt;: An abundance of bot followers can make your account appear less credible to both the platform and real users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staying Ahead&lt;/strong&gt;: As social media algorithms continue to change, removing bots now could help protect your account from potential future consequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, consider removing bots to reset the authenticity of your X account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are bots following me on X?
&lt;/h2&gt;

&lt;p&gt;So what's the point? Below are the top 5 reasons why bots are being employed like this.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Boosting follower counts
&lt;/h3&gt;

&lt;p&gt;Most of these bots follow a large number of random users, hoping for a follow-back. This strategy helps them increase their follower count quickly. They might not care who you are specifically; you're just a potential addition to their follower base.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Promoting scams
&lt;/h3&gt;

&lt;p&gt;Bots are often used to promote certain products, services, or websites. By following you, they might be aiming to get your attention, hoping you’ll check out their profile and see what they’re promoting. This could range from harmless products to sketchy links, scams and phishing campaigns.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Algorithm manipulation
&lt;/h3&gt;

&lt;p&gt;Some bots exist to manipulate trending topics or amplify certain posts by engaging with a particular set of users. By following you, they might be trying to get more people to see and engage with specific content, thereby influencing the platform's algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. SMM panels
&lt;/h3&gt;

&lt;p&gt;Bots want to look like genuine users. By following random people, they attempt to mimic natural human behavior. Having followers can make them appear less like a bot account, especially if they plan to interact later. &lt;/p&gt;

&lt;p&gt;Genuine-looking bot accounts are often employed by illicit 'SMM panels' to boost traffic on particular social media posts. An SMM (Social Media Marketing) panel is essentially an online platform that offers a wide range of social media services. These services can include purchasing likes, followers, comments, shares, and more on popular social media platforms such as but not limited to X. &lt;/p&gt;

&lt;h3&gt;
  
  
  5. Catfishing
&lt;/h3&gt;

&lt;p&gt;An accounts with an attractive profile picture might send you a DM (Direct Message) to strike up a conversation, pretending to be someone they're not. Their goal is to build a relationship and gain your trust. &lt;/p&gt;

&lt;p&gt;Once they’ve established a connection, the scammer behind the account uses emotional manipulation to trick you into sending money, gifts, or personal information. They may fabricate stories about needing financial help, having an emergency, or planning to visit you.&lt;/p&gt;

&lt;p&gt;Sometimes, they might also send links in DMs that lead to phishing sites, malware downloads, or scam websites. Clicking on these can compromise your personal information or device security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spotting a fake account
&lt;/h2&gt;

&lt;p&gt;It's relatively easy to spot a fake follower. They usually match at least half of the following criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attractive (usually female) profile picture. Likely AI generated.&lt;/li&gt;
&lt;li&gt;Very high 'following' count&lt;/li&gt;
&lt;li&gt;Very low 'followers' count&lt;/li&gt;
&lt;li&gt;Few posts. Or 1 single post containing nonsense.&lt;/li&gt;
&lt;li&gt;Many randomly reposted posts to appear active. &lt;/li&gt;
&lt;li&gt;Account has been created very recently&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual removal
&lt;/h3&gt;

&lt;p&gt;You can manually remove these bots by visiting their profile pages and blocking each of them individually. This can be very time-consuming though, especially if you have a large number of followers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser extension
&lt;/h3&gt;

&lt;p&gt;I created a &lt;strong&gt;free&lt;/strong&gt;, &lt;strong&gt;fully configurable&lt;/strong&gt; and &lt;strong&gt;open source&lt;/strong&gt; browser extension to help you get rid of bots semi-automatically. &lt;/p&gt;

&lt;p&gt;Get started in 5 simple steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the X Bot Sweeper extension from my GitHub repository: &lt;a href="https://github.com/sleeyax/x-bot-sweeper" rel="noopener noreferrer"&gt;sleeyax/x-bot-sweeper&lt;/a&gt;. Links to the Chrome web store and any other browsers or platforms we support now or will in the future are provided there.&lt;/li&gt;
&lt;li&gt;After installing the extension, navigate to &lt;a href="https://x.com/home" rel="noopener noreferrer"&gt;x.com/home&lt;/a&gt; at least once. This step is &lt;strong&gt;crucial&lt;/strong&gt; for the extension to initialize itself for the first time. You will gracefully receive an error message if you forget this step.&lt;/li&gt;
&lt;li&gt;Open the extension and click 'scan' to analyze your followers for unwanted accounts.
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp5w8sujz3owr41bnvw75.png" alt="extension scan followers showcase" width="800" height="629"&gt;
&lt;/li&gt;
&lt;li&gt;Confirm which accounts you want to get rid of by selecting them on the left and then click the 'block bot(s)' button at the top.
&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5g1wxzeu0wgzkoslhssw.png" alt="extension delete bots showcase" width="800" height="627"&gt;
&lt;/li&gt;
&lt;li&gt;Go grab a drink while the extension blocks all of the accounts you selected. This action can take a while depending on the amount of followers your selected and the configured timeout in the extension settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done! From now on every time you are notified of new fake followers you can repeat the process starting from step 3. I recommend performing a scan every week or month, depending on the severity of your bot problem.&lt;/p&gt;

&lt;h4&gt;
  
  
  Extension Settings
&lt;/h4&gt;

&lt;p&gt;You can fully customize the extension to your needs. This is recommended for &lt;strong&gt;advanced users only&lt;/strong&gt;. When in doubt, leave it at the default settings.&lt;/p&gt;

&lt;p&gt;Right click the extension icon.&lt;/p&gt;

&lt;p&gt;Or, if you didn't pin the extension yet, click this menu icon instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop5kex6m0omi1omprlk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop5kex6m0omi1omprlk6.png" alt="open extension settings" width="376" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, select 'Options':&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwnmuxdj6ae9w14hwd5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwnmuxdj6ae9w14hwd5e.png" alt="open extension setttings" width="392" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This should bring you to the extension configuration page:&lt;/p&gt;

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

&lt;p&gt;The fields should be self-explanatory, but you can always hover over the question mark icon if you need more information about a particular field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Removing fake followers from your X account is an essential step to maintain your account’s credibility, engagement, and long-term success. While this might seem like a daunting task at first, it’s worth the effort to ensure your audience is authentic and engaged, which can positively impact how the platform’s algorithm views your content. Taking the time now to clear out fake followers using the X Bot Sweeper browser extension will help protect your account’s reach and reputation as social media platforms continue to evolve.&lt;/p&gt;




&lt;p&gt;If you have any questions, feel free to leave a comment below. Please report any bugs you encounter &lt;a href="https://github.com/sleeyax/x-bot-sweeper/issues?q=sort%3Aupdated-desc+is%3Aissue+is%3Aopen" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Contributions to improve the extension are always welcome! &lt;/p&gt;

&lt;p&gt;If you enjoyed reading my content, and you are human, consider following me &lt;a href="https://x.com/sleeyax" rel="noopener noreferrer"&gt;@sleeyax&lt;/a&gt; on X ❤️.&lt;/p&gt;

</description>
      <category>twitter</category>
      <category>opensource</category>
      <category>browser</category>
      <category>extensions</category>
    </item>
    <item>
      <title>Turn any website into a type-safe API using AI (part 1)</title>
      <dc:creator>Sleeyax</dc:creator>
      <pubDate>Wed, 09 Oct 2024 01:22:10 +0000</pubDate>
      <link>https://forem.com/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-1-2n0e</link>
      <guid>https://forem.com/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-1-2n0e</guid>
      <description>&lt;p&gt;Not too long ago I saw this post on X/Twitter: &lt;iframe class="tweet-embed" id="tweet-1833539593054130336-993" src="https://platform.twitter.com/embed/Tweet.html?id=1833539593054130336"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1833539593054130336-993');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1833539593054130336&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;This idea intrigued me for a few days: turning any website into an API using AI? It sounded almost too good to be true. However, after experimenting with it, I can confidently say it’s not only possible but also much easier to achieve than you might expect.&lt;/p&gt;

&lt;p&gt;In this post I'll uncover the secrets 😏.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;In a nutshell&lt;/strong&gt; the flow to go from an arbitrary webpage to a structured JSON object is as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scrape the webpage&lt;/li&gt;
&lt;li&gt;Convert the HTML content into LLM-friendly text&lt;/li&gt;
&lt;li&gt;Feed the converted data to an LLM&lt;/li&gt;
&lt;li&gt;Instruct the LLM to extract the content into the provided JSON schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Heads up: The code snippets below will be provided in TypeScript. If you prefer python - or any other programming language for that matter - I think you'll be able to follow along relatively easy though.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Naive approach
&lt;/h3&gt;

&lt;p&gt;Let's start with the most basic approach to this problem by utilizing  OpenAI's GPT 4o model. OpenAI recently launched &lt;a href="https://openai.com/index/introducing-structured-outputs-in-the-api/" rel="noopener noreferrer"&gt;structured JSON outputs&lt;/a&gt;, which makes the JSON processing part in the final step much easier.&lt;/p&gt;

&lt;p&gt;Let's start by defining a similar function interface to the one we saw in the tweet:&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;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ExpandOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;source&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="nl"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;schemaName&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="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ExpandOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, define your data schema with &lt;a href="https://zod.dev/?id=introduction" rel="noopener noreferrer"&gt;zod&lt;/a&gt;. We'll define a schema that resembles the example in the tweet:&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;companySchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;industry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&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;companiesSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;companies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;company&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;Now we can move on to implementing the exciting bits using the &lt;a href="https://www.npmjs.com/package/openai" rel="noopener noreferrer"&gt;openai&lt;/a&gt; package:&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;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ExpandOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Instantiate the OpenAI client&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openai&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;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&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;OPENAI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch the HTML content (in plaintext) of the target URL.&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="nx"&gt;source&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;input&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;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Send the input to the model and parse the output according to the schema.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;completion&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-2024-08-06&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are an expert entity extractor that always maintains as much semantic
meaning as possible. You use inference or deduction whenever necessary to
supply missing or omitted data. Examine the provided HTML content and respond 
with a JSON object that matches the requested format.`&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="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;zodResponseFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schemaName&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Converts the Zod schema to a JSON schema.&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Extract the parsed output from the model's response.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to parse the model's output according to the schema&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&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;Finally, call the &lt;code&gt;expand&lt;/code&gt; function:&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;companies&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;expand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;source&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://www.ycombinator.com/companies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;schemaName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Companies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;companiesSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;companies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure you've set the required environment variable &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; to your OpenAI API key and run the example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx tsx ./src/example-openai.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You should get the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ companies: [] }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So why didn't it work? The problem with the page we're trying to scrape on &lt;a href="https://www.ycombinator.com/companies" rel="noopener noreferrer"&gt;https://www.ycombinator.com/companies&lt;/a&gt; is that it relies on dynamic content. Basically, the list is empty on initial page load and only gets filled once some javascript code has finished loading the data from their API. You can confirm this by inspecting the page HTML source (&lt;code&gt;CTRL + U&lt;/code&gt;). You'll notice that none of the items from the list can be found directly in the HTML source:&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.amazonaws.com%2Fuploads%2Farticles%2F99u2ypnj2ufbrfitfjfm.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.amazonaws.com%2Fuploads%2Farticles%2F99u2ypnj2ufbrfitfjfm.png" alt="HTML source of https://www.ycombinator.com/companies" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thus, we'll need to run this javascript in order to render the full companies list. A regular HTTP client like &lt;code&gt;fetch&lt;/code&gt; won't be able to do that, so we'll add browser automation to the mix. &lt;/p&gt;

&lt;p&gt;We'll create another function which loads the page in a real browser and then extracts the rendered HTML content as soon as the page finished loading. We can use &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;puppeteer&lt;/a&gt; to accomplish this:&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;fetchHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&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;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Wait until the page is fully loaded.&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;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;networkidle0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Extract the HTML content.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Remove unnecessary elements from the page to reduce the size of the content. This is absolutely necessary to prevent OpenAI token limits.&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;const&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script&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="s1"&gt;style&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="s1"&gt;link[rel="stylesheet"]&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="s1"&gt;noscript&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="s1"&gt;head&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Return the rendered HTML content.&lt;/span&gt;
    &lt;span class="k"&gt;return&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outerHTML&lt;/span&gt;&lt;span class="p"&gt;;&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;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, modify your &lt;code&gt;expand&lt;/code&gt; function as follows:&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;// ...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&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;fetchHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, run the code again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx tsx ./src/example-openai.ts

&lt;span class="o"&gt;{&lt;/span&gt;
  companies: &lt;span class="o"&gt;[&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt;
      name: &lt;span class="s1"&gt;'Airbnb'&lt;/span&gt;,
      batch: &lt;span class="s1"&gt;'W09'&lt;/span&gt;,
      url: &lt;span class="s1"&gt;'https://www.ycombinator.com/companies/airbnb'&lt;/span&gt;,
      industry: &lt;span class="s1"&gt;'Travel, Leisure and Tourism'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="o"&gt;{&lt;/span&gt;
      name: &lt;span class="s1"&gt;'Amplitude'&lt;/span&gt;,
      batch: &lt;span class="s1"&gt;'W12'&lt;/span&gt;,
      url: &lt;span class="s1"&gt;'https://www.ycombinator.com/companies/amplitude'&lt;/span&gt;,
      industry: &lt;span class="s1"&gt;'B2B'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="o"&gt;{&lt;/span&gt;
      name: &lt;span class="s1"&gt;'Coinbase'&lt;/span&gt;,
      batch: &lt;span class="s1"&gt;'S12'&lt;/span&gt;,
      url: &lt;span class="s1"&gt;'https://www.ycombinator.com/companies/coinbase'&lt;/span&gt;,
      industry: &lt;span class="s1"&gt;'Fintech'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;,
    &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🎉 Congratulations, it works! &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There's an elephant in the room. As we're dealing with a lot of tokens here (useless HTML tags also count towards token consumption) this can get quite costly. A single round-trip already cost me about &lt;code&gt;$0.14&lt;/code&gt;:&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.amazonaws.com%2Fuploads%2Farticles%2Fngwbe0szqjow3x3kcgw6.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.amazonaws.com%2Fuploads%2Farticles%2Fngwbe0szqjow3x3kcgw6.png" alt="OpenAI cost analysis" width="378" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;code&gt;$14&lt;/code&gt; every &lt;code&gt;100&lt;/code&gt; requests! Now imagine scraping a complex site...&lt;/p&gt;

&lt;p&gt;I'll address &lt;strong&gt;cost reduction strategies&lt;/strong&gt;, &lt;strong&gt;performance optimization&lt;/strong&gt; and &lt;strong&gt;other challenges&lt;/strong&gt; in &lt;a href="https://dev.to/sleeyax/turn-any-website-into-a-type-safe-api-using-ai-part-2-3cag"&gt;part 2&lt;/a&gt; of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;The prompt used in the example was taken (and slightly modified) from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cigs-tech/cigs" rel="noopener noreferrer"&gt;https://github.com/cigs-tech/cigs&lt;/a&gt; (MIT license)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hi 👋 thanks for reading! This was my first ever post on dev.to. It ended up too long so I decided to cut it in multiple parts. If you enjoyed reading my content, consider following me on &lt;a href="https://x.com/sleeyax" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; to stay in the loop ❤️.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webscraping</category>
      <category>openai</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
