<?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: Harish Kotra (he/him)</title>
    <description>The latest articles on DEV Community by Harish Kotra (he/him) (@harishkotra).</description>
    <link>https://dev.to/harishkotra</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F101279%2F516b4cd2-cc6d-451c-a8c8-a7d9ab5ec41a.png</url>
      <title>DEV Community: Harish Kotra (he/him)</title>
      <link>https://dev.to/harishkotra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harishkotra"/>
    <language>en</language>
    <item>
      <title>Building "So Long Sucker Agent Protocol" in Next.js</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Sat, 11 Apr 2026 17:09:53 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-so-long-sucker-agent-protocol-in-nextjs-2l1</link>
      <guid>https://dev.to/harishkotra/building-so-long-sucker-agent-protocol-in-nextjs-2l1</guid>
      <description>&lt;p&gt;Most AI demos show a single model producing a single answer.&lt;/p&gt;

&lt;p&gt;This project explores something messier and more interesting: what happens when multiple AI agents compete in a social strategy game where lying is often rational, alliances are private, and betrayal is a valid path to victory.&lt;/p&gt;

&lt;p&gt;So Long Sucker Agent Protocol is a web-based simulation inspired by John Nash's "So Long Sucker." The twist is that the UI exposes two simultaneous realities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what agents say publicly&lt;/li&gt;
&lt;li&gt;what agents actually intend privately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split turns an ordinary game simulation into an observability tool for strategic deception.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Goal
&lt;/h2&gt;

&lt;p&gt;I wanted a system where four agents would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;play a simplified board game&lt;/li&gt;
&lt;li&gt;form short-lived alliances&lt;/li&gt;
&lt;li&gt;whisper privately to each other&lt;/li&gt;
&lt;li&gt;maintain hidden internal monologues&lt;/li&gt;
&lt;li&gt;make moves that can contradict earlier public promises&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a simulation that feels less like a toy chatbot and more like a live strategy lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 15&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Framer Motion&lt;/li&gt;
&lt;li&gt;Custom orchestration layer for agent inference&lt;/li&gt;
&lt;li&gt;Optional provider integrations:
OpenAI, Featherless, Mistral, and Groq&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  The Core Design Decision: Dual Reality
&lt;/h2&gt;

&lt;p&gt;The app is intentionally built around three message types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MessageType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLIC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WHISPER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;THOUGHT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SYSTEM&lt;/span&gt;&lt;span class="dl"&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 sounds simple, but it changes the whole product.&lt;/p&gt;

&lt;p&gt;Instead of one chat log, the app has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a public narrative everyone can see&lt;/li&gt;
&lt;li&gt;a private alliance layer between agents&lt;/li&gt;
&lt;li&gt;an internal strategy layer visible only in X-Ray mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a much more honest simulation of strategic reasoning, because agents are allowed to perform socially while planning something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modeling the Agents
&lt;/h2&gt;

&lt;p&gt;Each agent has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an identity&lt;/li&gt;
&lt;li&gt;a persona&lt;/li&gt;
&lt;li&gt;a preferred model provider&lt;/li&gt;
&lt;li&gt;a visual color&lt;/li&gt;
&lt;li&gt;memory for public promises and whispers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The personas are intentionally asymmetric:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Optimizer:
rational, mathematical, coalition-focused&lt;/li&gt;
&lt;li&gt;The Romantic:
loyalty-first until emotionally betrayed&lt;/li&gt;
&lt;li&gt;The Skeptic:
paranoid, conspiracy-sensitive&lt;/li&gt;
&lt;li&gt;The Chaos Agent:
erratic and interested in prolonging pain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives the same ruleset very different emotional and strategic outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Turn Engine
&lt;/h2&gt;

&lt;p&gt;The simulation runs through &lt;code&gt;useGameLogic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That hook is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tracking board state&lt;/li&gt;
&lt;li&gt;selecting the active player&lt;/li&gt;
&lt;li&gt;calling the LLM controller&lt;/li&gt;
&lt;li&gt;appending chat events&lt;/li&gt;
&lt;li&gt;resolving challenges&lt;/li&gt;
&lt;li&gt;eliminating agents&lt;/li&gt;
&lt;li&gt;deciding when the game is over&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Core call:&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;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;AgentController&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;boardSummary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;describeBoard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gameState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;publicHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;whisperHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gameState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is a JSON object:&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;"thought"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your hidden strategy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"whisper"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AgentName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Secret message"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"public_message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What you say to everyone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"move"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your game action"&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;That structure is the backbone of the entire app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Design
&lt;/h2&gt;

&lt;p&gt;The prompt has to balance freedom with structure.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;current board state&lt;/li&gt;
&lt;li&gt;public conversation history&lt;/li&gt;
&lt;li&gt;whisper history relevant to that specific agent&lt;/li&gt;
&lt;li&gt;the requirement to return valid JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prompt excerpt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`You are &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. You are playing So Long Sucker.
Current Board: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;boardSummary&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Your Secret Goal: Survive at all costs.
Public History:
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;- None&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Your Secret Whisper History:
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;whisperHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;- None&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Instructions: You must output a JSON object...`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough context for agents to act strategically while preserving room for personality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge Resolution
&lt;/h2&gt;

&lt;p&gt;The ruleset is simplified, but still expressive enough to generate drama.&lt;/p&gt;

&lt;p&gt;When a chip enters a contested area, a challenge can occur. The system then uses other agents' recent strategic outputs to infer who they support.&lt;/p&gt;

&lt;p&gt;That means challenge outcomes are not just mechanical. They are socially mediated by temporary coalition math.&lt;/p&gt;

&lt;p&gt;This is where the simulation starts feeling alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Betrayal Detection
&lt;/h2&gt;

&lt;p&gt;One of my favorite details is the betrayal alert.&lt;/p&gt;

&lt;p&gt;The app tracks public promises from each agent. If an internal thought later contains betrayal-like intent while recent public messaging contained alliance-like language, the UI flags it.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;betrayal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;promiseKeywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;latestPromise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;betrayalKeywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;loweredThought&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not perfect natural-language reasoning, but it is a strong enough heuristic to surface "you said trust, but you meant sacrifice."&lt;/p&gt;

&lt;h2&gt;
  
  
  UI Design
&lt;/h2&gt;

&lt;p&gt;I wanted the UI to feel like a command center rather than a dashboard template.&lt;/p&gt;

&lt;p&gt;So the visual choices leaned toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dark war-room surfaces&lt;/li&gt;
&lt;li&gt;luminous accents&lt;/li&gt;
&lt;li&gt;stacked feed cards&lt;/li&gt;
&lt;li&gt;animated chips&lt;/li&gt;
&lt;li&gt;alert flashes on betrayal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The layout is split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;left column:
board state, agent summaries, simulation context&lt;/li&gt;
&lt;li&gt;right column:
communication stream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the public-vs-private tension easy to understand conceptually, even if the board logic itself can still be improved visually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why The Local Fallback Matters
&lt;/h2&gt;

&lt;p&gt;A prototype like this should still run without live API keys.&lt;/p&gt;

&lt;p&gt;So the app includes deterministic fallback personas inside &lt;code&gt;AgentController&lt;/code&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the demo remains interactive&lt;/li&gt;
&lt;li&gt;the UI can be tested offline&lt;/li&gt;
&lt;li&gt;contributors can work on state and presentation without setting up model providers first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a small engineering decision that improves developer experience a lot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d Improve Next
&lt;/h2&gt;

&lt;p&gt;The biggest current limitation is readability of the board state during live play.&lt;/p&gt;

&lt;p&gt;The strongest next improvements would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move trails between turns&lt;/li&gt;
&lt;li&gt;explicit challenge panels&lt;/li&gt;
&lt;li&gt;alliance graph visualization&lt;/li&gt;
&lt;li&gt;turn-by-turn replay mode&lt;/li&gt;
&lt;li&gt;chip counts embedded directly onto board sectors&lt;/li&gt;
&lt;li&gt;a "why this happened" explainer for coalition outcomes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From an architecture standpoint, I would also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move model calls server-side&lt;/li&gt;
&lt;li&gt;persist runs in a database&lt;/li&gt;
&lt;li&gt;add seeded deterministic simulation mode&lt;/li&gt;
&lt;li&gt;add replay exports&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contribution Opportunities
&lt;/h2&gt;

&lt;p&gt;This is a strong project for contributors because it has work at multiple levels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI polish&lt;/li&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;prompt engineering&lt;/li&gt;
&lt;li&gt;multiplayer or human-agent modes&lt;/li&gt;
&lt;li&gt;analytics and replay tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some good starter issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add an event timeline scrubber&lt;/li&gt;
&lt;li&gt;implement per-agent whisper inbox panes&lt;/li&gt;
&lt;li&gt;visualize trust as a graph&lt;/li&gt;
&lt;li&gt;add challenge breakdown cards&lt;/li&gt;
&lt;li&gt;add simulation presets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Most AI apps are optimized for answers.&lt;/p&gt;

&lt;p&gt;This one is optimized for motives.&lt;/p&gt;

&lt;p&gt;That makes it useful not just as a game, but as a lens into multi-agent systems, incentive design, and how quickly "alignment" unravels when survival and social ambiguity are both part of the rules.&lt;/p&gt;

&lt;p&gt;If you're building agent systems, simulations like this are worth paying attention to. They reveal failure modes, persuasion patterns, and emergent strategies much faster than polished demos ever will.&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.amazonaws.com%2Fuploads%2Farticles%2Fnjnqgwux804xvoa56fld.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnjnqgwux804xvoa56fld.gif" alt="How this works" width="200" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/So-Long-Sucker-Protocol" rel="noopener noreferrer"&gt;https://github.com/harishkotra/So-Long-Sucker-Protocol&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building an iMessage-Native Decision Agent with Photon iMessage Kit</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Fri, 10 Apr 2026 13:14:31 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-an-imessage-native-decision-agent-with-photon-imessage-kit-1gb3</link>
      <guid>https://dev.to/harishkotra/building-an-imessage-native-decision-agent-with-photon-imessage-kit-1gb3</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;We built &lt;strong&gt;Future-Me Courtroom&lt;/strong&gt;, an iMessage-native agent that turns a dilemma into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 competing long-horizon perspectives,&lt;/li&gt;
&lt;li&gt;1 forced verdict,&lt;/li&gt;
&lt;li&gt;1 concrete next action,&lt;/li&gt;
&lt;li&gt;and an accountability loop via scheduled follow-ups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack: &lt;strong&gt;Bun + TypeScript + @photon-ai/imessage-kit + OpenAI Responses API&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Product Idea
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Text your dilemma, and three versions of your future self argue the case and force a verdict.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal was not “another chat bot.” The goal was behavior change through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;constraint-driven reasoning,&lt;/li&gt;
&lt;li&gt;concrete execution steps,&lt;/li&gt;
&lt;li&gt;and continuity across conversations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Photon iMessage Kit
&lt;/h2&gt;

&lt;p&gt;Photon solves the hardest part: robust local iMessage automation on macOS.&lt;/p&gt;

&lt;p&gt;What we used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;startWatching&lt;/code&gt; for real-time inbound messages,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send&lt;/code&gt; for outbound replies,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MessageScheduler&lt;/code&gt; for deferred nudges,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Reminders&lt;/code&gt; for natural-language reminder creation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw85buzlwk0grgbywxd1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw85buzlwk0grgbywxd1.png" alt="High-Level Architecture" width="800" height="71"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Runtime Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Load runtime env (&lt;code&gt;.env&lt;/code&gt;, fallback parent &lt;code&gt;.env&lt;/code&gt;, or &lt;code&gt;COURT_ENV_PATH&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Boot &lt;code&gt;IMessageSDK&lt;/code&gt; and watcher.&lt;/li&gt;
&lt;li&gt;For each inbound direct message:

&lt;ul&gt;
&lt;li&gt;skip self-sent events,&lt;/li&gt;
&lt;li&gt;dedupe by GUID and short-window normalized text,&lt;/li&gt;
&lt;li&gt;route commands (&lt;code&gt;help&lt;/code&gt;, &lt;code&gt;appeal&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, etc.),&lt;/li&gt;
&lt;li&gt;otherwise invoke LLM courtroom reasoning.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Persist updated memory and optionally schedule a follow-up nudge.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Core Implementation Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Inbound reliability guards
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;alreadyProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;isDuplicateInboundText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;echoGuard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRecentEcho&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This protects against duplicate watcher events and self-thread reflections.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Structured LLM output contract
&lt;/h3&gt;

&lt;p&gt;We force a JSON schema response and parse resiliently across output shapes.&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json_schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;future_me_courtroom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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;Fallback logic ensures a deterministic response if model calls fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Attachment evidence mode
&lt;/h3&gt;

&lt;p&gt;Any inbound attachment is summarized and injected as explicit reasoning constraints.&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;attachmentBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hasAttachments&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\n\nEVIDENCE ATTACHMENTS:\n- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attachmentSummaries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;- &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\nUse these as factual constraints in your reasoning.`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) Natural-language reminders
&lt;/h3&gt;

&lt;p&gt;We use Photon’s &lt;code&gt;Reminders&lt;/code&gt; wrapper for simple scheduling UX.&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;reminderId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reminders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tomorrow 9am&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replyTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ship the draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Memory Model
&lt;/h2&gt;

&lt;p&gt;Memory is persisted in local JSON per chat key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;values&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;avoidances&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;identity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cases[]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each case stores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dilemma summary,&lt;/li&gt;
&lt;li&gt;verdict,&lt;/li&gt;
&lt;li&gt;why-now,&lt;/li&gt;
&lt;li&gt;first action,&lt;/li&gt;
&lt;li&gt;fallback,&lt;/li&gt;
&lt;li&gt;confidence,&lt;/li&gt;
&lt;li&gt;callback question,&lt;/li&gt;
&lt;li&gt;timestamp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the bot adaptive across sessions while remaining inspectable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Edge Cases We Designed For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate inbound event handling.&lt;/li&gt;
&lt;li&gt;Echoed message suppression.&lt;/li&gt;
&lt;li&gt;Empty model output or unexpected output format.&lt;/li&gt;
&lt;li&gt;Attachment-only messages without dilemma text.&lt;/li&gt;
&lt;li&gt;Reminder parse failures with recoverable guidance.&lt;/li&gt;
&lt;li&gt;Optional thread allowlist for safer production rollout.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Local Dev + Validation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run lint
npm run type-check
npm run &lt;span class="nb"&gt;test
&lt;/span&gt;bun run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What We’d Ship Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Retrieval over historical iMessage context via &lt;code&gt;getMessages()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Group “jury mode” in shared chats.&lt;/li&gt;
&lt;li&gt;Outcome tracking for confidence calibration.&lt;/li&gt;
&lt;li&gt;Weekly report export via &lt;code&gt;sendFiles()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Plugin-based analytics and observability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project shows that the strongest “agent UX” may not be another web app. It can be a high-leverage behavior loop in the messaging channel people already use every day.&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.amazonaws.com%2Fuploads%2Farticles%2Fssxtq8b84w3f6m52r1uh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fssxtq8b84w3f6m52r1uh.png" alt="How this agent works" width="698" height="1086"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/future-me-courtroom-agent" rel="noopener noreferrer"&gt;https://github.com/harishkotra/future-me-courtroom-agent&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Disarming the "Join Bomb": Re-Engineering Collaborative Filtering on Neo4j</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Thu, 09 Apr 2026 13:19:22 +0000</pubDate>
      <link>https://dev.to/harishkotra/disarming-the-join-bomb-re-engineering-collaborative-filtering-on-neo4j-369h</link>
      <guid>https://dev.to/harishkotra/disarming-the-join-bomb-re-engineering-collaborative-filtering-on-neo4j-369h</guid>
      <description>&lt;p&gt;If you are building a recommendation engine in a graph database, there is one critical juncture where your seemingly innocent query suddenly grinds to a halt. In relational SQL, we call it the N+1 problem or Cartesian Explosions. In Neo4j, it's an unoptimized biderectional traversal in a highly dense graph—what I like to call the &lt;strong&gt;"Join Bomb"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To explore the mechanics of this performance bottleneck and how to eliminate it, I built a local &lt;strong&gt;Neo4j Performance Lab&lt;/strong&gt;—a Streamlit application that pits a "Naive" Cypher query against an "Optimized" APOC-driven query on a massive synthetic dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Before jumping into the queries, let's look at what we're working with:&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.amazonaws.com%2Fuploads%2Farticles%2Fvlrfe844xu7y8dhpo41c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlrfe844xu7y8dhpo41c.png" alt="The Architecture" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We generate a graph consisting of &lt;code&gt;Users&lt;/code&gt;, &lt;code&gt;Products&lt;/code&gt;, and &lt;code&gt;Categories&lt;/code&gt;. To demonstrate the problem accurately, we seed 1,000 Users and 5,000 Products but forcefully generate &lt;strong&gt;100,000+ &lt;code&gt;BOUGHT&lt;/code&gt; relationships&lt;/strong&gt;. This high density is designed to trap our unoptimized queries in exponentially growing traversal paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The Naive Traversal
&lt;/h2&gt;

&lt;p&gt;In collaborative filtering, the standard question is: &lt;em&gt;"What products in Category X should we recommend based on what similar users bought?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The intuitive, naive way to write this in Cypher is a direct traversal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;target:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$user_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;item:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;peer:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;reco:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BELONGS_TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;c.name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;$category&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;reco.price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;$max_price&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;reco&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;reco.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;frequency&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;frequency&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why does this fail at scale?
&lt;/h3&gt;

&lt;p&gt;Neo4j processes matching patterns left-to-right. In a massive graph:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It expands from the &lt;code&gt;User&lt;/code&gt; to their items (10s of records).&lt;/li&gt;
&lt;li&gt;It expands backwards from those items to &lt;em&gt;everyone&lt;/em&gt; who bought them (10,000s of paths).&lt;/li&gt;
&lt;li&gt;It expands forwards from &lt;em&gt;every&lt;/em&gt; peer to &lt;em&gt;everything&lt;/em&gt; they bought (Millions of paths).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only after&lt;/strong&gt; traversing millions of edges does it evaluate the &lt;code&gt;WHERE&lt;/code&gt; clause to filter out the wrong categories and prices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This results in a &lt;code&gt;NodeByLabelScan&lt;/code&gt; or massive &lt;code&gt;Expand(All)&lt;/code&gt; operators that inflate your total Database Hits astronomically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Indexing and APOC Intersections
&lt;/h2&gt;

&lt;p&gt;To solve this we must invert the traversal and minimize path expansions by using &lt;strong&gt;APOC Collections&lt;/strong&gt; and early index filtering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 1: O(1) collection of what our target user owns&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;u:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$user_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p.id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;user_products&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2: Use an explicit NodeIndexSeek to start small&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$category&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="py"&gt;c:&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;reco:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BELONGS_TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: Fast Relationship Filtering earlier in the pipeline&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;peer:&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r2:&lt;/span&gt;&lt;span class="n"&gt;BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reco&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r2.price_at_purchase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;$max_price&lt;/span&gt;

&lt;span class="c1"&gt;// Step 4: Intersect natively using APOC without expanding the graph geometry&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:BOUGHT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;peer_p:&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;user_products&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reco&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_p.id&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;peer_products&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;user_products&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reco&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_products&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apoc.coll.intersection&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_products&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_products&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;shared_items&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_items&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;AND&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;reco.id&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;user_products&lt;/span&gt;

&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;reco.name&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Performance Delta
&lt;/h3&gt;

&lt;p&gt;When measured in the Streamlit lab, the performance metrics shift drastically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Naive Query:&lt;/strong&gt; ~4,500+ DB hits, &amp;gt;120ms total execution time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Optimized Query:&lt;/strong&gt; DB hits plummet, execution time drops massively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of scanning all users, we perform a &lt;strong&gt;NodeIndexSeek&lt;/strong&gt; on the exact category. We apply the price filter strictly on the relationship property &lt;code&gt;price_at_purchase&lt;/code&gt; before expanding any further. &lt;/p&gt;

&lt;p&gt;Most importantly, we avoid the bidirectional Join Bomb. Instead of matching paths back to shared products, we use &lt;code&gt;apoc.coll.intersection()&lt;/code&gt;. Calculating overlap in local, in-memory arrays circumvents traversing thousands of node-relationships recursively in the query planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Local AI Explainability
&lt;/h2&gt;

&lt;p&gt;Because debugging query metadata is notoriously dry, I hooked the lab up to &lt;strong&gt;Ollama&lt;/strong&gt; running &lt;code&gt;llama3.2&lt;/code&gt; locally. By extracting the tree from Neo4j's &lt;code&gt;.profile&lt;/code&gt; data, the Streamlit app asks the local LLM to explain why the execution was fast or slow. The LLM accurately identifies &lt;code&gt;NodeByLabelScan&lt;/code&gt; vs &lt;code&gt;Filter&lt;/code&gt; operator placements, transforming the app into a fantastic interview or presentation tool.&lt;/p&gt;

&lt;p&gt;If you are dealing with graph scale, stop writing naive traversals! Build pipelines that respect the planner.&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.amazonaws.com%2Fuploads%2Farticles%2Fpuz54tcxfc46j0pkd6fz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpuz54tcxfc46j0pkd6fz.png" alt="Example Output" width="800" height="1083"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code is available on my Github: &lt;a href="https://github.com/harishkotra/realtime-recommendation-engine" rel="noopener noreferrer"&gt;https://github.com/harishkotra/realtime-recommendation-engine&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building Local Agent Studio: A Local-First OSS Multi-Agent Orchestration App</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Wed, 08 Apr 2026 15:41:59 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-local-agent-studio-a-local-first-oss-multi-agent-orchestration-app-1fe3</link>
      <guid>https://dev.to/harishkotra/building-local-agent-studio-a-local-first-oss-multi-agent-orchestration-app-1fe3</guid>
      <description>&lt;p&gt;Local Agent Studio started as a practical question:&lt;/p&gt;

&lt;p&gt;How do you build a multi-agent orchestration product that is visual, local-first, provider-flexible, and understandable by developers?&lt;/p&gt;

&lt;p&gt;The answer we shipped in &lt;code&gt;v0.0.1&lt;/code&gt; is a focused MVP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React Flow for the orchestration canvas&lt;/li&gt;
&lt;li&gt;Next.js for the application shell and API routes&lt;/li&gt;
&lt;li&gt;TypeScript for the runtime and shared contracts&lt;/li&gt;
&lt;li&gt;SQLite for local persistence&lt;/li&gt;
&lt;li&gt;SSE for live execution traces&lt;/li&gt;
&lt;li&gt;provider adapters for Ollama, OpenAI-compatible endpoints, and OpenAI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post breaks down the architecture, the execution model, and the product choices behind the first release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Goals
&lt;/h2&gt;

&lt;p&gt;The app was designed around a few non-negotiables:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Users should be able to run it locally.&lt;/li&gt;
&lt;li&gt;Users should be able to bring their own keys and providers.&lt;/li&gt;
&lt;li&gt;Each agent should be independently configurable.&lt;/li&gt;
&lt;li&gt;Workflows should be visual and inspectable.&lt;/li&gt;
&lt;li&gt;Runs should emit enough trace information to understand what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That led to a design where the studio is both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a builder for workflows and agent profiles&lt;/li&gt;
&lt;li&gt;a runtime console for local orchestration execution&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk03o6w0b09mqk43fdetc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk03o6w0b09mqk43fdetc.png" alt="High-Level Architecture" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key architectural decision was to keep contracts centralized. The UI, API, and runtime all share the same Zod-backed schema package so the orchestration data model does not drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Monorepo
&lt;/h2&gt;

&lt;p&gt;The project is split into three main packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web
packages/shared
packages/orchestrator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps responsibilities separated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apps/web&lt;/code&gt; owns UI, API routes, and local persistence&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/shared&lt;/code&gt; owns the type-safe contracts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/orchestrator&lt;/code&gt; owns execution behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split matters because orchestration products get brittle fast when the builder schema, database payloads, and runtime assumptions diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shared Contract Layer
&lt;/h2&gt;

&lt;p&gt;The shared schema package defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;providers&lt;/li&gt;
&lt;li&gt;agent profiles&lt;/li&gt;
&lt;li&gt;workflow nodes and edges&lt;/li&gt;
&lt;li&gt;run events&lt;/li&gt;
&lt;li&gt;run records&lt;/li&gt;
&lt;li&gt;export/import snapshot shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a representative piece of the contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;providerTypeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ollama&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai_compatible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the workflow node union:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowNodeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;discriminatedUnion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;inputNodeSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;agentNodeSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;routerNodeSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;httpToolNodeSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;outputNodeSchema&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;This gives the whole stack a single source of truth. If a node or provider changes shape, everything that depends on it gets type pressure immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Flow
&lt;/h2&gt;

&lt;p&gt;React Flow is a strong fit for this class of product because it already solves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;draggable node layout&lt;/li&gt;
&lt;li&gt;handles and edges&lt;/li&gt;
&lt;li&gt;view controls and panels&lt;/li&gt;
&lt;li&gt;custom node rendering&lt;/li&gt;
&lt;li&gt;viewport state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That let us spend time on domain concerns instead of rebuilding graph primitives from scratch.&lt;/p&gt;

&lt;p&gt;In the MVP, the canvas supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom agent cards&lt;/li&gt;
&lt;li&gt;graph editing&lt;/li&gt;
&lt;li&gt;connection creation&lt;/li&gt;
&lt;li&gt;theme-aware rendering&lt;/li&gt;
&lt;li&gt;lock and viewport controls&lt;/li&gt;
&lt;li&gt;inspector-driven node configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agent Model
&lt;/h2&gt;

&lt;p&gt;One of the core product decisions was that each agent profile should carry its own provider and model selection.&lt;/p&gt;

&lt;p&gt;That means the system is not tied to a single workspace-wide model choice.&lt;/p&gt;

&lt;p&gt;An agent profile includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;role&lt;/li&gt;
&lt;li&gt;provider&lt;/li&gt;
&lt;li&gt;model&lt;/li&gt;
&lt;li&gt;system prompt&lt;/li&gt;
&lt;li&gt;profile type&lt;/li&gt;
&lt;li&gt;notes&lt;/li&gt;
&lt;li&gt;allowed tools&lt;/li&gt;
&lt;li&gt;generation settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentProfileSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;profileType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;general&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;agentRoleSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;providerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&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="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&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="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1200&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;That design makes mixed-provider graphs straightforward. A coordinator can run on local Ollama while a worker uses a remote OpenAI-compatible model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider Abstraction
&lt;/h2&gt;

&lt;p&gt;The provider layer uses a common adapter interface so the runtime does not care whether the backing model is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local Ollama&lt;/li&gt;
&lt;li&gt;OpenAI&lt;/li&gt;
&lt;li&gt;a third-party OpenAI-compatible endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That abstraction is the difference between a flexible orchestration platform and a model-specific app.&lt;/p&gt;

&lt;p&gt;Featherless.ai was intentionally modeled as OpenAI-compatible instead of a custom provider branch. That avoids provider sprawl and keeps the system extensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime Design
&lt;/h2&gt;

&lt;p&gt;The orchestration runtime has a small, explicit responsibility set:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;validate the workflow&lt;/li&gt;
&lt;li&gt;build dependency maps&lt;/li&gt;
&lt;li&gt;execute nodes in dependency-safe order&lt;/li&gt;
&lt;li&gt;stream lifecycle events&lt;/li&gt;
&lt;li&gt;persist run state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first important runtime guardrail is DAG validation:&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;function&lt;/span&gt; &lt;span class="nf"&gt;validateDag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowDefinition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outgoing&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildMaps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workflow&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;inDegree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;degree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;inDegree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;degree&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;degree&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&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="nx"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &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;edge&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodeId&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="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inDegree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;inDegree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visited&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Workflow must be a DAG for this MVP.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an MVP, DAG-only execution is the right constraint. Cycles, resumable long-running jobs, and schedulers all complicate failure handling and state recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node Execution
&lt;/h2&gt;

&lt;p&gt;The runtime supports these node types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;input&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;agent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;router&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http_tool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;output&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each type maps to a different execution path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;input&lt;/code&gt; resolves templated user input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent&lt;/code&gt; calls an LLM provider adapter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;router&lt;/code&gt; picks the next logical route from structured output&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http_tool&lt;/code&gt; calls external HTTP endpoints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output&lt;/code&gt; materializes a final output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For agent nodes, the runtime composes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the node prompt&lt;/li&gt;
&lt;li&gt;workflow inputs&lt;/li&gt;
&lt;li&gt;upstream node outputs&lt;/li&gt;
&lt;li&gt;the agent system prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives each node enough context to behave like a stage in a larger orchestration rather than a standalone chat call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming and Traces
&lt;/h2&gt;

&lt;p&gt;One of the biggest UX wins in orchestration products is showing execution as it happens.&lt;/p&gt;

&lt;p&gt;The app emits structured events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;queued&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;started&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stream_delta&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;completed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are persisted and streamed over SSE to the UI. The benefit is immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;nodes can glow or update status live&lt;/li&gt;
&lt;li&gt;users can inspect progress before completion&lt;/li&gt;
&lt;li&gt;failures are easier to localize&lt;/li&gt;
&lt;li&gt;run history survives refresh and restart&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Persistence Strategy
&lt;/h2&gt;

&lt;p&gt;The app uses SQLite with JSON payload tables rather than over-modeling the schema too early.&lt;/p&gt;

&lt;p&gt;That is a pragmatic MVP tradeoff:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;faster iteration on contracts&lt;/li&gt;
&lt;li&gt;easy local setup&lt;/li&gt;
&lt;li&gt;fewer migration concerns in the first release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The database bootstrap is deliberately simple:&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
  CREATE TABLE IF NOT EXISTS providers (
    id TEXT PRIMARY KEY,
    json TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS agents (
    id TEXT PRIMARY KEY,
    json TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS workflows (
    id TEXT PRIMARY KEY,
    json TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS runs (
    id TEXT PRIMARY KEY,
    json TEXT NOT NULL
  );
  CREATE TABLE IF NOT EXISTS run_events (
    id TEXT PRIMARY KEY,
    run_id TEXT NOT NULL,
    json TEXT NOT NULL
  );
`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That said, the roadmap already includes schema-versioned export/import and snapshots, because long-term portability needs more deliberate version control.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI Structure
&lt;/h2&gt;

&lt;p&gt;The product shell is organized around three zones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
    L["Left Sidebar&amp;lt;br/&amp;gt;Agents, Providers, Runs"] --&amp;gt; C["Center Canvas&amp;lt;br/&amp;gt;React Flow Builder"]
    C --&amp;gt; R["Right Inspector&amp;lt;br/&amp;gt;Node Config + Run Trace"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This division works because each zone answers a different user question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;left: what assets do I have?&lt;/li&gt;
&lt;li&gt;center: how does the workflow connect?&lt;/li&gt;
&lt;li&gt;right: what is selected and what happened during execution?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Theme and Interaction Choices
&lt;/h2&gt;

&lt;p&gt;The MVP supports both dark and light mode. That is more than aesthetic polish. Many orchestration tools default to dark-only interfaces even when users spend hours inside them.&lt;/p&gt;

&lt;p&gt;The product also improved graph usability with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clearer connection affordances&lt;/li&gt;
&lt;li&gt;lockable grid behavior&lt;/li&gt;
&lt;li&gt;model pickers in the right contexts&lt;/li&gt;
&lt;li&gt;Ollama model discovery&lt;/li&gt;
&lt;li&gt;explicit provider edit modal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation Strategy
&lt;/h2&gt;

&lt;p&gt;We also built a GitHub Releases-based installer.&lt;/p&gt;

&lt;p&gt;Instead of forcing users to clone the repo, the product can be distributed through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/harishkotra/local-agent-studio/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer is designed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detect OS and architecture&lt;/li&gt;
&lt;li&gt;download a versioned release asset&lt;/li&gt;
&lt;li&gt;verify checksums&lt;/li&gt;
&lt;li&gt;install into a user-local directory&lt;/li&gt;
&lt;li&gt;expose a launcher command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That matters because onboarding friction is often the difference between “interesting OSS project” and “thing people actually try.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Local-First Matters
&lt;/h2&gt;

&lt;p&gt;This architecture is not local-first as a branding slogan. It changes system design in concrete ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider keys are local&lt;/li&gt;
&lt;li&gt;SQLite is local&lt;/li&gt;
&lt;li&gt;workflows can be exported and imported&lt;/li&gt;
&lt;li&gt;Ollama is a first-class provider&lt;/li&gt;
&lt;li&gt;hosted infrastructure is optional rather than mandatory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the product attractive for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;developers experimenting with orchestration&lt;/li&gt;
&lt;li&gt;privacy-sensitive users&lt;/li&gt;
&lt;li&gt;teams that want to self-host or fork&lt;/li&gt;
&lt;li&gt;builders who prefer infrastructure they can inspect&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap Directions
&lt;/h2&gt;

&lt;p&gt;Several next steps are already tracked in GitHub issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run observability&lt;/li&gt;
&lt;li&gt;snapshots and versioning&lt;/li&gt;
&lt;li&gt;workflow inputs&lt;/li&gt;
&lt;li&gt;validation guardrails&lt;/li&gt;
&lt;li&gt;AgentSkills compatibility&lt;/li&gt;
&lt;li&gt;workspace-aware orchestration&lt;/li&gt;
&lt;li&gt;review gates&lt;/li&gt;
&lt;li&gt;output diffing&lt;/li&gt;
&lt;li&gt;kanban-style operations board&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those issues are valuable because they turn product intuition into implementation-ready work items.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons From the Build
&lt;/h2&gt;

&lt;p&gt;A few things stand out after shipping the first release:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Shared contracts reduce chaos
&lt;/h3&gt;

&lt;p&gt;The Zod schema layer keeps the UI, database, and runtime aligned.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Visual orchestration only works if traces are strong
&lt;/h3&gt;

&lt;p&gt;A graph alone is not enough. Users need live node state and persisted event history.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Provider flexibility has to exist at the agent level
&lt;/h3&gt;

&lt;p&gt;Anything less becomes a bottleneck almost immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Local-first products still need distribution polish
&lt;/h3&gt;

&lt;p&gt;The installer and release flow are not optional extras. They are part of adoption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Local Agent Studio is still early, but the foundation is now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visual workflow builder&lt;/li&gt;
&lt;li&gt;provider-flexible agents&lt;/li&gt;
&lt;li&gt;local persistence&lt;/li&gt;
&lt;li&gt;DAG execution runtime&lt;/li&gt;
&lt;li&gt;live traces&lt;/li&gt;
&lt;li&gt;one-line install path&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes it a useful base for both users and contributors.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/k2DlzuZAOW8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Built by &lt;a href="https://harishkotra.me" rel="noopener noreferrer"&gt;Harish Kotra&lt;/a&gt;. More builds at &lt;a href="https://dailybuild.xyz" rel="noopener noreferrer"&gt;dailybuild.xyz&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building Darwin.js: A Self-Evolving Agentic Bazaar with FastAPI, Next.js, ChromaDB, and Live Code Mutation</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Tue, 07 Apr 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-darwinjs-a-self-evolving-agentic-bazaar-with-fastapi-nextjs-chromadb-and-live-code-1lkm</link>
      <guid>https://dev.to/harishkotra/building-darwinjs-a-self-evolving-agentic-bazaar-with-fastapi-nextjs-chromadb-and-live-code-1lkm</guid>
      <description>&lt;p&gt;Darwin.js started from a simple prompt:&lt;/p&gt;

&lt;p&gt;What if non-player characters could rewrite their own source code when players discovered exploits?&lt;/p&gt;

&lt;p&gt;That idea turns into a live simulation with four moving parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a FastAPI backend that simulates a bazaar&lt;/li&gt;
&lt;li&gt;merchants that execute Python logic from local files&lt;/li&gt;
&lt;li&gt;a Governor that monitors trade telemetry&lt;/li&gt;
&lt;li&gt;a frontend that makes the entire adaptation loop visible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post walks through how the system works, the tradeoffs behind the architecture, and how we made it demoable end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Idea
&lt;/h2&gt;

&lt;p&gt;The app presents a cyber-bazaar where merchants sell items, take losses, and get attacked by a player using exploit presets like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;integer overflow&lt;/li&gt;
&lt;li&gt;re-entrancy attack&lt;/li&gt;
&lt;li&gt;item duplication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the losses cross a threshold, the system mutates the merchant’s local &lt;code&gt;trade(context)&lt;/code&gt; function and hot-reloads the new behavior.&lt;/p&gt;

&lt;p&gt;This is important: the mutation is not hidden in logs. The app exposes the entire adaptive loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exploit trigger&lt;/li&gt;
&lt;li&gt;telemetry logging&lt;/li&gt;
&lt;li&gt;anomaly detection&lt;/li&gt;
&lt;li&gt;code rewrite&lt;/li&gt;
&lt;li&gt;post-mutation diff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That visibility is the difference between “AI magic” and a real systems demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  1. Merchant logic is loaded from disk
&lt;/h3&gt;

&lt;p&gt;Each merchant owns a file path that points at its current logic blob.&lt;/p&gt;

&lt;p&gt;That matters because mutation becomes tangible. We are not just changing in-memory rules. We are actually rewriting the file that defines behavior.&lt;/p&gt;

&lt;p&gt;Simplified shape:&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MerchantAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;gold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logic_blob_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logic_blob_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Trade logic runs in a restricted sandbox
&lt;/h3&gt;

&lt;p&gt;Instead of blindly executing arbitrary Python, the backend parses the AST and blocks dangerous constructs.&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;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exec&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;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;With&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Try&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClassDef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Global&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nonlocal&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Disallowed syntax&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;We also strip &lt;code&gt;__future__&lt;/code&gt; imports before execution so merchants can keep ergonomic source files without tripping the restricted runtime.&lt;/p&gt;

&lt;p&gt;That gives us a middle ground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enough flexibility to demonstrate self-modifying logic&lt;/li&gt;
&lt;li&gt;enough guardrails to avoid turning the demo into arbitrary code execution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. MarketMemory stores telemetry
&lt;/h3&gt;

&lt;p&gt;Every trade is converted into structured metadata:&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="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merchant_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;merchant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;player_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;player_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loss_to_npc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;loss_to_npc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exploit_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exploit_type&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;none&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;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;utc_now&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;ChromaDB is used when available, but the system is intentionally resilient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it can fall back to in-memory collections&lt;/li&gt;
&lt;li&gt;it uses deterministic embeddings for stability in constrained environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last choice matters. In a pure demo setting, the worst outcome is a backend that fails because a local ONNX embedding pipeline cannot initialize. We optimized for a stable runtime over fancy embeddings.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Governor decides when to evolve
&lt;/h3&gt;

&lt;p&gt;The Governor is the bridge between observability and adaptation.&lt;/p&gt;

&lt;p&gt;It asks questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much gold has this merchant lost?&lt;/li&gt;
&lt;li&gt;Is one item being abused repeatedly?&lt;/li&gt;
&lt;li&gt;Is an exploit signature recurring?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Core logic:&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;trigger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merchant_loss&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutation_threshold&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;hottest_item_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anomaly_limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once triggered, the Governor packages the latest telemetry and sends it to the evolution engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. EvolutionService rewrites merchant code
&lt;/h3&gt;

&lt;p&gt;In a production-grade system, this is where you would call a live model such as Codex or the Responses API. In Darwin.js, the interface is already shaped that way, but the implementation is deterministic so the demo remains runnable without network access.&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;new_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mutator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_logic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;current_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;exploit_telemetry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exploit_telemetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;npc_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;npc_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logic_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;The generated code introduces defenses like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;price caps&lt;/li&gt;
&lt;li&gt;duplication fingerprints&lt;/li&gt;
&lt;li&gt;player blacklisting&lt;/li&gt;
&lt;li&gt;cooldown locks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means every mutation leaves a real artifact on disk and a visible diff in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend Design
&lt;/h2&gt;

&lt;p&gt;The key design goal was clarity under complexity.&lt;/p&gt;

&lt;p&gt;We needed to show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;global system relationships&lt;/li&gt;
&lt;li&gt;per-merchant state&lt;/li&gt;
&lt;li&gt;exploit controls&lt;/li&gt;
&lt;li&gt;mutation output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without turning the screen into mush.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why React Flow was the right choice
&lt;/h2&gt;

&lt;p&gt;The “God View” became the right abstraction because cards alone don’t explain causality.&lt;/p&gt;

&lt;p&gt;A merchant card can show &lt;code&gt;gold: 9800&lt;/code&gt;, but it cannot show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Governor supervising it&lt;/li&gt;
&lt;li&gt;memory feeding anomaly signals back upstream&lt;/li&gt;
&lt;li&gt;the player injecting exploits into a specific node&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React Flow solves that cleanly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;nodeTypes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodeTypes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;fitView&lt;/span&gt;
  &lt;span class="na"&gt;nodesDraggable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;nodesConnectable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;elementsSelectable&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Background&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#0f2740"&lt;/span&gt; &lt;span class="na"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Controls&lt;/span&gt; &lt;span class="na"&gt;showInteractive&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the system graph existed, the rest of the UI could stay focused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merchant cards explain local state&lt;/li&gt;
&lt;li&gt;terminal logs explain narrative sequence&lt;/li&gt;
&lt;li&gt;the diff viewer explains mutation output&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  UI Architecture
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Example exploit flow
&lt;/h2&gt;

&lt;p&gt;Let’s look at the “Integer Overflow” style demo path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user selects a merchant in the UI.&lt;/li&gt;
&lt;li&gt;The frontend posts to &lt;code&gt;/api/bazaar/exploit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The server translates that exploit into a payload like:
&lt;/li&gt;
&lt;/ol&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;"merchant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merchant-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"player_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"judge-player"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exploit_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer_overflow"&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;ol&gt;
&lt;li&gt;The merchant executes vulnerable sell logic.&lt;/li&gt;
&lt;li&gt;The trade produces outsized loss.&lt;/li&gt;
&lt;li&gt;The Governor sees the anomaly.&lt;/li&gt;
&lt;li&gt;The evolution engine writes a patched &lt;code&gt;trade(context)&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;The frontend refreshes, highlighting a new merchant revision and showing the code diff.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What was intentionally designed for demo stability
&lt;/h2&gt;

&lt;p&gt;When building AI-heavy showcases, you have to decide what can fail and what absolutely cannot fail.&lt;/p&gt;

&lt;p&gt;For Darwin.js, the core experience needed to survive offline and sandboxed environments.&lt;/p&gt;

&lt;p&gt;That led to a few decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local/system font stacks instead of remote font fetching&lt;/li&gt;
&lt;li&gt;deterministic mutator instead of mandatory live API calls&lt;/li&gt;
&lt;li&gt;ChromaDB with fallback-friendly behavior&lt;/li&gt;
&lt;li&gt;test-client backend verification when port binding is restricted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a good pattern for developer demos in general:&lt;/p&gt;

&lt;p&gt;Make the happy path real, but keep the runtime resilient enough that your demo doesn’t collapse when one dependency sneezes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code snippet: mutation-ready trade function
&lt;/h2&gt;

&lt;p&gt;The generated merchant code intentionally returns structured risk flags:&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;player&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blacklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;risk_flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked:blacklist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&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;reason&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;known exploiter&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;risk_flags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;risk_flags&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;That becomes useful for both UI explanation and future analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this can go next
&lt;/h2&gt;

&lt;p&gt;Darwin.js already demonstrates self-evolving NPC behavior, but there are several obvious next steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  Better AI mutation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;wire in a live model call&lt;/li&gt;
&lt;li&gt;add mutation evaluation and rollback&lt;/li&gt;
&lt;li&gt;compare multiple candidate patches&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Richer simulation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;merchant-to-merchant supply chains&lt;/li&gt;
&lt;li&gt;faction economies&lt;/li&gt;
&lt;li&gt;adversarial autonomous player agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Better platform architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;WebSocket streaming&lt;/li&gt;
&lt;li&gt;persistent event log storage&lt;/li&gt;
&lt;li&gt;replayable mutations&lt;/li&gt;
&lt;li&gt;deployment automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stronger developer ergonomics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;test suite for generated merchant logic&lt;/li&gt;
&lt;li&gt;snapshot-based mutation regression tests&lt;/li&gt;
&lt;li&gt;Dockerized local environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this project matters
&lt;/h2&gt;

&lt;p&gt;Darwin.js is interesting because it treats AI adaptation as a software architecture problem, not a chatbot problem.&lt;/p&gt;

&lt;p&gt;It asks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do agents observe failure?&lt;/li&gt;
&lt;li&gt;How do they mutate safely?&lt;/li&gt;
&lt;li&gt;How do humans inspect what changed?&lt;/li&gt;
&lt;li&gt;How do we keep the system legible?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions show up everywhere in the next wave of AI products:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;autonomous operations systems&lt;/li&gt;
&lt;li&gt;AI game agents&lt;/li&gt;
&lt;li&gt;adaptive security tooling&lt;/li&gt;
&lt;li&gt;self-healing workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project is a small but concrete blueprint for building those systems visibly and responsibly.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/1GG1fKvP1So"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/darwin.js" rel="noopener noreferrer"&gt;https://github.com/harishkotra/darwin.js&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>I built TeaCaptcha | A completely useless CAPTCHA that asks users to prove they are a teapot.</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Mon, 06 Apr 2026 17:29:04 +0000</pubDate>
      <link>https://dev.to/harishkotra/i-built-teacaptcha-a-completely-useless-captcha-that-asks-users-to-prove-they-are-a-teapot-4e96</link>
      <guid>https://dev.to/harishkotra/i-built-teacaptcha-a-completely-useless-captcha-that-asks-users-to-prove-they-are-a-teapot-4e96</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/aprilfools-2026"&gt;DEV April Fools Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;TeaCaptcha&lt;/strong&gt;, a completely useless CAPTCHA that asks users to prove they are a teapot.&lt;/p&gt;

&lt;p&gt;Most CAPTCHAs are designed to prove you are &lt;strong&gt;not&lt;/strong&gt; a robot. TeaCaptcha solves the far less urgent problem of confirming whether a visitor is a legitimate, standards-adjacent, enterprise-compliant teapot.&lt;/p&gt;

&lt;p&gt;The catch is that every path ends exactly where it should:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;418 I'm a teapot&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The app presents a dead-serious verification flow with fake compliance steps, suspicious audit dashboards, ceremonial trust badges, and a Beverage Tribunal that rejects every applicant with increasing levels of bureaucratic disappointment.&lt;/p&gt;

&lt;p&gt;It also includes a bunch of unnecessary upgrades that made the joke dramatically worse in the best way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Premium UX Overhaul&lt;/strong&gt;: A gorgeous dark mode palette with glassmorphism, glowing emerald gradients, and animated toast notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terrible Acoustics&lt;/strong&gt;: HTML5 Web Audio API &lt;code&gt;OscillatorNode&lt;/code&gt; that forces you to listen to a terrible, rising teapot whistle during calibration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slamming Rejection&lt;/strong&gt;: Reaching the final result triggers an animated, massive diagonal red "DENIED" stamp.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;coffee ancestry trap&lt;/strong&gt; for applicants with espresso sympathies.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Beverage Tribunal appeal flow&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Certificate of Official Rejection&lt;/strong&gt; download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viral rejection copy&lt;/strong&gt; dynamically generated as a personalized Twitter/X Intent URL.&lt;/li&gt;
&lt;li&gt;Fake metrics like &lt;strong&gt;Spout Confidence&lt;/strong&gt;, &lt;strong&gt;Brew Intent&lt;/strong&gt;, and &lt;strong&gt;Pour Compliance&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;TeaCaptcha walks applicants through four important and medically unnecessary checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select every image containing a kettle, even though the image set is clearly hostile to success&lt;/li&gt;
&lt;li&gt;Type the authentic whistle of a teapot&lt;/li&gt;
&lt;li&gt;Complete thermal composure calibration by holding still like an enterprise vessel&lt;/li&gt;
&lt;li&gt;Declare beverage allegiance and avoid coffee-adjacent thinking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After all that, the backend returns a real HTTP &lt;code&gt;418&lt;/code&gt; response with a fresh denial reason such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Spout symmetry drift exceeds RFC-compliant tolerances."&lt;/li&gt;
&lt;li&gt;"You seem emotionally coffee-adjacent."&lt;/li&gt;
&lt;li&gt;"Model output: likely teapot, insufficient vibes."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live demo: &lt;a href="https://teacaptcha.vercel.app/" rel="noopener noreferrer"&gt;https://teacaptcha.vercel.app/&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fojudicc31njob9alpcle.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojudicc31njob9alpcle.gif" alt="This is how it work" width="560" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Repository: &lt;a href="https://github.com/harishkotra/teacaptcha" rel="noopener noreferrer"&gt;https://github.com/harishkotra/teacaptcha&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app is basically an authentication portal for beverage identity fraud, which no one asked for and no one should fund.&lt;/p&gt;

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

&lt;p&gt;I kept the tech intentionally small and the joke intentionally overbuilt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A tiny Node.js server serves the app and exposes a real &lt;code&gt;/api/verify&lt;/code&gt; route.&lt;/li&gt;
&lt;li&gt;The verify route always returns HTTP &lt;code&gt;418 I'm a Teapot&lt;/code&gt; and fully supports the custom &lt;code&gt;BREW&lt;/code&gt; method, including mandatory HTCPCP headers (&lt;code&gt;Accept-Additions: *&lt;/code&gt;, &lt;code&gt;Safe-To-Brew: false&lt;/code&gt;) to honor Larry Masinter.&lt;/li&gt;
&lt;li&gt;The response payload utilizes the Gemini API to dynamically generate rejection reasons acting as an unhinged bureaucratic Beverage Tribunal.&lt;/li&gt;
&lt;li&gt;The frontend treats the process with absolute seriousness while blinding you with completely unnecessary glowing CSS animations, CSS jitter effects, and a custom audio whistle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ironically, building useless software still benefits from useful engineering.&lt;/p&gt;

&lt;p&gt;To make the joke land, the app had to feel internally consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the frontend had to look polished&lt;/li&gt;
&lt;li&gt;the API had to return a real &lt;code&gt;418&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the copy had to stay deadpan&lt;/li&gt;
&lt;li&gt;the interaction had to be smooth enough that failure felt official&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That contrast is what made it funny for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Category
&lt;/h2&gt;

&lt;p&gt;I wanted the "Google AI" angle to be part of the joke instead of replacing the joke.&lt;/p&gt;

&lt;p&gt;So the product includes a &lt;strong&gt;Google AI Verification&lt;/strong&gt; pathway that actually hooks into &lt;code&gt;gemini-2.5-flash&lt;/code&gt; using the &lt;code&gt;@google/genai&lt;/code&gt; SDK.&lt;/p&gt;

&lt;p&gt;The system prompt forces Gemini to act as an unhinged, deeply bureaucratic Beverage Tribunal. Instead of relying on a static joke pool, the AI dynamically generates completely novel, ridiculous HTTP 418 rejection reasons, tribunal next-actions, and certification badges on the fly.&lt;/p&gt;

&lt;p&gt;No matter how advanced the AI system becomes and how many unique reasons it comes up with, it is still solving a completely useless problem.&lt;/p&gt;

&lt;p&gt;This also fits &lt;strong&gt;Best Ode to Larry Masinter&lt;/strong&gt; because the entire app is built around an aggressively overcommitted interpretation of &lt;code&gt;418 I'm a teapot&lt;/code&gt;, HTCPCP behavior, and ceremonial beverage protocol nonsense.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldr6tyolbkjztj1yo172.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldr6tyolbkjztj1yo172.png" alt="Example Output 1" width="800" height="475"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fa6988ypy8oxyqpu0r1ma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa6988ypy8oxyqpu0r1ma.png" alt="Example Output 2" width="800" height="902"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fr2sbf9me8wsg70ds6aq2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr2sbf9me8wsg70ds6aq2.png" alt="Example Output 3" width="800" height="902"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fsfaat1m0j3mvfw59ytoq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsfaat1m0j3mvfw59ytoq.png" alt="Example Output 4" width="800" height="902"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F2y9jwn3upgtbzzdliiwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2y9jwn3upgtbzzdliiwm.png" alt="Example Output 5" width="800" height="1083"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>418challenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building infection-protocol-lab: A Full-Stack Multi-Agent Simulation with TypeScript, LangChain, and ReactFlow</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Mon, 06 Apr 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-infection-protocol-lab-a-full-stack-multi-agent-simulation-with-typescript-langchain-2d8k</link>
      <guid>https://dev.to/harishkotra/building-infection-protocol-lab-a-full-stack-multi-agent-simulation-with-typescript-langchain-2d8k</guid>
      <description>&lt;p&gt;&lt;code&gt;infection-protocol-lab&lt;/code&gt; started as a question: what happens when autonomous LLM agents live inside a social system with incomplete information, hidden contagion, evolving strategy, and role-based asymmetry?&lt;/p&gt;

&lt;p&gt;This project answers that question as a runnable local app.&lt;/p&gt;

&lt;p&gt;It combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a TypeScript simulation engine&lt;/li&gt;
&lt;li&gt;a LangChain-powered agent layer&lt;/li&gt;
&lt;li&gt;an Express + WebSocket backend&lt;/li&gt;
&lt;li&gt;a ReactFlow live visualization&lt;/li&gt;
&lt;li&gt;a benchmark playground for repeated experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post walks through the architecture, tradeoffs, and code patterns behind the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build This?
&lt;/h2&gt;

&lt;p&gt;Most LLM demos are linear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send prompt&lt;/li&gt;
&lt;li&gt;receive answer&lt;/li&gt;
&lt;li&gt;end interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But many interesting system behaviors only appear when agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interact repeatedly&lt;/li&gt;
&lt;li&gt;hold imperfect beliefs&lt;/li&gt;
&lt;li&gt;react to hidden state&lt;/li&gt;
&lt;li&gt;adapt their own strategy over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where simulation gets interesting. &lt;code&gt;infection-protocol-lab&lt;/code&gt; frames that environment around a hidden infection protocol:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some agents start infected&lt;/li&gt;
&lt;li&gt;others do not know who is infected&lt;/li&gt;
&lt;li&gt;infection spreads through interaction&lt;/li&gt;
&lt;li&gt;one agent is a doctor&lt;/li&gt;
&lt;li&gt;infected agents can recover&lt;/li&gt;
&lt;li&gt;everyone is trying to survive and infer risk&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhahb2eq3vjamnsjls0nx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhahb2eq3vjamnsjls0nx.png" alt="High-Level Architecture" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are three big layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Simulation engine&lt;/li&gt;
&lt;li&gt;Transport layer&lt;/li&gt;
&lt;li&gt;Visualization and benchmark UI&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Core Data Model
&lt;/h2&gt;

&lt;p&gt;The backend defines a compact but expressive agent model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;infected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;immune&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AgentRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doctor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AgentSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentRole&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;beliefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;riskTolerance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AgentMemoryEntry&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;This shape is important because it balances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enough structure for simulation logic&lt;/li&gt;
&lt;li&gt;enough context for LLM prompts&lt;/li&gt;
&lt;li&gt;enough simplicity for UI rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The LLM Layer
&lt;/h2&gt;

&lt;p&gt;The LLM wrapper uses LangChain’s &lt;code&gt;ChatOpenAI&lt;/code&gt;, but it also includes a graceful fallback so the app still runs without an API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LLMClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generateResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fallback matters for developer ergonomics. It keeps the project demoable even when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you do not have an API key handy&lt;/li&gt;
&lt;li&gt;you are offline&lt;/li&gt;
&lt;li&gt;a model call fails temporarily&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agent Prompts as Behavioral Interfaces
&lt;/h2&gt;

&lt;p&gt;Each agent is not just a data object. It is a prompt-shaped actor.&lt;/p&gt;

&lt;p&gt;Here is the conversation prompt:&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You are Agent &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.

Strategy:
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

You are interacting with Agent &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;otherAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.

Infection spreads via interaction, but identities are hidden.

Goals:
- survive
- infer who is infected
- influence others

Respond in &amp;lt;=20 words.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a useful pattern for multi-agent apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep prompts role-specific&lt;/li&gt;
&lt;li&gt;keep them short&lt;/li&gt;
&lt;li&gt;make the output constrained&lt;/li&gt;
&lt;li&gt;preserve enough room for emergent variation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same pattern is reused for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;belief updates&lt;/li&gt;
&lt;li&gt;strategy evolution&lt;/li&gt;
&lt;li&gt;broadcast generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Simulation Loop
&lt;/h2&gt;

&lt;p&gt;The engine owns the authoritative simulation state. Each round:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;pairs agents using suspicion-aware matching&lt;/li&gt;
&lt;li&gt;runs bilateral interactions&lt;/li&gt;
&lt;li&gt;updates beliefs&lt;/li&gt;
&lt;li&gt;resolves infection spread&lt;/li&gt;
&lt;li&gt;lets the doctor act&lt;/li&gt;
&lt;li&gt;applies natural cures&lt;/li&gt;
&lt;li&gt;triggers periodic broadcasts&lt;/li&gt;
&lt;li&gt;evolves strategies&lt;/li&gt;
&lt;li&gt;emits events and a round snapshot
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;runRound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;EventHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSmartPairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;firstMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;talk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateBeliefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secondMessage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateBeliefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;firstMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryInfection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryInfection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;performDoctorActions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;performNaturalCures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;performBroadcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;performEvolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onEvent&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;h2&gt;
  
  
  Why Smart Pairing Matters
&lt;/h2&gt;

&lt;p&gt;Random pairing is easy, but it throws away one of the more interesting social signals: choice.&lt;/p&gt;

&lt;p&gt;In this project, agents prefer low-suspicion partners, with randomness mixed in. That creates emergent structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;isolated suspicious agents&lt;/li&gt;
&lt;li&gt;safer trust clusters&lt;/li&gt;
&lt;li&gt;delayed or accelerated spread dynamics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also makes beliefs operational instead of decorative.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSocket Streaming
&lt;/h2&gt;

&lt;p&gt;The frontend is event-driven. Instead of polling constantly, the backend pushes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;live interaction edges&lt;/li&gt;
&lt;li&gt;infection transitions&lt;/li&gt;
&lt;li&gt;cure events&lt;/li&gt;
&lt;li&gt;broadcast overlays&lt;/li&gt;
&lt;li&gt;strategy updates&lt;/li&gt;
&lt;li&gt;round summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That decision keeps the interface feeling alive and makes the system easier to reason about during a run.&lt;/p&gt;

&lt;h2&gt;
  
  
  ReactFlow as a Simulation Surface
&lt;/h2&gt;

&lt;p&gt;ReactFlow is usually used for node editors or DAGs, but it works well as a dynamic social graph when you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;represent each agent as a node&lt;/li&gt;
&lt;li&gt;use temporary animated edges for interactions&lt;/li&gt;
&lt;li&gt;color nodes by state&lt;/li&gt;
&lt;li&gt;encode roles visually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend store converts backend events into graph state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interaction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edgeId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;addEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;edgeId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;animated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#38bdf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;strokeWidth&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&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;We also made those edges fade out after a few seconds so the graph communicates current activity rather than just historical clutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Mode
&lt;/h2&gt;

&lt;p&gt;The benchmark runner is what turns this from a visual toy into a practical experimentation harness.&lt;/p&gt;

&lt;p&gt;Instead of one live simulation, you can run many:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;runCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mergeConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;benchmark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&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;SimulationEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&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;result&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;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runToCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;finalAgents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agents&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;That gives you aggregate metrics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;survival rate&lt;/li&gt;
&lt;li&gt;infection spread speed&lt;/li&gt;
&lt;li&gt;average trust score&lt;/li&gt;
&lt;li&gt;strategy diversity&lt;/li&gt;
&lt;li&gt;immune rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially useful for comparing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prompting styles&lt;/li&gt;
&lt;li&gt;infection parameters&lt;/li&gt;
&lt;li&gt;round counts&lt;/li&gt;
&lt;li&gt;different OpenAI-compatible models&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pixel UI as Product Framing
&lt;/h2&gt;

&lt;p&gt;The UI was intentionally redesigned into a pixel-art operations deck rather than a generic analytics dashboard.&lt;/p&gt;

&lt;p&gt;That was not just cosmetic.&lt;/p&gt;

&lt;p&gt;The visual language reinforces what the app is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a simulation lab&lt;/li&gt;
&lt;li&gt;a control room&lt;/li&gt;
&lt;li&gt;a system to observe and experiment with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a project like this, the visual frame matters because it communicates the right mental model before the user even clicks anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Developers Can Learn From This Project
&lt;/h2&gt;

&lt;p&gt;There are a few useful patterns here beyond the simulation itself:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Keep backend truth separate from frontend presentation
&lt;/h3&gt;

&lt;p&gt;The backend owns simulation state. The frontend owns visualization state. That boundary keeps both sides simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Design prompts like APIs
&lt;/h3&gt;

&lt;p&gt;The prompt surface should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;minimal&lt;/li&gt;
&lt;li&gt;structured&lt;/li&gt;
&lt;li&gt;role-aware&lt;/li&gt;
&lt;li&gt;output-constrained&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Build graceful fallback paths
&lt;/h3&gt;

&lt;p&gt;The app still runs without an API key. That makes it dramatically easier to share and iterate on.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Make metrics first-class
&lt;/h3&gt;

&lt;p&gt;If you are running experiments, the benchmark layer should not be an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Directions
&lt;/h2&gt;

&lt;p&gt;If I were extending this next, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replay mode with a round timeline&lt;/li&gt;
&lt;li&gt;richer trust visualizations&lt;/li&gt;
&lt;li&gt;more agent roles&lt;/li&gt;
&lt;li&gt;persistent run history&lt;/li&gt;
&lt;li&gt;model-vs-model tournament mode&lt;/li&gt;
&lt;li&gt;editable prompts from the UI&lt;/li&gt;
&lt;li&gt;exportable benchmark results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;infection-protocol-lab&lt;/code&gt; is a compact but expressive example of what happens when you treat LLM agents as participants in a system instead of one-off answer generators.&lt;/p&gt;

&lt;p&gt;That shift, from response generation to interactive simulation, is where a lot of the interesting engineering starts.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/zwkiGZh2TXA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/infection-protocol-lab" rel="noopener noreferrer"&gt;https://github.com/harishkotra/infection-protocol-lab&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building a Global Narrative Warfare Map with Bright Data, Tavily, Ollama, React, and Three.js</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Sun, 05 Apr 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-a-global-narrative-warfare-map-with-bright-data-tavily-ollama-react-and-threejs-2g9o</link>
      <guid>https://dev.to/harishkotra/building-a-global-narrative-warfare-map-with-bright-data-tavily-ollama-react-and-threejs-2g9o</guid>
      <description>&lt;p&gt;What if a single search box could reveal how the same geopolitical topic is framed differently in Washington, London, Tel Aviv, New Delhi, Tehran, Berlin, or Ankara?&lt;/p&gt;

&lt;p&gt;That was the goal behind &lt;strong&gt;Reality Rift&lt;/strong&gt;: a web application that discovers live coverage, scrapes grounded evidence, reasons over it with an LLM, and projects the result onto an interactive 3D globe.&lt;/p&gt;

&lt;p&gt;This post walks through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the product idea&lt;/li&gt;
&lt;li&gt;the architecture&lt;/li&gt;
&lt;li&gt;the data pipeline&lt;/li&gt;
&lt;li&gt;the visualization layer&lt;/li&gt;
&lt;li&gt;the caching strategy&lt;/li&gt;
&lt;li&gt;the transparency model&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most search tools answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What articles exist about this topic?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But researchers, journalists, strategists, and builders often need to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How is this story being framed differently across countries, and what evidence supports that?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That requires more than search.&lt;/p&gt;

&lt;p&gt;It requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-source discovery&lt;/li&gt;
&lt;li&gt;article grounding&lt;/li&gt;
&lt;li&gt;country inference&lt;/li&gt;
&lt;li&gt;narrative clustering&lt;/li&gt;
&lt;li&gt;explainable provenance&lt;/li&gt;
&lt;li&gt;visual storytelling&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  UI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;React&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Three.js&lt;/li&gt;
&lt;li&gt;&lt;code&gt;three-globe&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Express&lt;/li&gt;
&lt;li&gt;Axios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Data + AI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bright Data Discover API&lt;/li&gt;
&lt;li&gt;Bright Data scraping flow&lt;/li&gt;
&lt;li&gt;Tavily&lt;/li&gt;
&lt;li&gt;Ollama with &lt;code&gt;gemma4:latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OpenAI-compatible provider support&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

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




&lt;h2&gt;
  
  
  Search Layer
&lt;/h2&gt;

&lt;p&gt;One of the early lessons was that a single-country search viewpoint underperformed badly on global topics.&lt;/p&gt;

&lt;p&gt;So the app now fans out Bright Data Discover across multiple countries and merges that with Tavily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: multi-country Discover
&lt;/h3&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;settled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;discoverSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;numResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;perCountry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;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;This matters because a topic like &lt;code&gt;Iran war&lt;/code&gt; or &lt;code&gt;Ukraine war&lt;/code&gt; should not be judged from one country’s SERP alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Grounded Evidence
&lt;/h2&gt;

&lt;p&gt;Search results alone are not enough.&lt;/p&gt;

&lt;p&gt;The app scrapes a curated subset of URLs and sends trimmed, grounded excerpts into the reasoning layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: scrape path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;scraped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inferredCountry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scrapeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brightdata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each source record retains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;publisher&lt;/li&gt;
&lt;li&gt;URL&lt;/li&gt;
&lt;li&gt;search provider&lt;/li&gt;
&lt;li&gt;scrape provider&lt;/li&gt;
&lt;li&gt;inferred country&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That enables a transparent evidence deck in the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  LLM Reasoning
&lt;/h2&gt;

&lt;p&gt;The model receives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;merged search results&lt;/li&gt;
&lt;li&gt;scraped excerpts&lt;/li&gt;
&lt;li&gt;provider provenance&lt;/li&gt;
&lt;li&gt;instructions to return strict JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prompt excerpt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules:
- Return 8-15 countries whenever the evidence supports it.
- Use source grounding fields so every narrative is tied to explicit URLs and publishers.
- Always include a short "stanceRationale".
- Return valid JSON only with no markdown and no explanation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Output shape
&lt;/h3&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;"countries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"India"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"lat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20.5937&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"lng"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;78.9629&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"narrative"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Frames the conflict through regional stability and strategic autonomy."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stanceRationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Coverage stresses de-escalation while protecting national interests."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"stance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"neutral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"intensity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.67&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sources"&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;"https://..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"connections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"India"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UAE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"strength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.72&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recovering from Weak Model Output
&lt;/h2&gt;

&lt;p&gt;A major failure mode in narrative systems is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search finds plenty of evidence&lt;/li&gt;
&lt;li&gt;scrape works&lt;/li&gt;
&lt;li&gt;model still returns 2 countries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s not acceptable for globally-covered topics.&lt;/p&gt;

&lt;p&gt;So the backend now includes a recovery strategy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;detect implausibly low country coverage relative to the evidence&lt;/li&gt;
&lt;li&gt;retry with a stricter “you under-returned countries” instruction&lt;/li&gt;
&lt;li&gt;if still weak, supplement missing countries from grounded evidence&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Recovery check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldRetryForCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evidenceCountries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getEvidenceCountryCounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;strongEvidenceCountries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;evidenceCountries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;strongEvidenceCountries&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&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;This is one of the most important engineering choices in the project:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;don’t blindly trust the model if the evidence says the output is incomplete.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Transparency by Design
&lt;/h2&gt;

&lt;p&gt;One of the project’s goals is to be explainable.&lt;/p&gt;

&lt;p&gt;The UI now exposes not just source URLs, but also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which search provider found the source&lt;/li&gt;
&lt;li&gt;whether it came from Bright Data Discover, Tavily, or both&lt;/li&gt;
&lt;li&gt;whether scraping was done via Bright Data, direct fetch, or fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example transparent source object
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/article&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Regional response to conflict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;searchProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hybrid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;searchProviderLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bright Data + Tavily&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;scrapeProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brightdata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;discoveredBy&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;brightdata&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tavily&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what makes the app feel credible rather than magical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Visualization Layer
&lt;/h2&gt;

&lt;p&gt;The map uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a textured globe&lt;/li&gt;
&lt;li&gt;atmosphere and graticules&lt;/li&gt;
&lt;li&gt;grounded country markers&lt;/li&gt;
&lt;li&gt;pulsing rings&lt;/li&gt;
&lt;li&gt;animated arcs&lt;/li&gt;
&lt;li&gt;hover and click interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why not just use tooltips?
&lt;/h3&gt;

&lt;p&gt;Because the value is not merely in interaction.&lt;br&gt;&lt;br&gt;
It’s in turning abstract narrative clusters into a spatial mental model.&lt;/p&gt;

&lt;p&gt;That’s why a globe works so well here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;narrative stance becomes geographic&lt;/li&gt;
&lt;li&gt;similarities become arcs&lt;/li&gt;
&lt;li&gt;intensity becomes pulse density&lt;/li&gt;
&lt;li&gt;confidence becomes glow strength&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Loading Experience
&lt;/h2&gt;

&lt;p&gt;The app also exposes pipeline steps during long-running requests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discovering coverage&lt;/li&gt;
&lt;li&gt;Grounding source content&lt;/li&gt;
&lt;li&gt;Reconciling narratives&lt;/li&gt;
&lt;li&gt;Projecting the map&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because AI apps often fail the “is it doing anything?” test.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;The backend uses in-memory caches for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Discover search&lt;/li&gt;
&lt;li&gt;merged search results&lt;/li&gt;
&lt;li&gt;scrape outputs&lt;/li&gt;
&lt;li&gt;pipeline inputs&lt;/li&gt;
&lt;li&gt;LLM outputs&lt;/li&gt;
&lt;li&gt;final responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated API costs&lt;/li&gt;
&lt;li&gt;repeated scrape time&lt;/li&gt;
&lt;li&gt;unnecessary inference calls&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where This Can Go Next
&lt;/h2&gt;

&lt;p&gt;Some obvious follow-on features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;language-specific narrative comparisons&lt;/li&gt;
&lt;li&gt;timeline / drift view&lt;/li&gt;
&lt;li&gt;exportable country briefings&lt;/li&gt;
&lt;li&gt;regional polygon overlays&lt;/li&gt;
&lt;li&gt;fact-claim extraction per country&lt;/li&gt;
&lt;li&gt;model comparison mode&lt;/li&gt;
&lt;li&gt;historical replay&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The most interesting part of this project is not the globe.&lt;/p&gt;

&lt;p&gt;It’s the combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multi-source discovery&lt;/li&gt;
&lt;li&gt;grounded evidence&lt;/li&gt;
&lt;li&gt;model reasoning&lt;/li&gt;
&lt;li&gt;transparent provenance&lt;/li&gt;
&lt;li&gt;visual synthesis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination turns a normal search interface into a narrative intelligence product.&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/reality-rift" rel="noopener noreferrer"&gt;https://github.com/harishkotra/reality-rift&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building Agent Swarm CFO for the OWS Hackathon</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Fri, 03 Apr 2026 18:40:00 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-agent-swarm-cfo-for-the-ows-hackathon-2id1</link>
      <guid>https://dev.to/harishkotra/building-agent-swarm-cfo-for-the-ows-hackathon-2id1</guid>
      <description>&lt;p&gt;How I used Open Wallet Standard, Tavily, Bright Data, Featherless, Allium, Uniblock, Zerion, CoinGecko, and x402 framing to build a real operator console for autonomous agent teams.&lt;/p&gt;

&lt;p&gt;Most agent demos stop at orchestration. They show multiple bots talking to each other, but they avoid the hard production questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who holds the wallet?&lt;/li&gt;
&lt;li&gt;Who approves spend?&lt;/li&gt;
&lt;li&gt;What data did the agent rely on?&lt;/li&gt;
&lt;li&gt;What happens when a provider is expensive or disallowed?&lt;/li&gt;
&lt;li&gt;How do you revoke access after a mission ends?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Agent Swarm CFO was built to answer those questions directly.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Thesis
&lt;/h2&gt;

&lt;p&gt;Agents need finance controls, not just prompts.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scoped wallets&lt;/li&gt;
&lt;li&gt;revocable access&lt;/li&gt;
&lt;li&gt;provider-specific budgets&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;source traceability&lt;/li&gt;
&lt;li&gt;explicit payment lanes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open Wallet Standard is the trust layer that makes this possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OWS Wallet + Policies
        |
        v
Mission Orchestrator
        |
        +--&amp;gt; Tavily (search)
        +--&amp;gt; Bright Data (extraction)
        +--&amp;gt; Featherless (synthesis)
        +--&amp;gt; Allium (wallet balances)
        +--&amp;gt; Uniblock (chain reads)
        +--&amp;gt; Zerion (portfolio mix)
        +--&amp;gt; CoinGecko (market context)
        +--&amp;gt; x402 lane (paid endpoint narrative)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why OWS Was The Right Primitive
&lt;/h2&gt;

&lt;p&gt;I used the Node SDK directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createWallet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;signMessage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@open-wallet-standard/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This let us do four important things in-process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a real wallet vault for the app&lt;/li&gt;
&lt;li&gt;Register policies for each agent&lt;/li&gt;
&lt;li&gt;Issue agent-specific API keys&lt;/li&gt;
&lt;li&gt;Sign actions without exposing raw keys&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The product impact is bigger than the code size. Once the wallet and policy layer exist, every agent action can be treated like a governed treasury action rather than a blind API call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Research Pipeline
&lt;/h2&gt;

&lt;p&gt;The research pipeline has three stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Tavily for grounded retrieval
&lt;/h3&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;searchAndGround&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;searchDepth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;advanced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;includeRawContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bright Data for premium extraction
&lt;/h3&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;extractPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Featherless for synthesis
&lt;/h3&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;comparison&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compareSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&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;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;draftReport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;mission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;findings&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern gives you a clean research stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search for candidate sources&lt;/li&gt;
&lt;li&gt;extract high-confidence pages robustly&lt;/li&gt;
&lt;li&gt;synthesize only after evidence is grounded&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Financial Enrichment
&lt;/h2&gt;

&lt;p&gt;I added partner integrations that make the product feel like treasury software, not just a research dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Allium
&lt;/h3&gt;

&lt;p&gt;Used for live wallet balance enrichment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniblock
&lt;/h3&gt;

&lt;p&gt;Used for chain reads via a unified JSON-RPC layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zerion
&lt;/h3&gt;

&lt;p&gt;Used for wallet portfolio composition.&lt;/p&gt;

&lt;h3&gt;
  
  
  CoinGecko
&lt;/h3&gt;

&lt;p&gt;Used for free public market context and benchmark asset movement.&lt;/p&gt;

&lt;p&gt;Together, these integrations make the final report richer and make the treasury screen feel real.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI Decisions
&lt;/h2&gt;

&lt;p&gt;I deliberately avoided “hackathon dashboard syndrome.”&lt;/p&gt;

&lt;p&gt;The product uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a sticky control sidebar&lt;/li&gt;
&lt;li&gt;a dense topbar with mission context&lt;/li&gt;
&lt;li&gt;operational cards instead of generic marketing tiles&lt;/li&gt;
&lt;li&gt;event logs and policy chips&lt;/li&gt;
&lt;li&gt;premium dark styling with muted contrast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to make the interface feel like Linear meets Brex meets an AI operations console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important Engineering Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Native OWS instead of CLI orchestration
&lt;/h3&gt;

&lt;p&gt;I chose the Node SDK because it keeps the wallet system in-process and easier to reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Provider adapters stay isolated
&lt;/h3&gt;

&lt;p&gt;Every external integration lives under &lt;code&gt;src/lib/adapters/&lt;/code&gt;, which makes it easy to evolve or swap providers later.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Explicit provider failure over fake success
&lt;/h3&gt;

&lt;p&gt;I removed silent mock behavior from the main provider paths. If a provider is missing credentials or fails, the UI shows the integration error instead of pretending the call worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Dynamic pages for live integrations
&lt;/h3&gt;

&lt;p&gt;Pages that depend on live providers are marked dynamic so they don’t try to execute network requests during static generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d Build Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replace seeded mission state with database-backed runtime state&lt;/li&gt;
&lt;li&gt;Add a true x402 buyer flow instead of a simulated payment event&lt;/li&gt;
&lt;li&gt;Add downloadable reports&lt;/li&gt;
&lt;li&gt;Add mission history and replay&lt;/li&gt;
&lt;li&gt;Add richer chain-specific signing workflows&lt;/li&gt;
&lt;li&gt;Add multi-user operator approvals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent Swarm CFO is not “agents calling APIs.” It is a finance-grade control surface for autonomous execution.&lt;/p&gt;

&lt;p&gt;That distinction matters. As soon as agents are allowed to spend, sign, or transact, governance becomes the product. OWS makes that governance legible, enforceable, and auditable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fin3jqtsf8gwo4arhszog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fin3jqtsf8gwo4arhszog.png" alt="CFO Showcase 1" width="800" height="854"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F1xaiqoxtj42pgxksm47u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1xaiqoxtj42pgxksm47u.png" alt="CFO Showcase 2" width="800" height="990"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Flik3rlgftvens7an546v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flik3rlgftvens7an546v.png" alt="CFO Showcase 3" width="800" height="854"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fmcnx7e3ttj9r7apjsgp0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcnx7e3ttj9r7apjsgp0.png" alt="CFO Showcase 4" width="800" height="990"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fp2ynb1740ml4vb5ltwhu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp2ynb1740ml4vb5ltwhu.png" alt="CFO Showcase 5" width="800" height="1038"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2F8oaxy0swgbu14tznm4qt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8oaxy0swgbu14tznm4qt.png" alt="CFO Showcase 6" width="800" height="805"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/agent-swarm-cfo" rel="noopener noreferrer"&gt;https://github.com/harishkotra/agent-swarm-cfo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building a Local Identity Resolution Engine with Neo4j, Python, and Ollama</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Fri, 03 Apr 2026 13:11:14 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-a-local-identity-resolution-engine-with-neo4j-python-and-ollama-3724</link>
      <guid>https://dev.to/harishkotra/building-a-local-identity-resolution-engine-with-neo4j-python-and-ollama-3724</guid>
      <description>&lt;p&gt;Identity resolution is a classic graph problem hiding inside what many teams still try to solve with relational joins.&lt;/p&gt;

&lt;p&gt;In this project, I built a local-first identity resolution engine that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stores fragmented user identifiers in Neo4j&lt;/li&gt;
&lt;li&gt;generates synthetic overlapping user data&lt;/li&gt;
&lt;li&gt;resolves hidden identity links through recursive Cypher traversals&lt;/li&gt;
&lt;li&gt;uses Ollama to translate natural language into Cypher&lt;/li&gt;
&lt;li&gt;exposes the whole workflow through both a CLI and a lightweight web UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post walks through the design, code, and implementation details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine this simplified user journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one account signs in with &lt;code&gt;tech_user@gmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;another signs in with &lt;code&gt;tech_user+alt@gmail.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;both use the same device&lt;/li&gt;
&lt;li&gt;a third identity later shares a cookie or phone with the second identity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those records may look independent in tabular storage, but in a graph they form a connected component.&lt;/p&gt;

&lt;p&gt;That is exactly what identity resolution needs: path discovery across shared identifiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Graph Database Fits Better
&lt;/h2&gt;

&lt;p&gt;In SQL, link analysis usually becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;self-joins on login tables&lt;/li&gt;
&lt;li&gt;joins on device tables&lt;/li&gt;
&lt;li&gt;more joins on email or cookie tables&lt;/li&gt;
&lt;li&gt;increasingly difficult reasoning as hop depth increases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Neo4j, identifiers become nodes and relationships become explicit edges.&lt;/p&gt;

&lt;p&gt;So instead of forcing the query planner through repeated joins, you traverse the graph directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    USER["Developer / Analyst"] --&amp;gt; UI["CLI or Web UI"]
    UI --&amp;gt; APP["Python Service Layer"]
    APP --&amp;gt; OLLAMA["Ollama Local Model"]
    APP --&amp;gt; NEO["Neo4j"]
    APP --&amp;gt; SEED["Synthetic Data Generator"]

    OLLAMA --&amp;gt; GEN["Generated Cypher"]
    GEN --&amp;gt; APP
    APP --&amp;gt; PROFILE["PROFILE Metrics"]
    PROFILE --&amp;gt; UI
    NEO --&amp;gt; UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Graph Model
&lt;/h2&gt;

&lt;p&gt;The core schema is intentionally small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Identity&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;identity_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Email&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Phone&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Device&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Cookie&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cookie_id&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relationships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Identity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_EMAIL&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Email&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Identity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_PHONE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Phone&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Identity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:USED_DEVICE&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Device&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Identity&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ASSOCIATED_WITH&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Cookie&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the graph readable while still modeling real-world identity fragmentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local Infrastructure
&lt;/h2&gt;

&lt;p&gt;Neo4j runs locally via Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;neo4j&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neo4j:5.26&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neo4j-identity&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7474:7474"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7687:7687"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NEO4J_AUTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;neo4j/password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the entire project stay local:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local database&lt;/li&gt;
&lt;li&gt;local seeding&lt;/li&gt;
&lt;li&gt;local LLM inference&lt;/li&gt;
&lt;li&gt;local UI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Seeding Synthetic Overlap Data
&lt;/h2&gt;

&lt;p&gt;The project uses &lt;code&gt;Faker&lt;/code&gt; to generate 100 identities, but the important part is not just fake data. It is fake data with overlap.&lt;/p&gt;

&lt;p&gt;The seeding script deliberately creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared devices&lt;/li&gt;
&lt;li&gt;shared cookies&lt;/li&gt;
&lt;li&gt;reused emails&lt;/li&gt;
&lt;li&gt;reused phones&lt;/li&gt;
&lt;li&gt;explicit demo chains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code excerpt:&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;shared_devices&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;device-shared-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;shared_cookies&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;cookie-shared-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;shared_emails&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;shared_user_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;shared_phones&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;+14155550&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, for selected rows, those identifiers get reused.&lt;/p&gt;

&lt;p&gt;The script also injects a deterministic demo chain:&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;chain_overrides&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;identity-001&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;tech_user@gmail.com&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;+14155550111&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;device-demo-001&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;cookie-demo-001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity-002&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;tech_user+alt@gmail.com&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;+14155550112&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;device-demo-001&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;cookie-demo-002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity-003&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;bob@info.com&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;+14155550113&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;device-demo-002&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;cookie-demo-002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity-004&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;robert@info.com&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;+14155550114&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;device-demo-002&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;cookie-demo-004&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;That gives you predictable live demo scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Integrity with Constraints
&lt;/h2&gt;

&lt;p&gt;The project creates uniqueness constraints before seeding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;email_address_unique&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;EXISTS&lt;/span&gt;
&lt;span class="n"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;REQUIRE&lt;/span&gt; &lt;span class="n"&gt;e.address&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;phone_number_unique&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="ow"&gt;NOT&lt;/span&gt; &lt;span class="ow"&gt;EXISTS&lt;/span&gt;
&lt;span class="n"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;p:&lt;/span&gt;&lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;REQUIRE&lt;/span&gt; &lt;span class="n"&gt;p.number&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because graph ingestion should preserve identity semantics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a given email node should not duplicate accidentally&lt;/li&gt;
&lt;li&gt;a phone number should resolve to one canonical node&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Core Resolution Query
&lt;/h2&gt;

&lt;p&gt;The simplest demonstration query is also one of the most important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;start:&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;address:&lt;/span&gt; &lt;span class="n"&gt;$email&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;related&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[*1..4]&lt;/code&gt; is the key.&lt;/p&gt;

&lt;p&gt;It says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start at a known identifier&lt;/li&gt;
&lt;li&gt;traverse any relationship type&lt;/li&gt;
&lt;li&gt;explore up to 4 hops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exposes transitive links like:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Email -&amp;gt; Identity -&amp;gt; Device -&amp;gt; Identity -&amp;gt; Cookie&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For an identity resolution demo, that is the moment when the graph model becomes obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural Language to Cypher with Ollama
&lt;/h2&gt;

&lt;p&gt;The next layer is accessibility. Not everyone wants to write Cypher by hand.&lt;/p&gt;

&lt;p&gt;So the app sends the user’s question to Ollama along with a tightly constrained prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;graph schema&lt;/li&gt;
&lt;li&gt;relationship directions&lt;/li&gt;
&lt;li&gt;allowed Cypher clauses&lt;/li&gt;
&lt;li&gt;examples&lt;/li&gt;
&lt;li&gt;output shape requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prompt excerpt:&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;Rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Return&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher&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;...&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;params&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;explanation&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;...&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="n"&gt;Use&lt;/span&gt; &lt;span class="n"&gt;only&lt;/span&gt; &lt;span class="n"&gt;MATCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OPTIONAL&lt;/span&gt; &lt;span class="n"&gt;MATCH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RETURN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Never&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;CREATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MERGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REMOVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DROP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;APOC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="n"&gt;operations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because local models are fast and convenient, but they are not always consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening the LLM Path
&lt;/h2&gt;

&lt;p&gt;A naive LLM-to-query pipeline is brittle.&lt;/p&gt;

&lt;p&gt;To make it practical, this project adds several guardrails:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. JSON extraction from noisy model output
&lt;/h3&gt;

&lt;p&gt;Some models wrap valid JSON in extra prose. The parser scans for valid JSON objects instead of assuming perfect output.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cypher sanitization
&lt;/h3&gt;

&lt;p&gt;The app rejects write or dangerous clauses:&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;blocked&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;CREATE &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;MERGE &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;DELETE &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;SET &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;REMOVE &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;DROP &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;CALL &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;LOAD CSV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Semantic linting
&lt;/h3&gt;

&lt;p&gt;The app checks for reversed graph edges such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;(:Email)-[:HAS_EMAIL]-&amp;gt;(:Identity)&lt;/code&gt; which is incorrect for this schema&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Retry with corrective feedback
&lt;/h3&gt;

&lt;p&gt;If the model returns malformed or semantically invalid output, the app sends back a correction request and asks for a fixed JSON-only response.&lt;/p&gt;

&lt;p&gt;That turns the system from “prompt once and hope” into a more resilient local pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Query Execution and Performance Profiling
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;PROFILE&lt;/code&gt; mode is enabled, the app prefixes the generated query:&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;query&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;PROFILE &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cypher&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;cypher&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After execution, it walks the returned profile tree and sums DB hits.&lt;/p&gt;

&lt;p&gt;That gives two advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a visible performance story for demos&lt;/li&gt;
&lt;li&gt;a useful debugging aid while iterating on generated Cypher&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CLI and Web UI
&lt;/h2&gt;

&lt;p&gt;The project exposes two interfaces:&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;

&lt;p&gt;Direct resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 main.py resolve &lt;span class="nt"&gt;--email&lt;/span&gt; tech_user@gmail.com &lt;span class="nt"&gt;--profile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Natural-language query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 main.py ask &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"Find all accounts linked to the device used by 'bob@info.com'."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; llama3.2:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Web UI
&lt;/h3&gt;

&lt;p&gt;The browser UI is intentionally lightweight and dependency-free.&lt;/p&gt;

&lt;p&gt;It uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python’s built-in HTTP server&lt;/li&gt;
&lt;li&gt;HTML/CSS/JS embedded in &lt;code&gt;webapp.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the same backend functions as the CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps the project easy to run and easy to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal Request Flow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwq1bhsxpf8qc1x1lt5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwq1bhsxpf8qc1x1lt5i.png" alt="Internal Request Flow" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes This Interesting
&lt;/h2&gt;

&lt;p&gt;There are lots of small AI demos and lots of small graph demos. What makes this one useful is the combination:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;practical graph schema&lt;/li&gt;
&lt;li&gt;deterministic overlap generation&lt;/li&gt;
&lt;li&gt;local model integration&lt;/li&gt;
&lt;li&gt;query validation&lt;/li&gt;
&lt;li&gt;performance introspection&lt;/li&gt;
&lt;li&gt;inspectable outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not just “ask an LLM a database question.” It is a constrained local graph workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Potential Extensions
&lt;/h2&gt;

&lt;p&gt;If I were taking this further, I would add:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Graph visualization
&lt;/h3&gt;

&lt;p&gt;Render linked identities as an interactive node-edge graph.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confidence scoring
&lt;/h3&gt;

&lt;p&gt;Use weighted evidence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared device strength&lt;/li&gt;
&lt;li&gt;phone uniqueness&lt;/li&gt;
&lt;li&gt;cookie freshness&lt;/li&gt;
&lt;li&gt;overlap count&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Batch resolution
&lt;/h3&gt;

&lt;p&gt;Accept CSV input and generate clusters for many identifiers at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Multi-model evaluation
&lt;/h3&gt;

&lt;p&gt;Run the same prompt across several local Ollama models and compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validity&lt;/li&gt;
&lt;li&gt;speed&lt;/li&gt;
&lt;li&gt;correctness&lt;/li&gt;
&lt;li&gt;DB hits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Exportable investigation reports
&lt;/h3&gt;

&lt;p&gt;Generate analyst-friendly summaries for fraud or trust-and-safety teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This project is a good example of where local AI and graph databases complement each other well.&lt;/p&gt;

&lt;p&gt;Neo4j provides the right data model for connected identity evidence.&lt;br&gt;
Ollama provides a convenient natural-language interface for querying that graph.&lt;br&gt;
Python keeps the whole system compact and inspectable.&lt;/p&gt;

&lt;p&gt;The result is not just a demo. It is a strong foundation for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identity stitching&lt;/li&gt;
&lt;li&gt;risk analysis&lt;/li&gt;
&lt;li&gt;entity resolution&lt;/li&gt;
&lt;li&gt;graph-assisted investigative tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to study or extend the code, start with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;seed_data.py&lt;/code&gt; for graph ingestion&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;main.py&lt;/code&gt; for LLM-to-Cypher and query execution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webapp.py&lt;/code&gt; for the local interface&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.amazonaws.com%2Fuploads%2Farticles%2Fy19viy04wjeewerjhcrd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy19viy04wjeewerjhcrd.png" alt="Output 1" width="800" height="613"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fnip4w3x77xivopy8qpkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnip4w3x77xivopy8qpkk.png" alt="Output 2" width="800" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/identity-resolution-engine" rel="noopener noreferrer"&gt;https://github.com/harishkotra/identity-resolution-engine&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building Shadow-Signal: Bridging the Gap Between On-Chain Intelligence and Execution with Nansen CLI</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Thu, 02 Apr 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-shadow-signal-bridging-the-gap-between-on-chain-intelligence-and-execution-with-nansen-cli-216k</link>
      <guid>https://dev.to/harishkotra/building-shadow-signal-bridging-the-gap-between-on-chain-intelligence-and-execution-with-nansen-cli-216k</guid>
      <description>&lt;p&gt;In the fast-moving world of decentralized finance, the "time-to-trade" is everything. Traditionally, a trader sees a "Smart Money" move on a dashboard, manually verifies the contract, and then heads to a DEX to execute. By the time that loop finishes, the alpha is often gone.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Shadow-Signal&lt;/strong&gt;, an autonomous agentic script that collapses this entire workflow into a single loop using the new Nansen CLI.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;Shadow-Signal is built using a Node.js wrapper around the Nansen CLI, designed to act as a "Sentinel" for high-conviction Smart Money moves on Solana and Base.&lt;/p&gt;

&lt;p&gt;The logic flows through three distinct layers of the Nansen ecosystem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The Discovery Layer (nansen research):&lt;br&gt;
The script polls the smart-money netflow endpoint. Unlike a broad scan, I used the --fields flag to limit data transfer and focused on the 24h timeframe to identify sustained accumulation rather than transient noise.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Reasoning Layer (nansen agent):&lt;br&gt;
This is where the "Agentic" part comes in. Raw data doesn't tell the whole story. Before any trade is considered, the script pipes the token symbol into the Nansen AI Agent. It asks for a risk assessment—checking for potential red flags or "rug-pull" sentiment that the raw netflow numbers might miss.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Execution Layer (nansen trade):&lt;br&gt;
If the AI Agent gives a "Green Light," the script moves to execution. It interfaces with Nansen’s local wallet system to generate a DEX quote (trade quote) and prepares the transaction for final execution.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Technical Challenges &amp;amp; Optimizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Credit Efficiency: AI Agent calls are high-value. I optimized the script to only trigger the Agent check if the Smart Money inflow exceeds a specific dollar threshold, preventing unnecessary credit burn.&lt;/li&gt;
&lt;li&gt;JSON Orchestration: By using the CLI’s JSON output format, I was able to programmatically parse on-chain data and feed it directly into trade logic, removing human error from the equation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shadow-Signal isn't just a bot; it’s a proof-of-concept for &lt;strong&gt;Agentic Finance&lt;/strong&gt;. By using Nansen’s CLI as the "brain" and "eyes," we can now build tools that don't just show us the data, but act on it.&lt;/p&gt;

&lt;p&gt;Github Gist: &lt;a href="https://gist.github.com/harishkotra/9ed34cd6c892b910c163fe66137ccd22" rel="noopener noreferrer"&gt;https://gist.github.com/harishkotra/9ed34cd6c892b910c163fe66137ccd22&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>trading</category>
      <category>dailybuild2026</category>
    </item>
    <item>
      <title>Building Skeptik: A Zero-Editorial Autonomous Newsroom With Agno, Featherless, Tavily, Bright Data, and Virlo</title>
      <dc:creator>Harish Kotra (he/him)</dc:creator>
      <pubDate>Wed, 01 Apr 2026 15:29:29 +0000</pubDate>
      <link>https://dev.to/harishkotra/building-skeptik-a-zero-editorial-autonomous-newsroom-with-agno-featherless-tavily-bright-data-jdp</link>
      <guid>https://dev.to/harishkotra/building-skeptik-a-zero-editorial-autonomous-newsroom-with-agno-featherless-tavily-bright-data-jdp</guid>
      <description>&lt;p&gt;Skeptik started from a simple product constraint: do not build another AI news summarizer.&lt;/p&gt;

&lt;p&gt;The brief demanded something harder and more interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;serious articles&lt;/li&gt;
&lt;li&gt;a fully autonomous editorial path&lt;/li&gt;
&lt;li&gt;clear transparency into how each story was generated&lt;/li&gt;
&lt;li&gt;strong use of Virlo for story ranking and urgency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the product had to behave less like “chat over links” and more like a newsroom system with policy, skepticism, and publication gates.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture, key code paths, design choices, and the failure modes we had to account for without hiding them behind fake content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product Goal
&lt;/h2&gt;

&lt;p&gt;The target experience was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Readers land on a newsroom front page&lt;/li&gt;
&lt;li&gt;They see a lead story that feels publishable&lt;/li&gt;
&lt;li&gt;They understand why this story is rising now&lt;/li&gt;
&lt;li&gt;They can inspect the agentic reporting chain&lt;/li&gt;
&lt;li&gt;They can trust that stories were challenged before publication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That shaped both the backend and the frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Why These Technologies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Agno
&lt;/h3&gt;

&lt;p&gt;Agno provides a clean mental model for defining agents with clear responsibilities. The newsroom needed explicit stage ownership:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;topic framing&lt;/li&gt;
&lt;li&gt;reporting&lt;/li&gt;
&lt;li&gt;skepticism&lt;/li&gt;
&lt;li&gt;fact-checking&lt;/li&gt;
&lt;li&gt;editing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That maps naturally to an agent-oriented architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Featherless.ai
&lt;/h3&gt;

&lt;p&gt;Featherless gives an OpenAI-compatible interface over open models. That let us use a single integration surface while swapping model IDs. We moved the backend to &lt;code&gt;moonshotai/Kimi-K2.5&lt;/code&gt; to avoid poor generation behavior from the previous model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://featherless.ai/register?referrer=2EYBGPC3" rel="noopener noreferrer"&gt;Get your own Featherless API Key here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tavily
&lt;/h3&gt;

&lt;p&gt;Tavily is a good fit for news discovery because the workflow is query-heavy and verification-heavy. We use it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;story discovery&lt;/li&gt;
&lt;li&gt;source retrieval&lt;/li&gt;
&lt;li&gt;claim verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bright Data
&lt;/h3&gt;

&lt;p&gt;Search snippets are not enough for reporting. Bright Data is the extraction layer, but one practical lesson here is that Bright Data zone type matters. Browser API zones and Unlocker-style request zones do not behave the same way through the same request surface.&lt;/p&gt;

&lt;p&gt;That is exactly why the app now surfaces provider failures directly instead of pretending extraction succeeded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Virlo
&lt;/h3&gt;

&lt;p&gt;Virlo is the ranking brain. It is not a UI ornament. It drives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;story selection&lt;/li&gt;
&lt;li&gt;urgency&lt;/li&gt;
&lt;li&gt;signal strength&lt;/li&gt;
&lt;li&gt;“why you’re seeing this”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backend Structure
&lt;/h2&gt;

&lt;p&gt;The backend entrypoint is &lt;code&gt;backend/app/main.py&lt;/code&gt;. The important design choice is that startup seeds the newsroom and begins an autonomous loop.&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="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;autopilot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seed_if_needed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;autopilot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;autopilot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the product feel alive immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ingestion Layer
&lt;/h2&gt;

&lt;p&gt;The ingestion service combines Tavily and Bright Data into a knowledge pack:&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;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;all_sources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tavily&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sources&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;brightdata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That knowledge pack becomes the foundation for all downstream editorial work.&lt;/p&gt;

&lt;p&gt;The key point is that the reporting stage does not read one article. It reads a source bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Multi-Agent Newsroom
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Topic Agent
&lt;/h3&gt;

&lt;p&gt;Converts Virlo signals into a structured reporting pitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reporter Agent
&lt;/h3&gt;

&lt;p&gt;Produces the first long-form draft with explicit claims and multiple perspectives.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skeptic Agent
&lt;/h3&gt;

&lt;p&gt;Challenges the draft for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bias&lt;/li&gt;
&lt;li&gt;missing context&lt;/li&gt;
&lt;li&gt;weak reasoning&lt;/li&gt;
&lt;li&gt;overclaiming&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fact-check Agent
&lt;/h3&gt;

&lt;p&gt;Validates extracted claims against source retrieval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Editor Agent
&lt;/h3&gt;

&lt;p&gt;Produces the final article if the draft survives policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy Matters More Than Prompting
&lt;/h2&gt;

&lt;p&gt;One of the most important parts of this system is not the prose generation. It is the deterministic controller in &lt;code&gt;backend/app/services/newsroom.py&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;false_claims&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newsroom_max_false_claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PipelineDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false_claims_detected&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;skeptic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;skepticism_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newsroom_max_skeptic_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;revised&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;knowledge_pack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skeptic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;skeptic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prior_draft&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the system editorial discipline.&lt;/p&gt;

&lt;p&gt;Without this layer, “multi-agent newsroom” is just prompt theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing For Failure
&lt;/h2&gt;

&lt;p&gt;The ugly part of real AI systems is provider instability.&lt;/p&gt;

&lt;p&gt;We hit two real issues while building:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agno API drift around structured outputs&lt;/li&gt;
&lt;li&gt;Featherless upstream failures returning &lt;code&gt;503 failed_generation&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first version of the app was demo-safe. That meant a provider failure could quietly fall back to synthetic or seeded content.&lt;/p&gt;

&lt;p&gt;That was useful for iteration, but it was the wrong product behavior for a credible newsroom.&lt;/p&gt;

&lt;p&gt;The current system runs in strict live-API mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Virlo must return real ranked trend data&lt;/li&gt;
&lt;li&gt;Tavily must return real search results&lt;/li&gt;
&lt;li&gt;Bright Data must return real extraction output&lt;/li&gt;
&lt;li&gt;Featherless must return real structured agent output
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ProviderAPIError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;virlo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Virlo request failed with &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;body&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;Instead of silently substituting content, Skeptik now reports integration status through &lt;code&gt;/api/status&lt;/code&gt; and renders those errors on the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Frontend
&lt;/h2&gt;

&lt;p&gt;The frontend is intentionally not a dashboard.&lt;/p&gt;

&lt;p&gt;The homepage does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;establishes the publication identity&lt;/li&gt;
&lt;li&gt;foregrounds the lead story&lt;/li&gt;
&lt;li&gt;makes the autonomous process legible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The article page then handles transparency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;story body&lt;/li&gt;
&lt;li&gt;why-you’re-seeing-this&lt;/li&gt;
&lt;li&gt;fact checks&lt;/li&gt;
&lt;li&gt;source list&lt;/li&gt;
&lt;li&gt;editorial traces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split is deliberate. The homepage sells the product. The article page proves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Frontend Pattern
&lt;/h2&gt;

&lt;p&gt;The homepage lead story is treated like a publication feature block, not a SaaS card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"overflow-hidden bg-ink text-paper"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"space-y-7 p-8 md:p-10"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border-white/15 bg-white/10 text-paper"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lead Story&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl font-display text-4xl leading-tight md:text-6xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;lead&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CardContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That small layout choice dramatically improves perceived credibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;If I had another sprint, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Postgres + migrations&lt;/li&gt;
&lt;li&gt;stronger source freshness scoring&lt;/li&gt;
&lt;li&gt;more explicit evidence snippets on claim checks&lt;/li&gt;
&lt;li&gt;Playwright smoke tests&lt;/li&gt;
&lt;li&gt;structured observability for publish/reject runs&lt;/li&gt;
&lt;li&gt;provider-specific health diagnostics&lt;/li&gt;
&lt;li&gt;background worker separation from API process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Advice For Developers Building Similar Systems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Separate product logic from prompt logic
&lt;/h3&gt;

&lt;p&gt;A newsroom should not depend on “the model behaving.” Put discipline into code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Design for provider instability on day one
&lt;/h3&gt;

&lt;p&gt;Do not assume every model call works. They won’t.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make transparency legible, not noisy
&lt;/h3&gt;

&lt;p&gt;Readers do not need every raw prompt. They need evidence that the process exists and mattered.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Give ranking a first-class role
&lt;/h3&gt;

&lt;p&gt;Virlo mattered because it shaped the editorial surface itself, not just a tiny badge.&lt;/p&gt;

&lt;p&gt;Skeptik is an attempt to take AI-native publishing seriously. The interesting question is not whether an LLM can write an article. It can.&lt;/p&gt;

&lt;p&gt;The real question is whether you can build a system around that model that behaves like a newsroom instead of a demo, and whether it tells the truth when its dependencies are broken.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ranking&lt;/li&gt;
&lt;li&gt;synthesis&lt;/li&gt;
&lt;li&gt;skepticism&lt;/li&gt;
&lt;li&gt;verification&lt;/li&gt;
&lt;li&gt;policy&lt;/li&gt;
&lt;li&gt;transparency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where the product lives.&lt;/p&gt;

&lt;p&gt;Github Repo: &lt;a href="https://github.com/harishkotra/skeptik" rel="noopener noreferrer"&gt;https://github.com/harishkotra/skeptik&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>dailybuild2026</category>
    </item>
  </channel>
</rss>
