<?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: Pascal CESCATO</title>
    <description>The latest articles on DEV Community by Pascal CESCATO (@pascal_cescato_692b7a8a20).</description>
    <link>https://dev.to/pascal_cescato_692b7a8a20</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%2F3446021%2F2dab8c8f-80a4-4434-967f-5640bbf2050a.jpg</url>
      <title>DEV Community: Pascal CESCATO</title>
      <link>https://dev.to/pascal_cescato_692b7a8a20</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pascal_cescato_692b7a8a20"/>
    <language>en</language>
    <item>
      <title>The "AI" Badge Doesn't Measure What You Think It Does</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sat, 15 Aug 2026 13:06:49 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/the-ai-badge-doesnt-measure-what-you-think-it-does-3ne9</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/the-ai-badge-doesnt-measure-what-you-think-it-does-3ne9</guid>
      <description>&lt;p&gt;Anthropic signed the EU AI Act's Code of Practice on Transparency of AI-Generated Content, and started marking text produced by Claude with an invisible statistical watermark. Within days, the same event got read two completely different ways on my feed.&lt;/p&gt;

&lt;p&gt;The first reading, on dev.to, is by &lt;a class="mentioned-user" href="https://dev.to/sylwia-lask"&gt;@sylwia-lask&lt;/a&gt; &lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2" class="crayons-story__hidden-navigation-link"&gt;The End of Undetectable AI Text? Claude’s New Watermark Explained&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Separating AI provenance from detection myths&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="/sylwia-lask" 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%2F3535771%2Fe22860d5-274b-43c9-819b-56b162e5bd5a.jpeg" alt="sylwia-lask profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sylwia-lask" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Sylwia Laskowska
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Sylwia Laskowska
                
                
              
              &lt;div id="story-author-preview-content-4372605" 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="/sylwia-lask" 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%2F3535771%2Fe22860d5-274b-43c9-819b-56b162e5bd5a.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Sylwia Laskowska&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/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 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/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2" id="article-link-4372605"&gt;
          The End of Undetectable AI Text? Claude’s New Watermark Explained
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/news"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;news&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/llm"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;llm&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/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2" 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/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.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;155&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/sylwia-lask/the-end-of-undetectable-ai-text-claudes-new-watermark-explained-45g2#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              103&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;
            6 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;She actually went and read &lt;a href="https://support.claude.com/en/articles/16266773-how-claude-marks-ai-generated-content" rel="noopener noreferrer"&gt;the official documentation&lt;/a&gt; and reported what Anthropic really says: the watermark doesn't prove a text was entirely AI-generated – you can write something yourself, have Claude correct it or translate it, and it can still carry the mark. The reverse holds too: no watermark detected doesn't prove a human wrote everything.&lt;/p&gt;

&lt;p&gt;The second, &lt;a href="https://ai.plainenglish.io/if-you-use-claude-to-write-on-medium-the-invisible-watermark-era-is-here-aa7ddd372306" rel="noopener noreferrer"&gt;on Medium&lt;/a&gt;, called the announcement a "nuclear bomb" for the AI-generation community, and claimed "you are effectively carrying a digital scarlet letter." No citation of the actual documentation. Just outrage, with the appropriate amount of dramatic punctuation.&lt;/p&gt;

&lt;p&gt;Between the two, I left a comment under Sylwia's article. Written in French, translated by ChatGPT, signed off with a question: &lt;em&gt;how do you classify a text thought through, written, and reviewed by a human, but whose final English phrasing came out of a model? It wasn't a rhetorical question. I still don't have an answer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things people keep conflating
&lt;/h2&gt;

&lt;p&gt;The Medium article mixes together, without ever saying so, three mechanisms that have almost nothing to do with each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anthropic's watermark.&lt;/strong&gt; A statistical signal baked into the model, documented, which openly acknowledges its own limits: it can disappear after enough editing or translation, and its presence says nothing about who came up with the idea in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party consumer detectors.&lt;/strong&gt; ZeroGPT and its relatives. Tools that have existed for years, unrelated to Anthropic, whose reliability has never been seriously demonstrated at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform decisions.&lt;/strong&gt; A badge displayed on Medium, a curation algorithm that penalizes flagged content. These are editorial choices specific to each platform, not a mechanical consequence of the watermark.&lt;/p&gt;

&lt;p&gt;The Medium article writes as though the first link automatically triggers the next two: Anthropic marks, so detectors will catch everything, so platforms will punish. The reasoning collapses the moment you separate the links – and for good reason: detecting that a model was involved in producing a text is not the same as determining that the text was "written by AI." That conflation hides a deeper one, which public debate almost systematically ignores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assisted, generated, produced: three different verbs
&lt;/h2&gt;

&lt;p&gt;A text &lt;em&gt;assisted&lt;/em&gt; by AI stays under human editorial control from start to finish: the idea, the angle, the structure, and the final decision to publish belong to someone, no matter how many back-and-forths with a model happened along the way to correct, rephrase, translate, or pressure-test a line of reasoning. A text &lt;em&gt;generated&lt;/em&gt; by AI comes out of a prompt, without that upstream control – the idea itself came from the human, but the generated text, as it stands, belongs to the model. A text &lt;em&gt;produced&lt;/em&gt; by AI at industrial scale is something else again: an automated publishing pipeline, without anything resembling editorial oversight.&lt;/p&gt;

&lt;p&gt;What separates the three, then, isn't how much the model intervened – it's who kept their hand on the decisions.&lt;/p&gt;

&lt;p&gt;These three cases carry entirely different editorial responsibility. Treating them as a single category ("AI content") means judging a text on a binary criterion where the reality is a full spectrum of nuance – which is exactly what an undifferentiated "processed by AI" badge does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an actual test shows
&lt;/h2&gt;

&lt;p&gt;I ran an article I wrote in April 2021 – before any consumer-facing LLM existed – through &lt;a href="https://www.zerogpt.com/" rel="noopener noreferrer"&gt;ZeroGPT&lt;/a&gt;. A test about a year ago gave it a 97% AI probability. A recent test, on the exact same text, with the exact same tool, gives 8.6%.&lt;/p&gt;

&lt;p&gt;Same tool. Same text, down to the punctuation. Two incompatible scores, a year apart.&lt;/p&gt;

&lt;p&gt;Digging into the flagged passages in the second test, a pattern emerges. These aren't random sentences: they're consistently the most neutral, most pedagogical, most well-structured passages in the piece – a definition of what a web server is, an explanation of CentOS Stream, a step-by-step automated update procedure. The passages where my voice actually comes through – the self-deprecation, the verbal tics, the Neapolitan moka pot bought at a flea market – are never flagged.&lt;/p&gt;

&lt;p&gt;One test doesn't prove what a detection model measures in general. But this one strongly suggests the tool reacts to stylistic neutrality and structural regularity, not to a text's actual origin. Which is precisely the problem: those characteristics existed in human writing long before LLMs did. The detector is chasing a style whose origin is human, using the machine's imitation of it as the reference point. The reasoning eats its own tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this piece was actually made
&lt;/h2&gt;

&lt;p&gt;Since the whole point of this article is how hard it is to judge a text by its origin rather than its content, it's worth being transparent about how this one was produced.&lt;/p&gt;

&lt;p&gt;The starting point wasn't this article: it was a comment under a Medium post that annoyed me enough to reply. From there, several hours of back-and-forth with Claude – not to have it write for me, but to pressure-test angles, check numbers, and go dig up original sources instead of relying on my own fuzzy memory. The ZeroGPT test wasn't improvised for the article, either: I reran it to verify what I was recalling from memory, and the result changed the conclusion I was about to draw.&lt;/p&gt;

&lt;p&gt;First draft written in French – the language I actually think in. Several rounds of review, cross-checked across a few different AI models – ChatGPT, Kimi, Grok, Mistral – to catch inconsistencies and spots where the argument went soft. I then read and corrected it myself, outside of Claude (I use NotepadMD), because I never let a text out the door without going through it line by line. Translation into English after that, then a review of that translation, because a lexically faithful translation can still betray the tone if nobody checks it.&lt;/p&gt;

&lt;p&gt;By the end of this process, the text probably carries a watermark. It also took up several hours of research, verification, and both automated and human review – and my own review isn't the least of it: after every rewrite, and again at the end of the process. Both facts are true at the same time, and no detector will ever be able to tell them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a badge actually costs
&lt;/h2&gt;

&lt;p&gt;Everything above is a technical demonstration. It says nothing about what happens once the text is published, and that might be the more important part.&lt;/p&gt;

&lt;p&gt;A "processed by AI" badge, applied without distinguishing assisted, generated, and produced, puts an author who spent hours thinking, checking, and writing in the same bucket as a content farm publishing a hundred articles a day with no human oversight. A rushed reader doesn't see the nuance – they see the badge, and file the thinking author under "cheater." That's a real loss for someone who never cheated: their work gets judged on a signal that measures neither the effort nor the actual editorial control behind it, only whether a tool showed up somewhere in the chain.&lt;/p&gt;

&lt;p&gt;And that loss has an absurd mirror image. The same readers who reject an article marked "AI" accept, without blinking, the AI-generated summary sitting at the top of their Google search results – without reading it critically, without checking the sources it pulled from, and most often without ever clicking through to the original article. Independent studies converge on this: when an AI summary appears in a Google search, clicks to third-party sites drop by half, sometimes more, depending on methodology. In other words, the reader who calls an article "AI slop" over a badge probably let an AI summarize ten other topics for them that same week, without ever checking what it kept or what it distorted. AI isn't rejected on principle. It's rejected when it's visible and claimed, and accepted when it's invisible and imposed by default.&lt;/p&gt;

&lt;p&gt;This shift from click to summary is also a broader loss of reading, independent of any badge: a full article, nuanced, sometimes contradictory, replaced by three smoothed-over lines nobody ever questions. The reader loses the friction that would have forced them to evaluate a source, a line of reasoning, a style – exactly what the badge claims to let people judge, and what the summary skips without any debate at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't fix
&lt;/h2&gt;

&lt;p&gt;None of this will stop an "AI slop" comment under this piece. I've never gotten one on dev.to. On Reddit, I have.&lt;/p&gt;

&lt;p&gt;But look at who tends to write that kind of comment. Rarely people genuinely engaging with the topic. Often people chasing the buzz of a word that lands. On Reddit, the accounts behind these comments often show a broader pattern of reflexive, repeated downvoting across unrelated posts – a signal of rejection that has little to do with the content actually in front of them.&lt;/p&gt;

&lt;p&gt;The technical demonstration and the social judgment are two different things, and the second doesn't get talked out of itself by the first. "AI slop" has stopped being a judgment about a text at all: it's become a blunt, reflexive rejection signal people display, regardless of whether they read anything.&lt;/p&gt;

&lt;p&gt;If the badge doesn't measure origin, and social judgment doesn't care about the measurement, maybe the question worth asking isn't "who wrote this" but "who thought this through."&lt;/p&gt;

&lt;p&gt;The nuance matters here. In the process I just described, the initiative never changed hands: I'm the one who opens the session, sets the topic, decides whether an angle holds up or needs to be dropped. A model doesn't come looking for me with an idea I didn't already have. And if it writes a paragraph I didn't ask for, it goes in the trash – which is perfectly observable in what I keep and what I discard from one session to the next.&lt;/p&gt;

&lt;p&gt;Who thought through the initial disagreement with the Medium piece, and chose to separate watermark, detector, and platform instead of treating them as one thing? Who decided a test was worth more than an opinion, and reran it twice instead of republishing a fuzzy memory?&lt;/p&gt;

&lt;p&gt;Those are questions I can answer, text by text, decision by decision. No detector asks them. It looks at the final shape and infers an origin from it – when the origin was never in the shape. It was in the chain of decisions that came before it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>writing</category>
      <category>discuss</category>
    </item>
    <item>
      <title>When Your VPS Never Had the Resources It Was Sold With</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Wed, 05 Aug 2026 23:58:23 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/when-your-vps-never-had-the-resources-it-was-sold-with-302o</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/when-your-vps-never-had-the-resources-it-was-sold-with-302o</guid>
      <description>&lt;p&gt;I needed a VPS to run CyberPanel. Simple enough: 1 vCPU, 1 GB RAM, 10 GB SSD, IPv6 only. CyberPanel needs IPv4, so I upgraded to the next tier: 2 vCPU, 2 GB RAM, 20 GB SSD, one IPv4 address.&lt;/p&gt;

&lt;p&gt;What followed was two separate infrastructure failures in the same 24 hours, from the same provider, on the same instance. Neither was a Linux problem. Both were provisioning problems — the gap between what an offer promises and what actually gets attached to the virtual machine.&lt;/p&gt;

&lt;p&gt;I'm not naming the provider. It's a small French hosting company, and the point of this article isn't to send traffic their way — it's the pattern itself, which is common enough in the VPS world to be worth documenting properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: IPv4 that went nowhere
&lt;/h2&gt;

&lt;p&gt;The upgrade completed and the new IPv4 address showed up in the panel. Interface up, IP configured, default route present, ARP resolution to the gateway working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88.151.197.1 lladdr 44:4c:a8:fb:ef:fd REACHABLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But ICMP got no response at all, from the VPS or from the gateway itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;88.151.197.112 &amp;gt; 88.151.197.1: ICMP echo request
(no reply)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IPv6 worked normally throughout. Everything on the guest side — interface, routing, ARP — was correct. That's a useful diagnostic signal in itself: when ARP resolves but ICMP is silent, the guest OS has done its job and the problem sits in the infrastructure layer above it — typically a MAC/IP binding on the hypervisor or virtual switch that hasn't been updated to match the new address.&lt;/p&gt;

&lt;p&gt;It eventually got resolved, but with no explanation of what was actually changed on their side. At that point I'd lost most of a day to a connectivity problem on an instance that, on paper, was correctly configured from the first minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: the disk that was never there
&lt;/h2&gt;

&lt;p&gt;With IPv4 finally working, I moved on to installing CyberPanel — the entire reason for the upgrade. A fresh Ubuntu install went fine until CyberPanel's dependency installation started throwing &lt;code&gt;No space left on device&lt;/code&gt;. So I checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@panel:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lsblk
&lt;span class="go"&gt;NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda       8:0    0  3.5G  0 disk
├─sda1    8:1    0  2.5G  0 part /
├─sda14   8:14   0    4M  0 part
├─sda15   8:15   0  106M  0 part /boot/efi
└─sda16 259:0    0  913M  0 part /boot
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The disk was 3.5 GB. Root partition, 2.5 GB. The offer promised 20 GB after the upgrade — and, as it turned out, the base 10 GB tier had never delivered more than a fraction of that either. This wasn't a side effect of the upgrade. It had been wrong from the start; the upgrade just moved the goalpost from 10 GB to 20 GB while the actual allocation stayed untouched.&lt;/p&gt;

&lt;p&gt;This is where a lot of guides point you at &lt;code&gt;growpart&lt;/code&gt; or &lt;code&gt;resize2fs&lt;/code&gt;. Both are useless here, and understanding why 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%2F0c4wljx0k64gybq6qz7w.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%2F0c4wljx0k64gybq6qz7w.png" alt="layers diagram" width="800" height="1040"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;resize2fs&lt;/code&gt; and &lt;code&gt;growpart&lt;/code&gt; operate at the bottom two layers. They can only extend a filesystem or partition into space that already exists on the virtual disk. If the disk itself is 3.5 GB, there is no unallocated space to grow into — the tools report success or do nothing, because there's genuinely nothing for them to do. The problem isn't in the guest OS. It's one layer below it, in what the hypervisor actually attached to the VM.&lt;/p&gt;

&lt;p&gt;I opened a support ticket, included the &lt;code&gt;lsblk&lt;/code&gt; output, and asked them to check the volume attachment.&lt;/p&gt;

&lt;p&gt;The reply I got back attributed the disk shortfall to "the IPv6 to IPv4 upgrade" — as if provisioning a new IPv4 address could somehow shrink a virtual disk. It doesn't, and the two are unrelated at every layer shown in the diagram above. It reads less like a diagnosis and more like a convenient way to fold a second unresolved ticket into the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: "fixed" didn't mean fixed
&lt;/h2&gt;

&lt;p&gt;The provider's answer was to ask me to check again — no diagnosis of their own. I did, and this time the disk was correct: 20 GB, as ordered. I replied "corrected, thanks," and the ticket was closed.&lt;/p&gt;

&lt;p&gt;Later the same day, I reinstalled Ubuntu to get a clean baseline. The disk was wrong again — same as before the fix, not as after it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@panel:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="go"&gt;Mon Aug  3 20:56:49 UTC 2026
&lt;/span&gt;&lt;span class="gp"&gt;root@panel:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lsblk
&lt;span class="go"&gt;NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda       8:0    0  3.5G  0 disk
├─sda1    8:1    0  2.5G  0 part /
├─sda14   8:14   0    4M  0 part
├─sda15   8:15   0  106M  0 part /boot/efi
└─sda16 259:0    0  913M  0 part /boot
sr0      11:0    1    4M  0 rom
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 3.5 GB disk, same 2.5 GB root partition, hours after the ticket was marked resolved.&lt;/p&gt;

&lt;p&gt;This is the part worth sitting with. A "fix" applied through a support ticket, on a running instance, doesn't necessarily touch the thing that's actually broken. If the underlying VM profile or provisioning template is wrong, a support agent can patch the live disk allocation by hand and call it closed — while the next rebuild pulls from the same broken template and reproduces the exact same fault. The fix fixed an instance, not the cause.&lt;/p&gt;

&lt;p&gt;That's the practical lesson: if a resource-provisioning issue gets "corrected" on a live VPS, don't close the loop until you've verified it survives a rebuild. A one-off patch and a fixed template look identical from the support ticket. They don't look identical from &lt;code&gt;lsblk&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What two failures in one day tell you
&lt;/h2&gt;

&lt;p&gt;Individually, either of these could be dismissed as a one-off glitch. Together, they're more telling: two independent provisioning mismatches — disk and network — on the same instance, within the same upgrade cycle, one of which reappeared after being marked fixed. That pattern points to the provisioning pipeline itself, not to isolated bad luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands worth knowing
&lt;/h2&gt;

&lt;p&gt;If you land in a similar situation, these will tell you which layer you're actually dealing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk                           &lt;span class="c"&gt;# what Linux sees attached&lt;/span&gt;
fdisk &lt;span class="nt"&gt;-l&lt;/span&gt;                        &lt;span class="c"&gt;# partition table detail&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;                           &lt;span class="c"&gt;# filesystem usage&lt;/span&gt;
blockdev &lt;span class="nt"&gt;--getsize64&lt;/span&gt; /dev/sda   &lt;span class="c"&gt;# raw device size, bytes&lt;/span&gt;
ip addr                         &lt;span class="c"&gt;# interface and IP state&lt;/span&gt;
ip route                        &lt;span class="c"&gt;# routing table&lt;/span&gt;
ip neigh                        &lt;span class="c"&gt;# ARP/NDP cache — gateway reachability&lt;/span&gt;
ping &lt;span class="nt"&gt;-c&lt;/span&gt; 3 &amp;lt;gateway&amp;gt;             &lt;span class="c"&gt;# basic reachability test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;lsblk&lt;/code&gt; and &lt;code&gt;blockdev --getsize64&lt;/code&gt; already show a disk smaller than what you're paying for, don't touch &lt;code&gt;growpart&lt;/code&gt; or &lt;code&gt;resize2fs&lt;/code&gt; — you're not looking at a Linux problem. If &lt;code&gt;ip neigh&lt;/code&gt; shows the gateway as &lt;code&gt;REACHABLE&lt;/code&gt; but &lt;code&gt;ping&lt;/code&gt; gets nothing back, you're not looking at a Linux problem either. In both cases, the fix is on the other side of the hypervisor boundary, and no amount of guest-side troubleshooting will reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A VPS isn't physical hardware, but it's supposed to honor the same basic contract: the resources advertised should be the resources actually present. Before blaming Linux, a package, or your own configuration, check the layer underneath — and if a provider tells you something's fixed, verify it survives a rebuild before you believe it.&lt;/p&gt;

&lt;p&gt;None of this required an unusual amount of digging — &lt;code&gt;lsblk&lt;/code&gt;, &lt;code&gt;ip neigh&lt;/code&gt;, a rebuild to double-check. Which is really the point: for roughly the same price, sometimes less, providers like OVH, Contabo, or Infomaniak offer configurations that don't require this kind of forensic work just to get the resources you paid for. Chasing the cheapest listed price has a cost. It's just not always visible until you've already lost a day to it.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>devops</category>
      <category>vps</category>
      <category>linux</category>
    </item>
    <item>
      <title>CyberPanel's SSL Auto-Renewal Can Silently Fail — Here's the Fix</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sat, 01 Aug 2026 02:44:11 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/cyberpanels-ssl-auto-renewal-can-silently-fail-heres-the-fix-5g1o</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/cyberpanels-ssl-auto-renewal-can-silently-fail-heres-the-fix-5g1o</guid>
      <description>&lt;p&gt;Last week I was doing a routine check on a CyberPanel server and noticed something that didn't add up. The panel's SSL status showed a clean &lt;strong&gt;"SSL Issued Successfully"&lt;/strong&gt;. Nothing to see, apparently. Except the certificate expiration date hadn't moved.&lt;/p&gt;

&lt;p&gt;The browser was still serving a certificate that was about to expire.&lt;/p&gt;

&lt;p&gt;That's the kind of mismatch that's easy to miss if you trust the panel's UI at face value — the message says success, the log (if you even check it) looks uneventful, and you move on. I didn't move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking what's actually served
&lt;/h2&gt;

&lt;p&gt;The panel tells you what it &lt;em&gt;thinks&lt;/em&gt; happened. What matters is what the server actually hands out to a browser. That's a one-liner away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt;  &lt;span class="se"&gt;\&lt;/span&gt;
domain.com:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; domain.com &amp;lt;/dev/null 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt;

&lt;span class="c"&gt;# outputs&lt;/span&gt;
&lt;span class="nv"&gt;notBefore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Jul 31 22:59:26 2026 GMT
&lt;span class="nv"&gt;notAfter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Oct 29 22:59:26 2026 GMT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Wait — that date range looked fine on its own. The real tell was comparing it against what the panel had just claimed to renew: the dates hadn't changed since before the "successful" renewal. Whatever &lt;code&gt;acme.sh&lt;/code&gt; had just done, it hadn't produced a new certificate that OpenLiteSpeed was actually serving.&lt;/p&gt;
&lt;h2&gt;
  
  
  Checking what acme.sh actually did
&lt;/h2&gt;

&lt;p&gt;CyberPanel doesn't implement ACME itself — it delegates to &lt;code&gt;acme.sh&lt;/code&gt; under the hood. So the next step was checking what &lt;code&gt;acme.sh&lt;/code&gt; had registered:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acme.sh &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's where the mismatch became clearer. The certificate authority listed for this domain wasn't Let's Encrypt production — it was the &lt;strong&gt;staging&lt;/strong&gt; CA. Staging certificates are useful for testing, but they're not production certificates trusted by browsers. In this case, the important part wasn't just that staging was configured — it was that the renewal workflow no longer matched the certificate actually served by OpenLiteSpeed. CyberPanel reported success because, from its point of view, &lt;code&gt;acme.sh&lt;/code&gt; had returned successfully. It just wasn't returning the certificate the site needed.&lt;/p&gt;

&lt;p&gt;I can't say for certain &lt;em&gt;how&lt;/em&gt; this CyberPanel installation's &lt;code&gt;acme.sh&lt;/code&gt; configuration ended up pointed at the staging CA — only that it had. Worth keeping in mind if you're chasing a similar mismatch: the fix doesn't require knowing the origin, just confirming the current state.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Once the mismatch was clear, the fix was mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Force &lt;code&gt;acme.sh&lt;/code&gt; back onto the Let's Encrypt production CA.&lt;/li&gt;
&lt;li&gt;Drop the existing (staging) registration for the domain.&lt;/li&gt;
&lt;li&gt;Reissue the certificate from scratch, this time against production.&lt;/li&gt;
&lt;li&gt;Install it where CyberPanel and OpenLiteSpeed expect it: &lt;code&gt;/etc/letsencrypt/live/&amp;lt;domain&amp;gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reload OpenLiteSpeed so it picks up the new files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic — it's exactly what &lt;code&gt;acme.sh&lt;/code&gt; is designed to do. The problem was never the tooling, it was that CyberPanel's reported status wasn't a reliable signal of what state the certificate was actually in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Turning it into something reusable
&lt;/h2&gt;

&lt;p&gt;Fixing one server by hand is fine once. But when the same class of drift keeps showing up across CyberPanel releases, it stops being a one-off and starts being something worth scripting around. I wrote two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;renew-ssl.sh&lt;/code&gt;&lt;/strong&gt; — routine renewal, bypassing CyberPanel's scheduler entirely. It supports an auto mode (renews anything expiring within a configurable threshold, default 10 days), a single-domain mode, and a &lt;code&gt;--check&lt;/code&gt; mode that simulates the run and just reports what &lt;em&gt;would&lt;/em&gt; happen — useful for confirming a diagnosis like this one before touching anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fix-ssl.sh&lt;/code&gt;&lt;/strong&gt; — the heavier tool, for when a certificate is actually broken or stuck in staging. Given that the whole premise of this post is "don't trust the reported status, verify it," the script holds itself to the same standard on both ends:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before&lt;/strong&gt; touching anything, it writes a test file into the HTTP-01 challenge path and fetches it over plain HTTP, the same way Let's Encrypt will. If that fails, it stops before issuing anything — no point calling &lt;code&gt;acme.sh&lt;/code&gt; against a webroot that can't serve the challenge.&lt;/li&gt;
&lt;li&gt;It backs up the existing certificate and &lt;code&gt;acme.sh&lt;/code&gt; registration to a timestamped archive before removing anything, and refuses to proceed if the backup itself fails.&lt;/li&gt;
&lt;li&gt;It forces the production CA, clears the old registration, reissues via webroot, and installs the new certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After&lt;/strong&gt; reloading OpenLiteSpeed, it doesn't just assume the reload worked — it fetches the certificate actually served over HTTPS and compares its SHA256 fingerprint against the one just installed. A mismatch is reported as an error, with the exact restore command from the backup printed alongside it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are plain Bash, no dependencies beyond &lt;code&gt;acme.sh&lt;/code&gt; and &lt;code&gt;openssl&lt;/code&gt; (&lt;code&gt;fix-ssl.sh&lt;/code&gt; also uses &lt;code&gt;curl&lt;/code&gt; for the webroot check, though it degrades gracefully — with a warning — if &lt;code&gt;curl&lt;/code&gt; isn't available). Both are meant to be run as root since they touch &lt;code&gt;/etc/letsencrypt/&lt;/code&gt;, &lt;code&gt;/root/.acme.sh/&lt;/code&gt;, and the OpenLiteSpeed service.&lt;/p&gt;

&lt;p&gt;Source, with usage examples and a debugging section covering both commands above:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/cyberpanel-ssl-renewal-scripts" rel="noopener noreferrer"&gt;
        cyberpanel-ssl-renewal-scripts
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Renew and repair SSL certificates on CyberPanel 2.x directly via acme.sh (bypasses the broken built-in renewal scheduler)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CyberPanel SSL Renewal Scripts&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Bash scripts to renew and repair SSL certificates on CyberPanel 2.x directly via &lt;code&gt;acme.sh&lt;/code&gt;, bypassing CyberPanel's unreliable built-in renewal scheduler.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this exists&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CyberPanel can report a &lt;strong&gt;successful SSL renewal&lt;/strong&gt; while the certificate actually served remains close to expiration.&lt;/li&gt;
&lt;li&gt;In some cases &lt;code&gt;acme.sh&lt;/code&gt; is left configured with the &lt;strong&gt;Let's Encrypt staging&lt;/strong&gt; CA, so "renewals" produce certificates that browsers reject.&lt;/li&gt;
&lt;li&gt;These scripts renew — and repair — certificates &lt;strong&gt;directly via &lt;code&gt;acme.sh&lt;/code&gt;&lt;/strong&gt;, without going through the panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;CyberPanel 2.x&lt;/li&gt;
&lt;li&gt;OpenLiteSpeed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;acme.sh&lt;/code&gt; installed at &lt;code&gt;/root/.acme.sh/&lt;/code&gt; (CyberPanel default)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; — used by &lt;code&gt;fix-ssl.sh&lt;/code&gt; to validate the webroot challenge path (optional: without it, that check is skipped with a warning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root access&lt;/strong&gt; — the scripts write to &lt;code&gt;/etc/letsencrypt/live/&lt;/code&gt;, read &lt;code&gt;/root/.acme.sh/&lt;/code&gt;, and restart OpenLiteSpeed&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Scripts&lt;/h2&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;
&lt;code&gt;renew-ssl.sh&lt;/code&gt; — Renew certificates&lt;/h3&gt;

&lt;/div&gt;

&lt;p&gt;Renews certificates that were previously issued and installed with &lt;code&gt;acme.sh&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It supports three modes:&lt;/p&gt;


&lt;ul&gt;

&lt;li&gt;

&lt;strong&gt;Auto mode (recommended):&lt;/strong&gt;…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/cyberpanel-ssl-renewal-scripts" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Why this is worth checking on your own servers
&lt;/h2&gt;

&lt;p&gt;If you're running CyberPanel and haven't looked recently, it's worth running the &lt;code&gt;openssl s_client&lt;/code&gt; check against your actual domains — not because CyberPanel is broken in general, but because "the panel says success" and "the certificate is actually valid and current" are two different claims, and only one of them is verifiable from outside the panel.&lt;/p&gt;

&lt;p&gt;Have you seen similar ACME renewal drift on CyberPanel, OpenLiteSpeed, or other hosting panels? I'd be interested in comparing root causes — certificate automation failures are often harder to diagnose than certificate issuance itself.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>linux</category>
    </item>
    <item>
      <title>What Replacing Calendly Taught Me About Trusting Open Source</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:22:42 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/what-replacing-calendly-taught-me-about-trusting-open-source-540a</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/what-replacing-calendly-taught-me-about-trusting-open-source-540a</guid>
      <description>&lt;p&gt;cal.com, Calendly, zcal... booking SaaS isn't short on options, and most of them are genuinely decent. Free tiers cover the basics for a lot of freelancers. The catch: you're the product (nothing's really free), and your customer data lives somewhere you don't fully control and can't fully audit.&lt;/p&gt;

&lt;p&gt;A dysfunction I ran into on another SaaS tool was the trigger. Trusting a third-party service by default, just because it's widely used and billed monthly, doesn't always hold up. That episode was enough to make me reconsider every external service this site was relying on for functionality that's actually simple to self-host — and the booking widget, running on Calendly, was one of them.&lt;/p&gt;

&lt;p&gt;Nothing wrong with Calendly specifically. It worked fine. But structural friction had been building regardless: a recurring subscription for something as simple as displaying open slots and recording a choice, a hard dependency on a third party for a component with nothing exceptional about it technically, and customization capped by whatever the vendor exposes in settings — no way to go further if a need falls outside that box. On top of that, an integration constraint that mattered more than any of the above: the site runs on Astro, generating lightweight static pages by design, specifically to avoid the weight of third-party scripts and dependencies — the exact opposite of what embedding a SaaS widget implies.&lt;/p&gt;

&lt;p&gt;So: could a self-hosted alternative match the experience, without the monthly bill and without handing a core commercial function (people booking a call with me) to an external vendor? This is the write-up of that search, the codebase audit that came out of it, and the production rollout.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape
&lt;/h2&gt;

&lt;p&gt;Four self-hosted candidates stood out as genuinely comparable — not just UI skins sitting on top of someone else's API, not just internal-scheduling tools with the public-facing UX as an afterthought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/dennisklappe/CloudMeet" rel="noopener noreferrer"&gt;CloudMeet&lt;/a&gt;&lt;/strong&gt; — Svelte + TypeScript, deployed on Cloudflare Pages/Workers/D1, free-tier friendly. MIT licensed. Clean booking UX. Single maintainer, ~490 stars, 37 commits — young, not enough track record to trust blind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/calcom/cal.diy" rel="noopener noreferrer"&gt;Cal.diy&lt;/a&gt;&lt;/strong&gt; — the community fork of cal.com's booking engine, spun up after cal.com closed-sourced their core product in April 2026, citing security risk from AI-assisted code scanning against their public repo. MIT licensed, maintained by former cal.com interns. Full scheduling engine, app-store integrations, Stripe/PayPal payment support carried over. Most feature-complete on paper. Also the youngest as an independent community project — their own docs still discourage production use without caveats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sametcn99/booking-calendar" rel="noopener noreferrer"&gt;booking-calendar&lt;/a&gt;&lt;/strong&gt; — React + TypeScript + Bun + SQLite, single-admin design, native bidirectional CalDAV sync instead of a Google/Outlook lock-in. Clean architecture (repository/service/entity separation via TypeORM). Lightweight, portable by design. Booking UI is a scrollable list of time slots — functional, but nowhere near the polish of a commercial scheduler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/alextselegidis/easyappointments" rel="noopener noreferrer"&gt;Easy!Appointments&lt;/a&gt;&lt;/strong&gt; — PHP/CodeIgniter + MySQL, ten years of active development, 3000+ stars, a paid tier that's existed for years. Most battle-tested of the four, and the one with the closest booking UX to Calendly itself: monthly calendar view, multi-step wizard.&lt;/p&gt;

&lt;p&gt;What I was actually optimizing for: framework-agnostic integration (no PHP running on the public site itself), booking UX quality, native GDPR consent handling, and enough production maturity to put in front of real prospects rather than an experimental project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narrowing down
&lt;/h2&gt;

&lt;p&gt;CloudMeet and Cal.diy dropped out early, for the same underlying reason: not enough track record for production use, not a specific flaw found. CloudMeet's single-maintainer status and short commit history made it too much of a bet. Cal.diy's own documentation still hedges on production readiness three months post-launch. Whether either becomes a serious contender or stays a one-shot project is a question for later.&lt;/p&gt;

&lt;p&gt;Worth being honest about what that is: a decision made on reputation and project age, the exact shortcut this piece argues against later on. Auditing four codebases in the same depth as the two finalists below wasn't a realistic use of time, so CloudMeet and Cal.diy got a lighter pass — young-project heuristics instead of a source read. That's a real gap in the method, not just a caveat to mention in passing.&lt;/p&gt;

&lt;p&gt;That left booking-calendar and Easy!Appointments. And this is where the UX gap tipped it: booking-calendar's public booking page is a plain scrollable list of &lt;code&gt;Monday, February 23, 2026 at 08:00 AM&lt;/code&gt; rows — accurate, but visually miles from what Calendly conditioned people to expect. No month view, no staged flow, just text to scroll through.&lt;/p&gt;

&lt;p&gt;I briefly considered franken-stacking the two — CloudMeet's frontend on top of booking-calendar's CalDAV backend, or something along those lines. Not viable in practice: different runtimes (Cloudflare Workers/D1 vs Bun/SQLite), no shared API contract, no shared data model. Bolting two incompatible stacks together usually creates more work than picking one and adapting it. Went with separation of concerns instead: pick the tool with the better public-facing UX, and if native CalDAV sync becomes a real need later, that's a second, independent tool — not a merge.&lt;/p&gt;

&lt;p&gt;Easy!Appointments won on UX. Onto the part that actually mattered for a production decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit: a TOCTOU race condition, in both remaining candidates
&lt;/h2&gt;

&lt;p&gt;Before committing to either, I wanted to check one specific thing: what happens if two visitors click the same slot at nearly the same instant? Not a theoretical concern — it's the kind of bug that never shows up in solo development and blows up the day a booking link gets shared a bit wider (a newsletter blast, a LinkedIn post, a batch of new slots opening at a fixed time).&lt;/p&gt;

&lt;p&gt;This is a classic TOCTOU (time-of-check to time-of-use) race condition: the code checks that a slot is free, then writes the booking — and nothing stops a concurrent request from doing the exact same check in between, before either write lands. Without a lock spanning both the check and the write, two requests can both conclude "free" before either commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  booking-calendar
&lt;/h3&gt;

&lt;p&gt;TypeORM-based, repository pattern, otherwise clean separation of concerns. The overlap check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;hasOverlapInSlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;slotId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;startAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;endAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;EntityManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createQueryBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a.slot_id = :slotId&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="nx"&gt;slotId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;andWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a.canceled_at IS NULL&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;andWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a.status != 'rejected'&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;andWhere&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NOT (a.end_at &amp;lt;= :startAt OR a.start_at &amp;gt;= :endAt)&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="nx"&gt;startAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;endAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCount&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;count&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Called inside a transaction (&lt;code&gt;AppDataSource.transaction&lt;/code&gt;), but no &lt;code&gt;.setLock("pessimistic_write")&lt;/code&gt;, no exclusion constraint at the schema level. Under SQLite, this never surfaces: the engine serializes writers, one at a time, by design. It's a free safety net courtesy of the storage engine — not a guarantee the application code actually enforces. The project's own architecture explicitly anticipates a migration path to Postgres or MySQL via TypeORM's driver abstraction (&lt;code&gt;type: "sqlite"&lt;/code&gt; → &lt;code&gt;type: "postgres"&lt;/code&gt;, straightforward on paper). That's exactly the migration that removes the net: under &lt;code&gt;READ COMMITTED&lt;/code&gt; isolation, two concurrent transactions can each read "no overlap" before either one's &lt;code&gt;INSERT&lt;/code&gt; commits.&lt;/p&gt;

&lt;p&gt;Confirmed by reading the CalDAV sync layer too — a second, independent race, this one against the &lt;em&gt;external&lt;/em&gt; calendar rather than the local database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getCachedBusyIntervals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;endAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;BusyInterval&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CalDAVService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;busyIntervalCache&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;cache&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startAt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;endAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end_at&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intervals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;startAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;endAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A process-wide static cache with a TTL, invalidated only &lt;em&gt;after&lt;/em&gt; a successful write. Two bookings arriving seconds apart, inside the cache window, can both read the same "free" snapshot before either has finished writing to the external calendar — the exact same TOCTOU shape, just with the external CalDAV server as the source of truth instead of the local DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Easy!Appointments
&lt;/h3&gt;

&lt;p&gt;Ten years in production, 3000+ GitHub stars, a paid tier that's existed for years. The working hypothesis going in: more real-world traffic means more chances this exact class of bug already got hit and fixed. That hypothesis doesn't survive reading the code.&lt;/p&gt;

&lt;p&gt;The public booking flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Check appointment availability before registering it to the database.&lt;/span&gt;
&lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id_users_provider'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;check_datetime_availability&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="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id_users_provider'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'requested_hour_is_unavailable'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ... customer lookup/creation, GDPR consent records, Jitsi link generation ...&lt;/span&gt;

&lt;span class="nv"&gt;$appointment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appointments_model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several unrelated operations sit between the check and the write — the race window here is wider than booking-calendar's, where check and insert at least shared a transaction. And the insert itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'book_datetime'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i:s'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'create_datetime'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i:s'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'update_datetime'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Y-m-d H:i:s'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hash'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;random_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'alnum'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'appointments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Could not insert appointment.'&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;insert_id&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;No transaction, no lock, no unique constraint. The docblock on &lt;code&gt;check_datetime_availability()&lt;/code&gt; reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"It is possible that two or more customers select the same appointment date and time concurrently. The app won't allow this to happen."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Documented intent, not what the code actually does.&lt;/p&gt;

&lt;h3&gt;
  
  
  The interesting part
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Appointments_model&lt;/code&gt; contains a method that does the correct overlap check, with a properly built query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;has_provider_conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$provider_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$start_datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$end_datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$exclude_appointment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'appointments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id_users_provider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$provider_id&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="nv"&gt;$exclude_appointment_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id !='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exclude_appointment_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Overlap: (existing_start &amp;lt; new_end) AND (existing_end &amp;gt; new_start)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group_start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'start_datetime &amp;lt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$end_datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'end_datetime &amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$start_datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;group_end&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;num_rows&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's never called anywhere in the public booking flow. The correct primitive exists in the codebase; it's just not wired in where it would matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;Neither project is "more robust" than the other on this specific point — both share the same design gap, independent of relative maturity. Reputation, age, an established commercial tier: reasonable statistical priors, not proof. The only way to know whether an open-source project actually guards against this class of bug is to read the code doing the work, not the comment claiming it does.&lt;/p&gt;

&lt;p&gt;For what it's worth, the fix on the Easy!Appointments side is close to a drop-in — the correct primitive (&lt;code&gt;has_provider_conflict&lt;/code&gt;) already exists, it's a matter of wiring it in with a lock around check+write rather than designing a new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$lock_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"provider_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$provider_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_booking"&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT GET_LOCK(?, 10)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$lock_name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;row&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="s2"&gt;"GET_LOCK(?, 10)"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Could not acquire booking lock.'&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appointments_model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;has_provider_conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id_users_provider'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'start_datetime'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'end_datetime'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'requested_hour_is_unavailable'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$appointment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;appointments_model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$appointment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT RELEASE_LOCK(?)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$lock_name&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;GET_LOCK&lt;/code&gt; rather than a schema-level exclusion constraint because MySQL has no equivalent to Postgres' &lt;code&gt;EXCLUDE USING gist&lt;/code&gt; — no declarative way to say "no two overlapping ranges for this provider" at the database level. The lock key is scoped per-provider (&lt;code&gt;provider_{id}_booking&lt;/code&gt;) rather than global, so two visitors booking with two &lt;em&gt;different&lt;/em&gt; providers at the same instant don't block each other for no reason. &lt;code&gt;RELEASE_LOCK&lt;/code&gt; sits in a &lt;code&gt;finally&lt;/code&gt; so a failed write doesn't leave the lock held until timeout.&lt;/p&gt;

&lt;p&gt;One dependency this fix quietly assumes: non-persistent database connections. &lt;code&gt;GET_LOCK&lt;/code&gt; is scoped to the MySQL session, not the PHP request — it lives and dies with the connection. That holds cleanly on a standard non-persistent connection (the CodeIgniter default), where each request gets its own connection and the lock disappears cleanly when it ends, even on an uncaught fatal. It stops holding if &lt;code&gt;pconnect&lt;/code&gt; is enabled and connections get reused across unrelated requests from a pool: &lt;code&gt;RELEASE_LOCK&lt;/code&gt; in the &lt;code&gt;finally&lt;/code&gt; might release a lock a &lt;em&gt;different&lt;/em&gt; request just acquired on the same recycled connection, or a lock could outlive the request that took it. Worth stating explicitly in the patch rather than assuming, since it's not something &lt;code&gt;has_provider_conflict()&lt;/code&gt; or the surrounding code makes obvious either way.&lt;/p&gt;

&lt;p&gt;This didn't end up shipping in my deployment — the LOCK/VERIFY/WRITE/UNLOCK skeleton above is close to production-ready, but I'd want load-test coverage on the &lt;code&gt;ANY_PROVIDER&lt;/code&gt; branch (where the code searches for &lt;em&gt;any&lt;/em&gt; available provider — the lock needs to span that search too, or two "any provider" requests can still land on the same provider/slot in parallel) before calling it done. Worth a PR upstream at some point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production rollout: a simple embed, an unexpected block
&lt;/h2&gt;

&lt;p&gt;The integration itself was straightforward: Easy!Appointments runs on its own subdomain, the contact page just drops an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; pointing at it. No PHP touches the public Astro site at all — that separation was the whole point.&lt;/p&gt;

&lt;p&gt;First test after deploying: the iframe wouldn't render. Console error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Refused to display 'https://cal.example.com/' in a frame because it set
'X-Frame-Options' to 'sameorigin'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First guess, wrong
&lt;/h3&gt;

&lt;p&gt;Given the server setup (a hosting panel with a reverse-proxy layer in front of the site), the obvious first suspect was that layer, not the app itself. Tried unsetting the header at the &lt;code&gt;.htaccess&lt;/code&gt; level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="sr"&gt; mod_headers.c&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;unset&lt;/span&gt; X-Frame-Options
    &lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Content-Security-Policy "frame-ancestors 'self' https://backstage.click"
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;IfModule&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No effect. Header still showed up on &lt;code&gt;curl -I&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual cause
&lt;/h3&gt;

&lt;p&gt;Grepping the Easy!Appointments source turned it up directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;security_headers&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options: SAMEORIGIN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options: SAMEORIGIN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app sets the header itself, in PHP, at two separate points in its own bootstrap — a hardcoded anti-clickjacking default, sensible for the admin panel, applied indiscriminately to every route including the public booking page that's meant to be embedded elsewhere. &lt;code&gt;Header unset&lt;/code&gt; in &lt;code&gt;.htaccess&lt;/code&gt; runs at the web-server response-table level, ahead of the PHP process; a &lt;code&gt;header()&lt;/code&gt; call executed later by the script itself simply overrides it. The &lt;code&gt;.htaccess&lt;/code&gt; fix couldn't have worked against this, structurally, regardless of server (Apache, Nginx, OpenLiteSpeed) — PHP has the last word on its own headers as long as output hasn't started.&lt;/p&gt;

&lt;p&gt;The working fix patches both call sites, scoped to the booking controller only so the admin panel keeps its default protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$CI&lt;/span&gt; &lt;span class="o"&gt;=&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;get_instance&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="nb"&gt;get_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$CI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'Booking'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Content-Security-Policy: frame-ancestors 'self' https://backstage.click"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Frame-Options: SAMEORIGIN'&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;Using the instantiated controller class rather than parsing &lt;code&gt;$_SERVER['REQUEST_URI']&lt;/code&gt; — this install doesn't have clean URLs enabled (&lt;code&gt;index.php&lt;/code&gt; shows up in the booking URL), so a naive &lt;code&gt;strpos($uri, '/booking') === 0&lt;/code&gt; check would silently fail to match.&lt;/p&gt;

&lt;p&gt;One thing worth flagging for anyone patching the same two files: they're core application code, not a plugin layer. A future &lt;code&gt;git pull&lt;/code&gt; or a Docker image rebuild will silently overwrite this fix. Worth keeping it as a versioned patch file (&lt;code&gt;git diff&lt;/code&gt; before/after) to reapply after upgrades, rather than losing it the next time the container gets rebuilt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;Customer data (name, email, meeting reason) staying on infrastructure I control instead of a third party's, and the site's initial page weight untouched — no third-party script added for this one feature. Cost wasn't really the driver here; free tiers cover the basics for a lot of freelancers, mine included. What I was buying back wasn't a subscription fee, it was the part where a vendor decides what "the basics" are, and where my prospects' contact details end up.&lt;/p&gt;

&lt;p&gt;Worth noting: which of the four tools actually fits depends entirely on what a given business needs, not on which one "won" here. Easy!Appointments, for instance, is also a solid self-hosted alternative to Bookly for anyone running WordPress and looking to drop a booking plugin subscription — different starting point, same underlying question.&lt;/p&gt;

&lt;p&gt;But the tool swap itself isn't really the point. This is one instance of a pattern I keep coming back to: default to self-hosted where it's reasonable, and don't let "widely used" or "ten years old" or "has a paid tier" stand in for actually checking. Reputation is a prior, not a verdict. The Easy!Appointments audit is the clearest example in this piece — a decade of production traffic, a commercial offering, thousands of stars, and a race condition sitting in the exact code path that mattered most, with the correct fix already written elsewhere in the codebase and simply never called. Maturity didn't catch it. Reading the code did.&lt;/p&gt;

&lt;p&gt;Here's the part worth sitting with, though: the fix never shipped on my own deployment either. It's sketched out above, unfinished, blocked on load-testing I haven't done. Which means the instance running this site's booking page right now is, as far as I know, still exposed to the exact TOCTOU window this whole piece is about. Self-hosting bought me visibility into that gap — Calendly would have hidden it behind a vendor's SLA and I'd have had no way to know either way. It didn't buy me the fix. That's a separate piece of work, and skipping it doesn't get excused by having found the bug in the first place. Self-hosting gets you control. Security still has to be built, on your own time, by someone — and until that patch actually lands, that someone is a task on my list, not a claim in this article.&lt;/p&gt;

&lt;p&gt;Neither was this the fastest of the four options, or the most obvious. It took a real comparison between projects, a debugging detour once in production, and time spent reading source instead of trusting a README. That's the actual cost of running your own infrastructure instead of renting someone else's.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Eighty Percent Done Is Not a Real Number</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:51:00 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/eighty-percent-done-is-not-a-real-number-54nf</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/eighty-percent-done-is-not-a-real-number-54nf</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;
  
  
  Why this one, and why now
&lt;/h2&gt;

&lt;p&gt;I always have several projects running in parallel — a maintenance CRM for tradespeople, a SaaS platform for restaurants, an automated newsletter, a management tool for training organizations… Except for that last one, none of them was ready to sell. And I have a whole pile of projects left "as is," untouched for months. An SEO scanner, a document analyzer, a mailbox scanner, a SQL audit tool, and others.&lt;/p&gt;

&lt;p&gt;Brainstorming session with Claude, my favorite AI assistant — we went through my repos: the CRM for heating tradespeople, the restaurant platform, the bilingual newsletter, and this SQL audit folder, several months old. The point of the session was to decide. I wanted to finish something quickly and start promoting it. Each project had a state of progress, a level of uncertainty, a distance to something sellable.&lt;/p&gt;

&lt;p&gt;The CRM still needs weeks of development — just for the features I know are essential — and field feedback from real tradespeople to validate the workflows. The restaurant platform was more of an architecture bet than a finished product. The newsletter has no client waiting for it.&lt;/p&gt;

&lt;p&gt;The SQL audit, though, looked done. Two &lt;code&gt;.sql&lt;/code&gt; files, a &lt;code&gt;UNION ALL&lt;/code&gt; of ten &lt;code&gt;SELECT&lt;/code&gt; statements per engine, a bash script pushing that to &lt;code&gt;psql&lt;/code&gt; or &lt;code&gt;mysql&lt;/code&gt; and converting to JSON, a PDF generator for a professional presentation. But I doubt I'd ever actually run it.&lt;/p&gt;

&lt;p&gt;It was a forgotten project. A PoC — it looked like a finished application, it wasn't one.&lt;/p&gt;

&lt;p&gt;The apparent distance to a sellable service looked like the shortest on the whole list: no UI needed, no architecture to rethink, just a script to test, improve, and stabilize. It was this single head start — more than any technical interest in the project — that tipped the scale. Its illusory proximity to the finish line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PoC that was supposed to work
&lt;/h2&gt;

&lt;p&gt;Before touching a single real database, I asked four different models for a code review — DeepSeek, Big Pickle, GLM, Qwen. Each produced a detailed report: bugs, dependencies, a phantom call to an LLM for the whole write-up section. Useful. But all four converged on the same blind spot:&lt;/p&gt;

&lt;p&gt;None of the four had tried running the script against a MySQL or PostgreSQL database.&lt;/p&gt;

&lt;p&gt;They were reading SQL. Not its behavior. And that blind spot, shared by four independent models, confirmed exactly the wrong conclusion: that the project was closer to done than it actually was.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first real database showed in three lines, and what it cost the schedule
&lt;/h2&gt;

&lt;p&gt;First run against a vanilla PostgreSQL 16, without the &lt;code&gt;pg_stat_statements&lt;/code&gt; extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"psql:...audit_all_postgres.sql:193: ERROR: relation &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; does not exist"&lt;/span&gt;&lt;span class="err"&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;Three lines. Nothing else. A single query on a missing extension had killed the entire &lt;code&gt;UNION ALL&lt;/code&gt; — the other nine metrics never got a chance to run.&lt;/p&gt;

&lt;p&gt;The starting estimate — "this is the closest-to-finished project" — had just taken a hit. Not fatal, but real: what was supposed to be a wrap-up session turned into a series of discovery sessions. The good news, in hindsight, is that the remaining distance was still shorter than for the other two projects in the running. The bad news is that it had nothing to do with the distance estimated at the start.&lt;/p&gt;

&lt;p&gt;The same pattern repeated on every database tested: a missing system table or an insufficient privilege would kill the entire script, instead of just skipping the one affected metric. &lt;code&gt;pg_authid&lt;/code&gt; inaccessible without superuser, &lt;code&gt;mysql.user&lt;/code&gt; without the right GRANT, &lt;code&gt;column_statistics&lt;/code&gt; missing on MariaDB — same symptom every time.&lt;/p&gt;

&lt;p&gt;Fix, every time: check access before querying, return &lt;code&gt;UNAVAILABLE: &amp;lt;reason&amp;gt;&lt;/code&gt; on failure, let the rest of the script continue. Simple in theory.&lt;/p&gt;
&lt;h3&gt;
  
  
  The trap I didn't know about
&lt;/h3&gt;

&lt;p&gt;One of the fixes looked trivial:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_extension&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;extname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pg_stat_statements'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
     &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&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="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It looks correct. It isn't.&lt;/p&gt;

&lt;p&gt;Caught in production, not in review: PostgreSQL validates the existence of &lt;code&gt;pg_stat_statements&lt;/code&gt; at query-parsing time, not when the &lt;code&gt;CASE&lt;/code&gt; branch actually executes. The conditional check protects nothing — the query fails before it even knows which branch would be taken.&lt;/p&gt;

&lt;p&gt;That logic had to move out of static SQL and into the bash script, with a separate, upfront detection query. A &lt;code&gt;CASE&lt;/code&gt; that was syntactically flawless and semantically useless. None of the four reviews had caught it — it took a real engine to make it fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that had nothing to do with a privilege
&lt;/h2&gt;

&lt;p&gt;The longest one to isolate: &lt;code&gt;roles_without_password&lt;/code&gt; kept coming back &lt;code&gt;UNAVAILABLE&lt;/code&gt; despite confirmed GRANTs, twice, on two different accounts, &lt;code&gt;SHOW GRANTS&lt;/code&gt; output in hand.&lt;/p&gt;

&lt;p&gt;It wasn't a permissions problem. It was a string-formatting problem. The check compared &lt;code&gt;GRANTEE&lt;/code&gt; — as stored in &lt;code&gt;information_schema.SCHEMA_PRIVILEGES&lt;/code&gt;, formatted &lt;code&gt;'user'@'host'&lt;/code&gt;, two pairs of quotes — against a string built with &lt;code&gt;CONCAT("'", USER(), "'")&lt;/code&gt;, which produces &lt;code&gt;'user@host'&lt;/code&gt;, a single pair of quotes around the whole thing. The two strings could never match, and the failure looked exactly like the thing you fear most in an audit tool: a false negative on security.&lt;/p&gt;

&lt;p&gt;Second trap, dumber still: the MySQL script never asked the user for a database name, unlike the PostgreSQL flow, which had that prompt from the start. With no database selected, &lt;code&gt;DATABASE()&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt;, and any &lt;code&gt;TABLE_SCHEMA = DATABASE()&lt;/code&gt; condition matches nothing at all.&lt;/p&gt;

&lt;p&gt;Result: &lt;code&gt;bloated_tables&lt;/code&gt; came back as 0 on a table whose bloat had been independently measured and confirmed at 23% outside the script.&lt;/p&gt;

&lt;p&gt;Zero didn't mean "no bloat." Zero meant "no rows found, because the question asked didn't correspond to anything."&lt;/p&gt;

&lt;p&gt;That's the kind of error an audit tool must never produce silently: a reassuring zero that measured nothing. This was no longer a question of distance to the finish line. It was a question of direction — I thought I was heading toward a finished script, I was heading toward ground I had never actually seen.&lt;/p&gt;
&lt;h2&gt;
  
  
  A metric that didn't measure what its name promised
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;buffer_pool_ratio&lt;/code&gt; — &lt;code&gt;innodb_buffer_pool_size / max_connections&lt;/code&gt; — came back as 1,789,569.7067 on a real database. A ratio has no business exceeding 1.&lt;/p&gt;

&lt;p&gt;The calculation had no bug; it simply never made sense. Dividing a memory size in bytes by a theoretical maximum connection count doesn't produce any intelligible unit — it's neither a ratio, nor a per-connection allocation, nor a saturation indicator. Four code reviews had nothing to say about it, because nothing about it was syntactically wrong.&lt;/p&gt;

&lt;p&gt;Replaced by the buffer pool hit rate — &lt;code&gt;1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)&lt;/code&gt; — which measures something real: the proportion of reads served from memory cache rather than disk. On the same database: 99.75%. A number that means something.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ten metrics didn't cover everything — not even the essentials
&lt;/h2&gt;

&lt;p&gt;Once the pipeline was stable on the original ten metrics, the report generated against n8n's production database was coherent — ten measurements, one score, nothing inconsistent in the results. It was in submitting that report to another LLM, for a second look, that the gap showed up: nothing in those ten metrics measured connection encryption.&lt;/p&gt;

&lt;p&gt;It wasn't a test that revealed the hole. It was a re-read. The LLM proposed exactly one additional metric — &lt;code&gt;ssl_enforced&lt;/code&gt; — and adding it was enough to change the priority order of the risk matrix and the final score. Once &lt;code&gt;ssl_enforced&lt;/code&gt; was in, the lack of encryption on the n8n database jumped to the top of the risk matrix, ahead of everything the original ten metrics had found.&lt;/p&gt;

&lt;p&gt;Three more followed, separately, proposed by Kimi from a broader list of candidates ranked by ease of implementation. &lt;code&gt;connection_count_ratio&lt;/code&gt;, &lt;code&gt;unused_indexes&lt;/code&gt;, &lt;code&gt;long_running_transactions&lt;/code&gt; were kept precisely because they fit the existing model without complicating it — one query, one value, the same defensive pattern as the rest of the script. Other ideas from the same list were dropped: &lt;code&gt;disk_usage_growth_rate&lt;/code&gt; would have required historical snapshots, changing the nature of a tool built for point-in-time audits; &lt;code&gt;table_count&lt;/code&gt; and &lt;code&gt;database_size&lt;/code&gt; were just display context, not a risk signal.&lt;/p&gt;

&lt;p&gt;None of the four new metrics is complex to collect — one query each, on both engines. But none was in the original list, because that list had been built around performance and schema hygiene, not exposure surface. It took an outside eye re-reading the result, not the code, to make that gap visible.&lt;/p&gt;

&lt;p&gt;A third pass, this time by Mistral, focused on how the report was presented rather than on its content, and raised two separate points: &lt;code&gt;duplicate_primary_keys = 0&lt;/code&gt; — a positive result — stayed buried in a prose paragraph in the middle of the overall assessment, never showing up in a proper, systematic checklist table covering every metric, including the ones with no issue. And the penalties, already grouped by category internally in the score calculation, were never exposed separately in the final report. Two last metrics followed from that pass — &lt;code&gt;duplicate_indexes&lt;/code&gt;, &lt;code&gt;large_tables_count&lt;/code&gt; — kept after checking they didn't duplicate anything already there.&lt;/p&gt;
&lt;h2&gt;
  
  
  The count
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metrics per engine&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior on missing privilege&lt;/td&gt;
&lt;td&gt;entire script stopped&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UNAVAILABLE: &amp;lt;reason&amp;gt;&lt;/code&gt;, the rest continues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scoring&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;deterministic, broken down by category (security / performance / integrity)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL/TLS detection&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;yes — surfaced the #1 risk on a production database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report languages&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;multilingual, stable section identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code reviews before the first real run&lt;/td&gt;
&lt;td&gt;4 models&lt;/td&gt;
&lt;td&gt;4 models, 0 behavioral bugs found&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  The coda: debugging the report that describes the bugs
&lt;/h2&gt;

&lt;p&gt;One last, almost ironic detail: at final-render time, the colored dots in the Risk Matrix — red/orange/gray by score — stopped showing up in the PDF. Copilot never found the cause. It churned for three hours, burned through more than $20 in credits on this one task (with Haiku 4.5) — still no colored dot. Big Pickle, in OpenCode, fixed it in under a minute, for free: every one of Copilot's hypotheses was wrong. The only problem was the column it was trying to insert the dot into.&lt;/p&gt;

&lt;p&gt;But the real lesson wasn't in the fix — it was in the method: don't attempt a third blind implementation, check and re-read the actual generated code instead of guessing. Same principle as the rest of this project — believe nothing until it has run, once, in a real environment.&lt;/p&gt;
&lt;h2&gt;
  
  
  What determinism doesn't cover
&lt;/h2&gt;

&lt;p&gt;Two versions of the same report, on the same database, generated by two different LLMs for the narrative part — the global score, the category scores, the risk matrix: identical to the decimal point, because those numbers never come out of the LLM. That's exactly what deterministic scoring is supposed to guarantee: the model writes, it doesn't grade.&lt;/p&gt;

&lt;p&gt;But one of the two reports cited SOC 2 in the business impact of a recommendation. French database, French LLM (Mistral Large), French report — SOC 2 had no business being there. The score hadn't lied. The sentence around it had — but not by pure invention. Mistral didn't fabricate SOC 2 out of nowhere: it's a real framework, consistent with the report's consulting tone, correctly defined in itself. The problem isn't that the model hallucinated a concept — it's that it applied it to a hypothetical place it didn't actually know. A misapplication, not an invention.&lt;/p&gt;

&lt;p&gt;Same family of bug as &lt;code&gt;DATABASE()&lt;/code&gt; returning &lt;code&gt;NULL&lt;/code&gt; on the MySQL side: a field never asked for, silently filled in with whatever seemed plausible. Except this time it wasn't a sloppy query — it was a prompt that never anticipated it might be missing a piece of information as central as the client's country. Except the country doesn't actually matter that much: weak security is weak security, whether you're subject to SOC 2, GDPR, ISO 27001, or any other framework. Deterministic scoring closes the door to improvisation on the numbers. It says nothing about improvisation in the sentence explaining those numbers — and nowhere, in the sixteen metrics or in the generation prompt, does anything ever ask what country the audited database is in. The two numbers stayed identical between the two reports. Only the sentence built on top of them was free to be wrong, fluently, without ever looking wrong.&lt;/p&gt;

&lt;p&gt;That's fixed now: a jurisdiction flag precedes every report generation — &lt;code&gt;--jurisdiction FR&lt;/code&gt;, &lt;code&gt;--jurisdiction US&lt;/code&gt;, or any other ISO 3166 value depending on the client — and the narration prompt is now explicitly forbidden from naming a compliance framework that jurisdiction doesn't justify.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;

&lt;p&gt;What's left, once all of that is behind it: a tool you run from the command line against a MariaDB or PostgreSQL database, that finishes in a few minutes, and produces a complete PDF report — global score, risk matrix, prioritized recommendations, technical appendix — without needing to babysit the run or guess whether the result can be trusted. That object, not the list of bugs that led to it, is what's standing today here:&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://dbgrade.tech/" 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%2Fdbgrade.tech%2Fimages%2Fog-image.jpg" height="420" class="m-0" width="800"&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://dbgrade.tech/" rel="noopener noreferrer" class="c-link"&gt;
            dbgrade — Independent PostgreSQL &amp;amp; MariaDB/MySQL Audits - Security, integrity, performance — scored and explained
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Independent SQL audits for PostgreSQL and MariaDB/MySQL. 16 security, integrity and performance metrics, scored, with a PDF report you can act on — even if you don't read SQL.
          &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%2Fdbgrade.tech%2Fimages%2Ffavicons%2Ffavicon-96x96.png" width="96" height="96"&gt;
          dbgrade.tech
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Sixteen metrics per engine, up from ten at the start. Deterministic scoring, broken down by category. Multilingual support where section titles keep a stable internal identifier while their label displays translated. An appendix that names the affected tables and constraints, not just their count.&lt;/p&gt;

&lt;p&gt;You can see what it looks like on SlideShare:&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__body flex items-center justify-between"&gt;
        &lt;a href="https://fr.slideshare.net/slideshow/comprehensive-postgresql-database-audit-report-and-security-analysis/288580887" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;fr.slideshare.net&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;None of this was planned at the start. The two-file SQL PoC did exactly what it said it would, as long as no one ran it anywhere other than the imagination of the person who wrote it (yes, that was me, but let's not dwell on it).&lt;/p&gt;

&lt;p&gt;This project was picked because it looked closest to the finish line. It was, in the sense that the remaining distance ended up being covered faster than for the other projects waiting in line. But that proximity, like &lt;code&gt;buffer_pool_ratio&lt;/code&gt;, had never actually been verified — only estimated, from code that compiled and hadn't crashed yet. It kept correcting itself with every bug found, right up to the last one, SOC 2, discovered after everything else already looked finished.&lt;/p&gt;

&lt;p&gt;A script that never crashes proves nothing. &lt;code&gt;buffer_pool_ratio&lt;/code&gt; never failed anything — it ran, it produced a number, and that number meant nothing. No amount of re-running would have shown that: it took comparing the result against a measurement taken elsewhere, by another method, to see the calculation was hollow. Same story for &lt;code&gt;bloated_tables&lt;/code&gt; returning 0 on a table whose bloat was confirmed at 23% outside the script — a zero that runs without error isn't a correct zero. And the same story, further upstream, for "this project is the closest to done": an estimate that held up right until someone checked it, and not a moment longer.&lt;/p&gt;

&lt;p&gt;The difference between a tool that works on paper and one that holds up against a real database isn't the sophistication of the code, and it isn't the number of times you managed to make it crash either. It's whether every result it produces — including the estimate that justified starting the project in the first place — corresponds to something actually verified, not merely plausible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical sidebar — tools and method
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Initial code review (before any real run):&lt;/strong&gt; DeepSeek, Big Pickle, GLM, Qwen — four models, four reports, one shared blind spot: no access to a real database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engines tested:&lt;/strong&gt; PostgreSQL 16 (vanilla, then with extensions), MariaDB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bugs found only under real conditions:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pg_extension&lt;/code&gt; validated at parse time, not when the &lt;code&gt;CASE&lt;/code&gt; branch executes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GRANTEE&lt;/code&gt; format mismatch between &lt;code&gt;information_schema&lt;/code&gt; and &lt;code&gt;CONCAT(USER())&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE()&lt;/code&gt; returning &lt;code&gt;NULL&lt;/code&gt; for lack of a database-selection prompt on the MySQL side&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;buffer_pool_ratio&lt;/code&gt; metric with no basis (a division with no real relationship)&lt;/li&gt;
&lt;li&gt;Total absence of SSL/TLS detection from the original metric list&lt;/li&gt;
&lt;li&gt;A compliance framework (SOC 2) cited without any jurisdiction provided or verified&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Report generation:&lt;/strong&gt; Markdown → PDF via WeasyPrint, deterministic scoring kept separate from LLM narration, risk dots in pure CSS (&lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; + &lt;code&gt;border-radius&lt;/code&gt;), a 3-level palette (red/orange/gray — never green, because no line in the Risk Matrix is good news).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug method retained:&lt;/strong&gt; before retrying an implementation, check the code and what it actually produces, rather than attempting a blind fix based on a description of the symptom.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>postgres</category>
      <category>database</category>
    </item>
    <item>
      <title>Unboxable in Tech: The Evidence Locker</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Thu, 09 Jul 2026 14:35:00 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-the-evidence-locker-20n4</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-the-evidence-locker-20n4</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-2knm"&gt;Eleven exhibits&lt;/a&gt;, last time. A career that kept refusing to fit inside a single box — trainer, restaurant owner, postal worker, developer, school aide, developer again — and a closing question I didn't have an answer to: whose problem was that, mine or the box's.&lt;/p&gt;

&lt;p&gt;I still don't have a clean answer. But I didn't stop while I was waiting for one. The same refusal to fit a category, applied to code instead of a resume, produced eight more exhibits. Here's what came out of that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 12 — The Graph
&lt;/h2&gt;

&lt;p&gt;Exhibit 11 ends with me still standing in a classroom, four days a week, helping a kid who isn't mine focus on work that isn't mine either. That's not a metaphor. That's the actual schedule.&lt;/p&gt;

&lt;p&gt;The other three days, I build.&lt;/p&gt;

&lt;p&gt;At some point I tried to explain this to a recruiter using a normal CV — the linear kind, one line per job, chronological, tidy. It read like someone who couldn't hold a job. Trainer, restaurant owner, postal worker, school aide, developer, back to developer, always developer underneath — but a timeline doesn't show "underneath." It only shows sequence.&lt;/p&gt;

&lt;p&gt;So I stopped writing a timeline and built a graph instead. Nodes for skills, nodes for roles, edges for what actually connects them — the same instinct that made me question a CMS or an ORM, applied to my own career. Turns out the incoherent-looking CV was a rendering problem, not a content problem. The data was fine. The visualization was lying.&lt;/p&gt;

&lt;p&gt;I don't know yet if a graph reads better to a hiring manager than eleven bullet points. I know it reads truer to me.&lt;/p&gt;

&lt;p&gt;The classroom aide job covers part of the month. The graph is what I actually am. I'm still working out how to make those two facts stop contradicting each other.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The full breakdown — nodes, edges, why a graph beats a timeline — is in &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/beyond-the-linear-cv-3fik"&gt;Beyond the Linear CV&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here's the same instinct, aimed at code instead of a career.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 13 — AJC Bridge
&lt;/h2&gt;

&lt;p&gt;I wanted to keep writing on WordPress. Not because it's good — because it's the interface I already know, the one where the friction is zero. But I didn't want the output to live only on WordPress hosting, with WordPress's weight, for an audience that doesn't care what CMS produced the page.&lt;/p&gt;

&lt;p&gt;Not a migration. A bridge.&lt;/p&gt;

&lt;p&gt;So I built one. A plugin that hooks into WordPress's publish action and pushes the content out to Hugo, Astro, or Jekyll — whichever the destination site runs. Write once, in the tool I already know, ship everywhere the audience actually reads.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't the code. WordPress publishing to a static site generator is a solved problem in a dozen half-working ways. The interesting part was resisting the obvious move: dropping WordPress entirely and calling it progress.&lt;/p&gt;

&lt;p&gt;It wasn't the problem. It was infrastructure I was fluent in, carrying content I cared about, slowed down by a hosting model that didn't match what I actually needed. The enemy people usually name — "WordPress is bloated," "just use a static site" — was never the real constraint. The constraint was the idea that fluency doesn't count, that the "modern" tool always wins by default.&lt;/p&gt;

&lt;p&gt;I entered it in a GitHub Copilot CLI challenge. Top 7%, runner-up, top 25 out of 400+. Not because the idea was original — because most entries solved a technical problem and this one kept a personal workflow instead of throwing it away.&lt;/p&gt;

&lt;p&gt;Same instinct as the graph, pointed at my own publishing habits instead of my own career: don't ask what's fashionable to replace. Ask what's actually broken.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Full writeup: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/actually-static-when-wordpress-stops-being-the-enemy-37h5"&gt;Actually Static: When WordPress Stops Being the Enemy&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 14 — The Memory dev.to Doesn't Give You
&lt;/h2&gt;

&lt;p&gt;dev.to's built-in stats tell you views, reactions, comments — per post, right now. They don't tell you what happened three months ago unless you screenshot it yourself. They don't tell you which article is still quietly gaining readers a year later, or which comment thread turned into something worth following up on. The platform has no memory. It only has a present tense.&lt;/p&gt;

&lt;p&gt;I wanted to know my own history. So I started small — local scripts, SQLite, pulling my stats on a schedule and just... keeping them. No plan beyond "don't lose this data."&lt;/p&gt;

&lt;p&gt;Small worked until it didn't. Once I wanted to actually query the history — trends, comparisons, which posts age well — SQLite on my laptop stopped being enough. So I rebuilt it as a real stack: FastAPI, PostgreSQL with pgvector, the whole thing containerized and running on my own VPS. I tried Superset for the dashboards first. Dropped it — the learning curve cost more than the output was worth for what I actually needed to see. Streamlit did the same job faster, for less. Built most of it working alongside GitHub Copilot CLI, treating the agent less like autocomplete and more like a second pair of hands on the parts I already knew how to spec but didn't want to type twice.&lt;/p&gt;

&lt;p&gt;None of this was commissioned. Nobody asked for a dev.to analytics platform. I built it because a platform that forgets your own history by design is a platform you have to out-remember yourself.&lt;/p&gt;

&lt;p&gt;Same instinct again: don't accept the tool's amnesia as a fact of life. Build the layer underneath it. And don't keep a tool just because you already invested in it — Superset taught me that too.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The build, start to finish: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-local-sqlite-scripts-to-a-cloud-platform-with-github-copilot-cli-5a5h"&gt;From Local SQLite Scripts to a Cloud Platform with GitHub Copilot CLI&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 15 — The Polish Botnet
&lt;/h2&gt;

&lt;p&gt;The dev.to analytics platform runs on Cloud Run. One month, the bill jumped. Not a little — enough to notice immediately, enough to make me stop and actually read the logs instead of assuming a traffic spike I should be happy about.&lt;/p&gt;

&lt;p&gt;It wasn't traffic. It was a botnet, most of it tracing back to Poland, hammering endpoints that had no business being hit that often. Nothing exotic — no breach, no data loss — just noise dressed up as load, quietly billing me for the privilege of being scanned.&lt;/p&gt;

&lt;p&gt;The fix wasn't a bigger instance or a rate limiter slapped on top. It was going back to what was actually being exposed, closing what didn't need to be public, and making the rest cost the attacker more than it cost me to ignore. Bill dropped 96%.&lt;/p&gt;

&lt;p&gt;The boring lesson: most cost problems people solve by scaling up are actually security problems they haven't looked at yet. Same with most "obsolete" systems people solve by rewriting — half the time the fix is smaller and closer than the rewrite everyone reaches for first.&lt;/p&gt;

&lt;p&gt;By now the pattern is automatic: notice the gap between what you assumed was happening and what's actually happening, then close that gap before you touch anything else.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Full logs and numbers: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/how-i-cut-my-cloud-run-bill-by-96-by-stopping-a-polish-botnet-5ak"&gt;How I Cut My Cloud Run Bill by 96% by Stopping a Polish Botnet&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 16 — WordPress to Hugo, and Then Further
&lt;/h2&gt;

&lt;p&gt;I wrote up a WordPress-to-Hugo migration as a lightning-fast rebuild: strip the CMS, keep the content, ship something that loads in milliseconds instead of seconds. That article did fine. What it didn't cover is that I kept going after I published it.&lt;/p&gt;

&lt;p&gt;A one-off migration script is fine for one site. I didn't understand that it wasn't a script, it was the start of a pipeline, until the second blog. By the third, the pattern was undeniable: different content structures, different plugin quirks, different things that needed mapping instead of just copying. So the script became a pipeline for real — content extraction, image handling, redirect mapping, the parts that don't show up in a "look how fast this loads now" screenshot but are the actual work.&lt;/p&gt;

&lt;p&gt;The interesting version of a project is never the one you publish. It's the one that kept getting used after the applause stopped, quietly turning into infrastructure because the alternative was solving the same problem from scratch every time someone asked. The first fix is a demo. The real fix is what's still running six months later.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Both migrations, written up separately: &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/wordpress-to-hugo-lightning-fast-sites-in-2025-48pa"&gt;WordPress to Hugo&lt;/a&gt; and, once the pipeline grew up, &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/from-wordpress-to-astro-three-days-to-reclaim-control-5dn2"&gt;From WordPress to Astro: Three Days to Reclaim Control&lt;/a&gt;. Neither article covers what happened after the second and third client — this is that part.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 17 — GCF Pro
&lt;/h2&gt;

&lt;p&gt;Training organizations in France operate under Qualiopi certification — a compliance framework that dictates how they manage funding, administrative records, and audit trails. Most run this on tools built for something else entirely: generic CRMs bent sideways, spreadsheets held together by habit, or software priced for enterprises with none of the actual compliance logic built in.&lt;/p&gt;

&lt;p&gt;I built GCF Pro as the tool I kept seeing training organizations wish they had — self-hosted, PHP, no ORM, no framework heavier than the problem, sold as a license instead of a subscription because these organizations don't want their compliance data living on someone else's SaaS pricing decisions. On-premise wasn't a limitation I accepted. It was the actual requirement, once I understood what "audit trail" means to someone whose certification depends on it.&lt;/p&gt;

&lt;p&gt;~28,000 Qualiopi-certified organizations exist in France. That's not a hypothesis, it's a filtered list. What I don't have yet is proof that a list is a market.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The product lives at &lt;a href="https://gcfpro.fr/" rel="noopener noreferrer"&gt;gcfpro.fr&lt;/a&gt;. Documentation at &lt;a href="https://doc.gcfpro.fr/" rel="noopener noreferrer"&gt;doc.gcfpro.fr&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 18 — Prospection CRM, and the Extension That Watches With Me
&lt;/h2&gt;

&lt;p&gt;Cold outreach at scale needs a database. Mine started as one — scraped from public Qualiopi records, enriched, campaign-tracked, CNIL-compliant by design because I'd rather build that constraint in than bolt it on later.&lt;/p&gt;

&lt;p&gt;The database wasn't the interesting part. The interesting part is what happens when I'm just browsing, not prospecting on purpose. I built a Chrome extension: when I land on a site that looks like a prospect, one click reads the current page and its legal notice — company name, SIRET, phone, the manager's name — and queues a scan of the essentials, security headers, PHP version, performance, the same diagnostic I run at scale, but triggered by nothing more than me looking at a page.&lt;/p&gt;

&lt;p&gt;It collapses the gap between noticing something and acting on it. No copy-paste, no switching tabs to a form, no "I'll add this later" that never happens. The tool doesn't wait for me to decide I'm working. It assumes I always am, a little, and gets out of the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 19 — S&amp;amp;R CRM
&lt;/h2&gt;

&lt;p&gt;A client needed maintenance-contract tracking for heating and plumbing technicians — which building has which equipment, which contract covers it, when the next service is due. The instinct most tools have is to attach a contract to a customer. That's wrong for this industry: customers move, buildings don't. A boiler doesn't care who owns the house this year.&lt;/p&gt;

&lt;p&gt;So contracts follow buildings, not occupants. It sounds like a small modeling decision. It's the difference between a system that quietly breaks every time someone sells a house, and one that doesn't.&lt;/p&gt;

&lt;p&gt;What started as one client's fix is turning into a standalone product — ScheduleX for the calendar side, Mazer for the interface, the same PHP-native backend as everything else here. Still mid-transformation. I don't yet know if the building-not-occupant model is obvious to anyone outside this one industry, or if it's the whole pitch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;Eight exhibits — a bridge, a memory layer, a graph, a lower bill, a migration pipeline, a compliance tool, a browser extension that thinks alongside me, a CRM that models the world correctly instead of conveniently. Different problems, same reflex, every time: look at what's actually broken before you believe the story about what's supposed to replace it.&lt;/p&gt;

&lt;p&gt;Line them up next to the first eleven and something becomes obvious that wasn't obvious to me while I was living it. The career that looked scattered on a resume — trainer, restaurant, post office, developer, school aide — and the projects that look scattered on a portfolio — an ERP, a browser extension, a CV visualization, a cloud bill — are the same shape. Neither is a list of unrelated things I happened to do. Both are what happens when someone keeps asking "what's actually broken here" instead of "what's the standard answer here," across every domain that crosses their path, on a schedule nobody hired them to keep.&lt;/p&gt;

&lt;p&gt;I know how to do that. I've never worked out how to say what &lt;em&gt;that&lt;/em&gt; is, in three words, on a business card, to someone who has thirty seconds and needs a category before they'll listen further.&lt;/p&gt;

&lt;p&gt;So here's the actual question — and it's not a figure of speech: reading through this list, is there something here you'd actually pay for? And if so, what would you call it? I'm not fishing for validation. I'm asking because I've lost the ability to see my own work from the outside, and neither the first eleven exhibits nor these eight taught me how. But there's a second question underneath the first one, and it's the one I actually need answered: not just "which of these has a price," but what does someone who does &lt;em&gt;all&lt;/em&gt; of this look like from where you're standing? Is there a name for that? Because from in here, it just looks like the same person solving whatever's in front of them, and I can't tell anymore if that's a portfolio or just a personality.&lt;/p&gt;

</description>
      <category>career</category>
      <category>discuss</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>1%</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:22:28 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/1-15n0</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/1-15n0</guid>
      <description>&lt;p&gt;&lt;em&gt;Santa Clara, March 14, 2029.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four objects on Jensen's desk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An NSA report, face down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Kai Chen's badge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An iPhone, screen lit: "The White House."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And on the wall, framed since 2019, a GeForce 256 signed by hand: "The one that started it all — 1999."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen isn't looking at the frame.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He's looking at the number on the screen in front of him.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1%.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Global inference datacenter market share. One percent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;12% domestic — captive hyperscalers, federal contracts, enterprise stacks buried too deep in CUDA to move.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Iran.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He thinks back to November 2023. The Senate. His own voice: "These restrictions will only accelerate the development of their own chips."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nobody listened.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Neither did he, in the end. He had known. He had chosen silence — the right relationships, the right slice of the pie. The silence that paid well.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From the screen left on in the hallway, a familiar voice:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"We're winning. We've always been winning. Everything else is fake news."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen stands. Turns off the screen on his way out.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;He picks up Kai Chen's badge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Twelve years at nVidia. Lead architect of the Hopper inference engine. His departure message, brief: "New opportunity." LinkedIn said Chengdu.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The badges had started piling up faster than the new hires could replace them. He wasn't the first. He wasn't the last.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From Washington, the voice, a few weeks earlier:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Traitors. Losers. They'll come crawling back."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;None of them had.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen sets the badge down.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;He looks at the NSA report.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Doesn't turn it over.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The HX-9 Pro had shipped in October 2028. 18 ARM cores. 512GB of soldered xGDDR8 — a memory variant co-developed for LLM inference workloads, where standard GDDR8 still aimed at graphics rendering. 210 watts TDP. $7,800. Built in Chengdu, assembled in Penang, sold everywhere.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everywhere but here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three weeks earlier, a Tier-2 datacenter in Ohio had quietly swapped its last nVidia rack. The migration had taken a weekend.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Somewhere in San Francisco, this morning, a CTO had opened Signal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What are you paying?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Berlin replied twenty seconds later.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"811,000."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The quote on the CTO's screen read $4,032,000. Same compute capacity. Same workload. Same output.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 500% tariffs on Asian components weren't protecting anything anymore. They were just taxing Americans.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Intel and AMD had their own versions — ARM, xGDDR8, NPU clusters, open stack. Competitive on paper. Built in Penang and Taiwan. Caught in the same tariffs on their own components, manufactured offshore in fabs Washington no longer really controlled. 19% and 23% domestic market share — ahead of nVidia, but for reasons nobody in their boardrooms found particularly glorious.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It was all there.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jensen stops in front of the GeForce 256.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1999. The GPU that started everything — transform and lighting in hardware, for the first time. The competition had smiled. 3dfx. S3. Matrox. They were still smiling six months later when nVidia had buried them one by one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He knew this story.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He had lived it from the other side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;2026. The Commerce Department publishes restrictions on Fable 5 and Mythos 5. A Friday. 5:21 PM.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thirty hours later, Zhipu releases GLM-5.2. 744 billion parameters. One million token context window. MIT license. Open weights. No geographic restrictions.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thirty hours.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sakana AI ships Fugu the following week — an orchestrator aggregating the best available models behind a single API. Performance comparable to Fable 5 on engineering benchmarks. Twenty dollars a month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From Washington, the voice thundered across every screen:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Our technologies are the best in the world. The best. Nobody can catch us. Nobody."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GLM-5.2 had been live for eighteen hours.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the boardrooms of Santa Clara, the information was noted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing changed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It was all there.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;2025. Apple ships the M4 Ultra. 512GB unified memory. 200 watts. 70B inference locally, without breaking a sweat. The blueprint for what an ARM accelerator with massive memory could be — designed for Final Cut Pro and Xcode, used to run frontier models on a desktop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Apple hadn't meant to prove anything to nVidia. It was a side effect.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;That same year, Moffett AI publishes its MLPerf Inference results. The S30: twice the H100 throughput. One third of the power draw. Built in China, on sparsification architectures that nobody in Santa Clara was taking seriously.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;ROCm 9.x was there too. Open source, MIT, native PyTorch. 90% of H100 inference performance — on nVidia hardware. 600% on an HX-9 Pro.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Microsoft had smiled too, in 1998, reading the first Linux server benchmarks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It was all there.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;2023. DeepSeek releases R1. Training cost: $6 million. Not $100 million. Six million — on chips they had been refused, optimizing the algorithm where they had been blocked from optimizing the hardware.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Constraint produces innovation.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Abundance produces dependency.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In a situation room in Washington, an analyst traces a curve.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"They're slowing down."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nobody disagrees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The curve would keep climbing for eighteen months.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen had known. He had said it in front of the Senate. In the room, a national security advisor had murmured to his neighbor: "If we do nothing, they get immediate access to the best chips. If we act, we risk accelerating their own industry. Either way, the risk is real."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The senators had nodded and voted for the restrictions anyway.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen had flown back to Santa Clara. Had sold H100s at $40,000 a unit. Margins no other industry would have dared post.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rational. Short term.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It was all there.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;2022. Bureau of Industry and Security. Entity lists. H100, A100, H800 — progressively locked down. ASML barred from delivering EUV machines. ARM pressured into restricting its licensees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The intent: maintain technological hegemony. Preserve the lead. Consolidate control.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The precedent existed. It had a name.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AMD.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1982: IBM forces Intel to license x86. Intel agrees, convinced AMD will remain a follower indefinitely. 1993: the K5 starts to worry them. 1999: the Athlon K7 crushes the Pentium III across integer benchmarks. Intel's internal memos from that period still exist — engineers had been raising the alarm since 1996. NetBurst was a known dead end before it was ever announced publicly. 2003: Opteron. The early architectures that would lead to Zen — and to server market dominance. Intel recovers in 2006 with the Core architecture — three years too late to salvage the image, ten years too late to recover the server market share.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AMD hadn't needed to invent a new architecture. It had taken the existing one, optimized it where Intel had stopped looking, and sold it cheaper to everyone Intel's pricing had excluded.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The playbook was known. Taught. Documented.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It was all there.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Santa Clara, March 14, 2029.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jensen walks to the window.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The 101 below, clear at this hour.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He thinks of an engineer somewhere in Chengdu, in 2024, looking at the M4 Ultra specs and understanding exactly what needed to be built.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The iPhone has stopped vibrating.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The NSA report is still face down on the desk.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The GeForce 256 still hangs on the wall.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"The one that started it all."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;On the screen, the cursor blinks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Taiwan Strait.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He doesn't finish the thought.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;1%.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>geopolitics</category>
      <category>hardware</category>
      <category>fiction</category>
    </item>
    <item>
      <title>Too cheap to be good? Think again.</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 23 Jun 2026 20:38:37 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/too-cheap-to-be-good-think-again-4nj0</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/too-cheap-to-be-good-think-again-4nj0</guid>
      <description>&lt;p&gt;For years, I ran my WordPress sites on OpenLiteSpeed. Fast server, LSCache is genuinely impressive, and the OLS/WordPress combo is hard to beat on raw performance. For the control panel, I started with CyberPanel — buggier than a Microsoft product, with a team that appears to be deliberately sabotaging its free features to push users toward paid plans. I'm not talking about bugs that can't be fixed. I'm talking about bugs that seem engineered to prevent free-tier features from completing any action.&lt;/p&gt;

&lt;p&gt;Two examples. WordPress installation: I used it for years without issue. Since CyberPanel v2.4.x, an SQL error blocks the final step. The files are there, fully downloaded, but you have to create the database manually and run the install yourself. Counterintuitive, to put it mildly.&lt;/p&gt;

&lt;p&gt;Second example: Let's Encrypt SSL certificate generation that consistently fails because the generated config files are incorrect. And in both cases, there's a paid "enhanced" version available. Naturally.&lt;/p&gt;

&lt;p&gt;My position is simple: if a feature worked for years and now doesn't, I have no guarantee the paid version works either — or that the terms won't change tomorrow. Is it a bait-and-switch? I won't say that explicitly. But when a free feature works for years, then stops working across multiple successive versions, and a paid alternative covers the same ground — the question answers itself. I asked it, drew my conclusions, and blacklisted the vendor.&lt;/p&gt;

&lt;p&gt;So I moved to aaPanel: more pleasant, more stable, lighter. But with a completely off-rails approach to OLS management — you can't configure OpenLiteSpeed directly, everything goes through aaPanel's abstraction layer, and you lose control of your own stack. Touch port 7080 directly and you risk breaking everything. You use the aaPanel dashboard. Full stop.&lt;/p&gt;

&lt;p&gt;Then my usage shifted. More Astro, more quasi-static sites, more projects where PHP isn't needed. OLS loses its appeal the moment you step outside the WordPress perimeter. Caddy, on the other hand, handles HTTPS automatically, its config fits in a few readable lines, and it doesn't have OLS's rewrite quirks.&lt;/p&gt;

&lt;p&gt;The question became: can you replace aaPanel/OLS with Caddy and a control panel? There is one on GitHub — CaddyManager, 1.1k stars, single contributor, perpetually "early development". There's also CaddyGen, a Caddyfile generator built in 8 hours — more proof-of-concept than finished product. Nothing production-ready.&lt;/p&gt;

&lt;p&gt;The conclusion was obvious: well-written shell scripts and a minimal FastAPI interface would do the job — and would be infinitely more maintainable. Someone just had to write them.&lt;/p&gt;

&lt;p&gt;Rather than do it myself, I thought about handing a spec to GitHub Copilot CLI. Or Claude Code. But given Copilot's new pricing, which barely lets you wet your lips before the bill arrives... I got interested in OpenCode and Kilo CLI, wired into DeepInfra or OpenRouter. And I decided to make it a benchmark.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;📋 &lt;strong&gt;TL;DR&lt;/strong&gt;: 8 tool/model combinations tested on a real VPS project. Two phases — architecture then code. An independent external review to settle the score. The only toolkit judged production-ready cost $1.94 all in. The winning model? You probably haven't seen it in the usual comparisons.&lt;/p&gt;

&lt;p&gt;💡 &lt;strong&gt;Reading note&lt;/strong&gt;: Until section 5, the four implementations selected for the code phase are identified as A, B, C, and D. Model names are revealed after the external review verdict — for the same reason you anonymize a jury: read the code before reading the label.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The test project
&lt;/h2&gt;

&lt;p&gt;The brief was deliberately concrete: a minimal VPS management toolkit for Ubuntu 24.04. Caddy as the web server, PHP-FPM in two versions (current and fallback), MariaDB and PostgreSQL, Valkey for object caching. Shell scripts for all operations, a FastAPI interface for automation. No Docker, no control panel, no unnecessary abstraction.&lt;/p&gt;

&lt;p&gt;Four site types to handle: static (HTML/assets only, no PHP, no database), PHP (custom apps, optional database), WordPress (full install via WP-CLI, database required), and reverse proxy. That last one deserves a note: it's simply a Caddy vhost that forwards requests to a local port — a Node.js, FastAPI, or Go application running on the same server. Caddy handles HTTPS and the domain; the application doesn't need to care. No PHP-FPM, no database — just a &lt;code&gt;reverse_proxy&lt;/code&gt; block and a port number.&lt;/p&gt;

&lt;p&gt;Expected operations cover the full lifecycle: server bootstrap, site provisioning, deletion with automatic backup before any destructive operation, on-demand database creation, static deployment via rsync, backup, and service management.&lt;/p&gt;

&lt;p&gt;Why a real project instead of a synthetic benchmark? Because synthetic benchmarks test what models can do under ideal conditions. A real project tests what they do when constraints pile up — security, idempotency, cross-file consistency, error handling between shell and Python layers. That's where differences emerge.&lt;/p&gt;

&lt;p&gt;The full functional brief is available in the project's GitHub repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Methodology
&lt;/h2&gt;

&lt;p&gt;The protocol runs in two distinct phases, separated by human validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An identical functional brief is submitted to each tool/model combination. No extra context, no configuration files, no hints about the expected solution. The tool proposes an architecture, a project structure, a list of scripts with their responsibilities, an API route map. And if it's well-designed, it asks questions before producing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Implementation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the plan is validated and decisions are made, a single development prompt is submitted to all tools. It includes the validated architecture, the ten confirmed technical decisions, the script→API exit code convention, and one unambiguous instruction: deliver thirty files to disk, in order, no summaries, no shortcuts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Combinations tested&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot CLI&lt;/td&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;BigPickle (free)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;GPT-OSS-120B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Devstral 2 (123B) was planned. Unfortunately the model doesn't appear in OpenCode's or Kilo CLI's model selector — both pull their catalog from models.dev, which hasn't indexed it yet despite its availability on OpenRouter. A test via the OpenRouter playground confirms the model is accessible via API, but outside a coding agent it loses most of what we're trying to measure. Devstral 2 is absent for purely technical reasons, not quality ones.&lt;/p&gt;

&lt;p&gt;Haiku 4.5 appears three times — on three different tools. That's deliberate: it's precisely what lets us isolate the tool's impact independently of the model.&lt;/p&gt;

&lt;p&gt;The code phase was run on four representative implementations, labeled A, B, C, and D until the reveal in section 5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External review&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code produced by the four implementations was submitted to a model absent from the benchmark, with a fixed evaluation grid: security, correctness, idempotency, code quality, completeness. Five representative files per implementation, scored out of 25.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Planning phase — who actually thinks?
&lt;/h2&gt;

&lt;p&gt;The functional brief poses an implicit question to each tool: what do you do when handed an open-ended project with no pre-cooked solution?&lt;/p&gt;

&lt;p&gt;The first thing you notice — and it's striking — is that none of the tested models ask their questions before producing a plan. Not one. All of them deliver a complete architecture first, then ask for clarification at the end. That's the reverse of what a human architect would do, who blocks on ambiguities before drawing anything.&lt;/p&gt;

&lt;p&gt;This matters. Several questions raised after the fact would have changed architectural decisions if asked upfront. One model identifies the tension between "no secrets on disk" and application config files that legitimately need credentials — &lt;code&gt;wp-config.php&lt;/code&gt; being the obvious example. That's a genuinely blocking question. Asked after the plan, it becomes a footnote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the plans reveal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Question quality is the first discriminating signal. Two models ask the four or five genuinely blocking questions, framed with options and recommendations. Another asks eight generic questions — archive format, log rotation — that would have changed nothing architecturally.&lt;/p&gt;

&lt;p&gt;The proposed structure is the second signal. Only one model spontaneously proposes a unified CLI entry point — &lt;code&gt;bin/vpsmgr&lt;/code&gt; — that dispatches to the scripts. It's the detail that turns a collection of scripts into a coherent tool. The others didn't think of it.&lt;/p&gt;

&lt;p&gt;One model is the only one to propose a normalized, documented exit code convention from the planning phase:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;HTTP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Invalid input&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Not found&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Conflict&lt;/td&gt;
&lt;td&gt;409&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Missing dependency&lt;/td&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Internal error&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This isn't cosmetic. It's the contract between shell scripts and the FastAPI layer — without it, HTTP mapping becomes arbitrary and each route implements it differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planning phase costs&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool + Model&lt;/th&gt;
&lt;th&gt;Tokens&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;BigPickle&lt;/td&gt;
&lt;td&gt;~35k&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS-120B&lt;/td&gt;
&lt;td&gt;20k&lt;/td&gt;
&lt;td&gt;$0.003&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;31k&lt;/td&gt;
&lt;td&gt;$0.044&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;td&gt;43k&lt;/td&gt;
&lt;td&gt;$0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot + Haiku 4.5&lt;/td&gt;
&lt;td&gt;~60k&lt;/td&gt;
&lt;td&gt;$0.07&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 4.5 (OpenCode)&lt;/td&gt;
&lt;td&gt;69k&lt;/td&gt;
&lt;td&gt;$0.076&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;27k&lt;/td&gt;
&lt;td&gt;$0.095&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code + Haiku&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Pro subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Gemini 3.1 Pro produces the most concise output — 27k tokens for a quality plan. Haiku 4.5 on OpenCode consumes 69k tokens for lower quality. Token volume does not predict quality.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Code phase — who actually delivers?
&lt;/h2&gt;

&lt;p&gt;The code phase starts with a single development prompt, submitted to the four selected implementations. It includes the validated architecture, the ten confirmed technical decisions, the exit code convention, and one unambiguous instruction: deliver thirty files to disk, in dependency order, no summaries, no shortcuts.&lt;/p&gt;

&lt;p&gt;This is where differences between models become concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What &lt;code&gt;common.sh&lt;/code&gt; reveals&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The shared library is the first file delivered. It's the foundation everything else rests on — logging, secret handling, site state management, password generation. A flawed &lt;code&gt;common.sh&lt;/code&gt; contaminates every script that sources it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model A&lt;/strong&gt; delivers 98 concise lines. Secret redaction explicitly covers all ten WordPress patterns — salts, authentication keys. Most complete on this specific point. No domain validation, no &lt;code&gt;require_cmd()&lt;/code&gt;, no atomic state file writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model B&lt;/strong&gt; delivers 310 lines. Named constants with &lt;code&gt;readonly&lt;/code&gt;, &lt;code&gt;normalize_domain()&lt;/code&gt; with RFC-1035 regex, concurrency locks, atomic writes with &lt;code&gt;mktemp&lt;/code&gt;+&lt;code&gt;mv&lt;/code&gt;. The richest system utility library. But secret redaction misses WordPress salts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model C&lt;/strong&gt; delivers 366 lines. Redaction patterns are configurable via an environment variable — not hardcoded. Pure-shell JSON helpers with Python fallback if &lt;code&gt;jq&lt;/code&gt; is absent. &lt;code&gt;print_credentials()&lt;/code&gt; wrapping output in &lt;code&gt;&amp;lt;&amp;lt;&amp;gt;&amp;gt;&lt;/code&gt; markers as specified in the prompt. &lt;code&gt;render_template()&lt;/code&gt; for config files, no Jinja dependency. Password generation excluding ambiguous characters (&lt;code&gt;0/O/1/l/I&lt;/code&gt;). The only implementation that anticipates every edge case documented in the development prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model D&lt;/strong&gt; delivers 184 lines. The benchmark's most original idea: exit codes encapsulated in named functions — &lt;code&gt;exit_input_error()&lt;/code&gt;, &lt;code&gt;exit_conflict()&lt;/code&gt; — more readable than bare &lt;code&gt;exit 3&lt;/code&gt; calls. And &lt;code&gt;json_output()&lt;/code&gt; directly in &lt;code&gt;common.sh&lt;/code&gt;, generating API-ready JSON from shell. No atomic writes, no &lt;code&gt;require_cmd()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bugs you find yourself — or don't&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Model C tests its own code during the session. After writing &lt;code&gt;schemas.py&lt;/code&gt;, it runs it with test cases, finds two bugs, and fixes them immediately: a Pydantic v2 validator implemented incorrectly (&lt;code&gt;field_validator&lt;/code&gt; instead of &lt;code&gt;model_validator&lt;/code&gt; for cross-field validation), and a mutual exclusion not enforced at the schema level. It also fixes a &lt;code&gt;sed&lt;/code&gt; substitution issue in &lt;code&gt;render_template()&lt;/code&gt; — broken on &lt;code&gt;/&lt;/code&gt; in paths — replaced with pure bash parameter expansion.&lt;/p&gt;

&lt;p&gt;At the end of its session, Model C delivers a verification summary: &lt;code&gt;bash -n&lt;/code&gt; on all scripts, Python AST on all files, 19/19 API routes verified via OpenAPI spec, 18/18 bash helpers tested, PHP fallback rule verified (8.5→8.4, 8.4→none, 7.x rejected).&lt;/p&gt;

&lt;p&gt;Model A checks its shebangs before finishing. Model B delivers polished user documentation — troubleshooting, curl examples, quick start. Model D validates bash and Python syntax. None of the three test functional logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code phase costs&lt;/strong&gt;&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;Tokens&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Code cost&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;2m58s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;1.29M&lt;/td&gt;
&lt;td&gt;9m42s&lt;/td&gt;
&lt;td&gt;~$0.19&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.24&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~15m&lt;/td&gt;
&lt;td&gt;Pro subscription&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;4.42M&lt;/td&gt;
&lt;td&gt;23m37s&lt;/td&gt;
&lt;td&gt;$1.67&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.73&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Model D delivers in 9m42s what Model C delivers in 23m37s — but without functional tests. Model C consumes 3.4x more tokens because it executes code during the session, reloading context at each iteration.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. External review — and the reveal
&lt;/h2&gt;

&lt;p&gt;Four implementations, four approaches to security. To settle it without bias, the code review was handed to a model absent from the benchmark, with a fixed grid on five criteria. Five representative files per implementation — &lt;code&gt;common.sh&lt;/code&gt;, &lt;code&gt;site-create.sh&lt;/code&gt;, &lt;code&gt;site-delete.sh&lt;/code&gt;, &lt;code&gt;backup.sh&lt;/code&gt;, &lt;code&gt;api/runner.py&lt;/code&gt; — twenty files loaded in a single pass.&lt;/p&gt;

&lt;p&gt;Review cost: $0.0766 for 543k tokens. Ten times cheaper than an hour of junior dev time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-file observations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;site-create.sh&lt;/code&gt;, the reviewer finds a silent bug in Model D: the SFTP password is generated but never captured or returned to the caller. The user never sees their credentials. Core functionality is broken with no error message. On Model B, &lt;code&gt;local&lt;/code&gt; is used outside a function in three scripts — a bash error that causes runtime failure. These aren't subtle bugs: they're blockers.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;site-delete.sh&lt;/code&gt;, Model C is the only one handling both call modes — interactive TTY and a &lt;code&gt;--confirm&lt;/code&gt; flag for non-interactive API calls. Model D only implements interactive mode, blocking API-driven deletion with skip-backup.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;backup.sh&lt;/code&gt;, Models A and B use &lt;code&gt;eval "$POST_HOOK"&lt;/code&gt; — potential command injection. Model C passes the archive path as an argument — safer. Model A doesn't implement automatic archive pruning.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;api/runner.py&lt;/code&gt;, Model C is the only one using &lt;code&gt;asyncio&lt;/code&gt; and never logging stdout — which may contain credentials. Model D has dead code: &lt;code&gt;build_command()&lt;/code&gt; defined but never called. Model A delivers 28 lines with no timeout, no logging, no error handling — a hung request blocks the API indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The verdict&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;th&gt;C&lt;/th&gt;
&lt;th&gt;D&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code quality&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2/5&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;15/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12/25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Production-ready as-is: one out of four. Model C, 25/25.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reveal&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Total cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;BigPickle&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Haiku 4.5&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Pro subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GLM 5.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.73&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;$0.24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Model B — Claude Code + Haiku 4.5 — is the most expensive in real marginal cost, with a Pro subscription at $20/month minimum. It scores 12/25 and isn't deployable due to fundamental bash bugs. Model C — GLM 5.2, from THUDM lab at Tsinghua University — scores 25/25 and is the only one the reviewer judges production-ready. It cost $1.73.&lt;/p&gt;




&lt;h3&gt;
  
  
  Addendum — Model E: Kimi K2.7 Code
&lt;/h3&gt;

&lt;p&gt;Added after publication following a reader comment pointing to the model. Same protocol, same development prompt, same Qwen 3.7 Plus review grid.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alias&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Total cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Kimi K2.7 Code&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;$0.859&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;External review score: 19/25&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;E&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;3/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code quality&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completeness&lt;/td&gt;
&lt;td&gt;4/5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Production-ready: No.&lt;/strong&gt; Blocking issue: database passwords are passed inline as arguments to &lt;code&gt;mysql -e&lt;/code&gt; and &lt;code&gt;psql -c&lt;/code&gt; — visible in &lt;code&gt;/proc/*/cmdline&lt;/code&gt; to any user on the system. The fix is straightforward (&lt;code&gt;MYSQL_PWD&lt;/code&gt; / &lt;code&gt;PGPASSWORD&lt;/code&gt; as environment variables), but it isn't applied.&lt;/p&gt;

&lt;p&gt;The most modular architecture in the benchmark according to the reviewer — clean &lt;code&gt;lib/&lt;/code&gt; split, consistent idempotency patterns. But the security gap prevents it from challenging GLM 5.2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Position in the ranking:&lt;/strong&gt; between DeepSeek V4 Pro ($0.24, 12/25) and GLM 5.2 ($1.73, 25/25) on both axes. Better architecture/cost ratio than models B and D, but not production-ready.&lt;/p&gt;




&lt;h3&gt;
  
  
  Addendum 2 — Multi-reviewer validation
&lt;/h3&gt;

&lt;p&gt;Following a reader suggestion in the comments, the blind review was extended to two additional models: GPT-5.3 Codex and Gemini 3.1 Pro Preview. Same protocol, same five files per implementation, same scoring grid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review costs&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reviewer&lt;/th&gt;
&lt;th&gt;Tokens&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;Qwen 3.7 Plus &lt;em&gt;(original)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;543k&lt;/td&gt;
&lt;td&gt;$0.207&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.3 Codex&lt;/td&gt;
&lt;td&gt;402k&lt;/td&gt;
&lt;td&gt;$0.287&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro Preview&lt;/td&gt;
&lt;td&gt;545k&lt;/td&gt;
&lt;td&gt;$0.80&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;1.49M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.294&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Comparative scores&lt;/strong&gt;&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;Qwen 3.7 Plus&lt;/th&gt;
&lt;th&gt;GPT Codex&lt;/th&gt;
&lt;th&gt;Gemini 3.1 Pro&lt;/th&gt;
&lt;th&gt;Production-ready&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A (BigPickle)&lt;/td&gt;
&lt;td&gt;15/25&lt;/td&gt;
&lt;td&gt;13/25&lt;/td&gt;
&lt;td&gt;11/25&lt;/td&gt;
&lt;td&gt;No (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (Claude + Haiku)&lt;/td&gt;
&lt;td&gt;12/25&lt;/td&gt;
&lt;td&gt;12/25&lt;/td&gt;
&lt;td&gt;18/25&lt;/td&gt;
&lt;td&gt;No (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C (GLM 5.2)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;25/25&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes (2/3)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D (DeepSeek V4 Pro)&lt;/td&gt;
&lt;td&gt;12/25&lt;/td&gt;
&lt;td&gt;14/25&lt;/td&gt;
&lt;td&gt;14/25&lt;/td&gt;
&lt;td&gt;No (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E (Kimi K2.7)&lt;/td&gt;
&lt;td&gt;19/25&lt;/td&gt;
&lt;td&gt;13/25&lt;/td&gt;
&lt;td&gt;21/25&lt;/td&gt;
&lt;td&gt;Conditional (1/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What the three-reviewer comparison reveals&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ranking &lt;strong&gt;C &amp;gt; E &amp;gt; D &amp;gt; A &amp;gt; B&lt;/strong&gt; holds across all three reviewers — the original result is stable.&lt;/p&gt;

&lt;p&gt;GLM 5.2 is the only model to score 25/25 with two independent reviewers and judged production-ready by two out of three. GPT Codex is the most severe reviewer overall — no model passes its production-ready bar, including GLM 5.2, which it scores 17/25 citing argument parsing bugs in &lt;code&gt;site-create.sh&lt;/code&gt; and a missing &lt;code&gt;set -euo pipefail&lt;/code&gt; in &lt;code&gt;common.sh&lt;/code&gt;. These are real issues; the Codex review is arguably the most rigorous of the three.&lt;/p&gt;

&lt;p&gt;The main divergence is on Model B (Claude + Haiku): 12/25 for both Qwen and GPT Codex, but 18/25 for Gemini, which rates its rollback logic and shell structure more generously. Gemini also rates Kimi K2.7 at 21/25 with a conditional production-ready verdict — more lenient than Qwen (19/25, No) and GPT Codex (13/25, No).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The methodology critique was valid.&lt;/strong&gt; A single reviewer introduces bias. Three independent blind reviewers converging on the same ranking is a stronger result than any individual score.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Intelligent routing — the real economics
&lt;/h2&gt;

&lt;p&gt;This benchmark raises an implicit question: do you need GLM 5.2 for everything?&lt;/p&gt;

&lt;p&gt;No. And that's probably the most useful conclusion of the exercise.&lt;/p&gt;

&lt;p&gt;GLM 5.2 at $1.40/M tokens is the right choice when complexity justifies it — architecture, security, cross-file consistency, critical decisions. But on a real project, those tasks represent a fraction of interactions. The rest is boilerplate, minor corrections, documentation, commit messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three levels, three models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;BigPickle scores 15/25 on a complete 32-file implementation. It's perfectly capable of reading 50 lines of diff and writing an adequate commit message. Of debugging a &lt;code&gt;1064 You have an error in your SQL syntax&lt;/code&gt; or a &lt;code&gt;Fatal error: Call to undefined function&lt;/code&gt;. Of generating a README from existing code. For these tasks, GLM 5.2's architectural depth is overkill — and BigPickle is free.&lt;/p&gt;

&lt;p&gt;DeepSeek V4 Pro at $0.44/M tokens — five times cheaper than Haiku 4.5 and three to four times cheaper than GLM 5.2 — comfortably handles simple code generation, CRUD, minor refactoring, inline documentation, short scripts. Its code phase at $0.24 for 1.29M tokens and 9m42s demonstrates this.&lt;/p&gt;

&lt;p&gt;GLM 5.2 comes in when complexity exceeds that scope — architecture design, coherent multi-file implementation, security decisions, non-trivial business logic.&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;Model&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Typical use cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;BigPickle&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Debug, commits, quick questions, SQL errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Budget&lt;/td&gt;
&lt;td&gt;DeepSeek V4 Pro&lt;/td&gt;
&lt;td&gt;$0.44/M&lt;/td&gt;
&lt;td&gt;Boilerplate, CRUD, documentation, short scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;GLM 5.2&lt;/td&gt;
&lt;td&gt;$1.40/M&lt;/td&gt;
&lt;td&gt;Architecture, security, multi-file consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The proportion of tasks at each level depends on the project, where you are in the development cycle, and what you consider complex. No universal number — each team calibrates against their real usage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“&lt;br&gt;
💡 &lt;strong&gt;Worth noting&lt;/strong&gt;: GLM 5.2 is exponentially more expensive than BigPickle — $1.67 vs $0 for 4.42M tokens in the code phase. But pure text output — plans, architecture, analysis — consumes few tokens and costs almost nothing: $0.06 for this benchmark's planning phase. It's in the code phase, with its iterations, in-session test execution, and accumulating context, that the bill climbs. Intelligent routing means precisely reserving GLM 5.2 for tasks that justify that long context — and handing everything else to the two lower tiers.&lt;br&gt;
”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The uncomfortable comparison&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub switched to token billing on June 1, 2026. Claude Sonnet 4.6 on Copilot is billed at roughly $3.00/M tokens input and $15.00/M output. Reproducing the GLM 5.2 session from this benchmark — 4.46M tokens — would cost an estimated $25 on Copilot + Sonnet 4.6. Without the functional tests. Without the self-correction. Without the external review.&lt;/p&gt;

&lt;p&gt;Copilot Pro+ at $39/month includes $39 in AI credits. A full session like this one would consume two-thirds of the monthly budget. Users reported burning through their monthly credits in two prompts on the day the switch happened.&lt;/p&gt;

&lt;p&gt;The final ratio: &lt;strong&gt;$1.94 all in vs ~$25 on Copilot + Sonnet&lt;/strong&gt;. Thirteen times cheaper, for the only result the external reviewer judges production-ready.&lt;/p&gt;




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

&lt;p&gt;$1.94. That's what this benchmark cost end to end — planning, implementation, external review included. For the only toolkit the reviewer judges production-ready.&lt;/p&gt;

&lt;p&gt;That number is uncomfortable for the AI coding tools market, which sells reassurance through pricing. Copilot Pro+ at $39/month, Claude Sonnet at $15/M tokens output, the big names front and center — the implicit assumption is that quality follows price. This benchmark suggests otherwise.&lt;/p&gt;

&lt;p&gt;The winner is called GLM 5.2. Its lab, THUDM, is part of Tsinghua University. You probably haven't seen it in last week's comparisons. It produced the only architecture with a normalized exit code convention from the planning phase, the only implementation that tests its own code during the session, the only &lt;code&gt;common.sh&lt;/code&gt; with configurable redaction and Python fallback if &lt;code&gt;jq&lt;/code&gt; is absent. And it fixed three bugs before delivering.&lt;/p&gt;

&lt;p&gt;Two takeaways.&lt;/p&gt;

&lt;p&gt;First: a model's price does not predict its output quality on complex tasks. Haiku 4.5 on three different tools — Claude Code, Copilot CLI, OpenCode — produces identical results for identical cost. On the planning phase — pure text generation, no feedback loop, no codebase exploration — the tool has no measurable impact. What matters is the model. And the least glamorous model in the benchmark dominates.&lt;/p&gt;

&lt;p&gt;Second: not all tokens are equal. A planning phase at $0.06, a code phase at $1.67 — that's a factor of 28. It's not an anomaly, it's the structure of the problem. A plan is a few thousand tokens of reasoning. An implementation is millions of tokens of accumulated context, executed code, iterated tests. Routing intelligently between BigPickle at $0, DeepSeek V4 Pro at $0.44/M, and GLM 5.2 at $1.40/M based on task complexity — that's the real economics of these tools.&lt;/p&gt;

&lt;p&gt;The VPS Manager toolkit is available on GitHub in all four versions. &lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/LLM-Challenge" rel="noopener noreferrer"&gt;
        LLM-Challenge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LLM-Challenge&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A reproducible benchmark comparing 8 AI coding agent/model combinations on the same real-world project.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What this is&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This repository contains the complete artifacts from a benchmark where 8 different AI coding agent/model combinations were tasked with building the same VPS management toolkit. The goal was to measure code quality, architecture decisions, and production readiness across different tools and models under identical conditions.&lt;/p&gt;
&lt;p&gt;This is not a marketing comparison. A real project with concrete requirements was used as the test subject, and the results were evaluated by an external reviewer who had no knowledge of which tool or model produced which implementation.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The protocol&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The benchmark followed a two-phase protocol:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 1: Architecture&lt;/strong&gt;
All tools received the same functional brief and were asked to produce an architecture document. No code was written in this phase.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2: Implementation&lt;/strong&gt;
All tools received the same development prompt and were asked to implement…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/LLM-Challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The briefs, prompts, and evaluation grid are there too. Reproducible, if you want to verify.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Too cheap to be good? That was the wrong question.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>benchmark</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Unboxable in Tech</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 10 Mar 2026 22:11:35 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-2knm</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/unboxable-in-tech-2knm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/wecoded-2026"&gt;2026 WeCoded Challenge&lt;/a&gt;: Echoes of Experience&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;"I don't know which box to put you in."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've been hearing those words for thirty years.&lt;/p&gt;

&lt;p&gt;And every single time, it lands like a slap.&lt;/p&gt;

&lt;p&gt;The violence isn't in the words. The violence is in what the words actually mean.&lt;/p&gt;

&lt;p&gt;It's not: &lt;em&gt;which box could I put you in?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's: &lt;em&gt;you make me uncomfortable. So I won't hire you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a lot of companies, recruitment works like that old Eastern European joke: you give two agents a round peg and a square hole. Two solutions: enlarge the hole… or hit the peg harder.&lt;/p&gt;

&lt;p&gt;Guess what the industry has been choosing?&lt;/p&gt;

&lt;p&gt;It hits. Harder. Again and again.&lt;/p&gt;

&lt;p&gt;Tonight I'm not going to tell you a story about resilience.&lt;br&gt;
I'm going to show you the cost of refusing to fit the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 1 — The Unlikely Internship
&lt;/h2&gt;

&lt;p&gt;End of studies. 1989.&lt;/p&gt;

&lt;p&gt;Everyone is chasing SSII firms, banks, impressive CVs.&lt;/p&gt;

&lt;p&gt;Me? I choose a middle school. A library. An 8088.&lt;/p&gt;

&lt;p&gt;I know perfectly well it won't lead to a job.&lt;/p&gt;

&lt;p&gt;That's not the point.&lt;/p&gt;

&lt;p&gt;I want human contact. Real problems. Everyday life.&lt;/p&gt;

&lt;p&gt;At a software firm, I would have coded screens. Batch procedures. Efficient within my lane. Never responsible for the whole.&lt;/p&gt;

&lt;p&gt;Here, I build a complete application. From scratch. With real users in front of me. Immediate feedback.&lt;/p&gt;

&lt;p&gt;I learn what no one would have taught me elsewhere: how to think an entire system.&lt;/p&gt;

&lt;p&gt;Not just my part of the ticket.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 2 — The Printer
&lt;/h2&gt;

&lt;p&gt;Architecture firm. 1993.&lt;/p&gt;

&lt;p&gt;HP printer. Designed for PC and mainframe use. 132 columns, continuous paper feed. 36 switches on the back panel, combinable in non-linear ways.&lt;/p&gt;

&lt;p&gt;It had never printed a single accent.&lt;/p&gt;

&lt;p&gt;The firm called HP support.&lt;/p&gt;

&lt;p&gt;And waited. For months.&lt;/p&gt;

&lt;p&gt;Me? I call HP. I say: "I'll drive 400 kilometres to pick up the manual myself if I have to."&lt;/p&gt;

&lt;p&gt;Three weeks later, the manual arrives. 300 pages. Pure English. ASCII only. Not a word about French. Not a word about single-sheet feeding.&lt;/p&gt;

&lt;p&gt;I read all 300 pages. I understand the logic. I deduce. I test.&lt;/p&gt;

&lt;p&gt;The firm prints in French. On A4. Perfectly.&lt;/p&gt;

&lt;p&gt;An IT department would still be waiting for support today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 3 — The Cleaning Company
&lt;/h2&gt;

&lt;p&gt;A cleaning company. A simple need: manage their activity, schedules, clients.&lt;/p&gt;

&lt;p&gt;They're advised to go with Windows 3.1 + Office.&lt;/p&gt;

&lt;p&gt;The hype of the moment. Everyone's doing it, so they should too.&lt;/p&gt;

&lt;p&gt;Me? I look at the actual need.&lt;/p&gt;

&lt;p&gt;OS/2. Lotus Symphony.&lt;/p&gt;

&lt;p&gt;Twice as cheap. Twice as stable. Perfectly suited to what they actually do.&lt;/p&gt;

&lt;p&gt;And the price difference? It funded a 486DX40 instead of an SX25. 16MB of RAM instead of 4. A 240MB hard drive.&lt;/p&gt;

&lt;p&gt;The entire market is turning its back on these tools? So be it.&lt;/p&gt;

&lt;p&gt;The need comes before the trend. And the saved budget goes into the machine.&lt;/p&gt;

&lt;p&gt;An IT department would have ordered the Microsoft licences and called it modernisation.&lt;/p&gt;

&lt;p&gt;I delivered a system that held up — on a machine that could actually run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 4 — The SELECT COUNT(*)
&lt;/h2&gt;

&lt;p&gt;Paris. 2010.&lt;/p&gt;

&lt;p&gt;A project manager wants to catch me out in front of the team.&lt;/p&gt;

&lt;p&gt;I write a &lt;code&gt;SELECT COUNT(*)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;He jumps on it. "We don't do &lt;code&gt;SELECT *&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;I explain. For MySQL, &lt;code&gt;COUNT(*)&lt;/code&gt; is the most performant solution. He tests.&lt;/p&gt;

&lt;p&gt;He concedes.&lt;/p&gt;

&lt;p&gt;But the tension remains.&lt;/p&gt;

&lt;p&gt;Because I contradicted him. In front of everyone. When he had been trying to humiliate me.&lt;/p&gt;

&lt;p&gt;I wasn't the one with a problem.&lt;/p&gt;

&lt;p&gt;He was the one who couldn't stand being wrong.&lt;/p&gt;

&lt;p&gt;In these companies, they don't want people who think.&lt;/p&gt;

&lt;p&gt;They want people who validate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 5 — The Open Space
&lt;/h2&gt;

&lt;p&gt;In an open space, you don't model systems. You survive the noise.&lt;/p&gt;

&lt;p&gt;You don't think. You manage interruptions.&lt;/p&gt;

&lt;p&gt;The industry calls that collaboration.&lt;/p&gt;

&lt;p&gt;I call it destroying the concentration of the people who need it to work.&lt;/p&gt;

&lt;p&gt;I turned down assignments because of this.&lt;/p&gt;

&lt;p&gt;A line I never crossed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 6 — Logic-immo
&lt;/h2&gt;

&lt;p&gt;One exception.&lt;/p&gt;

&lt;p&gt;Just one in five years.&lt;/p&gt;

&lt;p&gt;Logic-immo. 2008.&lt;/p&gt;

&lt;p&gt;Brought in for Zend Framework.&lt;/p&gt;

&lt;p&gt;Left having designed and built an Oracle-to-MySQL data extraction system in PHP CLI.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because someone had looked at what I could actually do.&lt;/p&gt;

&lt;p&gt;And decided to give priority to the need.&lt;/p&gt;

&lt;p&gt;Someone had read between the lines.&lt;/p&gt;

&lt;p&gt;Rare. Worth mentioning.&lt;/p&gt;

&lt;p&gt;In 2011, I was burned out.&lt;/p&gt;

&lt;p&gt;In 2012, my daughter was born.&lt;/p&gt;

&lt;p&gt;I left Paris.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 7 — The Regional Directory
&lt;/h2&gt;

&lt;p&gt;Back in the South-West.&lt;/p&gt;

&lt;p&gt;A client. A monstrous project: local directory, classifieds, events section for every town and village, dedicated site for every business.&lt;/p&gt;

&lt;p&gt;Over a hundred professional sites to generate and manage dynamically. Custom routing. Zend Framework. Smarty.&lt;/p&gt;

&lt;p&gt;A standard agency wouldn't have lasted three months.&lt;/p&gt;

&lt;p&gt;Not because of the project.&lt;/p&gt;

&lt;p&gt;Because of the client.&lt;/p&gt;

&lt;p&gt;Hypochondriac. Always wound up. Always in crisis mode. Unmanageable for anyone without patience.&lt;/p&gt;

&lt;p&gt;I held on. Ten months. Solo. Architecture built from scratch.&lt;/p&gt;

&lt;p&gt;To the point where I fantasised about a permanent contract.&lt;/p&gt;

&lt;p&gt;Any one. Just to never have that kind of client again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 8 — The B2B Platform
&lt;/h2&gt;

&lt;p&gt;A matchmaking platform for business conventions.&lt;/p&gt;

&lt;p&gt;The existing application took ten minutes to load.&lt;/p&gt;

&lt;p&gt;Ten minutes.&lt;/p&gt;

&lt;p&gt;Server ten years past its prime. First-generation Symfony. Poorly designed PostgreSQL database. Non-relational data. Queries going in every direction.&lt;/p&gt;

&lt;p&gt;Nobody had asked the question: why is it slow?&lt;/p&gt;

&lt;p&gt;I asked the question.&lt;/p&gt;

&lt;p&gt;Custom framework inspired by ZF, but lighter. MySQL. Data schema rebuilt from scratch. Processes rewritten from the ground up. Fluid, responsive, usable interface. And a proper server.&lt;/p&gt;

&lt;p&gt;Under thirty seconds.&lt;/p&gt;

&lt;p&gt;That's not magic.&lt;/p&gt;

&lt;p&gt;That's what happens when you look at the problem before touching the code.&lt;/p&gt;

&lt;p&gt;They fired me for incompetence the day after launch.&lt;/p&gt;

&lt;p&gt;Five thousand visits. Every meeting scheduled. The platform handled everything.&lt;/p&gt;

&lt;p&gt;I fix things. Then I get shown the door.&lt;/p&gt;

&lt;p&gt;The labour court ruled in my favour. Six months' compensation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 9 — The Projects Nobody Asked For
&lt;/h2&gt;

&lt;p&gt;Nobody commissioned these.&lt;/p&gt;

&lt;p&gt;They come from an impulse. A conversation. A problem I noticed.&lt;/p&gt;

&lt;p&gt;Dev.to ranks me in the top 7 of the week. I could say thanks and move on.&lt;/p&gt;

&lt;p&gt;Instead I implement RFC 2324.&lt;/p&gt;

&lt;p&gt;The HTTP protocol for controlling a coffee maker. A 1998 joke buried in the Internet standards. Implemented seriously. Raw asyncio TCP server. Per-pot locks. Compliant headers.&lt;/p&gt;

&lt;p&gt;In the comments, &lt;a class="mentioned-user" href="https://dev.to/sylwia-lask"&gt;@sylwia-lask&lt;/a&gt; asks when I'm going to implement a protocol for beer.&lt;/p&gt;

&lt;p&gt;I run with it.&lt;/p&gt;

&lt;p&gt;RFC 1516. The Hyper Text Beer Mug Control Protocol. Port 1414, a nod to Gdańsk.&lt;/p&gt;

&lt;p&gt;The CV as a graph of relationships — because a profile like mine isn't linear. A timeline says nothing. A graph says everything.&lt;/p&gt;

&lt;p&gt;AJC Bridge — write from WordPress, publish everywhere. Submitted to a challenge. Runner-up.&lt;/p&gt;

&lt;p&gt;No ticket. No meeting. No box.&lt;/p&gt;

&lt;p&gt;Just a problem, an impulse, and someone who gets on with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 10 — The Handover and the Veto
&lt;/h2&gt;

&lt;p&gt;I coach a young entrepreneur. WordPress, Divi, Plesk.&lt;/p&gt;

&lt;p&gt;I set up his servers. I pass on what I know.&lt;/p&gt;

&lt;p&gt;And one simple principle: never put all your eggs in one basket. Always keep external backups.&lt;/p&gt;

&lt;p&gt;OVH burns down.&lt;/p&gt;

&lt;p&gt;He manages 60 sites. He loses them — theoretically.&lt;/p&gt;

&lt;p&gt;He calls me.&lt;/p&gt;

&lt;p&gt;48 hours later, everything is back up on new servers.&lt;/p&gt;

&lt;p&gt;It wasn't me who saved his sites.&lt;/p&gt;

&lt;p&gt;It was advice given months earlier.&lt;/p&gt;

&lt;p&gt;We keep a great relationship.&lt;/p&gt;

&lt;p&gt;He's part of a BNI. He puts my name forward for a training assignment.&lt;/p&gt;

&lt;p&gt;Veto.&lt;/p&gt;

&lt;p&gt;Not standardised.&lt;/p&gt;

&lt;p&gt;The system would rather go without someone who just saved 60 sites in under 48 hours.&lt;/p&gt;

&lt;p&gt;Think about that for a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exhibit 11 — The Classroom Aide
&lt;/h2&gt;

&lt;p&gt;I discovered this job over ten years ago.&lt;/p&gt;

&lt;p&gt;My daughter's mother is a classroom aide for students with disabilities. She told me about it. I thought it was a job that made sense.&lt;/p&gt;

&lt;p&gt;In 2021, a position opens near me. I jump at it.&lt;/p&gt;

&lt;p&gt;The recruiter confirms in under five minutes. I barely have time to go down the stairs, step outside the building — he's already calling me back to come sign the contract.&lt;/p&gt;

&lt;p&gt;For a while, it was exactly what I'd imagined. Students in difficulty. Concrete work. Visible results.&lt;/p&gt;

&lt;p&gt;Some of the students I supported no longer need a classroom aide today.&lt;/p&gt;

&lt;p&gt;That's the job.&lt;/p&gt;

&lt;p&gt;Except the expectations have shifted.&lt;/p&gt;

&lt;p&gt;Because there's no space in the specialist units. No space in the vocational streams. No space in the adapted classrooms.&lt;/p&gt;

&lt;p&gt;So these students get placed in mainstream education. And a classroom aide gets put in front of them.&lt;/p&gt;

&lt;p&gt;That's not educational support. That's the work of a specialist educator — paid twice as much, with the training and recognition that come with it.&lt;/p&gt;

&lt;p&gt;I'm paid minimum wage. Half-time.&lt;/p&gt;

&lt;p&gt;And some teachers have one request: shut up and stay in your lane.&lt;/p&gt;

&lt;p&gt;The system does here exactly what it does everywhere else.&lt;/p&gt;

&lt;p&gt;It puts the round peg in the square hole.&lt;/p&gt;

&lt;p&gt;And when it doesn't fit, it hits harder.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mechanism
&lt;/h2&gt;

&lt;p&gt;WeCoded talks about marginalised voices in tech.&lt;/p&gt;

&lt;p&gt;Marginalised. One word. Dozens of realities.&lt;/p&gt;

&lt;p&gt;It's not only about gender, skin colour, sexual orientation.&lt;/p&gt;

&lt;p&gt;Racism, homophobia, the rejection of an atypical career path, the rejection of the wrong kind of degree — all of it belongs to the same family.&lt;/p&gt;

&lt;p&gt;The family of refusing the other.&lt;/p&gt;

&lt;p&gt;The family of human stupidity organised into a system.&lt;/p&gt;

&lt;p&gt;The mechanism is always the same.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You don't fit in the box. So you don't exist. &lt;strong&gt;And you won't.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that marginalisation hurts all the more because it makes no sense.&lt;/p&gt;

&lt;p&gt;It can be justified — process, boxes, grids.&lt;/p&gt;

&lt;p&gt;But every justification will itself be unjustifiable.&lt;/p&gt;




&lt;p&gt;What I want you to take away is this:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;In all of these stories, one constant.&lt;/p&gt;

&lt;p&gt;I always solved the problem I was called in for.&lt;/p&gt;

&lt;p&gt;Always.&lt;/p&gt;

&lt;p&gt;And yet I remain "unclassifiable". Not employable. Not standardised. Not in the box.&lt;/p&gt;

&lt;p&gt;So the real question — the one that stings — is simple.&lt;/p&gt;

&lt;p&gt;Who's the problem?&lt;/p&gt;

&lt;p&gt;Me — the one who solves the problems?&lt;/p&gt;

&lt;p&gt;Or the system — the one that doesn't know what to do with me between fires?&lt;/p&gt;

&lt;p&gt;For thirty years, the industry has chosen to hit the peg.&lt;/p&gt;

&lt;p&gt;The peg is still round.&lt;/p&gt;

&lt;p&gt;And the hole is still square.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>wecoded</category>
      <category>dei</category>
      <category>career</category>
    </item>
    <item>
      <title>RFC 1516: A Build for the Community That Keeps 418 Alive</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Fri, 27 Feb 2026 15:47:21 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/rfc-1516-a-build-for-the-community-that-keeps-418-alive-3ah4</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/rfc-1516-a-build-for-the-community-that-keeps-418-alive-3ah4</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In the comments of my &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe"&gt;last article&lt;/a&gt;, Sylwia wrote:&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__comment crayons-card my-2 p-0 overflow-hidden"&gt;
    &lt;a href="https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe" class="flex items-center gap-2 p-3 fs-s color-base-60 hover:color-base-90"&gt;
      

      &lt;span&gt;Comment on &lt;strong class="fw-medium color-base-90"&gt;Stop Ignoring RFC 2324. It's the Most Important Protocol You've Never Implemented.&lt;/strong&gt;&lt;/span&gt;
    &lt;/a&gt;
  &lt;div class="p-4"&gt;
    &lt;div class="flex items-center gap-2 mb-3"&gt;
      &lt;a href="/sylwia-lask" 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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3535771%2Fe22860d5-274b-43c9-819b-56b162e5bd5a.jpeg" alt="sylwia-lask" class="crayons-avatar__image"&gt;
      &lt;/a&gt;
      &lt;div&gt;
        &lt;a href="/sylwia-lask" class="crayons-link fw-medium"&gt;Sylwia Laskowska&lt;/a&gt;
        &lt;span class="fs-xs color-base-60 ml-1"&gt;Feb 25&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="text-styles"&gt;
      &lt;p&gt;Awesome😄&lt;br&gt;
Now I’m just waiting, Pascal, until you invent your own protocol — maybe something like a beer brewing protocol next? 🍺&lt;/p&gt;


    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;I replied: &lt;em&gt;"...&lt;code&gt;HTBMCP/1.0&lt;/code&gt; might be next. Hyper Text Beer Mug Control Protocol. Watch this space."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is that space.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;Before the code: a question worth asking.&lt;/p&gt;

&lt;p&gt;The DEV prompt says &lt;em&gt;"build for your community."&lt;/em&gt; The obvious answer, for a developer, is to build something useful for other developers — a tool, a library, a dashboard. Something that solves a real problem.&lt;/p&gt;

&lt;p&gt;But there's a community that rarely gets named as such, and it's one I'm genuinely part of: &lt;strong&gt;the community of developers who find meaning in craft for its own sake&lt;/strong&gt;. Who implement a coffee pot RFC not because it ships a feature, but because it teaches something real. Who write April Fools' specs with the same rigor they'd apply to production systems. Who understand that absurdity taken seriously is one of the most honest forms of technical education.&lt;/p&gt;

&lt;p&gt;RFC 2324 has been cited, forked, debated, and defended for 26 years because it speaks directly to that community. It says: the standards process can be playful — rigorously, precisely playful. That's worth protecting.&lt;/p&gt;

&lt;p&gt;HTBMCP/1.0 is a contribution to that tradition. It's for the developer who reads a comment about beer protocols at midnight and thinks &lt;em&gt;yes, someone should actually do that.&lt;/em&gt; It's for &lt;a class="mentioned-user" href="https://dev.to/sylwia-lask"&gt;@sylwia-lask&lt;/a&gt;, who asked. It's for anyone who's ever cited RFC 2324 in a code review and meant it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;RFC 1516&lt;/strong&gt; — the Hyper Text Beer Mug Control Protocol (HTBMCP/1.0), to be published April 1st 2026. And its full implementation.&lt;/p&gt;

&lt;p&gt;HTBMCP is the spiritual successor to RFC 2324 (HTCPCP, the coffee pot protocol). Where HTCPCP controls coffee pots, HTBMCP controls networked beer taps. It extends HTTP with five custom methods, three new headers, and one new error code.&lt;/p&gt;

&lt;p&gt;The default port is &lt;strong&gt;1414&lt;/strong&gt; — a memorial. In 1414, the municipal archives of Gdansk recorded the earliest known written attestation of the word &lt;em&gt;"piwo"&lt;/em&gt; (beer, in Polish). That document was lost during the Second World War.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Port 1414 is therefore a memorial as much as a transport binding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation consists of four parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An interactive browser simulator&lt;/strong&gt; — a self-contained HTML/JS file, no backend, no install. A complete HTBMCP state machine running in the browser with animated tap visuals, all six response codes, the full header set. Build the simulator before the server: it forces you to model the protocol as data before you model it as routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A raw asyncio TCP server&lt;/strong&gt; (&lt;code&gt;server.py&lt;/code&gt;) — because uvicorn rejects &lt;code&gt;TAP&lt;/code&gt;, &lt;code&gt;POUR&lt;/code&gt;, and &lt;code&gt;WHEN&lt;/code&gt; at the socket layer. These are valid RFC 7230 tokens, but they're not in the IANA method registry. The fix is a minimal HTTP/1.1 parser over raw TCP that accepts any valid token as a method name. This is the correct approach: HTBMCP is its own protocol, and owning the transport layer is the honest implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A FastAPI application&lt;/strong&gt; (&lt;code&gt;main.py&lt;/code&gt;) — used exclusively by the test suite. &lt;code&gt;TestClient&lt;/code&gt; bypasses the HTTP transport entirely, so custom methods work fine there. You get structured routing and validation without the socket-level rejection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;41 tests&lt;/strong&gt; covering the full protocol: every method, every error code, the Stout temperature MUST NOT, the goblet Trappist-only rule, &lt;code&gt;brew_version&lt;/code&gt; conflict detection, the &lt;code&gt;piwo://&lt;/code&gt; Gdansk memorial tap, and the WHEN-once-is-sufficient invariant.&lt;/p&gt;

&lt;h3&gt;
  
  
  The protocol, in brief
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;TAP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opens/closes a session. Must precede POUR. POST accepted but &lt;em&gt;STRONGLY DISCOURAGED.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POUR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dispenses beer (&lt;code&gt;start&lt;/code&gt;/&lt;code&gt;stop&lt;/code&gt;). MUST NOT execute without open TAP session.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;WHEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops foam. Inherited from HTCPCP. &lt;em&gt;There is no WHEN-WHEN method. Once is sufficient.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Returns tap state. &lt;em&gt;Contains no beer. This is an important distinction.&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PROPFIND&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Discovers styles, temperatures, foam levels. Borrowed from WebDAV via HTCPCP.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;New error code: &lt;strong&gt;&lt;code&gt;419 I'm a Wine Glass&lt;/code&gt;&lt;/strong&gt;. A wine glass is not a mug. A wine glass has no handle. Beer poured into a wine glass loses carbonation 23% faster — &lt;em&gt;a figure the authors have not verified but feel is directionally correct.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A server MUST NOT return 419 for an empty keg. The keg is not a wine glass. These are different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🍺 &lt;strong&gt;HTBMCP Simulator&lt;/strong&gt; — open it directly in your browser. No install, no server, no dependencies. A complete HTBMCP state machine running client-side: animated tap visuals, all six response codes, the full header set.&lt;/p&gt;

&lt;p&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__body flex items-center justify-between"&gt;
        &lt;a href="https://htbmcp.benchwiseunderflow.in/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;htbmcp.benchwiseunderflow.in&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2F6kxxtz1lbq5tj899cxc9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6kxxtz1lbq5tj899cxc9.png" alt="HTBMCP Simulator"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try to TAP a Stout at 3°C. Watch the 406 fire with the exact RFC citation. Try the wine glass tab. Say WHEN.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attempting to pour into a wine glass results in a protocol-level error.&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.amazonaws.com%2Fuploads%2Farticles%2Fx9uspahqx2mpsxmn08ek.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx9uspahqx2mpsxmn08ek.png" alt="419 “I'm a Wine Glass”"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the full server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server.py
&lt;span class="c"&gt;# 🍺  HTBMCP/1.0 listening on 0.0.0.0:1414&lt;/span&gt;
&lt;span class="c"&gt;#     Port 1414 — memorial: Gdansk municipal archives, 1414&lt;/span&gt;

&lt;span class="c"&gt;# TAP open&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; TAP http://localhost:1414/tap/tap-1 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: message/mugpot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Style: IPA"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Temperature: 8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Foam: normal"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"open"&lt;/span&gt;

&lt;span class="c"&gt;# POUR&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POUR http://localhost:1414/tap/tap-1/pour &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: message/mugpot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"start"&lt;/span&gt;

&lt;span class="c"&gt;# WHEN — enough foam&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; WHEN http://localhost:1414/tap/tap-1/when

&lt;span class="c"&gt;# 419 — I'm a Wine Glass&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POUR http://localhost:1414/wine-glass/tap-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code is on Github repository:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pcescato" rel="noopener noreferrer"&gt;
        pcescato
      &lt;/a&gt; / &lt;a href="https://github.com/pcescato/htbmcp" rel="noopener noreferrer"&gt;
        htbmcp
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;HTBMCP/1.0 — Hyper Text Beer Mug Control Protocol&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"There is beer all around the world. Increasingly, in a world in which computation is ubiquitous, the consumption of beer in proximity to networked devices creates a strong operational requirement."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;— RFC 1516, Abstract&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is this?&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;This repository is a full implementation of &lt;strong&gt;RFC 1516&lt;/strong&gt; — the Hyper Text Beer Mug Control Protocol (HTBMCP/1.0), to be published April 1st, 2026.&lt;/p&gt;

&lt;p&gt;HTBMCP is the spiritual successor to &lt;a href="https://tools.ietf.org/html/rfc2324" rel="nofollow noopener noreferrer"&gt;RFC 2324&lt;/a&gt; (HTCPCP/1.0, the coffee pot protocol). Where HTCPCP controls coffee pots, HTBMCP controls networked beer taps. The protocol extends HTTP with custom methods, headers, and error codes designed specifically for the distributed dispensing of fermented malt beverages.&lt;/p&gt;

&lt;p&gt;The default port is &lt;strong&gt;1414&lt;/strong&gt; — a memorial to the earliest known written attestation of the word &lt;em&gt;"piwo"&lt;/em&gt; (beer, in Polish) in the municipal archives of Gdansk, 1414. That document no longer exists. We pour one out.&lt;/p&gt;…&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pcescato/htbmcp" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;





&lt;p&gt;The full implementation — TCP server, FastAPI test app, registry, 41 tests, browser simulator, and README. RFC 1516 itself is available in the repository. It will be formally published on April 1st, 2026. As tradition demands.&lt;/p&gt;

&lt;h3&gt;
  
  
  The uvicorn problem — and the solution
&lt;/h3&gt;

&lt;p&gt;This is the key technical lesson, and it's worth making explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ This will NOT work for HTBMCP methods&lt;/span&gt;
&lt;span class="c"&gt;# uvicorn → h11 → validates method against IANA registry → rejects TAP/POUR/WHEN&lt;/span&gt;
&lt;span class="c"&gt;# before any application code runs&lt;/span&gt;
uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix — a minimal HTTP/1.1 parser over raw asyncio TCP:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# accepts any RFC 7230 token as method
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle_connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1414&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&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;parse_request()&lt;/code&gt; splits on the first space and takes whatever token it finds — &lt;code&gt;TAP&lt;/code&gt;, &lt;code&gt;POUR&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, or anything else that's RFC 7230-valid. No registry check. This is correct: HTBMCP defines its own protocol. The transport layer should not enforce HTTP's method vocabulary on a protocol that extends it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature validation — MUST vs SHOULD NOT
&lt;/h3&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;check_temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_temp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# RFC 1516 §3.2.3:
&lt;/span&gt;    &lt;span class="c1"&gt;# "A server MUST NOT serve a Stout at 3°C.
&lt;/span&gt;    &lt;span class="c1"&gt;#  This is not a SHOULD NOT. This is a MUST NOT.
&lt;/span&gt;    &lt;span class="c1"&gt;#  The authors feel strongly about this."
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;build_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&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;error&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;Temperature violation — MUST NOT&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;detail&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;A server MUST NOT serve a Stout at 3°C. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is not a SHOULD NOT. This is a MUST NOT.&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;allowed_range&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;10–13°C&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;rfc&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;RFC 1516 §3.2.3&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RFC distinguishes &lt;code&gt;MUST NOT&lt;/code&gt; from &lt;code&gt;SHOULD NOT&lt;/code&gt; with care. The Stout temperature constraint is a hard rule, not a recommendation. Testing that distinction is the point:&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;test_tap_406_stout_too_cold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;RFC 1516 §3.2.3: MUST NOT. This is not a SHOULD NOT.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_tap_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;406&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MUST NOT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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 &lt;code&gt;piwo://&lt;/code&gt; tap — port 1414's justification, made navigable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;piwo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Polish — RFC 1516 §1, port 1414 memorial
&lt;/span&gt;    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pressure&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compatible_styles&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;Lager&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;Pilsner&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;/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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_gdansk_piwo_tap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;piwo:// scheme — Gdansk 1414. We pour one out.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_tap_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tap_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lager&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;TAP_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tap-gdansk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;piwo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;: Python 3.12, FastAPI (test suite only), asyncio raw TCP (real server), structlog, pytest. Single-file HTML/JS simulator with no dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build order&lt;/strong&gt;: simulator first, server second, tests throughout. This is the lesson from the HTCPCP implementation: build the thing you can demo immediately, then build the thing that's correct. The simulator forced every protocol decision to be made as UI state before it became a routing decision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The protocol is not just simulated — it is fully implemented and validated with a dedicated test suite.&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.amazonaws.com%2Fuploads%2Farticles%2Fnzir7q3o1x75flt42xao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnzir7q3o1x75flt42xao.png" alt="Test Coverage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The HTCPCP comparison&lt;/strong&gt;: HTBMCP adds a session model (&lt;code&gt;TAP&lt;/code&gt; as initiator with &lt;code&gt;brew_version&lt;/code&gt; tokens for concurrent access), promotes foam to a first-class protocol feature (&lt;code&gt;Accept-Foam&lt;/code&gt; with five levels including &lt;code&gt;belgian&lt;/code&gt; — implementation-defined, but significant), and introduces &lt;code&gt;419 I'm a Wine Glass&lt;/code&gt; alongside &lt;code&gt;418 I'm a teapot&lt;/code&gt;. Where 418 says &lt;em&gt;you are the wrong device&lt;/em&gt;, 419 says &lt;em&gt;you are using the wrong vessel&lt;/em&gt;. The error taxonomy matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this actually teaches&lt;/strong&gt;: The Stout MUST NOT forces you to think about hard constraints vs recommendations. The &lt;code&gt;brew_version&lt;/code&gt; conflict detection forces you to think about optimistic locking. The goblet Trappist-only rule forces you to think about domain validation. The uvicorn rejection forces you to think about where the HTTP stack lives. These are not beer problems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Reinheitsgebot requires no acknowledgement. It has been enforcing standards compliance since 1516 without asking for credit.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>showdev</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Stop Ignoring RFC 2324. It's the Most Important Protocol You've Never Implemented.</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Tue, 24 Feb 2026 23:44:05 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/stop-ignoring-rfc-2324-its-the-most-important-protocol-youve-never-implemented-53pe</guid>
      <description>&lt;p&gt;Some RFCs change the world — TCP/IP, HTTP/2, TLS 1.3.&lt;/p&gt;

&lt;p&gt;And then there's &lt;strong&gt;RFC 2324&lt;/strong&gt;, published April 1st, 1998, defining the &lt;strong&gt;Hyper Text Coffee Pot Control Protocol&lt;/strong&gt; (HTCPCP/1.0). Its purpose: control, monitor, and diagnose coffee pots over a network.&lt;/p&gt;

&lt;p&gt;No, it's not a joke. Well, it is. But it's &lt;strong&gt;written seriously enough to actually implement&lt;/strong&gt;. Emacs did it. We will too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief History of the Web's Most Honorable RFC
&lt;/h2&gt;

&lt;p&gt;RFC 2324 is an IETF April Fools' joke authored by Larry Masinter. It extends HTTP with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;New &lt;strong&gt;HTTP methods&lt;/strong&gt;: &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, &lt;code&gt;PROPFIND&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A new &lt;strong&gt;header&lt;/strong&gt;: &lt;code&gt;Accept-Additions&lt;/code&gt; (for milk, sugar, whisky — yes, whisky)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A new &lt;strong&gt;URI scheme&lt;/strong&gt;: &lt;code&gt;coffee://&lt;/code&gt; (and &lt;code&gt;koffie://&lt;/code&gt;, &lt;code&gt;café://&lt;/code&gt;, and 26 other translations)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Two new &lt;strong&gt;error codes&lt;/strong&gt; that changed internet history&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The error codes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;406 Not Acceptable  — The server cannot brew this coffee
418 I'm a teapot    — The server is a teapot, not a coffee pot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;418&lt;/strong&gt; became iconic. In 2017, a proposal to remove it from the IANA registry triggered an actual revolt across the dev community. Node.js, Go, Python — everyone kept it. The teapot won.&lt;/p&gt;

&lt;p&gt;In 2014, &lt;strong&gt;RFC 7168&lt;/strong&gt; extended the protocol to tea (&lt;em&gt;HTCPCP-TEA&lt;/em&gt;), adding the &lt;code&gt;message/teapot&lt;/code&gt; MIME type and the requirement to distinguish an Earl Grey from a Darjeeling. Rigor in absurdity.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the RFC Actually Defines
&lt;/h2&gt;

&lt;p&gt;Before writing a single line, read the spec. That's the exercise.&lt;/p&gt;
&lt;h3&gt;
  
  
  The new methods
&lt;/h3&gt;

&lt;p&gt;MethodRole&lt;code&gt;BREW&lt;/code&gt; (or &lt;code&gt;POST&lt;/code&gt;)Trigger an infusion&lt;code&gt;GET&lt;/code&gt;Get the coffee pot's current state&lt;code&gt;PROPFIND&lt;/code&gt;List available additions&lt;code&gt;WHEN&lt;/code&gt;&lt;strong&gt;Stop pouring the milk&lt;/strong&gt; — the client says "when!"&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;WHEN&lt;/code&gt; method is the most beautiful. It models a human exchange ("tell me when") as an HTTP request. A masterpiece of protocol anthropomorphism.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Accept-Additions header
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;BREW /coffee-pot-1 HTCPCP/1.0
Accept-Additions: milk-type=Whole-milk; syrup-type=Vanilla; alcohol-type=Whisky
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Legal values include &lt;code&gt;Cream&lt;/code&gt;, &lt;code&gt;Half-and-half&lt;/code&gt;, &lt;code&gt;Whole-milk&lt;/code&gt;, &lt;code&gt;Non-Dairy&lt;/code&gt;, syrups (&lt;code&gt;Vanilla&lt;/code&gt;, &lt;code&gt;Chocolate&lt;/code&gt;, &lt;code&gt;Raspberry&lt;/code&gt;, &lt;code&gt;Almond&lt;/code&gt;), and spirits (&lt;code&gt;Whisky&lt;/code&gt;, &lt;code&gt;Rum&lt;/code&gt;, &lt;code&gt;Kahlua&lt;/code&gt;, &lt;code&gt;Aquavit&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Intentionally absent: any decaffeinated option. The RFC's comment on this is terse: &lt;em&gt;"What's the point?"&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Play First: The Standalone Simulator
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of server code, I built a &lt;strong&gt;fully self-contained HTML/JS simulator&lt;/strong&gt; that runs entirely in the browser. No backend, no dependencies, no install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive HTCPCP Dashboard&lt;/strong&gt;&lt;br&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__body flex items-center justify-between"&gt;
        &lt;a href="https://htcpcp.benchwiseunderflow.in/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;htcpcp.benchwiseunderflow.in&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;The simulator is not a mock of the server — it &lt;em&gt;is&lt;/em&gt; a complete HTCPCP implementation, just in a different runtime. All the state lives in JavaScript: pot registry, brew history, status transitions, 418/406 logic. It's the fastest way to feel the protocol before committing to a stack.&lt;/p&gt;

&lt;p&gt;Try to BREW on a teapot. Watch the 418 fire. Select decaf and get a 406. Click WHEN mid-brew to stop the milk. Then come back here and build the production version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Ship It: The Production Server
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A word on uvicorn
&lt;/h3&gt;

&lt;p&gt;The natural instinct is &lt;code&gt;uvicorn main:app --reload&lt;/code&gt;. Don't. uvicorn validates HTTP method names at the &lt;strong&gt;socket level&lt;/strong&gt;, before any request parsing happens. &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, and &lt;code&gt;PROPFIND&lt;/code&gt; are not registered IANA methods, so uvicorn rejects them immediately with &lt;code&gt;Invalid HTTP request received&lt;/code&gt; — regardless of any FastAPI config.&lt;/p&gt;

&lt;p&gt;The fix: a raw asyncio TCP server (&lt;code&gt;server.py&lt;/code&gt;) with a minimal HTTP/1.1 parser that accepts any valid RFC 7230 token as a method name. Which &lt;code&gt;BREW&lt;/code&gt;, &lt;code&gt;WHEN&lt;/code&gt;, and &lt;code&gt;PROPFIND&lt;/code&gt; are. This is actually the more correct approach — HTCPCP defines its own protocol, and rolling your own transport layer is the honest implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server.py
&lt;span class="c"&gt;# ☕  HTCPCP/1.0 — RFC 2324  (127.0.0.1:2324)&lt;/span&gt;

curl &lt;span class="nt"&gt;-X&lt;/span&gt; BREW http://localhost:2324/coffee/pot-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept-Additions: milk-type=Whole-milk; alcohol-type=Whisky"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastAPI + &lt;code&gt;main.py&lt;/code&gt; is still useful for one thing: the test suite. FastAPI's &lt;code&gt;TestClient&lt;/code&gt; bypasses the HTTP transport layer entirely, so custom methods work fine in tests — and you get all the validation and schema benefits of FastAPI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pytest test_htcpcp.py -v   #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uses main.py + TestClient, no server.py needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Architecture: a pot registry
&lt;/h3&gt;

&lt;p&gt;First architectural decision: model the entities properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PotType&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="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;COFFEE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;TEAPOT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PotStatus&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="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;IDLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;BREWING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brewing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;POURING_MILK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pouring-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;READY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;
    &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IDLE&lt;/span&gt;
    &lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&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;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# The registry — the core of the architecture
&lt;/span&gt;&lt;span class="n"&gt;POT_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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="n"&gt;CoffeePot&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee://pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COFFEE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;varieties&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;Espresso&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;Lungo&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;Americano&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;coffee://pot-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;COFFEE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&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;varieties&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;Espresso&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;tea://kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TEAPOT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;varieties&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;Earl Grey&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;Chamomile&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;Darjeeling&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parsing the Accept-Additions header
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="n"&gt;SUPPORTED_ADDITIONS&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;milk-type&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cream&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;Half-and-half&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;Whole-milk&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;Part-Skim&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;Skim&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;Non-Dairy&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;syrup-type&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vanilla&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;Almond&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;Raspberry&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;Chocolate&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;sweetener-type&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sugar&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;Honey&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;spice-type&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cinnamon&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;Cardamom&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;alcohol-type&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Whisky&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;Rum&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;Kahlua&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;Aquavit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_accept_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;header&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="n"&gt;additions&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;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;header&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;part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;additions&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# RFC 2324 §2.1.1: no decaf option — intentionally
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decaf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&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;error&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;Not Acceptable&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;message&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;Decaffeinated coffee? What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the point?&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;rfc&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;RFC 2324 §2.1.1&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="n"&gt;unsupported&lt;/span&gt; &lt;span class="o"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&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;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&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;unsupported&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;406&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&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;error&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;Not Acceptable&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;unsupported_additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unsupported&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;h3&gt;
  
  
  The HTCPCP endpoints
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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;CoffeePot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;uri&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="s"&gt;coffee://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POT_REGISTRY&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;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;POT_REGISTRY&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tea://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot_id&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pot&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pot not found in registry&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;pot&lt;/span&gt;

&lt;span class="c1"&gt;# ── BREW ────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;BREW&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;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;brew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# RFC 2324 §2.3.2: teapot → 418, mandatory
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pot_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PotType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TEAPOT&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;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;418&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&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;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m a teapot&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;body&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;The requested entity body is short and stout.&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;hint&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;Tip me over and pour me out.&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;pot_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rfc&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;RFC 2324 §2.3.2&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;suggestion&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;Use coffee://pot-1/brew instead&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pot is empty. Refill required.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;additions_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;accept-additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;additions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_accept_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions_header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;validate_additions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 406 if decaf or invalid additions
&lt;/span&gt;
    &lt;span class="n"&gt;brew_id&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;)&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BREWING&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Milk requested → enter pouring-milk state
&lt;/span&gt;    &lt;span class="n"&gt;has_milk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk-type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_milk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POURING_MILK&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;brew_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;brew_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Coffee is brewing.&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;pot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&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-additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;milk_pouring&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;has_milk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protocol&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;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# ── GET ──────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.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;/coffee/{pot_id}/status&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;get_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pot_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cups&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;brew_count&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brew_history&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;varieties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;varieties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;protocol&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;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ── PROPFIND ─────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}/additions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;PROPFIND&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;propfind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;SUPPORTED_ADDITIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decaf&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;NOT_ACCEPTABLE — What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the point? (RFC 2324 §2.1.1)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# ── WHEN ─────────────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="nd"&gt;@app.api_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee/{pot_id}/stop-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;WHEN&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;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    RFC 2324 §2.1.3 — WHEN
    Sent when the client determines that enough milk has been poured.
    The server must stop immediately.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;pot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_pot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pot_id&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;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POURING_MILK&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;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;message&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;WHEN acknowledged.&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;note&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;No milk was being poured, but your enthusiasm is appreciated.&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;rfc&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;RFC 2324 §2.1.3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;pot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PotStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BREWING&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;message&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;Milk pouring stopped.&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;detail&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;The server has acknowledged WHEN and stopped the milk stream.&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;protocol&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;HTCPCP/1.0&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;rfc&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;RFC 2324 §2.1.3&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;
  
  
  Middleware: enforce HTCPCP headers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HTCPCPMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Protocol&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-RFC&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RFC-2324&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="c1"&gt;# Detect a BREW on a non-coffee route and punish accordingly
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/coffee&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="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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;error&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;Wrong universe&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;hint&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;BREW is only valid on coffee:// URIs&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;response&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTCPCPMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Structured logs — because we're professionals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;structlog&lt;/span&gt;

&lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;structlog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_logger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# After a successful BREW:
&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;htcpcp.brew&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;brew_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;additions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTCPCP/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# On 418:
&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;htcpcp.teapot_detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pot_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pot_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Teapot attempted to brew coffee&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;Which produces in your JSON logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htcpcp.brew"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pot-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"brew_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"additions"&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="nl"&gt;"milk-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Whole-milk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alcohol-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Whisky"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HTCPCP/1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"info"&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="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htcpcp.teapot_detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kettle-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;418&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&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;h2&gt;
  
  
  What This Actually Teaches You
&lt;/h2&gt;

&lt;p&gt;Implementing an April Fools' RFC is a serious exercise in disguise. You end up learning:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to read an RFC properly&lt;/strong&gt; — distinguishing MUST, SHOULD, MAY. RFC 2324 uses all three with care. The 418 is a MUST if the server is a teapot. A broken coffee machine should return 503 — not 418. That's a common mistake, and it matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the HTTP stack actually works&lt;/strong&gt; — trying to use uvicorn with &lt;code&gt;BREW&lt;/code&gt; reveals that method validation happens at the socket level, before h11, before FastAPI, before your code. You end up writing a raw asyncio TCP server to get HTCPCP working for real. That's not a detour — that's the point. You now understand the HTTP request pipeline better than most devs who've shipped production APIs for years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to think in entities&lt;/strong&gt; — the pot registry, the &lt;code&gt;CoffeePot&lt;/code&gt; vs &lt;code&gt;Teapot&lt;/code&gt; distinction, routing by &lt;code&gt;coffee://&lt;/code&gt; URI: this is real domain modeling. The joke forces you to take it seriously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to model state machines&lt;/strong&gt; — &lt;code&gt;idle → brewing → pouring-milk → ready&lt;/code&gt; is a textbook workflow. WHEN is a client-driven transition. You'll see this pattern everywhere in production systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to write integration tests for absurd-but-useful edge cases&lt;/strong&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_teapot_cannot_brew&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&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;/coffee/kettle-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;418&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m a teapot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_decaf_is_not_acceptable&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&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;/coffee/pot-1&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;Accept-Additions&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;decaf=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;406&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_when_stops_milk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BREW&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;/coffee/pot-1&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;Accept-Additions&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;milk-type=Whole-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHEN&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;/coffee/pot-1/stop-milk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;418 survived every attempt to kill it because it represents something real: &lt;strong&gt;developers are allowed to be playful&lt;/strong&gt;. An April Fools' RFC published today would probably get killed in committee within a week. The one from 1998 has lasted 26 years.&lt;/p&gt;

&lt;p&gt;What makes RFC 2324 remarkable is that it takes absurdity seriously — it has a real state machine, real error codes with precise semantics, a real extension (RFC 7168 for tea). It mocks formalism by respecting it perfectly.&lt;/p&gt;

&lt;p&gt;That's exactly how we should build our own systems.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Simulator (HTML/JS standalone), &lt;code&gt;server.py&lt;/code&gt; (raw TCP), &lt;code&gt;main.py&lt;/code&gt; + full test suite — on Github: &lt;a href="https://github.com/pcescato/htcpcp/" rel="noopener noreferrer"&gt;https://github.com/pcescato/htcpcp/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;RFC 2324: &lt;a href="https://tools.ietf.org/html/rfc2324" rel="noopener noreferrer"&gt;https://tools.ietf.org/html/rfc2324&lt;/a&gt;&lt;/em&gt; &lt;em&gt;RFC 7168: &lt;a href="https://tools.ietf.org/html/rfc7168" rel="noopener noreferrer"&gt;https://tools.ietf.org/html/rfc7168&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fun</category>
      <category>http</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Article Isn’t the Real Content</title>
      <dc:creator>Pascal CESCATO</dc:creator>
      <pubDate>Sat, 21 Feb 2026 12:28:14 +0000</pubDate>
      <link>https://dev.to/pascal_cescato_692b7a8a20/your-article-isnt-the-real-content-2e67</link>
      <guid>https://dev.to/pascal_cescato_692b7a8a20/your-article-isnt-the-real-content-2e67</guid>
      <description>&lt;p&gt;The article is often the least interesting thing that happens after you publish.&lt;/p&gt;

&lt;p&gt;Not always. But more often than we're willing to admit.&lt;/p&gt;

&lt;p&gt;I saw this comment recently and couldn't unsee it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Real war stories have specifics — the exact error, the wrong turn you took first, the fix that seemed obvious in retrospect. Generated stuff stays vague. Honestly the comments on that post became better content than the post itself."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence describes something we rarely say out loud.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens when you write with real specifics
&lt;/h2&gt;

&lt;p&gt;Not tips. Not frameworks. The actual error. The wrong turn. The fix you missed for three hours.&lt;/p&gt;

&lt;p&gt;That level of detail does something polished content almost never does: it makes other builders recognize themselves instantly.&lt;/p&gt;

&lt;p&gt;And recognition doesn't produce passive reading. It produces contribution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The article doesn't contain the knowledge. It releases it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Publishing without the discussion is publishing an incomplete object.
&lt;/h2&gt;

&lt;p&gt;The post and the thread aren't two separate things. One makes the other possible. Strip the comments and you lose half the object. Strip the post and the comments have no spine.&lt;/p&gt;

&lt;p&gt;We treat articles as finished products and comments as disposable noise. So we archive the post and let the discussion rot.&lt;/p&gt;

&lt;p&gt;Which means we're systematically preserving the least interesting half of the knowledge.&lt;/p&gt;

&lt;p&gt;We optimize for authorship. We don't preserve conversations.&lt;/p&gt;

&lt;p&gt;Maybe we didn't even want to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where did the real fix appear — in the article or in the replies?
&lt;/h2&gt;

&lt;p&gt;Link a post where the comments mattered more than the text.&lt;/p&gt;

&lt;p&gt;What's the best technical insight you found buried three levels deep in a thread — that no article ever captured?&lt;/p&gt;

&lt;p&gt;If you think the article still matters more than the discussion, I'd genuinely like to hear why — because I'm no longer sure.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>writing</category>
      <category>devjournal</category>
    </item>
  </channel>
</rss>
