<?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: Sean</title>
    <description>The latest articles on DEV Community by Sean (@virerra).</description>
    <link>https://dev.to/virerra</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4001236%2F6af40a62-5dc8-4675-99b0-b9dc2e244108.jpg</url>
      <title>DEV Community: Sean</title>
      <link>https://dev.to/virerra</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/virerra"/>
    <language>en</language>
    <item>
      <title>Skein: a knowledge graph that never overwrites a contradiction</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Wed, 29 Jul 2026 20:16:16 +0000</pubDate>
      <link>https://dev.to/virerra/skein-a-knowledge-graph-that-never-overwrites-a-contradiction-1io6</link>
      <guid>https://dev.to/virerra/skein-a-knowledge-graph-that-never-overwrites-a-contradiction-1io6</guid>
      <description>&lt;p&gt;I kept losing the thread across AI chat sessions. Ask about something, the idea evolves over three or four separate chats, and by the end I've got five overlapping conversations open and no clean record of which decision actually won, or why I changed my mind halfway through.&lt;/p&gt;

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

&lt;p&gt;So I built &lt;a href="https://github.com/Virerra/Skein" rel="noopener noreferrer"&gt;Skein&lt;/a&gt; — paste a transcript in, it extracts the atomic claims (individual decisions and facts, not a summary), clusters them by topic, and when a new claim contradicts an old one, it chains onto it instead of overwriting it. Then you can actually query the result with real RAG.&lt;/p&gt;

&lt;p&gt;Two things about the build turned out more interesting than I expected going in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claims chain, they don't overwrite
&lt;/h2&gt;

&lt;p&gt;The core data model is almost embarrassingly simple. A claim looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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;text&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="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&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;superseded&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;correction&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;discarded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;supersedes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&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="c1"&gt;// id of the claim this one replaces&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new claim comes in on a topic that already has an active claim, and the text actually differs, the old claim flips to &lt;code&gt;superseded&lt;/code&gt; and the new one gets tagged &lt;code&gt;correction&lt;/code&gt; with &lt;code&gt;supersedes&lt;/code&gt; pointing back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyNewClaims&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingClaims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newClaims&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;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;existingClaims&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;incoming&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;newClaims&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;activeSameTopic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topic&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="nx"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&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;activeSameTopic&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;activeSameTopic&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;!==&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;activeSameTopic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;superseded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;claims&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="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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;correction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;supersedes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activeSameTopic&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="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;activeSameTopic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;claims&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;incoming&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;return&lt;/span&gt; &lt;span class="nx"&gt;claims&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;Nothing gets deleted. Walking &lt;code&gt;supersedes&lt;/code&gt; backward from any claim gives you the full decision history — Postgres, then Mongo, then back to Postgres because Mongo made relational queries harder than expected — instead of just the current answer with the reasoning stripped out.&lt;/p&gt;

&lt;p&gt;It's naive on purpose: same topic, different text, is the whole conflict-detection heuristic. It'll misfire on any topic that legitimately holds multiple simultaneous active claims. I know exactly where the edges are and haven't hit them yet in real use, which felt like the right bar for shipping it rather than building a more sophisticated version speculatively.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Retrieval had to be decoupled from chat provider — Anthropic has no embeddings API
&lt;/h2&gt;

&lt;p&gt;This one actually shaped the architecture. The app supports Anthropic, OpenAI-compatible (including local models via Ollama), and WebLLM running fully in-browser via WebGPU. Naturally I wanted embeddings to just come from whichever provider you'd picked for chat.&lt;/p&gt;

&lt;p&gt;Anthropic doesn't offer an embeddings endpoint at all. Not a gap in my code — a gap in their API surface; they point people to Voyage AI. So if embedding were tied to chat provider, anyone using Anthropic (probably most people) would get zero real retrieval.&lt;/p&gt;

&lt;p&gt;The fix was to stop treating "embedding provider" and "chat provider" as the same decision. Embeddings always run locally via WebLLM (a real embedding model, &lt;code&gt;snowflake-arctic-embed-s&lt;/code&gt;, not a chat model pressed into service) regardless of what's answering the question. Free, no key, and it means retrieval quality doesn't depend on which chat provider happens to have an embeddings endpoint.&lt;/p&gt;

&lt;p&gt;The retrieval layer has one more rule worth mentioning: semantic similarity has no concept of a correction. If the highest-scoring match for a query is a &lt;em&gt;superseded&lt;/em&gt; claim — its old wording just happened to match more closely than whatever replaced it — the system resolves it to that topic's current head before it's used as context. Otherwise you'd get an embedding-powered way to confidently answer with outdated information, which would defeat the entire premise of the correction-chain model above it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// One claim per topic in context, resolved to that topic's CURRENT head&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clusterByTopic&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;Map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildClusters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&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;cl&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="nx"&gt;cl&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="nx"&gt;cl&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;seenTopics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sources&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;claim&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;scored&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;sources&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="nx"&gt;TOP_K&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&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;seenTopics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&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="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;seenTopics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claim&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="nx"&gt;sources&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;clusterByTopic&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;claim&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="nx"&gt;head&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;claim&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;Synthesis goes through whichever chat provider &lt;em&gt;is&lt;/em&gt; selected, asked to answer strictly from the retrieved claims and cite them inline. If the claims don't contain enough to answer, it says so instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A CLI that shares prompts with the web app, not a copy of them
&lt;/h2&gt;

&lt;p&gt;There's also a zero-dependency CLI doing the same extract → categorize → query pipeline with no browser involved — for anyone who wants to be fast and doesn't need to see a graph. It started as a hand-copied port of the web app's prompts. That lasted about a week before they drifted: a fix applied to the web app's extraction prompt never made it to the CLI's copy.&lt;/p&gt;

&lt;p&gt;The actual fix wasn't "import everything from one side into the other" — the two have genuinely different provider layers (the web app supports three providers including a browser-only local model and passes around a &lt;code&gt;settings&lt;/code&gt; object; the CLI is BYOK-only across two with flat params), and unifying that would mean papering over a real difference. Instead, just the prompt &lt;em&gt;content&lt;/em&gt; moved into a shared module both sides import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Skein/
  shared/prompts.js   &amp;lt;- the actual prompt strings, single source of truth
  src/lib/             &amp;lt;- web app, imports from ../../shared/prompts
  cli/src/lib/          &amp;lt;- CLI, imports from ../../../shared/prompts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One nice side effect: fixing the drift required rewording a sentence that referenced "the graph node" a label appears on — phrasing that didn't make sense from a CLI with no graph. Turned out the underlying advice (a label needs to read standalone, without the full claim text next to it) was equally true of the CLI's &lt;code&gt;list&lt;/code&gt; output. It wasn't environment-specific after all, just narrowly worded. Once reworded generically, the whole prompt became shareable verbatim.&lt;/p&gt;

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

&lt;p&gt;The graph clusters claims by topic using a real force-directed layout (not a canned graph library — dropped the bundle by about 400KB), with topic groups rendered as soft metaball blobs rather than one circle sized to the cluster's farthest node. Correction history shows on the node itself (a status ring, not a line back to a predecessor) — click any node, or query the graph, and the full chain shows in a compact popover.&lt;/p&gt;

&lt;p&gt;Fully client-side. IndexedDB for storage, no backend, MIT licensed. Repo's at &lt;a href="https://github.com/Virerra/Skein" rel="noopener noreferrer"&gt;github.com/Virerra/Skein&lt;/a&gt;, live demo linked from the README. If you poke at it and something breaks, I'd genuinely like to know.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a no-code multi-agent orchestration platform — parallel execution, targeted feedback, runs in your browser</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Mon, 29 Jun 2026 04:11:23 +0000</pubDate>
      <link>https://dev.to/virerra/i-built-a-no-code-multi-agent-orchestration-platform-parallel-execution-targeted-feedback-runs-46nf</link>
      <guid>https://dev.to/virerra/i-built-a-no-code-multi-agent-orchestration-platform-parallel-execution-targeted-feedback-runs-46nf</guid>
      <description>&lt;h2&gt;
  
  
  The problem with most agent builders
&lt;/h2&gt;

&lt;p&gt;They're sequential. They fail without recovery. &lt;br&gt;
They require code. They route your data through &lt;br&gt;
their servers.&lt;/p&gt;

&lt;p&gt;Parlance is built to fix all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;A free, open-source, no-code visual workspace &lt;br&gt;
for orchestrating multi-agent Claude workflows. &lt;br&gt;
Build on a canvas, connect nodes with edges, &lt;br&gt;
run with real API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the engine actually does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Parallel execution&lt;/strong&gt;&lt;br&gt;
Modified Kahn's algorithm computes topological &lt;br&gt;
waves. Each wave dispatched with Promise.all. &lt;br&gt;
A→B→C runs sequentially. A and B feeding into &lt;br&gt;
C runs A and B simultaneously, then C. Automatic.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Three-tier retry&lt;/strong&gt;&lt;br&gt;
Self-retry → upstream escalation → Overseer &lt;br&gt;
intervention. When an agent fails, the engine &lt;br&gt;
tries to resolve it without user input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Targeted feedback&lt;/strong&gt;&lt;br&gt;
Overseer evaluates output against your conditions. &lt;br&gt;
On rejection: parses TARGET: [agent name], re-runs &lt;br&gt;
only that agent and its downstream forward chain. &lt;br&gt;
Agents upstream of the fix point untouched. Cost &lt;br&gt;
savings proportional to where in the chain the &lt;br&gt;
problem is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow chaining&lt;/strong&gt;&lt;br&gt;
Chain Input nodes pull output from other Parlance &lt;br&gt;
workspaces. Optional caching avoids re-running &lt;br&gt;
expensive upstream workflows.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;1,258-line execution engine (orchestrator.js, 
runEngine.js, claudeClient.js, mockClient.js, 
engineEventBridge.js)&lt;/li&gt;
&lt;li&gt;React 19 + ReactFlow canvas&lt;/li&gt;
&lt;li&gt;Single useReducer, 26 pure-function action types&lt;/li&gt;
&lt;li&gt;Backend: 3 Vercel serverless functions (CORS 
proxy only — all orchestration runs client-side)&lt;/li&gt;
&lt;li&gt;BYOK — key in localStorage, sent per-request, 
never written server-side&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;56 source files. 8,310 lines. MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full suite
&lt;/h2&gt;

&lt;p&gt;This is the fifth tool built in two weeks. &lt;br&gt;
All free, all open source, all BYOK:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reality Check: realitycheck-project.vercel.app&lt;/li&gt;
&lt;li&gt;Singularity: singularity-workspace.vercel.app&lt;/li&gt;
&lt;li&gt;LoopLens: looplens-project.vercel.app&lt;/li&gt;
&lt;li&gt;Claude Batch: virerra.github.io/Claude-Batch&lt;/li&gt;
&lt;li&gt;Parlance: parlance-project.vercel.app&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Live: parlance-project.vercel.app&lt;br&gt;
Source: github.com/Virerra/parlance&lt;br&gt;
License: MIT&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a paced Claude API queue in a single HTML file — 29 tests, 10 bugs found and fixed</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Sun, 28 Jun 2026 10:44:55 +0000</pubDate>
      <link>https://dev.to/virerra/i-built-a-paced-claude-api-queue-in-a-single-html-file-29-tests-10-bugs-found-and-fixed-202g</link>
      <guid>https://dev.to/virerra/i-built-a-paced-claude-api-queue-in-a-single-html-file-29-tests-10-bugs-found-and-fixed-202g</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Rate limits on bulk API work are painful. You &lt;br&gt;
start processing a spreadsheet row by row, hit &lt;br&gt;
a 429 after a few requests, wait, continue, &lt;br&gt;
hit it again. You end up babysitting an API &lt;br&gt;
for hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58e8e1okutmu7trs07qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58e8e1okutmu7trs07qn.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Batch is a paced request queue in a &lt;br&gt;
single HTML file. No build step, no npm install, &lt;br&gt;
no server. Open it in a browser and it works.&lt;/p&gt;

&lt;p&gt;Two input modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type prompts (one line = one request)&lt;/li&gt;
&lt;li&gt;Upload CSV/Excel and write a {ColumnName} 
template — one request per row, only the 
columns you reference get sent (typically 
70–80% fewer tokens)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set an interval (1–180s), press Start, walk &lt;br&gt;
away. Exports as JSON, CSV, or plain text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Retry with exponential backoff (2s, 4s, 8s, 
cap 20s)&lt;/li&gt;
&lt;li&gt;Respects retry-after header — server guidance 
overrides calculated backoff&lt;/li&gt;
&lt;li&gt;AbortController on every fetch — cancels the 
actual HTTP connection on Stop&lt;/li&gt;
&lt;li&gt;504 in the retryable set (gateway timeouts 
are transient)&lt;/li&gt;
&lt;li&gt;sleepAbortable() polls isRunning every 100ms 
during backoff — Stop resolves immediately&lt;/li&gt;
&lt;li&gt;29 tests across 95 assertions in a jsdom 
harness that loads the actual index.html and 
drives real UI events&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  10 real bugs found and fixed
&lt;/h2&gt;

&lt;p&gt;Every bug has a regression test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No retry on 429s — fixed with backoff logic&lt;/li&gt;
&lt;li&gt;Timer leak on repeated start/stop — fixed 
with tracked setInterval&lt;/li&gt;
&lt;li&gt;Stuck processing row after Stop — fixed with 
status reset sweep in stopRun()&lt;/li&gt;
&lt;li&gt;Clear All orphaning in-flight rows — fixed 
by disabling during run&lt;/li&gt;
&lt;li&gt;retry-after header ignored — now read and 
respected&lt;/li&gt;
&lt;li&gt;No AbortController on fetch — added&lt;/li&gt;
&lt;li&gt;504 missing from retryable set — added&lt;/li&gt;
&lt;li&gt;Draggable state not synced with isRunning&lt;/li&gt;
&lt;li&gt;stopRun() only re-rendered conditionally&lt;/li&gt;
&lt;li&gt;Dead code currentRowTimer — removed&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Live: virerra.github.io/Claude-Batch&lt;br&gt;
Source: github.com/Virerra/Claude-Batch&lt;br&gt;
License: MIT&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a pre-run cost simulator for agentic loops — 13 models, zero API calls, works offline</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Sat, 27 Jun 2026 08:56:32 +0000</pubDate>
      <link>https://dev.to/virerra/i-built-a-pre-run-cost-simulator-for-agentic-loops-13-models-zero-api-calls-works-offline-5c2p</link>
      <guid>https://dev.to/virerra/i-built-a-pre-run-cost-simulator-for-agentic-loops-13-models-zero-api-calls-works-offline-5c2p</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Agentic loops compound costs in ways that aren't &lt;br&gt;
obvious until the bill arrives.&lt;/p&gt;

&lt;p&gt;Iteration 1: $0.19. Iteration 30: $2.48. Same &lt;br&gt;
agent, same task — the model re-reads full context &lt;br&gt;
on every turn. By iteration 20 you're paying for &lt;br&gt;
the same context 20 times over.&lt;/p&gt;

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

&lt;p&gt;LoopLens is a pre-run cost simulator. Configure &lt;br&gt;
your loop before you run it and get a full &lt;br&gt;
per-iteration breakdown.&lt;/p&gt;

&lt;p&gt;What it models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 context accumulation strategies (full, sliding 
window, summarization, stateless)&lt;/li&gt;
&lt;li&gt;Tool call overhead (web search, code execution, 
RAG, browser, external API)&lt;/li&gt;
&lt;li&gt;Multi-agent fan-out (orchestrator + N subagents)&lt;/li&gt;
&lt;li&gt;Prompt caching break-even analysis&lt;/li&gt;
&lt;li&gt;13 models across Anthropic, OpenAI, Google, 
DeepSeek — compared side by side&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real numbers from a test configuration
&lt;/h2&gt;

&lt;p&gt;30 iterations · Claude Sonnet 4.6 · 4 parallel &lt;br&gt;
subagents · web search + code execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total: $39.96&lt;/li&gt;
&lt;li&gt;Same loop on DeepSeek V4 Flash: $1.85 (95% less)&lt;/li&gt;
&lt;li&gt;Caching break-even: 1 run, $23.33/month saved&lt;/li&gt;
&lt;li&gt;Sliding window vs full accumulation: 85% savings&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Pure deterministic math — zero API calls, works &lt;br&gt;
completely offline. Prices pulled live from the &lt;br&gt;
LiteLLM community pricing JSON on page load, &lt;br&gt;
session-cached 6 hours, hardcoded fallback updated &lt;br&gt;
weekly via GitHub Actions. Status badge shows &lt;br&gt;
which layer is active.&lt;/p&gt;

&lt;p&gt;The gap: every other cost tool (Langfuse, Helicone, &lt;br&gt;
Braintrust, Bifrost, W&amp;amp;B) is post-hoc observability. &lt;br&gt;
LoopLens is the only pre-run simulator I'm aware of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Live: looplens-project.vercel.app&lt;br&gt;
Source: github.com/Virerra/looplens&lt;br&gt;
License: MIT&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajxy0amgycd6qqwb0iqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajxy0amgycd6qqwb0iqo.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7g9gvl2kd6pwj6pv12d6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7g9gvl2kd6pwj6pv12d6.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg34m2goqh12wh53wgmc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg34m2goqh12wh53wgmc.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6po152pj49zsm9lp5769.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6po152pj49zsm9lp5769.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0f868830hhxnbzzqokjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0f868830hhxnbzzqokjx.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp35aitlpb38t3divq8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhp35aitlpb38t3divq8v.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a four-view visual workspace with no external UI libraries and a 60fps drag system</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Fri, 26 Jun 2026 14:22:52 +0000</pubDate>
      <link>https://dev.to/virerra/i-built-a-four-view-visual-workspace-with-no-external-ui-libraries-and-a-60fps-drag-system-3761</link>
      <guid>https://dev.to/virerra/i-built-a-four-view-visual-workspace-with-no-external-ui-libraries-and-a-60fps-drag-system-3761</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every workspace forces one paradigm. Singularity &lt;br&gt;
lets you view the same dataset as a node graph, &lt;br&gt;
kanban board, timeline, and table — simultaneously, &lt;br&gt;
all in sync.&lt;/p&gt;

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

&lt;p&gt;Six node types (Note, Task, Character, Event, &lt;br&gt;
Research, Location) with labeled directed connections. &lt;br&gt;
Four views of the same data. Change one, update all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering decisions
&lt;/h2&gt;

&lt;p&gt;No external UI libraries. Every node, edge, panel, &lt;br&gt;
modal, kanban column, timeline row, and table cell &lt;br&gt;
is hand-crafted with inline styles and a shared &lt;br&gt;
design token object.&lt;/p&gt;

&lt;p&gt;60fps drag system using requestAnimationFrame with &lt;br&gt;
pointer capture — no React re-renders on every &lt;br&gt;
pointer move event.&lt;/p&gt;

&lt;p&gt;Unique IDs via uid(prefix) combining Date.now() and &lt;br&gt;
a monotonic counter — no external library.&lt;/p&gt;

&lt;p&gt;Everything in localStorage. No backend, no cloud, &lt;br&gt;
no accounts. Works offline. Ships as a Windows &lt;br&gt;
desktop app via Electron + electron-builder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Live: singularity-workspace.vercel.app&lt;br&gt;
Source: github.com/Virerra/singularity&lt;br&gt;
License: MIT&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc476yuyq8jrrrnervdhz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc476yuyq8jrrrnervdhz.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qdpqerltf2sx62g8ihm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qdpqerltf2sx62g8ihm.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7sml4g20bedk68tx4f2u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7sml4g20bedk68tx4f2u.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3yx0hbuv8ksloitmzxz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3yx0hbuv8ksloitmzxz.png" alt=" " width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a free, client-side fact-checker using Claude — BYOK, no backend, four structured checks</title>
      <dc:creator>Sean</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:37:31 +0000</pubDate>
      <link>https://dev.to/virerra/i-built-a-free-client-side-fact-checker-using-claude-byok-no-backend-four-structured-checks-15f6</link>
      <guid>https://dev.to/virerra/i-built-a-free-client-side-fact-checker-using-claude-byok-no-backend-four-structured-checks-15f6</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most fact-checking tools give you a verdict without explaining the reasoning. That's not useful long-term — users get an answer but no framework for thinking about the next headline themselves.&lt;/p&gt;

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

&lt;p&gt;Reality Check runs four independent checks on any headline using Claude with live web search:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source Check&lt;/strong&gt; — who created this, is the source credible?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence Check&lt;/strong&gt; — what evidence supports the claim, can it be verified?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Check&lt;/strong&gt; — is this recent and in its original context?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotional Check&lt;/strong&gt; — is the framing designed to manipulate rather than inform?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each check returns a verdict and reasoning. An overall credibility score from 0–100 summarizes the full picture.&lt;/p&gt;

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

&lt;p&gt;Fully client-side. No backend server. No database. No serverless functions operated by me.&lt;/p&gt;

&lt;p&gt;BYOK — users supply their own Anthropic API key. It lives only in React component state, never written to localStorage or cookies. Cleared on page close. API calls go browser → api.anthropic.com directly using the &lt;code&gt;anthropic-dangerous-direct-browser-access&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Stack: React 18 + Vite, Tailwind CSS, &lt;code&gt;claude-sonnet-4-6&lt;/code&gt; with the &lt;code&gt;web_search_20250305&lt;/code&gt; tool (capped at 2 searches per analysis).&lt;/p&gt;

&lt;h2&gt;
  
  
  Live example
&lt;/h2&gt;

&lt;p&gt;Tested on: &lt;em&gt;"Senate Passes Resolution Directing Trump to End Hostilities With Iran"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Score: 88/100 — Likely Reliable. Source and Evidence both Looks OK (confirmed by NBC, CBS, NPR, PBS, ABC, AP). Context and Emotional flagged as Use Caution — the word "Directs" implies legal compulsion but the resolution is actually symbolic and non-binding. Concept Spotlight: Clickbait.&lt;/p&gt;

&lt;p&gt;Technically accurate. Deliberately misleading framing. The tool catches the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://realitycheck-project.vercel.app" rel="noopener noreferrer"&gt;https://realitycheck-project.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/Virerra/reality-check" rel="noopener noreferrer"&gt;https://github.com/Virerra/reality-check&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
