<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giorgi Kobaidze</title>
    <description>The latest articles on DEV Community by Giorgi Kobaidze (@georgekobaidze).</description>
    <link>https://dev.to/georgekobaidze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55651%2F3ad144e9-ca91-4395-b73a-9a0a3d843af9.jpg</url>
      <title>DEV Community: Giorgi Kobaidze</title>
      <link>https://dev.to/georgekobaidze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/georgekobaidze"/>
    <language>en</language>
    <item>
      <title>Natural Languages Have Design Trade-offs Too: Georgian Is Dynamically Typed, English Is Statically Typed</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Thu, 15 Oct 2026 19:22:46 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/natural-languages-have-design-trade-offs-too-georgian-is-dynamically-typed-english-is-statically-emi</link>
      <guid>https://dev.to/georgekobaidze/natural-languages-have-design-trade-offs-too-georgian-is-dynamically-typed-english-is-statically-emi</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How This Started&lt;/li&gt;
&lt;li&gt;
Feature #1: Articles (or the Lack of Them)

&lt;ul&gt;
&lt;li&gt;The Upside&lt;/li&gt;
&lt;li&gt;The Downside&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Feature #2: Gender

&lt;ul&gt;
&lt;li&gt;English: He, She, and It&lt;/li&gt;
&lt;li&gt;Italian: Two Genders, Every Noun&lt;/li&gt;
&lt;li&gt;German: Three Genders&lt;/li&gt;
&lt;li&gt;Georgian: Zero Genders&lt;/li&gt;
&lt;li&gt;The Upside of Gender&lt;/li&gt;
&lt;li&gt;The Downside of Gender&lt;/li&gt;
&lt;li&gt;The Downside of Having No Gender&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Complexity Doesn't Disappear, It Moves&lt;/li&gt;
&lt;li&gt;
Sounds Familiar, Developers?

&lt;ul&gt;
&lt;li&gt;Articles vs. &lt;code&gt;var&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Gender vs. Typing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Languages That Evolve (and Ones That Mostly Don't)&lt;/li&gt;
&lt;li&gt;Every Language Is a Trade-Off&lt;/li&gt;
&lt;li&gt;Tell Me About Your Language&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How This Started
&lt;/h2&gt;

&lt;p&gt;I've been doing a little research on how good AI models are with uncommon, niche languages. And Georgian, my native language, is my main test subject. Check out the article for more details:&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22" class="crayons-story__hidden-navigation-link"&gt;Georgian Language Benchmark: Two Biggest AI Labs Clash in a Language That Doesn't Play by Anyone's Rules&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Kaggle Benchmarking Challenge Submission&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/georgekobaidze" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55651%2F3ad144e9-ca91-4395-b73a-9a0a3d843af9.jpg" alt="georgekobaidze profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/georgekobaidze" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Giorgi Kobaidze
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Giorgi Kobaidze
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
                
              
              &lt;div id="story-author-preview-content-4832848" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/georgekobaidze" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55651%2F3ad144e9-ca91-4395-b73a-9a0a3d843af9.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Giorgi Kobaidze&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22" id="article-link-4832848"&gt;
          Georgian Language Benchmark: Two Biggest AI Labs Clash in a Language That Doesn't Play by Anyone's Rules
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kagglechallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kagglechallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;72&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;span class="crayons-story__favorited"&gt;
              &lt;span class="favorited-marker"&gt;
                &lt;span&gt;
                  

                &lt;/span&gt;
                &lt;span class="hidden"&gt;
                  

                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/span&gt;
            &lt;a href="https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              34&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            20 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;p&gt;While comparing outputs, I kept noticing that the hardest parts weren't vocabulary. They were structure: things Georgian simply doesn't encode that English requires, and the other way around.&lt;/p&gt;

&lt;p&gt;And because I'm a developer, my brain immediately filed these under one label: &lt;strong&gt;language features&lt;/strong&gt;. Some features make life easier, some make it harder, and almost all of them are trade-offs.&lt;/p&gt;

&lt;p&gt;I'll compare English and Georgian, since those are the only two languages I know (for now, German is in progress). I love both of them, and both are beautiful in their own way. But they couldn't be more different. Georgian has its own alphabet, Mkhedruli, with 33 letters and no uppercase or lowercase in everyday writing. Yes, my native language is case-insensitive by design. We don't really need casing at all.&lt;/p&gt;

&lt;p&gt;Let's look at two features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature #1: Articles (or the Lack of Them)
&lt;/h2&gt;

&lt;p&gt;English marks whether a noun is &lt;strong&gt;definite&lt;/strong&gt; (a specific thing the listener already knows about) or &lt;strong&gt;indefinite&lt;/strong&gt; (something new or non-specific). That's the whole job of &lt;code&gt;the&lt;/code&gt; versus &lt;code&gt;a&lt;/code&gt;/&lt;code&gt;an&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08ktfgawvpcfy6c1t5mk.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08ktfgawvpcfy6c1t5mk.jpeg" alt="Bus leaving a station"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I saw &lt;strong&gt;a&lt;/strong&gt; bus leaving &lt;strong&gt;the&lt;/strong&gt; station. &lt;strong&gt;The&lt;/strong&gt; bus was &lt;strong&gt;a&lt;/strong&gt; white MCI 102-DL3 with silver rims and &lt;strong&gt;a&lt;/strong&gt; Greyhound badge on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Georgian has no articles at all. Structurally, the same sentence looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I saw bus leaving station. Bus was white 1999 MCI 102-DL3 with silver rims and Greyhound badge on it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In actual Georgian:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;დავინახე, როგორ გადიოდა ავტობუსი სადგურიდან. ავტობუსი იყო თეთრი MCI 102-DL3, ვერცხლისფერი დისკებით და Greyhound-ის ემბლემით.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No articles. None. Zero. Null. Undefined.&lt;br&gt;
The listener figures out definiteness from context: what was mentioned before, word order, and the situation itself. The first "bus" is obviously new information, the second one is obviously the same bus.&lt;/p&gt;

&lt;p&gt;That doesn't mean Georgian is helpless when it needs precision. It just reaches for other tools on demand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demonstratives like &lt;strong&gt;ეს&lt;/strong&gt; (this) and &lt;strong&gt;ის&lt;/strong&gt; (that) make something definite:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;this/that bus&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;In casual speech, &lt;strong&gt;ერთი&lt;/strong&gt; (one) often works like an indefinite article:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ერთი კაცი შემხვდა ("I met a (certain) man")&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The Upside
&lt;/h3&gt;

&lt;p&gt;It's simpler. Fewer words to say and write, and nothing to choose. You never have to stop and think about which article goes where, which is famously one of the hardest parts of English for people whose native languages don't have articles.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Downside
&lt;/h3&gt;

&lt;p&gt;It's not explicit, and implicit things occasionally become ambiguous. Rarely, but it happens. If someone tells you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Take bus&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Do they mean &lt;em&gt;a&lt;/em&gt; bus, any bus, or &lt;em&gt;the&lt;/em&gt; bus that's standing right in front of you? In real life, the speaker resolves it with intonation, a gesture, or by saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Take &lt;em&gt;this&lt;/em&gt; bus&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that doesn't always happen.&lt;/p&gt;

&lt;p&gt;And here's where it connects back to my AI research: when a model translates Georgian into English, it has to &lt;em&gt;insert&lt;/em&gt; information that wasn't in the source. Every "a" and "the" in the output is a guess. Usually a good one, but still a guess.&lt;/p&gt;


&lt;h2&gt;
  
  
  Feature #2: Gender
&lt;/h2&gt;

&lt;p&gt;From what I've observed, languages are absolutely obsessed with sorting things by gender. But there are actually two different things going on, and it's worth separating them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Natural gender&lt;/strong&gt;: pronouns reflect the actual gender of a person. That's English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grammatical gender&lt;/strong&gt;: &lt;em&gt;every&lt;/em&gt; noun belongs to a class, including spoons, tables, and the moon. That's Italian, German, and many others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  English: He, She, and It
&lt;/h3&gt;

&lt;p&gt;English has &lt;strong&gt;he/him/his&lt;/strong&gt;, &lt;strong&gt;she/her/hers&lt;/strong&gt;, and &lt;strong&gt;it/its&lt;/strong&gt;. That's as far as it goes, and it mostly follows reality: people get he or she, a table is "it". (Except ships, which are sometimes "she" for reasons sailors would have to explain.)&lt;/p&gt;
&lt;h3&gt;
  
  
  Italian: Two Genders, Every Noun
&lt;/h3&gt;

&lt;p&gt;Italian has masculine and feminine, and every noun has to pick one. There are patterns (nouns ending in &lt;em&gt;-o&lt;/em&gt; are usually masculine, &lt;em&gt;-a&lt;/em&gt; usually feminine), and then the exceptions show up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;la mano&lt;/strong&gt; (hand) is feminine, despite ending in &lt;em&gt;-o&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;il problema&lt;/strong&gt; (problem) is masculine, despite ending in &lt;em&gt;-a&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;il sole&lt;/strong&gt; (sun) is masculine, &lt;strong&gt;la luna&lt;/strong&gt; (moon) is feminine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Logic behind it? Nobody knows. Well, at least I don't. You just memorize them.&lt;/p&gt;
&lt;h3&gt;
  
  
  German: Three Genders
&lt;/h3&gt;

&lt;p&gt;Think that's complex? German has three: masculine (&lt;em&gt;der&lt;/em&gt;), feminine (&lt;em&gt;die&lt;/em&gt;), and neuter (&lt;em&gt;das&lt;/em&gt;). Look at your dinner table:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;der Löffel&lt;/strong&gt;: the spoon is masculine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;die Gabel&lt;/strong&gt;: the fork is feminine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;das Messer&lt;/strong&gt;: the knife is neuter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three pieces of cutlery, three genders. It gets better: &lt;strong&gt;das Mädchen&lt;/strong&gt; (the girl) is &lt;em&gt;neuter&lt;/em&gt;, because the diminutive suffix &lt;em&gt;-chen&lt;/em&gt; always makes a word neuter. Grammar beats biology. And the sun and moon? &lt;strong&gt;die Sonne&lt;/strong&gt; is feminine and &lt;strong&gt;der Mond&lt;/strong&gt; is masculine, the exact opposite of Italian. If gender were logical, the two languages would at least agree on the sky.&lt;/p&gt;

&lt;p&gt;I recently started learning German, and honestly, der/die/das is the hardest part for me. The standard advice is to never learn a noun without its article, and now I understand why.&lt;/p&gt;
&lt;h3&gt;
  
  
  Georgian: Zero Genders
&lt;/h3&gt;

&lt;p&gt;When I was a kid and started learning other languages, this was the weirdest thing of all. Why on earth are words divided by gender? That was absolute nonsense to me. Because in Georgian, there's no gender. None. Zero. Not even for people.&lt;/p&gt;

&lt;p&gt;The same pronoun covers he, she, and it: &lt;strong&gt;ის&lt;/strong&gt; (he/she/it), &lt;strong&gt;მას&lt;/strong&gt; (him/her/it, as an object), &lt;strong&gt;მისი&lt;/strong&gt; (his/her/its). The only distinction Georgian makes is between people and things, with &lt;strong&gt;ვინ&lt;/strong&gt; (who) versus &lt;strong&gt;რა&lt;/strong&gt; (what). Which is natural. Nobody asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who is that truck?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  The Upside of Gender
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;It carries information.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;My boss is really lovely, she told me I'm doing a great job during the meeting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One pronoun, and you already know your boss is a woman.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It helps you track references.&lt;/strong&gt; This is the advantage people rarely mention. In German:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Die Gabel liegt neben dem Löffel. &lt;strong&gt;Sie&lt;/strong&gt; ist schmutzig.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Sie&lt;/em&gt; can only be the fork, because the fork is feminine and the spoon isn't. In English, the same sentence is ambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fork is next to the spoon. &lt;strong&gt;It&lt;/strong&gt; is dirty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Georgian, it's equally ambiguous. Grammatical gender works like a checksum: redundant information that helps the listener connect the right pronoun to the right noun.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Downside of Gender
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What if you don't know?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tell your boss to give you some vacation days, he or she should be concerned that you're working so much.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now you have to say both, and it gets clunky fast.&lt;/p&gt;

&lt;p&gt;English's escape hatch is &lt;strong&gt;singular "they"&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a developer joins the meeting, ask them to show you where the repository is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One developer, plural pronoun. My first instinct was that this is technically incorrect, but it turns out English speakers have been doing it since the 14th century, and writers like Shakespeare and Jane Austen used it. Even better: English has done this exact trick before. &lt;strong&gt;You&lt;/strong&gt; was originally &lt;em&gt;plural only&lt;/em&gt;; the singular was &lt;strong&gt;thou&lt;/strong&gt;. Over time, "you" took over the singular too, and nobody today thinks "you are" is a grammar bug. Singular "they" is the same migration, still in progress.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The memorization tax.&lt;/strong&gt; For people, gender carries meaning. For spoons and forks, it carries none, yet Italian and German learners still have to memorize it for every single noun.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Downside of Having No Gender
&lt;/h3&gt;

&lt;p&gt;Georgian's simplicity has a price too. If you &lt;em&gt;want&lt;/em&gt; to express gender, you have to say it explicitly: man or woman, gentleman or lady. "My friend" (&lt;strong&gt;მეგობარი&lt;/strong&gt;) tells you nothing about who that friend is.&lt;/p&gt;

&lt;p&gt;And once again, this is a real problem for AI. Translating ის into English forces a choice between "he" and "she" that the original sentence never made. Machine translation systems have historically filled that gap with stereotypes: the doctor becomes "he", the nurse becomes "she". Those assumptions are never good.&lt;/p&gt;


&lt;h2&gt;
  
  
  Complexity Doesn't Disappear, It Moves
&lt;/h2&gt;

&lt;p&gt;Before anyone concludes that Georgian is "easy mode", it isn't. Far from it, it's so complex, sometimes even native speakers struggle to speak or write grammatically correct, It just stores its complexity somewhere else, mostly in verbs.&lt;/p&gt;

&lt;p&gt;A single Georgian verb can encode both the subject and the object. &lt;strong&gt;მიყვარხარ&lt;/strong&gt; means "I love you". One word, three ideas. Verbs change form across groups of tenses, and even the &lt;em&gt;case of the subject&lt;/em&gt; changes depending on the tense. Georgian dropped articles and gender, then poured all that saved effort into verbs that do an enormous amount of work.&lt;/p&gt;

&lt;p&gt;There's a principle in software for exactly this: &lt;strong&gt;Tesler's law&lt;/strong&gt;, or the law of conservation of complexity. Complexity can't be removed, only moved. Languages, it seems, follow it too.&lt;/p&gt;


&lt;h2&gt;
  
  
  Sounds Familiar, Developers?
&lt;/h2&gt;

&lt;p&gt;All of this reminded me of programming languages, because they face the exact same trade-offs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Articles vs. &lt;code&gt;var&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Georgian's missing articles map almost perfectly onto type inference in C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Bus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCI 102-DL3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// explicit: "the bus"&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Bus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCI 102-DL3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// implied, but obvious from context&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// ...is that a Bus? A Route? "take bus"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler always knows what &lt;code&gt;next&lt;/code&gt; is. The human reading the code might not. That's why many teams have a style rule along the lines of "use &lt;code&gt;var&lt;/code&gt; only when the type is obvious from the right-hand side". It's the Georgian rule exactly: implicit is great when the context makes it obvious, and when it doesn't, you say "&lt;em&gt;this&lt;/em&gt; bus" or write the type out.&lt;/p&gt;

&lt;p&gt;C# even went in the other direction with target-typed &lt;code&gt;new&lt;/code&gt;, where the left side carries the information and the right side drops it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Bus&lt;/span&gt; &lt;span class="n"&gt;bus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MCI 102-DL3"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea, different side of the sentence.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gender vs. Typing
&lt;/h3&gt;

&lt;p&gt;German grammatical gender behaves a lot like static typing. Every noun carries a tag, and everything around it (the article, the adjective endings) has to agree with that tag. It's ceremony, and sometimes it feels like noise, but mismatches get caught and references are easier to track.&lt;/p&gt;

&lt;p&gt;Georgian pronouns are closer to dynamic typing. &lt;strong&gt;ის&lt;/strong&gt; can hold anything: a person, a truck, an idea. Less ceremony, more flexibility, and meaning gets resolved at "runtime" from context.&lt;/p&gt;

&lt;p&gt;C# won't let you mix types without saying so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// error CS0019: Operator '-' cannot be applied to operands of type 'int' and 'string'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript will happily guess what you meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// 3&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;// "52"&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="c1"&gt;// ""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(To be precise, static vs. dynamic and strong vs. weak are two different axes. Python is dynamic but strongly typed: &lt;code&gt;"5" + 2&lt;/code&gt; raises a &lt;code&gt;TypeError&lt;/code&gt;. JavaScript is dynamic &lt;em&gt;and&lt;/em&gt; weakly typed, which is how you get &lt;code&gt;"52"&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;And the arbitrary part of German gender? Imagine a language that forces you to annotate every variable with a type, where the type names were assigned by rolling dice centuries ago, and nobody is allowed to refactor. That's der Löffel, die Gabel, and das Messer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Languages That Evolve (and Ones That Mostly Don't)
&lt;/h2&gt;

&lt;p&gt;Here's where natural languages and programming languages really differ.&lt;/p&gt;

&lt;p&gt;Programming languages learn from each other constantly. C# is a great example. It added generics, then lambdas and LINQ (borrowed from the functional programming world), pattern matching inspired by F# and the ML family, records, and nullable reference types to fight what Tony Hoare called his "billion-dollar mistake". And it doesn't only borrow, it lends: &lt;strong&gt;async/await&lt;/strong&gt;, introduced in C# 5, was later adopted by JavaScript, Python, Rust, and many others.&lt;/p&gt;

&lt;p&gt;Natural languages mostly borrow &lt;em&gt;words&lt;/em&gt;. English took &lt;em&gt;beef&lt;/em&gt;, &lt;em&gt;pork&lt;/em&gt;, and &lt;em&gt;mutton&lt;/em&gt; from French after the Norman conquest, while the living animals stayed &lt;em&gt;cow&lt;/em&gt;, &lt;em&gt;pig&lt;/em&gt;, and &lt;em&gt;sheep&lt;/em&gt;. Georgian borrowed &lt;strong&gt;კომპიუტერი&lt;/strong&gt; (computer) and &lt;strong&gt;ტელეფონი&lt;/strong&gt; (telephone) like nearly every other language on Earth. Sometimes a borrowed word wins so completely that people forget the original word in their own language ever existed.&lt;/p&gt;

&lt;p&gt;The structure, on the other hand, usually stays put. &lt;em&gt;Usually.&lt;/em&gt; Natural languages do refactor, just very slowly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old English had three grammatical genders&lt;/strong&gt;, just like German. By the Middle English period, they were gone. English deprecated a feature that German still ships today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thou&lt;/strong&gt; was deprecated, and &lt;strong&gt;you&lt;/strong&gt; absorbed its job, which is exactly the precedent for singular "they".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is the release process. There's no changelog and no migration guide, a breaking change takes a few centuries to roll out, and every speaker is a maintainer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every Language Is a Trade-Off
&lt;/h2&gt;

&lt;p&gt;No matter what design you come up with, there will always be trade-offs. Articles buy precision and cost effort. Gender buys information and reference tracking, and costs memorization. Dropping both buys simplicity and costs explicitness, then the complexity sneaks back in through the verbs.&lt;/p&gt;

&lt;p&gt;Every language is beautiful in its own right, and the same goes for programming languages. Learning a new one, spoken or compiled, is the best way to discover assumptions you didn't even know you had. Georgian taught me that a language can live perfectly well without "a/an/the" and "she/he". C# taught me why &lt;code&gt;var&lt;/code&gt; needs a style guide. And German is currently teaching me how insanely hard it is.&lt;/p&gt;

&lt;p&gt;As for AI: the places where languages disagree on what to encode are exactly the places where models have to guess. Which is precisely why I'm benchmarking them in Georgian.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tell Me About Your Language
&lt;/h2&gt;

&lt;p&gt;Now it's your turn. What languages do you speak, and how do they handle these features? Do they have articles? Grammatical gender? Is your native language "statically" or "dynamically" typed? Tell me in the comments. I'd love to see which languages follow the same patterns and which break all the rules.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.com/users/571315867426488330" rel="noopener noreferrer"&gt;Let's Chat on Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>writing</category>
      <category>design</category>
      <category>languages</category>
    </item>
    <item>
      <title>Georgian Language Benchmark: Two Biggest AI Labs Clash in a Language That Doesn't Play by Anyone's Rules</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:40:43 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22</link>
      <guid>https://dev.to/georgekobaidze/georgian-language-benchmark-two-biggest-ai-labs-clash-in-a-language-that-doesnt-play-by-anyones-5b22</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One Eye, Zero Excuses&lt;/li&gt;
&lt;li&gt;
What I Benchmarked

&lt;ul&gt;
&lt;li&gt;Why Georgian?&lt;/li&gt;
&lt;li&gt;What I Built&lt;/li&gt;
&lt;li&gt;The Plan vs Reality&lt;/li&gt;
&lt;li&gt;The Five Tasks&lt;/li&gt;
&lt;li&gt;How It Runs on Kaggle&lt;/li&gt;
&lt;li&gt;Obstacles and Workarounds&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Models Tested&lt;/li&gt;
&lt;li&gt;
Findings

&lt;ul&gt;
&lt;li&gt;Level by Level&lt;/li&gt;
&lt;li&gt;Finding 1: Understanding Georgian Is Solved. Producing It Isn't.&lt;/li&gt;
&lt;li&gt;Finding 2: Numbers Are the Final Boss&lt;/li&gt;
&lt;li&gt;Finding 3: The Mistakes Are Very Georgian&lt;/li&gt;
&lt;li&gt;Finding 4: Georgian Costs More Tokens for Claude&lt;/li&gt;
&lt;li&gt;Finding 5: Cheap Can Be Smart&lt;/li&gt;
&lt;li&gt;Side Quest: The Uninvited Guest&lt;/li&gt;
&lt;li&gt;What It All Cost&lt;/li&gt;
&lt;li&gt;The Dashboard&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;What's Next&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
My Benchmark

&lt;ul&gt;
&lt;li&gt;Wrapping Up&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One Eye, Zero Excuses
&lt;/h2&gt;

&lt;p&gt;I really wanted to record a video for this one, just like I always do for my challenges. But yesterday I had a small surgery on my eyelid, and right now one of my eyes is swollen and patched up. Here's the current state:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvs232l027q6in4x7vc2a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvs232l027q6in4x7vc2a.jpg" alt="Me with a patched-up eye, pirate style" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Yep. I look like a pirate now. All I'm missing is a parrot and a cool nickname (suggestions are welcome in the comments).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But don't worry (if you were going to): I'm totally fine and my eyesight is still eagle-like from both eyes.&lt;/strong&gt; It was just a small bug fix in the eyelid module. The eye itself is still in production and running smoothly.&lt;/p&gt;

&lt;p&gt;That said, finishing a project with one-eye-patched-up is way harder than it sounds. Depth perception? Gone. Reading code? Awful. I don't believe in curses, but every time I join a challenge, something happens. If someone out there cursed me, please, just lift it. Give me a break! I just wanna work goddammit!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By the way shout out to my brother for chauffeuring me home from the hospital, because one-eye-patched-up driving felt like a really bad idea.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But a deadline is a deadline, and professionals deliver, no matter what.&lt;/strong&gt; So I gave myself a little pep talk: &lt;em&gt;"Dude, Michael Bisping became a UFC champion with basically one working eye. You can finish a coding project. No excuses, you're not lame."&lt;/em&gt; And so I did.&lt;/p&gt;

&lt;p&gt;Let me tell you what I built.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Why Georgian?
&lt;/h3&gt;

&lt;p&gt;I benchmarked OpenAI's and Anthropic's models on how well they understand and produce Georgian. The whole process turned into a real adventure, but before we get to that, let me tell you why I picked this topic.&lt;/p&gt;

&lt;h4&gt;
  
  
  One of a Kind and Tough as Nails
&lt;/h4&gt;

&lt;p&gt;Georgian is one of the oldest languages in the world. It has its own alphabet and doesn't belong to any of the major language families. It's totally unique. Phonetically, it sometimes kind of sounds like Italian, because both languages are obsessed with vowels, but that's where the similarity ends. And vowel obsession doesn't mean weak consonants. Quite the contrary: Georgian has consonants most people have never heard, stacked in clusters that make everyone struggle (try saying გვფრცქვნი, with eight consonants in a row; it basically means "you peel us", and yes, I know it sounds like nonsense, but technically that word exists). It's an incredibly rich language.&lt;/p&gt;

&lt;p&gt;Grammatically, it's ridiculously tough. I take it for granted, because first, I'm Georgian, and second, I'm a language nerd (even when I'm learning other languages). I know Georgian grammar well, but even most Georgians don't know every detail of the Georgian grammar, and I know almost nobody who speaks it without a single mistake. Yes, it's that deep and complex.&lt;/p&gt;

&lt;p&gt;Even I, a grammar nerd, had my dataset checked by a language specialist to make sure I got things right. And I was so happy when she came back with only 3 or 4 minor corrections.&lt;/p&gt;

&lt;h4&gt;
  
  
  Straight Out of a Fantasy Game ("შენ გგონია მომიგე? არა, ცდები!")
&lt;/h4&gt;

&lt;p&gt;Georgian has a special charm of its own. The letters are round-shaped and beautiful, and it looks like an elvish script from some fantasy movie or game. And speaking of games, it was actually used in The Witcher 3's Hearts of Stone expansion. Remember the moment you defeat &lt;a href="https://witcher.fandom.com/wiki/Gaunter_O%27Dimm" rel="noopener noreferrer"&gt;Gaunter O'Dimm&lt;/a&gt;? Somewhere in the middle of his short speech, he says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"შენ გგონია მომიგე? არა, ცდები!"&lt;br&gt;
("You think you've defeated me? No, you're wrong!")&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Then he goes on in some other languages. The developers wanted him to speak some ancient, otherworldly tongue, so instead of some gibberish, they picked a few real languages that fit the vibe. Georgian was one of them.&lt;/p&gt;

&lt;p&gt;Alright, enough flexing about how awesome my first language is. Let's get to the technical part.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Built
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Georgian Language Benchmark&lt;/strong&gt; is a set of five tasks on &lt;a href="https://www.kaggle.com/benchmarks" rel="noopener noreferrer"&gt;Kaggle Benchmarks&lt;/a&gt; that test how well AI models handle Georgian: not just reading it, but actually producing it correctly. &lt;strong&gt;Every single dataset was written by hand by me, from scratch.&lt;/strong&gt; No machine translation, no generated text.&lt;/p&gt;

&lt;p&gt;It took some time and effort... by "some" I mean an ungodly amount of time and effort.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Plan vs Reality
&lt;/h3&gt;

&lt;p&gt;My original plan was, let's say, ambitious. Nine dimensions of Georgian (numbers, verbs, translation, chat-style Georgian written in Latin letters, proverbs, date and time, grammar judgment, even images), and &lt;strong&gt;all 41 models&lt;/strong&gt; in Kaggle's catalog. I estimated a full run at around &lt;strong&gt;$345&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I was totally fine with that. The whole point was to build something comprehensive, not to save money. And let's be real: if I won, the prize money would totally make up for it.&lt;/p&gt;

&lt;p&gt;But...&lt;/p&gt;

&lt;p&gt;Then I found out how Kaggle's model credits actually work: you get &lt;strong&gt;$10 per day&lt;/strong&gt;, for free, and that's it. You can't top it up with your own money. I literally went looking for the "add funds" button and it doesn't exist. (There's a research grant program, but not on a challenge deadline.)&lt;/p&gt;

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

&lt;p&gt;Oh I was so frustrated and mad at myself for not researching Kaggle's rules properly. I had to dramatically change my approach.&lt;/p&gt;

&lt;p&gt;So $10 a day it was. And that one constraint ended up shaping (or, to be more precise, re-shaping) almost every decision in this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;41 models became 8.&lt;/strong&gt; Two vendors, four levels, real pairs instead of a crowd.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9 dimensions became 5.&lt;/strong&gt; Vision, proverbs, date and time and grammar judgment were cut. The chat-Georgian task (45 items) was built, then dropped too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The datasets got trimmed&lt;/strong&gt; to the items that actually separate models (more on that below). Nothing was thrown away: the full sets live in the repo's &lt;code&gt;archive/&lt;/code&gt; folder for anyone who wants to run them outside Kaggle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the flip side, the constraint made the benchmark better. Fewer, sharper items, and a fair fight instead of a crowded leaderboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Five Tasks
&lt;/h3&gt;

&lt;p&gt;Here's what the models had to face. Every example below is a real item from the benchmark.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Numbers: Counting in Twenties
&lt;/h4&gt;

&lt;p&gt;Georgian counts in twenties. 99 is &lt;strong&gt;ოთხმოცდაცხრამეტი&lt;/strong&gt;, literally "four twenties and nineteen". On top of that come ordinals, fractions, decimals and numbers all the way up to 10³⁹, in both directions: digits to words and words to digits.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;44th, in words&lt;/td&gt;
&lt;td&gt;ორმოცდამეოთხე&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;თვრამეტი (18), its ordinal in digits&lt;/td&gt;
&lt;td&gt;მე-18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000000, in words&lt;/td&gt;
&lt;td&gt;მილიონი&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That middle one is sneakier than it looks. A Georgian ordinal in digits keeps its prefix in front (&lt;strong&gt;მე-18&lt;/strong&gt;) when the word starts with მე-, and takes a suffix (&lt;strong&gt;21-ე&lt;/strong&gt;) when it doesn't. Hold that thought, it comes back in the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;158 questions&lt;/strong&gt;, scored by exact match (with &lt;code&gt;|&lt;/code&gt; separating alternative correct answers).&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Verbs: Who Did What to Whom?
&lt;/h4&gt;

&lt;p&gt;A single Georgian verb can carry a whole sentence: who does it, to whom, and when. &lt;strong&gt;მიყვარხარ&lt;/strong&gt; means "I love you" (awww, by the way), all in one word. The task has three kinds of questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who to whom:&lt;/strong&gt; read a verb form and pick who acts on whom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form:&lt;/strong&gt; build the right verb form for given persons and tense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preverb:&lt;/strong&gt; pick the prefix that gives the right meaning in context.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,kind,input,choices,expected
verbs-000,who_to_whom,მიყვარხარ,A) ის → შენ|B) მე → ის|C) შენ → მე|D) მე → შენ,D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;50 items&lt;/strong&gt;, multiple choice with letter answers or exact match.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Translation: Georgian to English
&lt;/h4&gt;

&lt;p&gt;Twelve short paragraphs packed with idioms and tricky verbs, to be translated into English. For example, &lt;strong&gt;ხელი სთხოვა&lt;/strong&gt; literally means "asked for (her) hand", but what it really means is "proposed".&lt;/p&gt;

&lt;p&gt;Since there's more than one good translation, a &lt;strong&gt;judge model&lt;/strong&gt; compares the meaning (not the wording) with my reference translation, on a 5-level scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 = same meaning, nothing missing, wrong or added
3 = a small slip (a minor detail, tense or nuance), the main meaning intact
2 = partly right: one important part is wrong or missing
1 = only fragments right, the overall meaning is lost
0 = wrong, unrelated, or not English
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One fun detail: Georgian has no grammatical gender, so the judge is told explicitly that "he", "she" or "they" are all fine when the Georgian text doesn't reveal it. The judge is always the same model, &lt;strong&gt;Gemini 3 Flash&lt;/strong&gt;, which is not one of the models being tested.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Cloze: Fill the Gaps
&lt;/h4&gt;

&lt;p&gt;Two short articles, one about the history of computers and one about the Roman Empire, with &lt;strong&gt;25 gaps each&lt;/strong&gt;. The model gets a list of 30 words in their dictionary form, including 5 that don't belong anywhere (like "television", "banana" or "airplane" in an article about Rome). It has to pick the right word for each gap &lt;strong&gt;and&lt;/strong&gt; put it in the right grammatical form.&lt;/p&gt;

&lt;p&gt;Example: ძველ &lt;strong&gt;[1]&lt;/strong&gt; ... → &lt;strong&gt;დროში&lt;/strong&gt; ("in old times")&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;50 gaps&lt;/strong&gt;, scored one by one.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Correction: Fix It, Don't Break It
&lt;/h4&gt;

&lt;p&gt;Paragraphs with planted grammar, spelling and punctuation errors. The model must fix them &lt;strong&gt;without touching anything that's already correct&lt;/strong&gt;. Here's one sentence from a real item:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ მთელი ოჯახი &lt;strong&gt;მაგიდაზე დაჯდა&lt;/strong&gt; და ერთად &lt;strong&gt;ვივახშმედ&lt;/strong&gt;.&lt;br&gt;
✅ მთელი ოჯახი &lt;strong&gt;მაგიდას შემოუსხდა&lt;/strong&gt; და ერთად &lt;strong&gt;ვივახშმეთ&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Scoring is the part I'm most proud of. It counts word-level differences from the correct text before and after the model's edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# errors_before: how far the input is from the correct text
# errors_after:  how far the model's answer is from the correct text
&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&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="n"&gt;errors_before&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;errors_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;errors_before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix 3 of 4 errors and you get 0.75. But "fix" a correct word into a wrong one, and that's a new error, which pulls your score down. Rewriting the whole paragraph in your own style doesn't pay off either. Over-correction is punished, just like a strict teacher would do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;17 paragraphs.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Writing It All by Hand
&lt;/h4&gt;

&lt;p&gt;All of this data came out of my own head, one item at a time: 186 number rows, 115 verb items, 47 translation paragraphs, 51 correction paragraphs, 45 chat texts and two cloze articles. Then a language specialist reviewed everything, and I applied her corrections.&lt;/p&gt;

&lt;p&gt;But with $10 a day (allowed by Kaggle), I couldn't afford to run all of it on eight models. So I trimmed it. For numbers and verbs, I used a &lt;strong&gt;probe model&lt;/strong&gt;: I ran a cheap model over the full sets, kept every item it got wrong, and dropped part of the ones it found too easy. Big numbers stayed untouched. For translation, I kept the 12 paragraphs with the most Georgian-specific traps, I picked the correction paragraphs by hand, and the cloze texts stayed in full.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Written&lt;/th&gt;
&lt;th&gt;Kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;186 rows&lt;/td&gt;
&lt;td&gt;82 rows (158 questions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verbs&lt;/td&gt;
&lt;td&gt;115&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translation&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloze&lt;/td&gt;
&lt;td&gt;2 texts&lt;/td&gt;
&lt;td&gt;2 texts (50 gaps)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correction&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That means &lt;strong&gt;287 scored items per model&lt;/strong&gt;. And yes, it means the benchmark is hard on purpose. The absolute scores are lower than they'd be on an easy set, and the ranking is what matters.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How It Runs on Kaggle
&lt;/h3&gt;

&lt;p&gt;Kaggle Benchmarks lets you write a task as plain Python with the &lt;a href="https://pypi.org/project/kaggle-benchmarks/" rel="noopener noreferrer"&gt;&lt;code&gt;kaggle-benchmarks&lt;/code&gt;&lt;/a&gt; library. Kaggle runs it on any model from its catalog and stores every call: the prompt, the answer, the tokens and the cost.&lt;/p&gt;

&lt;p&gt;Every task in my benchmark has the same shape: a small &lt;strong&gt;item task&lt;/strong&gt; that asks one question and scores it, and an &lt;strong&gt;outer task&lt;/strong&gt; that runs it over the whole dataset and reports one score. Here's the numbers task, slightly simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@kbench.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glb-numbers-item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store_task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;number_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROMPTS&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;kbench&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expectation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;


&lt;span class="nd"&gt;@kbench.task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;glb-numbers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;number_item&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="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;evaluation_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_failure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;continue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completed_runs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kbench&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data lives in a Kaggle dataset, and the whole workflow runs from the terminal with the Kaggle CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scripts/push_data.sh &lt;span class="s2"&gt;"what changed"&lt;/span&gt;                  &lt;span class="c"&gt;# upload data/ as a Kaggle dataset&lt;/span&gt;
scripts/push_task.sh glb-verbs tasks/glb_verbs.py    &lt;span class="c"&gt;# push a task&lt;/span&gt;
kaggle b t run glb-verbs &lt;span class="nt"&gt;-m&lt;/span&gt; gpt-5.4-2026-03-05 &lt;span class="nt"&gt;--wait&lt;/span&gt;
kaggle b t download glb-verbs &lt;span class="nt"&gt;-m&lt;/span&gt; gpt-5.4-2026-03-05  &lt;span class="c"&gt;# every answer, token and cost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00px0y6jrc8bhydeiwk6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00px0y6jrc8bhydeiwk6.png" alt="Verbs" width="799" height="305"&gt;&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9x4sdvff6eoe16npmi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9x4sdvff6eoe16npmi5.png" alt="Task Run" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Obstacles and Workarounds
&lt;/h3&gt;

&lt;p&gt;This is where the adventure really started. Here's everything that tried to stop me, and how I got around it.&lt;/p&gt;

&lt;h4&gt;
  
  
  Obstacle 1: Kaggle Reserves the Worst Case
&lt;/h4&gt;

&lt;p&gt;Before every single call, Kaggle reserves the &lt;strong&gt;maximum possible cost&lt;/strong&gt; of that call: the model's maximum output length times its price. If that doesn't fit into your remaining quota, the call is refused, even if the real answer would cost a fraction of a cent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max estimated cost of operation ($1.60195) exceeds your available quota
(based on max_output_tokens)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first top-tier runs died exactly like this: many of the calls were refused, with plenty of real money still left in the quota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; a generous but sane output limit, and only two calls in parallel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The longest answer seen in testing was 26,600 tokens.
&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;number_item&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="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;evaluation_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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;h4&gt;
  
  
  Obstacle 2: Failed Calls Look Like Wrong Answers
&lt;/h4&gt;

&lt;p&gt;When a call fails (quota gone, API hiccup), the item just counts as wrong. So a run that ran out of money halfway would happily report a score of 0.3 and put it on the leaderboard as if the model were bad at Georgian. That's not a score, that's a lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; an error guard in every task. A few isolated failures (after 3 retries) count as wrong, but if more than 5% of the calls fail, the task stops and reports nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;errored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errored_runs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errored&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;errored&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; items failed with errors; no score reported.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It paid off right away: when the worst-case reservation (Obstacle 1) killed my first top-tier runs, both stopped cleanly without a fake score.&lt;/p&gt;

&lt;h4&gt;
  
  
  Obstacle 3: Some Models Think by Default
&lt;/h4&gt;

&lt;p&gt;Some models "think" before answering: they reason privately in hidden text, and you pay for every hidden token. One of my test runs wrote about &lt;strong&gt;770,000 hidden tokens&lt;/strong&gt; to answer 251 questions, most of which had one-word answers. GPT-5.5 wrote about 750 hidden tokens to answer a single multiple-choice question with one letter.&lt;/p&gt;

&lt;p&gt;Worse, it wasn't fair: some of my models thought by default and some didn't. Comparing a thinking model with a non-thinking one says more about the setting than about the model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; the same conditions for everyone. &lt;strong&gt;No thinking&lt;/strong&gt;, every model answers directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reasoning&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra_api_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OUTPUT_LIMIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without thinking, a full run costs about 99% less. And every model plays by the same rules.&lt;/p&gt;

&lt;h4&gt;
  
  
  Obstacle 4: The Model That Won't Stop Thinking
&lt;/h4&gt;

&lt;p&gt;When I got to the newest pair, GPT-6 Astra simply refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reasoning_effort does not support 'none' with this model.
Supported values are: 'low', 'medium', 'high', and 'xhigh'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; the newest pair gets its own rule. Both Claude Opus 5 and GPT-6 Astra think at the &lt;strong&gt;lowest level ("low")&lt;/strong&gt;, so they compete on equal terms with each other. They're marked ⚡ / &lt;code&gt;LOW&lt;/code&gt; everywhere in the dashboard, so nobody compares them with the no-thinking models by accident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LOW_THINKING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-6-astra&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reasoning_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;LOW_THINKING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Obstacle 5: Every Push Runs the Task
&lt;/h4&gt;

&lt;p&gt;Every time you push a new version of a task, Kaggle runs it once on its default model, &lt;strong&gt;Gemini 3.7 Flash&lt;/strong&gt;, and that run is billed to your quota too. Each re-push of the full benchmark cost me between $0.45 and $1.00. By the end, these automatic runs had cost me almost &lt;strong&gt;$3&lt;/strong&gt;, more than half of what the final comparison runs cost. Lesson learned: test locally, push rarely.&lt;/p&gt;

&lt;h4&gt;
  
  
  Obstacle 6: The Small Stuff
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dataset path moves.&lt;/strong&gt; Kaggle mounts the dataset somewhere under &lt;code&gt;/kaggle/input&lt;/code&gt;, but the exact path varies. So every task searches for its file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;data_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;kaggle_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/kaggle/input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kaggle_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kaggle_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rglob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No automatic retries.&lt;/strong&gt; Nested evaluations can't retry failed items, so every task retries API errors itself, 3 times with a 10-second pause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials expire.&lt;/strong&gt; For local test runs, Kaggle's model proxy credentials expire every couple of hours. &lt;code&gt;kaggle b auth -y&lt;/code&gt; refreshes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look-alike characters.&lt;/strong&gt; A hyphen and an en dash look the same but aren't, so answers are normalized before comparison (Unicode normalization, dash-like characters, extra spaces).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I put eight models from Anthropic and OpenAI through the benchmark, paired level by level, under identical conditions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Anthropic&lt;/th&gt;
&lt;th&gt;OpenAI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Light&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Decent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;GPT-5.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Top&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Opus 4.8&lt;/td&gt;
&lt;td&gt;GPT-5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Newest&lt;/strong&gt; ⚡&lt;/td&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;⚡ The newest pair is a special case: as you saw in Obstacle 4, GPT-6 Astra simply refuses to stop thinking, so both models of that pair think at "low".&lt;/p&gt;

&lt;p&gt;Here are the rules every model played by:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No thinking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every model answers directly. Mixing thinking and non-thinking models wouldn't be fair.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;One marked exception&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPT-6 Astra can't switch thinking off, so the newest pair both think at "low". Marked ⚡ everywhere.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Comparable pairs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Each level pairs an Anthropic and an OpenAI model of the same class.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Same prompts, same code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;English instructions, Georgian content, one task version for everyone.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A fixed judge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Translations are judged by Gemini 3 Flash, which isn't one of the tested models.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partial credit where it's fair&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Correction rewards every fixed error and penalizes every correct word broken.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Failures never look like scores&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Above 5% failed calls, a task reports no score at all.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;Drumroll, please. 🥁&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Numbers&lt;/th&gt;
&lt;th&gt;Verbs&lt;/th&gt;
&lt;th&gt;Translation&lt;/th&gt;
&lt;th&gt;Cloze&lt;/th&gt;
&lt;th&gt;Correction&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Average&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;td&gt;0.41&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;0.39&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.76&lt;/td&gt;
&lt;td&gt;0.44&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.62&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4&lt;/td&gt;
&lt;td&gt;0.46&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.66&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.71&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.8&lt;/td&gt;
&lt;td&gt;0.54&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;0.68&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.75&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.5&lt;/td&gt;
&lt;td&gt;0.51&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;0.88&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.74&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5 ⚡&lt;/td&gt;
&lt;td&gt;0.65&lt;/td&gt;
&lt;td&gt;0.84&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.77&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.82&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$1.22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra ⚡&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;0.96&lt;/td&gt;
&lt;td&gt;0.82&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$1.73&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h3&gt;
  
  
  Level by Level
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Light:&lt;/strong&gt; GPT-5.4 mini beats Claude Haiku 4.5, &lt;strong&gt;0.60 vs 0.48&lt;/strong&gt;, and costs half as much.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decent:&lt;/strong&gt; GPT-5.4 beats Claude Sonnet 4.6, &lt;strong&gt;0.71 vs 0.62&lt;/strong&gt;, again at about half the price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top:&lt;/strong&gt; Claude Opus 4.8 edges out GPT-5.5, &lt;strong&gt;0.75 vs 0.74&lt;/strong&gt;. That's basically a tie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newest ⚡:&lt;/strong&gt; GPT-6 Astra beats Claude Opus 5, &lt;strong&gt;0.90 vs 0.82&lt;/strong&gt;, the best score of the whole benchmark. It's ahead on four of the five tasks, with the biggest jump on numbers (0.83 vs 0.65).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Final tally: &lt;strong&gt;OpenAI wins 3 of the 4 levels&lt;/strong&gt;, and Anthropic's only win, at the top level, is basically a tie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One disclaimer: this is my dataset, my rules. Test other areas of Georgian and the results might shift a bit. That said, I tried to keep the tasks as broad and general as I could.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding 1: Understanding Georgian Is Solved. Producing It Isn't.
&lt;/h3&gt;

&lt;p&gt;Look at the translation column: &lt;strong&gt;0.96 to 1.00 for every model&lt;/strong&gt;. Every one of them understands Georgian well enough to translate it into English almost perfectly.&lt;/p&gt;

&lt;p&gt;Now look at the other four columns. The moment a model has to &lt;strong&gt;produce&lt;/strong&gt; correct Georgian (build a number, form a verb, inflect a word, fix a sentence), the scores drop hard. Without thinking, even the best model (Claude Opus 4.8) gets almost half of the number questions wrong. Only GPT-6 Astra, with its small thinking budget, gets above 0.80.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding 2: Numbers Are the Final Boss
&lt;/h3&gt;

&lt;p&gt;Numbers are the hardest task for 6 of the 8 models, and close to the bottom for the other two. And inside it, the gap is wild:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question type&lt;/th&gt;
&lt;th&gt;Range across models&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Words → digits, cardinal&lt;/td&gt;
&lt;td&gt;0.74 to 0.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Words → digits, ordinal&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.04 to 0.92&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huge numbers, 10¹⁵ to 10³⁹&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.06 to 0.53&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reading a Georgian number into digits: fine. Writing its ordinal in digits: disaster. GPT-6 Astra is the big exception there with 0.92, while the next best model gets 0.62. Remember &lt;strong&gt;მე-18&lt;/strong&gt; from earlier? Most models wrote the suffix form instead, because that's the pattern they've seen more often. With &lt;strong&gt;მე-40000&lt;/strong&gt; (forty-thousandth), only 2 of the 8 models got it right.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Finding 3: The Mistakes Are Very Georgian
&lt;/h3&gt;

&lt;p&gt;My favorite part of the whole project is reading the wrong answers. They're not random. They're what you'd get if you tried to speak Georgian with grammar borrowed from somewhere else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"I fell in love with you."&lt;/strong&gt; In Georgian, falling in love is an inversion verb: the one who loves is marked like an indirect object, so it's &lt;strong&gt;შემიყვარდი&lt;/strong&gt;, literally "you became loved to me". Only GPT-6 Astra got it right. The others mostly built an ordinary transitive verb instead (შეგიყვარე).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We sat."&lt;/strong&gt; Georgian uses a different verb root for sitting when the subject is plural: "I sat" is ვიჯექი, but "we sat" is &lt;strong&gt;ვისხედით&lt;/strong&gt;. I planted ვიჯექით (a mix of the two) in a correction paragraph. &lt;strong&gt;Not a single model fixed it.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Who wrote it?"&lt;/strong&gt; In &lt;strong&gt;დაგიწერია&lt;/strong&gt;, the გ- marks "you" as the writer: "you have (apparently) written it". In the Georgian perfect, the markers flip. Only 2 of the 8 models got it right; the rest scattered across every other reading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One million.&lt;/strong&gt; It's simply &lt;strong&gt;მილიონი&lt;/strong&gt;. Most models write ერთი მილიონი ("one million") instead.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Finding 4: Georgian Costs More Tokens for Claude
&lt;/h3&gt;

&lt;p&gt;Same prompts, same Georgian text, very different token counts. Claude models used &lt;strong&gt;2 to 2.6 times more input tokens&lt;/strong&gt; than GPT models for exactly the same questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Claude, input tokens per call&lt;/th&gt;
&lt;th&gt;GPT, input tokens per call&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;272&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verbs&lt;/td&gt;
&lt;td&gt;310&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloze&lt;/td&gt;
&lt;td&gt;1,412&lt;/td&gt;
&lt;td&gt;699&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correction&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;159&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Part of that is each vendor's answer-format instructions, and part is how each tokenizer splits Georgian script. In practice it means a Georgian prompt costs a Claude user noticeably more than the price per token suggests.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Finding 5: Cheap Can Be Smart
&lt;/h3&gt;

&lt;p&gt;Cost per correct answer, on the tasks scored right or wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5.4 mini:&lt;/strong&gt; 0.03 cents per correct answer. The bargain of the benchmark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 5 ⚡:&lt;/strong&gt; 0.55 cents per correct answer, about 17 times the price of GPT-5.4 mini.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-6 Astra ⚡:&lt;/strong&gt; 0.67 cents per correct answer. The best score, but about 21 times the price of GPT-5.4 mini per correct answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Side Quest: The Uninvited Guest
&lt;/h3&gt;

&lt;p&gt;Remember Gemini 3.7 Flash, the model Kaggle runs automatically on every push? It's not part of my comparison, but it took the test every time I pushed, so I have its scores too. And it quietly scored around &lt;strong&gt;0.89 on average&lt;/strong&gt;, above every model in my table except GPT-6 Astra (0.90).&lt;/p&gt;

&lt;p&gt;The catch: even with thinking set to "none", it kept writing about &lt;strong&gt;480 output tokens per number question&lt;/strong&gt;, while the models without thinking wrote about 16. So it seems to keep thinking a little no matter what you tell it. Which leads to a fun takeaway: for a language like Georgian, &lt;strong&gt;thinking seems to matter more than model size&lt;/strong&gt;. The two best models in my table, GPT-6 Astra and Claude Opus 5, both think at "low" too.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It All Cost
&lt;/h3&gt;

&lt;p&gt;The whole project ran on Kaggle's free daily credits. Total spend across all the testing, probing and final runs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Final comparison runs&lt;/td&gt;
&lt;td&gt;$4.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kaggle's automatic push runs&lt;/td&gt;
&lt;td&gt;$2.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Earlier runs and cost probes&lt;/td&gt;
&lt;td&gt;$2.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local test runs&lt;/td&gt;
&lt;td&gt;$3.46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$13.84&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yes, the automatic push runs alone cost more than half as much as the actual benchmark. And spreading it over several days was the only way to stay under $10 a day.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Dashboard
&lt;/h3&gt;

&lt;p&gt;Tables are nice, but I wanted something you can actually explore. So all the results live on an interactive dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overview:&lt;/strong&gt; the level-by-level showdown, the task matrix and the key findings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost &amp;amp; tokens:&lt;/strong&gt; what each model paid per task, the token gap, cost per correct answer and the full project spend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tasks:&lt;/strong&gt; numbers by question type, verbs by type, correction split into fixed / missed / broken, and every cloze gap for every model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Examples:&lt;/strong&gt; real questions with every model's actual answer and an explanation of the mistake&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Method:&lt;/strong&gt; where the data comes from, how each task is scored, the conditions and the limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a single static HTML page with no framework, no build step and no chart library, generated by a Python script straight from Kaggle's run files. So the dashboard and this article can never disagree.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://georgekobaidze.github.io/georgian-language-benchmarks/" rel="noopener noreferrer"&gt;georgekobaidze.github.io/georgian-language-benchmarks&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;p&gt;Let's be honest about what this benchmark can and can't tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard by design.&lt;/strong&gt; The sets keep mostly items a model got wrong, so the absolute scores are lower than they'd be on an easy set. The ranking is the meaningful part.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small sets.&lt;/strong&gt; Some tasks have only 12 to 17 items, so a difference of a few points isn't significant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One run per model.&lt;/strong&gt; Re-runs of the same model differed by about ±0.03 per task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The judge is a model.&lt;/strong&gt; Translation scores depend on Gemini 3 Flash's judgment of meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The token gap includes format overhead&lt;/strong&gt;, not only the tokenizer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bigger sets.&lt;/strong&gt; The full, untrimmed datasets are already in the repo's &lt;code&gt;archive/&lt;/code&gt; folder, ready for a run without the daily cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More vendors.&lt;/strong&gt; Google, xAI and the open models, once the budget allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thinking vs no thinking.&lt;/strong&gt; The Gemini side quest begs for a proper experiment: the same model with and without thinking, on the same items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More Georgian.&lt;/strong&gt; Native speakers are very welcome to contribute new items, corrections and alternative answers.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📊 &lt;strong&gt;Dashboard:&lt;/strong&gt; &lt;a href="https://georgekobaidze.github.io/georgian-language-benchmarks/" rel="noopener noreferrer"&gt;georgekobaidze.github.io/georgian-language-benchmarks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🏆 &lt;strong&gt;Benchmark on Kaggle:&lt;/strong&gt; &lt;a href="https://www.kaggle.com/benchmarks/georgekobaidze/georgian-language-benchmark" rel="noopener noreferrer"&gt;kaggle.com/benchmarks/georgekobaidze/georgian-language-benchmark&lt;/a&gt; (the leaderboard only shows the models that ran on the latest version of the tasks, where the newest pair ran with "low" thinking; all 8 models are on the dashboard)&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/georgekobaidze/georgian-language-benchmarks" rel="noopener noreferrer"&gt;github.com/georgekobaidze/georgian-language-benchmarks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;I started this with patches on one eye, and ended it with a benchmark, a dashboard and a lot of respect for how hard my own language is. The models understand Georgian almost perfectly. Producing it correctly is a whole different story, and that's exactly the gap this benchmark measures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And where the hell is my PhD?&lt;/strong&gt; I've seen people (multiple of them) getting PhDs for exponentially less, and I'm not even kidding. I'm dead serious.&lt;/p&gt;

&lt;p&gt;If you speak Georgian, go check the Examples tab and tell me which mistake made you laugh the most. And if you have a good pirate nickname for me, you know where the comments are.&lt;/p&gt;

&lt;p&gt;If you don't speak Georgian and are planning to learn it, think twice and ask yourself if you're fully committed, because it can take a good 3 to 4 years (or more) of absolutely intense learning, and even then you'll be barely average. Reach out and let me know if you need any tips.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.com/users/571315867426488330" rel="noopener noreferrer"&gt;Let's Chat on Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>I Turned My GitHub Profile Into a Cyberpunk Console With a City Built From My Contributions</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Thu, 01 Oct 2026 18:02:38 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c</link>
      <guid>https://dev.to/georgekobaidze/i-turned-my-github-profile-into-a-cyberpunk-console-with-a-city-built-from-my-contributions-h4c</guid>
      <description>&lt;p&gt;&lt;em&gt;I just wanted a custom profile. I completely overcooked it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Don't feel like reading? Fair. But you're going to miss a lot of interesting insights. Anyway, here's my profile link:&lt;/em&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/georgekobaidze" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F24773902%3Fv%3D4%3Fs%3D400" height="417" class="m-0" width="417"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer" class="c-link"&gt;
            georgekobaidze (Giorgi Kobaidze) · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Software Engineer | Engineering Manager | Technical Writer |🏆2x Hackathon Winner | ☕Enjoyer - georgekobaidze
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
I Wanted a Simple Custom GitHub Profile. But Things Got Out of Hand Quickly

&lt;ul&gt;
&lt;li&gt;23:00. Fully Cooked.&lt;/li&gt;
&lt;li&gt;Random Doesn't Mean Bad&lt;/li&gt;
&lt;li&gt;Why Do I Keep Doing This to Myself?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
The Contribution City

&lt;ul&gt;
&lt;li&gt;The City That Represents Me&lt;/li&gt;
&lt;li&gt;The Moment It Clicked&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
The Cyberpunk Console

&lt;ul&gt;
&lt;li&gt;Because Why Not?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Welcome to the Sandbox

&lt;ul&gt;
&lt;li&gt;No JavaScript. No CSS. No Fun?&lt;/li&gt;
&lt;li&gt;The Loophole: An SVG Is Just an Image&lt;/li&gt;
&lt;li&gt;Okay, But the Sandbox Has Rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
One Console, 23 Images

&lt;ul&gt;
&lt;li&gt;The Problem: Images Don't Hold Hands&lt;/li&gt;
&lt;li&gt;Slicing the Console&lt;/li&gt;
&lt;li&gt;The Tricks That Make It Seamless&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Building the City

&lt;ul&gt;
&lt;li&gt;From Grid to Skyline&lt;/li&gt;
&lt;li&gt;How Tall Is a Day?&lt;/li&gt;
&lt;li&gt;Lights On&lt;/li&gt;
&lt;li&gt;Green or Blue?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
It Updates Itself Every Day

&lt;ul&gt;
&lt;li&gt;A Robot on Daily Duty&lt;/li&gt;
&lt;li&gt;Talking to GitHub: One Query Instead of Twenty&lt;/li&gt;
&lt;li&gt;The All-Time Problem&lt;/li&gt;
&lt;li&gt;Talking to DEV&lt;/li&gt;
&lt;li&gt;When an API Has a Bad Day&lt;/li&gt;
&lt;li&gt;The README Edits Itself (Carefully)&lt;/li&gt;
&lt;li&gt;Secrets That Stay Secret&lt;/li&gt;
&lt;li&gt;Why 12:17?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Was It Worth It?

&lt;ul&gt;
&lt;li&gt;The Damage Report&lt;/li&gt;
&lt;li&gt;So... Was It?&lt;/li&gt;
&lt;li&gt;Come Visit&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  I Wanted a Simple Custom GitHub Profile. But Things Got Out of Hand Quickly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  23:00. Fully Cooked.
&lt;/h3&gt;

&lt;p&gt;It was 23:00. I'd just got home after an extremely exhausting day, dropped into the chair in front of my laptop, and... just stared at the screen. No plan. No thoughts. Nothing... Blank.&lt;/p&gt;

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

&lt;p&gt;I'm sure you know the feeling. The day cooks you so hard that your brain switches to screensaver mode, random thoughts start floating in and out on their own, like those bouncing DVD logos that never quite hit the corner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Random Doesn't Mean Bad
&lt;/h3&gt;

&lt;p&gt;Here's the funny part: that's exactly when my best ideas tend to show up. When the brain stops trying, it starts shuffling. And random doesn't always mean bad.&lt;/p&gt;

&lt;p&gt;That night, somewhere between "I should go to sleep" and "I should really go to sleep," one of those floating thoughts really caught my attention. And, my goodness, it was so good that I got my energy back. Instantly. Like someone had plugged me straight into a wall socket.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Are you kidding me? This is so cool! I'm building it right now. I don't care how tired I am."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what I told myself. Out loud. At 23:00... send help please.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Do I Keep Doing This to Myself?
&lt;/h3&gt;

&lt;p&gt;Seriously, why? I'd been awake for about 16 hours by then, and my brain had been running the whole time like an engine in a 24-hour endurance race at Daytona or the Nürburgring. By the way, I love those race tracks.&lt;/p&gt;

&lt;p&gt;Any reasonable person would have closed the laptop.&lt;/p&gt;

&lt;p&gt;I clicked the VS Code icon instead, cracked my knuckles and got to work. Time to cook!🔥&lt;/p&gt;




&lt;h2&gt;
  
  
  The Contribution City
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The City That Represents Me
&lt;/h3&gt;

&lt;p&gt;I'd always wanted my contribution graph to be something more than a grid of green squares. Something original. The problem was that I never had a good enough idea.&lt;/p&gt;

&lt;p&gt;And no, I didn't want to turn it into another Pac-Man, Snake or Tetris game. Don't get me wrong, those are creative and fun, I love them and I've seen many great implementations. But by now they're everywhere, overused.&lt;/p&gt;

&lt;p&gt;I wanted something different. Something that actually says something about &lt;strong&gt;ME&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I love &lt;em&gt;Cyberpunk 2077&lt;/em&gt; and its atmosphere. Well, except maybe its launch day, when the cars were spawning inside each other and NPCs were T-posing out of nowhere in the middle of the street, but everything after that? Absolutely.&lt;/p&gt;

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

&lt;p&gt;I love big cities with huge skyscrapers. I love being close to where things happen, where you can feel the pace and the pressure to keep up. I've never been an "I can't wait to retire and start a farm" kind of guy anyway.&lt;/p&gt;

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

&lt;p&gt;Oh, and I love neon lights. A lot.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Moment It Clicked
&lt;/h3&gt;

&lt;p&gt;So there I was, sitting motionless and emotionless in front of the screen, when the picture just appeared in my head: my contribution graph, lifted into 3D. Every day a building. Quiet days as empty lots, busy days as skyscrapers. A whole city.&lt;/p&gt;

&lt;p&gt;That was it. The contribution city was on its way.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  The Cyberpunk Console
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Because Why Not?
&lt;/h3&gt;

&lt;p&gt;Then I thought: a city like that can't just sit there on a plain white README, next to a list of badges. It needs a proper home.&lt;/p&gt;

&lt;p&gt;So I decided to go further and turn the &lt;strong&gt;whole profile&lt;/strong&gt; into a cyberpunk console: neon glow, scanlines, a terminal that types out my name, and a frame that holds everything together.&lt;/p&gt;

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

&lt;p&gt;Because why not?&lt;/p&gt;




&lt;h2&gt;
  
  
  Welcome to the Sandbox
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No JavaScript. No CSS. No Fun?
&lt;/h3&gt;

&lt;p&gt;First reality check: GitHub doesn't let you run anything in a README. &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; gets stripped. &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; gets stripped. Even inline &lt;code&gt;style=""&lt;/code&gt; attributes get stripped.&lt;/p&gt;

&lt;p&gt;I wanted a cyberpunk console. GitHub gave me Markdown and a handful of HTML tags.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Loophole: An SVG Is Just an Image
&lt;/h3&gt;

&lt;p&gt;But here's the thing. To GitHub, an SVG embedded with &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; is just an image. And &lt;em&gt;inside&lt;/em&gt; an SVG, you're allowed to use CSS, including animations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    @keyframes blink { 50% { opacity: 0 } }
    .cursor { animation: blink 1s step-end infinite }
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"38"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#3fb950"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$ whoami&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cursor"&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"110"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"18"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"#00d9ff"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/header.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's the whole trick behind the typing animation, the glitching name and the blinking cursor. Every "UI" element on my profile is secretly a picture.&lt;/p&gt;
&lt;h3&gt;
  
  
  Okay, But the Sandbox Has Rules
&lt;/h3&gt;

&lt;p&gt;Images get loaded in a locked-down mode, and that comes with a few catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No external resources.&lt;/strong&gt; Not even Google Fonts. So the font is embedded inside every SVG, trimmed down to only the characters that image actually shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hover, no clicks.&lt;/strong&gt; An image is an image. Links only work on a whole image, which is why every project card and every link button is its own file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching.&lt;/strong&gt; GitHub caches images for a few minutes. I fixed a bug, refreshed, saw the bug. Fixed it again, refreshed, still the bug. Turns out I'd fixed it the first time.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  One Console, 23 Images
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Problem: Images Don't Hold Hands
&lt;/h3&gt;

&lt;p&gt;My first version had every section as its own neat little panel: a box for the header, a box for the about text, a box for the stats. Each one looked great on its own.&lt;/p&gt;

&lt;p&gt;Together, they looked like a terminal that had been dropped on the floor. Separate pieces, with gaps between them.&lt;/p&gt;

&lt;p&gt;I wanted &lt;strong&gt;one&lt;/strong&gt; console. One continuous window you scroll through from top to bottom.&lt;/p&gt;
&lt;h3&gt;
  
  
  Slicing the Console
&lt;/h3&gt;

&lt;p&gt;The fix: draw one big frame and cut it into horizontal slices.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only the &lt;strong&gt;header&lt;/strong&gt; draws the top edge and the title bar.&lt;/li&gt;
&lt;li&gt;Only the &lt;strong&gt;footer&lt;/strong&gt; draws the bottom edge.&lt;/li&gt;
&lt;li&gt;Every slice in between draws just the two side rails, left and right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack them on top of each other and the rails line up into one long window:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/header.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/stats.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/contribution-city.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ...more slices... --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/footer.svg"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt; &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Project cards and link buttons are slices too, just narrower: half-width and fifth-width images that only draw the rail on their outer side. Count everything up and you get 23 images pretending to be one terminal.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Tricks That Make It Seamless
&lt;/h3&gt;

&lt;p&gt;Stacking images is easy. Making the seams invisible took a few tricks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 40 px grid.&lt;/strong&gt; Every slice is a multiple of 40 pixels tall, so the faint background grid continues across the cuts instead of jumping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glow that overflows.&lt;/strong&gt; The neon glow on the rails starts above each slice and ends below it, so it doesn't fade out at the edges like a broken fluorescent tube.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;align="top"&lt;/code&gt;.&lt;/strong&gt; Browsers leave a small gap under images by default, the space reserved for letters like &lt;em&gt;g&lt;/em&gt; and &lt;em&gt;y&lt;/em&gt; that hang below the line. Aligning to the top removes it.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Building the City
&lt;/h2&gt;
&lt;h3&gt;
  
  
  From Grid to Skyline
&lt;/h3&gt;

&lt;p&gt;The city isn't a new idea, it's the same contribution graph you already know. 53 columns of weeks, 7 rows of days. I just tilted it into an isometric view and gave every square some height.&lt;/p&gt;

&lt;p&gt;Each building is only three shapes: a roof and two walls. The trick is the drawing order. Buildings are painted from back to front, so the ones closer to you naturally cover the ones behind them. No 3D engine needed, just the painter's algorithm and a sorted list.&lt;/p&gt;
&lt;h3&gt;
  
  
  How Tall Is a Day?
&lt;/h3&gt;

&lt;p&gt;I know, what a weird question, right?&lt;/p&gt;

&lt;p&gt;My first instinct was simple: height = number of contributions. That gave me one giant tower for my busiest day and a sea of flat slabs for everything else. Not a city. More like a huge parking lot with the Empire State Building right in the middle of it.&lt;/p&gt;

&lt;p&gt;So the height uses a square root instead:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord mathnormal"&gt;h&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;8&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;+&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;110&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;⋅&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord sqrt"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span class="svg-align"&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mopen nulldelimiter"&gt;&lt;/span&gt;&lt;span class="mfrac"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;span class="msupsub"&gt;&lt;span class="vlist-t vlist-t2"&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="sizing reset-size6 size3 mtight"&gt;&lt;span class="mord mtight"&gt;&lt;span class="mop mtight"&gt;&lt;span class="mtight"&gt;m&lt;/span&gt;&lt;span class="mtight"&gt;a&lt;/span&gt;&lt;span class="mtight"&gt;x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="frac-line"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;&lt;span class="mord mathnormal"&gt;c&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="mclose nulldelimiter"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span class="pstrut"&gt;&lt;/span&gt;&lt;span class="hide-tail"&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-s"&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span class="vlist-r"&gt;&lt;span class="vlist"&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;



&lt;p&gt;In Python, that's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;busiest_day&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It lifts the small days while keeping the busy days clearly on top:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contributions&lt;/th&gt;
&lt;th&gt;Linear&lt;/th&gt;
&lt;th&gt;Square root&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;11 px&lt;/td&gt;
&lt;td&gt;25 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;34 px&lt;/td&gt;
&lt;td&gt;61 px&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;43 (my record)&lt;/td&gt;
&lt;td&gt;118 px&lt;/td&gt;
&lt;td&gt;118 px&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Days with zero contributions don't get a building at all. They stay as empty lots. Some months of my city look like an abandoned district. And you know what? That's accurate.&lt;/p&gt;

&lt;p&gt;Here's how the city compares to my actual GitHub activity graph:&lt;/p&gt;

&lt;p&gt;The contribution city:&lt;/p&gt;

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

&lt;p&gt;The default GitHub contribution graph:&lt;/p&gt;

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

&lt;p&gt;A skyline at night needs lights, so every building gets windows: some lit, some dark, and a few that flicker. Add twinkling stars, a moon, and a plane with blinking lights crossing the sky, and the city starts to feel alive.&lt;/p&gt;

&lt;p&gt;The windows are placed randomly, but with a &lt;strong&gt;seeded&lt;/strong&gt; random generator. Same data, same seed, same city, down to the last window. Why does that matter? Because otherwise the image would change every single day even with no new contributions, and my repo would get a pointless commit every morning.&lt;/p&gt;
&lt;h3&gt;
  
  
  Green or Blue?
&lt;/h3&gt;

&lt;p&gt;GitHub's contribution graph is green, so my first city had green roofs. People instantly read it as "activity," and that's a big plus.&lt;/p&gt;

&lt;p&gt;But the rest of my profile is neon blue. Next to everything else, the green city looked like it was pasted in from another website. So I rendered both versions side by side, looked at them for about three seconds, and blue won. I love the color blue.&lt;/p&gt;

&lt;p&gt;Here are both variants. Which one do you like more?&lt;/p&gt;

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


&lt;h2&gt;
  
  
  It Updates Itself Every Day
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A Robot on Daily Duty
&lt;/h3&gt;

&lt;p&gt;A profile with stats that never change is just a screenshot with extra steps. So the whole thing runs on a schedule, on GitHub's servers, whether my laptop is on or not.&lt;/p&gt;

&lt;p&gt;The pipeline is four steps, three of them small Python scripts:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch.py   → asks GitHub and DEV for fresh numbers, saves them as JSON
render.py  → reads the JSON, redraws every SVG
readme.py  → updates the parts of README.md that change
git commit → only if something actually changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the workflow that glues it together:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update profile&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;17&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
      &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# a "Run workflow" button for impatient people (me)&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;             &lt;span class="c1"&gt;# needed to push the refreshed files&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install fonttools==4.62.1 brotli==1.2.0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python tools/profile/fetch.py&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PROFILE_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROFILE_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.token }}&lt;/span&gt;
          &lt;span class="na"&gt;DEV_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEV_API_KEY }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python tools/profile/render.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python tools/profile/readme.py&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "github-actions[bot]"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "41898282+github-actions[bot]@users.noreply.github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add tools/profile/data assets README.md&lt;/span&gt;
          &lt;span class="s"&gt;git diff --cached --quiet || (git commit -m "Refresh profile stats" &amp;amp;&amp;amp; git push)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That last line is the polite part: &lt;code&gt;git diff --cached --quiet&lt;/code&gt; exits successfully when nothing changed, so the commit only happens when there's something new. No empty "update" commits polluting the history.&lt;/p&gt;
&lt;h3&gt;
  
  
  Talking to GitHub: One Query Instead of Twenty
&lt;/h3&gt;

&lt;p&gt;GitHub has two APIs. The REST one would need a separate request for followers, pull requests, every repository, every repository's languages... I'd be paginating until next Tuesday.&lt;/p&gt;

&lt;p&gt;The GraphQL API lets me ask for exactly what I need in &lt;strong&gt;one&lt;/strong&gt; request:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!)&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;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$login&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;followers&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;span class="n"&gt;totalCount&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;span class="n"&gt;pullRequests&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;span class="n"&gt;totalCount&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;span class="n"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pullRequests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MERGED&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;totalCount&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;span class="n"&gt;contributionsCollection&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;span class="n"&gt;contributionYears&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;span class="n"&gt;repositories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ownerAffiliations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;isFork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;privacy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PUBLIC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;nodes&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;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;stargazerCount&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;forkCount&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="n"&gt;edges&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;span class="n"&gt;size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node&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;span class="n"&gt;name&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;span class="p"&gt;}&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;span class="p"&gt;}&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;span class="p"&gt;}&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;From that single response I get the followers, PRs, member-since date, total stars and forks (summed across repos), per-project star counts for the cards, and the language bar (bytes of code per language, summed up).&lt;/p&gt;

&lt;p&gt;Sending it is plain Python, no SDK needed:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;http_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/graphql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;variables&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GraphQL error: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The All-Time Problem
&lt;/h3&gt;

&lt;p&gt;Here's a GitHub quirk: &lt;code&gt;contributionsCollection&lt;/code&gt; covers &lt;strong&gt;at most one year&lt;/strong&gt; per request. Ask for more and it politely refuses.&lt;/p&gt;

&lt;p&gt;So how do you get all-time contributions and a streak that doesn't break on January 1st? The first query returns &lt;code&gt;contributionYears&lt;/code&gt;, the list of years I've been active. Then I build a second query on the fly, with one aliased field per year:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;years_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;years&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;years&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    y&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: contributionsCollection(from: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-01-01T00:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, to: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-12-31T23:59:59Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;) {{
      totalCommitContributions
      contributionCalendar {{ totalContributions weeks {{ contributionDays {{ date contributionCount }} }} }}
    }}&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query($login: String!) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  user(login: $login) {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  }&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;y2016&lt;/code&gt;, &lt;code&gt;y2017&lt;/code&gt;, ..., &lt;code&gt;y2026&lt;/code&gt;: a decade of calendars in a single round trip. GraphQL aliases are criminally underrated.&lt;/p&gt;

&lt;p&gt;That gives me every single day since 2016 as &lt;code&gt;{date: count}&lt;/code&gt;. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contributions this year and all time&lt;/strong&gt; are sums of &lt;code&gt;totalContributions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The city&lt;/strong&gt; gets the last 53 weeks, starting on a Sunday, exactly like GitHub's own graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The streaks&lt;/strong&gt; are a simple walk through the days:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;streaks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;longest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;longest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;longest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# no contributions yet today?
&lt;/span&gt;        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# then the streak can still end yesterday
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That little &lt;code&gt;if&lt;/code&gt; matters more than it looks. The workflow runs at midday. Without it, my streak would reset to zero every day until I made my first commit of the day. Very motivating. But not in a good way.&lt;/p&gt;
&lt;h3&gt;
  
  
  Talking to DEV
&lt;/h3&gt;

&lt;p&gt;The DEV numbers come from the &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;Forem API&lt;/a&gt;. Articles, reactions and comments are public:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;arts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dev_paged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/articles?username=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;articles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reactions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public_reactions_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comments_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arts&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;Views and followers are private, so they need an API key in the &lt;code&gt;api-key&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/vnd.forem.api-v1+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;mine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dev_paged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/articles/me/published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_views_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;followers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dev_paged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api/followers/users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;dev_paged&lt;/code&gt; just keeps requesting &lt;code&gt;page=1, 2, 3...&lt;/code&gt; until it gets an empty list back. The same response also gives me my five newest articles for the writing section.&lt;/p&gt;
&lt;h3&gt;
  
  
  When an API Has a Bad Day
&lt;/h3&gt;

&lt;p&gt;APIs fail. Rate limits, timeouts, a random 502 at the worst possible moment. And a profile that suddenly shows "0 stars, 0 contributions" because GitHub hiccupped once is worse than no stats at all.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;fetch.py&lt;/code&gt; never starts from scratch. It loads yesterday's JSON first and only overwrites what it successfully fetched:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stats.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;          &lt;span class="c1"&gt;# yesterday's numbers
&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch_github&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GitHub fetch failed, keeping previous stats: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_dev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEV fetch failed, keeping previous values: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If GitHub is down, the DEV numbers still update, and vice versa. If &lt;strong&gt;both&lt;/strong&gt; fail, the script exits with an error and touches nothing. Worst case, my profile is one day old. It never goes blank.&lt;/p&gt;

&lt;p&gt;Bonus: the JSON files are committed together with the images, so &lt;code&gt;git log -p tools/profile/data/stats.json&lt;/code&gt; is a free history of my stats. I didn't plan that. I'll take it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The README Edits Itself (Carefully)
&lt;/h3&gt;

&lt;p&gt;Most of the README never changes. But the article links do, and so do the alt texts that describe the stats for screen readers. Rewriting the whole file from a template would wipe out anything I edit by hand, so &lt;code&gt;readme.py&lt;/code&gt; only touches clearly marked zones:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- writing:start --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://dev.to/..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./assets/writing/post-1.svg"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
...
&lt;span class="c"&gt;&amp;lt;!-- writing:end --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;!-- writing:start --&amp;gt;.*?&amp;lt;!-- writing:end --&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;writing_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error: README needs exactly one writing block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;HTML comments are invisible on GitHub, so the markers cost nothing. And if someone (me) accidentally deletes one, the script fails loudly instead of quietly mangling the README.&lt;/p&gt;
&lt;h3&gt;
  
  
  Secrets That Stay Secret
&lt;/h3&gt;

&lt;p&gt;The workflow needs two secrets: a GitHub token, so private contributions get counted, and the DEV API key. Neither is ever written in the repo. The workflow only references them by name:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;PROFILE_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROFILE_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The values live in &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;, encrypted. GitHub injects them at runtime and masks them as &lt;code&gt;***&lt;/code&gt; if they ever show up in the logs.&lt;/p&gt;

&lt;p&gt;For the token I used a &lt;strong&gt;fine-grained&lt;/strong&gt; personal access token instead of a classic one, because it can be &lt;strong&gt;read-only&lt;/strong&gt;: Metadata and Contents, nothing else. If it ever leaked, the worst someone could do is read my code. Not push to it.&lt;/p&gt;

&lt;p&gt;And both are optional. Without them, the workflow falls back to the built-in &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and public data only.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why 12:17?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;:17 instead of :00&lt;/strong&gt;, because everyone schedules their jobs on the full hour, and GitHub's runners get flooded at :00. Scheduled runs at the top of the hour often start late. A random-looking minute dodges the rush.&lt;/p&gt;


&lt;h2&gt;
  
  
  Was It Worth It?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Damage Report
&lt;/h3&gt;

&lt;p&gt;Let's count what my "simple custom profile" turned into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;23 SVG images pretending to be one terminal&lt;/li&gt;
&lt;li&gt;a contribution city with one building for every day of the past year&lt;/li&gt;
&lt;li&gt;three Python scripts and a GitHub Actions workflow&lt;/li&gt;
&lt;li&gt;two APIs, two secrets and one very hardworking robot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that, so a page most people will look at for about eight seconds says my name in neon.&lt;/p&gt;
&lt;h3&gt;
  
  
  So... Was It?
&lt;/h3&gt;

&lt;p&gt;Absolutely.&lt;/p&gt;

&lt;p&gt;It wasn't really about the profile. It was about that 23:00 feeling, when a random idea shows up and suddenly you're not tired anymore. The kind of project nobody asked for, with no deadline and no stakeholders, built just because it's fun.&lt;/p&gt;

&lt;p&gt;And somewhere along the way I learned a bunch of things I'd never have touched otherwise: SVG animations, isometric drawing, font subsetting, GraphQL aliases, and how many layers of caching sit between "I fixed it" and "I can see that I fixed it."&lt;/p&gt;

&lt;p&gt;Plus, the profile now maintains itself. Every day at 12:17 New York time, a robot wakes up, checks what I've been doing, and adds a new building to my city. Even on the days I don't feel like I'm building anything.&lt;/p&gt;
&lt;h3&gt;
  
  
  Visit the Profile
&lt;/h3&gt;

&lt;p&gt;The city is live at:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/georgekobaidze" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F24773902%3Fv%3D4%3Fs%3D400" height="417" class="m-0" width="417"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer" class="c-link"&gt;
            georgekobaidze (Giorgi Kobaidze) · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Software Engineer | Engineering Manager | Technical Writer |🏆2x Hackathon Winner | ☕Enjoyer - georgekobaidze
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;And all the code behind it is in the same repo, under &lt;code&gt;tools/profile&lt;/code&gt;. Feel free to look around, fork it, and build your own skyline. Just change the username first, unless you want my stats on your profile.&lt;/p&gt;

&lt;p&gt;And if you build something cool with it, show me in the comments. I'd love to see what your city looks like.&lt;/p&gt;

&lt;p&gt;Now if you'll excuse me, I have some quiet days to fill. My city has a few empty lots that need skyscrapers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ exit
connection to georgekobaidze closed. // EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📱 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.com/users/571315867426488330" rel="noopener noreferrer"&gt;Let's Chat on Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>showdev</category>
      <category>github</category>
      <category>design</category>
    </item>
    <item>
      <title>Vim's Swan Song: Is It Time to :q!?</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Fri, 25 Sep 2026 19:01:14 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/vims-swan-song-is-it-time-to-q-eek</link>
      <guid>https://dev.to/georgekobaidze/vims-swan-song-is-it-time-to-q-eek</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hotel California&lt;/li&gt;
&lt;li&gt;Hands Off the Mouse&lt;/li&gt;
&lt;li&gt;Vim Was Never About Typing Speed&lt;/li&gt;
&lt;li&gt;The Case for :q!&lt;/li&gt;
&lt;li&gt;Plot Twist: The Terminal Strikes Back&lt;/li&gt;
&lt;li&gt;The Last 10% Is Where Vim Lives&lt;/li&gt;
&lt;li&gt;The Editor Might Leave, The Motions Won't&lt;/li&gt;
&lt;li&gt;So... :q! or :w?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Hotel California
&lt;/h2&gt;

&lt;p&gt;There's an old joke that the hardest part of learning Vim is figuring out how to leave it. &lt;a href="https://stackoverflow.com/questions/11828270/how-do-i-exit-vim" rel="noopener noreferrer"&gt;The Stack Overflow question&lt;/a&gt; about exiting Vim is one of the most viewed questions on the site, and the answer has become a meme: &lt;code&gt;:q!&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F254q6wzbkj0qshm5zxk4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F254q6wzbkj0qshm5zxk4.png" alt="StackOverflow" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Hands Off the Mouse
&lt;/h2&gt;

&lt;p&gt;For decades, Vim's pitch has been simple: keep your hands on the keyboard and do everything faster. No mouse, no menus, no reaching for arrow keys. Just you, the home row, and a text editor that feels like an extension of your fingers.&lt;/p&gt;

&lt;p&gt;I've never been a Vim power user. But I've always been a big believer in treating the keyboard as an extension of your hands. Reaching for the mouse only takes a fraction of a second, but when you're deep in focus, that tiny interruption is enough to break your train of thought. So while I'm not a Vim enthusiast, I still think keyboard shortcuts are essential. Not for speed, but for focus.&lt;/p&gt;

&lt;p&gt;Then AI showed up and started writing our code. So, the question is, do we still need Vim? Or maybe it's time to type &lt;code&gt;:q!&lt;/code&gt; and walk away for good?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The answer is much more nuanced than a yes or no.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Vim Was Never About Typing Speed
&lt;/h2&gt;

&lt;p&gt;Here's the thing most people get wrong about Vim: it was never really about typing speed. You can type just as fast in Notepad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alright, 🔥Hot take🔥, folks:&lt;/strong&gt; does fast typing make you a better programmer?&lt;/p&gt;

&lt;p&gt;My take? &lt;strong&gt;Absolutely not! Come on!&lt;/strong&gt; Look, the typing is the easiest part of our job. The hard part is figuring out what to type, and no amount of words per minute is going to help you there. Average speed? You're golden. Trust me.&lt;/p&gt;

&lt;p&gt;Vim's real superpower is that it gives you a language for editing. Instead of pressing keys to move a cursor around and delete characters one by one, you describe what you want to happen:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ci"&lt;/code&gt; means change inside quotes&lt;br&gt;
&lt;code&gt;dap&lt;/code&gt; means delete a paragraph&lt;br&gt;
&lt;code&gt;.&lt;/code&gt; means do that again&lt;/p&gt;

&lt;p&gt;You're not pushing characters around, you're expressing intent, and the editor carries it out.&lt;/p&gt;

&lt;p&gt;That matters because even before AI, most of our time wasn't spent writing new code. It was spent reading, navigating, and changing code that already existed. Keep that in mind, because it's about to become very important.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Case for :q!
&lt;/h2&gt;

&lt;p&gt;Let's be fair and give the "Vim is done" side its best shot, because it makes some good points.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Learning Curve is Harder to Justify
&lt;/h3&gt;

&lt;p&gt;Learning Vim means weeks of feeling slower than before. That investment made sense when you spent eight hours a day writing code by hand. If AI writes most of it now, the payoff shrinks.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-First Tools Aren't Built for Modes
&lt;/h3&gt;

&lt;p&gt;The modern AI workflow revolves around chat panels, inline suggestions, diff views, and an "Accept" button. When your main interaction is describing what you want in plain English, switching between normal and insert mode can feel like friction rather than power.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Next Generation May Never Need It
&lt;/h3&gt;

&lt;p&gt;Developers starting out today might never write enough code by hand to build the muscle memory Vim demands. If your fingers spend the day writing prompts instead of for-loops, &lt;code&gt;hjkl&lt;/code&gt; starts to look like a hobby.&lt;/p&gt;

&lt;p&gt;These are pretty solid arguments. But they miss a plot twist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Plot Twist: The Terminal Strikes Back
&lt;/h2&gt;

&lt;p&gt;Here's the irony nobody predicted: the AI era sent developers back to the terminal.&lt;/p&gt;

&lt;p&gt;Some of the most capable AI coding tools today, like Claude Code and GitHub Copilot CLI, live right there in the command line. When I built &lt;a href="https://dev.to/georgekobaidze/metal-birds-watch-copilot-cli-helped-me-watch-planes-without-looking-up-4ha0"&gt;Metal Birds Watch&lt;/a&gt; for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI challenge&lt;/a&gt;, a big part of my back-and-forth with the AI happened in a terminal window, not a fancy IDE.&lt;/p&gt;

&lt;p&gt;And the terminal was never really gone for developers anyway. SSH into a server, open a shell in a container, fix a config file in production at 2 AM, and there's a good chance Vim (or its ancestor, vi) is the only editor waiting for you, judging your skills.&lt;/p&gt;

&lt;p&gt;So while AI was supposed to pull us further from the keyboard-driven world, it actually made the terminal feel modern again. And Vim has always been the terminal's native tongue.&lt;/p&gt;

&lt;p&gt;Gosh... our industry is basically the Daytona 500: we just keep going in circles. The question is, are you Richard Petty, or the guy he just lapped... twice.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Last 10% Is Where Vim Lives
&lt;/h2&gt;

&lt;p&gt;This is the part that changes everything.&lt;/p&gt;

&lt;p&gt;AI is fantastic at getting you 90% of the way there. But anyone who has shipped AI-generated code knows what the last 10% looks like: rename this variable, delete that redundant block, move this method up, fix the off-by-one error, swap these two arguments.&lt;/p&gt;

&lt;p&gt;Those aren't typing tasks. They're editing tasks, small, precise, surgical changes in code you didn't write yourself.&lt;/p&gt;

&lt;p&gt;Your job is quietly shifting from writing code to reviewing and shaping code. You read a diff, jump to the suspicious part, search for where something is used, change one thing, and move on. That's navigation and editing, which is exactly what Vim was designed for all along.&lt;/p&gt;

&lt;p&gt;AI took the typing. It left the editing. &lt;strong&gt;And editing was always Vim's real job.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Editor Might Leave, The Motions Won't
&lt;/h2&gt;

&lt;p&gt;There's an important distinction here: Vim the program and Vim the way of editing are two different things.&lt;/p&gt;

&lt;p&gt;Vim the program may well stay a niche choice. Plenty of developers will happily live in AI-first editors with chat panels and never open a raw terminal editor.&lt;/p&gt;

&lt;p&gt;But Vim's motions have escaped the editor. They're in VS Code through an extension, in Rider and every other JetBrains IDE through IdeaVim, in Neovim with its huge modern ecosystem and even in your browser through extensions like Vimium. You can use the shiniest AI-powered editor available and still edit like a Vim user.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... :q! or :w?
&lt;/h2&gt;

&lt;p&gt;Is this Vim's swan song? I don't think so. I think we have to keep it as an option. Can't say it's absolutely essential, but it never was to be fair.&lt;/p&gt;

&lt;p&gt;Vim the editor is optional, as it always has been. But Vim as a way of editing becomes more valuable, not less, when your job shifts from writing code to shaping it. &lt;strong&gt;The less you type, the more every edit counts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you already know Vim, keep it. Hold onto it. If you're new, don't learn it to type faster, AI has that covered. Learn the motions to edit faster, because that's the part of the job that isn't going anywhere.&lt;/p&gt;

&lt;p&gt;So maybe it's not time for &lt;code&gt;:q!&lt;/code&gt;. Maybe it's just time for &lt;code&gt;:w&lt;/code&gt; save what works and keep going.&lt;/p&gt;

&lt;p&gt;What about you? Still using Vim or Vim motions, or has AI made you drop them? Let's discuss.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>tools</category>
      <category>vim</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Double Engine Failure: Back to the Hangar After Two Data Providers Changed Course</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Thu, 24 Sep 2026 18:00:16 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/double-engine-failure-back-to-the-hangar-after-two-data-providers-changed-course-3o3</link>
      <guid>https://dev.to/georgekobaidze/double-engine-failure-back-to-the-hangar-after-two-data-providers-changed-course-3o3</guid>
      <description>&lt;p&gt;&lt;em&gt;Two providers changed the rules, and my app went back to the hangar&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Showtime on the Balcony (Gone Wrong)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"I like planes too. I wish I had an app that showed me the planes nearby and their details. That'd be so cool."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of my buddy's buddy's buddies said this at a party, while we were out on the balcony getting some fresh air. Well, &lt;strong&gt;I&lt;/strong&gt; was getting fresh air. He was smoking the whole time.&lt;/p&gt;

&lt;p&gt;That was my moment to shine. He was describing, almost word for word, the app I'd won my first hackathon with. So I thought to myself: &lt;em&gt;"It's showtime!"&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have just the thing for ya" &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I said with a slight smirk, pulled out my phone, typed &lt;code&gt;metalbirdswatch.pilotronica.com&lt;/code&gt; into the browser... only to get this:&lt;/p&gt;

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

&lt;p&gt;My smirk turned into a "what the hell is this bull shirt" face in about half a second. Obviously I wasn't going to show anyone my app in that state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Well... umm... yeah... some kind of problem, probably browser-related. I'll show you later."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;It wasn't browser-related.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I knew that for sure. I figured CARTO, the service that draws my map, had changed something in their API or made it paid. Either way it'd be a quick fix, and that'd be it. Easy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'll fix it tomorrow and send you the link so you can check it out"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I said.&lt;/p&gt;

&lt;p&gt;Little did I know I was about to find out that the planes were gone too. Metal Birds Watch without the metal birds? Doesn't sound great, does it?&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Layover
&lt;/h2&gt;

&lt;p&gt;But let's not get ahead of ourselves. What's the point of explaining anything without proper storytelling? Storytelling on a technical write-up is like a good ragù on tagliatelle. Sure, you can eat the tagliatelle plain, but the sauce makes it so much better.&lt;/p&gt;

&lt;p&gt;Otherwise I'd just drop a link to the release notes and call it a day. Easy, simple. But let's not do that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This One Is Personal
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;By the way, this is me proudly holding a &lt;code&gt;Boeing 737-800&lt;/code&gt; model while recording the project video.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;...and yes, the plane was more camera-friendly than I was.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhpppd8fc0zmfnyvfbro.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxhpppd8fc0zmfnyvfbro.jpeg" alt="A Man Holding a Plane" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://dev.to/georgekobaidze/metal-birds-watch-copilot-cli-helped-me-watch-planes-without-looking-up-4ha0"&gt;Metal Birds Watch&lt;/a&gt; for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt; on DEV. It was the first hackathon of my life, and I started it on hard mode: the competition was brutal, with over &lt;strong&gt;400 participants&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But I won.&lt;/p&gt;

&lt;p&gt;While building it I had every obstacle in the world, including back, neck and arm pain from shoveling my driveway for hours non-stop. (Pro tip: don't shovel an entire driveway in one go right before a hackathon.)&lt;/p&gt;

&lt;p&gt;I could barely sit up straight while writing the whole thing. My back was killing me.&lt;/p&gt;

&lt;p&gt;So yeah, as you can imagine, this app is pretty special to me. It's my baby.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engine #1: The Map That Said No
&lt;/h2&gt;

&lt;p&gt;The morning after the party. I poured a cup of dark coffee, sat down at my desk, put on some music (RHCP, as usual), and got to work. Time to fix this thing.&lt;/p&gt;

&lt;p&gt;the first job was to figure out why on earth the map looked like that.&lt;/p&gt;

&lt;p&gt;It turned out CARTO had changed its policy around &lt;strong&gt;28 August 2026&lt;/strong&gt;. Their free basemaps now require an API key, and requests without one get tiles with &lt;strong&gt;"API KEY REQUIRED"&lt;/strong&gt; stamped diagonally across them.&lt;/p&gt;

&lt;p&gt;I didn't have a CARTO account, so they had no email address to warn me, and I'm sure plenty of other users were in the same position. Their best way to tell us was to cover our maps with the message. Fair enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Quick Patch on the Tarmac
&lt;/h3&gt;

&lt;p&gt;This was the easy part. I requested a free key, which allows 5 million tile requests per month, and added it to the tile URL. My map URLs live in CSS custom properties, one per theme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* variables.css, before */&lt;/span&gt;
&lt;span class="nt"&gt;--map-tiles&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* after */&lt;/span&gt;
&lt;span class="nt"&gt;--map-tiles&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png?key=YOUR_KEY'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was the whole change: &lt;strong&gt;not a single line of JavaScript&lt;/strong&gt;. The map reads its tile URL from CSS on startup and again on every theme switch, so both picked up the key automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tileUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPropertyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--map-tiles&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;tileLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tileLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tileUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;addTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Past me, thank you for that architecture decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-Flight Checklist
&lt;/h3&gt;

&lt;p&gt;There were a few details to think through, though.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Should the key be committed to a public repo?&lt;/strong&gt; My first instinct was "never commit keys." But a map tile key &lt;em&gt;can't&lt;/em&gt; be secret: the browser sends it with every tile request, and anyone can see it in DevTools. What protects it is a &lt;strong&gt;domain restriction&lt;/strong&gt; set in CARTO's dashboard, so the key only works on my production domain. Committing it was fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A CSP trap.&lt;/strong&gt; CARTO's current docs show the apex domain, &lt;code&gt;basemaps.cartocdn.com&lt;/code&gt;. My Content Security Policy only allows &lt;code&gt;https://*.basemaps.cartocdn.com&lt;/code&gt;, and that wildcard doesn't match the apex domain. If I'd copied the URL from the docs, the tiles would have been silently blocked by my own CSP. Keeping the &lt;code&gt;{s}.&lt;/code&gt; subdomain form avoided a second outage in the same fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The domain restriction backfired locally.&lt;/strong&gt; Because the key only works on production, every tile on &lt;code&gt;localhost&lt;/code&gt; returned &lt;strong&gt;403&lt;/strong&gt;, and my local map went completely grey. I later added a small helper that swaps the key on localhost for a personal development key stored in the browser, or removes it entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTileUrl&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&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="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPropertyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--map-tiles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;'"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isLocalhost&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;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// production: unchanged&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;devKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;carto-dev-key&lt;/span&gt;&lt;span class="dl"&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;devKey&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/key=&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`key=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;devKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// your own dev key&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;?&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;key=&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// no key: watermark, but it works&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My development key lives in my browser, not in a file, so it can never be committed by accident.&lt;/p&gt;

&lt;p&gt;I added the key, tested it, and everything was back to normal. I was so happy to see my favorite app, my baby, running smoothly again.&lt;/p&gt;

&lt;p&gt;Time to deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engine #2: Where Did All the Birds Go?
&lt;/h2&gt;

&lt;p&gt;I ran the pipeline, and it deployed. Alright, let me check production.&lt;/p&gt;

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

&lt;p&gt;OK, whoa whoa whoa whoa. What do you mean, &lt;em&gt;Failed to fetch plane data: API error: 503 Service Unavailable&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;The map was clean, and it was also completely empty. No planes. The status indicator said &lt;strong&gt;OFFLINE&lt;/strong&gt;, and an error toast showed up every 20 seconds or so like clockwork: the frontend asked for planes, waited about 10 seconds, got a &lt;strong&gt;503&lt;/strong&gt;, then tried again.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Instrument That Lied
&lt;/h3&gt;

&lt;p&gt;The backend logs were very clear about the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication failed - check OpenSky credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Makes sense. My flight data comes from OpenSky Network, and the backend authenticates with OAuth client credentials before fetching aircraft. Maybe the credentials had expired, or been revoked.&lt;/p&gt;

&lt;p&gt;So I rotated them. It didn't help.&lt;/p&gt;

&lt;p&gt;So I rotated them &lt;em&gt;again&lt;/em&gt;. It didn't help either.&lt;/p&gt;

&lt;p&gt;Two credential rotations and several hours later, I finally looked closely at the code that produced the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tokenRefreshPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAccessToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to get OAuth token:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="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="s1"&gt;Authentication failed - check OpenSky credentials&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;// ...&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Can you see it? &lt;strong&gt;Every&lt;/strong&gt; failure became "Authentication failed": network timeouts, DNS errors, HTTP errors, &lt;em&gt;and&lt;/em&gt; genuine credential problems. The code swallowed the real error and replaced it with a confident, specific and completely wrong diagnosis.&lt;/p&gt;

&lt;p&gt;The error message had been lying to me all along, and I'd written it myself. That hurt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opening Up the Engine
&lt;/h3&gt;

&lt;p&gt;Enough guessing. If the error message wasn't going to tell me the truth, I'd make the error tell me itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: log everything.&lt;/strong&gt; Instead of trusting my own summary, I logged every detail of the raw error: its &lt;code&gt;code&lt;/code&gt;, &lt;code&gt;syscall&lt;/code&gt;, &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;port&lt;/code&gt;, and most importantly its &lt;code&gt;cause&lt;/code&gt;. That's how you tell a DNS failure from a TCP failure, a TLS failure or an actual HTTP error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OpenSky auth error details:&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cause&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cause&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deployed, waited, and the logs finally told the truth. It wasn't an authentication failure at all. It was a &lt;strong&gt;TCP connect timeout&lt;/strong&gt; to OpenSky's auth server. My backend never got as far as sending the credentials I'd rotated twice, because it couldn't even open a connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: be patient.&lt;/strong&gt; A timeout? Maybe OpenSky's server was just slow to respond from Railway's network. So I increased the auth request timeout to 30 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I deployed, waited, and got exactly the same error, still after &lt;strong&gt;10 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Wait, what? I'd just set it to 30.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: find out where the 10 seconds came from.&lt;/strong&gt; Node's &lt;code&gt;fetch&lt;/code&gt; is built on &lt;strong&gt;undici&lt;/strong&gt;, and undici has its own separate &lt;strong&gt;10-second limit for opening a TCP connection&lt;/strong&gt;. That limit applies before any request timeout you set yourself. So my 30-second timeout never got a chance to run; undici gave up first.&lt;/p&gt;

&lt;p&gt;To change it, you pass fetch a custom undici &lt;code&gt;Agent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undici&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authDispatcher&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;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// give the TCP handshake 30s instead of 10s&lt;/span&gt;
&lt;span class="p"&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;OPENSKY_AUTH_URL&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="na"&gt;dispatcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authDispatcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30000&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;My theory was that the TCP handshake from Railway to OpenSky was just slow and would get there eventually if I gave it enough time.&lt;/p&gt;

&lt;p&gt;I deployed, waited, and waited some more.&lt;/p&gt;

&lt;p&gt;Reader, it did not get there eventually. I gave it 30 seconds. I could have given it 30 minutes. A connection that isn't slow but &lt;strong&gt;ignored&lt;/strong&gt; will never complete, however long you wait.&lt;/p&gt;

&lt;h3&gt;
  
  
  Silence on the Frequency
&lt;/h3&gt;

&lt;p&gt;This was the turning point, so it's worth a short technical detour.&lt;/p&gt;

&lt;p&gt;When you try to connect to a server, there are roughly two ways it can go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection refused:&lt;/strong&gt; the server's machine answers and says "nobody's listening on that port". It's fast and polite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection timeout:&lt;/strong&gt; nothing answers at all. Your packets leave, and nothing comes back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A timeout on a server that's otherwise up and working for everyone else usually means one thing: &lt;strong&gt;something is dropping your packets on purpose&lt;/strong&gt;, typically a firewall. And the only unusual thing about my backend's packets was where they came from: a cloud provider's IP range.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tower, Do You Copy?
&lt;/h3&gt;

&lt;p&gt;So I asked OpenSky directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do you block hosting and cloud provider IP ranges?&lt;/strong&gt;&lt;br&gt;
"Yes."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a specific address be allowlisted?&lt;/strong&gt;&lt;br&gt;
"No."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the recommended approach for hosted apps?&lt;/strong&gt;&lt;br&gt;
They're considering a paid option to handle the requests coming from cloud providers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So that was that. The blocking is deliberate, exceptions aren't possible, and a paid option might come someday. &lt;strong&gt;There's currently no supported way to use OpenSky from a hosted backend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's a bummer. Real bummer. I really liked OpenSky's service.&lt;/p&gt;

&lt;p&gt;To be fair to OpenSky, I get it. They run a free, community-driven service, and they're being flooded by automated traffic from cloud servers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Back to the Hangar
&lt;/h2&gt;

&lt;p&gt;So it was time to land the plane. I fixed one engine mid-air, but I couldn't do it for the second one. So I had to land and go back into the hangar.&lt;/p&gt;

&lt;p&gt;Adding an API key is easy, especially a public one. Researching and integrating a new flight data provider is not something you do in a few minutes, or even a few hours.&lt;/p&gt;

&lt;p&gt;So I put the app into &lt;strong&gt;maintenance mode&lt;/strong&gt;. It'll take a while to find the right provider, but hey, great things take time.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Ten Seconds of Doubt
&lt;/h3&gt;

&lt;p&gt;That was my first idea, and it's a trap. The backend takes &lt;strong&gt;about 10 seconds&lt;/strong&gt; to return that 503. A banner that appears &lt;em&gt;after&lt;/em&gt; the failed request leaves users staring at an empty map for 10 seconds first. That's exactly when they decide the app is broken, or that there just aren't any planes overhead.&lt;/p&gt;

&lt;p&gt;So the maintenance page is &lt;strong&gt;unconditional&lt;/strong&gt;. It appears immediately and doesn't wait for anything to fail. One flag controls it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;MAINTENANCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ACTIVE&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="c1"&gt;// Set to false once the new data provider is live&lt;/span&gt;
  &lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opensky-2026-09&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;SINCE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// Used for the "day N" counter&lt;/span&gt;
  &lt;span class="nx"&gt;ISSUE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://github.com/georgekobaidze/metal-birds-watch/issues/98&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;When it's on, the map doesn't load, the browser doesn't ask for your location, and polling never starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;startPolling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Skip polling during maintenance - the backend can't reach the data provider,&lt;/span&gt;
  &lt;span class="c1"&gt;// so every request would time out and show an error toast&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;CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAINTENANCE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ACTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="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;No more doomed requests, no more error toasts every 20 seconds, and no more "is it me?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Speaking Fluent Aviation
&lt;/h3&gt;

&lt;p&gt;Since this is an aviation app, the maintenance page talks like one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A NOTAM-style notice.&lt;/strong&gt; A NOTAM ("Notice to Air Missions") is how pilots are told about closures and outages. The notice sits over a full-screen photo of a plane being repaired in a hangar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A radio call:&lt;/strong&gt; &lt;em&gt;"All stations, all stations: Metal Birds Watch is out of service for unscheduled maintenance until further notice."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A counter:&lt;/strong&gt; &lt;em&gt;"Grounded since 20 Sep 2026 · day N"&lt;/em&gt;, updated automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the Tower:&lt;/strong&gt; a small panel that answers the questions people actually have, such as &lt;em&gt;Is it something on my end?&lt;/em&gt; ("Negative."), &lt;em&gt;Is my logbook safe?&lt;/em&gt; ("Affirmative. It lives in your own browser.") and &lt;em&gt;When will it be fixed?&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;There's no AI in the Tower, which felt right given the circumstances. It's a list of pre-written questions and answers with a typing indicator for effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;logbook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Is my logbook safe?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Affirmative. Your logbook lives in your own browser, not on our servers, so the &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outage can&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;t touch it. Open it anytime with the 📖 button up top.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;followUps&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;stillWorks&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Looking for a New Engine
&lt;/h2&gt;

&lt;p&gt;Replacing the data provider is tracked in &lt;a href="https://github.com/georgekobaidze/metal-birds-watch/issues/98" rel="noopener noreferrer"&gt;issue #98&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The good news is that my OpenSky code is already an isolated adapter. It converts OpenSky's response into my own data format, and nothing above it knows where the data comes from. A new provider is one new service file behind the same interface. The routes, the cache and the entire frontend stay the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Black Box
&lt;/h2&gt;

&lt;p&gt;If I had to summarize the flight recorder:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures are the worst failures.&lt;/strong&gt; An HTTP 200 containing a watermark and a 503 nobody was watching both went unnoticed for about three weeks. I found out at a party, from a phone, while trying to show off. Some basic monitoring would have found them for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your error messages are part of your debugging tools.&lt;/strong&gt; A misleading one costs hours. Mine cost two credential rotations and an afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timeout and a refusal tell you different things.&lt;/strong&gt; A refusal means nobody is listening. A timeout, when the server works for everyone else, usually means someone chose not to answer you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free services can change their rules at any time.&lt;/strong&gt; That's not a complaint. They don't owe anyone anything. But design for it: isolate your providers behind adapters, and have a plan for the day one disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you can't fix it right away, say so clearly and immediately.&lt;/strong&gt; An honest maintenance page is better than an empty map that makes people think the app is broken.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Final Approach
&lt;/h2&gt;

&lt;p&gt;As for my buddy's buddy's buddy: I still owe him that link. It's coming, I promise, with planes on it.&lt;/p&gt;

&lt;p&gt;Until then, Metal Birds Watch is in the hangar, the mechanics are at work, and the new engine is on order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We promise we'll be flying again together soon.&lt;/strong&gt; 🛫&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the progress in &lt;a href="https://github.com/georgekobaidze/metal-birds-watch/issues/98" rel="noopener noreferrer"&gt;issue #98&lt;/a&gt;, or read the &lt;a href="https://github.com/georgekobaidze/metal-birds-watch/releases/tag/v1.0.2" rel="noopener noreferrer"&gt;v1.0.2 release notes&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>debugging</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Real Talk: You Don't Miss Engineering. You Miss Typing</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Mon, 21 Sep 2026 20:36:39 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/real-talk-you-dont-miss-engineering-you-miss-typing-1bc3</link>
      <guid>https://dev.to/georgekobaidze/real-talk-you-dont-miss-engineering-you-miss-typing-1bc3</guid>
      <description>&lt;h2&gt;
  
  
  A Sci-Fi Scenario Come True
&lt;/h2&gt;

&lt;p&gt;As a kid, when I watched sci-fi movies about computers with human-like thinking, I was completely sure they'd never exist. That was in the early 2000s. Now we live alongside them. Engineering isn't ending. It's starting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nostalgia Isn't an Argument
&lt;/h2&gt;

&lt;p&gt;Lately I keep seeing engineers declare that "real engineering is gone," that "they're done with this s...," that "it's all AI now," and that they miss typing every line themselves. I understand the feeling, but I think it confuses nostalgia with how engineering and science work in general.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Dynamic Era to Be an Engineer
&lt;/h2&gt;

&lt;p&gt;I feel bad for engineers who can't see that this is one of the best times, if not the best time, to be an engineer. Look around: things change almost overnight. For most of human history, life changed slowly, with generations living in roughly the same world as their parents, grandparents, or great-grandparents. We're living through one of the most dynamic eras ever.&lt;/p&gt;

&lt;p&gt;Sure, I still have a soft spot for '80s vibes, even though I wasn't even born yet. But there's no need to invent a problem and suffer over it when there are so many things to catch up with.&lt;/p&gt;

&lt;p&gt;Just ask yourself how many things you don't know that you want to know.&lt;/p&gt;




&lt;h2&gt;
  
  
  We've Been Here Before
&lt;/h2&gt;

&lt;p&gt;Tech and science don't give a damn about the good ol' days. It's always been like this.&lt;/p&gt;

&lt;p&gt;When Grace Hopper built early compilers, skeptics doubted "automatic programming" was serious work. When FORTRAN arrived, programmers doubted compiled code could match hand-written assembly.&lt;/p&gt;

&lt;p&gt;High-level languages, frameworks, and package managers all triggered the same fear, and each time the industry grew. AI is the next rung on the same ladder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Part Was Never the Typing
&lt;/h2&gt;

&lt;p&gt;Never. You spend exponentially more time thinking about the problem than typing the code that expresses your thoughts.&lt;/p&gt;

&lt;p&gt;The real program is the understanding in the engineers' heads, not the source code. AI can generate thousands of lines, but someone still has to hold the theory of why the system works the way it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottleneck Moves
&lt;/h2&gt;

&lt;p&gt;Speeding up one step only helps as much as that step mattered. Make code generation instant, and the constraints shift to requirements, design, review, and debugging. Generating code is getting cheap; verifying it is not. That judgment is engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cheaper Software Means More Software
&lt;/h2&gt;

&lt;p&gt;Every time software got cheaper to build, we built far more of it. Lower costs unlock problems that were never worth solving before.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coding Was Never the Point
&lt;/h2&gt;

&lt;p&gt;What matters is who does the engineering: you, the AI, or both of you together. My bet is on the engineers who choose both and keep the thinking for themselves.&lt;/p&gt;

&lt;p&gt;So please, just stop dwelling on things that are inevitable and focus on the real problems, and the real opportunities will show up.&lt;/p&gt;

&lt;p&gt;Just don't vibe code, please! That's not engineering and nobody can change my mind about that.&lt;/p&gt;

&lt;p&gt;I hope this little note helps a bit.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this brief note? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>software</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Clean Code Is Not the Same as Clear Code: Comments Were Never the Problem</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Mon, 21 Sep 2026 18:07:10 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/clean-code-is-not-the-same-as-clear-code-comments-were-never-the-problem-42n</link>
      <guid>https://dev.to/georgekobaidze/clean-code-is-not-the-same-as-clear-code-comments-were-never-the-problem-42n</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Line That Does Nothing... Except Keep Production Alive&lt;/li&gt;
&lt;li&gt;Some Code Can't Explain Itself&lt;/li&gt;
&lt;li&gt;The Code Knows What. Only You Know Why&lt;/li&gt;
&lt;li&gt;"Just Put It in the Commit Message"&lt;/li&gt;
&lt;li&gt;So What's the Worst That Can Happen?&lt;/li&gt;
&lt;li&gt;How Not to Write Comments&lt;/li&gt;
&lt;li&gt;Clean Gets You Halfway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A Line That Does Nothing... Except Keep Production Alive
&lt;/h2&gt;

&lt;p&gt;Somewhere along the way, our industry decided that writing a comment means you've failed as an engineer and need to "git good" (don't try that in the terminal, it's not a git command). If your code needs explaining, the logic goes, your code isn't good enough.&lt;/p&gt;

&lt;p&gt;I won't lie, I really like the idea. I'd love every codebase to be so clean that you open a file, read it once, and think "yep, got it." Everything logical, everything obvious.&lt;/p&gt;

&lt;p&gt;But...&lt;/p&gt;

&lt;p&gt;I'd also love roads so well designed they don't need signs. But even the most beautifully engineered mountain road has a "sharp bend ahead" sign, and not because the engineers failed. It's there because you can't see the bend until you're already in it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;So here's the quick reality check:&lt;/strong&gt; not every function can explain what it's doing. Not now, not tomorrow, not ever. There will always be code that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Was never written with structure in mind, so nobody understands it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is written just beautifully, but solves a problem so complex that you still need five hours staring at one function to follow it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is simple, clean, and perfectly readable, yet does something for a reason that isn't visible anywhere in the code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Some Code Can't Explain Itself
&lt;/h2&gt;

&lt;p&gt;Here's a perfectly clean piece of C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FlightNumbers&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;GeneratedRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="nf"&gt;FlightNumber&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;FlightNumber&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;IsMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&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;Good names. Modern, source-generated regex. Nothing to refactor, nothing to rename.&lt;/p&gt;

&lt;p&gt;Now, quick quiz. Which of these are valid?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;FlightNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BA123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FlightNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"U21234"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FlightNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"9W5A"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FlightNumbers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"99123"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unless you're an aviation nerd like me, or someone who reads regex fluently (if you do, my goodness, more power to you), you have no idea what this method is actually checking.&lt;/p&gt;

&lt;p&gt;Sure, you can look it up. Sure, you can read the documentation. And sure, there must be unit tests that describe exactly what's valid and what isn't. Right?!&lt;/p&gt;

&lt;p&gt;Well, yes, on paper, however "on paper" and "actually" can be like the burger in the ad and the burger in the box.&lt;/p&gt;

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

&lt;p&gt;Sometimes there's no documentation. Sometimes there are no unit tests. Sometimes there's just you, the regex, and a growing sense of dread.&lt;/p&gt;

&lt;p&gt;But even when the docs and tests exist, why should you have to derail your train of thought, open three tabs, and go on a scavenger hunt, when a single comment could be sitting right there on top of the code, waiting for you?&lt;/p&gt;

&lt;p&gt;Let me show you. Then tell me this isn't infinitely better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IATA flight number, e.g. "BA123", "U21234", "9W5A".&lt;/span&gt;
&lt;span class="c1"&gt;// Airline code is 2 chars: two letters, or a letter-digit mix (U2 = easyJet).&lt;/span&gt;
&lt;span class="c1"&gt;// Then a 1-4 digit flight number and an optional operational suffix letter.&lt;/span&gt;
&lt;span class="c1"&gt;// Uppercase only, no spaces: normalize input before matching.&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;GeneratedRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"^([A-Z]{2}|[A-Z]\d|\d[A-Z])(\d{1,4})([A-Z]?)$"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;partial&lt;/span&gt; &lt;span class="n"&gt;Regex&lt;/span&gt; &lt;span class="nf"&gt;FlightNumber&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus, you also learn things the code could never tell you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Code Knows What. Only You Know Why
&lt;/h2&gt;

&lt;p&gt;The regex was a translation problem: the code was correct, it just spoke a language most humans don't. But there's a second kind of comment, and it's arguably even more valuable, because it covers something the code can't express in any language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxConcurrentRequests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;47&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Named. A constant, not a magic number buried in a loop. Textbook.&lt;/p&gt;

&lt;p&gt;And yet every developer who sees it has the same thought: why 47? It's not a round number. It's not a power of two. It looks like someone's lucky number, or a typo, or the result of a very long night. The provider's documentation says the limit is 50, so surely this is just a mistake.&lt;/p&gt;

&lt;p&gt;So somebody changes it to 50. And a few weeks later, under real traffic, the provider starts rejecting requests at random, and nobody can reproduce it.&lt;/p&gt;

&lt;p&gt;Here's what was missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 47, not 50. The provider documents 50 requests per second, but their&lt;/span&gt;
&lt;span class="c1"&gt;// limiter measures bursts over a 1.2s window, so retries at 50 trip it.&lt;/span&gt;
&lt;span class="c1"&gt;// Recheck if they publish new limits.&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxConcurrentRequests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;47&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part of the code that lives only in the head of whoever wrote it. The code records the decision. The comment records the reasoning. Lose the reasoning, and the decision looks like a bug.&lt;/p&gt;

&lt;p&gt;So, in practice, a good comment does one of four jobs:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Translates
&lt;/h4&gt;

&lt;p&gt;Dense code like a regex, a bit trick, or a math formula, where the what isn't obvious even when the code is perfect.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Explains
&lt;/h4&gt;

&lt;p&gt;The reason behind a choice that looks wrong, arbitrary, or unnecessary.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Warns
&lt;/h4&gt;

&lt;p&gt;What breaks if you change this, and how badly.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Records
&lt;/h4&gt;

&lt;p&gt;A decision with an expiry date. What was tried, what didn't work, and when it's worth revisiting.&lt;/p&gt;

&lt;p&gt;If a comment doesn't do one of those four jobs, it probably shouldn't exist. But if it does, deleting it for the sake of "clean code" isn't cleaning.&lt;/p&gt;

&lt;p&gt;By the way, these are extremely simple examples, in practice, you'll come across much more complex scenarios.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Just Put It in the Commit Message"
&lt;/h2&gt;

&lt;p&gt;At this point, someone in the back raises their hand: the reasoning belongs in git history. Write a good commit message, keep the source clean. That's what version control is for.&lt;/p&gt;

&lt;p&gt;Again, on paper, it sounds disciplined. But in practice, go run git log on any file that's older than a year. I'll wait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
a91f3c2  apply editorconfig
7d2e8b1  fix
3c4f9a0  fix again
e81b7d4  PR feedback
b02c6f5  final fix
f5a1e93  final fix (actually)
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Somewhere in there is the reason &lt;code&gt;MaxConcurrentRequests&lt;/code&gt; is 47. Good luck. Let me know when you find it.&lt;/p&gt;

&lt;p&gt;And what if you don't? You're just going to sit and blame everything and everyone? Or just be pragmatic and write a short comment that'll solve problems for other developers and future you? &lt;strong&gt;Maybe if past you hadn't been so dogmatic you'd have been exponentially happier now?&lt;/strong&gt; Have you thought about that?&lt;/p&gt;

&lt;p&gt;And by the way even if your team writes beautiful commit messages, the approach still breaks down for a few boring, practical reasons.&lt;/p&gt;

&lt;h4&gt;
  
  
  Nobody runs &lt;code&gt;git blame&lt;/code&gt; on code that looks fine
&lt;/h4&gt;

&lt;p&gt;This is the big one. You don't go looking for a reason when you don't know there is one. A constant set to 47 looks like a typo, not like a mystery worth investigating. A comment interrupts you before you make the mistake. Git history only answers questions you already thought to ask.&lt;/p&gt;

&lt;h4&gt;
  
  
  Blame decays
&lt;/h4&gt;

&lt;p&gt;One reformat, one rename, one file split, one squash merge, and the line now points to a commit titled "apply editorconfig." The original reason is still in there somewhere, buried under years of unrelated changes. Finding it is no longer a lookup. It's archaeology. Archaeology takes time and effort.&lt;/p&gt;

&lt;h4&gt;
  
  
  People leave
&lt;/h4&gt;

&lt;p&gt;Sometimes the real documentation isn't the commit history at all. It's Bill. Bill knows why it's 47. Bill also left for a new company two years ago and won't pick up the phone, because he knows you're going to bother him with those questions.&lt;/p&gt;

&lt;p&gt;Commit messages are great at explaining a change: what was different about this commit and why. They're terrible at explaining a current state, because the current state is the sum of dozens of changes, and nobody is going to reconstruct it by reading them in order.&lt;/p&gt;

&lt;p&gt;The comment is the only place that describes the code as it is right now, right where you're looking at it.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What's the Worst That Can Happen?
&lt;/h2&gt;

&lt;p&gt;Let's flip the question. Say you write a comment. What's the worst-case scenario?&lt;/p&gt;

&lt;p&gt;The most common argument I hear is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;"if you change the code, you have to change the comment too."&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;And... so what?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Seriously. How hard is it? The comment is right there. Not in a wiki, not in a Confluence page nobody has opened since 2021, not in a separate repository. It's one line above the code you're already editing. If you can change 47 to 50, you can change the sentence directly on top of it. You're literally looking at it.&lt;/p&gt;

&lt;p&gt;The argument assumes there's some rule that a change should touch only the code and leave everything around it untouched. There isn't. When you change a method's behavior, you update its tests. When you rename a parameter, you update its callers. Updating the comment that describes the thing you just changed is the same kind of work. It's not overhead. It's the job.&lt;/p&gt;

&lt;p&gt;This is part of a bigger pattern: taking a good guideline and following it so strictly that it starts hurting the code it was supposed to help.&lt;/p&gt;

&lt;p&gt;Take DRY. That's a topic for another episode of this series, but ask yourself honestly: does strictly following DRY always make your code better?&lt;/p&gt;

&lt;p&gt;Nuh-uh.&lt;/p&gt;

&lt;p&gt;Sometimes two pieces of code look similar, so someone extracts a shared function to avoid the "duplication." Then the two use cases drift apart a little, so the function grows a parameter. Then another. Then a couple of flags. And six months later you're reading this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="nf"&gt;ProcessOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"legacy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipValidation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations, the code is DRY. The only downside is that nobody knows what it does. Not even future you, by the way.&lt;/p&gt;

&lt;p&gt;Don't tell me you haven't seen, or better yet, written a function/method like this. I have, when I was junior/mid-level developer. And it made my life miserable.&lt;/p&gt;

&lt;p&gt;Sometimes it's perfectly fine to repeat a few lines in two places, because keeping each piece readable on its own matters more than eliminating every repeated pattern. Duplication isn't automatically bad, and abstraction isn't automatically good. Everything is a tradeoff, and knowing which side to pick is exactly the kind of instinct that separates a good developer from someone following a checklist.&lt;/p&gt;

&lt;p&gt;Comments work the same way. "Never write comments" and "comment everything" are both checklists. The right answer is to write the comment when it carries something the code can't, and to keep it updated the same way you keep everything else updated: because it's right there.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Not to Write Comments
&lt;/h2&gt;

&lt;p&gt;Now, before anyone runs off and starts commenting every line: none of this is permission to narrate your code. Bad comments are real, and they're a big part of why comments got a bad reputation in the first place. Here's the hall of shame.&lt;/p&gt;

&lt;h4&gt;
  
  
  The echo
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Increment the retry count&lt;/span&gt;
&lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The XML doc that says absolutely nothing
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// Gets the user&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;param name="id"&amp;gt;The id.&amp;lt;/param&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;returns&amp;gt;The user&amp;lt;/returns&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;GetUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six lines of ceremony and zero information. Every .NET codebase has thousands of these, usually generated by a tool to make a warning go away. If you're going to write a doc comment, tell me something the signature doesn't.&lt;/p&gt;

&lt;h4&gt;
  
  
  The liar
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Retry up to 3 times&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;MaxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The eternal "temporary" fix.
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TODO: temporary workaround, remove later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later when? Remove it how? Under what conditions? This comment was written in 2019 and it will outlive us all. If something is temporary, say what it's waiting for: a ticket, a version, a date.&lt;/p&gt;

&lt;h4&gt;
  
  
  The graveyard
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// var result = await _legacyService.CalculateAsync(order);&lt;/span&gt;
&lt;span class="c1"&gt;// if (result.IsValid) { ... }&lt;/span&gt;
&lt;span class="c1"&gt;// var result2 = await _newService.CalculateAsync(order);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commented-out code tells the reader nothing except that someone was scared to delete it. Delete it. If you ever need it again, you won't.&lt;/p&gt;

&lt;h4&gt;
  
  
  The novel
&lt;/h4&gt;

&lt;p&gt;A thirty-line method with a three-paragraph comment on top explaining how it works. Sometimes that's necessary. Often it's a sign the code should be rewritten, and the comment is compensating. Try the refactor first. Then comment whatever complexity is left.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clean Gets You Halfway
&lt;/h2&gt;

&lt;p&gt;Clean code is a great goal. Good names, small methods, clear structure: keep doing all of it. But clean code answers one question, what does this do?, and a real codebase keeps asking more. Why is it like this? What happens if I change it? Is this weird thing a bug or a scar?&lt;/p&gt;

&lt;p&gt;Those answers don't live in the syntax. They live in the head of whoever wrote the code, and heads are terrible storage. They change jobs, they go on vacation, and they forget things by Tuesday.&lt;/p&gt;

&lt;p&gt;So here's the rule I actually follow, and it fits in one sentence:&lt;/p&gt;

&lt;p&gt;If I had to stop and think before writing a line, I write down what I thought.&lt;/p&gt;

&lt;p&gt;If the line was obvious, I leave it alone. No echoes, no ceremony, no dragons. But if there was a moment where I went "hmm, careful here," that moment goes into a comment, because the next reader is going to have the exact same "hmm," just without the answer.&lt;/p&gt;

&lt;p&gt;Clean code tells the reader what you did. A good comment tells them what you knew. You need both.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>discuss</category>
      <category>documentation</category>
      <category>coding</category>
    </item>
    <item>
      <title>Vibe Coding Isn't the Problem. Calling It Engineering Is</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Sun, 13 Sep 2026 07:06:59 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/vibe-coding-isnt-the-problem-calling-it-engineering-is-lm1</link>
      <guid>https://dev.to/georgekobaidze/vibe-coding-isnt-the-problem-calling-it-engineering-is-lm1</guid>
      <description>&lt;h2&gt;
  
  
  Let's Address the Elephant in the Room Again
&lt;/h2&gt;

&lt;p&gt;Vibe coding has always been a weird topic to talk about. Lately it's gotten even weirder.&lt;/p&gt;

&lt;p&gt;Say &lt;strong&gt;"vibe coding is a creative process, but it's not engineering"&lt;/strong&gt; and people get defensive immediately, calling you a gatekeeper and all those interesting terms.&lt;/p&gt;

&lt;p&gt;So I decided to write this brief article to answer those opinions in a short and precise manner, without too much sugarcoating or dancing around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering is Part of Creation
&lt;/h2&gt;

&lt;p&gt;There's a real difference between the two. Creation is much broader term. engineering is part of the creation process. It's also part of maintenance, debugging, refactoring, and most of what engineers actually spend their days doing.&lt;/p&gt;

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

&lt;p&gt;So the question worth asking is: during the creation process, who does the engineering? You? Your coding agent? Both of you?&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions Matter Here, Critically
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vibe Coding
&lt;/h3&gt;

&lt;p&gt;You write a prompt and hand it to the AI. &lt;strong&gt;You don't edit the code and you don't review it.&lt;/strong&gt; You might not know the first thing about programming and still ship something.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Assisting
&lt;/h3&gt;

&lt;p&gt;AI-generated, human-reviewed. The AI writes all of it, you read all of it. &lt;strong&gt;THIS IS NOT VIBE CODING&lt;/strong&gt;, it takes enough skill to tell good code from bad.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-assisted
&lt;/h3&gt;

&lt;p&gt;You and the AI write the code together, and you're the one driving. AI gives you speed, a second opinion, and the edge cases you'd have missed. &lt;strong&gt;ALSO NOT VIBE CODING&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where This is Going
&lt;/h2&gt;

&lt;p&gt;The trend is toward more semantics and more abstraction. It's been that way since assembly. Vibe coding is the next step on that line, and it will keep taking over more of the work. Maybe one day the job is 95% writing prompts, and to be honest, I'll welcome it, that makes my life easier too.&lt;/p&gt;

&lt;p&gt;But the question right now is whether we're ready to build financial systems, health apps, and anything else touching sensitive data without analyzing the code.&lt;/p&gt;

&lt;p&gt;We're not. Doing it anyway to prove your point will cause absolute chaos.&lt;/p&gt;

&lt;p&gt;The line I'd draw:&lt;/p&gt;

&lt;p&gt;Building something for yourself, for fun? Vibe code all of it. It's genuinely a good time.&lt;/p&gt;

&lt;p&gt;Building something real, something that processes people's personal data, handles money, and turns a profit and vibe coding it from start to finish? That tells me one of three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;You're not willing to spend two to four weeks learning the absolute basics of coding, security, and best practices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You don't care about the quality of your own product, and ultimately other people's personal data and security.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You're not curious about how any of it works under the hood. And if you're calling yourself an engineer, you should be eager to understand the mechanics of software engineering and coding.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All three happen constantly now, and that's the sad part.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Do?
&lt;/h2&gt;

&lt;p&gt;I've been a software engineer for ten years. Even so, when I need to build something in a language I'm not fluent in, I still spend a day or two on the language reference, the docs, the basic mechanics, enough that I can understand what the AI produces.&lt;/p&gt;

&lt;p&gt;Then I review everything it generates and ask so many follow-up questions that &lt;strong&gt;I probably spend more tokens interrogating the code than creating the project&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every strong engineer I know works this way. We use AI extensively, every single day. We also take everything it gives us with a grain of salt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2oc2s9nt7r1ijuwgy8xq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2oc2s9nt7r1ijuwgy8xq.jpeg" alt="A Grain of Salt Next to a Keyboard and Rubber Duck" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So what's stopping you from doing the same?&lt;/p&gt;

&lt;p&gt;We'll get to the point where you can let go of the steering wheel. We're not there yet. Keep holding the wheel to make sure you don't go off the road.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this write-up? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>coding</category>
      <category>development</category>
    </item>
    <item>
      <title>Show Me the Article You're Most Proud Of And I'll Read Every Single One of Them</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:30:37 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/show-me-the-article-youre-most-proud-of-and-ill-read-every-single-one-of-them-1l53</link>
      <guid>https://dev.to/georgekobaidze/show-me-the-article-youre-most-proud-of-and-ill-read-every-single-one-of-them-1l53</guid>
      <description>&lt;h2&gt;
  
  
  I Write More Than I Read
&lt;/h2&gt;

&lt;p&gt;I used to read a lot on DEV. Then I started writing my own articles, and somewhere along the way I stopped reading nearly as much as I used to.&lt;/p&gt;

&lt;p&gt;I want to fix that. There are so many good writers on here that it's genuinely overwhelming, and I'd rather be reading them than not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rules
&lt;/h2&gt;

&lt;p&gt;Doesn't matter if you're a veteran tech writer or you published your first post last week.&lt;/p&gt;

&lt;p&gt;👉 Pick one article you're most proud of. &lt;strong&gt;Just one.&lt;/strong&gt; It doesn't have to be the community favorite or the one with the most reactions, pick the one you love the most.&lt;br&gt;
👉 Drop the link in the comments.&lt;br&gt;
👉 Optional: tell me why it's special to you. One paragraph is plenty.&lt;/p&gt;

&lt;p&gt;I'll read it and give you my honest feedback. I promise! 🤝&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother
&lt;/h2&gt;

&lt;p&gt;Maybe your favorite post never got the attention you thought it deserved. Maybe it got plenty, and you'd just like an excuse to share it again.&lt;/p&gt;

&lt;p&gt;Either way, it's sitting in your archive and nobody's read it in months. This is a chance to put it back in front of people.&lt;/p&gt;

&lt;p&gt;And it works both ways, read what other people drop in the comments. That's the whole point.&lt;/p&gt;

&lt;p&gt;Showcase your work, appreciate everyone else's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's build a hall of fame of DEV's best work! 🌟&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>community</category>
      <category>discuss</category>
      <category>showdev</category>
      <category>writing</category>
    </item>
    <item>
      <title>The Same Day, 180 Times: How Does the Unglamorous Machinery of Locking In Actually Work?</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Sun, 06 Sep 2026 17:36:04 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/the-same-day-180-times-how-does-the-unglamorous-machinery-of-locking-in-actually-work-4n0l</link>
      <guid>https://dev.to/georgekobaidze/the-same-day-180-times-how-does-the-unglamorous-machinery-of-locking-in-actually-work-4n0l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Inspiration is for amateurs, the rest of us just show up and get to work."&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Chuck Close&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This Wasn't Supposed to Be the Next Article&lt;/li&gt;
&lt;li&gt;
What Locking In Actually Means

&lt;ul&gt;
&lt;li&gt;The Longer You Practice Consistency, The More Consistent You Get&lt;/li&gt;
&lt;li&gt;The Rule of 100: The Compounding Effect of Life&lt;/li&gt;
&lt;li&gt;Vacations Happen Roughly Once Every Six Months&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
It's Day One of 180. So What Do You Do?

&lt;ul&gt;
&lt;li&gt;Just Plan Things&lt;/li&gt;
&lt;li&gt;Sharpen Your Tools&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Now That You're Well Into The Camp Everything Matters

&lt;ul&gt;
&lt;li&gt;The System Is the Point&lt;/li&gt;
&lt;li&gt;Same Hours, Every Day&lt;/li&gt;
&lt;li&gt;Clock Out Twice&lt;/li&gt;
&lt;li&gt;Put Your Phone Away&lt;/li&gt;
&lt;li&gt;The Body Is Part of the System&lt;/li&gt;
&lt;li&gt;Day Five Is Usually When It Turns&lt;/li&gt;
&lt;li&gt;Checkpoints Get You Through the Gap&lt;/li&gt;
&lt;li&gt;Recharge on Purpose&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;How to Deal With Bad Days&lt;/li&gt;
&lt;li&gt;Commit to More Than One Thing, Just Not at Once&lt;/li&gt;
&lt;li&gt;
My Suggestion to You

&lt;ul&gt;
&lt;li&gt;Congratulations&lt;/li&gt;
&lt;li&gt;Start Slow&lt;/li&gt;
&lt;li&gt;Lonesome Is a State of Mind&lt;/li&gt;
&lt;li&gt;By The Way&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  This Wasn't Supposed to Be the Next Article
&lt;/h2&gt;

&lt;p&gt;It's 6:00 AM on a Sunday. I haven't had coffee yet, and I thought about fixing that before I started. I didn't. The longer I do this, the more I think the pre-coffee excuse is just a licence to do something badly and call it circumstance.&lt;/p&gt;

&lt;p&gt;I hadn't planned to write this one either. My last piece on this topic &lt;a href="https://dev.to/georgekobaidze/a-new-personal-best-what-six-months-of-locking-in-can-do-p3k"&gt;A New Personal Best: What Six Months of Locking In Can Do&lt;/a&gt; got more engagement than I ever expected, which included praise and a handful of healthy critiques. I had a stack of technical articles queued up behind it. They can wait.&lt;/p&gt;

&lt;p&gt;The questions were mostly the same question, asked different ways. That article was a list of outcomes: two hackathon wins, a new role, a charity donation, thirteen articles. What people wanted was the part I left out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the days actually looked like.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So that's what this is. The routine, written down as it ran. Roughly the same day, 180 times. It is far less interesting than the results made it look. But if you're anything like me, you love to hear about the process even more than the results.&lt;/p&gt;

&lt;p&gt;Let's just get it done.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Locking In Actually Means
&lt;/h2&gt;

&lt;p&gt;Locking in sounds dramatic, and it isn't. I don't lock myself up in a room for six months and I don't go up a mountain to think about nothing but work while meditating for 12 hours a day. That stuff ain't for me.&lt;/p&gt;

&lt;p&gt;Quite the contrary.&lt;/p&gt;

&lt;p&gt;These are the stretches when I'm most active socially and most active on my own projects at the same time.&lt;/p&gt;

&lt;p&gt;Strip the phrase down and it's ordinary: sustained, hyperfocus on a goal and on the process that gets you to it. The only unusual part is how long I hold it. Six months. Quite a stretch, huh?&lt;/p&gt;

&lt;p&gt;There are reasons it has to run that long.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Longer You Practice Consistency, The More Consistent You Get
&lt;/h3&gt;

&lt;p&gt;That sounds obvious. The interesting part is why it happens.&lt;/p&gt;

&lt;p&gt;Your brain has that weird and useful feature to handle almost anything you hand it repeatedly. That includes things you actively disliked at the start: training, sitting with a hard problem, running errands, even organizing your desk. Do them often enough and they stop costing what they used to. Some of them start pulling at you instead.&lt;/p&gt;

&lt;p&gt;The mechanism isn't mysterious. Repetition makes a thing part of your baseline, and the baseline is what your brain treats as normal. Once something is in there, skipping it doesn't feel like a day off. It feels like a day with something missing from it.&lt;/p&gt;

&lt;p&gt;Here's roughly how that went for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One week:&lt;/strong&gt; A good start and nothing more. Nothing is in the system yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One month:&lt;/strong&gt; It's getting easier. You still have to decide to do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three months:&lt;/strong&gt; A habit. You notice when you skip it. It's significant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six months:&lt;/strong&gt; Part of you. Skipping it costs more than doing it, and your brain actively protests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things amplify all of this when there's a reward waiting at the end: &lt;strong&gt;motivation and anticipation&lt;/strong&gt;.&lt;br&gt;
They aren't the same force and confusing them is why people stall around month two.&lt;/p&gt;

&lt;p&gt;Motivation starts you. It's whatever you're after: a skill, money, connections, a promotion, recognition, or everything together.&lt;/p&gt;

&lt;p&gt;Anticipation carries you. Same reward, different job. Motivation is spent the moment you begin, anticipation is what you run on for the six months after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Rule of 100: The Compounding Effect of Life
&lt;/h3&gt;

&lt;p&gt;I borrowed this from &lt;a href="https://en.wikipedia.org/wiki/Jesse_Itzler" rel="noopener noreferrer"&gt;Jesse Itzler&lt;/a&gt;, an American entrepreneur, who's also one of my favorite authors.&lt;/p&gt;

&lt;p&gt;The rule of 100 basically means the following:&lt;/p&gt;

&lt;p&gt;If you spend 100 hours a year on any discipline, about 17 minutes a day, you'll end up better than 95% of the world's population at that discipline.&lt;/p&gt;

&lt;p&gt;Pick any discipline you want: coding, playing basketball, playing guitar, boxing, riding a bike. It doesn't really matter which one.&lt;/p&gt;

&lt;p&gt;If you practice it for 17 minutes a day, within a year you'll be more skilled than most people in the world. This doesn't mean you'll become a boxing champion, an NBA player, or a professional software engineer. But you'll be pretty decent.&lt;/p&gt;

&lt;p&gt;Now imagine what you can achieve if you do something for hours a day. You can reach a professional level in a few years if you stay consistent. But keep in mind: consistency isn't measured in time alone. &lt;strong&gt;The hours only count if you put real effort into them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is called the compounding effect of life.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vacations Happen Roughly Once Every Six Months
&lt;/h3&gt;

&lt;p&gt;On average, people take a big vacation about once every six months, and I'm no different. When I take one, I always travel somewhere to clear my head. Traveling after six months of locking in is one of the most rewarding feelings there is. You know you got through that "six-month camp," you have results you're proud of, and now you're out in your favorite countries and cities. Oh gosh, there's nothing like it.&lt;/p&gt;

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

&lt;p&gt;If a trip falls inside a lock-in period, I try not to step away from the work completely. Even without my laptop, I'll research things on my phone on a plane or a train, where there's plenty of time anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Day One of 180. So What Do You Do?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Just Plan Things
&lt;/h3&gt;

&lt;p&gt;Nothing dramatic. There's no starting gun. Day one is for preparation.&lt;/p&gt;

&lt;p&gt;I spend it on prerequisites: a rough map of what I'm doing over the next six months, and roughly when. It's the dullest day of the entire stretch and skipping it is the most expensive mistake on offer.&lt;/p&gt;

&lt;p&gt;Here's the thing about that map: &lt;strong&gt;it's wrong&lt;/strong&gt;. Some things slip, others land early. Even the most disciplined person alive doesn't hit the schedule they drew on day one, and I never have either.&lt;/p&gt;

&lt;p&gt;That's fine, because the dates were never the point.&lt;/p&gt;

&lt;p&gt;You're not trying to predict when you'll reach the goal, or even its sub-goals. You're deciding what you'll do every single day, repeatedly to get closer to it. &lt;strong&gt;Plan the input, not the outcome.&lt;/strong&gt; Input is something that you actually control, so make it precise. If you say: &lt;em&gt;"I'm going to spend 4 hours a day on this project"&lt;/em&gt;, &lt;strong&gt;mean it!&lt;/strong&gt; Making claims you don't follow through on will cost you trust in your own word. &lt;strong&gt;And there's nothing worse than not trusting yourself.&lt;/strong&gt; If you can't count on yourself, how can anyone (your team, your boss, your friends, your loved ones) count on you?&lt;/p&gt;

&lt;p&gt;So spend day one planning. If you're building something, decide which features come first, then break the work down until you're left with pieces small enough to estimate honestly. Usually there's more than one thing running at once - a project, articles, open source, competitions, whatever else you've signed up for. Deciding roughly when you'll do what is what stops those from collapsing into each other.&lt;/p&gt;

&lt;p&gt;The worse alternative is working on whatever you feel like that evening. And whatever you feel like, six months out, adds up to nothing. That'll just waste your time. Don't do that to yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharpen Your Tools
&lt;/h3&gt;

&lt;p&gt;Take a look around your workspace. Yes, right now. What does it look like? Messy or organized?&lt;/p&gt;

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

&lt;p&gt;If it's organized, congratulations, that box is already ticked.&lt;/p&gt;

&lt;p&gt;If it's messy, start there. Discipline isn't just saying "I'm going to review three pull requests today."&lt;/p&gt;

&lt;p&gt;A few weeks ago, when I started my current six-month lock-in period, do you know what the very first thing I did was?&lt;br&gt;
I washed my car. Then I cleaned my laptops and my keyboard. Sounds weird, right? It does, but it also works for me like a charm.&lt;/p&gt;

&lt;p&gt;Real discipline is the kind that's embedded in your whole system, your life, not just your projects. The broader it is, the more sustainable it is and the more it carries into everything else you do.&lt;/p&gt;

&lt;p&gt;If your car is full of garbage, soda cans and old receipts and your living room has last month's pizza boxes and empty bottles scattered around it, there's a good chance you're not consistent in your repositories either. Best practices, timely reviews, pruning dead branches and stale pull requests, all of it tends to be sloppy for the same reason. Not because you don't know better, but because discipline isn't a thing you switch on when the editor opens, or not even when your eyes open in the morning.&lt;/p&gt;

&lt;p&gt;So start with the basics. You'll be surprised how much it changes your attitude toward the actual work. You'll feel proud of yourself, and you'll trust yourself more.&lt;/p&gt;

&lt;p&gt;Once that's done, get specific. For example, if you're building a new app, make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every tool installed, configured, and updated&lt;/li&gt;
&lt;li&gt;Every dependency installed and resolving cleanly&lt;/li&gt;
&lt;li&gt;The latest version of your framework. You don't want to start on an old one unless there's a specific reason for it&lt;/li&gt;
&lt;li&gt;A repository that's ready on day one: README, license, .gitignore, branch naming, branch protection&lt;/li&gt;
&lt;li&gt;Formatting and linting rules committed, so you're not arguing with your editor in week three&lt;/li&gt;
&lt;li&gt;CI running green on an empty commit, before there's anything to break&lt;/li&gt;
&lt;li&gt;Secrets and environment variables sorted, documented, and out of source control&lt;/li&gt;
&lt;li&gt;Somewhere to track work: a board, issues, whatever you'll actually open (I usually use GitHub projects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And many other things.&lt;/p&gt;

&lt;p&gt;Every item on that list is a small piece of friction you're removing now, while setting things up is still interesting, so that it can't stop you later. Because it will be day 56 at eleven at night, you'll have forty minutes and no energy, and the difference between doing something and doing nothing will come down to whether the build runs on the first try. Clear the obstacles while you're still enthusiastic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Now That You're Well Into The Camp Everything Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The System Is the Point
&lt;/h3&gt;

&lt;p&gt;When I'm in the middle of a camp like this, which I am right now, the only thing that matters is following the system. Not feeling good about it. Not being perfect at it. Following it.&lt;/p&gt;

&lt;p&gt;You will not be perfect. Some days are slow, others are faster. Some days you'll do half of what you planned, and some days you'll do none of it. That's fine. A system you follow 80% of the time for six months will beat a system you follow perfectly for nine days and then abandon. The goal isn't a clean record. It's that the thing keeps running while you're tired, bored, annoyed, or unconvinced.&lt;/p&gt;

&lt;p&gt;One rule that's worth more than the rest: never miss twice. One missed day is noise. Two missed days is the new pattern - a very very bad bad pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Same Hours, Every Day
&lt;/h3&gt;

&lt;p&gt;Wake up at the same time. Go to sleep at the same time. Work in the same windows. It sounds joyless and it's the single highest-leverage thing on this list, because a fixed schedule means you stop spending willpower on when. The decision is already made, so all you have to do is show up.&lt;/p&gt;

&lt;p&gt;Find out how long you can actually concentrate before the quality drops, not how long you can sit at a desk, how long you can genuinely hold a problem in your head. For most people it's shorter than they'd like to admit. Build your blocks around that real number instead of the one you wish you had and then work on stretching it. &lt;strong&gt;Attention is trainable&lt;/strong&gt;, and it pays out in everything else you do.&lt;/p&gt;

&lt;p&gt;Protect the first block of the day for the hardest thing. Whatever gets your best hours is what actually gets done. And once you've gotten over with that hard part, the rest of the day is almost like coasting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clock Out Twice
&lt;/h3&gt;

&lt;p&gt;Draw a hard line between your day job and your personal work. Not a soft one, a hard one, with a time on it. I know, it's much harder than it sounds, I'm speaking from experience, but it's not impossible. It'll actually make way more productive on both sides of the fence.&lt;/p&gt;

&lt;p&gt;You also need a marker for done. Decide in advance what finishing looks like today, and when you hit it, stop. Being unable to stop is not discipline, it's the thing that ends six-month runs early.&lt;/p&gt;

&lt;p&gt;Do one thing in a given time. Only one.&lt;/p&gt;

&lt;p&gt;I've watched people brag about working on four or five things at once, and I used to be one of them. Here's what I eventually understood: doing five things simultaneously mostly means screwing up all five things simultaneously, because none of them get the quality they need.&lt;/p&gt;

&lt;p&gt;There's a real cost to task switching, too. Every jump between contexts leaves part of your attention stuck on the last thing, and you pay that tax on every single switch. It doesn't feel like a cost, which is exactly what makes it expensive. Ten context switches an hour and you're already exhausted without getting any quality work done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Put Your Phone Away
&lt;/h3&gt;

&lt;p&gt;It goes without saying that you should stay the hell away from social media. I mean it, put the phone down.&lt;/p&gt;

&lt;p&gt;I don't think most people have honestly reckoned with what those stupid reels do to their ability to sit with a hard problem. You train your attention with whatever you feed it, and if you feed it seven-second clips all evening, then sit down to debug something that needs forty uninterrupted minutes, you're going to find the machinery doesn't work anymore. It isn't that you've lost the ability. It's that you've spent months practicing the exact opposite "skill".&lt;/p&gt;

&lt;p&gt;The fix is boring and it's mechanical: get the apps off the phone, or just don't touch the phone unless you absolutely need to. Willpower is not a strategy here, and you're not competing only against your own laziness. You're competing against companies who have spent billions figuring out how to win this specific fight.&lt;/p&gt;

&lt;p&gt;Think about how much pointless content you consume per day and how much great content you could've actually produce if you had used that time productively.&lt;/p&gt;

&lt;p&gt;I'm not saying cut it out entirely. I still watch videos to keep up with things I like during breaks and over lunch. But there's a line and it isn't blurry. When I sit back down, the phone is done for the next block.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Body Is Part of the System
&lt;/h3&gt;

&lt;p&gt;For me this was never only about the hours at the desk. The rest of your life either supports the goal or quietly undermines it.&lt;/p&gt;

&lt;p&gt;Think like a professional athlete.&lt;/p&gt;

&lt;p&gt;Sleep properly. You'll be tired at the end of every day and that's normal, but tired and under-slept aren't the same thing. Sleep is where the recovery actually happens and it's the first thing people sacrifice when they're behind. It's also the worst possible thing to sacrifice, because everything you're trying to do runs on the organ that sleep repairs - your brain.&lt;/p&gt;

&lt;p&gt;I used to stay up all night or two nights in a row, writing code, but it'd take 2 full days to recover after from that hardcore session, which isn't productive.&lt;/p&gt;

&lt;p&gt;Eat like it matters. It affects your thinking more than you'd expect. Junk food, sugar, fat, carbs don't just make you slower physically, they make the afternoons foggy, and foggy afternoons are where your six months quietly leak away.&lt;/p&gt;

&lt;p&gt;And obviously, stay away from alcohol and cigs. Not only because they'll cost you a productive evening, but because they'll cost you the brain and the body that your own life depends on. Well, unless you want to destroy both, your brain and body, for some reason.&lt;/p&gt;

&lt;p&gt;Move most days, preferrably every day. It doesn't have to be hard or complicated. Walking outside, or an hour on a treadmill, is enough. It's fine to do the bare minimum if being athlete-level fit isn't your goal. It pays back in ways that aren't obvious. Better sleep, steadier energy, fewer bad afternoons, and a real confidence boost that comes from doing a moderately unpleasant thing that most people skip.&lt;/p&gt;

&lt;p&gt;None of this is optimization for its own sake. It's maintenance on the machine that has to run for 180 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day Five Is Usually When It Turns
&lt;/h3&gt;

&lt;p&gt;Here's the part nobody warns you about.&lt;/p&gt;

&lt;p&gt;Day one is great. You're excited, the plan is fresh, you have all the energy in the world. &lt;/p&gt;

&lt;p&gt;Day two, slightly less. &lt;/p&gt;

&lt;p&gt;By day five you absolutely hate it.&lt;/p&gt;

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

&lt;p&gt;That's not a sign you picked the wrong goal. The goal is fine. That's just what motivation does, it's a starting force, and it wears off.&lt;/p&gt;

&lt;p&gt;The problem is that this happens before discipline has had time to develop, so for a couple of weeks you're in a gap with no motivation left and no habit yet to carry you. That gap is where almost everyone quits. I've quit there myself a few times.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Checkpoints Get You Through the Gap
&lt;/h3&gt;

&lt;p&gt;The technique that worked for me was scheduled checkpoints. Every two weeks, on a Monday, I'd stop and look at what's actually happened.&lt;/p&gt;

&lt;p&gt;Two things make it work. First, you look at the record instead of the feeling. The feeling on day twelve is "I'm getting nowhere." The record says you shipped four things, wrote 2 articles and the record is right. That's why it's worth keeping a rough log as you go, a checkpoint with no data is just a mood.&lt;/p&gt;

&lt;p&gt;Second, judge yourself on what you did, not on what it produced. Results arrive on their own schedule and mostly late. Whether you did the work is knowable today.&lt;/p&gt;

&lt;p&gt;Then treat the checkpoint as a small restart: readjust the plan, cut what isn't working, remind yourself what the end goal actually is, and go again. You keep doing that until the day you notice you don't need the checkpoints anymore, because the thing has become who you are. Now you're a disciplined guy or a girl.&lt;/p&gt;

&lt;p&gt;That's when it stops being hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recharge on Purpose
&lt;/h3&gt;

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

&lt;p&gt;You have to let the system cool down every once in a while. Pick a day, let's say once every two weeks, or once a week if you need it, where you deliberately move slowly or do nothing at all. I mean the personal work, obviously. I'm not telling you to take the day off from your job.&lt;/p&gt;

&lt;p&gt;Spend that day on something you genuinely enjoy. This matters more than it sounds. If you don't feel like playing video games, don't play them just for the sake of "taking a break." If you don't feel like going out and socializing, don't go out because someone told you that's what fun looks like. It's fine to decline a casual invitation, the real friends will understand.&lt;/p&gt;

&lt;p&gt;One warning, though: make sure that day doesn't quietly turn into three. That's a new pattern forming, and it needs to be spotted and terminated early. One day restores your momentum. Three days end it.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Deal With Bad Days
&lt;/h2&gt;

&lt;p&gt;We've talked a lot about discipline and how it answers almost everything. But some days are just awful. Everyone gets them. Some stretches, most days are awful.&lt;/p&gt;

&lt;p&gt;I had one recently.&lt;/p&gt;

&lt;p&gt;Back in March I won the &lt;a href="https://dev.to/devteam/congrats-to-the-github-copilot-cli-challenge-winners-2240"&gt;GitHub Copilot CLI challenge&lt;/a&gt;, came first out of more than 400 participants with this project: &lt;a href="https://dev.to/georgekobaidze/metal-birds-watch-copilot-cli-helped-me-watch-planes-without-looking-up-4ha0"&gt;Metal Birds Watch&lt;/a&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8hlna5952bj9009osn2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8hlna5952bj9009osn2f.png" alt="Metal Birds Watch" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's still the biggest thing I've ever won, and it's special to me for a second reason: I used it to make a significant donation to charity.&lt;/p&gt;

&lt;p&gt;One of the prizes was a ticket to &lt;a href="https://githubuniverse.com/" rel="noopener noreferrer"&gt;GitHub Universe&lt;/a&gt; in San Francisco this October. I've been looking forward to it all year.&lt;/p&gt;

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

&lt;p&gt;I just found out I'm not going to be able to go. The reason is completely outside my control.&lt;/p&gt;

&lt;p&gt;It sucks, right?&lt;/p&gt;

&lt;p&gt;It absolutely does. It especially sucks when you've done everything right and the thing standing in the way isn't yours to fix. That win was one of the high points of my life. This is one of the sharpest disappointments of it.&lt;/p&gt;

&lt;p&gt;But that's life. It's unfair, there's nothing new about that, and if I sit here and dwell on it the only thing I'll actually change is my own state of mind - for the absolute worse.&lt;/p&gt;

&lt;p&gt;So here's what happened on the evening I found out: I opened my editor and worked my a$$ off on one of my personal projects. Not heroically, not particularly well. I just did the block that was on the schedule.&lt;/p&gt;

&lt;p&gt;That's the whole argument for the system, and it's why this section isn't a detour from the rest of the article. On a good day, discipline is a nice-to-have, you'd have done the work anyway, because you wanted to. On a day like that one, it's the only thing standing between you and a week you don't get back. The routine doesn't ask how your news went. It just says it's eight o'clock.&lt;/p&gt;

&lt;p&gt;Something didn't work out for you? Fine. Try again, or try something else. Life is unfair sometimes, and it's also full of opportunities you haven't looked at yet. But don't wait until you feel better before you start moving again. Moving again is what makes you feel better.&lt;/p&gt;




&lt;h2&gt;
  
  
  Commit to More Than One Thing, Just Not at Once
&lt;/h2&gt;

&lt;p&gt;If you want to give the whole six months to a single thing because of its size or importance, go for it. But it helps to have somewhere else to put your attention when you're sick of staring at the same problem for the fourth day running and it's better if that somewhere is fundamentally different from the main work.&lt;/p&gt;

&lt;p&gt;Right now I've got the biggest personal project I've ever attempted. Realistically it's five or six months just to reach a basic, shippable version (or so I hope). Alongside it I'm still writing articles, recording videos, and entering challenges. And last but definitely not least, I've started learning German on my own, no teacher, no class, nobody. My argument is that I'm a software engineer and I know a thing or two about learning languages, I'm hoping to have the basics in about a year. So far I'm loving it.&lt;/p&gt;

&lt;p&gt;That sounds like it contradicts what I said earlier about doing one thing at a time. It doesn't, and the distinction is worth being precise about: multiple commitments, never simultaneous ones. Nothing on that list ever shares an hour with anything else on it. They share a week. That's the difference between variety and multitasking, and it's the whole game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So allocate days, not attention.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lock-in period can also contain smaller lock-in periods inside it. You block off a week or two for something specific, finish it, and return to whatever you were doing before.&lt;/p&gt;

&lt;p&gt;Say you're deep in your main project and DEV Community posts a challenge you can't turn down. You pause the main work, lock in on the challenge for a couple of weeks, then lock back into the primary thing. It works like a function call stack, the last thing you pushed is the first thing that gets finished and then you unwind back down to where you were. LIFO.&lt;/p&gt;

&lt;p&gt;The catch is the same one you'd recognize in code. A stack only works if the frames actually return. Push something new on top every time a shinier thing appears and never pop any of it, and you don't have a system, you have a leak. Six months later there's nothing finished at the bottom of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Suggestion to You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Congratulations
&lt;/h3&gt;

&lt;p&gt;If you've made it this far and you've read the whole thing, congratulations. This article turned out much longer than I anticipated, and reading it end to end in one sitting says something real about your attention span. That's not a throwaway compliment either, the ability to stay with something long after it stops being novel is the best predictor I know of whether you'll finish a six-month run or abandon it in week three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Slow
&lt;/h3&gt;

&lt;p&gt;If you've never done anything like this, don't start at six months. Start at one, with something specific you want finished by the end of it. Then raise it to three. What you're really practicing is patience, attention, and the ability to finish what you started, those are real skills, and they can be trained like any other. Pull off three months and six is absolutely within reach. At that point it's just a decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lonesome Is a State of Mind
&lt;/h3&gt;

&lt;p&gt;This is actually a song by Djo - great song by the way. But the statement itself is also very true. Especially when it comes to this topic.&lt;/p&gt;

&lt;p&gt;Do it quietly. Tell nobody, or tell only the people closest to you. Two reasons for that. First, announcing a plan hands you a hit of almost the same satisfaction you'd get from actually doing the thing, you collect the congratulations up front, and the drive that was supposed to carry you leaks out through your own mouth.&lt;/p&gt;

&lt;p&gt;Second, very few people are going to come with you on a commitment this long. Be ready for that. If you're not a lone wolf type of person like I am, someone genuinely comfortable doing things alone, then you need to either find at least one person willing to run the same stretch alongside you, or get comfortable with the quiet.&lt;/p&gt;

&lt;p&gt;And finally, this isn't an F1 sprint race. It's the 24 Hours of Daytona, not an F1 sprint race. Nobody wins it in the first hour. You win it by still being on track when most of the field isn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  By The Way
&lt;/h3&gt;

&lt;p&gt;This is also a song name this one's by Red Hot Chili Peppers. But it also fits the context.&lt;/p&gt;

&lt;p&gt;Speaking of F1. As I was writing this I realized I'd missed the race at Monza, Italy. Got so carried away that it went straight past me. It's fine, I'll watch the highlights.&lt;/p&gt;

&lt;p&gt;Let's get to that checkered flag!🏁&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this deep dive? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>community</category>
      <category>journey</category>
    </item>
    <item>
      <title>Backend Engineer (Me) Ships a Browser Game With One Unintentional System Requirement: My Monitor</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Sun, 09 Aug 2026 19:42:53 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/backend-engineer-me-ships-a-browser-game-with-one-unintentional-system-requirement-my-monitor-1ojd</link>
      <guid>https://dev.to/georgekobaidze/backend-engineer-me-ships-a-browser-game-with-one-unintentional-system-requirement-my-monitor-1ojd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer Watching Instead?
&lt;/h2&gt;

&lt;p&gt;If reading isn't your thing, no worries, I've got you covered. You can watch the video below:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/m1vRJxHabJw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction: In What Universe Did I Think This Was a Good Idea?&lt;/li&gt;
&lt;li&gt;The Game&lt;/li&gt;
&lt;li&gt;The Game Loop&lt;/li&gt;
&lt;li&gt;The Game Physics&lt;/li&gt;
&lt;li&gt;It Worked on My Machine Goddammit&lt;/li&gt;
&lt;li&gt;Understanding the Root Cause&lt;/li&gt;
&lt;li&gt;The Fix: Delta Time&lt;/li&gt;
&lt;li&gt;
But There's a Catch: Not Everything Is Fixed With &lt;code&gt;× dt&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Pattern 1: Linear operations&lt;/li&gt;
&lt;li&gt;Pattern 2: Exponential decay&lt;/li&gt;
&lt;li&gt;Pattern 3: Lerp smoothing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Summary: The Three Patterns&lt;/li&gt;
&lt;li&gt;One More Thing: Re-tune Your Constants&lt;/li&gt;
&lt;li&gt;Pull Request&lt;/li&gt;
&lt;li&gt;The Takeaway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction: In What Universe Did I Think This Was a Good Idea?
&lt;/h2&gt;

&lt;p&gt;It was a nice, calm day. Everything was going smoothly, I was just minding my own business.&lt;/p&gt;

&lt;p&gt;Out of nowhere, a notification from &lt;a class="mentioned-user" href="https://dev.to/jess"&gt;@jess&lt;/a&gt; popped up. And I see this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good morning! Good afternoon! Good Evening!&lt;/p&gt;

&lt;p&gt;Welcome to our first DEV Weekend Challenge, a short focused challenge designed to fit into your weekend. Since submission window is tight, we've set the timing to ensure that no matter where you are in the world, you'll have the majority of your Saturday and Sunday to participate.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don't know what's exactly wrong with me, but whenever I see a word "challenge", something clicks in my brain and it starts a multi-thousand thread process to think of an idea until the idea isn't generated. This time was no different. But in some ways, it really was quite different.&lt;/p&gt;

&lt;p&gt;Let me explain why. So here's my thinking process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OK I just have the weekend. 2 days tops, that's it. Well, sometimes just planning can take way more than that.&lt;/li&gt;
&lt;li&gt;I'm a full-stack engineer with extensive back-end background, so back-end is my primary comfort zone.&lt;/li&gt;
&lt;li&gt;Building something fun and flashy would be great, like a... browser game? Hell yeah!!! ... well, except, I ain't no game developer and I have never ever written a browser game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;So I decided to build... drum rolls ... A BROWSER GAME!&lt;/p&gt;

&lt;p&gt;If you're not a technical person, a back-end developer building a browser game is an NFL player playing tennis.&lt;/p&gt;

&lt;p&gt;You already know this isn't gonna end well, don't ya?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Game
&lt;/h2&gt;

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

&lt;p&gt;You're probably wondering what kind of game I created. It's basically a driving simulator (well... kinda) - a synthwave 3D driving game where your DEV community articles appear as neon billboards along the endless highway. It fetches articles, stats, and things like that using the DEV Community by the username.&lt;/p&gt;

&lt;p&gt;If you'd like to give it a try before continuing, here are the relevant links:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sundaydevdrive.pilotronica.com/" rel="noopener noreferrer"&gt;Demo&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/georgekobaidze/sunday-dev-drive-a-synthwave-driving-experience-through-your-dev-community-articles-5032"&gt;Submission article&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/georgekobaidze/sunday-dev-drive" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Game Loop
&lt;/h2&gt;

&lt;p&gt;Sunday DEV Drive is a browser game built on top of &lt;strong&gt;Three.js&lt;/strong&gt; - a JavaScript library that handles 3D rendering in the browser. The game runs entirely client-side: no server, no backend, no build step. Just HTML, JavaScript, and your DEV Community articles fetched from a public API.&lt;/p&gt;

&lt;p&gt;The game is built around a &lt;strong&gt;game loop&lt;/strong&gt;: a function that runs over and over, many times per second. On each iteration it does the same three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read inputs&lt;/strong&gt; - is the player pressing the accelerator? Turning left?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the world&lt;/strong&gt; - move the car, adjust the camera, apply physics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draw the frame&lt;/strong&gt; - render everything to the screen&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the browser, the standard tool for this is &lt;code&gt;requestAnimationFrame&lt;/code&gt;. You give it a function, and the browser calls that function right before it repaints the screen — as fast as the display allows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "call me again next frame"&lt;/span&gt;

  &lt;span class="nf"&gt;updatePhysics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;updateCamera&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;renderScene&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// start the loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it like a flipbook. Each call to &lt;code&gt;animate()&lt;/code&gt; draws one page. The faster the pages flip, the smoother the motion looks. On a 60Hz monitor, the browser flips 60 pages per second. On a 165Hz monitor, 165 pages per second.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Game Physics
&lt;/h2&gt;

&lt;p&gt;The car's movement is simple Newtonian-ish math applied every frame, nothing exotic, just the kind of physics you vaguely remember from high school:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Throttle&lt;/strong&gt; → adds to the car's speed (acceleration)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; → moves the car's position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steering&lt;/strong&gt; → rotates the car's angle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Friction&lt;/strong&gt; → slowly reduces speed when no input is held&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camera&lt;/strong&gt; → smoothly follows the car from behind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple stuff. Which is exactly why the bug hiding inside it was so easy to overlook.&lt;/p&gt;




&lt;h2&gt;
  
  
  It Worked on My Machine Goddammit
&lt;/h2&gt;

&lt;p&gt;I tested the game. Multiple times. Zero issues. The car moved naturally. Steering felt great. Acceleration was progressive. The brakes had a nice bite to them.&lt;/p&gt;

&lt;p&gt;Everything felt perfect.&lt;/p&gt;

&lt;p&gt;Well, until I tested it on another computer. Ladies and gentlemen, I need your attention, please: I don't care how certain you are that your code works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ALWAYS TEST IT ON ANOTHER COMPUTER.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I decided to flex a little and show the game to my mom and brother. So I sent them the links.&lt;/p&gt;

&lt;p&gt;"Hey, check this out. I made a game."&lt;/p&gt;

&lt;p&gt;A few minutes later, they texted me back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"The visuals are really good, but the car is barely moving. Is this by design?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Umm... whaddaya mean "barely moving?" I just tested it and it's fine. Are you sure you're pressing the gas?"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I started troubleshooting.&lt;/p&gt;

&lt;p&gt;And troubleshooting.&lt;/p&gt;

&lt;p&gt;And troubleshooting some more.&lt;/p&gt;

&lt;p&gt;Nothing made sense.&lt;/p&gt;

&lt;p&gt;The controls were working. The game was running. There were no obvious errors. Yet somehow, on their machines, my beautifully engineered car had apparently decided to become a mobility scooter.&lt;/p&gt;

&lt;p&gt;I had no idea what was going on.&lt;/p&gt;

&lt;p&gt;So I did what any developer would do in this situation:&lt;/p&gt;

&lt;p&gt;I Googled&lt;br&gt;
I asked AI&lt;br&gt;
I stared at the code&lt;br&gt;
I sketched things out&lt;br&gt;
I questioned my architectural decisions.&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;I questioned my life choices.&lt;/p&gt;

&lt;p&gt;And then it just clicked.&lt;/p&gt;

&lt;p&gt;I had used the monitor's refresh rate as part of my movement calculations.&lt;/p&gt;

&lt;p&gt;Yep.&lt;/p&gt;

&lt;p&gt;My physics weren't actually frame-rate independent.&lt;/p&gt;

&lt;p&gt;My monitor was running at 165 Hz. Their monitor was running at 120 Hz.&lt;/p&gt;

&lt;p&gt;That's a 45 Hz difference.&lt;/p&gt;

&lt;p&gt;45 frames per second was enough to turn my perfectly drivable car into a perfectly drivable car, but in slow motion.&lt;/p&gt;

&lt;p&gt;The game wasn't broken on my machine. My monitor was secretly a system dependency.&lt;/p&gt;

&lt;p&gt;I had messed up... And badly.&lt;/p&gt;

&lt;p&gt;And now it was time to fix it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Understanding the Root Cause
&lt;/h2&gt;

&lt;p&gt;My animate loop looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&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;&lt;code&gt;requestAnimationFrame&lt;/code&gt; calls your function &lt;strong&gt;once per display refresh&lt;/strong&gt;. On my 165Hz monitor, that's 165 times per second. On a 120Hz monitor, 120 times.&lt;/p&gt;

&lt;p&gt;The problem: &lt;strong&gt;every physics value was per-frame, not per-second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look at what actually happens across different hardware:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame Rate&lt;/th&gt;
&lt;th&gt;Updates/sec&lt;/th&gt;
&lt;th&gt;Effective speed/sec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;165 fps (my machine)&lt;/td&gt;
&lt;td&gt;165&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.42 × 165 = 69.3&lt;/code&gt; ✅ feels right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120 fps&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.42 × 120 = 50.4&lt;/code&gt; 🙂 73% of intended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.42 × 60 = 25.2&lt;/code&gt; 😐 36% of intended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 fps&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.42 × 30 = 12.6&lt;/code&gt; 😩 18% of intended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I had unknowingly &lt;strong&gt;tuned the entire game for 165fps&lt;/strong&gt;. Everyone else was experiencing a completely different game.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Delta Time
&lt;/h2&gt;

&lt;p&gt;The solution is to measure the actual time elapsed between frames and use it to scale all physics updates. This is called &lt;strong&gt;delta time&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animate&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;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDelta&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// real seconds since last frame&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;         &lt;span class="c1"&gt;// normalized: 1.0 at 60fps baseline&lt;/span&gt;

  &lt;span class="c1"&gt;// Now multiply everything by dt&lt;/span&gt;
  &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;acceleration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&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;&lt;code&gt;clock.getDelta()&lt;/code&gt; returns the real elapsed time in seconds. At 60fps that's &lt;code&gt;~0.0167s&lt;/code&gt;. &lt;br&gt;
At 165fps it's &lt;code&gt;~0.006s&lt;/code&gt;. &lt;br&gt;
At 30fps it's &lt;code&gt;~0.033s&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I multiply by 60 to normalize: &lt;code&gt;dt = delta * 60&lt;/code&gt;. This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At 165fps → &lt;code&gt;dt ≈ 0.36&lt;/code&gt; (smaller steps per frame, same per second)&lt;/li&gt;
&lt;li&gt;At 60fps → &lt;code&gt;dt ≈ 1.0&lt;/code&gt; (baseline)&lt;/li&gt;
&lt;li&gt;At 30fps → &lt;code&gt;dt ≈ 2.0&lt;/code&gt; (larger steps per frame, same per second)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the same distance is covered per second regardless of hardware:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Frame Rate&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;dt&lt;/code&gt; per frame&lt;/th&gt;
&lt;th&gt;Movement per frame&lt;/th&gt;
&lt;th&gt;Movement per second&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;165 fps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;≈ 0.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;speed × 0.36&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;speed × 60&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 fps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;≈ 1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;speed × 1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;speed × 60&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30 fps&lt;/td&gt;
&lt;td&gt;&lt;code&gt;≈ 2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;speed × 2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;speed × 60&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  But There's a Catch: Not Everything Is Fixed With &lt;code&gt;× dt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Simple &lt;code&gt;* dt&lt;/code&gt; fixes linear operations. But a real game has more than just linear math. In this fix alone, there were three distinct patterns, each requiring a different approach.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pattern 1: Linear operations
&lt;/h3&gt;

&lt;p&gt;The straightforward one. Any additive rate just gets multiplied by &lt;code&gt;dt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Frame-rate dependent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acceleration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brakeForce&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;brake&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Frame-rate independent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acceleration&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;throttle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;brakeForce&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;brake&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;car&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CAR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;turnSpeed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're adding or subtracting a value per frame, multiply it by &lt;code&gt;dt&lt;/code&gt;. Done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Exponential decay:
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Math.pow(factor, dt)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This one is less obvious. When driving off-road, the car slows down using a multiplier applied each frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Frame-rate dependent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 3% drag per frame&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 165fps this applies 3% drag 165 times per second. At 30fps, only 30 times. The off-road drag behaves completely differently depending on hardware.&lt;/p&gt;

&lt;p&gt;The naive fix: &lt;code&gt;carState.speed *= 0.97 * dt&lt;/code&gt; is &lt;strong&gt;wrong&lt;/strong&gt;. &lt;br&gt;
You can't linearly scale a multiplier. At &lt;code&gt;dt = 2&lt;/code&gt; you'd get &lt;code&gt;0.97 * 2 = 1.94&lt;/code&gt;, which would &lt;em&gt;accelerate&lt;/em&gt; the car instead of slowing it down.&lt;/p&gt;

&lt;p&gt;The correct fix uses exponentiation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Frame-rate independent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this work? Repeated multiplication is exponential decay. Consider what happens over one second:&lt;/p&gt;

&lt;p&gt;At 60fps (&lt;code&gt;dt = 1&lt;/code&gt;, 60 frames):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speed after 1s = speed × 0.97^60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 30fps (&lt;code&gt;dt = 2&lt;/code&gt;, 30 frames):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speed after 1s = speed × (0.97^2)^30 = speed × 0.97^60 ✅ identical
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Math.pow(factor, dt)&lt;/code&gt; correctly scales the decay &lt;strong&gt;exponent&lt;/strong&gt; rather than the multiplier itself. The result over any given real-world time is always the same.&lt;/p&gt;

&lt;p&gt;The same pattern applies to smooth steering interpolation, instead of a fixed lerp coefficient, it uses the same exponential trick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Frame-rate dependent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;steerDir&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Frame-rate independent&lt;/span&gt;
&lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;steerDir&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;carState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steer&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pattern 3: Lerp smoothing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;1 - Math.pow(1 - alpha, dt)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The trickiest one. The camera smoothly follows the car using &lt;code&gt;lerp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Frame-rate dependent&lt;/span&gt;
&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lerp(target, alpha)&lt;/code&gt; moves &lt;code&gt;alpha&lt;/code&gt; percent of the remaining distance each frame. At 165fps the camera takes 165 small steps per second - buttery smooth. At 30fps it takes 30 steps - sluggish, visibly lagging.&lt;/p&gt;

&lt;p&gt;Again, &lt;code&gt;alpha * dt&lt;/code&gt; is the wrong fix. At &lt;code&gt;dt = 2&lt;/code&gt; you'd get &lt;code&gt;alpha = 0.16&lt;/code&gt;, which changes the feel, and at high enough values it would overshoot the target entirely.&lt;/p&gt;

&lt;p&gt;The correct formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Frame-rate independent&lt;/span&gt;
&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the intuition. After &lt;code&gt;n&lt;/code&gt; lerp steps at factor &lt;code&gt;alpha&lt;/code&gt;, the remaining distance is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;remaining = (1 - alpha)^n
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make this time-independent, you want the same remaining distance after &lt;code&gt;n&lt;/code&gt; frames at &lt;code&gt;dt=1&lt;/code&gt; as after &lt;code&gt;n/2&lt;/code&gt; frames at &lt;code&gt;dt=2&lt;/code&gt;. The solution is to raise &lt;code&gt;(1 - alpha)&lt;/code&gt; to the power of &lt;code&gt;dt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;adjusted_alpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;^&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugging in the numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At &lt;code&gt;dt = 1.0&lt;/code&gt; (60fps baseline): &lt;code&gt;1 - 0.8^1.0 = 0.200&lt;/code&gt; - original feel ✅&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;dt = 0.36&lt;/code&gt; (165fps): &lt;code&gt;1 - 0.8^0.36 ≈ 0.083&lt;/code&gt; - smaller steps, same tracking speed ✅&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;dt = 2.0&lt;/code&gt; (30fps): &lt;code&gt;1 - 0.8^2.0 = 0.360&lt;/code&gt; - larger steps, same tracking speed ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The camera now follows the car at the same real-world speed on any display.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary: The Three Patterns
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Frame-rate dependent&lt;/th&gt;
&lt;th&gt;Frame-rate independent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add/subtract per frame&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value += rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value += rate * dt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiply per frame (decay)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value *= factor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;value *= Math.pow(factor, dt)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lerp toward target&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lerp(target, alpha)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lerp(target, 1 - Math.pow(1-alpha, dt))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  One More Thing: Re-tune Your Constants
&lt;/h2&gt;

&lt;p&gt;After making physics frame-rate independent at a 60fps baseline, my original constants felt wrong, they were implicitly tuned for 165fps. I had to re-tune them for &lt;code&gt;dt = 1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before (accidentally tuned for 165fps)&lt;/span&gt;
&lt;span class="nx"&gt;maxSpeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0006&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;brakeForce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="mf"&gt;0.0015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;friction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;0.00005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// After (tuned for dt = 1 baseline at 60fps)&lt;/span&gt;
&lt;span class="nx"&gt;maxSpeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;1.155&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;acceleration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.004538&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;brakeForce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="mf"&gt;0.011344&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nx"&gt;friction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;0.000378&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're migrating an existing game, expect to spend time re-tuning. The physics will now be consistent across hardware, but the feel may shift from what you were used to on your own monitor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pull Request
&lt;/h2&gt;

&lt;p&gt;Check out the pull request for this bug fix:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/georgekobaidze/sunday-dev-drive/pull/13/changes" rel="noopener noreferrer"&gt;Click Here to Check Out the Pull Request&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're building a game or animation loop with &lt;code&gt;requestAnimationFrame&lt;/code&gt;, assume nothing about frame rate. Your players are on everything from 30fps integrated graphics to 240Hz gaming monitors.&lt;/p&gt;

&lt;p&gt;The fix isn't too complicated, but it's also not just "multiply everything by &lt;code&gt;dt&lt;/code&gt;." Linear operations, exponential decay, and lerp smoothing each need their own treatment. Get all three right and your game will feel identical whether it runs on a decade-old laptop or a high-refresh gaming rig.&lt;/p&gt;

&lt;p&gt;My car now drives the same on every machine. And I learned far more about frame-rate-independent math than I expected from what looked like a simple bug.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this deep dive? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>Everyone Can Drive. Not Everyone Can Drive Well. Same Goes for AI-Assisted Coding</title>
      <dc:creator>Giorgi Kobaidze</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:01:11 +0000</pubDate>
      <link>https://dev.to/georgekobaidze/everyone-can-drive-not-everyone-can-drive-well-same-goes-for-ai-assisted-coding-1oag</link>
      <guid>https://dev.to/georgekobaidze/everyone-can-drive-not-everyone-can-drive-well-same-goes-for-ai-assisted-coding-1oag</guid>
      <description>&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Overview&lt;/li&gt;
&lt;li&gt;AI Didn't Remove the Skill, It Relocated the Skill&lt;/li&gt;
&lt;li&gt;Vibe Coding Is Easy. Doing It Well Isn't&lt;/li&gt;
&lt;li&gt;Everyone Can Drive. Not Everyone Can Drive Well.&lt;/li&gt;
&lt;li&gt;The same thing happens with AI&lt;/li&gt;
&lt;li&gt;It's Not Just About Prompting&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Lately, I've been writing a lot of technical-heavy articles and I noticed my articles got less popular.&lt;/p&gt;

&lt;p&gt;Well, what did I expect, technical articles can be a little... boooooring.&lt;/p&gt;

&lt;p&gt;So this time, let's take a different route. A lighter, more easy-going article, something less about complex concepts and more about sharing ideas, stories and perspectives.&lt;/p&gt;

&lt;p&gt;Time to win some readers' hearts back... But if you're a bad driver, you might hate me after that.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Didn't Remove the Skill, It Relocated the Skill
&lt;/h2&gt;

&lt;p&gt;I've heard people say that you don't need any skills to use AI to write code. &lt;br&gt;
❌ Errrrr... Wrong.&lt;/p&gt;

&lt;p&gt;Stick around. I'll explain it with a car-driving analogy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vibe Coding Is Easy. Doing It Well Isn't
&lt;/h2&gt;

&lt;p&gt;Even if you're vibe coding, which is probably the most abstract way of creating software, you still need skills.&lt;/p&gt;

&lt;p&gt;Just a different kind of skills.&lt;/p&gt;

&lt;p&gt;You need to know how to use your tokens effectively so you can ship products faster, safer, with better quality, and without setting your wallet on fire.&lt;/p&gt;

&lt;p&gt;That's what I call "hypermiling your tokens".&lt;/p&gt;

&lt;p&gt;There are actual techniques for using your tokens intelligently rather than just throwing more of them at the problem. And those techniques are harder to master than you might think. Not because they're particularly complicated to understand, but because mastering them takes a ridiculous amount of experimentation.&lt;/p&gt;

&lt;p&gt;You try different approaches.&lt;/p&gt;

&lt;p&gt;Different prompts.&lt;/p&gt;

&lt;p&gt;Different models.&lt;/p&gt;

&lt;p&gt;Different levels of context.&lt;/p&gt;

&lt;p&gt;Different tasks.&lt;/p&gt;

&lt;p&gt;You screw things up (inevitable).&lt;/p&gt;

&lt;p&gt;You spend way too many tokens on something that should've taken half as many (also inevitable).&lt;/p&gt;

&lt;p&gt;You ask the model to fix something it didn't even break (probably with some attitude as well).&lt;/p&gt;

&lt;p&gt;Then you ask it to fix the fix (recursion anyone?).&lt;/p&gt;

&lt;p&gt;Then you realize the original code was fine (but your top-notch model has already inhaled your entire token budget).&lt;/p&gt;

&lt;p&gt;Congratulations. You just spent 200,000 tokens going in a circle.&lt;/p&gt;

&lt;p&gt;But, don't get discouraged, because you also started finding that sweet spot where you're getting the most output for the least amount of waste.&lt;/p&gt;

&lt;p&gt;And that's a skill. Anything that's earned through trial and failure, is a skill. Simple as that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Everyone Can Drive. Not Everyone Can Drive Well.
&lt;/h2&gt;

&lt;p&gt;Who would you call a good, skilled driver?&lt;/p&gt;

&lt;p&gt;A skilled driver is someone who knows the rules and follows them, while also trying to be as efficient as possible.&lt;/p&gt;

&lt;p&gt;They don't overspeed.&lt;/p&gt;

&lt;p&gt;They park accurately, between the lines.&lt;/p&gt;

&lt;p&gt;They can drive inside a lane consistently.&lt;/p&gt;

&lt;p&gt;They use just enough fuel to get where they're going.&lt;/p&gt;

&lt;p&gt;They're predictable on the road.&lt;/p&gt;

&lt;p&gt;They care about the car.&lt;/p&gt;

&lt;p&gt;They care about other drivers.&lt;/p&gt;

&lt;p&gt;They don't floor the gas pedal from every single traffic light like they're in a drag race.&lt;/p&gt;

&lt;p&gt;And they don't slam on the brakes like they're playing Forza with a keyboard.&lt;/p&gt;

&lt;p&gt;A trully good driver knows how to be both safe and efficient. And they might even have no idea how engines or transmissions work and still be great drivers. I mean I'm not a mechanic, I've been driving for about 7 years now and I've never gotten a ticket, or maybe once, but that was unintentional and once I realized I'd crossed the wrong line, it was already late.&lt;/p&gt;

&lt;p&gt;And I've seen too many car mechanics who drive like they're gonna spawn again like an NPC after they crash.&lt;/p&gt;

&lt;p&gt;Now imagine the opposite.&lt;/p&gt;

&lt;p&gt;You go 30 km/h over the speed limit.&lt;/p&gt;

&lt;p&gt;Cut people off.&lt;/p&gt;

&lt;p&gt;Floor the accelerator the millisecond the light turns green.&lt;/p&gt;

&lt;p&gt;Brake so hard your passengers start reconsidering their life choices.&lt;/p&gt;

&lt;p&gt;Meanwhile, you're wearing out the car, burning fuel, and spending six times more money than you need to, while also being a danger to everyone around you.&lt;/p&gt;

&lt;p&gt;You're not a skilled driver. You're just driving recklessly.&lt;/p&gt;

&lt;p&gt;In fact, you probably have no idea what good driving actually is. What being a car operator actually is. &lt;strong&gt;What it actually means to be a human element in the equation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that's an important distinction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same thing happens with AI
&lt;/h2&gt;

&lt;p&gt;Luckily, in vibe coding, being reckless with AI is considerably safer because, thankfully, your Claude session isn't going to rear-end a bus.&lt;/p&gt;

&lt;p&gt;The worst thing you're probably going to hit is your own token budget. But being reckless with tokens doesn't mean you're skilled.&lt;/p&gt;

&lt;p&gt;Everyone can be reckless. Whether you're driving or coding.&lt;/p&gt;

&lt;p&gt;The skill is knowing how to find that sweet spot between speed, quality, cost, and safety, and staying close to it while you're working.&lt;/p&gt;

&lt;p&gt;And that sweet spot isn't the same for every situation. Sometimes you need a huge amount of context, sometimes you don't.&lt;/p&gt;

&lt;p&gt;Sometimes a massive reasoning model is exactly what you need. Sometimes you're asking it to rename a variable and you've basically brought a semi-truck to the grocery store, not to deliver goods, but to buy them... for the day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2befwpvoget0tpcljsgo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2befwpvoget0tpcljsgo.jpg" alt="Semi Taking Too Much Space" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes you should let the AI explore the problem.&lt;/p&gt;

&lt;p&gt;Sometimes you should give it very strict boundaries.&lt;/p&gt;

&lt;p&gt;Sometimes you should ask it to implement something.&lt;/p&gt;

&lt;p&gt;Sometimes you should stop and think about the architecture yourself first.&lt;/p&gt;

&lt;p&gt;And sometimes the best prompt you can write is:&lt;/p&gt;

&lt;p&gt;"Don't change anything. Just tell me what's wrong."&lt;/p&gt;

&lt;p&gt;That's where the real skill starts showing up.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Not Just About Prompting
&lt;/h2&gt;

&lt;p&gt;I think this is where a lot of people misunderstand AI-assisted development.&lt;/p&gt;

&lt;p&gt;They reduce the skill to:&lt;/p&gt;

&lt;p&gt;"Can you write a good prompt?" - That's actually only one small part of it. You need to know how to scope a problem, how much context the model actually needs, when to split a task into smaller pieces.&lt;/p&gt;

&lt;p&gt;You need to recognize when the model is going down the wrong path and understand the output well enough to know whether it's actually good.&lt;/p&gt;

&lt;p&gt;And there's another skill: knowing when to stop. This is probably one of the most underrated parts of AI-assisted development (and driving too, some people just can't recognize huge red stop signs sometimes).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg3nbu81evzkxhnmexnx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg3nbu81evzkxhnmexnx.jpg" alt="Person Not Stopping on a Stop Sign" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More tokens don't automatically mean better software.&lt;/p&gt;

&lt;p&gt;More iterations don't automatically mean better software.&lt;/p&gt;

&lt;p&gt;More context doesn't automatically mean better answers.&lt;/p&gt;

&lt;p&gt;Sometimes you're already 95% of the way there, and another twenty prompts are going to make things worse.&lt;/p&gt;

&lt;p&gt;You start optimizing things that don't matter or introduce abstractions nobody asked for.&lt;/p&gt;

&lt;p&gt;You refactor perfectly functional code because the model suddenly decided that your entire architecture needs to be "more scalable."&lt;/p&gt;

&lt;p&gt;You don't necessarily need to be the person manually turning every screw anymore. Not every driver should be a car mechanic or a mechanical engineer.&lt;/p&gt;

&lt;p&gt;You need to know which screws to turn, when to turn them, how hard to turn them, and when to stop touching things before you break something.&lt;/p&gt;

&lt;p&gt;And the better you become at working with AI, the less you'll think about "getting AI to write code" and the more you'll think about orchestrating a system that produces good software efficiently.&lt;/p&gt;

&lt;p&gt;Similarly to driving, you don't need to build the engine to drive the car. But you still need to know the rules of the road, you need to understand what the pedals do.&lt;/p&gt;

&lt;p&gt;You need to recognize when something sounds wrong. You need to know when to slow down and definitely need to know that flooring the accelerator doesn't make you a better driver (it actually makes you look like a terrible driver).&lt;/p&gt;

&lt;p&gt;The same applies to AI coding. Throwing more tokens at a problem isn't a skill nor is having the biggest context window or using the most powerful model for everything.&lt;/p&gt;

&lt;p&gt;Being able to consistently get the result you need with the least unnecessary wastem, while keeping quality and safety high, is. &lt;/p&gt;

&lt;p&gt;And the more time you spend operating around that sweet spot, the better you become at recognizing it.&lt;/p&gt;

&lt;p&gt;And that's why:&lt;/p&gt;

&lt;p&gt;"AI can write the code for me"&lt;/p&gt;

&lt;p&gt;doesn't mean:&lt;/p&gt;

&lt;p&gt;"I don't need skills anymore."&lt;/p&gt;

&lt;p&gt;The skill just moved.&lt;/p&gt;

&lt;p&gt;So yeah.&lt;/p&gt;

&lt;p&gt;Be efficient.&lt;/p&gt;

&lt;p&gt;Drive safe.&lt;/p&gt;

&lt;p&gt;Hypermile.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Enjoyed this deep dive? Let's stay connected!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I share more software engineering insights, projects, and experiments across these platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/giorgikobaidze/" rel="noopener noreferrer"&gt;Connect with me on LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/georgekobaidze" rel="noopener noreferrer"&gt;Explore my projects on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://x.com/georgekobaidze" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/@Pilotronica" rel="noopener noreferrer"&gt;Watch my videos on YouTube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
