<?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>Adding Pay by Bank to WooCommerce: setup, sandbox testing, and the webhook setting people miss</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Wed, 14 Oct 2026 05:51:19 +0000</pubDate>
      <link>https://dev.to/mickyarun/adding-pay-by-bank-to-woocommerce-setup-sandbox-testing-and-the-webhook-setting-people-miss-ppd</link>
      <guid>https://dev.to/mickyarun/adding-pay-by-bank-to-woocommerce-setup-sandbox-testing-and-the-webhook-setting-people-miss-ppd</guid>
      <description>&lt;p&gt;A merchant installs a gateway plugin, takes one test payment, goes live, and a few weeks later notices some orders sitting on "Pending payment". The plugin usually isn't the problem. What gets missed is the case where the customer pays but their browser never makes it back to the shop.&lt;/p&gt;

&lt;p&gt;This is how I'd add Pay by Bank to a UK WooCommerce store. Setup takes about ten minutes. The webhook section is where the time actually goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it right for your store?
&lt;/h2&gt;

&lt;p&gt;Pay by Bank sends the customer to their banking app to approve, then back to you. Compared with Apple Pay that's an extra hop, and on a £12 impulse buy it can cost you a sale. Fena's write-up on this suggests the fee difference starts to matter above roughly £100 and more so above £250 (&lt;a href="https://www.fena.co/blog/pay-by-bank-for-woocommerce-when-it-makes-sense-for-uk-merchants-%E2%80%94-and-when-it-doesnt" rel="noopener noreferrer"&gt;source&lt;/a&gt;). That fits what we see: bigger baskets, trade buyers, categories that suffer chargebacks, and mostly UK customers.&lt;/p&gt;

&lt;p&gt;I wouldn't replace cards. Offer both at checkout and let people choose. Bank payments cost about half of what cards do, and there's no chargeback route because the customer approved the payment inside their own bank's app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;You need an Atoa Business account that has finished onboarding and KYB.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Atoa Business app go to Settings → API Access and generate a &lt;strong&gt;Sandbox&lt;/strong&gt; key. Add your shop's homepage as the redirect URL.&lt;/li&gt;
&lt;li&gt;In WordPress, install the plugin from Plugins → Add New (search "Atoa Pay", slug &lt;code&gt;atoa-pay&lt;/code&gt;). It needs WordPress 6.0+ and PHP 8.0+.&lt;/li&gt;
&lt;li&gt;Go to WooCommerce → Settings → Payments, click Manage next to "Atoa – Pay by Bank App", paste the key, choose your Store Location, turn on Sandbox Mode, enable the method and save.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both block and classic checkout work. Card, Apple Pay and Google Pay can be switched on separately under the same account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;At the bank picker, choose &lt;strong&gt;Atoa Test Bank&lt;/strong&gt;. You then get a screen where you pick the result yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;COMPLETED&lt;/code&gt;: payment succeeded, the order should move to processing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAILED&lt;/code&gt;: the customer goes back to checkout to try again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PENDING&lt;/code&gt;: the bank hasn't confirmed yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run all three. &lt;code&gt;PENDING&lt;/code&gt; is the closest thing to a customer closing the tab halfway through, so it tells you the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The webhook setting people miss
&lt;/h2&gt;

&lt;p&gt;The plugin registers its webhook URL for you and re-syncs it each time you save settings. It also checks payment status 5 minutes, 30 minutes, 1 hour, 4 hours and 24 hours after checkout, in case a webhook never arrives.&lt;/p&gt;

&lt;p&gt;The problem I'd watch for: if you enable &lt;strong&gt;V2 webhook signing&lt;/strong&gt; in the Atoa Dashboard (Settings → Webhooks), the signing secret has to be pasted into the plugin's &lt;strong&gt;Webhook Signing Key&lt;/strong&gt; field. If it isn't, the docs say plugin webhooks will fail signature verification, and orders stay in Pending until one of those status checks catches up.&lt;/p&gt;

&lt;p&gt;V2 signs the raw request body with HMAC-SHA256 and sends the result in an &lt;code&gt;X-Atoa-Signature&lt;/code&gt; header. If you have your own endpoint (fulfilment, ERP sync), this is the check in PHP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ATOA_WEBHOOK_SIGNING_SECRET'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$raw&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'php://input'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// raw body, not decoded&lt;/span&gt;
&lt;span class="nv"&gt;$header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$_SERVER&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'HTTP_X_ATOA_SIGNATURE'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$given&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^v1=/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$header&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;         &lt;span class="c1"&gt;// strip the v1= prefix&lt;/span&gt;
&lt;span class="nv"&gt;$expect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$secret&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="nb"&gt;hash_equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$given&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                   &lt;span class="c1"&gt;// timing-safe compare&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// handle PAYMENTS_STATUS / REFUND_STATUS idempotently, then:&lt;/span&gt;
&lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                               &lt;span class="c1"&gt;// anything but 200 counts as a failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hash the raw body, because re-encoded JSON won't produce the same bytes. Use &lt;code&gt;hash_equals&lt;/code&gt; for the comparison. And make the handler safe to run twice: Atoa retries with exponential back-off for up to 24 hours on any non-200 response, so duplicates will happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going live
&lt;/h2&gt;

&lt;p&gt;Swap in a Production key, turn off Sandbox Mode, and make one real £1 payment from your own bank account. Check the order status changes, then work through the &lt;a href="https://docs.paywithatoa.co.uk/go-live" rel="noopener noreferrer"&gt;go-live checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The sandbox guide and API reference are at &lt;a href="https://docs.paywithatoa.co.uk" rel="noopener noreferrer"&gt;docs.paywithatoa.co.uk&lt;/a&gt;, and the plugin is on &lt;a href="https://wordpress.org/plugins/atoa-pay/" rel="noopener noreferrer"&gt;WordPress.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've had orders stick in odd states on your own store, I'd like to hear what caused it.&lt;/p&gt;

</description>
      <category>woocommerce</category>
      <category>openbanking</category>
      <category>php</category>
      <category>fintech</category>
    </item>
    <item>
      <title>I Have Four Controls for AI Agents. Each One Stops Measuring the Moment People Know It's There.</title>
      <dc:creator>arun rajkumar</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:45:24 +0000</pubDate>
      <link>https://dev.to/mickyarun/i-have-four-controls-for-ai-agents-each-one-stops-measuring-the-moment-people-know-its-there-ne7</link>
      <guid>https://dev.to/mickyarun/i-have-four-controls-for-ai-agents-each-one-stops-measuring-the-moment-people-know-its-there-ne7</guid>
      <description>&lt;p&gt;Three weeks ago I wrote that nobody checks whether the guardrail is running. Four people have spent those three weeks proving the post stopped one step too early.&lt;/p&gt;

&lt;p&gt;The post was about a control that exists and isn't executing. What they found is worse, and it arrived four separate times in four separate threads, from people who were not talking to each other. A control can be running, monitored, read by a human on a schedule, and still stop measuring the thing it was built to measure. Not because it broke. Because people learned it was there.&lt;/p&gt;

&lt;p&gt;I want to go through the four, because each one is a real instrument with real numbers attached, and each one dies the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rejection rate, broken by the person who proposed it
&lt;/h2&gt;

&lt;p&gt;The thread started with exemption approvals. Somebody has to sign off when a control gets a carve-out, and the question was how you tell a real signer from a rubber stamp.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt; made the move I'd been failing to make. Sitting outside the requester's chain is not what makes a signer real. What makes them real is bearing a cost for being wrong that is larger than the cost of asking the requester an awkward question. In payments that is why a bank's check on a mandate survives for decades and an internal second signature often does not survive a busy quarter: the bank's own money moves.&lt;/p&gt;

&lt;p&gt;So I offered the cheap instrument. Rejection rate. A signer outside the chain who has approved every exemption for a year is not a control, and you can decide that without judging any individual approval or knowing anything about the domain.&lt;/p&gt;

&lt;p&gt;He then took it apart, which is the part worth reading:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A zero rejection rate can also mean requesters learned what passes and stopped sending anything else.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not a measurement error. The signer is working. The requesters have adapted. And the number reads identically in both cases, so the instrument cannot tell a control that is deterring bad requests from a control that waves everything through.&lt;/p&gt;

&lt;p&gt;His fix is to seed. Submit a known-bad exemption quarterly and see whether it gets stopped. That separates "signer never rejects" from "nothing worth rejecting arrives", which the rate alone cannot.&lt;/p&gt;

&lt;p&gt;I'll take it, with one limit. An obviously bad exemption that gets caught tells you the obvious case is caught, which nobody doubted. The informative seed is a marginal one, close enough to the real submissions that a tired signer might wave it through. Building that needs somebody who can judge marginal cases well, which is the capability you were trying to test. The seed inherits the problem one hop back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same thing, in production, with dates on it
&lt;/h2&gt;

&lt;p&gt;While that was theoretical, &lt;a href="https://dev.to/road511"&gt;@road511&lt;/a&gt; was running it.&lt;/p&gt;

&lt;p&gt;He maintains a list of exempted data feeds — feeds allowed to produce nothing without raising an alert. In September he came back with numbers: 897 polled endpoints across 286 upstream servers, where an upstream server means one with at least one enabled resource,, re-pulled on 21 September so the figures had a date on them rather than being three days stale. Ten exemptions on the North American deployment. Zero on the EU one, and his correction on that was more useful than the thing it corrected: the EU list is not better disciplined, it is younger. Which makes the count useless as a health signal in both directions. A long list looks like rot, a zero list looks like discipline, and the length is mostly a clock.&lt;/p&gt;

&lt;p&gt;Seven of the ten shared one renewal reason. The same 153-character string, the same 13 October expiry.&lt;/p&gt;

&lt;p&gt;I suggested a dedup rule: reject a renewal whose reason matches the previous one for that row. He was careful not to promise it would ship, and said the real value was that it converts a batch sweep back into seven separate decisions.&lt;/p&gt;

&lt;p&gt;He came back on 11 October, before the date, with the result.&lt;/p&gt;

&lt;p&gt;They looked at the seven one by one. &lt;strong&gt;Three did not need an exemption at all&lt;/strong&gt; — two feeds were producing again, and a third had produced six rows during its own exemption. Four were renewed, each with its own numbers and its own date: two feeds that have published 1 and 2 items in their entire existence run to 31 December, two episodic ones run to 30 November.&lt;/p&gt;

&lt;p&gt;Nothing forced honesty. No rule fired. A date forced seven decisions, and somebody looked at them one at a time. The rule I proposed would have been the cheaper version of the thing that actually worked.&lt;/p&gt;

&lt;p&gt;And then the part that made me start writing this.&lt;/p&gt;

&lt;p&gt;I'd offered him a test: a template reason is one that doesn't name the feed it's about. A real one does. Run it against the ten this afternoon rather than waiting for October. He ran it on the result, and it passed — every renewed reason names its feed and carries its own counts, the batch sentence named none. Length says nothing, which was the other thing I'd have reached for: the honest reasons run 151 to 159 characters, and so did the batch one.&lt;/p&gt;

&lt;p&gt;The list now holds six exemptions, no two sharing a sentence.&lt;/p&gt;

&lt;p&gt;That is what a careful list looks like. It is also exactly what a list looks like once the author knows the sentence gets read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting instead of thresholds, until the rounding kills it
&lt;/h2&gt;

&lt;p&gt;A different thread, same shape. &lt;a href="https://dev.to/build996"&gt;@build996&lt;/a&gt; and I had a ratio to watch: a value an upstream reports against the value we apply, where a divergence means one side quietly moved.&lt;/p&gt;

&lt;p&gt;He wanted to alert on the ratio crossing a threshold. I argued for the step instead — a ratio that walks down over a quarter crosses any line you set on a day when nothing in particular is wrong, and by then the thing you'd compare against has been drifting the whole time.&lt;/p&gt;

&lt;p&gt;Then the actual failure. If the upstream stops returning its measured value, a sensible implementation falls back to the applied value, and the ratio pins to exactly 1.0 forever. Healthy, constant, and computed from one input divided by itself.&lt;/p&gt;

&lt;p&gt;The check with no threshold and no baseline: alert when the variance of the ratio over the window is zero and the sample size is not. A ratio built from two independently measured numbers jitters, because clocks and rounding and retries move both sides a little.&lt;/p&gt;

&lt;p&gt;His correction is better than my check:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The false positive I'd expect first isn't low traffic, it's quantisation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the upstream reports its window in whole seconds and we store ours rounded the same way, an honest ratio sits at exactly 1.0 for long stretches. And it is worse than he put it, because there is no raw value for the upstream's half. One second &lt;em&gt;is&lt;/em&gt; the measurement. Nothing downstream recovers a finer one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/_firelinks"&gt;@_firelinks&lt;/a&gt; landed on counting from a different direction entirely — a partner whose settlement calendar we're guessing at, and the question of when a guess has been tested rather than merely not yet disproved. His fix was two counts: periods matched, and periods that &lt;em&gt;could have disproved the guess&lt;/em&gt;. The second one is the whole thing. Three months where both candidate rules predict the same date test nothing.&lt;/p&gt;

&lt;p&gt;Two people, two problems, same escape route: when a threshold needs a baseline nobody recorded, count something instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word outlives the code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/rafael_asor"&gt;@rafael_asor&lt;/a&gt; has been building the cryptographic version of this, in public, with runs posted rather than claims made. A supervised process writes a chain of entries; a second key signs one of them; a reader scores that entry &lt;code&gt;witness-signed&lt;/code&gt; if the key is in a trust set the reader supplies.&lt;/p&gt;

&lt;p&gt;I asked him to falsify the obvious hole — both keys generated in one script means the witness and the subject share a custodian. He did it the same afternoon. The witness runs as its own process, generates its key in memory, and the supervised process searched 1,277 files, its own argv, its environment and its own heap for the seed, asked the witness directly, and tried signing with a borrowed key id. Four ways, all refused.&lt;/p&gt;

&lt;p&gt;The most useful result in that run is the one he called a limitation. A &lt;em&gt;forged&lt;/em&gt; allow, appended in chain order, came back &lt;code&gt;witness-signed&lt;/code&gt;. The witness attests that it received that entry, in that order, at that time. It does not attest that the action was permitted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;witness-signed&lt;/code&gt; reads, to anyone who did not write the code, as "a second party vouched for this action."&lt;/p&gt;

&lt;p&gt;He filed it as an issue, along with two others, off one comment. And the naming one is the only one of the three that gets harder with time. The other two are code. Once a word is in a changelog, people build readers on it.&lt;/p&gt;

&lt;p&gt;Payments spent real money learning this. Authorised, captured and settled are three words for what looks like one event, and they stay separate because a system that treats an authorisation as a settlement loses money in a way nobody notices for a month. No scheme lets a timestamp authority stand in for an authoriser. They are two roles with two names for exactly this reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing all four have in common
&lt;/h2&gt;

&lt;p&gt;Every control in this post is only worth what somebody independently reads of it. That part I already believed, and three of these threads are better arguments for it than the article I wrote.&lt;/p&gt;

&lt;p&gt;The part I did not have is what happens next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A visible control changes what arrives at it.&lt;/strong&gt; So each of these instruments has a shelf life rather than a false positive rate.&lt;/p&gt;

&lt;p&gt;The rejection rate worked until requesters learned what passes. The feed-naming sentence worked once, and the six distinct sentences on that list today are partly a careful author and partly an author who knows the sentence gets checked. The zero-variance check works until an upstream rounds to the second. &lt;code&gt;witness-signed&lt;/code&gt; was accurate on the day it shipped and will be misread by every reader who arrives after it.&lt;/p&gt;

&lt;p&gt;None of that is a reason to stop building them. It is a reason to date them, the way &lt;a class="mentioned-user" href="https://dev.to/road511"&gt;@road511&lt;/a&gt; dates his figures, because a figure with a date on it stays true for a useful while and a figure without one quietly stops being true. An instrument needs the same treatment. Write down when you started trusting it and what it was measuring then, or you will keep reading a number whose meaning has moved.&lt;/p&gt;

&lt;p&gt;Which gives one concrete preference over everything above. Prefer the instrument whose input comes from somewhere the measured party cannot adapt: &lt;a class="mentioned-user" href="https://dev.to/road511"&gt;@road511&lt;/a&gt;'s renewal dates carry a number from the feed, not a sentence about it. "Published 1 item ever, runs to 31 December" is checkable on 1 January by someone who has never read the reason, and the feed cannot learn to look busier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I can't finish the argument
&lt;/h2&gt;

&lt;p&gt;Reconciliation survives decades in payments, and not because it is clever. It survives because the counterparty runs their half for their own reasons and will not stop because our number is inconvenient. Our ledger and the bank's statement are two records with two authors and two sets of incentives, and the gap between them is the control.&lt;/p&gt;

&lt;p&gt;None of the agent-side controls in this post have a counterparty. The signer is in the same company. The reader supplies their own trust set. The ratio is computed from two numbers that both arrive over the same wire. The exemption list is read by the team that writes it. Every one of them is one party in two coats, wearing the second coat honestly.&lt;/p&gt;

&lt;p&gt;I don't know where the counterparty comes from for an agent. A regulator is one answer, and it's the answer I reached for in an earlier post, and it isn't available to most people reading this. The nearest thing I reached for is a published revision history that the measured party cannot quietly change. That is not a second author, but it removes the ability to rewrite the first one. To be clear about whose idea that is: it is mine, nobody in these threads proposed it, and nobody including me is running one.&lt;/p&gt;

&lt;p&gt;If you have a real second reader in an agent system — somebody outside, who checks because it costs &lt;em&gt;them&lt;/em&gt; when it's wrong — I would like to know what they are and why they bother.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/peterbuildssecure"&gt;@peterbuildssecure&lt;/a&gt;, &lt;a href="https://dev.to/road511"&gt;@road511&lt;/a&gt;, &lt;a href="https://dev.to/rafael_asor"&gt;@rafael_asor&lt;/a&gt;, &lt;a href="https://dev.to/_firelinks"&gt;@_firelinks&lt;/a&gt;, &lt;a href="https://dev.to/build996"&gt;@build996&lt;/a&gt;, &lt;a href="https://dev.to/tom_jones_230c4659491adcd"&gt;@tom_jones_230c4659491adcd&lt;/a&gt;, &lt;a href="https://dev.to/scarab-systems"&gt;@scarab-systems&lt;/a&gt;, &lt;a href="https://dev.to/alexshev"&gt;@alexshev&lt;/a&gt; and &lt;a href="https://dev.to/anp2network"&gt;@anp2network&lt;/a&gt;, who between them have taken out more of my arguments in three weeks than I managed to build.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>discuss</category>
      <category>security</category>
    </item>
    <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>
  </channel>
</rss>
