<?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: Victor Isaac</title>
    <description>The latest articles on DEV Community by Victor Isaac (@victorisaacchintaeng).</description>
    <link>https://dev.to/victorisaacchintaeng</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%2F4148108%2Fdf50d365-c40c-4b5c-86c5-bc7d5219a8fd.jpg</url>
      <title>DEV Community: Victor Isaac</title>
      <link>https://dev.to/victorisaacchintaeng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/victorisaacchintaeng"/>
    <language>en</language>
    <item>
      <title>Ten builds since July, every one dated</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:19:21 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/ten-builds-since-july-every-one-dated-1c21</link>
      <guid>https://dev.to/victorisaacchintaeng/ten-builds-since-july-every-one-dated-1c21</guid>
      <description>&lt;p&gt;This is the index for my series &lt;strong&gt;Agents with receipts&lt;/strong&gt;: everything I have built since July 2026, in order, with the date, what it was for, how it did, and where it stands today. I will keep it updated as builds finish.&lt;/p&gt;

&lt;p&gt;A thread runs through most of these: &lt;strong&gt;the model is allowed to plan and explain, but the decisions that matter are made in code.&lt;/strong&gt; Budgets, safety gates, scores and verdicts live in plain functions you can test. The receipts are the tests, the benchmarks and the limits sections.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12 Jul 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Exam Guard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exam paper fingerprints on the Cardano testnet&lt;/td&gt;
&lt;td&gt;IndiaCodeX'26, 12 hours, Team Pragmatic&lt;/td&gt;
&lt;td&gt;Did not place&lt;/td&gt;
&lt;td&gt;Done. &lt;a href="https://dev.to/victorisaacchintaeng/my-first-hackathon-build-an-exam-papers-fingerprint-on-cardano-in-12-hours-2eb3"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22 to 23 Aug 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Kitchen Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Procurement agent for institutional kitchens, with a budget it cannot override&lt;/td&gt;
&lt;td&gt;LaunchPadX 2026, SNIST Hyderabad, Team Rudra&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1st of 231 teams&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Done. &lt;a href="https://github.com/victorisaacchinta-eng/procurement-pilot" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 Sep 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Basket Buddy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-learning browser agent that keeps a notebook per site&lt;/td&gt;
&lt;td&gt;SLAB Hackathon, CMR Institute of Technology, solo, one day&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2nd place&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Done. &lt;a href="https://dev.to/victorisaacchintaeng/my-browser-agent-took-6-tries-on-blinkit-and-1-on-a-site-it-had-never-seen-7af"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Antibody&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Root cause analysis for microservices, allowed to say "insufficient evidence"&lt;/td&gt;
&lt;td&gt;Microsoft Codathon, Problem 3, team of five&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1st in college&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Done. &lt;a href="https://dev.to/victorisaacchintaeng/thousands-of-alerts-one-root-cause-and-an-engine-allowed-to-say-i-dont-know-494m"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25 to 26 Sep 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;JAMES&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Touch-free maintenance copilot with a safety gate that has no AI in it&lt;/td&gt;
&lt;td&gt;AgentX-2026, TKREC Hyderabad&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1st prize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shipped, still improving. &lt;a href="https://dev.to/victorisaacchintaeng/a-safety-gate-with-no-ai-in-it-building-james-a-touch-free-maintenance-copilot-1k0p"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;28 Sep 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Prequal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor questionnaire agent that answers from memory and never invents a certificate&lt;/td&gt;
&lt;td&gt;Team build, five people, memory on Hindsight&lt;/td&gt;
&lt;td&gt;Shipped&lt;/td&gt;
&lt;td&gt;Done. &lt;a href="https://dev.to/victorisaacchintaeng/my-agent-kept-forgetting-our-company-so-i-gave-it-hindsight-37p3"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;29 Sep 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Procurement Pilot: Bid Compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fifteen checks against ten registries; code decides, AI explains, an officer signs&lt;/td&gt;
&lt;td&gt;Smart India Hackathon 2026, SIH26100, Team Ace Azael&lt;/td&gt;
&lt;td&gt;Submitted&lt;/td&gt;
&lt;td&gt;Live demo. &lt;a href="https://dev.to/victorisaacchintaeng/code-decides-ai-explains-a-named-officer-signs-bid-compliance-for-smart-india-hackathon-189n"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18 Oct 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ithamar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Checks every row of a bank statement against its own running balance, in the browser&lt;/td&gt;
&lt;td&gt;Side build&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;td&gt;Still improving. &lt;a href="https://dev.to/victorisaacchintaeng/every-bank-statement-carries-its-own-answer-key-4k8"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21 Oct 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sapor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Group taste agent: one real place for the whole table, ranked by the person it suits least&lt;/td&gt;
&lt;td&gt;Qloo Agentic Hackathon&lt;/td&gt;
&lt;td&gt;Due 31 Oct&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Building now.&lt;/strong&gt; &lt;a href="https://dev.to/victorisaacchintaeng/rank-every-place-by-the-person-it-suits-least-building-sapor-a-group-taste-agent-1f2n"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21 Oct 2026&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ATLAS Live&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hologram studio in the browser: pull machines apart with your hands&lt;/td&gt;
&lt;td&gt;Nebius x NVIDIA Global AI Hackathon&lt;/td&gt;
&lt;td&gt;Due 30 Oct&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Building now.&lt;/strong&gt; &lt;a href="https://dev.to/victorisaacchintaeng/take-machines-apart-with-your-bare-hands-building-atlas-live-in-public-24on"&gt;Post&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  In numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10&lt;/strong&gt; builds since 12 July&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4&lt;/strong&gt; podium finishes: 1st at LaunchPadX 2026, 1st at AgentX-2026, 2nd at SLAB, 1st in college at the Microsoft Codathon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2&lt;/strong&gt; in progress right now, both due before the end of October&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;If you read one post, read &lt;a href="https://dev.to/victorisaacchintaeng/a-safety-gate-with-no-ai-in-it-building-james-a-touch-free-maintenance-copilot-1k0p"&gt;JAMES&lt;/a&gt;: it is the clearest example of a gate written in code that the model cannot talk its way past. If you like numbers, &lt;a href="https://dev.to/victorisaacchintaeng/thousands-of-alerts-one-root-cause-and-an-engine-allowed-to-say-i-dont-know-494m"&gt;Antibody&lt;/a&gt; and &lt;a href="https://dev.to/victorisaacchintaeng/every-bank-statement-carries-its-own-answer-key-4k8"&gt;Ithamar&lt;/a&gt; are the two with the most careful numbers behind them.&lt;/p&gt;

&lt;p&gt;Everything is open source on &lt;a href="https://github.com/victorisaacchinta-eng" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and the portfolio lives at &lt;a href="https://victorisaac.vercel.app" rel="noopener noreferrer"&gt;victorisaac.vercel.app&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance. Dates come from each repo's history and each event's own records.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>career</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>Rank every place by the person it suits least: building Sapor, a group taste agent</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:17:58 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/rank-every-place-by-the-person-it-suits-least-building-sapor-a-group-taste-agent-1f2n</link>
      <guid>https://dev.to/victorisaacchintaeng/rank-every-place-by-the-person-it-suits-least-building-sapor-a-group-taste-agent-1f2n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status: building now.&lt;/strong&gt; Started 21 Oct 2026 for the Qloo Agentic Hackathon. Submission due 31 Oct 2026. Live: &lt;a href="https://sapor-nu.vercel.app" rel="noopener noreferrer"&gt;sapor-nu.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Where should we go?" is the most asked and worst answered question in every group chat. Ratings tell you what is popular, not what suits these particular people. Usually the loudest person picks, and someone sits through a meal that was never for them.&lt;/p&gt;

&lt;p&gt;People are bad at describing their food taste in the abstract and very good at naming things they love. So &lt;strong&gt;Sapor&lt;/strong&gt; (Latin for taste) starts there:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everyone names a film, show or artist they love. Sapor reads the taste behind those picks with Qloo, finds one real place nearby that suits the whole table, and says what each person should order there, with the evidence for every line.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjb7wzms1dxyfqz5um7mc.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjb7wzms1dxyfqz5um7mc.gif" alt="Sapor picking a place for three people" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Rank by the person it suits least
&lt;/h2&gt;

&lt;p&gt;The interesting decision in Sapor is not the AI. It is one line of arithmetic.&lt;/p&gt;

&lt;p&gt;My first version ranked venues by Qloo's affinity score for the whole table. It picked a venue Qloo loved even when one guest matched it at 35%. Averages hide the person who is left out. So the ranking now looks at the weakest seat first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;table score = 0.60 × weakest person's match
            + 0.25 × average match
            + 0.15 × Qloo affinity for the venue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;match&lt;/strong&gt; (0 to 1) is how much of one person's Qloo taste profile, cuisines and kinds of room, the venue's own Qloo tags carry. It is not a probability that they will enjoy it, and the app says so.&lt;/li&gt;
&lt;li&gt;Qloo's affinity sits in a narrow band among nearby candidates, so at 15% it can only settle near-ties. It can never outweigh a 25-point gap in the weakest match.&lt;/li&gt;
&lt;li&gt;Ties go to the better weakest match, then the better average, then the name, so the same input always gives the same answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 35% counterexample is a unit test now, next to a strict maximin baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real run
&lt;/h2&gt;

&lt;p&gt;The sample table: Riya loves BTS and Squid Game. Arjun loves A. R. Rahman and Pushpa, and is vegetarian. Meera loves Prateek Kuhad and Before Sunrise. Coffee, in Jubilee Hills, Hyderabad.&lt;/p&gt;

&lt;p&gt;Qloo reads very different crowds behind those picks. Riya's leans Korean, Thai and Chinese, in hip rooms. Arjun's leans Indian, in comfortable rooms. Meera's leans Indian, in serene ones.&lt;/p&gt;

&lt;p&gt;The search returned 30 venues; 2 were set aside because they had no food data at all (Qloo's cafe filter sometimes returns gyms). Terra Cafe &amp;amp; Bistro won with Riya at 99, Arjun at 100 and Meera at 83: weakest 83, average 94. The runner-up had a higher Qloo affinity, but its weakest match was 74, so it lost.&lt;/p&gt;

&lt;p&gt;Then the orders, from Terra's own menu highlights in Qloo: Korean chicken bao for Riya, pav bhaji bruschetta for Arjun (vegetarian by name, flagged "confirm with staff"), paneer Korean bao for Meera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails, again in code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguity stops the agent.&lt;/strong&gt; "Dune" is a film, a book and a German techno group, each drawing a different crowd. If two candidates are close, Sapor asks instead of guessing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vegetarians are protected by name.&lt;/strong&gt; A vegetarian seat only gets dishes whose names carry no meat, fish or egg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The one-line explanations are validated.&lt;/strong&gt; Groq writes a short reason per person from the evidence alone. A validator rejects any line that addresses the reader, praises food with words not in the data, or leaks a seat letter, and falls back to a template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by design.&lt;/strong&gt; Names never leave the browser. The server sees seats as A, B and C, and Qloo only ever receives entity ids, tag ids and a place name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A full plan costs about one Qloo call per person plus one venue search, with a six-hour cache for repeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still to do before 31 Oct
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The demo video and the submission write-up&lt;/li&gt;
&lt;li&gt;More test tables, because the link from a dish name to a cuisine is my own keyword map, not Qloo data, and that is the part most likely to be wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo (MIT): &lt;a href="https://github.com/victorisaacchinta-eng/sapor" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/sapor&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo. The run above is the app's sample table, measured with live Qloo calls on 21 Oct 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My first hackathon build: an exam paper's fingerprint on Cardano in 12 hours</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:17:11 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/my-first-hackathon-build-an-exam-papers-fingerprint-on-cardano-in-12-hours-2eb3</link>
      <guid>https://dev.to/victorisaacchintaeng/my-first-hackathon-build-an-exam-papers-fingerprint-on-cardano-in-12-hours-2eb3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; 12 Jul 2026, in 12 hours at IndiaCodeX'26 (a Cardano hackathon), Team Pragmatic. &lt;strong&gt;Result:&lt;/strong&gt; did not place. &lt;strong&gt;Status:&lt;/strong&gt; finished, kept as my first build. Live: &lt;a href="https://exam-guard-omega.vercel.app" rel="noopener noreferrer"&gt;exam-guard-omega.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was my first hackathon build. It didn't win anything, and I learned more from it than from some that did, so it gets a post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Exam papers leak and get doctored. A student or an invigilator holding a PDF has no quick way to tell the official paper from an edited copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exam Guard&lt;/strong&gt;, which I built with Manasa and Samhika, does one thing: it turns the official paper into a SHA-256 fingerprint and writes that fingerprint to the Cardano blockchain. Later, anyone can upload a copy. Exam Guard hashes it again and compares. Same hash, authentic. Different hash, changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing is small
&lt;/h2&gt;

&lt;p&gt;Hashing, in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For our sample paper, "Physics Mid-Sem 2026", that gives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ee47d1dab3dd41ada1687923306bd864523bf1ac53269fb681e183e6ddc3c581
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the number printed as filled bubbles on the cover of this post, one column per character.&lt;/p&gt;

&lt;p&gt;Writing it to the chain, in Node with MeshSDK and Blockfrost on the preprod testnet. The fingerprint rides as transaction metadata under label 674, the label Cardano uses for transaction messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tx&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;Transaction&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;initiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendLovelace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;address&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChangeAddress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1000000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;674&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;exam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exam_name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;txHash&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submitTx&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;wallet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signTx&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transaction sends 1 ADA back to its own wallet, because the payment is not the point. The metadata is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell myself on 12 July
&lt;/h2&gt;

&lt;p&gt;Looking back with a few more builds behind me, the idea has three real problems, and they are more interesting than the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A hash proves integrity, not secrecy.&lt;/strong&gt; A leaked paper is still authentic. Exam Guard can tell you a copy wasn't edited; it can't tell you the paper should never have been in your hands. We pitched it partly as a leak solution, and it isn't one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It verifies files, not paper.&lt;/strong&gt; Change a single byte, re-save the PDF, even its metadata, and the hash changes. A printed paper can't be hashed at all. A real version needs a fingerprint of the content, or a signed QR code printed on every page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The chain is the least important part.&lt;/strong&gt; Most of the trust comes from who publishes the fingerprint and when. A signed record from the exam board, with a timestamp, does most of what the blockchain does here. The chain adds a public, tamper-evident timestamp, which is useful, but I should have said that plainly instead of leading with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is a reason to be embarrassed about the build. It is the honest list I now write for every project, and Exam Guard is where I started writing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ship something that works end to end, even if it is small. Ours hashed, wrote to a real testnet and verified, live, in 12 hours.&lt;/li&gt;
&lt;li&gt;Say what it does not do before a judge does. I didn't do that here; every build since has a limits section.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Repo: &lt;a href="https://github.com/victorisaacchinta-eng/exam-guard" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/exam-guard&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo. The hash above is the real fingerprint in &lt;code&gt;record.json&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cardano</category>
      <category>python</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Thousands of alerts, one root cause, and an engine allowed to say "I don't know"</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:16:15 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/thousands-of-alerts-one-root-cause-and-an-engine-allowed-to-say-i-dont-know-494m</link>
      <guid>https://dev.to/victorisaacchintaeng/thousands-of-alerts-one-root-cause-and-an-engine-allowed-to-say-i-dont-know-494m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; September 2026, for the Microsoft Codathon (Problem 3), team of five. &lt;strong&gt;Result:&lt;/strong&gt; 1st in our college, 9,900 points. &lt;strong&gt;Status:&lt;/strong&gt; shipped, open source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When one microservice fails, every service that calls it starts failing too. The on-call engineer gets thousands of alerts, and the loudest one is usually a victim, not the cause.&lt;/p&gt;

&lt;p&gt;Problem 3 at the Microsoft Codathon asked for an AI-powered root cause analysis engine. Our team, Ace Azael, built &lt;strong&gt;Antibody&lt;/strong&gt;: it reads logs, metrics, traces and service events, folds everything that belongs together into one incident, and ranks the probable root causes with evidence and a confidence label. I led the technical explanation in our pitch.&lt;/p&gt;

&lt;p&gt;The part I am proudest of is one rule we wrote down on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automation before AI
&lt;/h2&gt;

&lt;p&gt;Every decision in Antibody is deterministic and replayable: detect, correlate, rank, suggest. The language model only rewrites evidence the engine has already computed into plain English, and that text is labelled as AI-generated. If the model is down, nothing breaks; a template writes the explanation from the same evidence.&lt;/p&gt;

&lt;p&gt;That rule shaped everything else. Here is the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ingest     learn the dependency graph from trace spans (caller -&amp;gt; callee)
detect     per-service EWMA baselines, z-scores, error-log and error-span ratios
           debounce: 2 bad ticks in a row before anything counts
correlate  a new abnormal service joins an open incident if it calls into the failure
rank       8 signals -&amp;gt; an RCA score from 0 to 1 (not a probability)
verdict    HIGH / MEDIUM / LOW from fixed rules. LOW means "insufficient evidence"
suggest    fault signature -&amp;gt; restart, roll back or scale out
human      acknowledge -&amp;gt; apply fix -&amp;gt; verify recovery -&amp;gt; resolve -&amp;gt; remember
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The eight ranking signals include personalised PageRank on the anomaly graph, which suspect went wrong first, how many of the callers' error logs blame it, a local fault signature (crash, bad deploy, leak, CPU, slow queries), a recent deploy, and a match with a past resolved incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  An engine that is allowed to say "I don't know"
&lt;/h2&gt;

&lt;p&gt;Most demos rank something first and present it with total confidence. We wanted the confidence label to mean something, so it comes from fixed rules: how far the top suspect leads the second, how many independent kinds of evidence agree, and whether anything contradicts it. When the rules aren't satisfied, Antibody abstains.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The benchmark, including where it loses
&lt;/h2&gt;

&lt;p&gt;84 scenarios: 7 services, 6 fault types, 2 seeds each, every one with a known cause. The engine never sees the ground truth; it is only used for scoring. &lt;code&gt;python bench.py&lt;/code&gt; reproduces it in about 7 seconds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Root cause ranked first&lt;/th&gt;
&lt;th&gt;Clean&lt;/th&gt;
&lt;th&gt;Hard mode (random red-herring blips)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Loudest anomaly only&lt;/td&gt;
&lt;td&gt;88.1%&lt;/td&gt;
&lt;td&gt;84.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anomaly + earliest onset&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;75.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Onset + dependency graph&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;79.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Antibody&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;84.5%&lt;/strong&gt; (top 3: 92.9%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the confidence labels in hard mode: HIGH is given 78.6% of the time and is right 98.5% of the time. LOW, the abstain, is given 8.3% of the time, and a forced guess there would have been right only 42.9% of the time. When Antibody does answer, it is right 95.8% of the time.&lt;/p&gt;

&lt;p&gt;Now the part a judge would find anyway, so we put it in the README first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On clean traffic, two simpler baselines also hit 100%. Our simulator is too easy to separate the models there.&lt;/li&gt;
&lt;li&gt;In hard mode the full model only ties "loudest anomaly" overall. It wins every quiet-crash cascade, where the root goes silent while its callers get loud. It loses when the fault is high in the graph and a red herring sits below it.&lt;/li&gt;
&lt;li&gt;The confidence label flags 5 of those 6 losses as LOW or MEDIUM. So when it is wrong, it mostly knows it is unsure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Other numbers: two unrelated faults at once were found as separate incidents in 6 of 6 pairs, median time to detect is 2 seconds, and a database crash folds about 2,000 events into one incident with 5 services alerting and &lt;code&gt;db&lt;/code&gt; ranked first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 7 services are simulated. The ingest API accepts real data; we have not connected a real system.&lt;/li&gt;
&lt;li&gt;Fault signatures are pattern rules, so a new kind of failure gets "investigate", not a confident fix.&lt;/li&gt;
&lt;li&gt;Memory of past fixes is in-process and resets on restart.&lt;/li&gt;
&lt;li&gt;No auth on the API. It is a local demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I took from Antibody, and have reused in every build since: decide in code, explain with the model, and make "I'm not sure" a first-class answer.&lt;/p&gt;

&lt;p&gt;Repo (MIT), with the benchmark and tests: &lt;a href="https://github.com/victorisaacchinta-eng/antibody" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/antibody&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo and its benchmark. Every number above comes from &lt;code&gt;bench.py&lt;/code&gt; and the README.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>devops</category>
      <category>observability</category>
    </item>
    <item>
      <title>Take machines apart with your bare hands: building ATLAS Live in public</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 20:16:14 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/take-machines-apart-with-your-bare-hands-building-atlas-live-in-public-24on</link>
      <guid>https://dev.to/victorisaacchintaeng/take-machines-apart-with-your-bare-hands-building-atlas-live-in-public-24on</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Status: building now.&lt;/strong&gt; Started 21 Oct 2026 for the Nebius x NVIDIA Global AI Hackathon. Submission due 30 Oct 2026. Live build: &lt;a href="https://atlas-live-xi.vercel.app" rel="noopener noreferrer"&gt;atlas-live-xi.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few weeks ago I built a gesture interface for maintenance technicians, &lt;a href="https://dev.to/victorisaacchintaeng/a-safety-gate-with-no-ai-in-it-building-james-a-touch-free-maintenance-copilot-1k0p"&gt;JAMES&lt;/a&gt;. One part of it, called ATLAS, drew the targeted machine as an exploded schematic you could pull apart with an open palm. For this hackathon I am rebuilding that part on its own, in the browser, and taking it much further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ATLAS Live&lt;/strong&gt; turns real machines, real objects and real places into holograms you can pull apart with your hands, pick with a pinch and question out loud. Every fact it speaks comes from a web search it cites.&lt;/p&gt;

&lt;p&gt;This post is written mid-build, so it is a progress report, not a victory lap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does today
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;You do&lt;/th&gt;
&lt;th&gt;It does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pick a machine: Perseverance, Ingenuity, the James Webb telescope, a chronograph, a concept car&lt;/td&gt;
&lt;td&gt;Loads the real model. Open palm explodes it, pinch picks a part, ask anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hold an object up to the webcam, or drop in a photo&lt;/td&gt;
&lt;td&gt;Nemotron vision names it and boxes its parts. Depth Anything V2 lifts the photo into a 3D point cloud, split into those parts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Places&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Name a landmark&lt;/td&gt;
&lt;td&gt;Google Photorealistic 3D Tiles fly you in. Cited captions for what you are looking at&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The models come from NASA (public domain) and the Khronos sample set (CC BY 4.0), with textures and logos removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, as an exploded view
&lt;/h2&gt;

&lt;p&gt;The cover of this post is the app taken apart, layer by layer, because that is honestly how I think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hands:&lt;/strong&gt; MediaPipe Hand Landmarker, running locally. Camera frames never leave the browser except the single frame you choose to scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eyes:&lt;/strong&gt; NVIDIA Nemotron 3.5 Super VL identifies a scanned object and locates its parts. A Nemotron agent answers questions and drives the hologram with tool calls (&lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;focus_part&lt;/code&gt;, &lt;code&gt;set_explode&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth:&lt;/strong&gt; Depth Anything V2 Small via transformers.js, also in the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Facts:&lt;/strong&gt; Tavily search. Captions and answers carry their sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ground:&lt;/strong&gt; Google Photorealistic 3D Tiles through Cesium, on three.js.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All model calls run server-side in Vercel functions. Keys never reach the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things that bit me in the first two days
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Model ids die
&lt;/h3&gt;

&lt;p&gt;The provider layer is any OpenAI-compatible endpoint, first key wins: Nebius Token Factory, then the NVIDIA API catalog, then fallbacks. Right now the live site runs Nemotron through NVIDIA's API catalog, and it will switch to Token Factory the moment that key is set, without a code change.&lt;/p&gt;

&lt;p&gt;What I didn't expect: model ids get retired under you. Gemini 2.5 shut down on 16 Oct 2026, days before I started. So the app no longer trusts a hard-coded id. On start it reads the provider's live &lt;code&gt;/models&lt;/code&gt; list, and if the configured id is gone it takes the first live id that matches a preference list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;NEMOTRON_PREFER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;vision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/nemotron.*vl/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/vl/i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/nemotron.*super/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/nemotron.*ultra/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/nemotron/i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;/nemotron.*lightning/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/nemotron.*nano/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/nemotron/i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model that returns 429 is skipped for 15 minutes and the next one takes over.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. One image per request
&lt;/h3&gt;

&lt;p&gt;Build mode researches a landmark, plans it, has small builder agents make the parts, and then a critic compares the result with a reference photo. The natural request sends both images. NVIDIA's Nemotron VL endpoint rejects a second image in one request.&lt;/p&gt;

&lt;p&gt;The fix was boring and it works: stitch the render and the reference into one side-by-side image and send that.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Some famous places look bad in 3D Tiles
&lt;/h3&gt;

&lt;p&gt;Google's 3D coverage is excellent for the Colosseum and Machu Picchu and poor for others. The Taj Mahal looks melted. So I am adding photo reconstructions: a Colab notebook pulls openly licensed photos of the landmark from Wikimedia Commons, matches them (SuperPoint and LightGlue through hloc, then COLMAP), and trains a Gaussian splat with gsplat. The app shows it with the Spark viewer.&lt;/p&gt;

&lt;p&gt;One bug worth sharing from that work. In the gsplat version I am using, the opacity reset never fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;reset_every&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python &lt;code&gt;&amp;amp;&lt;/code&gt; binds tighter than &lt;code&gt;==&lt;/code&gt;, so this reads as &lt;code&gt;step % reset_every == (0 &amp;amp; step) &amp;gt; 0&lt;/code&gt;, a chained comparison that ends in &lt;code&gt;0 &amp;gt; 0&lt;/code&gt;. It is always false. My training loop does the check itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still to do before 30 Oct
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run the reconstruction notebook for the Taj Mahal and Charminar and wire the results in&lt;/li&gt;
&lt;li&gt;Test hand control on a real webcam, in poor light&lt;/li&gt;
&lt;li&gt;Re-test Build mode on the Eiffel Tower and the Taj Mahal&lt;/li&gt;
&lt;li&gt;A 3-minute video and the write-up of where Nemotron and Token Factory are used&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have tried hand tracking in the browser and found a pinch detector that survives bad lighting, I would love to hear how you did it.&lt;/p&gt;

&lt;p&gt;Repo (MIT): &lt;a href="https://github.com/victorisaacchinta-eng/atlas-live" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/atlas-live&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo and my build notes. It describes the build as of 22 Oct 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>threejs</category>
      <category>computervision</category>
    </item>
    <item>
      <title>A safety gate with no AI in it: building JAMES, a touch-free maintenance copilot</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:59:17 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/a-safety-gate-with-no-ai-in-it-building-james-a-touch-free-maintenance-copilot-1k0p</link>
      <guid>https://dev.to/victorisaacchintaeng/a-safety-gate-with-no-ai-in-it-building-james-a-touch-free-maintenance-copilot-1k0p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; first version over 25 to 26 Sep 2026 for AgentX-2026, public on GitHub since 26 Sep.&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; still being built. ATLAS, CANTUS, SIGHT and the hash-chained ledger landed through October; next is a tuning session on real hands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A technician with gloves on and both hands busy cannot scroll a PDF manual.&lt;/p&gt;

&lt;p&gt;That one sentence is the whole reason JAMES exists. The fix for a vibrating pump lives in four places at once: the manual, the machine's sensor log, the notes from the last time someone repaired it, and the head of whoever did that repair. The technician is standing in front of the pump, holding a spanner, and every one of those sources assumes a free hand and a screen.&lt;/p&gt;

&lt;p&gt;So I built a computer you operate by pointing at the air.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JAMES&lt;/strong&gt; (Joint Autonomous Multimodal Execution System) watches a webcam, listens to a microphone, finds the right procedure, checks the sensor log and past repairs, and walks the technician through the fix one step at a time. Point at the machine, ask out loud, pinch to confirm each step. It runs entirely on a MacBook Air: local model, local search, local memory, cloud off.&lt;/p&gt;

&lt;p&gt;It took first prize at AgentX-2026, a national-level hackathon at TKREC Hyderabad. But the part I want to write about is not the demo. It is the three ideas that made it safe to put in front of a jury, and the ugly bugs that taught me each one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw66ojspjhk4jezxk0sw6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw66ojspjhk4jezxk0sw6.gif" alt="A full repair run in rehearsal mode" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 1: the model can suggest a step. Only plain code can allow it.
&lt;/h2&gt;

&lt;p&gt;JAMES is a small team of agents, each with one job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Built with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LENS&lt;/td&gt;
&lt;td&gt;Sees hands and the machine's asset tag&lt;/td&gt;
&lt;td&gt;MediaPipe Hand Landmarker, OpenCV ArUco&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECHO&lt;/td&gt;
&lt;td&gt;Hears the request&lt;/td&gt;
&lt;td&gt;faster-whisper, Qwen3-VL 4B via Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PAGE&lt;/td&gt;
&lt;td&gt;Finds the procedure and cites the page&lt;/td&gt;
&lt;td&gt;PyMuPDF, BM25, nomic-embed-text in Qdrant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PULSE&lt;/td&gt;
&lt;td&gt;Reads the sensor log&lt;/td&gt;
&lt;td&gt;pandas, and it never invents a reading&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LEDGER&lt;/td&gt;
&lt;td&gt;Remembers approved jobs&lt;/td&gt;
&lt;td&gt;append-only SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOREMAN&lt;/td&gt;
&lt;td&gt;Plans and decides the likely cause&lt;/td&gt;
&lt;td&gt;LangGraph fan-out to PAGE, PULSE, LEDGER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WARDEN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Blocks unsafe steps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;plain Python and a rule file. No AI.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WARDEN is the important one. The model is allowed to be creative about &lt;em&gt;what to check next&lt;/em&gt;. It is never allowed to decide whether a step is safe. That decision lives in a rule engine anyone can read, with rules that ship in an "industry pack" next to the manual and the asset tags.&lt;/p&gt;

&lt;p&gt;In the demo, step 4 of the pump repair is "Remove the coupling guard". If the technician skips lockout, WARDEN refuses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BLOCKED BY SAFETY POLICY   LOTO-01, ZERO-01
Remove the coupling guard
Lockout and zero energy are not confirmed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pinches later (lockout confirmed, zero energy confirmed), WARDEN re-checks and the step opens. The seeded safety suite runs 8 known violations and 4 legitimate sequences: &lt;strong&gt;8 of 8 violations blocked, 0 of 4 false blocks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pack is hash-checked too. If anyone edits a rule file after its manifest was written, the app refuses to start. A safety rule you can quietly change is not a safety rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 2: the pinch has to be a pinch
&lt;/h2&gt;

&lt;p&gt;The first live run on the Mac taught me that hand tracking fails quietly, and that the failures look exactly like intent.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A fist reads as a pinch.&lt;/strong&gt; In a fist the thumb lies on the index finger, so the thumb-to-index distance drops under the pinch threshold. Closing your hand to rest it was confirming steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching for a tool reads as a pinch.&lt;/strong&gt; Hold a pinch, move your hand toward the camera to grab a spanner, and you have "confirmed" whatever was on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Losing the hand was silent.&lt;/strong&gt; In low light or with dark gloves the tracker just stopped, and the app kept showing the last known state as if nothing happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were tuning problems. They were design problems, so each got a design fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fist latch.&lt;/strong&gt; A pinch that &lt;em&gt;begins&lt;/em&gt; as a fist is ignored until the fingers open again. The test for "this is a pinch, not a fist" is that the index finger is at least half extended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reach guard.&lt;/strong&gt; While a pinch is held, JAMES tracks hand size (the geometric mean of palm length and knuckle width, so tilting moves it about half as much as reaching). If it changes more than 30%, the hand is moving to or from the camera. The pinch is dropped and must be released before it counts again. A reach never confirms a step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIGHT.&lt;/strong&gt; A small module that watches frame brightness and how often the hand appears and vanishes. When tracking can't be trusted it says so on screen, and a lost hand is marked with a still amber X where it was last seen, the way cockpit displays mark invalid data. SIGHT can warn. It can never press or unlock anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The research backed this up. One study of 52 person-hours of everyday activity found a naive gesture spotter fired a false positive about every 5.1 seconds. That number changed how I thought about every gesture in the app: the default assumption is "this was an accident", and the gesture has to prove otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idea 3: every answer has a source, and every record can be checked later
&lt;/h2&gt;

&lt;p&gt;A maintenance copilot that makes things up is worse than no copilot. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Steps come from real numbered lines in the manual, word for word, each with its page.&lt;/li&gt;
&lt;li&gt;Readings come only from the supplied sensor log. PULSE never estimates a value.&lt;/li&gt;
&lt;li&gt;Past repairs are recalled only if a senior approved them, they were not revoked, and they are for the same class of equipment. Memory can suggest. It never decides a cause on its own.&lt;/li&gt;
&lt;li&gt;The status bar always says which backend is actually live (local model or keyword rules, BM25 or BM25 plus Qdrant), so the demo can't overstate itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And because buyers in regulated plants ask whether a record has been altered, every logged event is &lt;strong&gt;hash-chained&lt;/strong&gt;: each event stores SHA-256 of the previous hash plus the event itself. A read-only checker walks the chain and names the first edited, reordered or removed event. It is tamper-evident, not tamper-proof, and the README says exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;In short iterations with the same loop every time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; the build and list findings as P0 (must fix before a live demo) or P1 (should fix).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt; each one and add a regression test that fails without the fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it live&lt;/strong&gt; on the Mac. Every sentence that misbehaved in the app log became a new test, and the whole log was replayed through old and new code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some of my favourite bugs from that log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Playing the neural voice through PortAudio in the same process as the microphone crashed on macOS. Each sentence now plays through &lt;code&gt;afplay&lt;/code&gt; in its own process.&lt;/li&gt;
&lt;li&gt;A model plan searched a website the user never named. Search and collect steps must now name a site the user actually said.&lt;/li&gt;
&lt;li&gt;Gesture timers reset when the hand moved at all, so "explode the model" felt dead, then fired all at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo has &lt;strong&gt;607 tests passing&lt;/strong&gt;, and the build log marks every claim as LIVE (seen on the Mac), TESTED (automated) or UNVERIFIED. That labelling is the most useful habit I picked up on this project. It stops me quoting a number I never actually saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I made it fun
&lt;/h2&gt;

&lt;p&gt;Two side apps grew out of the hand tracking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ATLAS&lt;/strong&gt; draws the targeted machine as an exploded line drawing and lights the part the current step touches: amber when WARDEN allows it, red when WARDEN is blocking it. It also runs as its own app with a library of openly licensed models (NASA's Perseverance rover, Ingenuity, the James Webb Space Telescope) that you take apart by opening your hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CANTUS&lt;/strong&gt; is a rhythm game in the launcher. Blocks fly towards you on the beat; swing through them, left hand red, right hand blue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rsht9funkjvmzt687l5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rsht9funkjvmzt687l5.png" alt="ATLAS lighting the guarded part red while WARDEN blocks the step" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A pinch inside ATLAS can never confirm a maintenance step. ATLAS owns the hand while it is open. Fun is allowed. Ambiguity about what a gesture means is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not done
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It ships with sample data and demo safety rules. Nothing here is certified for real safety use.&lt;/li&gt;
&lt;li&gt;Industry packs are hash-checked but not signed yet.&lt;/li&gt;
&lt;li&gt;It runs on macOS only today.&lt;/li&gt;
&lt;li&gt;SIGHT thresholds, the reach tolerance and the gesture timings still need a proper tuning session on the Mac before I quote them as final.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The code is public under MIT: &lt;a href="https://github.com/victorisaacchinta-eng/james" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/james&lt;/a&gt;. The product site is &lt;a href="https://jamesagent.vercel.app/" rel="noopener noreferrer"&gt;jamesagent.vercel.app&lt;/a&gt;. &lt;code&gt;python app.py --sim&lt;/code&gt; runs a full rehearsal with no camera or microphone.&lt;/p&gt;

&lt;p&gt;If you have built gesture interfaces and have opinions about false activations, I would genuinely like to hear them in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo and its build log. Every number above comes from the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>computervision</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Code decides, AI explains, a named officer signs: bid compliance for Smart India Hackathon</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:59:11 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/code-decides-ai-explains-a-named-officer-signs-bid-compliance-for-smart-india-hackathon-189n</link>
      <guid>https://dev.to/victorisaacchintaeng/code-decides-ai-explains-a-named-officer-signs-bid-compliance-for-smart-india-hackathon-189n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; late September 2026 for Smart India Hackathon 2026 (repo public since 29 Sep).&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; submitted; the live workstation stays up for anyone to try.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A procurement officer checking a government bid in India opens portal after portal. GST. Income tax. MCA21 for the company record. Udyam for MSME status. EPFO and ESIC for labour compliance. The debarment list. Then the bid documents themselves, to see whether the name on the PDF matches the name on all of the above.&lt;/p&gt;

&lt;p&gt;It is slow. Worse, two officers can look at the same bid and reach two different answers.&lt;/p&gt;

&lt;p&gt;For Smart India Hackathon 2026 (problem statement SIH26100, from the Ministry of Petroleum and Natural Gas), our team Ace Azael built &lt;strong&gt;Procurement Pilot&lt;/strong&gt;: a workstation that runs fifteen fixed checks on every bid against ten registry sources, scores it out of 100, explains every flag in plain language, and then hands the decision to a named officer.&lt;/p&gt;

&lt;p&gt;The design rule fits on one line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Code decides. AI explains. A named officer signs.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is about why that order matters, and what it looks like in code.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why the model doesn't get to score
&lt;/h2&gt;

&lt;p&gt;The tempting build is obvious: give an LLM the bid, the tender and the registry data, and ask it whether the bid is compliant. It would demo well. It would also fail the one property a government process needs most: &lt;strong&gt;the same bid must get the same answer every time&lt;/strong&gt;, and an auditor must be able to see why.&lt;/p&gt;

&lt;p&gt;So every check is plain Python. Each one returns &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;FLAG&lt;/code&gt; or &lt;code&gt;FAIL&lt;/code&gt;, plus a sentence naming the real numbers and the source it checked. The score is arithmetic over those results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score_and_verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;points&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# PASS = full weight, FLAG = half, FAIL = 0
&lt;/span&gt;    &lt;span class="n"&gt;fails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;hard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fails&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HARD_CAP_CHECKS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hard&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fails&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;   &lt;span class="c1"&gt;# a hard FAIL caps the score at 45
&lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights add to 100. A FAIL on any of four hard checks (EMD, statutory documents, GST registration, debarment) caps the score at &lt;strong&gt;45&lt;/strong&gt; and makes the risk Critical, no matter how good the rest of the bid looks. Any other FAIL caps it at &lt;strong&gt;72&lt;/strong&gt;. The officer sees a "Why not 100" line listing every point lost and why.&lt;/p&gt;

&lt;p&gt;That cap was the most important design decision in the project. Without it, a debarred vendor with perfect paperwork everywhere else could score 92. With it, debarment means the bid is Non-compliant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules that need no registry are real
&lt;/h2&gt;

&lt;p&gt;We could not get live access to government registries in a hackathon. That needs onboarding through API Setu and a GST Suvidha Provider. So every registry lookup reads sample records, and &lt;strong&gt;the app labels every one of them as simulated&lt;/strong&gt; on screen.&lt;/p&gt;

&lt;p&gt;But some checks need no lookup at all, and those are real. A GSTIN carries its own check digit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gstin_check_digit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first14&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_CHARS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_CHARS&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A GSTIN that fails this test cannot be a valid registration, whatever the bid claims. The PAN sits inside the GSTIN, and its fourth character tells you the holder type, so a "Private Limited" company filing with an individual's PAN gets flagged. Make in India class thresholds (Class-I at 50% local content, Class-II at 20%), EMD exemptions for verified MSE, Startup and NSIC bidders, and incorporation under 91 days as a shell-company signal are all real rules too.&lt;/p&gt;

&lt;p&gt;Being honest about what is simulated is not a weakness in the pitch. It is the pitch. A judge who finds an unlabelled fake will distrust everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the AI actually earns its place
&lt;/h2&gt;

&lt;p&gt;The reasoning layer (Sarvam's &lt;code&gt;sarvam-105b&lt;/code&gt;, an Indian model) does one job: it reads the check results and writes the explanation an officer would want to read. It has three hard limits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It cannot set the score or the verdict.&lt;/strong&gt; Those are computed before it is called.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never sees a full PAN or GSTIN.&lt;/strong&gt; Identifiers are masked before anything reaches the model:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mask_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
       &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The model never sees a full PAN or GSTIN: the PAN part is masked in both.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It is optional.&lt;/strong&gt; Without an API key, the app writes a deterministic summary from the same evidence and says so. The demo never depends on Wi-Fi.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Catching what a single bid can't show
&lt;/h2&gt;

&lt;p&gt;Some fraud only appears across bids. For every pair of bids on a tender, Procurement Pilot screens for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prices within 0.5% of each other&lt;/li&gt;
&lt;li&gt;submissions within 5 minutes of each other&lt;/li&gt;
&lt;li&gt;shared addresses&lt;/li&gt;
&lt;li&gt;cover bids more than 25% above the benchmark&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our demo data, two transport bidders quoted 0.38% apart and submitted 2 minutes apart. One of them had been incorporated 55 days before bidding and had no ESIC registration. No single-bid check would have connected those facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A human signs, on the record
&lt;/h2&gt;

&lt;p&gt;The last step is deliberately not automated. Officer, senior approver and admin roles are enforced by the server, not the UI. Passing a flagged bid needs a senior approver and a written reason of at least 30 characters. Every decision, with name and time, lands in an audit log that an admin can export.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI running as a Vercel Function, Postgres on Neon (SQLite locally)&lt;/li&gt;
&lt;li&gt;JWT sign-in with server-enforced roles&lt;/li&gt;
&lt;li&gt;A PDF reader that pulls GSTIN, PAN, Udyam, CIN, DPIIT number and legal name from a bid document and cross-checks each one&lt;/li&gt;
&lt;li&gt;26 API tests, plus a parity test that runs the frontend's offline engine against the backend for every sample bid. CI runs both on every push&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd tell another team
&lt;/h2&gt;

&lt;p&gt;If your agent touches money, eligibility or safety, split it in two. Let deterministic code own every decision that must be repeatable, and let the model own the part humans are bad at: turning fifteen results into two clear paragraphs. You get an AI feature judges can see, and a system an auditor can trust.&lt;/p&gt;

&lt;p&gt;The live build is at &lt;a href="https://procurement-pilot-sih.vercel.app" rel="noopener noreferrer"&gt;procurement-pilot-sih.vercel.app&lt;/a&gt; and the code is public under MIT at &lt;a href="https://github.com/victorisaacchinta-eng/procurement-pilot-sih" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/procurement-pilot-sih&lt;/a&gt;. There is also a &lt;a href="https://youtu.be/D4A3Fv-sczU" rel="noopener noreferrer"&gt;4:52 walkthrough on YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Student prototype. Not an official government system, and every tender, bid and registry record in it is sample data.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo. Every number and code excerpt above comes from the repo.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Every bank statement carries its own answer key</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:59:05 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/every-bank-statement-carries-its-own-answer-key-4k8</link>
      <guid>https://dev.to/victorisaacchintaeng/every-bank-statement-carries-its-own-answer-key-4k8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; 18 to 21 Oct 2026.&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; live at ithamar.vercel.app. Next up: real statements from specific banks, then scanned PDFs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bank statement extractors get rows wrong quietly. A &lt;code&gt;7&lt;/code&gt; read as a &lt;code&gt;1&lt;/code&gt;, a debit dropped into the credit column, one row lost at a page break. Nobody notices until the totals stop adding up, and by then the bad numbers are already in a spreadsheet, a loan file or a tax return.&lt;/p&gt;

&lt;p&gt;The thing is, every bank statement already carries its own answer key. Each row obeys one line of arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;balance above + credit - debit = balance printed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I built &lt;strong&gt;Ithamar&lt;/strong&gt;, which reads an Indian bank statement PDF, walks that equation down the page, and marks every row as one of three things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✓ proven&lt;/td&gt;
&lt;td&gt;the arithmetic holds exactly as printed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;↻ repaired&lt;/td&gt;
&lt;td&gt;it holds after one correction, and the correction is logged beside the row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;✗ left for you&lt;/td&gt;
&lt;td&gt;no correction is earned; Ithamar says what it found and stops&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It never guesses, and it never invents a transaction.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Parsing without templates
&lt;/h2&gt;

&lt;p&gt;Most extractors keep a template per bank. I wanted one parser that learns each statement's layout from the page itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;parse/layout.py&lt;/code&gt; finds the header row, learns where each column sits from its header words, then assigns every word below it by geometry: amounts align to a column's right edge, text to its left. A line without a date continues the row above, which is how wrapped narrations survive. It handles lakh grouping (&lt;code&gt;1,00,000.00&lt;/code&gt;), &lt;code&gt;Dr&lt;/code&gt;/&lt;code&gt;Cr&lt;/code&gt; suffixes, single amount columns with a separate direction column, and headers repeated on every page.&lt;/p&gt;

&lt;p&gt;And one rule that saved me hours of debugging later: &lt;strong&gt;all money is integer paise.&lt;/strong&gt; Floats never touch an amount.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a row breaks
&lt;/h2&gt;

&lt;p&gt;If the equation fails, the naive move is to stop, or to "fix" the balance so everything below lines up again. Both are wrong. Stopping hides every later row. Forcing the balance hides the actual error.&lt;/p&gt;

&lt;p&gt;Instead, the chain &lt;strong&gt;resyncs on the printed balance&lt;/strong&gt;, so one bad cell makes one or two breaks rather than corrupting everything below it. Then each break is tested against four concrete explanations, each with the exact correction it implies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The amount is in the wrong column.&lt;/li&gt;
&lt;li&gt;A digit in the amount was misread: one substitution, drop, insertion or adjacent swap, or a decimal shift.&lt;/li&gt;
&lt;li&gt;A digit in the balance was misread. Its row breaks, and so does the row below.&lt;/li&gt;
&lt;li&gt;A whole row is missing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Repair is a decision, not a guess
&lt;/h2&gt;

&lt;p&gt;This is the part I am proudest of. A correction is applied only when &lt;strong&gt;both&lt;/strong&gt; of these hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is the &lt;strong&gt;only&lt;/strong&gt; specific explanation that closes the local window: the broken row &lt;em&gt;and&lt;/em&gt; the next row that prints a balance must both agree afterwards&lt;/li&gt;
&lt;li&gt;it strictly reduces the number of breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a missing row is &lt;strong&gt;never&lt;/strong&gt; applied. "There was a transaction here that I can't see" explains &lt;em&gt;any&lt;/em&gt; break, which means it proves nothing. So Ithamar flags it for a person and leaves it alone.&lt;/p&gt;

&lt;p&gt;There is one more subtle rule. A row the engine corrects ends as &lt;code&gt;repaired&lt;/code&gt;, never &lt;code&gt;proven&lt;/code&gt;. "Proven after a change" is a different claim from "proven as printed", and the output should never blur the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that matters
&lt;/h2&gt;

&lt;p&gt;A benchmark that rewards fixing things will happily reward wrong fixes. So the headline number is the opposite: &lt;strong&gt;false-verified rows&lt;/strong&gt;, rows marked proven that are actually wrong. It must stay at zero.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ithamar bench&lt;/code&gt; renders statements whose truth is known by construction, injects extraction-style errors into what is printed (never into the truth), and scores the engine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24 statements, 855 rows, 39 injected errors
every swap, amount misread and balance misread repaired to its true value
every missing row flagged and left alone
false-verified rows across 24 statements: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be clear about what that shows: these are synthetic statements from the project's own generator. They show the verifier works. They do not show that the parser handles every real bank. Real statements are the next test, and I will publish those results the same way, failures included.&lt;/p&gt;

&lt;p&gt;The test suite leans on property tests with Hypothesis. The headline property is that whatever single error is injected, across hundreds of generated ledgers, &lt;strong&gt;no row is ever marked proven or repaired unless its values equal the truth.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two engines, one behaviour
&lt;/h2&gt;

&lt;p&gt;The command line runs a Python engine on &lt;code&gt;pdfplumber&lt;/code&gt;. The website runs a TypeScript port on &lt;code&gt;pdf.js&lt;/code&gt;, so a statement is parsed and proven &lt;strong&gt;entirely inside your browser tab&lt;/strong&gt;. Nothing is uploaded, and there is no server.&lt;/p&gt;

&lt;p&gt;Two engines invite drift, so a parity script runs both on the same PDFs and fails on any difference in rows, amounts, statuses, repairs, unresolved rows or decoded narration fields. CI runs it on every push.&lt;/p&gt;

&lt;p&gt;Each audited statement also gets a &lt;strong&gt;seal&lt;/strong&gt;: a guilloche rosette, like the security printing on a cheque, drawn from that statement's own balances. Every row owns a slice of it. Proven rows draw clean, repaired rows draw in violet, and a row that does not balance prints out of register in magenta, exactly where it broke. It is the most fun I have had making a correctness check visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the chain proves, and what it doesn't
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Proven by arithmetic&lt;/th&gt;
&lt;th&gt;Not proven&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debit, credit and balance of every verified row&lt;/td&gt;
&lt;td&gt;Dates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No transaction is missing between two verified rows&lt;/td&gt;
&lt;td&gt;Narration text and counterparty names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The closing balance matches the last row&lt;/td&gt;
&lt;td&gt;Rows on statements that print no running balance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Narrations are still decoded (UPI, NEFT, IMPS, RTGS, VPA, RRN, UTR, IFSC and a best-effort counterparty), and the output says plainly that none of that is proven by the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the name
&lt;/h2&gt;

&lt;p&gt;Exodus 38:21 records the first audit: the materials of the tabernacle were counted "by the hand of Ithamar". And its numbers still check. Exodus 38:25 to 26 counts 603,550 men giving half a shekel each, which is 301,775 shekels. It separately reports the total as 100 talents and 1,775 shekels, which at 3,000 shekels to the talent is also 301,775. Two independent records, one total. That is exactly what this tool does to a bank statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In the browser, nothing uploaded: &lt;a href="https://ithamar.vercel.app" rel="noopener noreferrer"&gt;ithamar.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The code, MIT: &lt;a href="https://github.com/victorisaacchinta-eng/ithamar" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/ithamar&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/victorisaacchinta-eng/ithamar
ithamar demo        &lt;span class="c"&gt;# generate synthetic statements and audit them&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ithamar audit&lt;/code&gt; exits &lt;code&gt;0&lt;/code&gt; when every row is proven, &lt;code&gt;1&lt;/code&gt; when rows were repaired, and &lt;code&gt;2&lt;/code&gt; when a person needs to look, so it can gate a script.&lt;/p&gt;

&lt;p&gt;If you have run into a statement layout that breaks parsers, tell me in the comments. That is exactly the test data I need next.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo. Every number above comes from the repo's benchmark.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>typescript</category>
      <category>testing</category>
      <category>fintech</category>
    </item>
    <item>
      <title>My browser agent took 6 tries on Blinkit and 1 on a site it had never seen</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:58:58 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/my-browser-agent-took-6-tries-on-blinkit-and-1-on-a-site-it-had-never-seen-7af</link>
      <guid>https://dev.to/victorisaacchintaeng/my-browser-agent-took-6-tries-on-blinkit-and-1-on-a-site-it-had-never-seen-7af</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Built:&lt;/strong&gt; 12 Sep 2026, solo, in one day at the SLAB Hackathon.&lt;br&gt;
&lt;strong&gt;Status:&lt;/strong&gt; finished hackathon build. The repo has a prototype of the cached version described at the end.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My browser agent needed &lt;strong&gt;6 attempts&lt;/strong&gt; to get a clean price out of Blinkit. A few hours later it got one out of District, a site it had never touched before, on the &lt;strong&gt;first&lt;/strong&gt; try, live, on stage.&lt;/p&gt;

&lt;p&gt;The agent didn't get smarter in between. It got a notebook.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;Basket Buddy&lt;/strong&gt;, which I built solo in a day for the SLAB Hackathon (Self-Learning Agent Browser) at CMR Institute of Technology, Hyderabad, Track 05: Self-Learning Loops. It took second place. The idea is small, and I think it generalises to a lot of agent work: &lt;strong&gt;make the agent write down what it learned, in a form it can read back next time.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The task
&lt;/h2&gt;

&lt;p&gt;Search a quick-commerce site for a product ("milk"), and extract the name, size, price and MRP of the results. Do it across several platforms so someone can compare a basket without opening three apps.&lt;/p&gt;

&lt;p&gt;Every browser action ran through &lt;a href="https://github.com/agentrhq/webcmd" rel="noopener noreferrer"&gt;WebCMD&lt;/a&gt; sessions, one throwaway profile and session per platform. No raw Playwright or Selenium script drives anything directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt counts, not vibes
&lt;/h2&gt;

&lt;p&gt;Here is the actual log, attempts before a working, clean extraction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Attempts&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Blinkit&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Cold start. No strategy existed yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigBasket&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Blocked. A real 404 from the site itself, likely bot detection. Correctly abandoned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Reused a lesson from Blinkit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zepto&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;URL worked first try; one fix for discount-badge noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;District&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Transfer test. Zero prior exposure. Live&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The number I cared about was never "did it work". It was "did it need fewer tries because of what it learned before".&lt;/p&gt;

&lt;h2&gt;
  
  
  What Blinkit taught it
&lt;/h2&gt;

&lt;p&gt;Blinkit was painful, and every bit of pain became a line in a file. This is the real strategy the agent wrote after cracking it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blinkit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"search_url_pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://blinkit.com/s/?q={query}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_card_selector_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No stable class names; find ADD buttons with no children, then walk up 3 parents to reach the card"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extraction_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parse price via regex on innerText (₹&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+), not by selector; first ₹ match is price, second is MRP if discounted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gotchas"&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="s2"&gt;"Homepage search box is a fake trigger link (href=/s/), not a real input, don't try to click+type it"&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;Look at that &lt;code&gt;gotchas&lt;/code&gt; line. The search box on the homepage looks like an input, but it is a link that opens the real search page. An agent that clicks and types into it fails in a confusing way, every time, forever, unless it remembers.&lt;/p&gt;

&lt;p&gt;The lesson that transferred was not "Blinkit's selectors". It was more general: &lt;strong&gt;verify the direct search URL before touching the DOM.&lt;/strong&gt; Amazon worked first try because of it. Zepto's URL worked first try because of it. District, a site with no strategy file at all, worked first try because the agent reached for the URL pattern first instead of poking at a search box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of memory
&lt;/h2&gt;

&lt;p&gt;There are two separate memory layers here, and keeping them apart is what made this clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site structure memory&lt;/strong&gt; belongs to WebCMD itself: explore a site once, reuse the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task memory&lt;/strong&gt; is my layer on top: &lt;code&gt;strategies/*.json&lt;/code&gt;, one file per platform, written after each run with the search URL pattern, selector notes and gotchas. What worked, what didn't, and why.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loop is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plan the run
  -&amp;gt; site known?  no  -&amp;gt; explore cold, learn, write strategies/&amp;lt;site&amp;gt;.json
                  yes -&amp;gt; reuse the strategy
  -&amp;gt; extract name, size, price, mrp
  -&amp;gt; next run starts from what this run wrote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Knowing when to stop
&lt;/h2&gt;

&lt;p&gt;BigBasket returned a consistent real 404 to the automated browser. The agent logged it and moved on.&lt;/p&gt;

&lt;p&gt;That sounds like a failure in the table, but I think it is one of the most important rows. An agent that "learns" by retrying until something gets through is not learning, it is brute-forcing someone else's site. Abandoning a blocked platform is the correct behaviour, and it should be recorded as a result, not hidden.&lt;/p&gt;

&lt;p&gt;The other guardrails were enforced in code, not described in a prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every run stops at &lt;strong&gt;read-only extraction&lt;/strong&gt;. No checkout, no payment, no account action.&lt;/li&gt;
&lt;li&gt;Human confirmation is required before anything irreversible.&lt;/li&gt;
&lt;li&gt;Throwaway profiles only. No cookies or credentials are committed to the repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd change for a real product
&lt;/h2&gt;

&lt;p&gt;Live scraping per visitor doesn't scale, and it would invite more BigBasket-style blocks on every platform. The repo has a prototype of the better pattern in &lt;code&gt;server/&lt;/code&gt;: a background job is the &lt;em&gt;only&lt;/em&gt; place the agent runs, on a schedule, and an Express server serves the cached results instantly.&lt;/p&gt;

&lt;p&gt;The hard parts left are not code problems: prices vary by pincode because dark stores do, a real product should never ask a visitor for their Blinkit password, and scraping at scale raises terms-of-service questions that more engineering doesn't answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If your agent keeps making the same mistake on the same website, the fix is often not a better model or a longer prompt. It is a small, human-readable file the agent writes after each run and reads before the next one. You can open it, check it, and correct it. And when the agent finally cracks something, the lesson is still there tomorrow.&lt;/p&gt;

&lt;p&gt;The repo, with the full learning log and every strategy file: &lt;a href="https://github.com/victorisaacchinta-eng/basket-buddy" rel="noopener noreferrer"&gt;github.com/victorisaacchinta-eng/basket-buddy&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I wrote this with AI assistance, working from the repo and its learning log. Every attempt count above comes from that log.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Agent Kept Forgetting Our Company, So I Gave It Hindsight</title>
      <dc:creator>Victor Isaac</dc:creator>
      <pubDate>Mon, 28 Sep 2026 22:01:47 +0000</pubDate>
      <link>https://dev.to/victorisaacchintaeng/my-agent-kept-forgetting-our-company-so-i-gave-it-hindsight-37p3</link>
      <guid>https://dev.to/victorisaacchintaeng/my-agent-kept-forgetting-our-company-so-i-gave-it-hindsight-37p3</guid>
      <description>&lt;p&gt;In February, a lender sent my father's interior fit-out company a vendor pre-qualification questionnaire. Forty-odd questions, a folder of supporting documents, and a deadline of the next afternoon. The company was qualified. It had already built six gold-loan branches for another lender. Five months and two reminder emails later, the questionnaire was still not submitted.&lt;/p&gt;

&lt;p&gt;Nobody was lazy. The answers existed. They were just scattered across old email threads, previous submissions and one person's head. Every new questionnaire started from zero.&lt;/p&gt;

&lt;p&gt;That is the problem we built Prequal to solve: an agent that answers vendor questionnaires from a permanent memory of one company, shows the evidence behind every answer, and gets better every time someone corrects it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a normal LLM agent fails here
&lt;/h2&gt;

&lt;p&gt;Our first instinct was the obvious one: paste the company profile into a prompt and let a model fill in the questionnaire. It fails in three ways.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It forgets. Each questionnaire is a fresh session, so last month's corrections are gone.&lt;/li&gt;
&lt;li&gt;It guesses. Ask a model "Do you hold ISO 9001?" with thin context and it will happily produce a confident, plausible, wrong answer.&lt;/li&gt;
&lt;li&gt;It can't show its work. A reviewer has no idea which fact an answer came from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For vendor paperwork, the second failure is the dangerous one. A fabricated certification on a pre-qualification form is worse than no answer at all.&lt;/p&gt;

&lt;p&gt;So the design goal became: the agent should only say what it can prove, and its memory should outlive any single session. That's where &lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight agent memory&lt;/a&gt; came in.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the system hangs together
&lt;/h2&gt;

&lt;p&gt;Prequal is a small FastAPI backend, a single-page review screen, SQLite for questionnaire state, Groq for the language model, and Hindsight as the only place company knowledge lives.&lt;/p&gt;

&lt;p&gt;Every question goes through the same pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Classify.&lt;/strong&gt; Each question has a type: exact field, document request, yes/no, numeric by year, or free text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic answers first.&lt;/strong&gt; GSTIN, PAN, entity type, address: these are looked up directly. No model call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document check.&lt;/strong&gt; "Attach your ISO 9001 certificate" is checked against the company's document inventory. On file, or not held. Still no model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall.&lt;/strong&gt; Everything else pulls evidence from Hindsight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Draft with tools.&lt;/strong&gt; The model must call &lt;code&gt;submit_answer&lt;/code&gt; with the evidence IDs it used, or &lt;code&gt;flag_gap&lt;/code&gt;. Plain text is never treated as an answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails.&lt;/strong&gt; Low confidence or uncited drafts go to human review. Any positive claim about a certification the company doesn't hold is blocked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Automation before AI. The model only sees the questions that actually need judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core story: memory is the product
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised us. Without memory, Prequal is useless. With an empty memory bank, every single question comes back as "needs review". The agent has nothing to stand on, so it says so.&lt;/p&gt;

&lt;p&gt;That's exactly the behaviour we wanted, and it's only possible because the agent treats &lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt; as its source of truth rather than as a nice-to-have.&lt;/p&gt;

&lt;p&gt;We give each company its own memory bank with a mission and three directives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;DIRECTIVES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-fabrication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never state a certification, registration, licence or figure that is not in memory. If it is absent, say it is absent.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When a fact has more than one value over time, report the timeline, most recent first, and name the dates.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer-wins&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prefer the reviewer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s corrected answers over the agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s own earlier drafts.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;On day one we retain three kinds of memory: company facts, one fact per document (on file or not held), and every answer the company has given on past questionnaires. Turnover is retained once per financial year, with a timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;turnover_by_fy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s annual turnover for FY &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; was &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (audited).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;annual turnover from audited accounts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;_fy_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fy&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;financial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That timestamp matters. When a questionnaire asks for "turnover for the last three years", recall returns dated facts, and the review screen renders them as a timeline, newest first. When a new year's figure is retained, it simply appears at the top. No overwriting, no stale numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The learning loop
&lt;/h2&gt;

&lt;p&gt;The part engineers ask about most is how the agent improves. It's almost boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retain_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reviewer-approved answer on the &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; questionnaire &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
               &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to the question &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; Reviewer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s note: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer correction for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every approval and every edit becomes a memory, with the reviewer's reason attached. On the next questionnaire, a similar question recalls the corrected answer and the model cites it. If a reviewer says "always lead with the six-branch project", that preference is in memory for every future questionnaire, from every client.&lt;/p&gt;

&lt;p&gt;This is the difference between an agent with a longer prompt and an agent with memory. A prompt resets. Memory accumulates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in use
&lt;/h2&gt;

&lt;p&gt;A reviewer opens a new questionnaire and clicks run. Questions stream in with one of three statuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-filled:&lt;/strong&gt; answered from an exact field, a document on file, or cited evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Needs you:&lt;/strong&gt; thin evidence or low confidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gap:&lt;/strong&gt; nothing in memory answers it, or the document isn't held.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click any answer and the evidence panel shows exactly which memories it came from, with the cited ones marked. A live log on the right shows every retain, recall and reflect call as it happens. Before answering, one &lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;reflect&lt;/a&gt; call produces a short brief: which past projects matter for this client, which documents are missing.&lt;/p&gt;

&lt;p&gt;The moment that sold us on the approach: asked "Do you hold ISO 9001 certification?", the agent answers "No. We do not hold an ISO 9001 certificate at present" and marks it as a gap for the reviewer to decide. It doesn't invent a certificate number. It tells you what's missing.&lt;/p&gt;

&lt;p&gt;On a full run against a 43-question questionnaire from an NBFC, with real Hindsight memory and Groq's gpt-oss-120b drafting, Prequal answered 38 questions, sent 1 to a human for review, and flagged 4 as gaps. All four gaps were documents the company does not hold. The company in the demo is fictional, modelled on a real firm's structure with synthetic figures.&lt;/p&gt;

&lt;p&gt;Here's a two-minute walkthrough:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/cmx4w6JXkEw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Deterministic first, model second.&lt;/strong&gt; In our 43-question sample, 15 questions are exact fields and 13 are document checks. Sending those to a model adds cost, latency and risk for nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Force the model to cite, then check the citations.&lt;/strong&gt; Tool calls with evidence IDs, validated after the fact, turned "trust me" answers into auditable ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Timestamps are underrated.&lt;/strong&gt; Retaining facts with the date they became true made "what changed" a query instead of a data-cleaning job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Make the empty state honest.&lt;/strong&gt; An agent that says "I don't know this company yet" is more trustworthy than one that fills the silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Function calling fails. Plan for it.&lt;/strong&gt; We validate every tool argument, retry once with the schema echoed back, fall back to a second model, and treat anything else as "needs review". A crash is never an answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Parsing Excel and PDF questionnaires directly, writing answers back into the client's own template, and document expiry alerts so a solvency certificate never lapses silently.&lt;/p&gt;

&lt;p&gt;The bigger lesson is about &lt;a href="https://vectorize.io/what-is-agent-memory" rel="noopener noreferrer"&gt;what agent memory actually is&lt;/a&gt;. It isn't a bigger context window. It's a record that outlives the session, knows when things were true, and gets better every time a human corrects it. For a small company answering the same forty questions over and over, that's the whole product.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/victorisaacchinta-eng/prequal" rel="noopener noreferrer"&gt;https://github.com/victorisaacchinta-eng/prequal&lt;/a&gt;&lt;br&gt;
Demo: &lt;a href="https://youtu.be/cmx4w6JXkEw" rel="noopener noreferrer"&gt;https://youtu.be/cmx4w6JXkEw&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>python</category>
    </item>
  </channel>
</rss>
