<?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: Dylan Merigaud</title>
    <description>The latest articles on DEV Community by Dylan Merigaud (@dylanmerigaud).</description>
    <link>https://dev.to/dylanmerigaud</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%2F1187075%2F724be4e5-1241-485b-859d-9472a2f0e89c.jpeg</url>
      <title>DEV Community: Dylan Merigaud</title>
      <link>https://dev.to/dylanmerigaud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dylanmerigaud"/>
    <language>en</language>
    <item>
      <title>I Built 30 Programmatic Pages. Google Indexed Zero.</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Fri, 28 Aug 2026 17:38:59 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/i-built-30-programmatic-pages-google-indexed-zero-2h6f</link>
      <guid>https://dev.to/dylanmerigaud/i-built-30-programmatic-pages-google-indexed-zero-2h6f</guid>
      <description>&lt;p&gt;&lt;em&gt;I built one landing page per US city for RentalReels, thirty of them. Six months after the pages went live, Google had indexed zero of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;RentalReels turns a listing's photos into a video walkthrough. Early on I built one landing page per US city: &lt;code&gt;/cities/miami&lt;/code&gt;, &lt;code&gt;/cities/austin&lt;/code&gt;, thirty of them, each opening with a line like "turn your Miami Airbnb listing into a video tour" and then the same pitch, the same pricing, the same FAQ underneath. Standard programmatic SEO. The shape every pSEO guide tells you to ship.&lt;/p&gt;

&lt;p&gt;Six months after the pages went live, Google had indexed zero of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Search Console actually showed
&lt;/h2&gt;

&lt;p&gt;The coverage report, read 2026-08-23, labeled the thirty city pages "Discovered, currently not indexed," last crawl N/A. Not deindexed. Not penalized. Never crawled at all.&lt;/p&gt;

&lt;p&gt;The proximate cause was a bug, not a policy. The sitemap listed the pages under the apex domain (rentalreels.com), the apex 307-redirects to www, and the www page that actually served content declared its own canonical back at the apex. Google had no way out of that loop: fetch the apex, get redirected, land on a page whose canonical tag says "the real one is over there," repeat. Fixed on 2026-08-23 in commit &lt;code&gt;ac1117c9&lt;/code&gt;, one host chosen (www, the domain the hosting config already served in production) and every canonical pointed the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before the crawl reached them, I measured what it would find
&lt;/h2&gt;

&lt;p&gt;With the redirect loop closed, Google was about to crawl thirty city pages for the first time in the site's history. Before that happened, I ran the served HTML through a thin-content and near-duplicate check. Measured 2026-08-25, commit &lt;code&gt;5cd74a82&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"30 pages, 1227 to 1230 tokens each, mean pairwise similarity 99.4 percent, min 98.2, max 100.0, 416 of 435 pairs at or above the doorway threshold, 30 of 30 flagged thin."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eight of the thirty (Miami, Orlando, San Diego, San Francisco, Los Angeles, Napa, Key West, Destin) were byte-identical to each other once each page's own slug was stripped out of the text. Not similar. Identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method, so the number is checkable
&lt;/h2&gt;

&lt;p&gt;The check that produced those numbers, unpacked: the module is &lt;code&gt;microsaas-kit/src/pseo&lt;/code&gt;, and this run is commit &lt;code&gt;5cd74a82&lt;/code&gt;. It reads the text actually served inside &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, not the source template, so it measures what a crawler sees, not what the code author intended.&lt;/p&gt;

&lt;p&gt;For every page: lowercase the body, strip the page's own slug from it (split the slug on hyphens, remove each resulting word wherever it occurs as a literal substring), then tokenize with &lt;code&gt;Intl.Segmenter&lt;/code&gt;'s word-boundary detection rather than a whitespace split, so a script with no spaces between words is not silently counted as one giant token. Build the set of every contiguous 3-word shingle in what is left. Compare every pair of pages with Jaccard similarity: the size of the intersection of their shingle sets divided by the size of the union. Two identical texts score 1.0. Two texts sharing no 3-word phrase score 0.0.&lt;/p&gt;

&lt;p&gt;A pair counts as a near-duplicate at 0.75 Jaccard or above, the threshold this check calls a doorway page: same sentence template, a word or two swapped. A page also fails on its own if it carries fewer than 15 tokens once its own slug is stripped, too short to be a real page whether or not it collides with anything. Both numbers are module defaults, chosen against a false-positive stress test rather than picked to make one failing case pass, and they are the same defaults the rest of the portfolio's page sets run under.&lt;/p&gt;

&lt;p&gt;The corpus was every pair among the thirty pages, 435 of them. 416 of those pairs, 95.6 percent, cleared 0.75. The eight-page byte-identical cluster is not the method finding something extreme. It is what "swap the city name, keep the rest" looks like once you actually measure it instead of eyeballing two pages side by side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there was nothing local to write
&lt;/h2&gt;

&lt;p&gt;I looked for real differentiation before deciding there wasn't any, because inventing it was the other option, and a worse one. The job runs from a listing link: a customer pastes a URL, gets a video back. Nobody visits the property. Nothing about the price, the turnaround or the edit changes between Miami and Dallas. There was exactly one sample video in the codebase and it belonged to no city. The database held no delivered order to cite per market. Writing "Gatlinburg is all cabins, we shoot for the mountain look" would have been exactly the kind of unmeasured claim this same site had spent the prior week removing from its own homepage: invented satisfaction percentages, testimonials from people who do not exist, a client logo wall for companies with no relationship to the product. A city page for a service with no local dimension has nothing honest to say that the other twenty-nine city pages don't already say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Noindex, before Google ever got there
&lt;/h2&gt;

&lt;p&gt;The decision: the thirty pages stay live and answer &lt;code&gt;noindex, follow&lt;/code&gt;. Live, because an old ad or a direct link should still land on a page that sells something, and a &lt;code&gt;noindex&lt;/code&gt; is reversible the day the offer grows an actual local dimension. Noindex, because letting Google index thirty copies of the same page right after its first real crawl of them is how a scaled-content flag gets earned, not avoided.&lt;/p&gt;

&lt;p&gt;In their place, the sitemap now offers one page, &lt;code&gt;/cities&lt;/code&gt;, written once, saying directly what the thirty implied by omission: the service has no local edition. The same check run against the home page, computed on 2026-08-25: 1.6 percent similarity, 495 of its own tokens, 0 of 2 flagged thin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;This moves a denominator worth naming so a later reading of it isn't misread. The bet carries a pre-declared kill signal: if cumulative impressions on the &lt;code&gt;/cities&lt;/code&gt; pages stay under 300 by mid-November, and zero real signups arrive in the meantime, the bet dies. The same sitemap that feeds Search Console reporting went from 31 URLs to 2 the day the noindex landed. A jump in the indexed-page ratio at the next reading is that sitemap edit, not organic gain. The impressions half of the gate is untouched either way: the thirty pages produced zero impressions across the whole six months they were live, so pulling them from the sitemap removes a population that had never contributed anything the gate was counting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the number is actually for
&lt;/h2&gt;

&lt;p&gt;Every guide on programmatic SEO I have read argues from the win: here is how we shipped ten thousand pages and traffic went up forty times. None of them show the check that would have told them, before publishing, which of those pages were real. Google's own scaled-content policy, updated 2026-05-15, does not leave this ambiguous:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Scaled content abuse is when many pages are generated for the primary purpose of manipulating search rankings and not helping users. This abusive practice is typically focused on creating large amounts of unoriginal content that provides little to no value to users, no matter how it's created."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"No matter how it's created" is the part that matters here. The pages were not written badly. They were written correctly, thirty times, for a service that has exactly one thing to say. The mistake was picking the city as the axis for a page set before checking whether the city changed anything measurable about the content. It didn't, and a script that took a few minutes to write said so in one run, on text that had not yet been seen by a single crawler.&lt;/p&gt;

&lt;p&gt;I would rather find that out from my own measurement than from Google's next core update. Same instinct as &lt;a href="https://dylan.merigaud.com/blog/the-straight-through-limit" rel="noopener noreferrer"&gt;the straight-through limit&lt;/a&gt; I wrote about earlier: a threshold that only lives in someone's head is not a control, and neither is an assumption about how a page will read to a crawler. Write the check, run it before publishing, and let the number decide instead of the intent behind the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;Source: served &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; HTML of the thirty live &lt;code&gt;/cities/&amp;lt;slug&amp;gt;&lt;/code&gt; pages on rentalreels.com, measured 2026-08-25, commit &lt;code&gt;5cd74a82&lt;/code&gt;. Tool: &lt;code&gt;microsaas-kit/src/pseo&lt;/code&gt;, TypeScript, no dependency beyond &lt;code&gt;Intl.Segmenter&lt;/code&gt;. Per page: lowercase the body, strip the page's own slug (split on hyphens, remove each resulting word as a literal substring wherever it appears), segment into words with &lt;code&gt;Intl.Segmenter({granularity: "word"})&lt;/code&gt;, build the set of contiguous 3-word shingles. Pairwise score: Jaccard similarity of the two shingle sets, computed for all 435 unordered pairs among the 30 pages. Flags: any page under 15 tokens on its own; any pair at or above 0.75 Jaccard; any exact match after stripping. Thresholds are the module's defaults, not tuned for this result, documented in the same file that runs the check (&lt;code&gt;packages/microsaas-kit/src/pseo/index.ts&lt;/code&gt;). Coverage status: Search Console UI, property &lt;code&gt;sc-domain:rentalreels.com&lt;/code&gt;, read 2026-08-23.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/thirty-pages-google-indexed-zero" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>programmaticseo</category>
      <category>measurement</category>
      <category>typescript</category>
    </item>
    <item>
      <title>The hard part of an AI feature is knowing where NOT to use AI</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:53:28 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/the-hard-part-of-an-ai-feature-is-knowing-where-not-to-use-ai-56l1</link>
      <guid>https://dev.to/dylanmerigaud/the-hard-part-of-an-ai-feature-is-knowing-where-not-to-use-ai-56l1</guid>
      <description>&lt;p&gt;&lt;em&gt;A payment decision has to be exact and repeatable. So in the product I built, the money logic is deterministic code, and the agent only touches the parts where judgement is genuinely open-ended.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every AI demo right now is an agent doing everything. Point it at the problem, let it reason end to end, marvel at the trace. It demos beautifully. Then you try to put it in front of a real workflow with real money and it falls apart, because the thing that makes a demo impressive, the model deciding freely, is exactly the thing you cannot allow when the output is a payment.&lt;/p&gt;

&lt;p&gt;I spent a while building a procure-to-pay product: a vendor invoice comes in, gets extracted, matched against a purchase order, routed through an approval workflow, and reconciled. It is the kind of thing everyone now wants to put an agent on. So I did, sort of. But the interesting decision, the one that took the longest to get right, was not where to add the agent. It was where to refuse to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: a payment decision must be exact and repeatable
&lt;/h2&gt;

&lt;p&gt;A model is a probability distribution. Ask it the same question twice and you can get two answers. That is a feature when the task is fuzzy and a liability when the task is "does this $48,200 invoice match this purchase order". Matching, the approval engine, reconciliation: these have to be exact, auditable, and identical every run. So they are plain deterministic code. No model in the path. If a controller asks why this got approved, the answer is a code path they can read, not "the model felt it was fine".&lt;/p&gt;

&lt;p&gt;That sounds obvious written down. It is not how most people are building AI features right now. The default has become: agent first, and carve out the deterministic parts only when something breaks. I did the opposite. Deterministic by default, agent only where the trajectory is genuinely open-ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three places the agent actually earns its keep
&lt;/h2&gt;

&lt;p&gt;Once you hold that line, the places where AI belongs get very clear, because they are exactly the places a deterministic rule would be brittle or impossible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading the messy vendor PDF.&lt;/strong&gt; Invoices are a thousand different layouts. A parser built on regex and rules is a losing game. This is real fuzziness: vision model in, structured data out, validated against a schema. The model does the perception; code does everything downstream once the data is structured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deriving the approval workflow from the org chart.&lt;/strong&gt; Onboarding a client used to be a forward-deployed engineer reading their HR system and hand-building the approval rules. The genuinely hard part is mapping titles to signing authority: which role approves what, resolved to a real person, with the data-quality problems (a terminated manager, two people who both look like the CEO) flagged for a human. That judgement is fuzzy, so the agent makes it. But it produces a proposal, not a decision: a human reviews and edits it in plain language before anything goes live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Investigating a flagged exception.&lt;/strong&gt; When a bill trips a rule, someone has to judge it against unstructured context, notes, prior invoices, vendor history, and recommend. That is reading and reasoning over messy evidence, which is what models are good at. So the agent investigates and recommends. It does not act. A human sees the recommendation and decides.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notice the pattern in all three: the model does perception or judgement over unstructured input, and hands a proposal to either deterministic code or a human. It never holds the pen on the outcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Human in the loop is a design principle, not a disclaimer
&lt;/h2&gt;

&lt;p&gt;"Human in the loop" usually shows up as a safety label bolted onto an otherwise autonomous system. Here it is structural. Nothing posts until a person approves. The agent derives, reads, investigates, recommends, and then stops. The interesting engineering is in making that handoff good: showing the human exactly what the agent concluded and why, right where they decide, so the review is fast instead of a rubber stamp or a bottleneck.&lt;/p&gt;

&lt;p&gt;That is also the honest answer to "will AI replace this job". In a system that touches money, the agent removes the tedium (reading the PDF, drafting the workflow, triaging the exception) and leaves the decision with the person accountable for it. That is not a limitation I worked around. It is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the actual skill
&lt;/h2&gt;

&lt;p&gt;The prompt was never the hard part. Wiring a model to a task is a weekend. The hard part is the judgement about the system: which parts must be exact, which parts are genuinely open-ended, and how the two halves hand off without the fuzzy half ever making a decision the exact half should own. Get that wrong and you have an impressive demo that no finance team will ever trust. Get it right and the AI disappears into the places it belongs, and the rest is boring, auditable, correct code, which is exactly what you want when the output is someone's money.&lt;/p&gt;

&lt;p&gt;If you want to see the whole loop, an agent deriving an approval workflow from an HR system, then a real invoice routed through it with a live trace, paused for a human at the gate: the &lt;a href="https://dylan.merigaud.com/work/ledgerloop" rel="noopener noreferrer"&gt;ledgerloop case study&lt;/a&gt; has it, the &lt;a href="https://ledgerloop-eta.vercel.app/" rel="noopener noreferrer"&gt;demo is live&lt;/a&gt;, and the &lt;a href="https://github.com/DylanMerigaud/ledgerloop" rel="noopener noreferrer"&gt;source is on GitHub&lt;/a&gt;. Built with &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/where-not-to-use-ai" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>One pass of my eval bills $9.14 on the API and $0 through the CLI</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:52:47 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/one-pass-of-my-eval-bills-914-on-the-api-and-0-through-the-cli-3eof</link>
      <guid>https://dev.to/dylanmerigaud/one-pass-of-my-eval-bills-914-on-the-api-and-0-through-the-cli-3eof</guid>
      <description>&lt;p&gt;&lt;em&gt;One pass of my board eval bills $9.14 on the Anthropic API. Through Claude Code it bills $0. Same model, claude-opus-4-8.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is 27 calls, and it is not an estimate. The CLI prints a &lt;code&gt;total_cost_usd&lt;/code&gt; in its envelope: what the run would have cost on the API. It bills the subscription instead, so the number is a receipt for money nobody spent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switch fixed something better than the bill
&lt;/h2&gt;

&lt;p&gt;Running with &lt;code&gt;--output-format json&lt;/code&gt; and &lt;code&gt;--json-schema&lt;/code&gt; rides the same structured-output machinery the API does, an internal forced tool call. Format reliability on my suite went from 7 out of 15 to 15 out of 15.&lt;/p&gt;

&lt;p&gt;The schema needs relaxing first: strip &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;maxLength&lt;/code&gt;, &lt;code&gt;minItems&lt;/code&gt;, &lt;code&gt;maxItems&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt; and the &lt;code&gt;$schema&lt;/code&gt; meta-ref, because the CLI validator rejects draft-2020-12. The strict version stays in Zod on the caller side, so nothing is actually loosened, the validation just moves to where it can run.&lt;/p&gt;

&lt;h2&gt;
  
  
  One trap is worth the whole post
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; sits in the child process environment, the CLI quietly bills the API account rather than the subscription. Nothing errors. Nothing warns. The invoice arrives. It gets stripped explicitly at spawn.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is the failure mode I would look for first in anyone else's runner: the money leak is silent, and the only symptom is a bill at the end of the month for a run you believed was free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  And the limit, which matters more than the savings
&lt;/h2&gt;

&lt;p&gt;This is a dev-loop tool. Anthropic's consumer terms prohibit automated access "except when you are accessing our Services via an Anthropic API Key or where we otherwise explicitly permit it", and the commercial terms governing API use do not cover consumer subscriptions. An eval runner on my own machine is the CLI used as designed. A shipped service is not.&lt;/p&gt;

&lt;p&gt;Iterate on the CLI, ship on the API. What does one pass of your eval suite cost, and does anyone know it?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/what-one-eval-pass-costs" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>evals</category>
      <category>claude</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I built the approval gate, then put a price on it</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:52:06 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/i-built-the-approval-gate-then-put-a-price-on-it-2adg</link>
      <guid>https://dev.to/dylanmerigaud/i-built-the-approval-gate-then-put-a-price-on-it-2adg</guid>
      <description>&lt;p&gt;&lt;em&gt;In ledgerloop, a clean invoice under $1,000 posts with no human involved. I built the approval gate, then put a price on it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A company raised $30M last week to take the human out of agent payments. The expected take from someone with my background is a post defending the human. I agree with them, up to a thousand dollars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two conditions, and nothing else
&lt;/h2&gt;

&lt;p&gt;The manager gate fires on two conditions: any exception, or a clean bill over $1,000. Below that, a clean three-way match posts straight through and no one signs.&lt;/p&gt;

&lt;p&gt;From the seeded scenarios: $730 clean goes straight through. $9,360 clean still stops, because a material bill gets a human whatever the match says. A steel bar invoiced 9% over the PO stops. Invoiced 100 units, received 80, stops.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Two things exactly, and they are the load-bearing ones. Below the floor, what posts the invoice is deterministic tested code, not the model: the agent reads, investigates and proposes, it does not decide an amount. And these are seeded demo scenarios, not production traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The argument I actually want
&lt;/h2&gt;

&lt;p&gt;It is not whether humans should approve payments. It is that "a human approves payments" stops being a control the moment no one wrote down which payments.&lt;/p&gt;

&lt;p&gt;An unwritten threshold is not a policy, it is a habit, and a habit cannot be audited. The number itself is arguable and probably wrong for your business. Its existence, in code, with a reason next to it, is not.&lt;/p&gt;

&lt;p&gt;If you run AP: what is your straight-through limit, and who set it? The whole loop, an agent deriving the workflow then a real invoice routed through it, is in the &lt;a href="https://dylan.merigaud.com/work/ledgerloop" rel="noopener noreferrer"&gt;ledgerloop case study&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/the-straight-through-limit" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>ai</category>
      <category>automation</category>
      <category>architecture</category>
    </item>
    <item>
      <title>React Flow auto layout with dagre for custom, variable-size nodes</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:51:26 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/react-flow-auto-layout-with-dagre-for-custom-variable-size-nodes-12p7</link>
      <guid>https://dev.to/dylanmerigaud/react-flow-auto-layout-with-dagre-for-custom-variable-size-nodes-12p7</guid>
      <description>&lt;p&gt;&lt;em&gt;Variable-size nodes break dagre's centering, first paint flickers, and straight chains render with kinked edges. Here is the why and the fix for each.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every React Flow and dagre tutorial shows the same thing: uniform gray boxes, laid out in a neat tree, everything centered. You copy the pattern, wire it up, and it works. Then you replace the gray boxes with real cards. A title that wraps to two lines. A card with a description and one without. And the layout starts to look subtly wrong: a parent sits off-center from its children, edges bend where they should be straight, and everything flashes in the top-left corner for a frame before jumping into place.&lt;/p&gt;

&lt;p&gt;I hit all three building an approval-workflow graph for a fintech app. The nodes were cards with variable content, so none of the fixed-size assumptions held. It took a while to understand that these are three separate bugs with three separate causes. So here is each one, why it happens, and the fix. At the end: the small package where I put all of it, so you do not have to rebuild this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why dagre centers nodes off-balance (and the bounding-box fix)
&lt;/h2&gt;

&lt;p&gt;dagre centers a parent on the barycenter of its children, meaning the average of their center positions. That is correct when every child is the same size. It is visibly wrong when they are not.&lt;/p&gt;

&lt;p&gt;Concrete numbers from a graph I probed. A parent with two children, one 40px tall and one 200px tall. dagre puts the children at centers y=20 and y=180, so the parent lands at their average, y=100. But the visual middle of that group, the midpoint of the bounding box from the top of the small child to the bottom of the tall one, is y=140. The parent is 40px off from where your eye says it should be, and the taller the imbalance, the worse it gets.&lt;/p&gt;

&lt;p&gt;The fix is a post-pass on dagre's output: for every parent with two or more children, recompute its cross-axis position as the midpoint of the children's bounding box, walking deepest rank first so children settle before their parents. Join nodes get the mirrored treatment, centered on their parents' box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing the React Flow layout flicker on first render
&lt;/h2&gt;

&lt;p&gt;React Flow can only measure a node after it renders. dagre needs the sizes before it can lay out. So the naive order is: render at 0,0, measure, lay out, move. The user sees every node stacked in the corner for a frame, then the jump. There is a long-running xyflow discussion about exactly this (&lt;a href="https://github.com/xyflow/xyflow/issues/2973" rel="noopener noreferrer"&gt;xyflow/xyflow#2973&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The sequence that works: seed every node with &lt;code&gt;visibility: hidden&lt;/code&gt; so nothing paints. Wire &lt;code&gt;onNodesChange&lt;/code&gt;, or the measurements never flow back and &lt;code&gt;useNodesInitialized&lt;/code&gt; never flips. When it is true, run the layout with the measured sizes, apply positions, and flip to &lt;code&gt;visibility: visible&lt;/code&gt; in the same update.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The trap inside the trap: right after a node resizes, &lt;code&gt;node.measured&lt;/code&gt; can hold the stale value for a tick. Read sizes off the dimensions change events instead; they carry the new size at the moment it changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Straight edges in React Flow: fixing dagre curved connectors
&lt;/h2&gt;

&lt;p&gt;A chain A to B to C, single edge in and out, should read as one straight line. Instead the connector often steps down and back up (&lt;a href="https://github.com/xyflow/xyflow/issues/3218" rel="noopener noreferrer"&gt;xyflow/xyflow#3218&lt;/a&gt;). The cause: a smoothstep edge places its elbow at the midpoint between the two handles, and handles sit at node centers, so different heights mean different centers and the connector bends.&lt;/p&gt;

&lt;p&gt;For linear chains, snap the child's cross-axis center onto the source's so the run is colinear. For fan-outs and joins, anchor the elbow to the shared hub so every elbow lines up whatever the node sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fourth one, which is not a layout problem at all
&lt;/h2&gt;

&lt;p&gt;dagre runs crossing-minimisation, which is the correct thing for a general graph and the wrong thing for an approval flow. It reshuffles fan-out branches to reduce edge crossings, so the order you declared your edges in is not the order they render in.&lt;/p&gt;

&lt;p&gt;That matters when the order carries meaning. In an approval flow the branches are not interchangeable, they are a policy someone wrote in a specific sequence and will be asked to defend. So the library keeps fan-out in your declared edge order and accepts the extra crossing. Three of the four fixes here are not smarter graph theory, they are refusing a defensible default that is wrong for this domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The library: react-flow-auto-layout
&lt;/h2&gt;

&lt;p&gt;I extracted all of this into a small library. Install it with &lt;code&gt;npm i react-flow-auto-layout&lt;/code&gt;. The &lt;code&gt;useAutoLayout&lt;/code&gt; hook handles the whole measure-then-layout dance; there is also a pure layout function, plus &lt;code&gt;AlignedStepEdge&lt;/code&gt; and &lt;code&gt;withAlignedElbows&lt;/code&gt; for the hub-anchored elbows. dagre underneath, dual ESM and CJS, typed, MIT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onNodesChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onEdgesChange&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;useAutoLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sourceEdges&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The measurement dance, hidden behind one hook.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Honest limits: the elbow alignment only applies to step edges, and dagre suits the graph sizes React Flow is typically used at, tens of nodes in a few milliseconds, not thousands. The measurement pass also costs a hidden render, which is invisible at approval-flow sizes and would not be at graph-explorer sizes. I have not measured where that stops being free, so I am not going to claim a number for it.&lt;/p&gt;

&lt;p&gt;It lays out the canvas in &lt;a href="https://dylan.merigaud.com/work/approvals-ui" rel="noopener noreferrer"&gt;approvals-ui&lt;/a&gt; and the approval flow in &lt;a href="https://dylan.merigaud.com/work/ledgerloop" rel="noopener noreferrer"&gt;ledgerloop&lt;/a&gt;. &lt;a href="https://www.npmjs.com/package/react-flow-auto-layout" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · &lt;a href="https://github.com/DylanMerigaud/react-flow-auto-layout" rel="noopener noreferrer"&gt;source&lt;/a&gt; · &lt;a href="https://react-flow-auto-layout-ecru.vercel.app" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/react-flow-auto-layout-with-dagre-variable-size-nodes" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>dagre</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Enterprise fintech deals die in onboarding, and the config already exists</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:50:45 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/enterprise-fintech-deals-die-in-onboarding-and-the-config-already-exists-41h5</link>
      <guid>https://dev.to/dylanmerigaud/enterprise-fintech-deals-die-in-onboarding-and-the-config-already-exists-41h5</guid>
      <description>&lt;p&gt;&lt;em&gt;Enterprise fintech deals don't die in the demo. They die in week six of onboarding, while someone re-types the customer's approval rules into a canvas.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I spent two years inside a procurement fintech and the pattern was consistent: the product demos great, the contract gets signed, and then comes the wall. Setup that drags for weeks. Change requests every single week. Users who don't fully understand what was configured for them, so they ask instead of doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three things that cut onboarding time by 90%
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Integrating end to end with the systems the client already runs.&lt;/strong&gt; The ERP connection wasn't a checkbox: granular sync per data type, bulk imports, master data flowing both ways. Every field the client doesn't re-enter is a support ticket that never exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a v1 of their approval workflow instead of handing them a blank canvas&lt;/strong&gt;: business rules and best practices, applied to the real people pulled from their HRIS. The client reviews and adjusts a draft. Nobody designs from zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Giving clients simple tools to help themselves&lt;/strong&gt;, including a chatbot, so "how do I change this?" stopped requiring us.&lt;/p&gt;

&lt;p&gt;None of it was glamorous. All of it was product engineering aimed at time-to-first-value. A CTO building in this space told me recently that time-to-first-value, not features, is what decides procurement deals. That matches everything I saw from the inside.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow was never missing
&lt;/h2&gt;

&lt;p&gt;Watch an enterprise onboarding for any workflow product and you'll see the same ritual: a kickoff call, a shared screen, and someone rebuilding the org's approval logic box by box. Who approves above $10k. Who signs off on IT purchases. What happens when the manager is on leave.&lt;/p&gt;

&lt;p&gt;None of that information is new. It sits in the HRIS (who reports to whom, titles, departments) and in the ERP (vendors, open POs, spend history). The customer is being asked to re-enter reality the software could have read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the next iteration looks like
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dylan.merigaud.com/work/ledgerloop" rel="noopener noreferrer"&gt;ledgerloop&lt;/a&gt; is that idea taken further. An agent reads the HRIS and derives the approval workflow, each gate resolved to a real person from the org chart. Rules get you a solid draft; the genuinely fuzzy part, mapping titles to actual approval authority, is where an LLM earns its place.&lt;/p&gt;

&lt;p&gt;Dirty data gets flagged for a human before anything else: a terminated manager still listed as an approver, two people who both look like the CEO, junk records. Derived does not mean trusted.&lt;/p&gt;

&lt;p&gt;Then you maintain the workflow in plain language: "above $25k, also require CFO." You get a preview, you approve or revert. Nothing applies until a human says so. And the money path stays deterministic code: matching, thresholds, routing. The AI reads and proposes. It never decides a payment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is the 2026 twist: everyone is adding AI to the product surface. Copilots, chat, insights. Almost nobody is pointing it at onboarding. Yet "read the customer's systems, derive their configuration, have a human review it" is exactly the kind of fuzzy, one-shot, reviewable task current models are genuinely good at. The AI feature gets the meeting. Onboarding gets the renewal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today this mapping is done by hand, by forward-deployed engineers, and they are a big part of what onboarding costs. If you build or buy B2B fintech: how long from signed contract to the first real invoice or PO through the system?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/onboarding-is-where-the-deal-dies" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>onboarding</category>
      <category>ai</category>
      <category>saas</category>
    </item>
    <item>
      <title>"It's just an approval workflow" is the most expensive sentence in procurement software</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Wed, 12 Aug 2026 18:50:04 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/its-just-an-approval-workflow-is-the-most-expensive-sentence-in-procurement-software-4kn5</link>
      <guid>https://dev.to/dylanmerigaud/its-just-an-approval-workflow-is-the-most-expensive-sentence-in-procurement-software-4kn5</guid>
      <description>&lt;p&gt;&lt;em&gt;In the demo, it's three boxes: request, manager, CFO. Everyone nods. Then production shows up with questions the canvas never asked.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The questions the canvas never asked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The approver left the company last month, and the workflow still points at them.&lt;/li&gt;
&lt;li&gt;The amount lands exactly on the threshold. Above 10k goes to finance. Is 10k above 10k?&lt;/li&gt;
&lt;li&gt;The request was approved, then someone edited one line. Does the whole chain re-run, or just the delta? Who decides that?&lt;/li&gt;
&lt;li&gt;The manager is on leave and delegated their approvals. Does the delegate's own delegation count? Until when?&lt;/li&gt;
&lt;li&gt;Approval by group: any of the five? All of them? Three out of five? In what order?&lt;/li&gt;
&lt;li&gt;A condition depends on an answer given two steps earlier. That answer just changed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I spent two years shipping and maintaining an approval workflow engine at a procurement fintech. The three boxes took a sprint. The list above took the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we actually answered it
&lt;/h2&gt;

&lt;p&gt;We froze the workflow at init: conditions resolved once at launch, and a running request never re-derived them. Mid-flight edits simply didn't exist. Approval groups came straight from the teams in the HRIS. Vacations earned a proper feature, a replacement approver that applied even to workflows already running, because absence is the one thing you can't freeze. And the approver who had left the company? Fixed by hand, more often than I'd like to admit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Freezing at init isn't a hack. It's the honest trade-off: deterministic, auditable, and it quietly declines half the list above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A workflow builder is a programming language your users never asked to learn
&lt;/h2&gt;

&lt;p&gt;Every condition is syntax, every unhandled edge case is a bug they'll file. So my opinion hasn't moved: keep the engine boring, deterministic, tested code, and derive the configuration from the systems that already know the answer, editable in plain language.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://dylan.merigaud.com/work/ledgerloop" rel="noopener noreferrer"&gt;ledgerloop&lt;/a&gt; does with the HRIS, and what the components in &lt;a href="https://dylan.merigaud.com/work/approvals-ui" rel="noopener noreferrer"&gt;approvals-ui&lt;/a&gt; model directly: quorum gates, amount thresholds, and a policy lint that knows what segregation of duties means. Which one of these bit you first?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://dylan.merigaud.com/blog/its-just-an-approval-workflow" rel="noopener noreferrer"&gt;dylan.merigaud.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fintech</category>
      <category>workflow</category>
      <category>architecture</category>
      <category>product</category>
    </item>
    <item>
      <title>A rulebook of how money code breaks, and an agent that refutes its own findings</title>
      <dc:creator>Dylan Merigaud</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:10:39 +0000</pubDate>
      <link>https://dev.to/dylanmerigaud/a-rulebook-of-how-money-code-breaks-and-an-agent-that-refutes-its-own-findings-c1l</link>
      <guid>https://dev.to/dylanmerigaud/a-rulebook-of-how-money-code-breaks-and-an-agent-that-refutes-its-own-findings-c1l</guid>
      <description>&lt;h2&gt;
  
  
  Money bugs pass review
&lt;/h2&gt;

&lt;p&gt;Money bugs are rare per repository and expensive when they happen. Rounding that loses a&lt;br&gt;
cent per invoice, a webhook that captures a payment twice on retry, a cached balance that&lt;br&gt;
quietly drifts from the ledger. These do not look like bugs in review. The code compiles,&lt;br&gt;
the tests pass, and the tests pass precisely because they use round numbers and one&lt;br&gt;
currency, which is itself one of the failure modes.&lt;/p&gt;

&lt;p&gt;Most "AI code review" aimed at this problem is a single prompt that pattern-matches on&lt;br&gt;
&lt;code&gt;float&lt;/code&gt; and calls it a day. That catches the easy cases and misses the part that actually&lt;br&gt;
costs money, which is semantic and spread across files: an allocation that does not sum to&lt;br&gt;
its total, a counter updated without a lock, a tax total rounded at the wrong level.&lt;/p&gt;
&lt;h2&gt;
  
  
  Put the value in a rulebook, not a prompt
&lt;/h2&gt;

&lt;p&gt;fintech-roast is built the other way around. The product is a rulebook of 41 rules across&lt;br&gt;
10 domains: storage and types, rounding and allocation, idempotency and concurrency, ledger&lt;br&gt;
design, FX, time and dates, aggregation, taxes, API serialization, and testing. Each rule&lt;br&gt;
has per-language detection and fixes for TypeScript, Python, and Java, at least two sources&lt;br&gt;
you can check (language specs, ISO standards, tax-authority manuals, engineering write-ups&lt;br&gt;
from practitioners), and its own false-positive notes documenting where it cries wolf.&lt;/p&gt;

&lt;p&gt;A rule is a claim you can check, not a regex. So a human can read the reasoning and decide&lt;br&gt;
instead of trusting the tool. The same rule keeps working when the executor is a cheaper&lt;br&gt;
model next year. And when a rule is wrong, you can argue with the specific claim.&lt;/p&gt;

&lt;p&gt;An example, abbreviated. Rule ROU-2, pro-rata allocation that loses or creates cents:&lt;br&gt;
detect a total split by looping over shares and rounding each independently with no&lt;br&gt;
reconciliation of the residual. Why it breaks: the rounded parts do not sum back to the&lt;br&gt;
total, so a 100.00 split three ways becomes 33.33 + 33.33 + 33.33 = 99.99 and a cent&lt;br&gt;
vanishes. The fix: allocate with an explicit remainder pass (largest-remainder, or push the&lt;br&gt;
residual to the last bucket). False positives: a high-precision internal allocation that&lt;br&gt;
carries the residual forward and only rounds once at the end is fine, do not flag it.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the agent applies it
&lt;/h2&gt;

&lt;p&gt;The agent is a Claude Code plugin, read-only, running on your own session. It scans the repo&lt;br&gt;
for where money lives, then fans out one auditor subagent per domain, each pointed at that&lt;br&gt;
domain's rule file and the candidate code. Then a second agent runs whose only job is to&lt;br&gt;
refute each finding: is this display-only, a rate rather than an amount, dead code, already&lt;br&gt;
guarded a layer up, a misreading? Refuted findings are dropped before you ever see them. The&lt;br&gt;
survivors come back with a severity, a confidence tier from the verifier, the offending&lt;br&gt;
code, a fix direction, and the rule citation.&lt;/p&gt;
&lt;h2&gt;
  
  
  What happened on real code
&lt;/h2&gt;

&lt;p&gt;The eval fixtures in the repo are bugs I planted, which is useful for measuring recall but&lt;br&gt;
proves nothing about false positives on code that is mostly correct. So I ran it on real&lt;br&gt;
codebases.&lt;/p&gt;

&lt;p&gt;On Medusa, the open-source commerce platform, I pointed it at the money-core files across&lt;br&gt;
six domains, at a pinned commit. The auditors emitted 16 findings; the verifier refuted 10,&lt;br&gt;
downgraded 2 to narrower claims, and confirmed 4. The confirmed 4 are one concurrency&lt;br&gt;
cluster. Simplified, the payment capture path does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// read how much has already been captured&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;capturedAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* sum */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// guard: refuse to capture more than authorized minus already-captured&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;newCaptureAmount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;authorizedAmount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;capturedAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// insert the capture&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;captureService_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The read, the guard, and the write are not serialized. Under the default READ COMMITTED&lt;br&gt;
isolation, two concurrent captures of a 100 authorization both read captured = 0, both see&lt;br&gt;
100 remaining, both pass the guard, and both insert. Result: 200 captured against a 100&lt;br&gt;
authorization. There is no row lock, no version check, and no unique constraint to catch it.&lt;br&gt;
The same shape appears in refunds and in two promotion-budget counters. The gateway does not&lt;br&gt;
save you: the bundled Stripe provider swallows the already-captured error, so both capture&lt;br&gt;
rows persist, and each refund row carries its own idempotency key, so stripe.refunds.create&lt;br&gt;
moves real money twice. I filed the cluster upstream with a failing test that drops straight&lt;br&gt;
into Medusa's own concurrency test block; it is filed, not yet triaged, so judge it by the&lt;br&gt;
test: &lt;a href="https://github.com/medusajs/medusa/issues/16012" rel="noopener noreferrer"&gt;https://github.com/medusajs/medusa/issues/16012&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The refutations are the interesting part
&lt;/h2&gt;

&lt;p&gt;The verifier killed 10 of the 16 Medusa findings, and that is the number I would judge the&lt;br&gt;
tool on. A tool that dumps raw findings costs you an afternoon per false positive and loses&lt;br&gt;
your trust on the first wrong one.&lt;/p&gt;

&lt;p&gt;One example. An auditor flagged that Medusa stores tax rates in a single-precision &lt;code&gt;REAL&lt;/code&gt;&lt;br&gt;
column, and by the letter of the rule that is a hit. The verifier worked the whole chain&lt;br&gt;
mechanically: a float32 stored rate, read back through PostgreSQL 12+'s&lt;br&gt;
shortest-round-trip text output, parsed by node-postgres, then converted to the exact&lt;br&gt;
decimal. Every rate tested (0.21, 8.25, 9.975, 8.0625, 13.9125) round-trips exactly;&lt;br&gt;
corruption would need a rate with seven or more significant digits. The finding died on the&lt;br&gt;
rule's own test: does an inexact binary value ever actually reach the money math? The&lt;br&gt;
answer was no. That refutation used a false-positive note I had added to the rulebook the&lt;br&gt;
same day, which is the feedback loop working.&lt;/p&gt;

&lt;p&gt;All ten kills are published with their mechanisms in the field report, so a wrong kill is&lt;br&gt;
itself checkable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;86% recall on the cold scan is a best case, not proof: I planted the bugs and wrote the
answer key, the fixture only holds bug classes I already knew to write rules for, and
the scan still missed 5 of the 35. The misses are in the repo.&lt;/li&gt;
&lt;li&gt;On that fixture the verifier refuted nothing (0 of 53): every file is dense with planted
bugs, so the fixture says nothing about false-positive suppression. The kill rates on
real code are the evidence: 10 of 16 on Medusa, 14 of 36 on the private repo.&lt;/li&gt;
&lt;li&gt;The verifier is adversarial, not human. "Confirmed" means the finding survived an attack
by a second model, not that a person proved it.&lt;/li&gt;
&lt;li&gt;One of my two field writeups is on a private, anonymized codebase you cannot reproduce.
The Medusa one you can, and its issue is filed upstream, not yet accepted.&lt;/li&gt;
&lt;li&gt;No Go or Ruby yet.&lt;/li&gt;
&lt;li&gt;A whole-repo run costs real tokens on your own account (the two runs in field report 1
cost roughly 2.2M subagent tokens total). Diff mode is the cheap day-to-day run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, or argue with a rule
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add DylanMerigaud/fintech-roast
/plugin install fintech-roast@fintech-roast
/fintech-roast:roast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules are claims about how money code breaks. If one is wrong, overstated, or missing a&lt;br&gt;
jurisdiction nuance, that is the most valuable contribution you can make. The rulebook, the&lt;br&gt;
evals with their misses, and both field reports are all in the repo:&lt;br&gt;
&lt;a href="https://github.com/DylanMerigaud/fintech-roast" rel="noopener noreferrer"&gt;https://github.com/DylanMerigaud/fintech-roast&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fintech</category>
      <category>codereview</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
