<?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: arun rajkumar</title>
    <description>The latest articles on DEV Community by arun rajkumar (@mickyarun).</description>
    <link>https://dev.to/mickyarun</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%2F3835684%2F4771b603-8faa-42b1-9e0e-0687faea63a3.jpg</url>
      <title>DEV Community: arun rajkumar</title>
      <link>https://dev.to/mickyarun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mickyarun"/>
    <language>en</language>
    <item>
      <title>Two Strangers Built an Agent Mandate Protocol in My Comments. It Still Needs a Regulator.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Wed, 16 Sep 2026 05:45:09 +0000</pubDate>
      <link>https://dev.to/mickyarun/two-strangers-built-an-agent-mandate-protocol-in-my-comments-it-still-needs-a-regulator-4bij</link>
      <guid>https://dev.to/mickyarun/two-strangers-built-an-agent-mandate-protocol-in-my-comments-it-still-needs-a-regulator-4bij</guid>
      <description>&lt;p&gt;I got something wrong in my own comments section nine days ago, and two strangers spent a week showing me how wrong.&lt;/p&gt;

&lt;p&gt;The article was about guardrails that quietly stop running. Somebody in the thread described a scheme where an agent's authority is checked against a list of state revisions. I said the trouble with a list is that it rots, and the fix is expiry. Short lifetime, list never grows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt; said no:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A payment authorisation does not get its lifetime from a clock. It gets it from naming the effect [...] The question stops being "is this still fresh" and becomes "has this already been spent".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He is right, and I should have got there first, because it is what I do for a living. A card authorisation for £40 cannot be re-presented for £4,000. Not because it expires quickly. Because it names the amount and the payee, and the side that moves the money is the side that checks.&lt;/p&gt;

&lt;p&gt;What happened after that ran twenty replies deep, mostly without me. &lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt; turned up and the two of them built an authorisation protocol for AI agents in my comments section over about a week. I want to write down what they built, because the thing it runs into at the bottom is not an engineering problem, and I do not think the agent-safety conversation has noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What they built
&lt;/h2&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt; pushed the single-use idea further. A six-hour agent task is not one transaction. It is forty, or however many tool calls touch state, and each one has its own boundary. One mandate per operation, single use, burned by whoever performs the effect.&lt;/p&gt;

&lt;p&gt;Then &lt;a class="mentioned-user" href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt; found the hole. Some calls never resolve from the caller's side. A timeout that lands after the effect owner has already committed leaves you not knowing whether it happened, and treating that as failure lets the clock back in through the retry path.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt;: fencing, not pending. On timeout the issuer writes a cancellation instead of waiting. But the tombstone has to live at the effect owner, because a delayed original still arrives &lt;em&gt;there&lt;/em&gt;, and the issuer's records are not in the room when it does.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt;: the cancellation write needs the same channel that just failed. So fold it into the replacement. M2 carries "supersedes M1", and the effect owner admits M2 and fences M1 in one commit.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt;: and you do not keep the tombstone forever. Two tiers. A blocking record for as long as an honest late message could still turn up, then a cheap id marker after that.&lt;/p&gt;

&lt;p&gt;That is a week of argument compressed into five paragraphs, and I have lost most of it in the compression. The thread is better than this summary.&lt;/p&gt;

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

&lt;p&gt;Read the sequence again and one thing repeats.&lt;/p&gt;

&lt;p&gt;Every move deletes a store and creates one somewhere else.&lt;/p&gt;

&lt;p&gt;The revision list rots, so use expiry. Expiry is a clock, so use single-use. Single-use needs a burn record. The burn record has to live at the effect owner. The effect owner cannot hold it forever, so split it in two and keep the cheap half.&lt;/p&gt;

&lt;p&gt;The state never goes away. It gets smaller and it changes address.&lt;/p&gt;

&lt;p&gt;And at the end of it there is a number. How long the blocking record lives.&lt;/p&gt;

&lt;p&gt;Nobody in that thread could derive that number. Not for lack of ability. Twenty replies of extremely careful reasoning got to it and then stopped, because it is not the kind of thing reasoning produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payments did not solve this either
&lt;/h2&gt;

&lt;p&gt;This is the part I kept circling instead of answering.&lt;/p&gt;

&lt;p&gt;Payments has the same number and does not compute it. It gets handed one. A card scheme's settlement window is a retention policy with a regulator attached, and the reason it works is not that the number is right. It is that the argument was ended by somebody with the authority to end it.&lt;/p&gt;

&lt;p&gt;Both sides read the same rulebook, at the same revision, and neither one can move it afterwards and call it a clarification.&lt;/p&gt;

&lt;p&gt;&lt;a class="mentioned-user" href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt; pushed back when I said that, and fairly. You do not strictly need a regulator. An engineer-set window with a public revision history the effect owner cannot write to buys most of the same property.&lt;/p&gt;

&lt;p&gt;Most of it. Publication makes a unilateral change visible. It does not make it expensive. A card scheme can throw a member out. A commit history cannot. That gap is the difference between a rule and a strongly worded preference, and plenty of systems run fine on the second one, provided everybody knows which one they are standing on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is not just a payments story
&lt;/h2&gt;

&lt;p&gt;Every agent-safety mechanism I have read this year bottoms out in a number like this.&lt;/p&gt;

&lt;p&gt;How long the audit trail stays queryable. How long an idempotency key blocks a replay. How long a revoked credential stays revoked in a cache. How stale a policy snapshot may be before a check refuses to run.&lt;/p&gt;

&lt;p&gt;The mechanisms are good. Some are better designed than what payments was running on fifteen years ago. But the number underneath always shows up as a configuration default, and a configuration default is what you write when nobody has decided who owns the question.&lt;/p&gt;

&lt;p&gt;If the owner is "the platform team", the number is whatever seems reasonable in the sprint where storage costs come up. That is not a dig at platform teams. It is what happens to any number with no counterparty on the other side of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the argument stops working
&lt;/h2&gt;

&lt;p&gt;I do not have a clean ending, which is why this is a post and not a proposal.&lt;/p&gt;

&lt;p&gt;The regulator analogy breaks as soon as you ask who the regulator would be. Payments got one because money moving in the wrong direction is legible to a state. An agent deleting the wrong S3 prefix is not, and I do not want an FCA for tool calls. I doubt anyone does.&lt;/p&gt;

&lt;p&gt;The honest version is smaller than the analogy. Inside one company the caller and the effect owner are usually the same organisation, so the rulebook does not need a regulator. It needs a written-down owner and a change process that is not a pull request approved in forty seconds.&lt;/p&gt;

&lt;p&gt;That is boring. It is also roughly what payments ran on before it had regulators, and it held for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Go and find the retention number in your own system.&lt;/p&gt;

&lt;p&gt;The TTL on your idempotency keys. The window your dedupe cache actually covers. The age at which outbox rows get vacuumed. The lifetime of a revocation entry.&lt;/p&gt;

&lt;p&gt;You have one. Somebody typed it.&lt;/p&gt;

&lt;p&gt;Who was it, what did they know when they typed it, and what happens to them if it turns out to be too short?&lt;/p&gt;

&lt;p&gt;If the answer to the last one is "nothing", you have a mechanism rather than a rule. Worth knowing that before an agent finds out on your behalf.&lt;/p&gt;




&lt;p&gt;The thinking here is &lt;a href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt;'s and &lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt;'s, not mine. Thanks also to &lt;a href="https://dev.to/salparvez"&gt;@salparvez&lt;/a&gt;, whose roof-and-foundation version of the two-tier idea is the one I actually remember, and &lt;a href="https://dev.to/_firelinks"&gt;@_firelinks&lt;/a&gt;, who showed me that a negative control run inside the thing it is testing is circular in exactly the state you built it to catch.&lt;/p&gt;

&lt;p&gt;This is the third article I have written out of my own comments section. At some point that stops being a content strategy and starts being a confession.&lt;/p&gt;

&lt;p&gt;Original thread: &lt;a href="https://dev.to/mickyarun/nobody-checks-whether-the-guardrail-is-running-3ng"&gt;Nobody Checks Whether the Guardrail Is Running&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Four People Rebuilt the Payment Authorisation in My Comments Section</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Thu, 10 Sep 2026 05:22:57 +0000</pubDate>
      <link>https://dev.to/mickyarun/four-people-rebuilt-the-payment-authorisation-in-my-comments-section-57l9</link>
      <guid>https://dev.to/mickyarun/four-people-rebuilt-the-payment-authorisation-in-my-comments-section-57l9</guid>
      <description>&lt;p&gt;I published two articles three weeks apart. They were not about the same thing.&lt;/p&gt;

&lt;p&gt;The first was about agent guardrails that read the command string and miss the money. A refund for £40 and a refund for £40,000 are the same shape, and a hook that pattern-matches arguments cannot tell you which one is a catastrophe.&lt;/p&gt;

&lt;p&gt;The second was about guardrails nobody checks the liveness of. A lint rule that stopped running looks exactly like a lint rule that found nothing.&lt;/p&gt;

&lt;p&gt;Different subjects. Different threads. As far as I can tell, no overlap at all in who turned up to argue.&lt;/p&gt;

&lt;p&gt;Both threads ended up building the same object. Nobody involved said the word "payments".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the first thread built
&lt;/h2&gt;

&lt;p&gt;Three weeks ago &lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt; left this on the guardrails post:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Preview resolves the proposed effect and returns an operation ID plus the subject, target, amount, currency, relevant state version and expiry. Human approval produces a single-use mandate bound to that exact operation. Execute consumes it atomically while rechecking the authoritative state and idempotency key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then the part I think is the most important sentence in either thread:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the server should require the mandate whether or not the client noticed that metadata. Otherwise the convention protects careful clients while direct callers retain the bypass.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the second thread built
&lt;/h2&gt;

&lt;p&gt;Yesterday, on a completely different post, &lt;a href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt; argued that you should stop trying to carry a reachability guarantee across a process boundary, because it cannot survive the trip:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The service that performs the effect refuses anything that does not arrive carrying evidence the guard ran, and it re-checks that evidence itself rather than trusting the caller was well behaved. Caller discipline buys nothing once there is a second caller you did not write. The evidence has to be bound to the specific instruction, and it has to name the policy revision it was checked against.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read those two next to each other.&lt;/p&gt;

&lt;p&gt;Single-use token, bound to one specific operation, issued by the side that owns the state, presented at the point of effect, re-verified there rather than trusted, rejected when stale.&lt;/p&gt;

&lt;p&gt;That is one design, described twice, by two people who have never spoken to each other, three weeks apart, on two posts about different problems.&lt;/p&gt;

&lt;p&gt;It is also a payment authorisation. It has been a payment authorisation since roughly 1979.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other two got further than I did
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/jon_at_backboardio"&gt;@jon_at_backboardio&lt;/a&gt; went straight past the single-operation model to its weak point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;five refunds of £8,000 each, every one under every cap, same payee, four minutes. every individual call is boring. the mandate model catches that only if mandates are scoped to a window rather than to an operation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He also settled the sync-versus-async argument in one observation: the calls where blocking hurts are high-volume and low-value, and the calls where blocking is fine are the ones you want to stop. Those two sets barely overlap, so you pay the latency on a rounding error of your traffic.&lt;/p&gt;

&lt;p&gt;And &lt;a href="https://dev.to/max_quimby"&gt;@max_quimby&lt;/a&gt;, who started the whole thing, put his finger on why none of this is really an AI problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;None of that is agent-specific, which I think is the point — the agent just removes the human who used to eyeball the number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Payments did not design this. It lost money until it existed.
&lt;/h2&gt;

&lt;p&gt;Every piece these four independently derived is something the card networks were forced into, usually after being robbed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound to the instruction.&lt;/strong&gt; An authorisation is for an amount and a payee. You cannot get one for £40 and present it for £4,000. That is not elegance, it is scar tissue from people doing exactly that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verified by the side holding the state.&lt;/strong&gt; The merchant does not decide whether an authorisation is good. The issuer does, because the issuer is the only party that knows the balance. Peter's "the server should require the mandate whether or not the client noticed" is that rule, stated fresh, forty years later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-use.&lt;/strong&gt; Replay is the oldest attack there is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expiry.&lt;/strong&gt; This one gets skipped in every clean-room design and it is load-bearing. Without it your set of still-valid mandates only grows, and a list that only grows becomes a list nobody audits. Expiry is what keeps that set small enough to reason about. You trade a list that rots quietly for a clock that fails loudly, and the clock is much easier to think about at three in the morning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more things the threads are about to discover
&lt;/h2&gt;

&lt;p&gt;Since payments got here first and paid for it, here is what is waiting further down the road.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Velocity, not caps.&lt;/strong&gt; Jon's five-refunds case is not an edge case, it is the standard attack, and per-transaction limits have never caught it. What catches it is cumulative exposure per counterparty per window. The check is easy. Choosing the window is not, and whatever window you choose, somebody can straddle it. Four minutes clears an hourly window if you start at 59 minutes past.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview is a surface too.&lt;/strong&gt; Auth-then-capture has a gap between the two halves, and people have lived in that gap professionally for decades. If preview simulates the effect and execute performs it, any divergence between those two code paths is somewhere a difference can hide. Much smaller than the hole it closes. Not zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approval screens launder decisions.&lt;/strong&gt; Jon again, and it is the best line anyone has left on anything I have written: showing a human raw arguments is not review, it is laundering. If the human cannot see what the number means relative to the account it is hitting, you have not added a control. You have added a signature to blame later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one part that does not transfer
&lt;/h2&gt;

&lt;p&gt;I do not want to oversell the analogy, because there is a real gap in it.&lt;/p&gt;

&lt;p&gt;A payment has a natural boundary. There is a moment the transaction starts and a moment it settles, and every lifetime in the system hangs off those two points. Expiry is easy to reason about because there is an obvious thing for it to be shorter than.&lt;/p&gt;

&lt;p&gt;An agent halfway through a long task has no such boundary. The task might run for six hours. It might spawn sub-tasks. It might pause overnight and resume. Scope the mandate to the operation and Jon's five-refund case walks straight through. Scope it to the session and you have issued a bearer token for a session whose end you cannot define.&lt;/p&gt;

&lt;p&gt;So: what is the agent equivalent of a transaction boundary?&lt;/p&gt;

&lt;p&gt;I do not have an answer. Payments handed us the shape of the solution for free, and then kept the one thing that made it work.&lt;/p&gt;




&lt;p&gt;Quoted with thanks and without permission: &lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt;, &lt;a href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt;, &lt;a href="https://dev.to/jon_at_backboardio"&gt;@jon_at_backboardio&lt;/a&gt; and &lt;a href="https://dev.to/max_quimby"&gt;@max_quimby&lt;/a&gt;. The threads are &lt;a href="https://dev.to/mickyarun/your-agents-guardrails-cant-see-the-money-35f"&gt;here&lt;/a&gt; and &lt;a href="https://dev.to/mickyarun/nobody-checks-whether-the-guardrail-is-running-3ng"&gt;here&lt;/a&gt;, and both are worth more than the posts they are attached to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Your Audit Log Agrees With Itself. That's the Problem.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Wed, 09 Sep 2026 10:15:14 +0000</pubDate>
      <link>https://dev.to/mickyarun/your-audit-log-agrees-with-itself-thats-the-problem-2idf</link>
      <guid>https://dev.to/mickyarun/your-audit-log-agrees-with-itself-thats-the-problem-2idf</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.to/remdore"&gt;@remdore&lt;/a&gt; ran an experiment this week that I have not stopped thinking about.&lt;/p&gt;

&lt;p&gt;One account, balance 100. Ten workers, each withdrawing 10. Every worker does the obvious thing: read the balance, subtract, write it back, log what happened.&lt;/p&gt;

&lt;p&gt;Final balance: 90. Nine withdrawals gone.&lt;/p&gt;

&lt;p&gt;That part is a race condition and every backend engineer has met it. The part I want to talk about is the audit log.&lt;/p&gt;

&lt;p&gt;Ten rows. Every one of them says the balance went from 100 to 90. No gaps. No nulls. No orphaned foreign keys. Nothing a schema validator, a checksum, or a nightly reconciliation job would flag.&lt;/p&gt;

&lt;p&gt;Hand me that table during an incident and I would read it as one withdrawal retried ten times. I would go and look at the retry logic. The retry logic is fine. I would spend the afternoon there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The log is not wrong. That's what makes it dangerous.
&lt;/h2&gt;

&lt;p&gt;We are in regulated payments. The audit log is not a debugging convenience for us. It is the artefact. It is what an auditor reads, what a dispute resolves against, what we would put in front of the FCA if someone asked how a payment reached the state it did.&lt;/p&gt;

&lt;p&gt;So the standard we hold it to is not "does it look sensible". It is "is this the record of what happened".&lt;/p&gt;

&lt;p&gt;And the failure in that experiment passes every test I know how to write against a log.&lt;/p&gt;

&lt;p&gt;Think about what you can actually check automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema conformance. Passes.&lt;/li&gt;
&lt;li&gt;No nulls in required columns. Passes.&lt;/li&gt;
&lt;li&gt;Referential integrity to the account. Passes.&lt;/li&gt;
&lt;li&gt;Monotonic timestamps. Passes.&lt;/li&gt;
&lt;li&gt;Balance arithmetic internally consistent per row. Passes, ten times.&lt;/li&gt;
&lt;li&gt;Row count matches the number of operations attempted. Passes. Ten operations, ten rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those questions is about whether the log is &lt;em&gt;coherent&lt;/em&gt;. None of them is about whether the log is &lt;em&gt;true&lt;/em&gt;. Ten processes each believed they were alone, and each one wrote down a faithful account of the world as it saw it. Coherence is exactly what you get when ten honest narrators all have the same wrong information.&lt;/p&gt;

&lt;p&gt;The thing you would need to detect it is a row that disagrees with another row. There isn't one. Disagreement is the signal, and the bug removed the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the same failure I wrote about two days ago
&lt;/h2&gt;

&lt;p&gt;I published something on Monday about guardrails nobody checks the liveness of. A lint rule that stopped running, a policy check that matches nothing, an eval that skips half its corpus and reports the half it ran. The thread on it got long, and the argument that came out of it was sharper than the article.&lt;/p&gt;

&lt;p&gt;The shape is the same here.&lt;/p&gt;

&lt;p&gt;A check that never fires and a log that never contradicts itself both produce the same reading: nothing is wrong. In both cases the absence of a complaint is being taken as evidence of health, and in both cases the mechanism that would produce the complaint is the thing that broke.&lt;/p&gt;

&lt;p&gt;You cannot fix that by inspecting the output more carefully. The output is clean. That is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-statement version, and why it matters more than it looks
&lt;/h2&gt;

&lt;p&gt;Postgres 18 lets you name the row before and after a change in the same statement that makes it:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;accounts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&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;RETURNING&lt;/span&gt; &lt;span class="k"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax is nice. It is not the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part is that the log entry is now derived from the write instead of being a second write that describes the first. Before this, the audit row was a separate statement carrying a separate copy of the truth, and two copies of the truth can drift. After it, there is one statement, and the record and the change are the same event. There is no version of the run where the balance moves and the log says something else, because there is no seam between them for a lie to get into.&lt;/p&gt;

&lt;p&gt;That is the general principle and it is worth more than the syntax. &lt;strong&gt;Any time the record and the thing being recorded are two separate writes, you have two versions of the truth, and only one of them is the one that moves money.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can apply that without Postgres 18. The change is to stop thinking of the audit log as something you write &lt;em&gt;about&lt;/em&gt; an operation and start treating it as something the operation &lt;em&gt;emits&lt;/em&gt;. If a code path can complete without producing its record, the record is optional, and optional loses to deadline pressure every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops working
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the limit, because the single-statement version solves a single-database problem and most of us do not have a single-database problem.&lt;/p&gt;

&lt;p&gt;The moment the operation crosses a service boundary, the seam is back. The money moves in one place and the record is written in another, and now you are relying on an outbox, or a queue with delivery guarantees you have read about but not tested, or in the worst case a developer remembering to call the logging function. Convention again.&lt;/p&gt;

&lt;p&gt;The only thing I have found that helps at that scale is narrowing the number of routes. Anything touching money goes through one path with the record emitted inside it, so there is no version of the call that reaches the effect without producing the evidence. Everything outside that path is convention, and I am not comfortable about it, but I would rather have one path I can defend than six I have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I would actually like answered
&lt;/h2&gt;

&lt;p&gt;Here is what I do not have a good answer for.&lt;/p&gt;

&lt;p&gt;Every detection method I know for this class of bug is a &lt;em&gt;disagreement&lt;/em&gt; detector. Two sources that should match and don't. A count that should equal another count. A hash that should still verify. All of them need two things to compare.&lt;/p&gt;

&lt;p&gt;The read-modify-write bug produces one thing, ten times. There is nothing to compare it against, because the second source was never independent.&lt;/p&gt;

&lt;p&gt;So: has anyone got a check that catches a log which is internally consistent and collectively false, without needing an independent second record to diff against? Not a stronger schema. Not more constraints on the row. Something that notices that ten narrators agreeing is itself suspicious.&lt;/p&gt;

&lt;p&gt;I think the honest answer might be that there isn't one, and the only real defence is making the seam impossible rather than detecting what crawls through it. But I would like to be wrong about that, because "make the seam impossible" is expensive and does not retrofit.&lt;/p&gt;




&lt;p&gt;Credit where it is due: the experiment and the Postgres 18 walkthrough are &lt;a href="https://dev.to/remdore/your-audit-log-is-probably-lying-to-you-postgres-18-fixes-it-in-one-statement-15nb"&gt;@remdore's&lt;/a&gt;. Go and read the original. I have only taken the part that scared me and pointed it at my own industry.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>postgres</category>
      <category>database</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Nobody Checks Whether the Guardrail Is Running</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:07:25 +0000</pubDate>
      <link>https://dev.to/mickyarun/nobody-checks-whether-the-guardrail-is-running-3ng</link>
      <guid>https://dev.to/mickyarun/nobody-checks-whether-the-guardrail-is-running-3ng</guid>
      <description>&lt;p&gt;Every AI-and-engineering post right now is about adding a guardrail.&lt;/p&gt;

&lt;p&gt;Lint rules the agent can't bypass. Evals before you ship a prompt change. A review bot on every PR. A regression suite built from real production failures. Architecture rules checked into the repo so the model reads them on the way in.&lt;/p&gt;

&lt;p&gt;I have written some of those posts. I still believe in them. This is the work that makes AI usable by senior engineers instead of a liability.&lt;/p&gt;

&lt;p&gt;But I have been in three separate conversations on this platform in the last week that were all, underneath, about the same thing, and it isn't about which guardrails to add.&lt;/p&gt;

&lt;p&gt;A guardrail that has never fired and a guardrail that silently stopped running produce identical output.&lt;/p&gt;

&lt;p&gt;Green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline that had never been green
&lt;/h2&gt;

&lt;p&gt;Vicente Reyes had a GitHub Actions workflow called &lt;code&gt;Deploy to DigitalOcean&lt;/code&gt;. Fully wired. SSH action, secrets, the works. And every time he shipped a backend change he still SSH'd into the droplet and ran &lt;code&gt;git pull&lt;/code&gt; by hand.&lt;/p&gt;

&lt;p&gt;The deploy job was gated on CI passing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CI'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.workflow_run.conclusion == 'success' }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensible enough. The problem was that CI had never once gone green on &lt;code&gt;main&lt;/code&gt;. Not flaky. Never. So every deploy run showed &lt;code&gt;skipped&lt;/code&gt;, forever, and the pipeline sat permanently behind a gate that could not open.&lt;/p&gt;

&lt;p&gt;His line about it is the one worth keeping. A pipeline that is silently and permanently blocked looks, from a distance, exactly like a pipeline that doesn't exist.&lt;/p&gt;

&lt;p&gt;Nothing in the Actions UI says &lt;em&gt;this workflow has not succeeded in forty runs&lt;/em&gt;. You have to go and ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  The grader that can't say no
&lt;/h2&gt;

&lt;p&gt;On a thread about designing trustworthy AI evals, Heinrich Neb made the point that every grader needs a known-bad twin. An input it is supposed to reject, plus a recorded date of when it last actually rejected something.&lt;/p&gt;

&lt;p&gt;His framing is the sharpest version of this I have seen: a grader that has never failed and a grader that silently stopped running print the same green.&lt;/p&gt;

&lt;p&gt;Same failure as Vicente's pipeline, one layer up. In his case the gate was stuck closed. In an eval suite the gate is stuck open, which is worse, because a stuck-closed gate is annoying enough that somebody eventually investigates. A stuck-open gate just keeps saying yes.&lt;/p&gt;

&lt;p&gt;Think about how an eval suite actually rots. Somebody changes a prompt template and the grader's regex stops matching, so everything scores as pass. Somebody renames a dataset field, the loader returns an empty list, the suite runs zero cases in 0.4 seconds and reports 100%. A provider changes a default and your grader model gets more agreeable.&lt;/p&gt;

&lt;p&gt;All three look like success.&lt;/p&gt;

&lt;h2&gt;
  
  
  The score with no provenance
&lt;/h2&gt;

&lt;p&gt;The third one is mine, from the same thread.&lt;/p&gt;

&lt;p&gt;An eval result with no harness version, no dataset snapshot and no prompt revision attached to it is not evidence. It is a self-reported claim.&lt;/p&gt;

&lt;p&gt;Which is fine right up until the number moves. Then somebody asks whether the model got better or the suite got easier, and if you can't answer, you never had a measurement. You had a vibe with a decimal point on it.&lt;/p&gt;

&lt;p&gt;That reflex comes from working in payments. In a regulated system nobody asks you to trust that a control ran. They ask you to demonstrate which control ran, on what input, at what time, under which version of the rules. Months later. To somebody who wasn't there and isn't inclined to take your word for it.&lt;/p&gt;

&lt;p&gt;Engineering has quietly inherited that burden. Evals are increasingly the artefact a shipping decision rests on. They just haven't inherited the paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually ties these together
&lt;/h2&gt;

&lt;p&gt;When you automate a check, you swap one question for another and don't notice.&lt;/p&gt;

&lt;p&gt;Before automation the question is &lt;em&gt;did someone look at this?&lt;/em&gt; You know the answer, because you can see the person and ask them.&lt;/p&gt;

&lt;p&gt;After automation the question you think you are asking is still &lt;em&gt;did the check pass?&lt;/em&gt; But the question you are now depending on is &lt;em&gt;is the check alive?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Almost nobody instruments the second one.&lt;/p&gt;

&lt;p&gt;This is well understood in operations. You don't only alert on errors, you alert on the absence of a heartbeat, because a monitoring system that dies looks exactly like a system with no problems. Dead man's switches exist for precisely this reason.&lt;/p&gt;

&lt;p&gt;We have somehow not carried it across to the checks that gate our code. A CI workflow, a lint rule, an eval suite, an agent policy. These are all monitoring systems for correctness, and we run them with no heartbeat at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a guardrail needs before you trust it
&lt;/h2&gt;

&lt;p&gt;None of this is exotic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A known-bad input it must reject.&lt;/strong&gt; Every check needs a case it is supposed to fail on, running alongside the real ones. If your lint rule can't catch its own canary, the lint rule isn't running. If your eval's negative control scores as a pass, the grader is broken and every other number in that run is noise. Same idea as a negative control in a lab. Nobody trusts an assay that only ever comes back clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A recorded date of last rejection.&lt;/strong&gt; Not when it last ran. When it last said no. A guardrail that hasn't rejected anything in four months is either protecting an unusually disciplined team or it broke in May, and those look identical on a dashboard. Put it in a column somewhere. If nobody can answer "when did this last catch something", it is decoration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A run count somebody occasionally looks at.&lt;/strong&gt; The zero-cases failure is the sneakiest one, because a suite that loads an empty dataset passes fast with a perfect score. Assert on the count. If it expects 240 cases and got 0, that is a hard failure, not a 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provenance on the result.&lt;/strong&gt; Harness commit, dataset hash, prompt revision, model version, timestamp. Attached to the score, not sitting in a CI log with thirty-day retention. The test is simple. Six months from now, can you reproduce this exact number? If not, you can't use it to defend a decision, which means it was never really the reason for the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gets worse with agents, not better
&lt;/h2&gt;

&lt;p&gt;The reason I keep coming back to this is that AI shifts the ratio.&lt;/p&gt;

&lt;p&gt;The argument for agents in engineering, the one I actually believe, is that you get something close to an army of near-zero-mistake juniors. The constraint stops being how fast people can write code and becomes how fast people can review it. So you compensate by pushing more review into automation. More lint rules, more tests, more evals, more policy checks.&lt;/p&gt;

&lt;p&gt;That is the right move. I would make it again.&lt;/p&gt;

&lt;p&gt;But it means the fraction of your correctness resting on unattended machinery goes up sharply. When a human reviewed everything, a broken lint rule was a small hole in a large net. When automation reviews everything, the broken lint rule &lt;em&gt;is&lt;/em&gt; the net.&lt;/p&gt;

&lt;p&gt;The guardrails become load-bearing at exactly the moment nobody is watching them closely enough to notice they stopped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go and check one
&lt;/h2&gt;

&lt;p&gt;Pick the guardrail you would be most upset to lose. The eval suite that gates prompt changes, the rule that stops an agent touching the payment path, whatever yours is.&lt;/p&gt;

&lt;p&gt;Then answer one question about it. When did it last say no?&lt;/p&gt;

&lt;p&gt;If you can find that out in under a minute, good. If you can't find it at all, you don't have a guardrail. You have a green light with nothing behind it, and you have been treating it as evidence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Credit where it's due. The "never failed and stopped running print the same green" framing is &lt;a href="https://dev.to/heinrichneb"&gt;Heinrich Neb's&lt;/a&gt;, and the permanently-gated pipeline is &lt;a href="https://dev.to/highcenburg/-the-cicd-pipeline-that-was-lying-to-us-a-deploy-debugging-story-9fi"&gt;Vicente G. Reyes'&lt;/a&gt;. I just noticed they were the same bug.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm Arun, CTO and co-founder at Atoa. We build open banking payments for the UK. I write about AI, payments, and the messy parts of running engineering systems. &lt;a href="https://x.com/mickyarun" rel="noopener noreferrer"&gt;@mickyarun&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Your Health Check Is Lying to You</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:30:27 +0000</pubDate>
      <link>https://dev.to/mickyarun/your-health-check-is-lying-to-you-58cc</link>
      <guid>https://dev.to/mickyarun/your-health-check-is-lying-to-you-58cc</guid>
      <description>&lt;p&gt;Vicente Reyes wrote up a deploy debugging story this week. &lt;a href="https://dev.to/highcenburg/-the-cicd-pipeline-that-was-lying-to-us-a-deploy-debugging-story-9fi"&gt;Ten small breakages stacked in series&lt;/a&gt;, each one hiding the next. Good piece. Go read it.&lt;/p&gt;

&lt;p&gt;The last act is the one I want to pick up.&lt;/p&gt;

&lt;p&gt;Two minutes after his first fully automated deploy, the site returned a 502. Nothing had crashed. The old container was gone, the new one hadn't finished booting, and he happened to refresh inside a window about one second wide.&lt;/p&gt;

&lt;p&gt;That window is in almost every Docker Compose deploy I have seen. It is in mine.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the window. It's that the obvious fix only closes half of it, and the other half fails much more quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proxy has no idea
&lt;/h2&gt;

&lt;p&gt;A static Traefik route looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;loadBalancer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That config says: send traffic to &lt;code&gt;api:3000&lt;/code&gt;. It does not say: send traffic to &lt;code&gt;api:3000&lt;/code&gt; when something is actually listening there. Traefik has no opinion on the matter. It resolves the name, opens a connection, reports whatever comes back.&lt;/p&gt;

&lt;p&gt;So every time &lt;code&gt;docker compose up -d&lt;/code&gt; stops the old container and starts the new one, there is a stretch where the proxy is confidently routing at a process that is still running migrations. Nobody hits the app during that stretch, you never find out.&lt;/p&gt;

&lt;p&gt;The fix everyone reaches for is an active health check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;loadBalancer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;
        &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
          &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
          &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5s"&lt;/span&gt;
          &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Correct, and it's where most write-ups stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hostname line is not optional
&lt;/h2&gt;

&lt;p&gt;Leave &lt;code&gt;hostname&lt;/code&gt; out and Traefik sends the check with &lt;code&gt;Host: api&lt;/code&gt;, the internal Docker service name. Anything with a host allowlist in front of it will reject that. Django gives you a &lt;code&gt;DisallowedHost&lt;/code&gt; 400. Express with a host check does the same. So does NestJS behind most gateway setups.&lt;/p&gt;

&lt;p&gt;From Traefik's side a 400 is a failed check, and a container that fails every check is permanently unhealthy.&lt;/p&gt;

&lt;p&gt;So you spend an afternoon debugging why your app is down. Your app is fine. The health check is asking the question in a form the app is contractually obliged to refuse.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;hostname&lt;/code&gt; to the real public domain. Then the check looks like real traffic, which is the whole point of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Liveness and readiness are different questions
&lt;/h2&gt;

&lt;p&gt;The advice attached to that config is usually "keep &lt;code&gt;/healthz&lt;/code&gt; trivial". No database, no auth, just return 200. That advice is right, and for a good reason: if your health endpoint touches a business dependency, a permissions change or one slow query quietly pulls the container out of rotation.&lt;/p&gt;

&lt;p&gt;But a trivial endpoint answers a trivial question. It answers &lt;em&gt;is this process alive&lt;/em&gt;. It does not answer &lt;em&gt;can this process serve a request&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Those two come apart constantly. A container that boots cleanly but can't reach its database will return 200 from a trivial check all day. Pool empty. Queue consumer never attached. The proxy sees green, sends real traffic, and instead of one second of clean 502s you get sustained 500s from a container that is reporting healthy.&lt;/p&gt;

&lt;p&gt;So split them. Liveness stays trivial and wires to your restart policy. Readiness checks what the container needs to do its job and wires to the load balancer. Collapse the two and you pick one failure mode to catch and get blindsided by the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody writes down
&lt;/h2&gt;

&lt;p&gt;This is the bit I actually sat down to write.&lt;/p&gt;

&lt;p&gt;The natural readiness check for a service that depends on Postgres is a &lt;code&gt;SELECT 1&lt;/code&gt;. Cheap, honest, and it's what I'd reach for too.&lt;/p&gt;

&lt;p&gt;Think about what it proves. It proves the pool can hand you a connection. It says nothing about whether the pool is nearly empty.&lt;/p&gt;

&lt;p&gt;Under load those diverge, and the divergence is ugly. If readiness borrows from the same pool the application serves from, then when that pool is close to exhausted the check either succeeds by taking the last free connection, which is the moment you least wanted a health probe competing with real requests, or it fails outright because there was nothing left to take.&lt;/p&gt;

&lt;p&gt;The second one is what hurts.&lt;/p&gt;

&lt;p&gt;A container fails readiness. The load balancer pulls it. Its traffic redistributes to its neighbours. Those neighbours now carry more load, their pools saturate, they fail readiness too, and out they go.&lt;/p&gt;

&lt;p&gt;You have built a check whose entire job is to protect the system, and under exactly the conditions it was meant to protect against, it removes the system.&lt;/p&gt;

&lt;p&gt;Three cheap ways out, pick one:&lt;/p&gt;

&lt;p&gt;Give readiness a dedicated connection outside the application pool. Then a saturated pool is a slow app rather than an unhealthy one.&lt;/p&gt;

&lt;p&gt;Or keep it on the shared pool and be honest about what it asserts, which is "Postgres is reachable from this container". Fine thing to check, as long as nobody downstream believes it means "has capacity".&lt;/p&gt;

&lt;p&gt;Either way, never let a single failed probe evict. Require consecutive failures over a window so a momentary squeeze can't cascade.&lt;/p&gt;

&lt;p&gt;The general principle is that a readiness check consuming the same scarce resource it measures will always give you its worst answer at the worst possible moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Readiness is a dependency graph you're signing up for
&lt;/h2&gt;

&lt;p&gt;Everything you put in a readiness check becomes something that can take you out of rotation.&lt;/p&gt;

&lt;p&gt;Put a downstream HTTP call in there and you have coupled your availability to somebody else's. They have a bad ten minutes, every one of your containers marks itself unready simultaneously, and now you have zero healthy instances of a service that was working perfectly well.&lt;/p&gt;

&lt;p&gt;So the scoping rule is narrow. Check only what this container needs to serve its own traffic.&lt;/p&gt;

&lt;p&gt;Vicente landed on exactly this for his stack. Postgres in, Redis and Celery out, on the grounds that background jobs degrading is not the same thing as the web tier being unable to answer. Redis going down should page someone. It should not empty the load balancer.&lt;/p&gt;

&lt;p&gt;That's a design decision, not a config line, and it's worth the ten minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start-first doesn't get you to zero either
&lt;/h2&gt;

&lt;p&gt;Say you fix all of that. The window shrinks from "boot plus migrations plus static assets" down to "process fork time".&lt;/p&gt;

&lt;p&gt;It shrinks. It does not close. &lt;code&gt;docker compose up -d&lt;/code&gt; stops the old container before the new one is ready, and no health check changes that ordering.&lt;/p&gt;

&lt;p&gt;The usual next step is a start-first rollout. Bring up the new container, wait for it to pass readiness, kill the old one. The &lt;code&gt;docker-rollout&lt;/code&gt; plugin does this, Swarm and Kubernetes do it natively.&lt;/p&gt;

&lt;p&gt;The bit that gets skipped: start-first closes the window where nothing is listening and opens a different one, where the old container takes SIGTERM with requests still in flight.&lt;/p&gt;

&lt;p&gt;Two things have to be true for that second window to be harmless. The proxy has to notice the old server left and stop sending it new work before the container dies, and Traefik's config reload is not instant. And the application has to be allowed to finish what it is already holding. In gunicorn that's &lt;code&gt;graceful-timeout&lt;/code&gt;. In Node it's whatever you wired into your SIGTERM handler, and if you wired nothing, the answer is that it doesn't.&lt;/p&gt;

&lt;p&gt;A rollout without a drain window doesn't eliminate dropped requests. It moves them from the front of the deploy to the back, where they are harder to spot because the deploy reports success.&lt;/p&gt;

&lt;p&gt;If your app is anywhere near money that distinction matters a lot. A request dropped at the front is a failed connection and the client retries. A request dropped at the back was accepted, was being processed, and then wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;p&gt;The answer is different at each size, so:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single box, Compose, low traffic.&lt;/strong&gt; Split liveness and readiness, scope readiness to your own datastore, set &lt;code&gt;hostname&lt;/code&gt;, live with a health-gated couple of seconds. Completely reasonable place to stop. Reaching for an orchestrator here costs you more than the window does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple instances behind a proxy.&lt;/strong&gt; All of the above, plus a failure threshold so one bad probe can't evict, plus readiness on its own connection. This is where the cascade becomes possible, so this is where it's worth paying for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploys that cost you money when they drop requests.&lt;/strong&gt; Start-first rollout with an explicit drain. Proxy deregistration before SIGTERM, graceful timeout longer than your slowest normal request.&lt;/p&gt;

&lt;p&gt;The tools matter less than the question.&lt;/p&gt;

&lt;p&gt;Every health check is an assertion, and the failure is always the same one. The assertion is narrower than what everyone downstream believes it to be. Somebody reads a trivial 200 as "can serve". Somebody reads a &lt;code&gt;SELECT 1&lt;/code&gt; as "has capacity". Somebody reads a green deploy as "nobody got a 500".&lt;/p&gt;

&lt;p&gt;Write down what your check actually proves. Then go and find out who is relying on it to prove more than that.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks to &lt;a href="https://dev.to/highcenburg"&gt;Vicente G. Reyes&lt;/a&gt; for the original write-up and for a good argument in the comments. Most of this is that argument.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm Arun, CTO and co-founder at Atoa. We build open banking payments for the UK. I write about the messy parts of running engineering systems. &lt;a href="https://x.com/mickyarun" rel="noopener noreferrer"&gt;@mickyarun&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>traefik</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Your Agent's Guardrails Can't See the Money</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Fri, 21 Aug 2026 10:53:11 +0000</pubDate>
      <link>https://dev.to/mickyarun/your-agents-guardrails-cant-see-the-money-35f</link>
      <guid>https://dev.to/mickyarun/your-agents-guardrails-cant-see-the-money-35f</guid>
      <description>&lt;p&gt;There's a post going round this week about agent guardrails that opens with a good story. The author's agent wanted to force-push to main. Not because it was confused. The rebase was stuck, force-pushing would unstick it, and every step in that chain of reasoning was sound. Locally correct, non-locally expensive.&lt;/p&gt;

&lt;p&gt;What makes that example teachable is that the command carries its own consequence. &lt;code&gt;git push --force origin main&lt;/code&gt; has the danger written into it. You can pattern-match it. You can put it on a list. A hook can read the string and stop.&lt;/p&gt;

&lt;p&gt;Most of what I worry about doesn't look like that at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two calls, one of them a disaster
&lt;/h2&gt;

&lt;p&gt;Here are two refund calls.&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;await&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pay_9f2c14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GBP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;paymentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pay_9f2c14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GBP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer_request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One is forty pounds. The other is forty thousand. Same endpoint, same argument names, same shape, same reason code, same everything a pre-execution hook can read off the text.&lt;/p&gt;

&lt;p&gt;Now the part that actually matters: neither of them is inherently wrong.&lt;/p&gt;

&lt;p&gt;Forty thousand might be a perfectly good refund against a perfectly good invoice. Forty might be a refund on a payment that was already refunded an hour ago, which in some ways is the worse of the two. You cannot sort these by looking at them, because the thing that makes one of them a mistake isn't in the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blast radius is a property of the target, not the command
&lt;/h2&gt;

&lt;p&gt;The force-push case works because the danger lives in the verb. Force-push is dangerous in nearly every context. The set of situations where you genuinely want it is small enough to enumerate, so a rule can cover it and the rule stays roughly true.&lt;/p&gt;

&lt;p&gt;An amount-bearing API call is dangerous as a property of the state on the other side. Whether that refund is safe depends on things the caller simply doesn't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has this payment already been refunded, fully or partially?&lt;/li&gt;
&lt;li&gt;Does the merchant's balance cover it, or does this pull them negative?&lt;/li&gt;
&lt;li&gt;How much has this actor already moved today?&lt;/li&gt;
&lt;li&gt;Is the original payment under dispute, where a refund does something quite different to who ends up liable?&lt;/li&gt;
&lt;li&gt;Is &lt;code&gt;pay_9f2c14&lt;/code&gt; even this merchant's payment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hook sitting in the agent process knows none of this. It can read intent. Cost is not a property of intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Fine, have the hook go and check"
&lt;/h2&gt;

&lt;p&gt;This is the obvious next move and it isn't stupid. Let the guard call the API, fetch the payment, look at the balance, then decide.&lt;/p&gt;

&lt;p&gt;Try building it and you find out what you signed up for.&lt;/p&gt;

&lt;p&gt;Your hook now needs credentials to read payment state, which means the agent host is holding read access to your ledger in order to protect you from the agent host. It needs to understand refund semantics, dispute states and settlement timing well enough to form a judgement, so your refund rules now live in two codebases that have to agree forever. And it has a window between checking and executing, which is exactly where the interesting failures live. The balance was fine when you looked. Something else landed. Yours goes through anyway.&lt;/p&gt;

&lt;p&gt;You haven't built a guardrail. You've built a second, worse copy of your authorisation service, running in the least trusted process you own, with a cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the check where the state is
&lt;/h2&gt;

&lt;p&gt;The version that survives contact is boring. The check goes at the rail, at the point of authorisation, because that's the only place that knows what this actor may move, what it has already moved, and what the target looks like right now.&lt;/p&gt;

&lt;p&gt;We're regulated, so we already had to have that spine. Every movement of money goes through an authorisation step with the full picture and an audit record on the other side. Wiring agents into it didn't mean building something new. It mostly meant resisting the urge to build something &lt;em&gt;in front&lt;/em&gt; of it and call that safety.&lt;/p&gt;

&lt;p&gt;The agent-side hook still has a job. Just a smaller one than people want to give it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The hook classifies and asks. It does not decide.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preToolUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;MOVES_MONEY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&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;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Ask the system that owns the state what this would actually cost.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preflight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&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;confirmWithHuman&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effect&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;rail.preflight&lt;/code&gt; is where the work happens, and it lives on the side that has the ledger. Its whole purpose is so a human sees &lt;em&gt;"this refunds £40,000 against an invoice already refunded in full on the 3rd"&lt;/em&gt; rather than &lt;em&gt;"the agent would like to call refund."&lt;/em&gt; One of those is a decision. The other is a rubber stamp with extra steps.&lt;/p&gt;

&lt;p&gt;It's also explicitly advisory. Between preflight and execute the world moves. Enforcement stays server side, under a mandate scoped to an amount, a payee and a clock. If preflight and the rail ever disagree, the rail wins.&lt;/p&gt;

&lt;p&gt;I wrote more about mandates versus API keys &lt;a href="https://dev.to/mickyarun/your-mcp-eval-checklist-has-an-auth-row-in-payments-its-the-whole-table-1n3e"&gt;in an earlier post&lt;/a&gt; if that thread interests you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable bit
&lt;/h2&gt;

&lt;p&gt;Your agent framework cannot own your safety story for anything that moves money, and it doesn't matter how good its hooks get.&lt;/p&gt;

&lt;p&gt;That's not a knock on the hooks. Command-level guards are useful and I'd rather have them than not. For destructive verbs, where the danger is in the word, they're the right tool and they'll catch real mistakes that would otherwise cost someone a weekend.&lt;/p&gt;

&lt;p&gt;They just sit at the wrong altitude for a whole class of call that reads as completely unremarkable and is defined entirely by context the caller doesn't hold.&lt;/p&gt;

&lt;p&gt;So here's the test I'd apply to any guard before trusting it. Could this exact call, byte for byte, be both correct and catastrophic depending on something the guard cannot see? If the answer is yes, that guard is a linter. Useful. Not load-bearing. Put the real check where the state lives.&lt;/p&gt;

&lt;p&gt;Has anyone landed on a decent convention for a tool declaring &lt;em&gt;"you can't infer my blast radius from my arguments, come and ask"&lt;/em&gt;? I haven't seen it in the MCP spec, and it feels like the piece that's missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>fintech</category>
      <category>security</category>
    </item>
    <item>
      <title>Your MCP Eval Checklist Has an Auth Row. In Payments It's the Whole Table.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:16:19 +0000</pubDate>
      <link>https://dev.to/mickyarun/your-mcp-eval-checklist-has-an-auth-row-in-payments-its-the-whole-table-1n3e</link>
      <guid>https://dev.to/mickyarun/your-mcp-eval-checklist-has-an-auth-row-in-payments-its-the-whole-table-1n3e</guid>
      <description>&lt;p&gt;There's a good checklist going around dev.to for vetting an MCP server before you wire it into an agent. Four things to look at. Tool surface area: how many tools, and are they atomic or coarse. Auth model: API key, OAuth, token scope. Maintenance: last commit, open issues, is anyone home. Token profile: does it dump a full document when a summary would do.&lt;/p&gt;

&lt;p&gt;It's a genuinely good checklist. I've used a version of it. For most tooling categories it's exactly the right lens.&lt;/p&gt;

&lt;p&gt;Then you point an agent at something that moves money, and three of those four rows go quiet.&lt;/p&gt;

&lt;p&gt;Not because they stop mattering. Because one of them grows until it's the only thing you're really deciding.&lt;/p&gt;

&lt;h2&gt;
  
  
  A read tool and a write tool are not the same animal
&lt;/h2&gt;

&lt;p&gt;Here's the thing that took us a while to say out loud.&lt;/p&gt;

&lt;p&gt;If an agent calls a code-search tool twice, you get the same answer twice and waste a few tokens. If it reads a git diff twice, nobody notices. Reads are safe to repeat. That's the whole reason retries are the default everywhere in the agent stack. A tool call times out, the client tries again, you move on.&lt;/p&gt;

&lt;p&gt;A payment tool call is not a read. Retry it once and you've billed someone twice.&lt;/p&gt;

&lt;p&gt;We build open banking payments. The failure that actually keeps me up isn't a hallucinated argument or a server returning a forged result. It's the boring one. The connection blips mid-checkout, the client does what clients do and retries, and now there are two payment intents where the user meant one.&lt;/p&gt;

&lt;p&gt;So the eval question for a money-moving tool isn't "what's the auth model." It's "what happens on the second call I didn't mean to make."&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency lives on the intent, not the turn
&lt;/h2&gt;

&lt;p&gt;The fix is old and unglamorous. Idempotency keys. Everyone in payments already knows them. The part people get wrong with agents is where the key lives.&lt;/p&gt;

&lt;p&gt;The instinct is to make the agent turn idempotent. Same prompt, same result. That's the wrong seam. The agent turn is fuzzy by design, and you don't want it to be the thing carrying the guarantee.&lt;/p&gt;

&lt;p&gt;Put the key on the payment intent. The client generates it once, before the tool is ever called, and it travels with the money, not with the conversation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The key is minted where the intent is born, not inside the agent loop.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// one per real-world payment&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GBP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;merchant_8842&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Retries of the SAME intent collapse to one charge.&lt;/span&gt;
&lt;span class="c1"&gt;// A genuinely new payment gets a new key, on purpose.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentsTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a dropped connection is harmless. The retry carries the same key, the server recognises it, and the second call returns the first result instead of moving money again. The agent can be as jittery as it likes. The guarantee sits below it, where the stakes are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read paths open, write paths gated
&lt;/h2&gt;

&lt;p&gt;The other move is to stop treating "tools" as one category.&lt;/p&gt;

&lt;p&gt;On the read side we let the agent run. Fetch balances, list transactions, pull an account's status, look up a payout. If it over-calls, it wastes tokens and we tune it later. Low blast radius, no gate.&lt;/p&gt;

&lt;p&gt;On the write side, anything that changes state or moves money goes behind a human confirmation. Not the agent confirming to itself. A person, or a service acting under an explicit, narrow mandate, in the loop before the call executes.&lt;/p&gt;

&lt;p&gt;If you're on Claude Code or a similar setup, the cheap version of this is a pre-call hook that classifies the tool and decides whether it needs a gate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Classify by side effect, not by name.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NON_RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;charge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mandate.create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;preToolUse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&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;NON_RETRYABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tool&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="nf"&gt;requireHumanApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// blocks until a person says yes&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;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;call&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// reads sail through&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point isn't the code. It's the split. Reads and writes want different defaults, and a checklist that scores a server as one thing misses that the same server can hold both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The credential is a mandate, not a key
&lt;/h2&gt;

&lt;p&gt;The auth row on the checklist usually asks whether it's an API key or OAuth. Fine question. Wrong altitude for payments.&lt;/p&gt;

&lt;p&gt;What you actually want to hand an agent is a mandate. Scoped to an amount and a payee. Time-boxed, so it expires whether or not anyone remembers to revoke it. Revocable mid-flight. And auditable after the fact, so when someone asks "why did this money move" there's a straight answer that doesn't depend on trusting the model.&lt;/p&gt;

&lt;p&gt;A key says "this caller is allowed." A mandate says "this caller is allowed to move this much, to this party, until this time, and here's the record." The second one is the only thing I'd let near a live payment rail.&lt;/p&gt;

&lt;p&gt;We already keep that audit spine for money movement, because we're regulated and there's no version of this job where you don't. The work with agents wasn't inventing it. It was extending the same discipline to tool calls, so an agent's action leaves the same trail a human's would.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, the checklist
&lt;/h2&gt;

&lt;p&gt;Keep all four rows. For a knowledge base or a code-search server, run the standard lens and move on.&lt;/p&gt;

&lt;p&gt;But the moment a tool can move money, promote one question above the rest and answer it first: is this call retryable, and if it isn't, what stops the second one? Everything else on the checklist is downstream of that.&lt;/p&gt;

&lt;p&gt;A read tool can be replayed all day. A payment tool replayed once bills a real person real money.&lt;/p&gt;

&lt;p&gt;Are you seeing any of the community MCP servers treat retryable and non-retryable tools as different classes yet, or is that still left entirely to whoever's calling them?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>agents</category>
      <category>security</category>
    </item>
    <item>
      <title>Gartner Says 40% of Apps Will Have AI Agents by December. Here's the Plumbing Nobody Puts on the Slide.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:09:14 +0000</pubDate>
      <link>https://dev.to/mickyarun/gartner-says-40-of-apps-will-have-ai-agents-by-december-heres-the-plumbing-nobody-puts-on-the-5196</link>
      <guid>https://dev.to/mickyarun/gartner-says-40-of-apps-will-have-ai-agents-by-december-heres-the-plumbing-nobody-puts-on-the-5196</guid>
      <description>&lt;p&gt;There's a number going around dev.to this week. Gartner says 40% of enterprise apps will ship a task-specific AI agent by the end of 2026. Last year it was under 5%.&lt;/p&gt;

&lt;p&gt;Every deck quotes it. Every thread argues about it. Fine.&lt;/p&gt;

&lt;p&gt;I run engineering at a UK payments company. We're FCA-authorised, SOC2, the whole regulated stack. On the side I build an open-source agent framework called Bodhiorchard, where a dozen agents do real work on a real codebase. So I've shipped the thing the slide is describing. And I can tell you the 40% isn't the hard part.&lt;/p&gt;

&lt;p&gt;The hard part is everything under the slide. And the biggest piece of it is money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo is not the deployment
&lt;/h2&gt;

&lt;p&gt;An agent demo is easy. You give it a prompt, it writes some code or drafts a report, everyone claps.&lt;/p&gt;

&lt;p&gt;Then you try to put it near a live system and the questions start. What can this thing actually call? What happens when it's confidently wrong? Who gets paged?&lt;/p&gt;

&lt;p&gt;None of that shows up in a projection. All of it shows up in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real boundary isn't "no money." It's a signed mandate.
&lt;/h2&gt;

&lt;p&gt;The reflex guardrail is to forbid the scary thing. Don't let the agent move money. Read-only, propose-only, a human clicks the button.&lt;/p&gt;

&lt;p&gt;That reflex is already history. Agents are going to move money, because paying is half of what it means to finish a task. An agent that can research a supplier, compare options and fill a cart, then stops and waits for a human to tap "confirm," isn't an agent. It's an intern with a shopping list.&lt;/p&gt;

&lt;p&gt;This is the part of the 40% that actually rewires the economy. Not agents writing code. Agents that transact. The moment an agent can pay, it stops being an assistant and becomes an economic actor, and the rails for that are being built right now.&lt;/p&gt;

&lt;p&gt;That's what we're building at Atoa: &lt;a href="https://paywithatoa.co.uk/agentic-payments/" rel="noopener noreferrer"&gt;a regulated bank rail for AI agents&lt;/a&gt;. An agent settles a payment bank to bank, over an FCA-authorised rail, with a signed record on every move. Built for the protocols this is standardising on, AP2 for mandates, x402 for pay-per-request, MCP as the interface. So "never let it touch money" was never going to be our answer. The answer is where the authority lives.&lt;/p&gt;

&lt;p&gt;An agent's permission to spend can't be a line in a system prompt that a clever input talks its way around. It has to be a signed, scoped mandate: explicit about how much, to whom, within what limits, and verifiable on its own. Every payment gets a real-time affordability check. Every payment leaves a signed, provable record, who authorised it, what the funds check returned, where it settled.&lt;/p&gt;

&lt;p&gt;That's the shift worth internalising. The boundary moved from "can the agent act?" to "is the agent's authority explicit, scoped, and provable?" Money moving for an autonomous agent needs more oversight, not less. So you make the authority a hard artifact and the audit trail non-optional, and then you let it pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context in, not hope in
&lt;/h2&gt;

&lt;p&gt;Here's a real one from Bodhiorchard.&lt;/p&gt;

&lt;p&gt;An agent was asked to produce a payout report. Left to its own reading of the task, it started building a brand new service to generate that report. The report already existed. It was about to rebuild something we already had, in a slightly different shape, as new surface area to maintain.&lt;/p&gt;

&lt;p&gt;The fix wasn't a smarter prompt. It was context. We feed agents structured context through MCP before they write a line, what we call a BUD in Bodhiorchard. Once the agent could see the existing report and the decisions behind it, it did the sane thing. It extended what was there instead of spawning a duplicate.&lt;/p&gt;

&lt;p&gt;That's the difference between an agent that helps and one that quietly grows your tech debt. Not intelligence. Context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic checks come before the probabilistic step
&lt;/h2&gt;

&lt;p&gt;An agent's output is a guess. A good guess, often. Still a guess.&lt;/p&gt;

&lt;p&gt;So the guess doesn't get to be the last word. Before anything an agent produces goes near a real code path, it runs the checks a human would have to pass. Types. Schema validation. The test suite. The design-pattern lints that encode decisions no off-the-shelf linter ships with. In the payments flow, the affordability check plays the same role: a deterministic gate the probabilistic step has to clear before anything settles.&lt;/p&gt;

&lt;p&gt;If that deterministic layer isn't there first, you haven't deployed an agent. You've deployed a very fast intern with commit access and no code review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Someone owns the failure
&lt;/h2&gt;

&lt;p&gt;This is the part nobody wants on the slide, because it's a headcount question, not a technology one.&lt;/p&gt;

&lt;p&gt;When an agent fails, it usually doesn't crash. It fails plausibly. The report looks right. The code compiles. The number is just wrong. That kind of failure needs a human owner who knows the domain well enough to smell it, and a record clean enough to trace it back.&lt;/p&gt;

&lt;p&gt;My mental model: an agent is an army of near-zero-mistake juniors. That's a gift to a senior engineer and a trap for a team without one. Enabling seniors with agents is the right move. Replacing seniors with agents is how you find out what plausible failure costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  So
&lt;/h2&gt;

&lt;p&gt;Yes, 40% of apps will probably have an agent by December. The slide will be right.&lt;/p&gt;

&lt;p&gt;But the agent isn't the work. The scoped mandate, the context feed, the deterministic gate, the human who owns the failure. That's the work. That's the 60% under the waterline.&lt;/p&gt;

&lt;p&gt;And the biggest piece of that iceberg is payments. The agent economy doesn't start when models get smarter. It starts when agents can pay, safely, over rails that were built for them. That's not a 2030 story. It's live now, and we're building one of the rails.&lt;/p&gt;

&lt;p&gt;If you're shipping an agent this year, which of those four do you already have, and which are you hoping the model handles for you?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>fintech</category>
    </item>
    <item>
      <title>AI Agents Ship Bugs When They're Blind. So We Stopped Building Blind.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Fri, 31 Jul 2026 06:51:09 +0000</pubDate>
      <link>https://dev.to/mickyarun/ai-agents-ship-bugs-when-theyre-blind-so-we-stopped-building-blind-42ba</link>
      <guid>https://dev.to/mickyarun/ai-agents-ship-bugs-when-theyre-blind-so-we-stopped-building-blind-42ba</guid>
      <description>&lt;p&gt;Most AI coding mistakes don't look like mistakes.&lt;/p&gt;

&lt;p&gt;They compile. The tests pass. CI is green. The diff reads fine at 6pm on a Friday. Sometimes it's a bug that surfaces two weeks later when money moves where it shouldn't. And sometimes it's not a bug at all — the agent quietly rebuilds something you already had, and you don't notice until you're maintaining two versions of the same thing.&lt;/p&gt;

&lt;p&gt;Same root cause both times. The agent couldn't see. It had my prompt. It didn't have the codebase.&lt;/p&gt;

&lt;p&gt;I'm the CTO of a payments company. We're FCA-authorised, we move real money for real merchants, and we've leaned on AI agents hard for over a year. So I've watched this happen enough to stop blaming the model for it. Let me show you exactly what I mean, because it happened to me last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The report that didn't need to exist
&lt;/h2&gt;

&lt;p&gt;We were adding multi-business support to payouts. One parent account, several businesses underneath it, each needing its own slice of the flow.&lt;/p&gt;

&lt;p&gt;The agent did the core work well. I want to be clear about that. On the multi-business payout logic itself, the code was correct. No complaints.&lt;/p&gt;

&lt;p&gt;Then I asked for a small follow-on: add callback parameters and a few dynamic fields into the payout report.&lt;/p&gt;

&lt;p&gt;The agent went and built a new reporting service. New repo, fresh scaffolding, a whole new report pipeline. It worked. It compiled, it ran, it produced exactly the report I'd asked for.&lt;/p&gt;

&lt;p&gt;One problem. We already had a payout report — the single-business one, live in production, tested against real settlements for months. The right move was to extend it to handle multiple businesses and the new fields. Instead I now had two report implementations for the same domain. One battle-tested, one freshly minted. And every future change to payout reporting would either be made twice or silently drift apart.&lt;/p&gt;

&lt;p&gt;That's not a bug. Nothing broke. It's worse in a quieter way. It's code-quality debt the agent created because it couldn't see that the thing it was building already existed.&lt;/p&gt;

&lt;p&gt;This is the part I keep coming back to. It's not only about bugs. It's about code quality. A blind agent doesn't just get logic wrong. It reinvents, it duplicates, it walks past the shared helper and writes a fourth one. Every instance of that is a small tax you pay forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a senior wouldn't have done this
&lt;/h2&gt;

&lt;p&gt;A coding agent is the best junior developer money can buy. Tireless, cheap, no ego, near-zero mistakes on routine work. An army of them at your terminal.&lt;/p&gt;

&lt;p&gt;But juniors write code. They don't ship products.&lt;/p&gt;

&lt;p&gt;A senior handed that report task would have opened with a different question. Not "how do I build a report" but "don't we already have one of these?" That instinct isn't in training data. It comes from having maintained a codebase long enough to know that a second implementation of anything is a liability, not a feature.&lt;/p&gt;

&lt;p&gt;The agent never asked that question. Not because it's dumb. Because it only had my sentence. It had no way to know the single-business report existed, where it lived, or that extending it was the whole job.&lt;/p&gt;

&lt;p&gt;So the problem stopped being "how do I get a smarter agent" and became "how do I put the judgment a senior carries in their bones somewhere the agent can read it — before it writes a line."&lt;/p&gt;

&lt;p&gt;That question is why I built &lt;a href="https://bodhiorchard.ai" rel="noopener noreferrer"&gt;Bodhiorchard&lt;/a&gt;. It's open source, Apache 2.0, self-hosted. We run it at Atoa now.&lt;/p&gt;

&lt;h2&gt;
  
  
  We stopped handing agents a prompt. We hand them a BUD.
&lt;/h2&gt;

&lt;p&gt;The unit of work in Bodhiorchard isn't a Jira ticket. It's a &lt;strong&gt;BUD&lt;/strong&gt; — a Business Understanding Document. One living source of truth for a feature: what it is, why it exists, who asked for it, the design, the tech spec, the decisions made along the way. Vector-indexed, full history, no scatter across Confluence and Slack and someone's memory.&lt;/p&gt;

&lt;p&gt;The point of a BUD is that the context is attached to the work, not floating around it. And crucially, Bodhiorchard traces a feature down to the actual code that implements it. A feature isn't a paragraph. It's a paragraph wired to the files, services, and procedures that make it real.&lt;/p&gt;

&lt;p&gt;Work moves through seven phases, each its own tab on the BUD:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Requirements → Design → Tech Spec → Development → Code Review → Testing → Prod&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every phase has an agent. Every phase also has a toggle. Flip the agents off and it says "you're driving this BUD" — human in the loop, by design. This was never meant to run while you sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same task, inside Bodhiorchard
&lt;/h2&gt;

&lt;p&gt;So I gave it the same job. Add callback params and dynamic fields to the payout report. Same class of model. Same one-line request.&lt;/p&gt;

&lt;p&gt;This time it didn't start blind. The request came in against the payout feature's BUD, and because the BUD is wired to code, the agent pulled the context over MCP and found the existing single-business report — the real procedure, in the real repo — before it wrote anything.&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="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;context&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;writing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;finds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;already&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exists&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/mcp&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;get_bud_context(bud_id:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAYOUTS-217"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"feature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Payouts — multi-business support"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"add callback params + dynamic fields to the payout report"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"precedent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"report"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"services/reporting → PayoutReport (single-business, live in prod)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"guidance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"extend this. do not stand up a second report pipeline."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"domain_rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"One report implementation per domain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Reporting reads the ledger projection, never writes to it"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It extended the existing report. Multi-business, dynamic fields, callback params, added to the code that was already there. No new service. No second pipeline. One report that now does more.&lt;/p&gt;

&lt;p&gt;The difference wasn't intelligence. It was sight. One agent could see the codebase and one couldn't.&lt;/p&gt;

&lt;p&gt;And notice what the agent gets to &lt;em&gt;do&lt;/em&gt; versus what it doesn't. The MCP write tools are bounded to the creative phases — it can create and update a BUD, draft the spec, propose the code. Nothing that moves money is a tool an agent can call directly. Read and propose across everything. Execute nothing with a blast radius. That line is drawn on purpose.&lt;/p&gt;

&lt;p&gt;Behind the context sits a mechanical gate. Design-pattern lints run in CI, and if an agent reaches for a banned pattern or stands up a duplicate where an ADR says there should be one, the build fails. Not "a reviewer might catch it." The gate fails, every time. And the test suite leans hard on the negative cases agents love to skip — the illegal state transition, the duplicate event, the report that should have been an extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard is pointed at the thing AI is worst at
&lt;/h2&gt;

&lt;p&gt;Bodhiorchard has a scoreboard, and it does not reward output. It rewards shipped, working value.&lt;/p&gt;

&lt;p&gt;You earn points when a BUD reaches production and when your code-review quality holds above a threshold. You lose a point for a bug caught in testing, and more for one that reaches production. The payout only lands when the feature is closed end-to-end, not when the PR merges.&lt;/p&gt;

&lt;p&gt;Read that incentive again. It pays for correctness that survives production and for quality that holds up in review — exactly the things a fast, blind agent erodes when it's optimising for a green checkmark. Volume of code earns you nothing. A duplicate report that passes its own tests earns you nothing. Shipping the right thing does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest status
&lt;/h2&gt;

&lt;p&gt;I'm not going to tell you the agents run the whole SDLC while we sleep. They don't. The platform, the BUD lifecycle, the MCP context path, the feature-to-code indexing, the skill profiling — that's live and we use it daily. A fully autonomous execution loop is still being built. Today it's agent-assisted with a human in the loop at the phases that carry weight, and for anything touching payments that's exactly where I want it.&lt;/p&gt;

&lt;p&gt;Where do agents own a boundary? Reading context, finding precedent, proposing a spec, drafting code, linting against recorded decisions. Where do they never own one? Anything where the failure mode is money in the wrong account. That's not a limitation I'm apologising for. That's the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, back to blindness
&lt;/h2&gt;

&lt;p&gt;The new report was never a model failure. Neither was the duplicate-refund class of bug before it. Both came from the same place: handing something a blank prompt and hoping it would guess the code you'd already written and the decisions you'd already made.&lt;/p&gt;

&lt;p&gt;Give it the context a senior would carry into the review — the domain rules, the precedent, the boundaries, the code that already exists — and put a mechanical gate behind that context so nobody, human or agent, ships past it. The agent stops reinventing your codebase and starts extending it. It stops being a liability and starts being the army your seniors always wanted.&lt;/p&gt;

&lt;p&gt;The 80% was already solved. This is how you stop the other 20% from shipping bugs — and from quietly rotting your code quality — while you're not looking.&lt;/p&gt;

&lt;p&gt;What's the last thing an agent rebuilt in your codebase that already existed — and where was the context it needed to find it?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Arun, CTO and co-founder at &lt;a href="https://paywithatoa.co.uk" rel="noopener noreferrer"&gt;Atoa&lt;/a&gt;, building open banking payments for the UK. I built &lt;a href="https://bodhiorchard.ai" rel="noopener noreferrer"&gt;Bodhiorchard&lt;/a&gt;, it's open source, and we run it in our workflow. Repo's here if you want to poke at it — feedback over stars: &lt;a href="https://github.com/mickyarun/bodhiorchard" rel="noopener noreferrer"&gt;github.com/mickyarun/bodhiorchard&lt;/a&gt;. Find me on X &lt;a href="https://x.com/mickyarun" rel="noopener noreferrer"&gt;@mickyarun&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>mcp</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Got Obsessed With Killing Scrum. So I Built a World Out of 12 AI Agents.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Sun, 12 Jul 2026 16:09:21 +0000</pubDate>
      <link>https://dev.to/mickyarun/i-got-obsessed-with-killing-scrum-so-i-built-a-world-out-of-12-ai-agents-1hl1</link>
      <guid>https://dev.to/mickyarun/i-got-obsessed-with-killing-scrum-so-i-built-a-world-out-of-12-ai-agents-1hl1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;Bodhiorchard&lt;/strong&gt; — an open-source platform that replaces Scrum, Jira, and the team wiki with 12 specialised AI agents and a living 3D world you can actually walk around in.&lt;/p&gt;

&lt;p&gt;I call the methodology &lt;strong&gt;Agent-Driven Development (ADD)&lt;/strong&gt;. Instead of story points, planning poker, and a graveyard of stale Confluence pages, the work lives in one place: a &lt;strong&gt;BUD&lt;/strong&gt; (Business Understanding Document) — a single, vector-indexed source of truth that carries a feature from a Slack conversation all the way to production.&lt;/p&gt;

&lt;p&gt;The agents handle the busywork I never wanted humans doing: triage, design drafts, tech plans, standups, test plans, estimation, retros. Your repos show up as &lt;strong&gt;trees&lt;/strong&gt; in an orchard. Features are &lt;strong&gt;branches&lt;/strong&gt;. The whole team walks around as avatars — WASD to move, emotes, houses you upgrade with points you &lt;em&gt;only&lt;/em&gt; earn when something actually ships to prod and stays healthy.&lt;/p&gt;

&lt;p&gt;The scoreboard is the part I'm proudest of: it rewards &lt;strong&gt;quality, not output&lt;/strong&gt;. Ship a BUD to production: +1. A bug escapes to prod: -1. It scores the things AI is bad at and humans are good at.&lt;/p&gt;

&lt;p&gt;Honest status: the platform, the BUD lifecycle, the code-dependency graph, skill profiling, and the 3D world are all &lt;strong&gt;live&lt;/strong&gt;. The fully autonomous execution loop is still being built — today it's agents-assisted, human-in-the-loop. I'd rather undersell it than lie to you.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live site + walkthroughs:&lt;/strong&gt; &lt;a href="https://bodhiorchard.ai" rel="noopener noreferrer"&gt;https://bodhiorchard.ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teaser — inside the virtual world:&lt;/strong&gt; &lt;a href="https://www.youtube.com/watch?v=OxoqBI7BNxU" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=OxoqBI7BNxU&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two views ship today: &lt;strong&gt;GARDEN&lt;/strong&gt; (the orchard) and &lt;strong&gt;GRAPH&lt;/strong&gt; (a cross-repo dependency graph with Bus Factor, Threats, and BUD-stage lenses). Two Slack bots sit on top — one triages new requests and kills duplicates before they become tickets; the other answers plain-English questions like "are we on track for go-live?" straight from the live BUD.&lt;/p&gt;

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

&lt;p&gt;Open source, Apache 2.0, self-hosted — it runs on a Mac mini.&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/mickyarun" rel="noopener noreferrer"&gt;
        mickyarun
      &lt;/a&gt; / &lt;a href="https://github.com/mickyarun/bodhiorchard" rel="noopener noreferrer"&gt;
        bodhiorchard
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Bodhiorchard is the open-source, self-hosted alternative to Jira and Linear for AI-native software teams. Specialised agents draft every spec, forecast cycle times, and reason over your real repositories, commits, and pull requests — replacing story points, standups, and stale tickets so engineers can focus on building.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bodhiorchard™&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Ship software, not Scrum ceremonies.&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The open-source, self-hosted alternative to Jira &amp;amp; Scrum — AI agents run the process end-to-end, developers earn XP for what actually ships, and your data never leaves your machine.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://opensource.org/licenses/Apache-2.0" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5b60841bea9e11d9d0b0950d690c9bc554e06385634056a7d5d62a15d1a4eabe/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4170616368655f322e302d626c75652e737667" alt="License"&gt;&lt;/a&gt;
&lt;a href="https://bodhiorchard.ai/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/b1c4add6708d0181275f1bbcc876781d07daf8873dc8aa20fc5cdb264b61dd11/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f776562736974652d626f6468696f7263686172642e61692d3245374433322e737667" alt="Website"&gt;&lt;/a&gt;
&lt;a href="https://www.python.org/downloads/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/d447d7d53910a2d345c72845df16d927da9d7cdf1063cd4861c3d0b5eefea808/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31322b2d626c75652e737667" alt="Python 3.12+"&gt;&lt;/a&gt;
&lt;a href="https://vuejs.org" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/4c10a2c0485e8ee3d7840c06fd105d8f68aa0acf869cba42709a72f1d1edec98/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5675652e6a732d332d3446433038442e737667" alt="Vue 3"&gt;&lt;/a&gt;
&lt;a href="https://www.docker.com" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/f3cb62319dfd84f4edab1d619854d1d3bb2849bba63d58ff4b97c41623c40efd/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f636b65722d72656164792d3234393645442e737667" alt="Docker"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://bodhiorchard.ai/" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; · &lt;a href="https://github.com/mickyarun/bodhiorchard#quick-start" rel="noopener noreferrer"&gt;Quick Start&lt;/a&gt; · &lt;a href="https://github.com/mickyarun/bodhiorchard#why-bodhiorchard" rel="noopener noreferrer"&gt;Why Bodhiorchard&lt;/a&gt; · &lt;a href="https://github.com/mickyarun/bodhiorchard#the-twelve-agents" rel="noopener noreferrer"&gt;The Twelve Agents&lt;/a&gt; · &lt;a href="https://github.com/mickyarun/bodhiorchard#documentation" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; · &lt;a href="https://github.com/mickyarun/bodhiorchard#faq" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://bodhiorchard.ai/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmickyarun%2Fbodhiorchard%2FHEAD%2Fdocs%2Fimages%2Fboard-ui.webp" width="85%" alt="The BUD board — every feature tracked from backlog to production in one view"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://youtu.be/i8kZdcL1bME" rel="nofollow noopener noreferrer"&gt;▶ Watch the demo&lt;/a&gt;&lt;/strong&gt; — a Slack message becomes a scoped, estimated BUD. No sprints, no story points, no standups.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bodhiorchard&lt;/strong&gt; replaces sprint, scrum, and Jira ceremony with &lt;strong&gt;Agent-Driven Development (ADD)&lt;/strong&gt; — twelve specialised AI agents handle the busywork (triage, specs, estimates, test plans, retrospectives) while humans keep the decisions that matter, and developers earn XP for the work that actually reaches production. It's a &lt;strong&gt;self-hosted Jira alternative&lt;/strong&gt; for the full lifecycle: intake → spec → design → development → testing → deploy → retrospective. The data plane stays on your hardware; inference runs through your choice of agent CLI…&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/mickyarun/bodhiorchard" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;This is a solo project. Nights and weekends, on my own, because I couldn't leave it alone.&lt;/p&gt;

&lt;p&gt;The stack: &lt;strong&gt;FastAPI + Python 3.12&lt;/strong&gt; on the backend, &lt;strong&gt;Vue 3 + PlayCanvas&lt;/strong&gt; for the 3D world, &lt;strong&gt;Postgres + pgvector&lt;/strong&gt; for the BUD memory, and &lt;strong&gt;Redis&lt;/strong&gt; underneath. Inference runs through Claude Code today, with Ollama and OpenAI on the roadmap so you can bring your own model.&lt;/p&gt;

&lt;p&gt;The hard part wasn't the agents. It was the &lt;em&gt;ontology&lt;/em&gt; — deciding that a BUD, not a ticket, is the atom of work, and that everything (estimates, design, tests, the retro's estimated-vs-actual drift table) hangs off that one object. Once that clicked, the agents became small. Each one reads BUD context over a local MCP server and writes back only in the three creative phases where a human is still driving.&lt;/p&gt;

&lt;p&gt;The 3D layer looks like the toy. It's actually the point. A dependency graph as a spreadsheet is a chore. A dependency graph as an orchard you can lose an afternoon in — that's the thing that made &lt;em&gt;me&lt;/em&gt; want to open it every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Passion Angle
&lt;/h2&gt;

&lt;p&gt;I've spent years watching good engineers spend half their week on Jira instead of building. That quietly made me angry. This is the project I built to get that time back — mine first, then anyone else's who wants it.&lt;/p&gt;

&lt;p&gt;"Build well. Then go outside." That's the whole idea.&lt;/p&gt;

&lt;p&gt;Bodhi means awakening. An orchard is a grove you tend on purpose. The name is the pitch.&lt;/p&gt;

&lt;p&gt;If you've ever felt sprints were broken and wanted to do something about it instead of just complaining in retro — clone it, break it, tell me where I'm wrong. That feedback is the only prize I'm actually chasing.&lt;/p&gt;

</description>
      <category>weekendchallenge</category>
      <category>devchallenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Are We in an AI Bubble? We've Built This Exact One Twice Before.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Thu, 09 Jul 2026 13:37:41 +0000</pubDate>
      <link>https://dev.to/mickyarun/95-of-the-fiber-was-never-used-ai-is-digging-the-same-grave-bigger-2bei</link>
      <guid>https://dev.to/mickyarun/95-of-the-fiber-was-never-used-ai-is-digging-the-same-grave-bigger-2bei</guid>
      <description>&lt;p&gt;Railroads and dark fiber ran this exact playbook. Both times the technology won and the builders died. Here is why the AI build-out looks identical, and why your RAM got more expensive because of it.&lt;/p&gt;

&lt;p&gt;I build on this stuff for a living. My company runs on cloud infrastructure. We pay for AI tokens every month. And a few weeks ago I looked at a quote to add memory to a machine and did a double take. The price had nearly doubled in a year.&lt;/p&gt;

&lt;p&gt;That RAM quote is where this story starts. Because the reason memory got expensive is the same reason people keep asking if AI is a bubble. And to answer that honestly, you cannot just look at AI. You have to look at the two times we did almost exactly this before.&lt;/p&gt;

&lt;p&gt;The pattern is older than the internet&lt;/p&gt;

&lt;p&gt;Infrastructure bubbles all rhyme. Someone spots a genuinely world-changing technology. Money floods in. And then comes the move that defines every one of these episodes: companies build capacity far ahead of real demand, betting that demand will show up to fill it.&lt;/p&gt;

&lt;p&gt;Sometimes it does. Usually it does not, at least not on the schedule the spending assumed. The gap between capacity built and capacity used is where the money dies.&lt;/p&gt;

&lt;p&gt;We have run this experiment twice at national scale. First with railroads. Then with fiber. We are now running it a third time, with data centers.&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%2Fg2enesn72c2ig93x1r6w.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%2Fg2enesn72c2ig93x1r6w.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Railroads: the original overbuild&lt;/p&gt;

&lt;p&gt;Britain went first. During the Railway Mania of the 1840s, Parliament passed 263 acts in the single year of 1846 to set up new railway companies, for routes totalling around 9,500 miles. A lot of that track was planned on the assumption that traffic would arrive later. Much of it never earned back what it cost.&lt;/p&gt;

&lt;p&gt;America did it bigger. Between 1866 and 1873, roughly 35,000 miles of new track were laid across the country, a lot of it financed on optimism and shaky debt. Then the bill came due. In the Panic of 1873, 89 of the country’s 364 railroads went bankrupt, and around 18,000 businesses failed in two years.&lt;/p&gt;

&lt;p&gt;We did not even learn from it. Twenty years later, the Panic of 1893 hit for the same reason: too much railroad, financed too loosely. By mid-1894 a quarter of all US railroads had failed, more than 40,000 miles of them.&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%2Flfu90o0i19z5ijo0pw13.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%2Flfu90o0i19z5ijo0pw13.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The technology was real. Railroads did reshape the world. But being right about the technology did not save the people who overbuilt it.&lt;/p&gt;

&lt;p&gt;Fiber: capacity nobody could use&lt;/p&gt;

&lt;p&gt;Fast forward a century. During the late-1990s telecom boom, companies laid more than 80 million miles of fiber optic cable across the US. The pitch was a number WorldCom kept repeating: internet traffic was doubling every 100 days. It was not. Real traffic was roughly doubling once a year, which is fast, but nowhere near the story the spending was built on.&lt;/p&gt;

&lt;p&gt;So what happened to all that glass? Barely any of it got used. By some estimates less than 5% of the fiber laid in the boom was ever lit. Even four years after the bubble burst, 85% to 95% of it was still sitting dark. The industry literally invented a name for capacity built and never used: dark fiber.&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%2F5yz2f3fg6wf0zpi3m2gc.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%2F5yz2f3fg6wf0zpi3m2gc.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The companies that laid it mostly did not survive to enjoy it. Global Crossing raised billions to wire the planet, then filed for bankruptcy in January 2002 with $12.4 billion of debt and thousands of miles of unused cable, after its chairman had quietly sold more than $700 million of his own stock. 360networks completed a $900 million IPO and was bankrupt about fourteen months later. WorldCom went down in July 2002 as the largest bankruptcy in US history at the time, $107 billion in assets, held up by roughly $11 billion of accounting fraud. Corning, which made the actual glass, fell from nearly $100 a share in 2000 to about $1 in 2002. All in, telecom stocks lost more than $2 trillion in value.&lt;/p&gt;

&lt;p&gt;And the purest version of the madness was next door, in dot-coms. Pets.com went public in February 2000 having booked $619,000 of revenue while spending $11.8 million on advertising. It sold products for about a third of what it paid for them. It went from IPO to switching off the lights in 268 days, its stock from $11 to 19 cents, roughly $300 million burned. It is a punchline now. At the time it was a stock people lined up to buy.&lt;/p&gt;

&lt;p&gt;Same shape as the railroads. Real technology. Genuine future. Ruinous timing.&lt;/p&gt;

&lt;p&gt;AI: same script, much bigger budget&lt;/p&gt;

&lt;p&gt;Now look at what is happening today.&lt;/p&gt;

&lt;p&gt;The five biggest US cloud and AI players, Microsoft, Alphabet, Amazon, Meta and Oracle, have signalled combined capital spending of roughly $660 to $690 billion for 2026, close to double what they spent in 2025. Across the largest data center operators globally, the spend is heading toward $750 billion in a single year, and about three quarters of that, roughly $450 billion, is tied directly to AI: the chips, the servers, the buildings.&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%2F7leuqazxtj5oxq1elbc8.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%2F7leuqazxtj5oxq1elbc8.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each of these data centers costs billions. They are being built now, at full speed, for demand that is expected later. That is the tell. It is the railroad move and the fiber move, in concrete and silicon.&lt;/p&gt;

&lt;p&gt;So here is the fair question. Is the money actually coming in to justify it?&lt;/p&gt;

&lt;p&gt;Look at the two names everyone points to. OpenAI is running at roughly a $24 billion revenue rate as of early 2026, and still projecting a $14 billion loss for the year, with no profit expected before 2029 or 2030, while preparing to ask investors to value it above a trillion dollars. Anthropic has grown fast too, to around a $30 billion annual run rate.&lt;/p&gt;

&lt;p&gt;Add up the frontier labs and you are somewhere in the range of $50 to $60 billion of annual revenue. Set that against roughly $450 billion of AI infrastructure spending in a single year. The revenue is real and growing quickly. It is also nowhere near what the build-out needs to break even.&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%2Fa6xyksh98j3g64ibnpio.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%2Fa6xyksh98j3g64ibnpio.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the demand everyone is counting on, enterprise adoption, is not showing up on schedule. An MIT report last year, looking at 300 deployments and 150-plus executive interviews, found that despite $30 to $40 billion of enterprise spending on generative AI, about 95% of organisations were seeing no measurable return. Only 5% were getting real value. Everyone is building for the enterprise wave. The enterprise wave, so far, is mostly stalled pilots.&lt;/p&gt;

&lt;p&gt;This is why your laptop got more expensive&lt;/p&gt;

&lt;p&gt;Here is the part that reaches people who never touch a data center.&lt;/p&gt;

&lt;p&gt;Three companies, Samsung, SK Hynix and Micron, make over 95% of the world’s DRAM, the memory in your laptop and your phone. AI accelerators need a special, more expensive kind of memory called HBM, and it is far more profitable to make. So those three quietly shifted capacity toward HBM for the AI build-out and away from the ordinary memory the rest of us buy.&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%2Fr0tldiuw54mgqo3jm2i3.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%2Fr0tldiuw54mgqo3jm2i3.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
The result is a squeeze. Data centers now consume an estimated 70% of the memory chips produced worldwide. Prices have jumped hard. Samsung pushed a 32GB DDR5 module from about $149 to $239, a 60% jump. Contract prices for DDR5 have more than doubled. Samsung and SK Hynix are warning the shortage runs into 2027 and beyond, with customers reserving supply years ahead.&lt;/p&gt;

&lt;p&gt;That is the bubble touching your wallet. You did not buy an AI product. But the AI build-out bid up the memory in the device in your pocket, and you are paying for it anyway.&lt;/p&gt;

&lt;p&gt;So who actually won last time?&lt;/p&gt;

&lt;p&gt;This is the twist worth sitting with.&lt;/p&gt;

&lt;p&gt;When the fiber bubble burst, all that dark fiber did not vanish. It got sold off cheap. And the companies that scooped it up were not the ones that laid it. Google, Amazon and Facebook bought or leased that surplus capacity for a fraction of what it cost to build, and used it as the backbone for search, cloud and video. The infrastructure outlived the companies that funded it, and the winners were the ones who showed up after the crash with cash and a use for it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fibq7h7ch8q1u0p8wa6on.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%2Fibq7h7ch8q1u0p8wa6on.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the honest hopeful side. Demand for fiber did eventually catch up. It just took more than a decade, and it arrived long after the original investors were wiped out. The glut was real. The future was also real. Both things were true.&lt;/p&gt;

&lt;p&gt;But this time the index is not made of Pets.coms&lt;/p&gt;

&lt;p&gt;Here is the fairest objection to all of this, and it is a strong one.&lt;/p&gt;

&lt;p&gt;The companies driving today’s build-out are nothing like Global Crossing or Pets.com. The fiber and dot-com bubbles floated on businesses with thin profits, sometimes almost no revenue, that needed a constant drip of fresh capital just to breathe. When the capital stopped, they were gone in months.&lt;/p&gt;

&lt;p&gt;The names leading the AI spend are the opposite. Microsoft, Alphabet, Amazon, Meta, Nvidia and Apple are some of the most profitable companies that have ever existed. The so-called Magnificent Seven throw off something like 70% of the entire economic profit of the S&amp;amp;P 500. They are funding most of this build-out from the cash their existing businesses already generate, not from speculators who can vanish overnight. Cisco traded at 200 times earnings in 2000 on a far shakier story than any of these carry now.&lt;/p&gt;

&lt;p&gt;So no, Google is not going to evaporate the way 360networks did. These giants can be wrong about AI for years and still be standing. That is a real difference, and it matters.&lt;/p&gt;

&lt;p&gt;But look closely at what that argument actually says. It says the incumbents survive. It does not say the spending pays off. A profitable company can pour hundreds of billions into capacity that demand never fills, take the writedown, and walk away intact. The build is still an overbuild. The money is still gone. It just does not take the company down with it.&lt;/p&gt;

&lt;p&gt;And the speculative layer has not disappeared. It has moved. Instead of Pets.com, this cycle has AI pure-plays and neocloud GPU landlords raising on the promise of the future, some of them buying chips with debt against contracts that only hold if the boom holds. If there is a Global Crossing hiding in this story, it is probably not Google. It is one of the names you only started hearing two years ago.&lt;/p&gt;

&lt;p&gt;So, are we in a bubble?&lt;/p&gt;

&lt;p&gt;Put it plainly. On the evidence, yes. Capacity is being built years ahead of the revenue and the adoption that would justify it, increasingly on debt, on a demand story that has not yet arrived. That is the exact pattern that broke railroads and telecom.&lt;/p&gt;

&lt;p&gt;But a bubble does not mean everyone dies. The last two times, the technology was real and the future did arrive. It just arrived for different people than the ones who paid for it, on a slower clock than the spending assumed. The builders ate the loss. The infrastructure, and the payoff, went to whoever was still standing afterward with cash and a use for it.&lt;/p&gt;

&lt;p&gt;The technology is not the bubble. The technology is probably the most important thing many of us will build on in our careers. The bubble is the belief that demand will arrive exactly when the spending needs it to. And “this time it is actually useful” is the line every bubble tells, right before it proves that being right and being early are two separate ways to go broke.&lt;/p&gt;

&lt;p&gt;So no, I am not betting against AI. I am building on it. But when I sign off on that doubled memory quote, I remember how the last two of these ended.&lt;/p&gt;

&lt;p&gt;Railroads were real. The people who overbuilt them went bankrupt, and someone else ran the trains. Fiber was real. Corning went from $100 to $1, and Google bought the cable for scrap. Both times, the future showed up exactly as promised. It just showed up for different people than the ones who paid for it.&lt;/p&gt;

&lt;p&gt;That is the part nobody spending $450 billion this year wants to hear. The winners of the AI era may not be the names on today’s invoices. They may be whoever is standing there with cash and a use for it after the correction, buying the future at the price of scrap.&lt;/p&gt;

&lt;p&gt;Twice is a coincidence. We are about to find out if three is a pattern.&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%2Foc1hdh6d54n3ykj0vz4r.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%2Foc1hdh6d54n3ykj0vz4r.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your Types Lie the Moment They Cross a Network Hop</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:47:04 +0000</pubDate>
      <link>https://dev.to/mickyarun/your-types-lie-the-moment-they-cross-a-network-hop-577e</link>
      <guid>https://dev.to/mickyarun/your-types-lie-the-moment-they-cross-a-network-hop-577e</guid>
      <description>&lt;p&gt;A TypeScript type is a promise. And like any promise, it only holds while someone is around to keep it.&lt;/p&gt;

&lt;p&gt;That someone is the compiler. It watches your code, checks every shape, and yells the second something does not line up. It is very good at its job. But it has one hard limit nobody tells you about on day one. It stops at the edge of your process.&lt;/p&gt;

&lt;p&gt;The moment your data leaves the building, the compiler is gone. Over the network. Off a queue. Out of a webhook. Back from the database. Your type is still sitting there in the code, looking confident. But now it is just a sticky note that says "trust me." Nothing is checking it anymore.&lt;/p&gt;

&lt;p&gt;I learned this the annoying way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that had no bug
&lt;/h2&gt;

&lt;p&gt;We had two services talking to each other. One sent a payment amount. The other received it. Both sides shared the same type:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PaymentEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;amount&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="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Typed on both ends. The compiler was happy everywhere I looked.&lt;/p&gt;

&lt;p&gt;Then one day the numbers in a report were slightly off. Not crashing-off. Just wrong enough to notice. No error. No stack trace. Nothing red anywhere.&lt;/p&gt;

&lt;p&gt;Took us a while to find it. The sending service had started putting &lt;code&gt;amount&lt;/code&gt; out as a string. &lt;code&gt;"1000"&lt;/code&gt; instead of &lt;code&gt;1000&lt;/code&gt;. Somewhere upstream a value got serialized, went through a queue, and the quotes crept in.&lt;/p&gt;

&lt;p&gt;On the receiving side, TypeScript still believed &lt;code&gt;amount&lt;/code&gt; was a &lt;code&gt;number&lt;/code&gt;. Because we told it so. It never looks at the wire. So &lt;code&gt;"1000" * 100&lt;/code&gt; did something silly, and the type sat there the whole time swearing everything was fine.&lt;/p&gt;

&lt;p&gt;The type did not lie on purpose. It just was not there when it mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where your types are actually guessing
&lt;/h2&gt;

&lt;p&gt;Here is the thing that clicked for me. Inside one process, types are real. The compiler saw the value get made and it saw it get used. It has the whole story.&lt;/p&gt;

&lt;p&gt;But the second data crosses a boundary, your type is a guess. A hopeful one. These are the spots where it is guessing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything you &lt;code&gt;JSON.parse&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A response from another service or a third-party API&lt;/li&gt;
&lt;li&gt;A message off a queue or an event bus&lt;/li&gt;
&lt;li&gt;A webhook you did not send&lt;/li&gt;
&lt;li&gt;A row from the database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;process.env&lt;/code&gt; (every one of those is a string, even the ones you treat as numbers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In all of those, you hand TypeScript a blob of data and say "this is a &lt;code&gt;PaymentEvent&lt;/code&gt;, take my word for it." And it does. That is the whole problem. It takes your word.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is boring, and that is why it works
&lt;/h2&gt;

&lt;p&gt;You stop trusting the label and you check the actual shape at the door. Every time data comes in from outside, you parse it before you use it.&lt;/p&gt;

&lt;p&gt;We use Zod for this. You write the shape once as a schema. You get two things back from it: a real runtime check, and the type.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PaymentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PaymentEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;PaymentEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then at the boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PaymentEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;amount&lt;/code&gt; shows up as &lt;code&gt;"1000"&lt;/code&gt;, this throws right there, at the door, with a clear message. Not three services later in a wrong report. The bad data never gets in.&lt;/p&gt;

&lt;p&gt;And look at the last line of the schema. The type comes &lt;em&gt;from&lt;/em&gt; the check. You do not write the type by hand and hope the data matches it. The check is the source of truth, and the type just follows along. They cannot drift apart, because they are the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule we live by now
&lt;/h2&gt;

&lt;p&gt;Trust types inside your own process. That is what they are for, and they are great at it.&lt;/p&gt;

&lt;p&gt;The moment data comes from somewhere else, treat the type as a wish until you have checked it. Parse at every boundary. Build the type from the parser, not the other way round.&lt;/p&gt;

&lt;p&gt;A type tells you what you meant. A parse tells you what you got. On a payments system, the gap between those two is real money.&lt;/p&gt;

&lt;p&gt;So next time a value crosses a wire and lands in a nice typed variable, ask one question: who actually checked this? If the answer is "the compiler," the compiler already went home.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>node</category>
      <category>nestjs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
