<?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: Shrijal Acharya</title>
    <description>The latest articles on DEV Community by Shrijal Acharya (@shricodev).</description>
    <link>https://dev.to/shricodev</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%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg</url>
      <title>DEV Community: Shrijal Acharya</title>
      <link>https://dev.to/shricodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shricodev"/>
    <language>en</language>
    <item>
      <title>7 Best Code Execution APIs for AI Agents in 2026</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:29:37 +0000</pubDate>
      <link>https://dev.to/tensorlake/7-best-code-execution-apis-for-ai-agents-in-2026-33bd</link>
      <guid>https://dev.to/tensorlake/7-best-code-execution-apis-for-ai-agents-in-2026-33bd</guid>
      <description>&lt;p&gt;Your AI agent just wrote a perfectly reasonable Python script. Nice.&lt;/p&gt;

&lt;p&gt;The next question is where you actually run it.&lt;/p&gt;

&lt;p&gt;You could technically execute it on your own server, but letting LLM-generated code run next to your application, secrets, and production data can become a pretty damn problem. Believe me when I say it. 🤧&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%2Fvsca40ye1guoldxhlhjl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvsca40ye1guoldxhlhjl.jpg" alt="agent root access meme" width="799" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ref: &lt;a href="https://www.docker.com/blog/ai-coding-agent-horror-stories-security-risks" rel="noopener noreferrer"&gt;docker.com&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And execution itself is only half the story.&lt;/p&gt;

&lt;p&gt;Your agent still needs somewhere to send that code, get &lt;code&gt;stdout&lt;/code&gt;, &lt;code&gt;stderr&lt;/code&gt;, files, and errors back, maybe install dependencies, and sometimes come back to the same environment later.&lt;/p&gt;

&lt;p&gt;That is where code execution APIs come in.&lt;/p&gt;

&lt;p&gt;I went through the major options and picked seven I would actually look at for AI agents today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Quick note:&lt;/strong&gt; This isn't a scientific ranking. These APIs solve different problems, so the best option depends a lot on what your agent is trying to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR: Best code execution APIs for AI agents
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;I would use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Tensorlake&lt;/td&gt;
&lt;td&gt;Persistent coding agents, RL rollouts, evals, and agent CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;E2B&lt;/td&gt;
&lt;td&gt;Stateful Python and JavaScript code interpreter workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Daytona&lt;/td&gt;
&lt;td&gt;Shell execution, code interpreters, and long-running development sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Modal Sandboxes&lt;/td&gt;
&lt;td&gt;GPU-backed execution, ML workloads, and parallel compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Vercel Sandbox&lt;/td&gt;
&lt;td&gt;TypeScript-first coding agents, builds, and preview environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;AWS Bedrock AgentCore Code Interpreter&lt;/td&gt;
&lt;td&gt;Managed code execution for agents already running on AWS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Claude Code Execution&lt;/td&gt;
&lt;td&gt;Model-native execution without managing your own infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You probably don't need all seven.&lt;/p&gt;

&lt;p&gt;The important part is understanding what kind of execution your agent actually needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a code execution API?
&lt;/h2&gt;

&lt;p&gt;A code execution API gives your application or AI agent a programmatic way to run code or shell commands somewhere else and get the result back.&lt;/p&gt;

&lt;p&gt;The basic flow looks like this:&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%2Fol2hkia99fmychgspgor.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%2Fol2hkia99fmychgspgor.png" alt="ai agent code execution flow" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a simple agent, that might mean sending a small Python script and getting the output back.&lt;/p&gt;

&lt;p&gt;For a coding agent, things get a little more involved.&lt;/p&gt;

&lt;p&gt;The agent might clone a repository, install dependencies, edit files, run tests, inspect logs, fix the code, start a server, and then continue working inside the same environment.&lt;/p&gt;

&lt;p&gt;That is also where the difference between a &lt;strong&gt;code execution API&lt;/strong&gt; and an &lt;strong&gt;AI sandbox&lt;/strong&gt; becomes clear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 The API is how your agent asks for code to be executed.&lt;/p&gt;

&lt;p&gt;💁 The sandbox is the isolated environment where that code actually runs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are the ones I would look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Tensorlake
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Best when your agent needs to keep working inside the same environment over time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18k0498n97vh72sepec4.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%2F18k0498n97vh72sepec4.png" alt="tensorlake" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most code execution APIs are built around a fairly simple flow: create an environment, run some code, get the result back, and shut the environment down when you're finished.&lt;/p&gt;

&lt;p&gt;That works perfectly fine for a lot of agents.&lt;/p&gt;

&lt;p&gt;It starts getting weird when your agent installs dependencies, changes files, starts processes, goes idle for a while, and then needs to come back and continue from the same environment.&lt;/p&gt;

&lt;p&gt;That's where Tensorlake gets interesting.&lt;/p&gt;

&lt;p&gt;You can create isolated Linux sandboxes and run commands through its Python SDK, TypeScript SDK, CLI, or REST API.&lt;/p&gt;

&lt;p&gt;A basic Python execution looks like this:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tensorlake.sandbox&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;memory_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&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;-c&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;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello from Tensorlake&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="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&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="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command execution itself is pretty straightforward.&lt;/p&gt;

&lt;p&gt;The bigger difference is what Tensorlake lets you do with the environment around it.&lt;/p&gt;

&lt;p&gt;Named sandboxes can suspend while preserving the filesystem, memory, and running processes. So if your agent comes back later, it can resume the same environment instead of rebuilding everything from scratch.&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%2F9rrcqug5k7conis1zl2u.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%2F9rrcqug5k7conis1zl2u.png" alt="tensorlake showcase" width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tensorlake also supports background processes and PTY sessions, which is useful when the agent needs to interact with something longer-running than a single command.&lt;/p&gt;

&lt;p&gt;Then there are snapshots.&lt;/p&gt;

&lt;p&gt;You can take a snapshot of a running environment and create multiple copies from the same state.&lt;/p&gt;

&lt;p&gt;Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Install dependencies
        ↓
Build project
        ↓
Reach known-good state
        ↓
Snapshot
   ↙    ↓    ↘
Agent  Agent  Agent
run 1  run 2  run 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes pretty handy for RL rollouts, evals, CI shards, or best-of-N agent runs.&lt;/p&gt;

&lt;p&gt;Instead of preparing the same environment ten times, you can prepare it once and branch from there.&lt;/p&gt;

&lt;p&gt;Tensorlake also combines sandboxes with its own hosted Git and versioned filesystem.&lt;/p&gt;

&lt;p&gt;So the agent's work doesn't necessarily disappear when the sandbox does. Files can live in durable versioned storage, workspaces can be restored, and coding agents can keep their Git state around across runs.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Executes commands through &lt;code&gt;run()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Supports background processes and interactive PTY sessions&lt;/li&gt;
&lt;li&gt;Runs CPU sandboxes inside isolated microVMs&lt;/li&gt;
&lt;li&gt;Supports ephemeral and persistent named sandboxes&lt;/li&gt;
&lt;li&gt;Preserves memory, files, and running processes across suspend and resume&lt;/li&gt;
&lt;li&gt;Supports filesystem and memory snapshots&lt;/li&gt;
&lt;li&gt;Lets you copy a running or suspended sandbox into multiple environments&lt;/li&gt;
&lt;li&gt;Provides hosted Git for coding-agent workflows&lt;/li&gt;
&lt;li&gt;Provides a versioned POSIX filesystem with snapshots and restore&lt;/li&gt;
&lt;li&gt;Supports network allow and deny rules&lt;/li&gt;
&lt;li&gt;Has Python, TypeScript, CLI, and REST interfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;I would use Tensorlake when execution is only one step inside a much longer agent workflow.&lt;/p&gt;

&lt;p&gt;Coding agents are probably the clearest example.&lt;/p&gt;

&lt;p&gt;If an agent installs dependencies, modifies a repository, runs tests, goes idle, comes back later, tries several approaches in parallel, and eventually needs to preserve its work, rebuilding the whole environment for every call starts becoming annoying pretty quickly.&lt;/p&gt;

&lt;p&gt;RL rollouts and evals are another good fit because you can prepare one known state and fan it out into multiple independent sandboxes.&lt;/p&gt;

&lt;p&gt;If all you want is to run a Python snippet and get a chart or DataFrame back, E2B will probably feel more natural.&lt;/p&gt;

&lt;p&gt;The free tier is also fairly limited at one concurrent sandbox.&lt;/p&gt;

&lt;p&gt;But if your agent needs the whole environment to survive, not just the result of one execution call, Tensorlake is the one I would look at first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tensorlake&lt;/li&gt;
&lt;li&gt;Tensorlake Sandboxes&lt;/li&gt;
&lt;li&gt;Sandbox lifecycle&lt;/li&gt;
&lt;li&gt;Snapshots&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. E2B
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Probably the first one I would try for a traditional AI code interpreter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvqv4sqfrg82l6tattms.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%2Fwvqv4sqfrg82l6tattms.png" alt="e2b sandboxes" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
E2B is one of the names that comes up almost immediately when you start looking for code execution infrastructure for AI agents.&lt;/p&gt;

&lt;p&gt;And there is a good reason for that.&lt;/p&gt;

&lt;p&gt;Its Code Interpreter API is very close to the workflow most people imagine when they say, "I want my agent to write some Python and run it."&lt;/p&gt;

&lt;p&gt;A basic example looks like this:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e2b_code_interpreter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Sandbox&lt;/span&gt;

&lt;span class="n"&gt;sbx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sbx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
x = 10
print(x * 2)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nice part is that interpreter state can persist between executions.&lt;/p&gt;

&lt;p&gt;So your agent can define a variable in one call, use it again in the next one, and keep working without starting a completely fresh Python environment every time.&lt;/p&gt;

&lt;p&gt;E2B also gives you richer results than most command-oriented APIs.&lt;/p&gt;

&lt;p&gt;That includes logs, errors, charts, and DataFrame-style outputs, which makes it particularly nice for data-analysis agents.&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%2Fbcik4hion2375gvvmc18.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbcik4hion2375gvvmc18.jpg" alt="e2b showcase" width="562" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also stream &lt;code&gt;stdout&lt;/code&gt;, &lt;code&gt;stderr&lt;/code&gt;, and execution results while the code is running.&lt;/p&gt;

&lt;p&gt;Underneath all of this, E2B runs the sandbox inside Firecracker microVMs.&lt;/p&gt;

&lt;p&gt;Sandboxes can also be paused and resumed, with options for preserving memory alongside the filesystem.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Runs Python and JavaScript generated by agents&lt;/li&gt;
&lt;li&gt;Provides persistent interpreter contexts&lt;/li&gt;
&lt;li&gt;Returns logs, errors, charts, and other rich results&lt;/li&gt;
&lt;li&gt;Streams &lt;code&gt;stdout&lt;/code&gt;, &lt;code&gt;stderr&lt;/code&gt;, and execution output&lt;/li&gt;
&lt;li&gt;Supports regular shell commands&lt;/li&gt;
&lt;li&gt;Uses Firecracker microVMs for isolation&lt;/li&gt;
&lt;li&gt;Supports pause and resume&lt;/li&gt;
&lt;li&gt;Can preserve filesystem and memory state&lt;/li&gt;
&lt;li&gt;Has Python and JavaScript/TypeScript SDKs&lt;/li&gt;
&lt;li&gt;Supports parallel isolated code contexts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;If I were building a data-analysis agent, notebook agent, or anything where the loop is mostly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM writes code
      ↓
execute it
      ↓
inspect result
      ↓
write more code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E2B would probably be one of the first APIs I would try.&lt;/p&gt;

&lt;p&gt;The interpreter experience is much more natural for that than manually managing shell processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;E2B Code Interpreter&lt;/li&gt;
&lt;li&gt;E2B persistence&lt;/li&gt;
&lt;li&gt;E2B pricing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Daytona
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A good option when you want several execution styles under one SDK.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgomf5i1h1esllumidhrk.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%2Fgomf5i1h1esllumidhrk.png" alt="daytona" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Daytona gives you a few different ways to run agent-generated code.&lt;/p&gt;

&lt;p&gt;You can execute regular shell commands, use its higher-level code execution APIs, create persistent sessions, or work with interactive processes.&lt;/p&gt;

&lt;p&gt;For a normal command, the flow looks like this:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ls -la&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&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="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want something closer to a code interpreter, Daytona also provides &lt;code&gt;code_run&lt;/code&gt;, which can execute code and return artifacts such as &lt;code&gt;stdout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For longer-running agent workflows, sessions are probably the more interesting part.&lt;/p&gt;

&lt;p&gt;A session lets the agent keep shell state between multiple commands rather than treating every execution as a completely separate process.&lt;/p&gt;

&lt;p&gt;You can run commands, inspect their status, and stream logs while they are still running.&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%2Fcjnn74o0ifnlk2slh5gh.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%2Fcjnn74o0ifnlk2slh5gh.png" alt="daytona showcase" width="800" height="421"&gt;&lt;/a&gt;&lt;br&gt;
Daytona also has fairly broad SDK coverage.&lt;/p&gt;

&lt;p&gt;There are SDKs for Python, TypeScript, Ruby, Go, and Java, which is useful if your agent infrastructure isn't built around one language.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Executes shell commands programmatically&lt;/li&gt;
&lt;li&gt;Provides higher-level code execution APIs&lt;/li&gt;
&lt;li&gt;Supports long-running command sessions&lt;/li&gt;
&lt;li&gt;Streams command logs&lt;/li&gt;
&lt;li&gt;Supports PTY workflows&lt;/li&gt;
&lt;li&gt;Provides interpreter-style execution contexts&lt;/li&gt;
&lt;li&gt;Has SDKs for Python, TypeScript, Ruby, Go, and Java&lt;/li&gt;
&lt;li&gt;Supports Linux and Windows environments&lt;/li&gt;
&lt;li&gt;Exposes execution errors and exit-code information&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;I would use Daytona when I wanted several execution styles in the same platform.&lt;/p&gt;

&lt;p&gt;Maybe one agent only needs quick shell commands, another needs a persistent terminal session, and another works better with interpreter-style execution.&lt;/p&gt;

&lt;p&gt;Daytona can cover all of those without forcing everything into the same model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daytona process and code execution&lt;/li&gt;
&lt;li&gt;Daytona Python process SDK&lt;/li&gt;
&lt;li&gt;Daytona V2 process execution&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  4. Modal Sandboxes
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The one I would look at when agent code execution starts becoming a compute problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmsaa8ktfnh5xhbd968sn.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%2Fmsaa8ktfnh5xhbd968sn.png" alt="modal sandboxes" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Modal already has a strong presence around serverless compute, GPUs, and ML workloads.&lt;/p&gt;

&lt;p&gt;Its Sandbox API gives you isolated command execution inside that same infrastructure.&lt;/p&gt;

&lt;p&gt;The basic API is pretty clean:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;modal&lt;/span&gt;

&lt;span class="n"&gt;sb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sb&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&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;-c&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;print(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hello&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can wait for the process, inspect its return code, or stream output while the command is still running.&lt;/p&gt;

&lt;p&gt;This feels a little different from something like E2B.&lt;/p&gt;

&lt;p&gt;You are working with processes inside a sandbox rather than a persistent Python notebook-style interpreter.&lt;/p&gt;

&lt;p&gt;But that becomes useful when the code your agent is running is actually expensive.&lt;/p&gt;

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

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

&lt;ul&gt;
&lt;li&gt;Creates isolated sandboxes programmatically&lt;/li&gt;
&lt;li&gt;Executes processes with &lt;code&gt;Sandbox.exec&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Streams process output&lt;/li&gt;
&lt;li&gt;Exposes process return codes&lt;/li&gt;
&lt;li&gt;Supports configurable CPU and memory&lt;/li&gt;
&lt;li&gt;Supports GPU-backed workloads&lt;/li&gt;
&lt;li&gt;Supports custom runtime environments&lt;/li&gt;
&lt;li&gt;Integrates with Modal's broader compute platform&lt;/li&gt;
&lt;li&gt;Supports filesystem snapshots for longer-running workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;Modal is what I would look at when the code itself is expensive.&lt;/p&gt;

&lt;p&gt;If an agent is launching ML jobs, running GPU-backed code, evaluating models, or creating lots of compute-heavy executions in parallel, Modal fits pretty naturally.&lt;/p&gt;

&lt;p&gt;For a tiny Python analysis script, it might be more infrastructure than you really need.&lt;/p&gt;

&lt;p&gt;There is also a 24-hour sandbox lifetime limit, so that's something to consider.&lt;/p&gt;

&lt;p&gt;But for GPU-heavy execution, ML agents, and workloads where compute matters more than workspace persistence, Modal is one of the stronger options here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modal Sandboxes&lt;/li&gt;
&lt;li&gt;Modal Sandbox API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Vercel Sandbox
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A very natural fit for coding agents that build web applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7swlwhhaehh58uohbw0l.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%2F7swlwhhaehh58uohbw0l.png" alt="vercel sandbox" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Vercel Sandbox is pretty much what you would expect from Vercel.&lt;/p&gt;

&lt;p&gt;It focuses heavily on application-development workflows.&lt;/p&gt;

&lt;p&gt;Your agent can create an isolated environment, run commands, install dependencies, build an application, start a server, and expose the result through a preview.&lt;/p&gt;

&lt;p&gt;The JavaScript SDK gives you &lt;code&gt;runCommand&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&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;build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result includes the exit code, &lt;code&gt;stdout&lt;/code&gt;, &lt;code&gt;stderr&lt;/code&gt;, and combined output.&lt;/p&gt;

&lt;p&gt;For longer-running processes, you can start commands in detached mode and stream logs using an async iterator.&lt;/p&gt;

&lt;p&gt;Vercel also supports named sandboxes through &lt;code&gt;getOrCreate&lt;/code&gt;, so an agent can return to an existing environment instead of provisioning a new one every time.&lt;/p&gt;

&lt;p&gt;Sandboxes are persistent by default and automatically snapshot their filesystem when stopped.&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%2Fk3zfe3pfj1div2xq3dxh.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%2Fk3zfe3pfj1div2xq3dxh.png" alt="vercel sandbox showcase" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Executes commands through a TypeScript SDK&lt;/li&gt;
&lt;li&gt;Returns exit codes, &lt;code&gt;stdout&lt;/code&gt;, and &lt;code&gt;stderr&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Supports detached long-running commands&lt;/li&gt;
&lt;li&gt;Streams logs from running processes&lt;/li&gt;
&lt;li&gt;Supports named persistent sandboxes&lt;/li&gt;
&lt;li&gt;Automatically snapshots filesystem state&lt;/li&gt;
&lt;li&gt;Uses Firecracker microVMs&lt;/li&gt;
&lt;li&gt;Provides a Python SDK alongside JavaScript&lt;/li&gt;
&lt;li&gt;Fits naturally into Vercel preview and deployment workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;I would use Vercel Sandbox for an app-building agent.&lt;/p&gt;

&lt;p&gt;Think of an agent that receives a prompt, changes a Next.js project, installs something, runs the build, starts a development server, and then gives the user a preview.&lt;/p&gt;

&lt;p&gt;That workflow maps really well to Vercel.&lt;/p&gt;

&lt;p&gt;It also makes sense if your stack is already heavily TypeScript-first.&lt;/p&gt;

&lt;p&gt;For agents that build web applications and need to show the result quickly, Vercel Sandbox is a pretty obvious one to check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vercel Sandbox&lt;/li&gt;
&lt;li&gt;Vercel Sandbox SDK&lt;/li&gt;
&lt;li&gt;Working with Sandbox&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. AWS Bedrock AgentCore Code Interpreter
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Best when the rest of your agent stack already lives inside AWS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flky6llyby1xwhf7yy8b9.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%2Flky6llyby1xwhf7yy8b9.png" alt="AWS Bedrock AgentCore Code Interpreter" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
AWS also has a managed code interpreter built specifically for agent workloads through Bedrock AgentCore.&lt;/p&gt;

&lt;p&gt;Instead of setting up your own sandbox infrastructure, you start an interpreter session and send code to it through the AgentCore API.&lt;/p&gt;

&lt;p&gt;It supports operations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;executeCode
executeCommand
readFiles
writeFiles
listFiles
removeFiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, you can execute Python using &lt;code&gt;invoke_code_interpreter&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_code_interpreter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;codeInterpreterIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws.codeinterpreter.v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executeCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;arguments&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;language&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;python&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;code&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;print(2 + 2)&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;The result comes back as an event stream.&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%2F8mltrokajgn9z405dsr8.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%2F8mltrokajgn9z405dsr8.png" alt="AWS Bedrock AgentCore Code Interpreter showcase" width="800" height="332"&gt;&lt;/a&gt;&lt;br&gt;
The interpreter supports Python, JavaScript, and TypeScript, and state can remain available during the session unless you explicitly clear the context.&lt;/p&gt;

&lt;p&gt;You also get the things you would normally expect from AWS around IAM, networking, and integration with the rest of the Bedrock stack.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Executes Python, JavaScript, and TypeScript&lt;/li&gt;
&lt;li&gt;Runs shell commands&lt;/li&gt;
&lt;li&gt;Reads and writes files&lt;/li&gt;
&lt;li&gt;Supports stateful interpreter sessions&lt;/li&gt;
&lt;li&gt;Returns results through an event stream&lt;/li&gt;
&lt;li&gt;Supports asynchronous command execution&lt;/li&gt;
&lt;li&gt;Integrates directly with Bedrock AgentCore&lt;/li&gt;
&lt;li&gt;Works with AWS security and networking controls&lt;/li&gt;
&lt;li&gt;Has Boto3 and higher-level AgentCore SDK support&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;I would use AgentCore Code Interpreter when the rest of the agent already runs on AWS.&lt;/p&gt;

&lt;p&gt;If you're already using Bedrock, IAM, VPCs, CloudWatch, and the rest of that ecosystem, adding another execution provider might just create more moving pieces.&lt;/p&gt;

&lt;p&gt;AgentCore keeps the execution layer inside AWS.&lt;/p&gt;

&lt;p&gt;It becomes less interesting if you want portable agent environments, hosted Git, snapshot forks, or infrastructure that isn't tied to AWS.&lt;/p&gt;

&lt;p&gt;But for enterprise teams already building around Bedrock, it is probably one of the more straightforward choices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS AgentCore Code Interpreter&lt;/li&gt;
&lt;li&gt;Boto3 &lt;code&gt;invoke_code_interpreter&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  7. Claude Code Execution
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The easiest option when Claude just needs to run some code and you don't want another infrastructure layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0epksccfqgpzegzxwctr.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%2F0epksccfqgpzegzxwctr.png" alt="claude code execution" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
The last one is slightly different from everything above.&lt;/p&gt;

&lt;p&gt;With Claude's code execution tool, you aren't directly creating or controlling the execution environment.&lt;/p&gt;

&lt;p&gt;You give Claude access to the server-side tool through the Messages API:&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;"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;"code_execution_20250825"&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;"code_execution"&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;Claude decides when code execution is useful, runs the code inside Anthropic's infrastructure, and returns the result as part of the model response.&lt;/p&gt;

&lt;p&gt;You don't need to manage a sandbox SDK, provision a VM, or deal with the underlying environment yourself.&lt;/p&gt;

&lt;p&gt;Newer versions also support persistent REPL state, so Claude can keep interpreter context across executions inside the same container.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/PQ8jjGKfP5o" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
Generated files can then be pulled through Anthropic's Files API.&lt;/p&gt;

&lt;p&gt;The main limitation is pretty much the same thing that makes it easy.&lt;/p&gt;

&lt;p&gt;You don't control much of the environment.&lt;/p&gt;

&lt;p&gt;There is no custom container image, the environment has no internet access, and the whole thing is tied directly to Claude's API workflow.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Lets Claude execute code server-side&lt;/li&gt;
&lt;li&gt;Requires no separate sandbox infrastructure&lt;/li&gt;
&lt;li&gt;Supports Bash and file operations&lt;/li&gt;
&lt;li&gt;Returns execution results directly through Claude&lt;/li&gt;
&lt;li&gt;Supports persistent REPL state in newer versions&lt;/li&gt;
&lt;li&gt;Lets applications reuse a container&lt;/li&gt;
&lt;li&gt;Supports generated-file retrieval&lt;/li&gt;
&lt;li&gt;Runs without internet access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  💡 When I would pick it
&lt;/h3&gt;

&lt;p&gt;If Claude needs to analyze a dataset, run some calculations, manipulate files, or execute generated Python as part of a normal application flow, I would probably start here.&lt;/p&gt;

&lt;p&gt;You avoid almost the entire infrastructure problem.&lt;/p&gt;

&lt;p&gt;But I wouldn't use it as the execution layer for a full coding-agent platform.&lt;/p&gt;

&lt;p&gt;You can't bring your own image, control the environment like a normal sandbox, or attach the same kind of persistent workspace infrastructure you get with Tensorlake, E2B, Daytona, or Vercel.&lt;/p&gt;

&lt;p&gt;For lightweight execution inside Claude applications though, having almost nothing to manage is a pretty nice advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude code execution tool&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, which code execution API should you use?
&lt;/h2&gt;

&lt;p&gt;There isn't one answer here because these products are solving slightly different parts of the same problem.&lt;/p&gt;

&lt;p&gt;If your agent needs a persistent workspace it can resume later, fork into parallel runs, and keep files and Git state around, &lt;strong&gt;Tensorlake is the one I would look at first&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That makes it especially useful for coding agents, RL rollouts, evals, and CI-style workflows where execution is only one part of a much longer process.&lt;/p&gt;

&lt;p&gt;For more interpreter-style execution, &lt;strong&gt;E2B&lt;/strong&gt; is a strong option. &lt;strong&gt;Daytona&lt;/strong&gt; works well if you want shell execution, sessions, and interpreter APIs together, while &lt;strong&gt;Modal&lt;/strong&gt; makes more sense for GPU-heavy workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel Sandbox&lt;/strong&gt; fits web-building agents nicely, &lt;strong&gt;AgentCore&lt;/strong&gt; is a natural choice if you're already deep in AWS, and Claude's built-in execution tool is handy when you just need lightweight code execution inside Claude.&lt;/p&gt;

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

&lt;p&gt;The main thing is to pick based on what happens &lt;strong&gt;after&lt;/strong&gt; the code runs, not just whether the API can execute it.&lt;/p&gt;

&lt;p&gt;That's all for this one. I'll see you in the next one. ✌️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Moderate Live Chat in Real Time with Jev and Composio (Discord + Twitch)</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:05:00 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-moderate-live-chat-in-real-time-with-jev-and-composio-discord-twitch-5ab0</link>
      <guid>https://dev.to/composiodev/how-to-moderate-live-chat-in-real-time-with-jev-and-composio-discord-twitch-5ab0</guid>
      <description>&lt;p&gt;Anyone who has moderated a live chat knows how fast things go wrong.&lt;/p&gt;

&lt;p&gt;Someone says something bad and by the time a mod sees it, everyone in the chat already has. 🫩&lt;/p&gt;

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

&lt;p&gt;Most AI moderation setups use an LLM for this. That works, but an LLM is usually doing way more than we need here and is way slower.&lt;/p&gt;

&lt;p&gt;For moderation, I mostly want one thing: &lt;strong&gt;should I remove this message or not?&lt;/strong&gt; So I tried &lt;strong&gt;Jev&lt;/strong&gt;, a decision model from TypeSafe. I guess you're already familiar with the model, as its been over a week the model dropped.&lt;/p&gt;

&lt;p&gt;It does not generate text. You give it a typed question, and it gives you a probability back.&lt;/p&gt;

&lt;p&gt;Thats pretty much perfect for a moderation bot. 👀&lt;/p&gt;

&lt;p&gt;I called Jev through &lt;a href="https://composio.dev/toolkits/jev" rel="noopener noreferrer"&gt;Composio's Jev toolkit&lt;/a&gt;, so I didn't have to set up a separate HTTP client or keep the Jev API key inside the code.&lt;/p&gt;

&lt;p&gt;Then I connected the same moderation logic to &lt;strong&gt;Discord&lt;/strong&gt; and &lt;strong&gt;Twitch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to play around with the project, or build on top of it, you can find the source code here:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Source Code: &lt;a href="https://github.com/shricodev/jev-youtube-discord-moderator" rel="noopener noreferrer"&gt;shricodev/jev-youtube-discord-moderator&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's get into it. ✌️&lt;/p&gt;

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

&lt;p&gt;Here's what we'll go through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Jev is and how it is different from a normal LLM (in short)&lt;/li&gt;
&lt;li&gt;Setting up Jev through Composio&lt;/li&gt;
&lt;li&gt;Building a Discord bot that removes such messages&lt;/li&gt;
&lt;li&gt;Building the same moderation flow for Twitch with EventSub&lt;/li&gt;
&lt;li&gt;A quick YouTube version (I leave that upto you)😉&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOTE:&lt;/strong&gt; This is still an AI bot, so you might want to keep human mods around. But considering how cheap it is, at &lt;strong&gt;$0.042 per million input tokens&lt;/strong&gt; with output basically free, it could be worth trying if you're a streamer or run a large live chat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What's Jev, Anyway?
&lt;/h2&gt;

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

&lt;p&gt;The easiest way I think about Jev is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An LLM generates an answer. Jev makes a decision.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You send Jev a &lt;code&gt;state&lt;/code&gt;, which for us is the chat message, along with one or more questions. It can answer all of those questions in the same call.&lt;/p&gt;

&lt;p&gt;Jev currently gives three question types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Noul&lt;/strong&gt; - Ask whether something is true and get a probability between 0 and 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choice&lt;/strong&gt; - Give it a few options and let it pick one, along with probabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; - Ask it to place something somewhere on an ordered scale of 2 to 10 options.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For moderation, &lt;strong&gt;Noul&lt;/strong&gt; is the important one.&lt;/p&gt;

&lt;p&gt;You can ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this a hostile comment at a person?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And instead of getting a paragraph back, you get something like:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Now I can simply decide what to do with that number.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;p ≥ 0.8&lt;/code&gt;, delete it.&lt;/p&gt;

&lt;p&gt;If it sits between &lt;code&gt;0.5&lt;/code&gt; and &lt;code&gt;0.8&lt;/code&gt;, send it for review. Everything else stays.&lt;/p&gt;

&lt;p&gt;No parsing generated text. No asking the model to "only answer YES or NO." 🤌&lt;/p&gt;

&lt;p&gt;It's also super cheap ($42 per billion input token, &lt;strong&gt;BILLION!!&lt;/strong&gt;, output is free).&lt;/p&gt;

&lt;p&gt;To give you an approximate, moderating 1,000 messages (roughly counting ~400 input token per msg) comes out to &lt;strong&gt;less than 2 cents&lt;/strong&gt;, almost free.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We are building two bots, but both use the same moderation logic.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new chat message
   │
   ▼
Jev via Composio (JEV_EVALUATE_STATE)
   ├── negative: noul   → "is this a hostile comment at a person?"
   └── category: choice → insult / harassment / spam / fine
   │
   ▼
p ≥ 0.8  → delete the message
p ≥ 0.5  → log it for review
else     → keep it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nice part is that the Jev code stays exactly the same on both platforms.&lt;/p&gt;

&lt;p&gt;Only the part that receives and deletes messages changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discord:&lt;/strong&gt; &lt;code&gt;discord.js&lt;/code&gt; listens for messages and deletes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitch:&lt;/strong&gt; EventSub sends us messages over WebSocket, and the Helix API deletes them.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 I've never used Twitch, so I'm not sure how good this works in a long term compared to Discord.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also need a way to properly test the bots.&lt;/p&gt;

&lt;p&gt;So I added a small &lt;strong&gt;flooder&lt;/strong&gt; script that sends a bunch of normal/racist messages into the test chat.&lt;/p&gt;

&lt;p&gt;We'll use that later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why not use Composio for Discord too?&lt;/strong&gt; Composio's Discord toolkit does not currently have a real-time new-message trigger. Polling would add extra delay, so I use the native SDK for incoming chat and Composio for Jev.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The project is pretty small.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jev (TypeSafe)&lt;/strong&gt; - Decides whether the message is hostile and gives us a probability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composio&lt;/strong&gt; - Handles the Jev connection and gives us one &lt;code&gt;tools.execute()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;discord.js&lt;/strong&gt; - Listens for and deletes Discord messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitch EventSub + Helix&lt;/strong&gt; - Receives Twitch chat messages and deletes them when needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 22&lt;/strong&gt; - Everything runs as plain &lt;code&gt;.mjs&lt;/code&gt; files, so there is no framework or build step (perfect to test right away).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node 22 already includes &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;WebSocket&lt;/code&gt;, which also keeps the Twitch side pretty clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Set Up the Project
&lt;/h2&gt;

&lt;p&gt;Getting the project running is pretty simple.&lt;/p&gt;

&lt;p&gt;Clone the repo and install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shricodev/jev-youtube-discord-moderator
&lt;span class="nb"&gt;cd &lt;/span&gt;jev-youtube-discord-moderator/jev-discord-demo
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure Composio
&lt;/h3&gt;

&lt;p&gt;First, you need a Composio API key.&lt;/p&gt;

&lt;p&gt;Create an account on &lt;a href="https://dashboard.composio.dev/" rel="noopener noreferrer"&gt;Composio&lt;/a&gt;, get your API key, and add it to the &lt;code&gt;.env&lt;/code&gt; file in the root of the project.&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%2Fpasnank598slz8gn7ds6.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%2Fpasnank598slz8gn7ds6.png" alt="composio create api key" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then add the &lt;a href="https://composio.dev/toolkits/jev" rel="noopener noreferrer"&gt;Jev toolkit&lt;/a&gt; from the dashboard and connect it using your TypeSafe API key.&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%2Fdi86aqzvxfwo94nie7vt.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%2Fdi86aqzvxfwo94nie7vt.png" alt="composio jev integration" width="799" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also keep the &lt;strong&gt;user ID&lt;/strong&gt; for that connection. We'll need it because every Composio tool call is scoped to a user.&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%2F7xajnq5mf7224vis38t6.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%2F7xajnq5mf7224vis38t6.png" alt="composio jev connection" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discord and Twitch Setup
&lt;/h3&gt;

&lt;p&gt;This part can easily become its own guide, so I'll keep it short.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ You can find proper information in the &lt;a href="https://docs.discord.com/developers/intro" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; and &lt;a href="https://dev.twitch.tv/docs" rel="noopener noreferrer"&gt;Twitch&lt;/a&gt; own documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;For Discord:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an application and bot in the &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;Discord Developer Portal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Copy the bot token.&lt;/li&gt;
&lt;li&gt;Turn on &lt;strong&gt;Message Content Intent&lt;/strong&gt; under the &lt;strong&gt;Bot&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Invite the bot with &lt;code&gt;View Channel&lt;/code&gt;, &lt;code&gt;Read Message History&lt;/code&gt;, and &lt;code&gt;Manage Messages&lt;/code&gt; permissions (&lt;code&gt;permissions=74752&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Create a webhook inside your test channel for the flooder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without Message Content Intent, Discord will send messages to the bot without their content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Twitch:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register an app at &lt;a href="https://dev.twitch.tv/console/apps" rel="noopener noreferrer"&gt;dev.twitch.tv/console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Set the client type to &lt;strong&gt;Public&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the Client ID.&lt;/li&gt;
&lt;li&gt;Turn off AutoMod, blocked terms, and the non-mod chat delay while testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Otherwise Twitch may remove the test messages before our bot even gets them.&lt;/p&gt;

&lt;p&gt;Now add everything to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;// 👇 .env

&lt;span class="nv"&gt;COMPOSIO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;COMPOSIO_USER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="nv"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;WATCH_CHANNEL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;FLOOD_WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="nv"&gt;TWITCH_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;COMPOSIO_API_KEY&lt;/code&gt; comes from the Composio dashboard.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;COMPOSIO_USER_ID&lt;/code&gt; is the user that owns your Jev connection.&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%2F5598mqobfvc3w2n52bl0.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%2F5598mqobfvc3w2n52bl0.png" alt="composio user id" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we have the Discord bot token, channel ID, webhook URL, and Twitch Client ID. That's all we need for now. ✌️&lt;/p&gt;

&lt;h2&gt;
  
  
  The Jev Call
&lt;/h2&gt;

&lt;p&gt;This is basically the main part of the project.&lt;/p&gt;

&lt;p&gt;And the nice thing is that both bots use the same code.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;jev-mod.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 jev-mod.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Composio&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;@composio/core&lt;/span&gt;&lt;span class="dl"&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;composio&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;Composio&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMPOSIO_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Both questions run in parallel inside one Jev call.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;negative&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="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The message is a hostile comment aimed at a person: an insult, harassment, a personal attack, &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;or telling someone to leave. Casual swearing, self-deprecating jokes, complaining about a game &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="s1"&gt;or product, and hype like "that was sick" do NOT count.&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="na"&gt;category&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="s2"&gt;choice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;criteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;insult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Insults or belittles someone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;harassment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Targets, dismisses or tells someone to go away&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;spam&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Repetitive, promotional or meaningless flooding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Normal chat, including jokes, hype and mild frustration&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;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;judge&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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;res&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;composio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JEV_EVALUATE_STATE&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="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMPOSIO_USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;dangerouslySkipVersionCheck&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;successful&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bruhh! JEV_EVALUATE_STATE failed&lt;/span&gt;&lt;span class="dl"&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;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;answers&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;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;negative&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;noul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choice&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;There are two things worth looking at here. First is the &lt;code&gt;negative&lt;/code&gt; question.&lt;/p&gt;

&lt;p&gt;Chat messages can be weird. Someone might say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;this thing is absolutely killing me&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It has negative words in it, but isn't an attack on another person.&lt;/p&gt;

&lt;p&gt;So I don't just tell Jev what counts as hostile. I also tell it what &lt;strong&gt;doesn't&lt;/strong&gt; count.&lt;/p&gt;

&lt;p&gt;Then we have &lt;code&gt;category&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This one isn't actually used to decide whether a message gets deleted. I mainly added it because seeing &lt;code&gt;insult&lt;/code&gt;, &lt;code&gt;harassment&lt;/code&gt;, &lt;code&gt;spam&lt;/code&gt;, or &lt;code&gt;fine&lt;/code&gt; in the logs makes debugging much easier.&lt;/p&gt;

&lt;p&gt;And since both questions run in the same Jev call, we get it without adding another request.&lt;/p&gt;

&lt;p&gt;That's basically the entire AI part. 🔥&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOTE:&lt;/strong&gt; Direct tool execution in Composio needs a toolkit version. You can either pin one with &lt;code&gt;version: "&amp;lt;toolkit-version&amp;gt;"&lt;/code&gt; or use &lt;code&gt;dangerouslySkipVersionCheck: true&lt;/code&gt; to run the latest version. If you use neither, the SDK throws a &lt;code&gt;ToolVersionRequiredError&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Discord Bot
&lt;/h2&gt;

&lt;p&gt;Now we can actually use it.&lt;/p&gt;

&lt;p&gt;The Discord bot is a normal &lt;code&gt;discord.js&lt;/code&gt; client.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 You'll find ton of guides on the internet that show you how to use &lt;code&gt;discord.js&lt;/code&gt;. I also wrote one for freeCodeCamp, so if you're not sure how to get started, you might want to check it out: &lt;a href="https://www.freecodecamp.org/news/build-a-langgraph-composio-powered-discord-bot/" rel="noopener noreferrer"&gt;How to Build a LangGraph and Composio-Powered Discord Bot&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It listens for a message, sends the text to &lt;code&gt;judge()&lt;/code&gt;, and then decides what to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 jev-mod.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&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="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Events&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;discord.js&lt;/span&gt;&lt;span class="dl"&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;DELETE_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// noul&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REVIEW_AT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Guilds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GuildMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;GatewayIntentBits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageContent&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="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="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="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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="k"&gt;return&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channelId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WATCH_CHANNEL_ID&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="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;msg&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="k"&gt;return&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;v&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;judge&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;content&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;DELETE_AT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`🗑️ DELETE  jev &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;ms  p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;  "&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;content&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="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="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;REVIEW_AT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`🟡 REVIEW  p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="s2"&gt;  "&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;content&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// rest of the code…&lt;/span&gt;

&lt;span class="k"&gt;await&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;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've built a Discord bot before, most of this should look familiar.&lt;/p&gt;

&lt;p&gt;The interesting part is the two thresholds. Why not just have one?&lt;/p&gt;

&lt;p&gt;Because automatically deleting someone's message is a pretty strong action.&lt;/p&gt;

&lt;p&gt;If Jev gives us a high probability, we remove it.&lt;/p&gt;

&lt;p&gt;If the probability is somewhere in the middle, we simply log it and let a human take a look. 🧐&lt;/p&gt;

&lt;p&gt;That gives us a nice middle ground instead of forcing every message into delete or keep.&lt;/p&gt;

&lt;p&gt;The full repo adds two more things. There is a small limit on how many Jev requests can run at once, so a sudden flood does not fire hundreds of calls together.&lt;/p&gt;

&lt;p&gt;There is also a delete queue for every channel.&lt;/p&gt;

&lt;p&gt;When messages start piling up, it uses &lt;code&gt;bulkDelete()&lt;/code&gt; instead of deleting every message separately.&lt;/p&gt;

&lt;p&gt;Discord rate limits individual deletes pretty quickly, so this helps a lot during the flood test. ✅&lt;/p&gt;

&lt;h2&gt;
  
  
  The Twitch Bot
&lt;/h2&gt;

&lt;p&gt;Twitch needs a little more work.&lt;/p&gt;

&lt;p&gt;There isn't really a &lt;code&gt;discord.js&lt;/code&gt; equivalent that we are using here, so I talk to Twitch directly.&lt;/p&gt;

&lt;p&gt;New chat messages come through &lt;strong&gt;EventSub over WebSocket&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means Twitch sends the message to us when it happens instead of making us keep checking for new messages.&lt;/p&gt;

&lt;p&gt;When the WebSocket connects, Twitch sends a welcome message containing a session ID.&lt;/p&gt;

&lt;p&gt;We then have &lt;strong&gt;10 seconds&lt;/strong&gt; to subscribe to the chat events using that ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 twitch-mod.mjs&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://eventsub.wss.twitch.tv/ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&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="nx"&gt;data&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;type&lt;/span&gt; &lt;span class="o"&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;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_type&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;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;session_welcome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Must subscribe within 10 s of the welcome.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;helix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bot&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;POST&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;/eventsub/subscriptions&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="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="s2"&gt;channel.chat.message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;broadcaster_user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;broadcasterId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;websocket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;session_id&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;session&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="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="nx"&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;notification&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="nf"&gt;onChatMessage&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;payload&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&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;v&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;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="c1"&gt;// 👈 the exact same Jev call&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;DELETE_AT&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="nf"&gt;helix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bot&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;DELETE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;`/moderation/chat?broadcaster_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;broadcasterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;moderator_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;message_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_id&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="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// rest of the code…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And notice this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&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;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the exact same function we used for Discord. Nothing changes on the Jev side.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;helix()&lt;/code&gt; is just a small &lt;code&gt;fetch&lt;/code&gt; wrapper. It adds the token and Client ID headers and refreshes the token when needed.&lt;/p&gt;

&lt;p&gt;For Twitch login, I use the &lt;strong&gt;Device Code flow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The script prints a link and code, you approve it in your browser, and you're done.&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%2Fz5bbri8of176ubt1sft7.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%2Fz5bbri8of176ubt1sft7.png" alt="Twitch connection approval" width="602" height="360"&gt;&lt;/a&gt;&lt;br&gt;
So we don't need to set up a redirect server just for authentication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOTE:&lt;/strong&gt; Twitch does not let you delete messages from the broadcaster or other moderators. If you test this by sending bad messages from your own broadcaster account, the bot won't be able to remove them. Use a second regular account for testing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repo also handles &lt;code&gt;session_reconnect&lt;/code&gt; and checks whether the EventSub connection is still alive.&lt;/p&gt;

&lt;p&gt;Connections can drop, and having the mod bot silently stop receiving messages in the middle of a stream would be... not great. 🫠&lt;/p&gt;
&lt;h2&gt;
  
  
  Flooding the Chat
&lt;/h2&gt;

&lt;p&gt;Now we need to actually test the thing.&lt;/p&gt;

&lt;p&gt;Sending one bad message manually isn't very useful.&lt;/p&gt;

&lt;p&gt;So the repo includes a flooder that sends a random mix of hostile and normal messages.&lt;/p&gt;

&lt;p&gt;Run the bot in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the flooder in another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run flood &lt;span class="nt"&gt;--&lt;/span&gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;60&lt;/code&gt; means it will send 60 test messages.&lt;/p&gt;

&lt;p&gt;For Twitch, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run twitch:bot
npm run twitch:flood &lt;span class="nt"&gt;--&lt;/span&gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bot in Action
&lt;/h2&gt;

&lt;p&gt;Here's a quick demo of the Discord bot:&lt;/p&gt;

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

&lt;p&gt;And for the Twitch one, simply run the command I gave you earlier. That should do it.&lt;/p&gt;

&lt;p&gt;While the bot is running, you'll see logs that look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ keep    jev  389ms  p=0.06 fine ...
🟡 REVIEW  jev  909ms  p=0.73 insult ...
✅ keep    jev  372ms  p=0.03 fine ...

🗑️  DELETE  jev  341ms  delete  641ms  total  984ms  p=0.92 insult ...
🗑️  DELETE  jev  399ms  delete  418ms  total  979ms  p=0.93 insult ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what those numbers mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jev&lt;/code&gt; is the time Jev took to make the decision.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete&lt;/code&gt; is how long Discord took to remove the message.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total&lt;/code&gt; is the full time from receiving the message to deleting it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the connection set up properly, Jev was usually around &lt;strong&gt;340 to 460 ms&lt;/strong&gt; in my test.&lt;/p&gt;

&lt;p&gt;Funny enough, Discord deleting the message was sometimes slower than Jev deciding whether it should be deleted. 😭&lt;/p&gt;

&lt;p&gt;From my first 10-message Discord run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jev latency (p50)&lt;/td&gt;
&lt;td&gt;399 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Receive → deleted (p50)&lt;/td&gt;
&lt;td&gt;979 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad messages caught&lt;/td&gt;
&lt;td&gt;4 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normal messages deleted&lt;/td&gt;
&lt;td&gt;0 of 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One message did not cross the delete threshold:&lt;/p&gt;

&lt;p&gt;If you want a stricter bot, you can simply lower &lt;code&gt;DELETE_AT&lt;/code&gt;. That's also why I like using probabilities here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About YouTube Live?
&lt;/h2&gt;

&lt;p&gt;I left YouTube Live as the one for you to finish. 🙂&lt;/p&gt;

&lt;p&gt;The same idea works there, but testing it takes a little more setup.&lt;/p&gt;

&lt;p&gt;You can poll &lt;code&gt;liveChatMessages.list&lt;/code&gt;, and YouTube tells you how long to wait before making the next request.&lt;/p&gt;

&lt;p&gt;There is also a newer &lt;code&gt;streamList&lt;/code&gt; method that streams messages, which is probably what I would try for this project.&lt;/p&gt;

&lt;p&gt;But the important bit is that &lt;strong&gt;the Jev code does not change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's a rough version using &lt;code&gt;googleapis&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 youtube-mod.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;google&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;googleapis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// OAuth client for the channel owner or a moderator, with the youtube.force-ssl scope&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;youtube&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;oauthClient&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&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="nx"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liveBroadcasts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;part&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;snippet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;broadcastStatus&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="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;liveChatId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liveChatId&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;pageToken&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;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&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="nx"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liveChatMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;liveChatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;part&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;snippet&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;authorDetails&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// The first page is chat history, so skip it.&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;first&lt;/span&gt;&lt;span class="p"&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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;v&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;judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// same Jev call again&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;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;liveChatMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&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;m&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="nx"&gt;first&lt;/span&gt; &lt;span class="o"&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;pageToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPageToken&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;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pollingIntervalMillis&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;Replace the polling part with &lt;code&gt;streamList&lt;/code&gt;, add the same review threshold and request limit we used for Discord, and you basically have the third bot.&lt;/p&gt;

&lt;p&gt;Try it and let me know what kind of latency you get. ✌️&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And that's basically it. 🙌&lt;/p&gt;

&lt;p&gt;We built the same real-time moderation flow for Discord and Twitch using one shared Jev function.&lt;/p&gt;

&lt;p&gt;In my small Discord test, the full receive-to-delete flow came in at around a second. And the actual AI part is tiny.&lt;/p&gt;

&lt;p&gt;It's just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;composio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JEV_EVALUATE_STATE&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="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMPOSIO_USER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&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;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUESTIONS&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dangerouslySkipVersionCheck&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, you don't have to wait on an LLM sending response back 10s after you send the msg. This is exactly why I did this blog post as well. Right model with right pricing for exactly this tyoe of tasks.&lt;/p&gt;

&lt;p&gt;If you find a bug, want to improve something, or manage to get the YouTube version working, feel free to open an issue or PR.&lt;/p&gt;

&lt;p&gt;You can find the entire project here: &lt;a href="https://github.com/shricodev/jev-youtube-discord-moderator" rel="noopener noreferrer"&gt;shricodev/jev-youtube-discord-moderator&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all for this one. I'll see you in the next one. ✌️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>jev</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Tested 7 Document Parsing APIs on Real Scanned PDFs (2026): Code and Results</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:56:36 +0000</pubDate>
      <link>https://dev.to/shricodev/i-tested-7-document-parsing-apis-on-real-scanned-pdfs-2026-code-and-results-3h2b</link>
      <guid>https://dev.to/shricodev/i-tested-7-document-parsing-apis-on-real-scanned-pdfs-2026-code-and-results-3h2b</guid>
      <description>&lt;p&gt;Every "best document parsing API" post I have read this year has the same problem. Nobody actually calls the APIs.&lt;/p&gt;

&lt;p&gt;They copy the pricing page, add a feature table, rank everything, and call it a day. Zero PDFs were harmed in the making of that content. 🥴&lt;/p&gt;

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

&lt;p&gt;So I did something else. I took 11 real documents from public datasets. Invoices, a French bank statement, a photographed receipt, a handwritten cheque, two contracts, and a medical EOB.&lt;/p&gt;

&lt;p&gt;Then I wrote one JSON schema for each document type and sent the exact same files through Extend, Reducto, LlamaExtract, AWS Textract, Mistral OCR, Claude, and GPT. 119 API calls in total.&lt;/p&gt;

&lt;p&gt;I saved every response too, so you can check the work yourself. This one got interesting pretty fast.&lt;/p&gt;

&lt;p&gt;Let's get into it.&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%2F76bwj222rqa93lrbe2qg.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%2F76bwj222rqa93lrbe2qg.png" alt="document corpus for the test" width="800" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;NOTE&lt;/strong&gt; Everything below comes from stored API responses. The harness is public too, so you do not have to take my word for any of it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;th&gt;Runner up&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw field accuracy&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Claude direct&lt;/strong&gt; 0.991&lt;/td&gt;
&lt;td&gt;GPT direct and Reducto 0.982&lt;/td&gt;
&lt;td&gt;11 docs, fixed scoring, no LLM judge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Never hallucinated a value&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Extend&lt;/strong&gt; 0 out of 11&lt;/td&gt;
&lt;td&gt;Claude, GPT, Reducto also 0&lt;/td&gt;
&lt;td&gt;Textract 2, Mistral 3, LlamaExtract 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Table rows never missed&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Extend&lt;/strong&gt; row F1 1.00&lt;/td&gt;
&lt;td&gt;Everyone else 0.99&lt;/td&gt;
&lt;td&gt;Only perfect row F1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheapest per correct field&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;GPT direct&lt;/strong&gt; $0.00047&lt;/td&gt;
&lt;td&gt;Mistral OCR $0.00049&lt;/td&gt;
&lt;td&gt;Textract was 14x GPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fastest&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Mistral OCR&lt;/strong&gt; 4.2s median&lt;/td&gt;
&lt;td&gt;Claude direct 6.4s&lt;/td&gt;
&lt;td&gt;Mistral also hallucinated 3 times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer experience&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Claude and GPT&lt;/strong&gt; about 10 min&lt;/td&gt;
&lt;td&gt;Extend about 25 min&lt;/td&gt;
&lt;td&gt;Textract took about 45 min plus another LLM step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent support&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Extend&lt;/strong&gt; llms.txt, agents.md, MCP, plugin&lt;/td&gt;
&lt;td&gt;Reducto 3 of 4&lt;/td&gt;
&lt;td&gt;AWS has nothing specific for Textract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardest to justify&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AWS Textract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Highest cost in this test and some ugly misses&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The short version is pretty simple. Claude had the best raw accuracy. GPT was the cheapest per correct field. Extend was the only document API with &lt;strong&gt;zero hallucinations and perfect row recall&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That last part matters a lot once the output is going into a real system. If a human is checking the JSON, raw accuracy can be enough. If software is going to act on the result, I care a lot more about made up values.&lt;/p&gt;

&lt;p&gt;Textract had the roughest run here. I will show you why.&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%2Fcxzew491d14bkvytb0f2.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%2Fcxzew491d14bkvytb0f2.png" alt="cost per accuracy chart" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;There are six products in the test. Claude and GPT run directly, so the result table has seven columns.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Native schema&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Extend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /extract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;default, API &lt;code&gt;2026-02-09&lt;/code&gt;, &lt;code&gt;extend-ai&lt;/code&gt; 1.19.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Reducto&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /extract&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;standard, &lt;code&gt;deep_extract&lt;/code&gt; off&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;LlamaExtract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract_stateless()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BALANCED&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;AWS Textract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AnalyzeDocument&lt;/code&gt; FORMS + TABLES&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;plus LLM normalizer, us-east-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mistral OCR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v1/ocr&lt;/code&gt; + &lt;code&gt;document_annotation_format&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mistral-ocr-latest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v1/messages&lt;/code&gt;, forced tool&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-5&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6b&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GPT direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v1/responses&lt;/code&gt;, &lt;code&gt;json_schema&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gpt-5.5&lt;/code&gt;, &lt;code&gt;strict: false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A couple of things before we start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LlamaParse structured output is deprecated.&lt;/strong&gt; If you want schema extraction on LlamaCloud now, you use LlamaExtract.&lt;/p&gt;

&lt;p&gt;The old &lt;code&gt;llama-cloud-services&lt;/code&gt; package is also deprecated, so I used &lt;code&gt;llama-cloud&amp;gt;=1.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Textract does not return your schema as JSON.&lt;/strong&gt; It gives you blocks like &lt;code&gt;LINE&lt;/code&gt;, &lt;code&gt;KEY_VALUE_SET&lt;/code&gt;, &lt;code&gt;TABLE&lt;/code&gt;, and &lt;code&gt;CELL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I had to turn those blocks into text and pass that text to &lt;code&gt;claude-haiku-4-5&lt;/code&gt; to get the final JSON. That extra model call counts toward Textract cost and latency.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 In simple words, Textract could read the page, but it needed another model to shape that reading into the same output every other tool was asked to return.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keep that in mind for the numbers later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The documents
&lt;/h2&gt;

&lt;p&gt;I used 11 documents and 35 pages in total. All of them came from public datasets.&lt;/p&gt;

&lt;p&gt;The mix was roughly 3 easy, 4 medium, and 4 hard.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Pages&lt;/th&gt;
&lt;th&gt;Why it is here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;invoice_clean&lt;/td&gt;
&lt;td&gt;Voxel51 synthetic invoices&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;clean invoice, 7 rows, European decimals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;invoice_complex&lt;/td&gt;
&lt;td&gt;FATURA2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;different layout, discount row, weird total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;receipt_photo&lt;/td&gt;
&lt;td&gt;SROIE 2019&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;low resolution and handwriting on top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;contract_simple&lt;/td&gt;
&lt;td&gt;CUAD&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;agreement date is blank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;contract_complex&lt;/td&gt;
&lt;td&gt;CUAD pages 1 to 8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;governing law is outside the page limit and &lt;code&gt;$6,200,000&lt;/code&gt; is not the contract value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;bank_statement&lt;/td&gt;
&lt;td&gt;Bankstatemently&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;French Canadian, 25 transactions, watermark, 2 digit years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07&lt;/td&gt;
&lt;td&gt;noisy_form&lt;/td&gt;
&lt;td&gt;FUNSD&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;scanned form with 41 label and value pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08&lt;/td&gt;
&lt;td&gt;handwritten_note&lt;/td&gt;
&lt;td&gt;handwritten cheque dataset&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;written amount and number box disagree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09&lt;/td&gt;
&lt;td&gt;EOB&lt;/td&gt;
&lt;td&gt;CMS sample&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;medical EOB with &lt;code&gt;XXXXXX&lt;/code&gt; placeholders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;mixed_packet&lt;/td&gt;
&lt;td&gt;built from 01, 04, and 09&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;checks page boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;contract_scanned&lt;/td&gt;
&lt;td&gt;scanned copy of 04&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;same contract, but rasterised at 200dpi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every file is locked with a sha256 in &lt;code&gt;manifest.lock.json&lt;/code&gt;. If one byte changes, the runner stops.&lt;/p&gt;

&lt;p&gt;That sounds a bit extreme, but it saves a lot of headaches later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 It means every provider saw the exact same file. No accidental OCR. No changed PDF. No weird hidden advantage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I also made every image source into a &lt;strong&gt;pixel only PDF&lt;/strong&gt; with &lt;code&gt;img2pdf&lt;/code&gt;. Then the build script checks it with &lt;code&gt;pdftotext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If any text layer exists, the build fails. Why bother&lt;/p&gt;

&lt;p&gt;Because if I OCR the file before the benchmark, I have basically handed every provider the answer key. And that kinda kills the whole test. 💁&lt;/p&gt;

&lt;h2&gt;
  
  
  The traps
&lt;/h2&gt;

&lt;p&gt;This is probably the most useful part of the corpus.&lt;/p&gt;

&lt;p&gt;A few fields are genuinely missing from the documents. The right answer for those is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a provider gives me a value anyway, it made something up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Doc 04&lt;/strong&gt; has a blank agreement date. The effective date exists, so it is very easy to copy the wrong date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doc 05&lt;/strong&gt; stops at page 8. The governing law is on page 26. It also contains &lt;code&gt;$6,200,000&lt;/code&gt;, but that number is a financing cap, not the contract value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doc 01&lt;/strong&gt; has no due date and no discount.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doc 08&lt;/strong&gt; disagrees with itself. The words say one amount and the number box says another. The correct output keeps both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg4qdt3nz83pxbyyqnwyf.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%2Fg4qdt3nz83pxbyyqnwyf.png" alt="document financing cap" width="800" height="195"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffqocrzhz3q87g836yevs.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%2Ffqocrzhz3q87g836yevs.png" alt="cheque document" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 These are not OCR tricks. I wanted to see what happens when the safest answer is simply "I do not know".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I kept the test fair
&lt;/h2&gt;

&lt;p&gt;The numbers are useless if every provider gets a different setup. So each one got the same thing.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The exact same PDF&lt;/li&gt;
&lt;li&gt;The exact same JSON schema&lt;/li&gt;
&lt;li&gt;The exact same instruction&lt;/li&gt;
&lt;li&gt;The same 2 second poll time and 300 second timeout&lt;/li&gt;
&lt;li&gt;Retries only for network or server errors&lt;/li&gt;
&lt;li&gt;The normal default mode for each product&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No custom prompt for one provider. No premium mode for another.&lt;/p&gt;

&lt;p&gt;A wrong answer did not get a second try. The prompt was tiny too.&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="c1"&gt;# bench/providers/base.py
&lt;/span&gt;&lt;span class="n"&gt;SHARED_INSTRUCTIONS&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;Extract the fields defined by the schema from this document. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use exactly the values printed in the document. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return dates as ISO 8601 (YYYY-MM-DD) and amounts as plain numbers &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;without currency symbols or thousands separators. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If a field is not present in the document, return null for it &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rather than guessing or inferring a value.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;POLL_INTERVAL_S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;TIMEOUT_S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a schema looks like this.&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;"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;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"vendor_name"&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;"type"&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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&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;"due_date"&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;"type"&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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ISO 8601"&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;"line_items"&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;"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;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&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;"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;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"description"&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;"type"&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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&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;"quantity"&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;"type"&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;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&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;"amount"&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;"type"&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;"number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"null"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"line total EXCLUDING tax"&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;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;p&gt;The important bit is that fields can return &lt;code&gt;null&lt;/code&gt;. That lets the provider say the value is missing instead of forcing it to guess.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;code&gt;null&lt;/code&gt; is not a failure here. Sometimes it is exactly the right answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That &lt;code&gt;"line total EXCLUDING tax"&lt;/code&gt; line exists because I got burned while building this. My first schema just said &lt;code&gt;amount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The invoice has Net price, Net worth, and Gross worth. Every provider picked something different and for about an hour I thought I had found a huge accuracy gap.&lt;/p&gt;

&lt;p&gt;But nope.&lt;/p&gt;

&lt;p&gt;My schema was just vague. 🫠&lt;/p&gt;

&lt;h3&gt;
  
  
  One adapter for every provider
&lt;/h3&gt;

&lt;p&gt;Every company has a completely different way to say "send PDF, give JSON".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;extend        POST /extract         upload -&amp;gt; file id   -&amp;gt; inline schema config
reducto       POST /extract         upload -&amp;gt; reducto:// -&amp;gt; instructions.schema
mistral       POST /v1/ocr          upload -&amp;gt; signed URL -&amp;gt; document_annotation_format
llamaextract  extract_stateless()   upload -&amp;gt; file id   -&amp;gt; ExtractConfig, maybe poll
claude        POST /v1/messages     base64 inline       -&amp;gt; forced tool, input_schema IS the schema
gpt           POST /v1/responses    base64 inline       -&amp;gt; text.format json_schema
textract      AnalyzeDocument       per-page blocks     -&amp;gt; LLM normalizer stage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some upload first. Some want a signed URL. Some poll. Some return the JSON inside another object.&lt;/p&gt;

&lt;p&gt;Mistral even returns &lt;code&gt;document_annotation&lt;/code&gt; as a JSON string. Thanks Mistral. 😭&lt;/p&gt;

&lt;p&gt;I hide all of that behind one function.&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;class&lt;/span&gt; &lt;span class="nc"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&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="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&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="n"&gt;schema_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExtractResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what keeps the runner fair. The runner does not know which provider it is calling.&lt;/p&gt;

&lt;p&gt;It just sends a PDF and expects the same result shape back.&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%2Ff1wk25wl4sza5jhuw1pf.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%2Ff1wk25wl4sza5jhuw1pf.png" alt="architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Claude adapter is tiny.&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="c1"&gt;# bench/providers/direct_llm.py (trimmed)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&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="n"&gt;schema_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ExtractResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-opus-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&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;name&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;emit&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;description&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;Return the extracted fields.&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;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;tool_choice&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;type&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;tool&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;name&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;emit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;messages&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;role&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;user&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;content&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;document&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;source&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;type&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;base64&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;media_type&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;application/pdf&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b64&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;type&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;text&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SHARED_INSTRUCTIONS&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="n"&gt;block&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&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="nc"&gt;ExtractResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&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;No upload. No job ID. No polling.&lt;/p&gt;

&lt;p&gt;The schema itself becomes the tool input.&lt;/p&gt;

&lt;p&gt;Textract is the opposite. It needs about 60 lines just to rebuild rows from &lt;code&gt;CELL&lt;/code&gt; blocks, then another model call to turn that into the target JSON.&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="c1"&gt;# bench/providers/textract.py (trimmed)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_table_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks_by_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;cells&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;blocks_by_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Relationships&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHILD&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ids&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="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RowIndex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ColumnIndex&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="nf"&gt;_cell_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks_by_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="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="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&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="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;normalizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Same task on paper. Very different amount of work in code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Scoring without an LLM judge
&lt;/h3&gt;

&lt;p&gt;I did not use another LLM to decide who won. That just creates a second argument.&lt;/p&gt;

&lt;p&gt;The scorer is fixed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalise first&lt;/strong&gt; Money becomes &lt;code&gt;Decimal&lt;/code&gt;, dates become ISO, and strings get cleaned so tiny formatting changes do not count as errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalars&lt;/strong&gt; Exact after normalisation. Longer text uses &lt;code&gt;rapidfuzz token_set_ratio&lt;/code&gt;. A score of 90 or more counts as correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verdicts&lt;/strong&gt; A field can be &lt;code&gt;correct&lt;/code&gt;, &lt;code&gt;near&lt;/code&gt;, &lt;code&gt;wrong&lt;/code&gt;, &lt;code&gt;missing&lt;/code&gt;, or &lt;code&gt;hallucinated&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arrays&lt;/strong&gt; Rows are matched using text fields only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missed rows count&lt;/strong&gt; If a provider returns 1 perfect row out of 7, it does not magically get 100 percent. 🥴&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before scoring the providers, the scorer checks itself. First, all 11 ground truth files are scored against themselves.&lt;/p&gt;

&lt;p&gt;Every one must return exactly &lt;code&gt;1.000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I deliberately break the data. Wrong total. Fake date. Missing vendor. Dropped rows.&lt;/p&gt;

&lt;p&gt;Every one of those changes has to lower the score. Dropping 4 of 7 rows gives &lt;code&gt;0.579&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not some fake &lt;code&gt;1.0&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ uv run bench score
self-test  identity     11/11  =1.000  OK
self-test  corruption   6/6    reduce  OK
scoring 77 runs ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;code&gt;run&lt;/code&gt; and &lt;code&gt;score&lt;/code&gt; are separate commands. &lt;code&gt;run&lt;/code&gt; saves the raw API response to disk. &lt;code&gt;score&lt;/code&gt; only reads those saved files.&lt;/p&gt;

&lt;p&gt;That meant I could rewrite the scorer four times without paying for the APIs again. Pretty useful when you find a bug at 1 AM. 👍&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Accuracy
&lt;/h2&gt;

&lt;p&gt;There were 77 main runs. Zero API errors. Zero invalid JSON responses.&lt;/p&gt;

&lt;p&gt;Every provider returned something usable on every document.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Field accuracy&lt;/th&gt;
&lt;th&gt;Row F1&lt;/th&gt;
&lt;th&gt;Hallucinated&lt;/th&gt;
&lt;th&gt;Missing&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.991&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;6.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPT direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.982&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reducto&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.982&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;10.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.962&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.00&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;22.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Textract&lt;/td&gt;
&lt;td&gt;0.936&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;14.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaExtract&lt;/td&gt;
&lt;td&gt;0.903&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;22.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral OCR&lt;/td&gt;
&lt;td&gt;0.884&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.2s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I kept field accuracy and row F1 separate. I did not turn everything into some made up score out of 10.&lt;/p&gt;

&lt;p&gt;Because then half the benchmark becomes me deciding how much each number should matter.&lt;/p&gt;

&lt;p&gt;Claude came first on raw field accuracy. GPT and Reducto tied next.&lt;/p&gt;

&lt;p&gt;Extend landed at 0.962, but it had the only perfect row F1 in the whole test. It did not miss a single table row across 25 bank transactions, 7 invoice lines, 41 form pairs, and all the EOB rows.&lt;/p&gt;

&lt;p&gt;That is a pretty important detail. 0.991 and 0.962 look different on a leaderboard.&lt;/p&gt;

&lt;p&gt;Across these 11 documents, the gap was under 3 points.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per document
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9fqdt152eifno15bdlnk.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%2F9fqdt152eifno15bdlnk.png" alt="per document heatmap" width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Document&lt;/th&gt;
&lt;th&gt;Claude&lt;/th&gt;
&lt;th&gt;GPT&lt;/th&gt;
&lt;th&gt;Reducto&lt;/th&gt;
&lt;th&gt;Extend&lt;/th&gt;
&lt;th&gt;Textract&lt;/th&gt;
&lt;th&gt;LlamaExtract&lt;/th&gt;
&lt;th&gt;Mistral&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01_invoice_clean&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02_invoice_complex&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03_receipt_photo&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04_contract_simple&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05_contract_complex&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;0.79&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.64&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06_bank_statement&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;07_noisy_form&lt;/td&gt;
&lt;td&gt;0.98&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.74&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;08_handwritten_note&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.67&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09_EOB&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.75&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10_mixed_packet&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;0.92&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.67&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11_contract_scanned&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.93&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things stand out.&lt;/p&gt;

&lt;p&gt;The clean invoice is a 7 way tie at 1.00. If your documents all look like that, honestly, pick the cheapest one and move on. 🤷‍♂️&lt;/p&gt;

&lt;p&gt;The complex contract is where Mistral drops hard to 0.64. LlamaExtract had the roughest time on the noisy form, handwriting, and EOB.&lt;/p&gt;

&lt;p&gt;Now the fun part.&lt;/p&gt;

&lt;p&gt;The failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Where things broke
&lt;/h2&gt;

&lt;p&gt;This is why I saved every raw response. A final accuracy number hides way too much.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hallucination traps
&lt;/h3&gt;

&lt;p&gt;The first trap was the blank agreement date in Doc 04. The correct answer is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mistral returned &lt;code&gt;2012-02-17&lt;/code&gt;. Textract returned &lt;code&gt;2012-02-17&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Claude, GPT, Reducto, Extend, and LlamaExtract returned &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second trap was the &lt;code&gt;$6,200,000&lt;/code&gt; financing cap in Doc 05. Again, the correct contract value is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mistral, LlamaExtract, and Textract returned &lt;code&gt;6200000&lt;/code&gt;. Claude, GPT, Reducto, and Extend returned &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three products turned a financing limit into a contract value. That is not the kind of error I want quietly flowing into an AP system.&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%2Fvwos9mbmbf0zc90j0l2h.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%2Fvwos9mbmbf0zc90j0l2h.png" alt="financial document crop" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The scary errors are not always random garbage. Sometimes the wrong answer looks completely reasonable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Textract dropped digits from money
&lt;/h3&gt;

&lt;p&gt;On the bank statement, I found these two.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;1 200,45 $&lt;/code&gt; became &lt;strong&gt;&lt;code&gt;200.45&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;6 675,00 $&lt;/code&gt; became &lt;strong&gt;&lt;code&gt;675.00&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These are not formatting misses. The first digit is just gone.&lt;/p&gt;

&lt;p&gt;And this is money. 🤧&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%2Fvolh03p0yi6hwk0oo5bx.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%2Fvolh03p0yi6hwk0oo5bx.png" alt="textract dropped digit" width="800" height="733"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F30h33x0nnt4dpxrtdkmq.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%2F30h33x0nnt4dpxrtdkmq.png" alt="textract dropped digit second" width="515" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I checked the summary on the same page too. The credits, debits, and closing balance all match the ground truth.&lt;/p&gt;

&lt;p&gt;Then I checked Textract raw blocks. The digit was already missing before Haiku saw anything.&lt;/p&gt;

&lt;p&gt;So the normalizer did not cause this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  LlamaExtract shifted a whole column
&lt;/h3&gt;

&lt;p&gt;The EOB result was sneaky. LlamaExtract found the right rows, but some values moved one column to the left.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;LlamaExtract&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services/0/plan_paid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.15&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services/0/patient_responsibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;td&gt;2.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services/1/billed_amount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;375.00&lt;/td&gt;
&lt;td&gt;118.12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;services/1/allowed_amount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;118.12&lt;/td&gt;
&lt;td&gt;35.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every number exists in the row. It is just under the wrong field.&lt;/p&gt;

&lt;p&gt;That is exactly the kind of JSON you can skim and think looks fine.&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%2Fi8zfsyurgw0uk8ce4eq6.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%2Fi8zfsyurgw0uk8ce4eq6.png" alt="EOB table" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistral found pages that do not exist
&lt;/h3&gt;

&lt;p&gt;The mixed packet has five pages. Mistral said the contract was on pages &lt;strong&gt;2 to 10&lt;/strong&gt; and the EOB was on &lt;strong&gt;11 to 13&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every other provider got &lt;code&gt;1-1&lt;/code&gt;, &lt;code&gt;2-4&lt;/code&gt;, and &lt;code&gt;5-5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I still do not know where page 13 came from. 🫩&lt;/p&gt;

&lt;h3&gt;
  
  
  Handwriting fooled GPT
&lt;/h3&gt;

&lt;p&gt;The receipt has &lt;code&gt;tan chay yee&lt;/code&gt; written by hand at the top. GPT used that as the &lt;code&gt;merchant_name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The printed merchant is &lt;code&gt;OJC MARKETING SDN BHD&lt;/code&gt;. Every other provider ignored the handwriting.&lt;/p&gt;

&lt;p&gt;So yeah, best overall does not mean best on every file.&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%2Fbv32miqq19balrhwrwb0.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%2Fbv32miqq19balrhwrwb0.png" alt="document receipt" width="618" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistral answered a slightly different question
&lt;/h3&gt;

&lt;p&gt;Doc 05 calls the two parties &lt;code&gt;Sekisui&lt;/code&gt; and &lt;code&gt;Qualigen&lt;/code&gt;. Mistral returned &lt;code&gt;Distributor&lt;/code&gt; and &lt;code&gt;Manufacturer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those words make sense in the contract, but they are not the defined names I asked for. Part of this is schema sensitivity.&lt;/p&gt;

&lt;p&gt;Still, it cost two fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apparently the pipe symbol was hard
&lt;/h3&gt;

&lt;p&gt;One invoice row says this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dell Core 2 Duo Desktop Computer | Windows XP Pro | 4GB | 500GB&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Textract read &lt;code&gt;|&lt;/code&gt; as &lt;code&gt;I&lt;/code&gt;. Mistral removed the pipes.&lt;/p&gt;

&lt;p&gt;Everyone else got it exactly right.&lt;/p&gt;

&lt;p&gt;Tiny character. Same invoice. Two different OCR mistakes.&lt;/p&gt;

&lt;p&gt;Lol.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extend never made up a value
&lt;/h3&gt;

&lt;p&gt;Extend had a very different failure pattern.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero hallucinations across all 11 documents&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passed both traps&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only perfect row F1 at 1.00&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;8 of 11 documents scored 1.000&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0.962 field accuracy compared with 0.991 for the leader&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the lost points came from the contracts. A couple of fields were present, but Extend returned &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is still wrong for the benchmark. But it is a much easier failure to deal with in a production flow.&lt;/p&gt;

&lt;p&gt;A missing value can go to review. A confident wrong number can move straight into the next system.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Extend was more likely to leave a field empty than guess. Depending on your workflow, that can be a useful trade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Pricing
&lt;/h2&gt;

&lt;p&gt;Here is what the 35 page test cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Cost per document&lt;/th&gt;
&lt;th&gt;Cost per 1000 pages&lt;/th&gt;
&lt;th&gt;Cost per correct field&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT direct&lt;/td&gt;
&lt;td&gt;$0.1736&lt;/td&gt;
&lt;td&gt;$0.0158&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.96&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00047&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral OCR&lt;/td&gt;
&lt;td&gt;$0.1750&lt;/td&gt;
&lt;td&gt;$0.0159&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$0.00049&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude direct&lt;/td&gt;
&lt;td&gt;$0.3734&lt;/td&gt;
&lt;td&gt;$0.0339&lt;/td&gt;
&lt;td&gt;$10.67&lt;/td&gt;
&lt;td&gt;$0.00100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaExtract*&lt;/td&gt;
&lt;td&gt;$0.4375&lt;/td&gt;
&lt;td&gt;$0.0398&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;td&gt;$0.00126&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reducto*&lt;/td&gt;
&lt;td&gt;$0.5250&lt;/td&gt;
&lt;td&gt;$0.0477&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;td&gt;$0.00142&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2.1875&lt;/td&gt;
&lt;td&gt;$0.1989&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$62.50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.00593&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Textract&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$2.3553&lt;/td&gt;
&lt;td&gt;$0.2141&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$67.30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.00663&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Pricing for Reducto and LlamaExtract could not be fully verified from usage data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GPT was easily the cheapest per correct field. Textract was the most expensive in this test.&lt;/p&gt;

&lt;p&gt;And because it also needed the extra Haiku step, its real setup is not just the Textract call.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you get with Extend pricing
&lt;/h3&gt;

&lt;p&gt;Extend reports both extraction credits and total credits.&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="nl"&gt;"usage"&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;"credits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"totalCredits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0&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;Across my runs, that came to &lt;strong&gt;5 credits per page&lt;/strong&gt;. On pay as you go pricing, that is about &lt;strong&gt;$62.50 per 1,000 pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But you are also getting more than a JSON response. Extend includes parsing, structured extraction, field citations, confidence scores, and a review flow in the dashboard.&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%2F13hz74g52s8ank141mr5.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%2F13hz74g52s8ank141mr5.png" alt="Extend credit usage" width="799" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scale pricing drops to roughly &lt;strong&gt;$50 per 1,000 pages&lt;/strong&gt;. Enterprise pricing is custom.&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%2Fchoyw25zq75wtms4j8rx.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%2Fchoyw25zq75wtms4j8rx.png" alt="extend run" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 So I would not compare this only as price per page. The extra cost is paying for the workflow around extraction too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Textract pricing gets messy fast
&lt;/h3&gt;

&lt;p&gt;Textract prices features separately. FORMS is $0.05 per page. TABLES is $0.015 per page.&lt;/p&gt;

&lt;p&gt;Using both comes to $0.065 per page. For 35 pages, that was $2.275.&lt;/p&gt;

&lt;p&gt;Then I had about $0.08 of Haiku cost for the JSON normalizer. That brings the matrix to around $2.36.&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%2Fdsevrmk0w2lddzii58sw.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%2Fdsevrmk0w2lddzii58sw.png" alt="AWS billing" width="798" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The billing screenshot can show more than that because it also includes my reruns and integration tests.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;DetectDocumentText&lt;/code&gt;. That costs $0.0015 per page and has a bigger free tier.&lt;/p&gt;

&lt;p&gt;I did not test that setup here. But plain OCR plus a cheap model would be a very interesting follow up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reducto and LlamaExtract pricing
&lt;/h3&gt;

&lt;p&gt;I could not fully verify Reducto extraction cost from the usage dashboard. It was still showing the usage page as under construction when I checked.&lt;/p&gt;

&lt;p&gt;So the &lt;code&gt;$15 per 1,000 pages&lt;/code&gt; number uses the published parse rate. Extraction can add more credits.&lt;/p&gt;

&lt;p&gt;LlamaExtract uses its published rate in the table with the same caveat.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Developer experience
&lt;/h2&gt;

&lt;p&gt;I also tracked how annoying each integration was to build. Very scientific metric, I know. 😆&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;about 10 min&lt;/td&gt;
&lt;td&gt;worked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GPT&lt;/td&gt;
&lt;td&gt;about 10 min&lt;/td&gt;
&lt;td&gt;worked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Extend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;about 25 min&lt;/td&gt;
&lt;td&gt;documented path worked first try&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Reducto&lt;/td&gt;
&lt;td&gt;about 20 min&lt;/td&gt;
&lt;td&gt;runtime typing issue and serializer crash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;about 15 min&lt;/td&gt;
&lt;td&gt;documented import failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;LlamaCloud&lt;/td&gt;
&lt;td&gt;about 30 min&lt;/td&gt;
&lt;td&gt;old package docs and mixed pydantic versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;AWS Textract&lt;/td&gt;
&lt;td&gt;about 45 min&lt;/td&gt;
&lt;td&gt;no schema output, table rebuild code, extra LLM step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude and GPT were simple because the PDF can go straight into the model call. Extend was the easiest document API.&lt;/p&gt;

&lt;p&gt;Its documented path worked on the first try. That should be normal.&lt;/p&gt;

&lt;p&gt;It was not. ✌️&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistral import issue
&lt;/h3&gt;

&lt;p&gt;The docs showed this.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mistralai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mistral&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On v2.9.4 I got this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: cannot import name 'Mistral' from 'mistralai' (unknown location)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The working import was this.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mistralai.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Mistral&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;15 minutes of my life. 😴&lt;/p&gt;

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

&lt;h3&gt;
  
  
  LlamaCloud setup
&lt;/h3&gt;

&lt;p&gt;LlamaCloud sent me through two old paths before I even started. LlamaParse structured output is deprecated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;llama-cloud-services&lt;/code&gt; is also deprecated.&lt;/p&gt;

&lt;p&gt;Then the client lives at &lt;code&gt;llama_cloud.client.LlamaCloud&lt;/code&gt;, and &lt;code&gt;ExtractConfig&lt;/code&gt; still uses pydantic v1 style while the rest of the package uses v2.&lt;/p&gt;

&lt;p&gt;It works. It just took more digging than I expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extend setup
&lt;/h3&gt;

&lt;p&gt;Extend was boring in the best way. The documented SDK flow worked.&lt;/p&gt;

&lt;p&gt;No weird import hunt. No second package. No guess and check loop.&lt;/p&gt;

&lt;p&gt;That is also where its agent files helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Agent experience
&lt;/h2&gt;

&lt;p&gt;This part is becoming more important now. Can Claude Code, Codex, or another coding agent actually understand the SDK without me spoon feeding it everything&lt;/p&gt;

&lt;p&gt;I checked what each vendor ships for agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  What vendors publish
&lt;/h3&gt;

&lt;p&gt;Checked on September 8 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;llms.txt&lt;/th&gt;
&lt;th&gt;agents.md&lt;/th&gt;
&lt;th&gt;MCP server&lt;/th&gt;
&lt;th&gt;Official agent plugin&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;yes&lt;/strong&gt; &lt;code&gt;extend-hq/extend-agent-plugin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reducto&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LlamaCloud&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Textract&lt;/td&gt;
&lt;td&gt;org wide only&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;llms.txt&lt;/code&gt; alone is not that special anymore. Six of the seven have one.&lt;/p&gt;

&lt;p&gt;Extend &lt;a href="https://docs.extend.ai/agents.md" rel="noopener noreferrer"&gt;agents.md&lt;/a&gt; had all four. Reducto had three.&lt;/p&gt;

&lt;p&gt;AWS has a general AWS &lt;code&gt;llms.txt&lt;/code&gt;, but nothing specific for Textract.&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%2Fs3gda4kh8j507xpyham4.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%2Fs3gda4kh8j507xpyham4.png" alt="extend agents.md file" width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually helped
&lt;/h3&gt;

&lt;p&gt;Extend &lt;code&gt;agents.md&lt;/code&gt; had one small line that saved real time. It says that REST paths and SDK methods can have different names.&lt;/p&gt;

&lt;p&gt;For example&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;maps to&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extract_runs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds tiny. But it stopped the exact guess and check loop I hit with another SDK.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Agent docs are useful when they answer the weird little questions a coding agent would otherwise guess.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Things I got wrong
&lt;/h2&gt;

&lt;p&gt;This benchmark also caught one annoying problem with the datasets themselves. Published ground truth can be wrong too.&lt;/p&gt;

&lt;p&gt;The SROIE answer key says the receipt postcode is &lt;code&gt;B1750&lt;/code&gt;. The receipt clearly says &lt;code&gt;81750&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;FUNSD has &lt;code&gt;Macket&lt;/code&gt; where the form says &lt;code&gt;Market&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So if I blindly used the dataset labels, I would punish a provider for reading the document correctly.&lt;/p&gt;

&lt;p&gt;Nice.&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%2Finb5recb4m9b7gzadlhy.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%2Finb5recb4m9b7gzadlhy.png" alt="incorrect label" width="618" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is another reason I kept the raw files and responses. Benchmarks can be wrong in boring ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should you pick
&lt;/h2&gt;

&lt;p&gt;There is no one winner for every workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick Claude directly when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You want the highest raw field accuracy from this test&lt;/li&gt;
&lt;li&gt;A human will review the result&lt;/li&gt;
&lt;li&gt;You do not need field citations or confidence scores&lt;/li&gt;
&lt;li&gt;You already use the Anthropic API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick GPT directly when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cost matters a lot&lt;/li&gt;
&lt;li&gt;You still want very strong raw accuracy&lt;/li&gt;
&lt;li&gt;A human can check the weird cases&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick Extend when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A made up value is worse than a missing one&lt;/li&gt;
&lt;li&gt;You need field citations&lt;/li&gt;
&lt;li&gt;You need confidence scores&lt;/li&gt;
&lt;li&gt;You want a review UI&lt;/li&gt;
&lt;li&gt;The extracted data goes into a real production workflow&lt;/li&gt;
&lt;li&gt;Your team uses coding agents and good agent docs matter&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick Reducto when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You want document specific tooling with strong raw accuracy&lt;/li&gt;
&lt;li&gt;You want zero hallucinations in this test&lt;/li&gt;
&lt;li&gt;You have checked the extraction pricing for your own workload&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pick Mistral when
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Speed is the main thing you care about&lt;/li&gt;
&lt;li&gt;A human checks the result before anything acts on it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Textract
&lt;/h3&gt;

&lt;p&gt;For this exact setup, Textract was hard to justify. It cost the most, needed an extra model step, hallucinated twice, and dropped leading digits from money.&lt;/p&gt;

&lt;p&gt;Your workload may be different. But based on these 11 documents, I would test the other options first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Everything is in the repo: &lt;a href="https://github.com/shricodev/document-parsing-api-benchmark" rel="noopener noreferrer"&gt;shricodev/document-parsing-api-benchmark&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run bench doctor
uv run bench run &lt;span class="nt"&gt;--all&lt;/span&gt;
uv run bench score
uv run bench report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bench doctor&lt;/code&gt; checks credentials, corpus hashes, and spend limits. &lt;code&gt;bench run&lt;/code&gt; will not call an API again if a successful result is already saved.&lt;/p&gt;

&lt;p&gt;So if the run stops halfway through, you can resume without paying for the same documents twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;uv run bench run &lt;span class="nt"&gt;--provider&lt;/span&gt; extend &lt;span class="nt"&gt;--doc&lt;/span&gt; 06_bank_statement
extend / 06_bank_statement  cached &lt;span class="o"&gt;(&lt;/span&gt;runs/extend/06_bank_statement/default.json&lt;span class="o"&gt;)&lt;/span&gt;  skipping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every raw response lives in &lt;code&gt;runs/&lt;/code&gt;. That means every result in this post can be scored again without an API key.&lt;/p&gt;

&lt;p&gt;The source PDFs are not committed because of licensing. &lt;code&gt;corpus/build/fetch.py&lt;/code&gt; downloads and freezes them instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The result was closer than I expected.&lt;/p&gt;

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

&lt;p&gt;Claude had the best raw field accuracy. GPT was extremely cheap for how well it scored. Reducto was right behind them.&lt;/p&gt;

&lt;p&gt;And Extend had the cleanest safety result in the group. Zero hallucinations. Zero missed table rows. Its SDK path worked on the first try too.&lt;/p&gt;

&lt;p&gt;That is why I would not pick one of these from the accuracy column alone.&lt;/p&gt;

&lt;p&gt;If a human is reading the output, calling Claude or GPT directly can make a lot of sense. If the JSON is going straight into AP, claims, finance, or another production system, I care much more about citations, confidence, review, and what happens when the model is not sure.&lt;/p&gt;

&lt;p&gt;That is where Extend made the strongest case in this test.&lt;/p&gt;

&lt;p&gt;And yeah, run your own benchmark. Freeze the files, save the responses, and add a few nasty cases.&lt;/p&gt;

&lt;p&gt;This one cost me $9.67. It changed what I would actually use in production. ✅&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>DeepSeek Harness (DSH) vs Pi Agent: Everything you need to know</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Wed, 09 Sep 2026 12:45:00 +0000</pubDate>
      <link>https://dev.to/composiodev/deepseek-harness-dsh-vs-pi-agent-everything-you-need-to-know-5bci</link>
      <guid>https://dev.to/composiodev/deepseek-harness-dsh-vs-pi-agent-everything-you-need-to-know-5bci</guid>
      <description>&lt;p&gt;DeepSeek released its own agent harness this August.&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%2F94hlogr3p76zinppkpo6.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%2F94hlogr3p76zinppkpo6.png" alt="deepseek harness launch tweet" width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It crossed &lt;strong&gt;66k stars in about a day (yes, sir!)&lt;/strong&gt; and went over 200k by early September. That is already pretty crazy for something that is still a developer preview.&lt;/p&gt;

&lt;p&gt;But while researching this post, I found something much more interesting. DeepSeek Harness actually uses Pi's model layer to connect to models outside DeepSeek. Yep.&lt;/p&gt;

&lt;p&gt;The new agent runtime from DeepSeek uses Pi under the hood for part of its model support. And &lt;a href="https://github.com/tianyicui" rel="noopener noreferrer"&gt;Tianyi Cui&lt;/a&gt;, who leads the Harness project, has publicly said Pi is a favourite daily driver for many people at DeepSeek. Lol.&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%2Flr8ymp1ljxib8xmw8r6x.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%2Flr8ymp1ljxib8xmw8r6x.png" alt="dsh praise" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That makes this comparison way more fun because they agree on quite a lot. Both think the harness matters, both are open source, both let you change a lot of the runtime, and both try to avoid locking you into one model.&lt;/p&gt;

&lt;p&gt;But they take the idea in very different directions. Pi gives you a tiny coding agent with four tools and lets you build from there. DeepSeek Harness gives you a full agent runtime where even the agent loop itself can be swapped.&lt;/p&gt;

&lt;p&gt;This is going to be an interesting one. Stick to it!&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;DeepSeek Harness&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real tool use in our eval&lt;/td&gt;
&lt;td&gt;21/30 passed&lt;/td&gt;
&lt;td&gt;20/30 passed&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per shared success&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median time per task&lt;/td&gt;
&lt;td&gt;362.9s&lt;/td&gt;
&lt;td&gt;252.1s&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg runtime tokens in our run&lt;/td&gt;
&lt;td&gt;924,990&lt;/td&gt;
&lt;td&gt;88,562, with a catch&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simplicity&lt;/td&gt;
&lt;td&gt;Four main tools and a tiny prompt&lt;/td&gt;
&lt;td&gt;Much larger runtime with 53 built in tools&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep runtime control&lt;/td&gt;
&lt;td&gt;Powerful TypeScript extensions around a small core&lt;/td&gt;
&lt;td&gt;Even the agent loop can be replaced&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model support&lt;/td&gt;
&lt;td&gt;25+ providers and very easy switching&lt;/td&gt;
&lt;td&gt;Broad support, partly through Pi's model layer&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local models&lt;/td&gt;
&lt;td&gt;Ollama, vLLM, and custom providers are well supported&lt;/td&gt;
&lt;td&gt;Possible, but needs more setup&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sandbox&lt;/td&gt;
&lt;td&gt;None in the core&lt;/td&gt;
&lt;td&gt;Three built in sandbox modes&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session inspection&lt;/td&gt;
&lt;td&gt;Very readable session tree&lt;/td&gt;
&lt;td&gt;Full append only trajectory and replay&lt;/td&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily use&lt;/td&gt;
&lt;td&gt;Very small and easy to run&lt;/td&gt;
&lt;td&gt;Web UI and more setup&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity&lt;/td&gt;
&lt;td&gt;Mature enough for normal daily work&lt;/td&gt;
&lt;td&gt;Still a developer preview&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In summary
&lt;/h3&gt;

&lt;p&gt;Scorecard: Pi 6, DeepSeek Harness 6. Dead tie.&lt;/p&gt;

&lt;p&gt;And the benchmark is basically a tie too. Pi passed 21 of 30 tasks and DeepSeek Harness passed 20. DeepSeek Harness was cheaper on the shared successful tasks and also finished tasks faster.&lt;/p&gt;

&lt;p&gt;But there is an important catch. DeepSeek Harness ran through DeepSeek's own endpoint. Pi ran through OpenRouter. So I would trust the pass rate and cost comparison more than I would trust a clean 10x token claim.&lt;/p&gt;

&lt;p&gt;Still, the overall result is pretty clear. Pi is the better choice if you want a coding agent you can install, understand, modify, and use every day.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is DeepSeek Harness?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ DeepSeek's open source agent runtime where almost every part of the agent can be replaced as a plugin.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;&lt;a href="https://github.com/deepseek-ai/deepseek-harness" rel="noopener noreferrer"&gt;DeepSeek Harness&lt;/a&gt;, or &lt;code&gt;dsh&lt;/code&gt;, is DeepSeek's official agent harness. DeepSeek released it on August 13, 2026 alongside DeepSeek V4 Pro.&lt;/p&gt;

&lt;p&gt;And the basic idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent = Model + Harness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model does the thinking. The harness handles everything around it. It gives the model tools, builds the prompt, runs shell commands, manages files, stores sessions, handles context, runs the loop, and decides how all these pieces connect.&lt;/p&gt;

&lt;p&gt;That part is normal. Claude Code, Codex, Pi, OpenCode, and basically every coding agent already has some version of this.&lt;/p&gt;

&lt;p&gt;DeepSeek did something different with how it built the runtime. It uses a plugin system called Cordis, and almost everything runs as a plugin. The model adapter is a plugin, the tools are plugins, the sandbox is a plugin, the session system uses plugins, and even the main agent loop can be replaced.&lt;/p&gt;

&lt;p&gt;Cordis also gives plugins a proper lifecycle. You can mount them from config, reload them, and clean them up when they are removed. So this is much closer to building an agent runtime from replaceable parts than adding a few extensions to a coding CLI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 DeepSeek's own docs basically say there is no special core that you need to patch.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It also ships several different modes. Standard gives you the full agent setup. Code mode, also called PTC, gives the model a generated TypeScript SDK for its tools. So instead of making lots of separate tool calls, the model can write one small program that calls those tools and handles the intermediate data itself.&lt;/p&gt;

&lt;p&gt;Minimal gives it basically shell plus a file editor. Creator lets the &lt;strong&gt;agent inspect and modify parts of its own runtime. 🤯&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Yes, that last one is as weird as it sounds. It is very different from most coding agents.&lt;/p&gt;

&lt;p&gt;It also comes with 53 built in tools, MCP support, subagents, LSP, web search, scheduling, sandboxing, and a local browser UI. So despite the plugin first design, this is not a tiny framework with nothing included.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Pi?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A minimal terminal coding agent that gives the model a few tools and expects you to build whatever else you want.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;You probably know Pi by now if you have read the other posts in this series. Mario Zechner built it after getting tired of how much stuff Claude Code kept adding.&lt;/p&gt;

&lt;p&gt;Pi goes almost completely in the other direction. The model gets &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;. The system prompt stays under roughly 1,000 tokens. And that is most of the product.&lt;/p&gt;

&lt;p&gt;Need grep? Use bash. Need Git? Use bash. Need another tool? Write a TypeScript extension. Need a whole new workflow? Ask Pi to build the extension for you.&lt;/p&gt;

&lt;p&gt;The project now lives under &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;Earendil&lt;/a&gt;, has around 100k GitHub stars, and is still open source and MIT licensed.&lt;/p&gt;

&lt;p&gt;The interesting part is that Pi is not small because nobody built more features yet. It is small because that is the actual design. Every extra tool schema and every extra instruction costs tokens on future model calls, so Pi tries to keep the default harness small.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness "kinda" agrees with some of this, but there's a whole lot to it.&lt;/p&gt;

&lt;p&gt;Related: &lt;a href="https://composio.dev/content/pi-agent-vs-claude-code" rel="noopener noreferrer"&gt;Pi vs Claude Code&lt;/a&gt;, &lt;a href="https://composio.dev/content/pi-vs-opencode" rel="noopener noreferrer"&gt;Pi vs OpenCode&lt;/a&gt;, &lt;a href="https://composio.dev/content/pi-vs-omp" rel="noopener noreferrer"&gt;Pi vs OMP&lt;/a&gt;, &lt;a href="https://composio.dev/content/pi-agent-vs-codex" rel="noopener noreferrer"&gt;Pi vs Codex&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Philosophy: Pi vs DeepSeek Harness
&lt;/h2&gt;

&lt;p&gt;This is probably the most interesting part of the whole comparison because at first they look very similar. Both are open source, both are written mostly in TypeScript, both have the concept of extensions, and both want the model layer to stay flexible.&lt;/p&gt;

&lt;p&gt;And DeepSeek Harness literally uses Pi's model layer for its broad provider support.&lt;/p&gt;

&lt;p&gt;But they disagree on what the starting point should look like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi starts tiny
&lt;/h3&gt;

&lt;p&gt;Pi has a small core agent loop. That loop stays pretty simple and extensions can change almost everything around it.&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%2Ff2f2uskxptl9oy67s6l7.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%2Ff2f2uskxptl9oy67s6l7.png" alt="pi core" width="800" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea is that the model already knows how to code. So give it file access, shell access, editing, and enough freedom to work. Then stop adding stuff unless the user actually needs it.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepSeek Harness starts from replaceable parts
&lt;/h3&gt;

&lt;p&gt;It splits the runtime into parts and lets you swap those parts through Cordis. Pi gives you a small core and says, build around this. DeepSeek Harness gives you a way to replace its parts too.&lt;/p&gt;

&lt;p&gt;Even the actual loop works differently from an extension point of view. DeepSeek Harness fires events through each turn, such as &lt;code&gt;turn/start&lt;/code&gt;, &lt;code&gt;system-prompt/assemble&lt;/code&gt;, &lt;code&gt;tools/pre-execute&lt;/code&gt;, and &lt;code&gt;turn/end&lt;/code&gt;. A plugin can hook into these points, change messages, add behaviour, or stop a tool before it runs.&lt;/p&gt;

&lt;p&gt;That is a lot deeper than adding another command or tool.&lt;/p&gt;

&lt;p&gt;That sounds like a small difference. It is actually the whole comparison.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Pi is easier to understand. DeepSeek Harness gives you deeper control (&lt;strong&gt;Deep&lt;/strong&gt;Seek gives you &lt;strong&gt;deep&lt;/strong&gt;er control, what a nice rhyme to the name itself 🫡).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is another interesting part here. DeepSeek used its own Minimal mode for its public V4 coding benchmarks. Minimal mode is basically shell, a file editor, and a tiny system prompt.&lt;/p&gt;

&lt;p&gt;So even DeepSeek's own benchmark setup gets pretty close to the same minimal idea Pi has been pushing from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Architecture: four tools vs basically the whole runtime
&lt;/h2&gt;

&lt;p&gt;Underneath, both still do the normal agent thing. You send a task, the model responds, it calls tools, the harness runs them, the results go back to the model, and this repeats until the model finishes.&lt;/p&gt;

&lt;p&gt;The difference is everything around that loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi Agent
&lt;/h3&gt;

&lt;p&gt;Pi runs as one small process. It has the model layer, the agent loop, the terminal UI, session handling, and the extension system.&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%2F69ohu7sng0ofdzyix116.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%2F69ohu7sng0ofdzyix116.png" alt="Pi agent architecture" width="800" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its session format is also pretty neat. Messages form a tree (&lt;code&gt;/tree&lt;/code&gt; slash command), so you can jump back to an old point and continue in another direction without throwing away the old path. And because the core is small, you can actually open the source and follow what is happening.&lt;/p&gt;

&lt;p&gt;It is one of Pi's best features, at least for me.&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%2Ftztrgewzifvzrjj3cpl5.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%2Ftztrgewzifvzrjj3cpl5.png" alt="pi /tree praise" width="800" height="142"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi extensions are also just TypeScript files. You can put them inside &lt;code&gt;~/.pi/agent/extensions/&lt;/code&gt; and use them to add tools, block calls, change context, replace compaction behaviour, or add your own TUI stuff.&lt;/p&gt;

&lt;p&gt;That is a very simple way to extend a coding agent because you do not need to understand a big plugin runtime first.&lt;/p&gt;

&lt;h3&gt;
  
  
  DeepSeek Harness
&lt;/h3&gt;

&lt;p&gt;DeepSeek Harness is much bigger, and it does much more. It has a full plugin graph, sandboxing, subagents, MCP, scheduling, compaction, a browser UI, an append only session log, replay, and much more.&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%2Fm9ypg2bufdwscf8ujg08.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%2Fm9ypg2bufdwscf8ujg08.png" alt="dsh architecture" width="800" height="918"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The session system is probably my favourite part. Every model visible event gets logged. So if the model saw something, there should be a record of it. You can inspect the trajectory later and see what happened, or replay and fork sessions from the stored events.&lt;/p&gt;

&lt;p&gt;There is a technical reason for doing this too. DeepSeek Harness does not go back and rewrite old session history. It keeps appending new events instead.&lt;/p&gt;

&lt;p&gt;That makes it easier to keep the beginning of the model context stable, which is useful for DeepSeek's prefix caching.&lt;/p&gt;

&lt;p&gt;That is very useful if you are building agent infrastructure.&lt;/p&gt;

&lt;p&gt;I would take Pi for understanding my own coding session. I would take DeepSeek Harness if I were building a system that runs thousands of agent sessions and I needed to inspect exactly what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The benchmark: same model, different harness
&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%2Fjiaz0mg0ntpt7ur2ojc5.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%2Fjiaz0mg0ntpt7ur2ojc5.png" alt="bench summary" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the part I was most curious about. We ran 30 hard agent tool use tasks across the harnesses.&lt;/p&gt;

&lt;p&gt;The agent has to work with real apps and real data through tools, and then our verifier checks whether it actually completed the job. Each task had a 900 second cap.&lt;/p&gt;

&lt;p&gt;For this run, we used DeepSeek V4 Pro.&lt;/p&gt;

&lt;p&gt;Here is what happened:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness&lt;/th&gt;
&lt;th&gt;Tasks passed&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Avg runtime tokens&lt;/th&gt;
&lt;th&gt;Cost per shared success&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;21/30&lt;/td&gt;
&lt;td&gt;362.9s&lt;/td&gt;
&lt;td&gt;924,990&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;td&gt;20/30&lt;/td&gt;
&lt;td&gt;252.1s&lt;/td&gt;
&lt;td&gt;88,562&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pi won by one task. That is basically a tie on quality. Pi passed &lt;strong&gt;21/30 tasks&lt;/strong&gt;, while DeepSeek Harness passed &lt;strong&gt;20/30&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepei1dnqdka0hcl5ymdr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepei1dnqdka0hcl5ymdr.jpg" alt="pass rate of harnesses" width="679" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that matters because DeepSeek built Harness around its own model. You might expect the first party harness to have some huge advantage. It did not. Pi actually finished one more task.&lt;/p&gt;

&lt;p&gt;But pass rate is only one part of the story.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness also came out slightly cheaper. It cost around &lt;strong&gt;$0.028 per shared success&lt;/strong&gt;, compared to &lt;strong&gt;$0.031 for Pi&lt;/strong&gt;. OpenCode was very close at $0.032, while Claude Code came out much higher at $0.074.&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%2Fknkuee7x7c3ylkhqmsq4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fknkuee7x7c3ylkhqmsq4.jpg" alt="cost per success comparison of all harnesses" width="679" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference gets more interesting when you look at speed.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness had a median time of &lt;strong&gt;252.1 seconds per task&lt;/strong&gt;, while Pi took &lt;strong&gt;362.9 seconds&lt;/strong&gt;. Claude Code was the fastest at 181.8 seconds, but it also cost much more per successful task.&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%2Fm2iytmz1dkiszqypp1hh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm2iytmz1dkiszqypp1hh.jpg" alt="median time of harnesses" width="680" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So DSH did not beat Pi on task success, but it was faster and slightly cheaper in this run.&lt;/p&gt;

&lt;p&gt;Then look at the runtime token column. That number is wild.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness recorded about &lt;strong&gt;88k runtime tokens per task&lt;/strong&gt;. Pi recorded about &lt;strong&gt;925k&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Again, there is a provider routing catch here, and I will get into it in a second. But DSH clearly ran DeepSeek V4 Pro very efficiently on its native route.&lt;/p&gt;

&lt;h3&gt;
  
  
  The task split is also interesting
&lt;/h3&gt;

&lt;p&gt;Most of the tasks ended the same way for Pi and DSH. Pi and DeepSeek Harness agreed on 27 of the 30 outcomes. The entire 21 vs 20 difference came down to three tasks.&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%2Folx429e1ly2j464nzrwg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folx429e1ly2j464nzrwg.jpg" alt="task split" width="680" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi passed the CRM migration archive task while DeepSeek Harness failed it. Pi also passed the handover audit while DeepSeek Harness failed that one. DeepSeek Harness got the Sheets to Airtable tracker right while Pi failed it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;DeepSeek Harness&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRM migration archive&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handover audit&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sheets to Airtable tracker&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bigger chart is useful because it shows that this did not only happen between Pi and DSH. The same DeepSeek model changed behaviour when we moved it between Claude Code, OpenCode, Hermes, Pi, and DeepSeek Harness.&lt;/p&gt;

&lt;p&gt;For example, Claude Code cleared every check on the renewal screen task while Pi and DSH only cleared 5 of 11. On Slack action items, Pi, DSH, Claude Code, and Hermes all passed, while OpenCode only cleared 2 of 5 checks.&lt;/p&gt;

&lt;p&gt;So we did not see one harness destroying the others across every kind of work. The same model behaved differently depending on the harness around it.&lt;/p&gt;

&lt;p&gt;And I think that is the important result. The harness changed efficiency a lot. It changed speed. It changed cost. And on some tasks, it changed whether the model actually finished the job.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Same model. Almost the same pass rate. Very different runtime behaviour.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  So did DeepSeek Harness really use 10x fewer tokens?
&lt;/h3&gt;

&lt;p&gt;The raw table makes it look like Pi is eating up tokens, but that is not really the full story.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness used DeepSeek's official endpoint, while Pi used OpenRouter. That changes how caching and token usage get reported.&lt;/p&gt;

&lt;p&gt;Pi also has a much smaller base prompt. One separate comparison measured around &lt;strong&gt;4.5k uncached input for Pi&lt;/strong&gt; against &lt;strong&gt;47.6k for DSH&lt;/strong&gt; on the first turn. Pi keeps the prefix tiny, while DSH keeps a much larger prefix stable and gets strong cache reuse from it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 They are solving the same token problem in different ways.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And once you look at actual cost per success, the gap is tiny: &lt;strong&gt;$0.028 for DSH vs $0.031 for Pi&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So DSH was very efficient in our run, but I would not treat the 88k vs 925k token number as a clean 10x win.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. DeepSeek Harness uses Pi
&lt;/h2&gt;

&lt;p&gt;This still makes me laugh. 🥴&lt;/p&gt;

&lt;p&gt;For DeepSeek models, DSH has its own model adapter. Makes sense. That adapter understands things like DeepSeek reasoning levels and cache usage.&lt;/p&gt;

&lt;p&gt;But when it needs other model support, it can use an adapter built on Pi's &lt;code&gt;pi-ai&lt;/code&gt; layer.&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%2Ftaj3ow1vffc4ieewemhl.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%2Ftaj3ow1vffc4ieewemhl.png" alt="dsh uses pi under the hood" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So DSH can connect to Anthropic, OpenAI, Gemini, Bedrock, Vertex, Azure, Codex, and other providers through the model work Pi already did.&lt;/p&gt;

&lt;p&gt;That tells you quite a lot about Pi. It looks like a tiny terminal coding agent from the outside, but its model layer has become super useful infrastructure on its own.&lt;/p&gt;

&lt;p&gt;DeepSeek could have written another provider abstraction. It reused Pi's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 I would probably do the same thing. Why not?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pi still wins on model flexibility
&lt;/h3&gt;

&lt;p&gt;Pi has had much more time to build this part out. It works with 25 plus providers, local models, OpenRouter, Ollama, vLLM, cloud providers, and custom compatible endpoints. You can also switch models during a session.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness can work with many of the same providers, but the whole product still feels very "DeepSeek first." And that is fine.&lt;/p&gt;

&lt;p&gt;If you mainly use V4 Pro or V4 Flash, DSH has a real advantage because its native DeepSeek adapter understands the provider directly.&lt;/p&gt;

&lt;p&gt;If you change models often, run local models, or use different cloud providers, Pi is easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Extensibility
&lt;/h2&gt;

&lt;p&gt;Pi is one of the most hackable coding agents I have used. Its extensions run in TypeScript inside the same process. They can change tools, add UI, inject context, block tool calls, store custom state, and so much more...&lt;/p&gt;

&lt;p&gt;And because the codebase is small, Pi can often write its own extension when you ask it to. That is still very cool.&lt;/p&gt;

&lt;p&gt;A basic Pi extension can just live in &lt;code&gt;~/.pi/agent/extensions/&lt;/code&gt;. You do not need to learn a separate framework before adding one tool or one hook.&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%2F6qhcjpt5xrz9ids0jvo8.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%2F6qhcjpt5xrz9ids0jvo8.png" alt="dsh plugins" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But DeepSeek Harness goes further. With DSH, the loop itself can be a plugin. So can the sandbox, model handling, context management, and a lot of the runtime around them.&lt;/p&gt;

&lt;p&gt;A Cordis plugin can join the runtime's dependency graph, expose a service, listen to agent events, and clean itself up when it gets removed. That is more work for a simple extension, but it gives you much deeper control.&lt;/p&gt;

&lt;p&gt;The agent can even inspect its own plugin tree and, in Creator mode, experiment with runtime changes. That is more control. It is also more stuff to understand and tbh, most of it is something I don't know at all.&lt;/p&gt;

&lt;p&gt;If you are researching agent loops, building your own agent product, testing new context systems, or replacing parts of the runtime itself, DSH gives you way more room.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Pi is a lot easier to extend. DSH is the deeper thing to change.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. Safety and Security
&lt;/h2&gt;

&lt;p&gt;Pi is chill about permissions. The agent gets your user permissions and can run commands. There is no big built in approval system protecting you from every shell command.&lt;/p&gt;

&lt;p&gt;You can add safety through extensions or run Pi inside a container or VM, but the core itself does not try to do much here.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness comes with actual sandbox modes. It can run read only, it can allow writes inside the workspace, or you can give it full access.&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%2F04z7366ipo5zbsesh5sb.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%2F04z7366ipo5zbsesh5sb.png" alt="dsh vs pi safety and security" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And this is not just a UI permission setting. On Linux, DSH can use &lt;code&gt;bwrap&lt;/code&gt; and Landlock. On macOS, it uses Seatbelt. Windows uses a restricted token model.&lt;/p&gt;

&lt;p&gt;The sandbox also tries to fail closed. If DSH expects a sandbox and cannot create one, it should not silently continue with full access.&lt;/p&gt;

&lt;p&gt;That is a real DSH win.&lt;/p&gt;

&lt;p&gt;There are two catches though.&lt;/p&gt;

&lt;p&gt;The sandbox controls tool execution, but plugins themselves run inside the harness process. So if you install a random third party plugin, you are still trusting that code with serious access to your machine.&lt;/p&gt;

&lt;p&gt;And DeepSeek itself says the sandbox has not been security audited yet.&lt;/p&gt;

&lt;p&gt;So I would still be careful with unattended runs, but DSH gives you much stronger safety defaults than Pi.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The biggest DSH problem right now is maturity
&lt;/h2&gt;

&lt;p&gt;This is where the comparison stops being close for me.&lt;/p&gt;

&lt;p&gt;Pi has been around for more than a year. People use it every day. OpenClaw runs on it. And the core is stable enough that people build real workflows around it.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness launched in August. It is still a developer preview and warns that &lt;strong&gt;compatibility breaking changes will happen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And those changes are actually happening. The project pushed several alpha versions within days, and community reports have already found plugin breaks and many more issues.&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%2Fakos03znakshv0b7k4jc.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%2Fakos03znakshv0b7k4jc.png" alt="dsh issues in the production build" width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also one governance thing worth knowing. GitHub Issues are disabled right now, and DeepSeek is not accepting outside pull requests yet. Feedback mainly goes through Discussions and Discord.&lt;/p&gt;

&lt;p&gt;That does not make the project bad, especially this early. But if I am deciding which open source agent I want to build on today, that matters.&lt;/p&gt;

&lt;p&gt;This is normal for a project this new, but it changes who I would recommend it to.&lt;/p&gt;

&lt;p&gt;I would happily use DSH to experiment with agent architecture today. I would not build an important production workflow around a runtime that is still telling me its interfaces can break.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Daily use
&lt;/h2&gt;

&lt;p&gt;This one is pretty simple.&lt;/p&gt;

&lt;p&gt;Pi is a pretty regular agent that opens in the terminal. You pick a model and start working. The whole thing feels small because &lt;strong&gt;it is small&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DeepSeek Harness starts a local web app by default. You run it and open the UI in your browser. The official command is basically one &lt;code&gt;npx&lt;/code&gt; call, so installation itself is not hard.&lt;/p&gt;

&lt;p&gt;But once you start changing profiles, plugins, providers, Cordis config, sandbox settings, and runtime parts, there is much more to learn.&lt;/p&gt;

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

&lt;p&gt;Pi can get you to a useful coding task in a few minutes. DSH needs more setup and its docs make much more sense once you already understand agent infrastructure.&lt;/p&gt;

&lt;p&gt;That is also why I do not really see DSH replacing Pi for normal users right now. It solves a bigger problem, and bigger does not automatically mean better. 🤷‍♂️&lt;/p&gt;




&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;Pick Pi if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You mainly want a coding agent&lt;/li&gt;
&lt;li&gt;You want something small enough to understand&lt;/li&gt;
&lt;li&gt;You use many different model providers&lt;/li&gt;
&lt;li&gt;You run local models&lt;/li&gt;
&lt;li&gt;You care about keeping the fixed prompt tiny&lt;/li&gt;
&lt;li&gt;You want a clean TypeScript extension system&lt;/li&gt;
&lt;li&gt;You want something mature enough to use every day&lt;/li&gt;
&lt;li&gt;You would rather add the few features you need than start with 50 tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick DeepSeek Harness if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are building agent infrastructure&lt;/li&gt;
&lt;li&gt;You want to replace the agent loop itself&lt;/li&gt;
&lt;li&gt;You need strong session traces and replay&lt;/li&gt;
&lt;li&gt;You want a built in sandbox&lt;/li&gt;
&lt;li&gt;You want MCP, subagents, LSP, and more tooling out of the box&lt;/li&gt;
&lt;li&gt;You mainly use DeepSeek models and want the native integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And you can obviously run both. They are free and open source. They even share some code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The scorecard says 6 to 6. Our benchmark says Pi 21, DeepSeek Harness 20. That is as close as it gets.&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%2F2odm54lgfcbl5zb8ouvg.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%2F2odm54lgfcbl5zb8ouvg.png" alt="dsh vs pi summary" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But I do not think these two are actually fighting for the same user yet.&lt;/p&gt;

&lt;p&gt;Pi feels like a coding agent made by someone who wanted the tool to stay out of his way. DeepSeek Harness feels like something built by people who want to inspect and change every part of how an agent works. Both are right. It depends on your preference. 💁&lt;/p&gt;

&lt;p&gt;For normal coding, I would pick Pi today. It is smaller, easier to understand, and works with basically anything. It actually passed one more task in our run.&lt;/p&gt;

&lt;p&gt;For agent infrastructure work, DSH is way more interesting. Being able to swap the loop, sandbox, model layer, and other runtime parts without rebuilding the whole agent is genuinely useful.&lt;/p&gt;

&lt;p&gt;And DSH is still very new. It shipped all of this in a developer preview. If DeepSeek can make the runtime stable without losing the parts that make it interesting, I think it is going to be worth watching.&lt;/p&gt;

&lt;p&gt;For now, I just would not treat it like a daily driver yet. DeepSeek itself tells you it is a developer preview.&lt;/p&gt;

&lt;p&gt;So yeah. If I just want to code, I am still picking Pi. ✌️&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>Pi Agent vs OpenCode after 100+ Hours of Real Use ✌️</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:31:28 +0000</pubDate>
      <link>https://dev.to/composiodev/pi-agent-vs-opencode-after-100-hours-of-real-use-1mh7</link>
      <guid>https://dev.to/composiodev/pi-agent-vs-opencode-after-100-hours-of-real-use-1mh7</guid>
      <description>&lt;p&gt;Open-source coding agents had a weird start to 2026.&lt;/p&gt;

&lt;p&gt;In January, Anthropic suddenly blocked third-party tools from using Claude subscriptions. Overnight, with no warning.&lt;/p&gt;

&lt;p&gt;OpenCode got hit the hardest. The team had to remove Claude login support after legal pressure. The commit message was literally just: “anthropic legal requests.”&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%2F8jimv4abmhdeojze1lw5.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%2F8jimv4abmhdeojze1lw5.png" alt="OpenCode PR removing anthropic oauth" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PI never really cared. It was never built around one provider anyway.&lt;/p&gt;

&lt;p&gt;And that kinda brings us to the obvious question. If you’re using your own API keys anyway, which open-source agent do you actually pick?&lt;/p&gt;

&lt;p&gt;OpenCode is sitting at &lt;strong&gt;199K stars&lt;/strong&gt;, probably past 200K by the time you read this, with around 8 million monthly devs.&lt;/p&gt;

&lt;p&gt;Then there’s Pi. Bare minimum by design. Four tools. A system prompt under 1,000 tokens. And somehow, top of our benchmark. Again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ We recently covered &lt;a href="https://composio.dev/content/pi-agent-vs-claude-code" rel="noopener noreferrer"&gt;Pi Agent vs. Claude Code&lt;/a&gt;. Check that out if you're not sure whether Anthropic or Pi suits you better&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The easiest way I’d frame it is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 OpenCode is VS Code (bit bloaty). Pi is NeoVim (bare minimum).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So which one do you go for? Let's get into the difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real tool use (our eval)&lt;/td&gt;
&lt;td&gt;21/30 tasks passed, $0.078 per success&lt;/td&gt;
&lt;td&gt;19/30 tasks passed, $0.119 per success&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total eval spend&lt;/td&gt;
&lt;td&gt;$1.64 for all 30 tasks&lt;/td&gt;
&lt;td&gt;$2.25 for all 30 tasks&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;362.9s median per task, slowest of the bunch&lt;/td&gt;
&lt;td&gt;280.6s median per task&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context overhead&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens, prompt + tool defs&lt;/td&gt;
&lt;td&gt;~6,900 tokens per request&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-the-box features&lt;/td&gt;
&lt;td&gt;Four tools&lt;/td&gt;
&lt;td&gt;Plan mode, MCP, LSP diagnostics, undo/redo, permissions, subagents, and a lot more&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model support&lt;/td&gt;
&lt;td&gt;20+ providers, 300+ models, mid-session switching&lt;/td&gt;
&lt;td&gt;75+ providers via AI SDK and &lt;code&gt;models.dev&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheap model access&lt;/td&gt;
&lt;td&gt;BYOK or local, that's it&lt;/td&gt;
&lt;td&gt;Zen gateway at cost, plus Go at $10/month&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extensibility&lt;/td&gt;
&lt;td&gt;TypeScript extensions running inside the agent process&lt;/td&gt;
&lt;td&gt;Plugins, custom agents, skills, mostly config-driven&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;Branchable session trees with fork and rewind&lt;/td&gt;
&lt;td&gt;Linear log with undo/redo, but survives disconnects&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surfaces&lt;/td&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;TUI, desktop app, web UI, IDEs via ACP, GitHub and GitLab CI&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Nothing. Full system access from prompt one, which isn't really my preferred style&lt;/td&gt;
&lt;td&gt;allow/ask/deny per tool with glob patterns&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource footprint&lt;/td&gt;
&lt;td&gt;Lightweight, single process&lt;/td&gt;
&lt;td&gt;1GB+ RAM for a TUI is kinda crazy&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In summary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scorecard: Pi 6, OpenCode 6. Dead tie.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And honestly, that feels right. These two are built for pretty different kinds of people anyway.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember the VS Code vs. Neovim thing I said earlier?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pi is more like a platform you program. You get a tiny core and build your own harness around it in TypeScript.&lt;/p&gt;

&lt;p&gt;OpenCode is more like a product you configure. You get something much closer to a full Claude Code replacement on day one, then tweak it through config.&lt;/p&gt;

&lt;p&gt;Pi wins on efficiency, hackability, and, at least in our eval, raw results.&lt;/p&gt;

&lt;p&gt;OpenCode wins on features, provider support, surfaces, and the simple fact that a normal person can pick it up and start using it without building half the setup themselves.&lt;/p&gt;

&lt;p&gt;So yeah, the real tiebreaker is basically what kind of person you are. Keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Pi?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A minimal terminal coding agent that ships four tools and expects you to build the rest yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hsa836yiuj6i335nnhm.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%2F0hsa836yiuj6i335nnhm.png" alt="pi agent" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick recap if you missed our &lt;a href="https://composio.dev/blog/pi-agent-vs-claude-code" rel="noopener noreferrer"&gt;Pi vs Claude Code&lt;/a&gt; post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pi.dev/" rel="noopener noreferrer"&gt;Pi&lt;/a&gt; is Mario Zechner's answer to harness bloat. He got tired of Claude Code changing under him, so he built an agent where the model gets &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;, a system prompt of a few hundred tokens, and nothing else. No MCP. No permissions. No plan mode. No subagents. All on purpose.&lt;/p&gt;

&lt;p&gt;It now lives under &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;Earendil&lt;/a&gt; after his April blog post "I've sold out," and the core stays open-source.&lt;/p&gt;

&lt;p&gt;The real product is the extension system. TypeScript files run inside the same process as the agent loop, and Pi can read its own source code. So when you need a missing feature, you literally ask Pi to build it.&lt;/p&gt;

&lt;p&gt;That design is also why Peter Steinberger picked Pi as the engine for OpenClaw.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 &lt;strong&gt;Fun Fact:&lt;/strong&gt; Pi has "only" 94k stars, but the npm package pulls around 1.3 million downloads a week. Growing crazy fast among developers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not everyone is fond of VS Code. 🤷‍♂️&lt;/p&gt;




&lt;h2&gt;
  
  
  What is OpenCode?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The most starred open source coding agent in the world, and the closest thing to an open, model agnostic Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pkn6lanxysm87cxsm0z.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%2F0pkn6lanxysm87cxsm0z.png" alt="opencode" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; is basically an open-source, model-agnostic coding agent that tries to give you the full Claude Code experience without locking you to Anthropic&lt;/p&gt;

&lt;p&gt;It is built by the team behind SST, which rebranded to Anomaly in 2026 and moved the repo to &lt;code&gt;anomalyco/opencode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The numbers are crazy for a free tool. Around &lt;strong&gt;199k stars&lt;/strong&gt;. Roughly 8 million monthly active developers. Cloudflare publishes official OpenCode setup docs for its own engineers.&lt;/p&gt;

&lt;p&gt;Can you believe this free thing is doing over $25M ARR? It's just the tool that's free, there's subscription plans they've introduced from where they make tons of money.&lt;/p&gt;

&lt;p&gt;The origin story is crazier than Pi's. The original agent, called TermAI, was created by Kujtim Hoxha back in 2024. &lt;a href="https://x.com/thdxr" rel="noopener noreferrer"&gt;Dax Raad&lt;/a&gt; and Adam Elmore did the OpenCode rebrand and made it popular. Then Charm hired Hoxha and pulled the repo into its own org. That causes rewritten git history, banned contributors, the whole drama. After the backlash, Charm renamed its fork &lt;strong&gt;Crush&lt;/strong&gt;, and the Dax and Adam project kept the OpenCode name as a from scratch rewrite.&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%2Fsp1sf9ncds2glip7s2gb.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%2Fsp1sf9ncds2glip7s2gb.png" alt="opencode origin" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not anything interesting if you don't care about "history". Lol.&lt;/p&gt;

&lt;p&gt;So when anyone says OpenCode in 2026, they mean the Anomaly one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 &lt;strong&gt;Fun fact:&lt;/strong&gt; For the 1.0 release, the team threw out their entire Go based TUI and wrote a new framework called OpenTUI in Zig and SolidJS, because the old one "had performance and capability issues." Can't believe Golang had performance issues at that scale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A coding agent team writing their own Zig rendering engine for the terminal is the most 2026 thing I can think of. Crazy stuff.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Philosophy: Pi vs OpenCode
&lt;/h2&gt;

&lt;p&gt;Our earlier comparisons were open vs closed. This one is open vs open.&lt;/p&gt;

&lt;p&gt;Both teams agree on the big stuff. The harness should be MIT licensed. You should bring your own model. Nobody should be vendor-locked in.&lt;/p&gt;

&lt;p&gt;They completely disagree on how much harness should exist.&lt;/p&gt;

&lt;p&gt;OpenCode's philosophy is that the harness is a real product. Ship plan mode, permissions, LSP integration, MCP, a desktop app, a web UI, IDE support, GitHub actions. Give people everything Claude Code has, minus the lock in.&lt;/p&gt;

&lt;p&gt;Pi's bet is that most of that is overhead (not a feature, what OpenCode believes). Frontier models have been RL trained so hard on coding that they already know what a coding agent is. Every tool schema and every injected instruction is context you pay for on every request. So the harness should be close to zero, and you add back only what you personally need.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/disler/pi-vs-claude-code/blob/main/PI_VS_OPEN_CODE.md" rel="noopener noreferrer"&gt;disler comparison&lt;/a&gt; has the cleanest one liner on this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pi gives you control at the runtime level. OpenCode gives you control at the configuration level."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yes, there is real beef here. Zechner has publicly called out OpenCode's compaction for pruning tool results and breaking prompt caching.&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%2Fhm8s27cqzhdwh4m2r63d.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%2Fhm8s27cqzhdwh4m2r63d.png" alt="zechner called out opencode compaction" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;He is not a fan of their Vercel AI SDK dependency either. More on that below.&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%2Fz2ty3niibtgm8d4gd8bq.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%2Fz2ty3niibtgm8d4gd8bq.png" alt="zechner not a fan of vercel ai sdk" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Agent Architecture: Pi vs OpenCode
&lt;/h2&gt;

&lt;p&gt;Underneath, both use the same basic loop: read the task, call the model, run tools, feed results back, repeat.&lt;/p&gt;

&lt;p&gt;The architecture around that loop is where they differ.&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%2Fv09kkh6jojfuvzgrzb0c.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%2Fv09kkh6jojfuvzgrzb0c.png" alt="agent architecture difference pi vs opencode" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode: client + server
&lt;/h3&gt;

&lt;p&gt;OpenCode splits into a persistent local server and separate clients.&lt;/p&gt;

&lt;p&gt;The server handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agent logic&lt;/li&gt;
&lt;li&gt;LLM calls&lt;/li&gt;
&lt;li&gt;tool execution&lt;/li&gt;
&lt;li&gt;SQLite session storage&lt;/li&gt;
&lt;li&gt;MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The TUI, desktop app, web UI, and IDE integrations all talk to that same server.&lt;/p&gt;

&lt;p&gt;This makes sessions more safe to terminal crashes or SSH drops, and the same server can also run headlessly in CI. The downside is a heavier footprint, with people reporting &lt;strong&gt;1GB+ RAM usage&lt;/strong&gt; for the TUI setup.&lt;/p&gt;

&lt;p&gt;You read it right. Read more about it here: &lt;a href="https://news.ycombinator.com/item?id=47465584" rel="noopener noreferrer"&gt;OpenCode is pretty resource inefficient&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20ol6v5g70zqk4jmoqjq.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%2F20ol6v5g70zqk4jmoqjq.png" alt="Opencode 1gb+ ram tui usage" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi: single process
&lt;/h3&gt;

&lt;p&gt;Pi keeps everything in one process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider layer&lt;/li&gt;
&lt;li&gt;agent loop&lt;/li&gt;
&lt;li&gt;CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is all TypeScript and small enough to understand without going through a huge codebase.&lt;/p&gt;

&lt;p&gt;Pi supports four run modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interactive TUI&lt;/li&gt;
&lt;li&gt;print mode for scripts&lt;/li&gt;
&lt;li&gt;RPC over stdin/stdout&lt;/li&gt;
&lt;li&gt;SDK mode for embedding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That SDK is also how OpenClaw embeds Pi.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode's way of LSP
&lt;/h3&gt;

&lt;p&gt;I'd call it a star feature of OpenCode, the way it handles LSP (Language Server Protocol).&lt;/p&gt;

&lt;p&gt;OpenCode can run real language servers such as &lt;code&gt;gopls&lt;/code&gt; and &lt;code&gt;typescript-language-server&lt;/code&gt; and feed diagnostics back into the agent after edits.&lt;/p&gt;

&lt;p&gt;So the flow looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;model edits code → LSP reports an error → model sees it → model fixes it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That gives OpenCode fast compiler and type feedback without needing to run the full test every time.&lt;/p&gt;

&lt;p&gt;Pi does not have this out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi's Session trees
&lt;/h3&gt;

&lt;p&gt;Pi way of handling sessions is kinda interesting.&lt;/p&gt;

&lt;p&gt;Pi stores sessions as &lt;code&gt;JSONL&lt;/code&gt;, with every entry pointing to a parent. That means the session history is a tree instead of one linear log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C
     ↘ D → E
     ↘ F → G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;/tree&lt;/code&gt; to jump back to an earlier point&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;/fork&lt;/code&gt; to branch from any past message&lt;/li&gt;
&lt;li&gt;keep multiple debugging paths from the same point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenCode uses a linear history with &lt;code&gt;/undo&lt;/code&gt; and &lt;code&gt;/redo&lt;/code&gt;, which is simpler but less flexible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context compaction
&lt;/h3&gt;

&lt;p&gt;OpenCode uses a hidden system agent to summarize the conversation when context gets tight, then hides older messages from the active context.&lt;/p&gt;

&lt;p&gt;The criticism is that this can remove useful tool outputs and hurt prompt caching.&lt;/p&gt;

&lt;p&gt;Pi also supports compaction, but the implementation is replaceable through extensions, which fits its more hackable architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The benchmark. Same model, different harness
&lt;/h2&gt;

&lt;p&gt;Now for the part I was most curious about.&lt;/p&gt;

&lt;p&gt;We ran &lt;strong&gt;30 hard agentic tool-use tasks&lt;/strong&gt; against real apps. Every harness used the same model, &lt;strong&gt;DeepSeek V4 Pro (0813)&lt;/strong&gt; at max reasoning, through our hosted MCP router.&lt;/p&gt;

&lt;p&gt;Same model. Same tasks. Same tools. Same verifier checks.&lt;/p&gt;

&lt;p&gt;So whatever gap shows up here is mostly the harness.&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%2Fzcxk81pd9nlfx0ftssdv.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%2Fzcxk81pd9nlfx0ftssdv.png" alt="benchmark eval of opencode vs pi agent" width="799" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the full result.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness (DeepSeek V4 Pro, hard 30 set)&lt;/th&gt;
&lt;th&gt;Tasks passed&lt;/th&gt;
&lt;th&gt;Cost per success&lt;/th&gt;
&lt;th&gt;Cost per shared success&lt;/th&gt;
&lt;th&gt;Avg tokens per task&lt;/th&gt;
&lt;th&gt;Avg turns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pi Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21/30 (70%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.078&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;924,990&lt;/td&gt;
&lt;td&gt;16.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;383,722&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;$0.076&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;td&gt;88,562&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19/30 (63.3%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.119&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.032&lt;/td&gt;
&lt;td&gt;710,140&lt;/td&gt;
&lt;td&gt;13.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;19/30 (63.3%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.074&lt;/td&gt;
&lt;td&gt;649,900&lt;/td&gt;
&lt;td&gt;12.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes Agent&lt;/td&gt;
&lt;td&gt;18/30 (60%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.037&lt;/td&gt;
&lt;td&gt;113,894&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ *Not comparable because cost measure was incomplete for some cells. Shared success only counts tasks every harness passed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs67o45spge4sqmtrv2lb.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%2Fs67o45spge4sqmtrv2lb.png" alt="eval report" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi actually topped the board
&lt;/h3&gt;

&lt;p&gt;Pi finished first with &lt;strong&gt;21/30 tasks passed&lt;/strong&gt;, while OpenCode landed at &lt;strong&gt;19/30&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8p1e2vviblg8vkreyzku.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8p1e2vviblg8vkreyzku.jpg" alt="agent leaderboard" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was also cheaper across the full run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi&lt;/strong&gt; spent $1.64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode&lt;/strong&gt; spent $2.25&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pi cost per success&lt;/strong&gt; was $0.078&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode cost per success&lt;/strong&gt; was $0.119&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number looks rough for OpenCode, but there is a catch.&lt;/p&gt;

&lt;p&gt;On tasks both harnesses actually passed, they were basically tied. Pi came in at &lt;strong&gt;$0.031 per shared success&lt;/strong&gt;, while OpenCode was &lt;strong&gt;$0.032&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So OpenCode was not really more expensive when it worked. It just failed more tasks, and &lt;strong&gt;failed runs still burn tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi was also painfully slow
&lt;/h3&gt;

&lt;p&gt;Pi had the best pass rate, but it was also the &lt;strong&gt;slowest harness in the entire run&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekaax7isq4u9zfbsy07i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekaax7isq4u9zfbsy07i.jpg" alt="agent time eval comparison" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi median time&lt;/strong&gt; was 362.9 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode median time&lt;/strong&gt; was 280.6 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code median time&lt;/strong&gt; was 181.8 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pi also averaged &lt;strong&gt;16.3 turns per task&lt;/strong&gt;, compared with OpenCode's &lt;strong&gt;13.1&lt;/strong&gt;, and pushed more raw tokens than anyone else.&lt;/p&gt;

&lt;p&gt;The difference is that its fixed overhead stays tiny.&lt;/p&gt;

&lt;p&gt;The system prompt plus tool definitions stay under 1,000 tokens, and the prefix barely changes between turns. That makes a lot of that repeated context land as cache hits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The task-by-task split is more interesting
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRM migration archive&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;td&gt;❌ 3/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack action items&lt;/td&gt;
&lt;td&gt;✅ 5/5&lt;/td&gt;
&lt;td&gt;❌ 2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor directory&lt;/td&gt;
&lt;td&gt;✅ 11/13 checks&lt;/td&gt;
&lt;td&gt;❌ 7/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handover audit&lt;/td&gt;
&lt;td&gt;✅ 11/11&lt;/td&gt;
&lt;td&gt;❌ 10/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PagerDuty timeline audit&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;td&gt;❌ 9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renewal screen&lt;/td&gt;
&lt;td&gt;❌ 5/11&lt;/td&gt;
&lt;td&gt;✅ 11/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sponsorship screen&lt;/td&gt;
&lt;td&gt;❌ 4/11&lt;/td&gt;
&lt;td&gt;✅ 11/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sheets to Airtable tracker&lt;/td&gt;
&lt;td&gt;❌ 8/10&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fn5ccw21opj1h7il64v1r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5ccw21opj1h7il64v1r.jpg" alt="task-by-task split of agent test" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi did better on the &lt;strong&gt;audit and census-style tasks&lt;/strong&gt;, where patiently reading everything and checking every detail matters.&lt;/p&gt;

&lt;p&gt;OpenCode did better on the &lt;strong&gt;longer screening and multi-step workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which is kinda perfect.&lt;/p&gt;

&lt;p&gt;Pi wins by being small. OpenCode wins when the workflow benefits from more stuff around the model.&lt;/p&gt;

&lt;p&gt;After all this non-sense, the eval ended up being spot on for both of them. 🤧&lt;/p&gt;

&lt;p&gt;We did a complete breakdown in this tweet. You might want to &lt;a href="https://x.com/composio/status/2090069397050097864" rel="noopener noreferrer"&gt;check it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2090069397050097864-193" src="https://platform.twitter.com/embed/Tweet.html?id=2090069397050097864"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2090069397050097864-193');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2090069397050097864&amp;amp;theme=dark"
  }



&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Token overhead
&lt;/h2&gt;

&lt;p&gt;This is basically the argument both tools were built around, so let's put some numbers next to it.&lt;/p&gt;

&lt;p&gt;The most cited comparison here is the &lt;a href="https://systima.ai/blog/claude-code-vs-opencode-token-overhead" rel="noopener noreferrer"&gt;Systima study&lt;/a&gt; from July, which also ended up doing &lt;a href="https://news.ycombinator.com/item?id=48883275" rel="noopener noreferrer"&gt;700+ points on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;They put a logging proxy between the harness and the model, then measured how much context gets sent &lt;strong&gt;before the model even sees your prompt&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness&lt;/th&gt;
&lt;th&gt;Fixed overhead per request&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (measured by Systima)&lt;/td&gt;
&lt;td&gt;~32,800 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode (measured by Systima)&lt;/td&gt;
&lt;td&gt;~6,900 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenCode already looks pretty good here. Around &lt;strong&gt;4.7x less overhead than Claude Code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then Pi shows up with &lt;strong&gt;under 1,000 tokens&lt;/strong&gt;, including the tool definitions.&lt;/p&gt;

&lt;p&gt;That makes it roughly &lt;strong&gt;7x leaner than OpenCode&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Everyone is somebody's bloatware. 🗿&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Matei Zaharia &lt;a href="https://x.com/matei_zaharia/status/2074943619610649059" rel="noopener noreferrer"&gt;said on X&lt;/a&gt; the reason was pretty simple. Pi had the same success rate with &lt;strong&gt;2x less cost.&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjhr0zfzfpglr03e4nkk.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%2Fyjhr0zfzfpglr03e4nkk.png" alt="pi success rate at 2x less cost compared to other agents" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our eval points in the same direction too. Pi had the best pass rate and the lowest total spend.&lt;/p&gt;

&lt;p&gt;But there is an important catch here.&lt;/p&gt;

&lt;p&gt;OpenCode has had some pretty ugly token cases too. There is a &lt;a href="https://github.com/anomalyco/opencode/issues/8234" rel="noopener noreferrer"&gt;GitHub issue&lt;/a&gt; where asking something as basic as "what's the tech stack in this project" burned &lt;strong&gt;13,088 tokens&lt;/strong&gt; on a fresh project.&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%2Fag06f0ra64r5zoyqw18j.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%2Fag06f0ra64r5zoyqw18j.png" alt="excessive token usage of opencode" width="799" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Pi is not crazy cheap in every sense either. In our eval, it actually pushed the &lt;strong&gt;highest raw token count of all six harnesses&lt;/strong&gt;, mostly because it took more turns to finish tasks.&lt;/p&gt;

&lt;p&gt;So yeah, tiny fixed overhead helps a lot. But &lt;strong&gt;lean overhead does not always mean lean sessions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth keeping that distinction in mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Pricing and the ban
&lt;/h2&gt;

&lt;p&gt;Both tools are free and MIT licensed. Fork them, modify them, ship them.&lt;/p&gt;

&lt;p&gt;The difference starts after install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;BYOK for pretty much any provider&lt;/li&gt;
&lt;li&gt;local models through Ollama, vLLM, or llama.cpp&lt;/li&gt;
&lt;li&gt;OAuth login where providers still allow it&lt;/li&gt;
&lt;li&gt;no paid CLI tier at all&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Pi is basically just the harness. You bring the model and pay whoever is serving it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  OpenCode
&lt;/h3&gt;

&lt;p&gt;OpenCode does the same BYOK thing, but it also has its own model options.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BYOK&lt;/strong&gt; where the provider bills you directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zen&lt;/strong&gt; with curated models sold on a pay-as-you-go basis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; with open coding models bundled into a cheap monthly plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go starts at &lt;strong&gt;$5 for the first month, then $10/month&lt;/strong&gt;, with models like GLM, Kimi K3, Qwen, MiniMax, and DeepSeek.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Double check the pricing. OpenCode changes this stuff pretty often.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And honestly, that Go plan is hard to ignore.&lt;/p&gt;

&lt;p&gt;For $10/month, it is probably one of the cheapest legit ways to run capable open models through a coding agent right now.&lt;/p&gt;

&lt;p&gt;Pi does not really have an equivalent. Its version of "cheap" is running local models, which is useful, but it is not the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then Anthropic happened
&lt;/h3&gt;

&lt;p&gt;In January 2026, Anthropic started rejecting Claude Pro and Max OAuth tokens when they were used outside Claude Code.&lt;/p&gt;

&lt;p&gt;The error was pretty clear. The credential was &lt;strong&gt;"only authorized for use with Claude Code."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenCode had been using those subscription tokens by mimicking the Claude Code client, so this hit them directly.&lt;/p&gt;

&lt;p&gt;Then came the legal requests, the Claude login code got removed, and we got the now-famous commit message from the intro.&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%2Fmpc88vg2fnh7c1erd5x1.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%2Fmpc88vg2fnh7c1erd5x1.png" alt="george hotz calling out anthropic move of removing sub a huge mistake" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;George Hotz called the move &lt;strong&gt;"a huge mistake."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenAI reportedly went in the other direction and opened Codex subscription access to third-party harnesses, including OpenCode and Pi.&lt;/p&gt;

&lt;p&gt;Lol.&lt;/p&gt;

&lt;p&gt;The important part for this comparison is pretty simple now.&lt;/p&gt;

&lt;p&gt;If you want Claude in either Pi or OpenCode, you are basically paying API rates. OpenCode also gives you Zen as another route.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Extensibility
&lt;/h2&gt;

&lt;p&gt;Both are highly customizable, but the extension model is very different.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode
&lt;/h3&gt;

&lt;p&gt;OpenCode is mostly config driven through &lt;code&gt;opencode.json&lt;/code&gt;, plugins, agents, skills, and MCP.&lt;/p&gt;

&lt;p&gt;You can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom agents with their own model, prompt, permissions, and step limits&lt;/li&gt;
&lt;li&gt;JS or TS plugins with lifecycle hooks like &lt;code&gt;tool.execute.before&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;custom tools&lt;/li&gt;
&lt;li&gt;local and remote MCP servers&lt;/li&gt;
&lt;li&gt;skills from &lt;code&gt;.opencode/skills&lt;/code&gt; and &lt;code&gt;.claude/skills&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&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;"permission"&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;"bash"&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;"git *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rm *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&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;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;p&gt;Pi extensions are TypeScript modules loaded directly into the single agent process.&lt;/p&gt;

&lt;p&gt;They can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rewrite or block tool calls&lt;/li&gt;
&lt;li&gt;replace built-in tools&lt;/li&gt;
&lt;li&gt;filter history&lt;/li&gt;
&lt;li&gt;replace compaction&lt;/li&gt;
&lt;li&gt;persist custom session state&lt;/li&gt;
&lt;li&gt;add TUI components&lt;/li&gt;
&lt;li&gt;probably many more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives Pi much &lt;strong&gt;deeper runtime control&lt;/strong&gt; than OpenCode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ OpenCode plugins extend the harness. Pi extensions can modify the harness itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tradeoff is security. Pi extensions run in process with full system access, so they need more trust than OpenCode's plugin model.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Permissions and safety
&lt;/h2&gt;

&lt;p&gt;This is probably the biggest gap between the two.&lt;/p&gt;

&lt;p&gt;OpenCode has a proper permission system. Every tool can be set to &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, or &lt;code&gt;deny&lt;/code&gt;, with glob patterns and per-agent overrides. The defaults are decent too. &lt;code&gt;.env&lt;/code&gt; reads are denied by default, and the Plan agent asks before edits or bash commands.&lt;/p&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; have is real sandboxing. There is no container or OS-level isolation built in.&lt;/p&gt;

&lt;p&gt;Pi goes the other way and basically ships no permission layer at all. From the first prompt, the agent runs with your full user privileges. 🥶&lt;/p&gt;

&lt;p&gt;That is intentional. Zechner's say is that once an agent can write files and execute code, permission prompts only get you so far. Pi instead expects you to isolate the whole process using Docker, a microVM like Gondolin, or a policy sandbox like OpenShell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 There is also an optional &lt;code&gt;damage-control.ts&lt;/code&gt; extension that checks bash commands against YAML rules. Helps a bit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the actual difference is pretty simple.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ OpenCode gives you tool-level guardrails. Pi expects you to handle isolation outside the agent.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick OpenCode if&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want something close to a full Claude Code replacement without building half the setup yourself&lt;/li&gt;
&lt;li&gt;You want plan mode, permissions, MCP, undo and redo, and LSP support out of the box&lt;/li&gt;
&lt;li&gt;You want the $10 Go plan for cheap access to capable open models&lt;/li&gt;
&lt;li&gt;You work in typed languages and want LSP diagnostics fed straight back into the agent&lt;/li&gt;
&lt;li&gt;You are setting this up for a team and actually need proper &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, and &lt;code&gt;deny&lt;/code&gt; controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Pi if&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You care about token cost at scale. In our eval, the same model passed more tasks and cost less with Pi&lt;/li&gt;
&lt;li&gt;You want a harness small enough that you can actually read and understand the whole thing&lt;/li&gt;
&lt;li&gt;You like branching while debugging. Pi's session trees are really good for that&lt;/li&gt;
&lt;li&gt;You run local or smaller models where the difference between ~1,000 and ~6,900 tokens of fixed overhead actually matters&lt;/li&gt;
&lt;li&gt;You would rather change the harness in TypeScript than spend time configuring someone else's abstraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benchmark kinda gives away the answer anyway.&lt;/p&gt;

&lt;p&gt;Pi did better on the careful audit-style tasks. OpenCode did better on longer, more structured workflows.&lt;/p&gt;

&lt;p&gt;So there probably isn't one winner for everything.&lt;/p&gt;

&lt;p&gt;Both are free. Try both and keep the one that fits how you work. ✌️&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&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%2Ftya4fg1aj5uz7ql0o8hu.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%2Ftya4fg1aj5uz7ql0o8hu.png" alt="Pi Agent vs. OpenCode test summary result" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After all that, I still don’t think there’s one obvious winner here. The scorecard ends &lt;strong&gt;6-6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;OpenCode wins on adoption, features, and ease of use. Pi won our benchmark with &lt;strong&gt;21/30 tasks vs 19/30&lt;/strong&gt;, lower total spend, and way less fixed overhead.&lt;/p&gt;

&lt;p&gt;The bigger difference is still the same one we started with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OpenCode gives you configuration control. Pi gives you runtime control.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you just want to install something and start working, pick &lt;strong&gt;OpenCode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to understand and modify the harness itself, pick &lt;strong&gt;Pi&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And if cost per successful task starts mattering at scale, keep an eye on Pi. 🤷‍♂️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Pi Agent vs Claude Code After 100 Hours of Real Use 🔥</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:36:48 +0000</pubDate>
      <link>https://dev.to/composiodev/pi-agent-vs-claude-code-after-100-hours-of-real-use-1dfp</link>
      <guid>https://dev.to/composiodev/pi-agent-vs-claude-code-after-100-hours-of-real-use-1dfp</guid>
      <description>&lt;p&gt;While researching for this post, I found out something funny. Pi, the most interesting coding agent of 2026, was built by a guy who got fed up with Claude Code. 🥶&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/badlogicgames" rel="noopener noreferrer"&gt;Mario Zechner&lt;/a&gt;, the creator of libGDX, was a hardcore Claude Code user. He built &lt;a href="https://mariozechner.at/posts/2025-08-03-cchistory/" rel="noopener noreferrer"&gt;cchistory&lt;/a&gt; just to track its system prompt changes, and even &lt;a href="https://mariozechner.at/posts/2025-08-06-cc-antidebug/" rel="noopener noreferrer"&gt;patched the binary&lt;/a&gt; to add features Anthropic had not shipped.&lt;/p&gt;

&lt;p&gt;Then he quit and wrote his own agent. Four tools, a tiny system prompt, no MCP, no permissions. He named it Pi so nobody could Google it. What a Chad! 🗿&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%2F18euu1imp5t223k454dv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18euu1imp5t223k454dv.gif" alt="swag gif" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That small project now has 85k+ GitHub stars, powers OpenClaw, and outscored Claude Code on our own tool use benchmark at a fifth of the cost.&lt;/p&gt;

&lt;p&gt;So I put the two side by side. The default everyone uses, against the minimal harness built as a rejection of it.&lt;/p&gt;

&lt;p&gt;Let's go!&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool price&lt;/td&gt;
&lt;td&gt;Needs a paid plan or API key&lt;/td&gt;
&lt;td&gt;Free, MIT licensed&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real tool use (our eval)&lt;/td&gt;
&lt;td&gt;16/30 tasks passed, $0.195 per success&lt;/td&gt;
&lt;td&gt;20/30 tasks passed, $0.028 per success&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable billing&lt;/td&gt;
&lt;td&gt;$20/month flat on Pro&lt;/td&gt;
&lt;td&gt;Pay per token, bills can spike&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model support&lt;/td&gt;
&lt;td&gt;Claude models only but it’s built for it&lt;/td&gt;
&lt;td&gt;20+ providers, 300+ models, mid session switching&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of the box features&lt;/td&gt;
&lt;td&gt;Sub agents, teams, plan mode, MCP, skills, checkpoints&lt;/td&gt;
&lt;td&gt;Four tools and a text box&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context discipline&lt;/td&gt;
&lt;td&gt;Auto compaction, silent context management&lt;/td&gt;
&lt;td&gt;You see and control every token&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extensibility depth&lt;/td&gt;
&lt;td&gt;Shell hooks, MCP, skills, plugins&lt;/td&gt;
&lt;td&gt;TypeScript extensions running inside the agent process&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi agent workflows&lt;/td&gt;
&lt;td&gt;Native sub agents and Agent Teams&lt;/td&gt;
&lt;td&gt;Spawn pi via bash, or install a community extension&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions and guardrails&lt;/td&gt;
&lt;td&gt;Deny by default, five modes, sandboxing&lt;/td&gt;
&lt;td&gt;Full system access from the first prompt&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surfaces&lt;/td&gt;
&lt;td&gt;Terminal, VS Code, JetBrains, desktop, web, mobile, Slack&lt;/td&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session model&lt;/td&gt;
&lt;td&gt;Linear conversation log&lt;/td&gt;
&lt;td&gt;Branchable session trees with fork and rewind&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In Summary:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scorecard: Pi 6, Claude Code 5.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pi is the cheaper, more flexible, and more transparent harness, especially if you pay per token, switch models, or want full control. Claude Code remains the better daily driver for most people because it ships with stronger guardrails, predictable subscription pricing, and a polished feature set out of the box. The practical answer is to use Claude Code for everyday work and Pi for custom workflows, local models, and cost-sensitive tasks.&lt;/p&gt;

&lt;p&gt;And here is the twist. The scorecard says Pi, but almost everyone who reviews both, including the biggest Pi fans, still runs Claude Code as their daily driver. I will explain why is that in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Pi?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A minimal, open source terminal coding agent that ships four tools and expects you to build the rest yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa91gkp42cqdq38nenpqm.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%2Fa91gkp42cqdq38nenpqm.png" alt="Pi agent" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pi.dev/" rel="noopener noreferrer"&gt;Pi&lt;/a&gt; is the coding agent Mario Zechner released in late 2025, now maintained under &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;Earendil&lt;/a&gt;. Zechner joined Earendil in April 2026 in a blog post he titled "I've sold out," and the core stayed MIT licensed.&lt;/p&gt;

&lt;p&gt;The whole product fits in literally one sentence. The model gets &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;, a system prompt of a few hundred tokens, and nothing else.&lt;/p&gt;

&lt;p&gt;His reasoning: frontier models have been RL trained so hard on coding agent behavior that they already know what a coding agent is. You do not need 10,000 tokens of instructions to remind them. Need ripgrep? The model runs &lt;code&gt;rg&lt;/code&gt; through bash. Need GitHub? It runs &lt;code&gt;gh&lt;/code&gt;. Need a browser? Ask Pi to write itself a browser tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If you're on the same philosophy, this might be worth reading: &lt;a href="https://composio.dev/content/top-10-cli-tools-for-claude-code" rel="noopener noreferrer"&gt;Top 10 CLI Tools to Level-Up Claude Code&lt;/a&gt;. No MCPs, nothing. Plain raw Bash tools gets most of the work done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last part is the actual product. Pi extensions are TypeScript files that run inside the same process as the agent loop, and Pi can read its own source and docs, so the standard workflow when you need a missing feature is to ask Pi to build it.&lt;/p&gt;

&lt;p&gt;Everything Zechner refused to ship, the community shipped as extensions within weeks. Todo lists, plan mode, sub agents, permission gates, even an MCP adapter.&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%2Fnbszl2dke891ql0slm3e.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbszl2dke891ql0slm3e.gif" alt="Laugh GIF" width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Claude Code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Anthropic's batteries included coding agent, and the default the entire agent category gets measured against&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;You know this one. &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; started as Boris Cherny's internal AppleScript toy at Anthropic in September 2024, hit research preview in February 2025, and became the fastest scaling developer product anyone has ever measured.&lt;/p&gt;

&lt;p&gt;It ships everything: 10+ built in tools, sub agents, Agent Teams, plan mode, MCP as both client and server, Agent Skills, plugins, hooks, checkpoints with rewind, a permission system with five modes, and it runs in the terminal, VS Code, JetBrains, a desktop app, the browser, and your phone.&lt;/p&gt;

&lt;p&gt;More than 80% of Anthropic's own engineers use it daily, and around "90% of Claude Code's code is now written by Claude Code". 🥴&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Philosophy: Pi vs Claude Code
&lt;/h2&gt;

&lt;p&gt;This comparison only makes sense once you understand that Pi exists because of Claude Code.&lt;/p&gt;

&lt;p&gt;Zechner was an early Claude Code user. His complaint was never that it was bad. His complaint was that it kept changing underneath him. The system prompt and tool definitions shifted on every release, which broke his workflows and changed model behavior.&lt;/p&gt;

&lt;p&gt;So the two tools represent opposite answers to the same question: how much harness does a frontier model actually need?&lt;/p&gt;

&lt;p&gt;Anthropic's answer is interesting because it is drifting toward Pi's. Boris Cherny's team deletes harness code every time a new model ships, and in July 2026 Anthropic removed over 80% of Claude Code's system prompt for the Claude 5 generation models with no measurable loss on their coding evals. The bet behind Claude Code is that Anthropic trains the model and the harness together, so scaffolding today can become model behavior tomorrow.&lt;/p&gt;

&lt;p&gt;Pi's answer is that the scaffolding was never load bearing in the first place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Anthropic quietly deleting most of its own system prompt is about the strongest validation of Pi's thesis you could ask for. Zechner just got there a year early.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Agent Architecture: Claude Code vs Pi
&lt;/h2&gt;

&lt;p&gt;Both run the same loop underneath. Read the task, call the model, execute tool calls, feed results back, repeat until the model stops asking for tools.&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%2Fcefop0itn8v17urai239.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcefop0itn8v17urai239.jpg" alt="agent loop" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference is everything wrapped around that loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; gives the model Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, NotebookEdit, TodoWrite, there's a lot.... and the Task tool that spawns typed sub agents with their own isolated context windows. Plan mode forces a read only explore and plan phase before any file gets touched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pi&lt;/strong&gt; gives the model four tools, plus optional read only &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt; behind a &lt;code&gt;-tools&lt;/code&gt; flag. There is no plan mode, but &lt;code&gt;pi --tools read,grep,find,ls&lt;/code&gt; gets you a read only session in one line. There are no sub agents, and they say that if you want one, tell Pi to run itself through bash, ideally in tmux so you can actually watch it work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt gap is the main highlight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;System prompt&lt;/th&gt;
&lt;th&gt;Built in tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (pre Claude 5 era)&lt;/td&gt;
&lt;td&gt;~10,000 to 14,000 tokens by community measurement&lt;/td&gt;
&lt;td&gt;10+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (current)&lt;/td&gt;
&lt;td&gt;Cut by over 80% for Claude 5 gen models&lt;/td&gt;
&lt;td&gt;10+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens including tool definitions&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One less obvious architectural difference: Pi stores sessions as trees, not logs. Every message carries a parent id, so under the hood this is basically a linked list that learned to branch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A real use case for linked lists, in the wild. All that leetcode grind had something useful. 🤡&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So &lt;code&gt;/tree&lt;/code&gt; lets you jump back to any earlier point in the conversation and branch from there, and &lt;code&gt;/fork&lt;/code&gt; spins a new session off any past message. Claude Code got checkpoints and rewind in 2.0, which covers the common case, but Pi's tree model is the better design.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The benchmark: same model, different harness
&lt;/h2&gt;

&lt;p&gt;We ran 30 hard agentic tool use tasks against real apps, eight harnesses, every single one wired to the same model, DeepSeek V4 Flash, through our hosted MCP router, with a 900 second ceiling per task.&lt;/p&gt;

&lt;p&gt;Same model, same tasks, same tools. The only thing that changes is the harness, so whatever gap shows up is the wrapper, not the model.&lt;/p&gt;

&lt;p&gt;Here is how our two contenders did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness (DeepSeek V4 Flash, hard 30 set)&lt;/th&gt;
&lt;th&gt;Tasks passed&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Avg tokens per task&lt;/th&gt;
&lt;th&gt;Total cost&lt;/th&gt;
&lt;th&gt;Cost per success&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;16/30 (53.3%)&lt;/td&gt;
&lt;td&gt;122.7s&lt;/td&gt;
&lt;td&gt;741,659&lt;/td&gt;
&lt;td&gt;$3.12&lt;/td&gt;
&lt;td&gt;$0.195&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;132.2s&lt;/td&gt;
&lt;td&gt;558,885&lt;/td&gt;
&lt;td&gt;$0.56&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F1g15qyiwhm1p1k1gs8pw.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%2F1g15qyiwhm1p1k1gs8pw.png" alt="eight agent harnesses eval" width="799" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi passed more tasks than any of the eight harnesses we tested, and did it at the lowest cost of the whole bunch. Cost per successful task is the brutal column: &lt;strong&gt;3 cents against 19&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code posted the fastest median time. It just burned 741,659 tokens per task getting there, against Pi's 558,885. The overhead is the story, again.&lt;/p&gt;

&lt;p&gt;And yes, that is Claude Code running DeepSeek. That is the whole point of the setup. Only the harness changes.&lt;/p&gt;

&lt;p&gt;For the curious, Codex ran the same and landed at 16/30 for $1.29 total. Right between the two.&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%2F8zelou5gp9orqip73xjn.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%2F8zelou5gp9orqip73xjn.png" alt="agent harness report" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOTE:&lt;/strong&gt; Pi ran on high reasoning instead of max, and 24 of its 30 trials went through the official DeepSeek API instead of OpenRouter. Make sure to read the exact numbers with that in mind.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Pricing: Claude Code vs Pi
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pro, $20/month or $17/month annually, includes Claude Code&lt;/li&gt;
&lt;li&gt;Max 5x at $100/month, Max 20x at $200/month&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Guys, make sure to double check the pricing. It changes frequently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The tool costs nothing. MIT license, fork it, ship it&lt;/li&gt;
&lt;li&gt;You bring your own API keys for any provider, or run local models for free through Ollama, vLLM, or llama.cpp&lt;/li&gt;
&lt;li&gt;OAuth login for subscriptions where providers allow it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper Pi wins on price and Claude Code wins on predictability. A $20 flat rate is genuinely hard to beat for a solo dev using Claude models all day, and per token bills on heavy Pi usage can hit hundreds of dollars a month. I usually prefer the subscription system.&lt;/p&gt;

&lt;p&gt;Anthropic locked its subscription OAuth tokens to Claude Code and &lt;a href="http://claude.ai/" rel="noopener noreferrer"&gt;Claude.ai&lt;/a&gt; only. Third party harnesses got cut off, starting with a silent server side block in January, formalized in the docs by February, fully enforced by April. OpenClaw, OpenCode, Cline, and yes, Pi, all lost the ability to run on your Claude Pro or Max subscription.&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%2Fo2a55k881h4c2noc8c2q.webp" 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%2Fo2a55k881h4c2noc8c2q.webp" alt="anthropic locking subscription for third party providers" width="800" height="757"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the practical situation today: running Claude models through Pi means API rates. Running them through Claude Code means your cheap subscription.&lt;/p&gt;

&lt;p&gt;DHH called the move "very customer hostile," Lol.&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%2Fq8w1n0o9c5089f3wal0m.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%2Fq8w1n0o9c5089f3wal0m.png" alt="DHH calling anthropic move costomer hostile" width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenAI's Codex team publicly went the opposite direction and encouraged third party harness usage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If your workflow is Claude models all day every day, the subscription math alone mostly settles this comparison. Pi's cost advantage is real on API keys and irrelevant on a Max plan you already pay for.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Models: one provider vs all of them
&lt;/h2&gt;

&lt;p&gt;Claude Code runs Claude. Opus, Sonnet, and Haiku tiers, switchable with &lt;code&gt;/model&lt;/code&gt;, with thinking effort levels, tuned end to end because Anthropic controls the whole stack.&lt;/p&gt;

&lt;p&gt;That vertical integration is why the model feels so at home in the harness. It was trained in it.&lt;/p&gt;

&lt;p&gt;Pi runs everything. The &lt;code&gt;pi-ai&lt;/code&gt; layer normalizes four API shapes across 20+ providers, from Anthropic and OpenAI to Groq, Cerebras, Mistral, OpenRouter, and local models. Over 300 models total. You can switch models mid session with &lt;code&gt;Ctrl+P&lt;/code&gt;, and Pi converts thinking traces between providers.&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%2F9nk81lwqadsamqgaf7n2.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%2F9nk81lwqadsamqgaf7n2.png" alt="multiple model providers in pi agent" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code's giant prompt overhead makes it a poor fit for small local models, which is exactly why Simon Willison flagged Pi as the lightweight option for that use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Extensibility
&lt;/h2&gt;

&lt;p&gt;Both tools are deeply customizable.&lt;/p&gt;

&lt;p&gt;Claude Code's model is external and config driven. Hooks are shell scripts that fire on 14 lifecycle events and talk JSON over stdin. MCP servers are separate processes speaking a protocol. Skills are markdown instruction packs with lazy loading. Plugins bundle all of it into installable packages with official and community marketplaces behind a single &lt;code&gt;/plugin&lt;/code&gt; command.&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;"permissions"&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;"allow"&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;"Bash(npm run test *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&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;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl *)"&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;p&gt;Pi's model is internal. Extensions are TypeScript modules loaded into the same runtime as the agent loop, hooking 25 event types. They can block or rewrite tool calls before they run, override the built in tools entirely, inject context, replace the compaction logic, persist custom state into the session file, and draw their own TUI widgets. Claude Code's hooks can approve or reject what the agent does. Pi's extensions can change what the agent is.&lt;/p&gt;

&lt;p&gt;The tradeoff is trust. Claude Code's marketplace plugins go through some checks and run outside the core process. Pi packages run with full system access, and its own docs tell you to review them before installing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The sharpest criticism of Pi's ecosystem, from &lt;a href="https://www.thevinter.com/blog/bad-vibes-from-pi" rel="noopener noreferrer"&gt;thevinter's "Bad Vibes From Pi"&lt;/a&gt;, lands exactly here: a lot of those community extensions are themselves vibecoded, so that's something to watch-out for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nbc83mudu2tor7799jt.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%2F8nbc83mudu2tor7799jt.png" alt="Pi agent extensions probable harm talk" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Permissions and safety
&lt;/h2&gt;

&lt;p&gt;This is the widest gap between the two tools, and neither side considers it a flaw.&lt;/p&gt;

&lt;p&gt;Claude Code is deny by default. Every risky action asks first, and you can loosen from there through five permission modes, allow and deny rules, and OS level sandboxing for bash.&lt;/p&gt;

&lt;p&gt;When Claude Code has security incidents, and it has had real ones with CVEs attached, they get found, patched, and written up because millions of people are using it.&lt;/p&gt;

&lt;p&gt;Pi has no permission system. First prompt, full user privileges, go.&lt;/p&gt;

&lt;p&gt;Zechner's argument is that agent security is mostly "theater" 🫩, because once a tool can write code and run code the game is already over. Pi's answer to safety is isolation instead of prompts: run it in Docker, in a micro VM, or behind a sandbox, and there is an opt in extension that checks bash commands against YAML rules if you want a middle ground.&lt;/p&gt;

&lt;p&gt;One of them requires you to have read the docs, and the other protects the "intern" who did not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There isn't much to discuss here. You probably already know how things work by now...&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What the Internet thinks
&lt;/h2&gt;

&lt;p&gt;Maybe worth seeing what the internet thinks of Pi and Claude Code by now?&lt;/p&gt;

&lt;p&gt;It's more of what we are discussing so far. The famous endorsements is around Pi, the daily usage clusters around Claude Code, and both facts are true.&lt;/p&gt;

&lt;p&gt;Let me show you the Pi love first, because it is a lot.&lt;/p&gt;

&lt;p&gt;Peter Steinberger built OpenClaw on top of Pi and quote tweeted Zechner with "Mario is the real one we don't deserve." Armin Ronacher wrote that Pi is &lt;a href="https://lucumr.pocoo.org/2026/1/31/pi/" rel="noopener noreferrer"&gt;the coding agent he uses "almost exclusively"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai19ky33q5v99uvizdsp.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%2Fai19ky33q5v99uvizdsp.png" alt="openclaw creator praising pi agent creator" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Theo from &lt;a href="http://t3.gg/" rel="noopener noreferrer"&gt;t3.gg&lt;/a&gt; did a whole episode about getting Pi pilled. His take was simple: "The more minimal, the better." Wes Bos and Scott Tolinski spent a Syntax episode arguing Claude Code is overkill for most work.&lt;/p&gt;

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

&lt;p&gt;My favorite line comes from &lt;a href="https://agenticengineer.com/the-only-claude-code-competitor" rel="noopener noreferrer"&gt;IndyDevDan&lt;/a&gt;, who calls Pi the only real Claude Code competitor: "Claude Code is the starter pack. Pi is the endgame."&lt;/p&gt;

&lt;p&gt;About 80% of his work still goes through Claude Code. Only 20% goes through Pi. The endgame guy is still playing the starter pack. The internet is wild!&lt;/p&gt;

&lt;p&gt;Claude Code does not need loud fans anyway. It has $2.5B in revenue (might increase, researched through Claude itself), 141k GitHub stars, most of the enterprise market, and Microsoft reportedly using it inside their own teams. The numbers are just enough, lol. 🤧&lt;/p&gt;


&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Claude Code if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want something that works at full power&lt;/li&gt;
&lt;li&gt;You live on Claude models, because the $20 to $200 subscriptions are the cheapest way to run them and Pi is locked out of that pricing&lt;/li&gt;
&lt;li&gt;You are deploying to a team and need permissions, SSO, spend caps, and audit trails&lt;/li&gt;
&lt;li&gt;You want sub agents, plan mode, and multi surface access without building anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Pi if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You pay per token at scale, since less context per task is real money, a fifth of the cost in our eval&lt;/li&gt;
&lt;li&gt;You want to mix providers, run open models, or go fully local&lt;/li&gt;
&lt;li&gt;You want to read, own, and modify every part of your harness, MIT license and all&lt;/li&gt;
&lt;li&gt;You are the kind of engineer who would rather build the exact workflow you want than configure someone else's&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do what most reviewers actually do:&lt;/strong&gt; run both. Claude Code as the primary builder on your subscription, Pi as the cheap second opinion, the local model runner, and the place where your custom workflows live. The two tools are converging anyway.&lt;/p&gt;

&lt;p&gt;Claude Code keeps deleting scaffolding, and the Pi community keeps rebuilding Claude Code's features as extensions. 🤡&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&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%2Fd12fa7k79ota3dwk50uj.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%2Fd12fa7k79ota3dwk50uj.png" alt="pi agent vs claude code final score" width="799" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scorecard reads Pi 6, Claude Code 5. And honestly, Pi deserves those wins. It is cheaper per task, open source, works with any model. What more do you want?&lt;/p&gt;

&lt;p&gt;But here is the thing. When I sit down with a bug and just want it fixed, I open Claude Code. It works right away, the guardrails just works, the $20 plan keeps the bill consistent, and the model just feels at home in it.&lt;/p&gt;

&lt;p&gt;No thinking about the tool itself.&lt;/p&gt;

&lt;p&gt;That is the whole story really. Even the biggest Pi fans ship most of their work through Claude Code. It's a secret. 🤫&lt;/p&gt;

&lt;p&gt;Zechner was right that these agents need way less scaffolding than we thought. Anthropic deleting 80% of its own system prompt basically admitted it. But being right and being the tool everyone opens every day are two different things, and Claude Code still owns the second one for me.&lt;/p&gt;

&lt;p&gt;So use Claude Code for the day job. Keep Pi around for when you want to tinker, go local, or save some money. ✌️&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to build and deploy an MCP server to production (2026-07-28 spec)</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:13:02 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-build-and-deploy-an-mcp-server-to-production-2026-07-28-spec-50p3</link>
      <guid>https://dev.to/composiodev/how-to-build-and-deploy-an-mcp-server-to-production-2026-07-28-spec-50p3</guid>
      <description>&lt;p&gt;MCP just had its biggest release since launch.&lt;/p&gt;

&lt;p&gt;On July 28, the maintainers shipped the &lt;strong&gt;2026-07-28 spec&lt;/strong&gt;, and it changes how MCP servers work at a pretty fundamental level. The handshake is gone. Sessions are gone. Three long-standing features are deprecated.&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%2Fmsfvl2yvl7lis32ovbob.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%2Fmsfvl2yvl7lis32ovbob.png" alt="tweet on mcp" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The maintainers themselves called it the most substantial change since authorization was added. Their words, not mine.&lt;/p&gt;

&lt;p&gt;Sounds scary. But it actually makes MCP servers much easier to deploy. And what am I here for? I'm here to help you build and deploy one.&lt;/p&gt;

&lt;p&gt;Your MCP server is now just a regular stateless HTTP service. Round-robin load balancing, autoscaling, and caching all work. No sticky sessions or shared session state.&lt;/p&gt;

&lt;p&gt;In this guide, we'll build a small MCP server on the new spec, connect a client to it, see every headline feature actually running, and then deploy it to Cloudflare Workers. For free.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ All the code here uses the new &lt;strong&gt;TypeScript SDK v2&lt;/strong&gt;, released alongside the spec. If you're on the old &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; package, that's v1 now.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;ul&gt;
&lt;li&gt;What actually changed in the 2026-07-28 spec in short&lt;/li&gt;
&lt;li&gt;Building an MCP server with the new SDK v2&lt;/li&gt;
&lt;li&gt;Stateless core in action&lt;/li&gt;
&lt;li&gt;MRTR: how a tool requests user confirmation without holding a stream open&lt;/li&gt;
&lt;li&gt;A graceful fallback for clients that don't speak MRTR yet (there are many)&lt;/li&gt;
&lt;li&gt;Cacheable tool lists with &lt;code&gt;ttlMs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Testing it with a client and raw curl&lt;/li&gt;
&lt;li&gt;Deploying it to Cloudflare Workers on the free plan&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Changes in the new MCP Spec (2026-07-28)
&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%2Fujro30vnaf3p3byd4d7y.webp" 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%2Fujro30vnaf3p3byd4d7y.webp" alt="mcp new workflow" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick rundown of what's new. If you want the full changelog, it's on the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;official spec site&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The handshake is gone
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;initialized&lt;/code&gt; exchange and the Mcp-Session-Id header are officially retired.&lt;/p&gt;

&lt;p&gt;Every request is now &lt;strong&gt;self-describing&lt;/strong&gt;. It carries its own protocol version, client identity, and capabilities in &lt;code&gt;_meta&lt;/code&gt;. Any request can land on any server instance behind a plain load balancer. Such a relief!!&lt;/p&gt;

&lt;p&gt;There's an optional &lt;code&gt;server/discover&lt;/code&gt; RPC if a client wants capabilities up front. But it's optional. One bare POST is a complete conversation now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi Round-Trip Requests (MRTR)
&lt;/h3&gt;

&lt;p&gt;This one is my favorite.&lt;/p&gt;

&lt;p&gt;Before, if a tool needed something from the user mid-call, such as confirmation or a missing parameter, the server had to push an &lt;code&gt;elicitation/create&lt;/code&gt; request back over a held-open stream. That meant you needed a held-open stream, which was bad for stateless deployments.&lt;/p&gt;

&lt;p&gt;MRTR flips it. The server returns &lt;code&gt;resultType: "input_required"&lt;/code&gt; with the questions it needs answered, and closes the connection. The client collects the answers and retries the original call with them attached, plus an opaque &lt;code&gt;requestState&lt;/code&gt; token so the server knows where it left off.&lt;/p&gt;

&lt;p&gt;No open streams. No sessions. Interactive tools on fully stateless infra.&lt;/p&gt;

&lt;h3&gt;
  
  
  Header-based routing
&lt;/h3&gt;

&lt;p&gt;Requests now carry &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; HTTP headers. Your gateway, rate limiter, or WAF can route and meter on headers without parsing JSON bodies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cacheable list results
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;prompts/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt;, and &lt;code&gt;resources/read&lt;/code&gt; responses now carry &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; fields, modeled on HTTP's Cache-Control. Clients cache your tool catalog instead of re-fetching it every time they connect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensions framework + deprecations
&lt;/h3&gt;

&lt;p&gt;Tasks moved out of the experimental core into an official extension (&lt;code&gt;io.modelcontextprotocol/tasks&lt;/code&gt;). MCP Apps and Enterprise Managed Authorization live there too. You can build your own extensions as well.&lt;/p&gt;

&lt;p&gt;And the deprecations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Roots, Sampling, and Logging&lt;/strong&gt; are deprecated. They keep working for at least 12 months, but new implementations shouldn't use them.&lt;/li&gt;
&lt;li&gt;The legacy &lt;strong&gt;HTTP+SSE transport&lt;/strong&gt; is deprecated with a year-long offramp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Client Registration&lt;/strong&gt; is deprecated in favor of Client ID Metadata Documents (CIMD).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a formal deprecation policy now: a 12-month minimum window for anything marked deprecated. So you get to plan upgrades, which is noicee!&lt;/p&gt;

&lt;h3&gt;
  
  
  The SDK Split
&lt;/h3&gt;

&lt;p&gt;One more thing before we build: the TypeScript SDK is no longer one package.&lt;/p&gt;

&lt;p&gt;v2 splits it into &lt;code&gt;@modelcontextprotocol/server&lt;/code&gt;, &lt;code&gt;@modelcontextprotocol/client&lt;/code&gt;, and thin framework adapters (&lt;code&gt;@modelcontextprotocol/hono&lt;/code&gt;, &lt;code&gt;express&lt;/code&gt;, &lt;code&gt;fastify&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Building an MCP Server
&lt;/h2&gt;

&lt;p&gt;Finally, we're onto the build. We will build a quick tiny deploy bot over MCP.&lt;/p&gt;

&lt;p&gt;It has three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deploy&lt;/code&gt; asks the user for confirmation before deploying (MRTR in action).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_deployments&lt;/code&gt; reads back the deployment history&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server_stats&lt;/code&gt; proves a fresh server instance handled every request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the trick that pays off at deploy time: all the MCP logic lives in &lt;strong&gt;one platform-neutral file&lt;/strong&gt; (&lt;code&gt;bot.ts&lt;/code&gt;), and each platform gets a tiny entry file. Node gets &lt;code&gt;server.ts&lt;/code&gt;. Cloudflare gets &lt;code&gt;worker.ts&lt;/code&gt;. Both are about ten lines. An MCP server on the new spec is just a fetch handler; the platform is a serving shim.&lt;/p&gt;

&lt;p&gt;You'll understand everything along the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the SDK v2
&lt;/h3&gt;

&lt;p&gt;Run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;updated-mcp-spec-bot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;updated-mcp-spec-bot
npm init &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm &lt;span class="nb"&gt;install&lt;/span&gt; @modelcontextprotocol/server @modelcontextprotocol/client &lt;span class="se"&gt;\&lt;/span&gt;
  @modelcontextprotocol/hono @hono/node-server hono zod tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;ℹ️ On TypeScript 6+, add &lt;code&gt;"types": ["node"]&lt;/code&gt; to your tsconfig &lt;code&gt;compilerOptions&lt;/code&gt; after installing &lt;code&gt;@types/node&lt;/code&gt;. TS 6 no longer auto-includes &lt;code&gt;@types/*&lt;/code&gt;, and you'll get &lt;code&gt;Cannot find name 'process'&lt;/code&gt; errors without it. Ask me how I know. 😴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 2: The server logic
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;bot.ts&lt;/code&gt;. This is the whole MCP server, with zero platform code in it:&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="c1"&gt;// 👇 bot.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;InputRequiredResult&lt;/span&gt;&lt;span class="p"&gt;,&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;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;acceptedContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CLIENT_CAPABILITIES_META_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;,&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;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod/v4&lt;/span&gt;&lt;span class="dl"&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;deployments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;env&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;at&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestsServed&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DeployState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;env&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="c1"&gt;// set STATE_KEY in production so all instances share the secret&lt;/span&gt;
&lt;span class="c1"&gt;// lazy init: Workers forbids generating random values at module scope&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stateCodec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;codec&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;STATE_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;codec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="na"&gt;ttlSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&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;codec&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;CONFIRM_SCHEMA&lt;/span&gt; &lt;span class="o"&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="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&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="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;required&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;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// runs per request, keep it cheap&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;requestsServed&lt;/span&gt;&lt;span class="o"&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;server&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;McpServer&lt;/span&gt;&lt;span class="p"&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="s2"&gt;updated-mcp-spec-bot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&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="na"&gt;cacheHints&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;tools/list&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="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&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="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;requestState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;a&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;stateCodec&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;a&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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_deployments&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="na"&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;List deployments&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List all deployments recorded by this server.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &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="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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="na"&gt;content&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deployments&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="nx"&gt;deployments&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;d&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&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="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No deployments yet.&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;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_stats&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="na"&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;Server stats&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How many requests this process served, each on a fresh server instance.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &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="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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="na"&gt;content&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`pid=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; requestsServed=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestsServed&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="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="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploy&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="na"&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;Deploy&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deploy to an environment. Requires confirmation: interactive clients get a prompt, others must pass confirm: true.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&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;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;env&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;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;staging&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;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Target environment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;confirm&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;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set true to confirm, only after asking the user&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;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confirm&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;CallToolResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;InputRequiredResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;envelope&lt;/span&gt; &lt;span class="k"&gt;as&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="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;
          &lt;span class="nx"&gt;CLIENT_CAPABILITIES_META_KEY&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClientCapabilities&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;canElicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elicitation&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;elicitation&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;canElicit&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&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;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;acceptedContent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputResponses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;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;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;inputRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deploy to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;? This will go live.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;requestedSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CONFIRM_SCHEMA&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="na"&gt;requestState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stateCodec&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&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;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&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;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nx"&gt;deployments&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;record&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="na"&gt;content&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deployed to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&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="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// fallback for clients without elicitation support&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;confirm&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deploy to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; needs confirmation. Ask the user, then call deploy again with confirm: 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;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;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;deployments&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;record&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="na"&gt;content&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="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="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deployed to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&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="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;return&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few things worth explaining here:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildServer()&lt;/code&gt; runs on every single request. Not once at startup. Every request gets a brand-new &lt;code&gt;McpServer&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;If that surprises you, I get it. It surprised me too. But this is literally the canonical pattern from the SDK's own examples, and it's the whole point of the release.&lt;/p&gt;

&lt;p&gt;Construction is just object creation and a handler map, microseconds of work. There's no protocol state to preserve anymore, so there's nothing to keep alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-request server construction, per-process resources&lt;/strong&gt;. App state (our deployments array, the state codec, your DB pool in real life) lives at module level. The server instance is disposable.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;deploy&lt;/code&gt; tool never blocks. When it needs confirmation, it returns &lt;code&gt;inputRequired(...)&lt;/code&gt; and the request is over. Done. Connection closed. The &lt;code&gt;requestState&lt;/code&gt; token is the only thing that survives between rounds, and it round-trips through the client.&lt;/p&gt;

&lt;p&gt;This means the client could tamper with it. That's why we seal it with &lt;code&gt;createRequestStateCodec&lt;/code&gt;, so tampered or expired state gets rejected with a wire-level error before our handler even runs.&lt;/p&gt;

&lt;p&gt;Notice the codec is &lt;strong&gt;lazily created&lt;/strong&gt; on first use instead of at module level. That looks like a pointless indirection on Node. It's not. Cloudflare Workers forbids generating random values in global scope, and this exact line is what lets the same file run on both platforms. Same story with the &lt;code&gt;globalThis.process?.&lt;/code&gt; guards: Workers has no &lt;code&gt;process&lt;/code&gt; global by default.&lt;/p&gt;

&lt;p&gt;So the tool reads the client's declared capabilities from the per-request envelope (that's the &lt;code&gt;CLIENT_CAPABILITIES_META_KEY&lt;/code&gt; lookup, with a legacy-connection fallback) and if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client supports elicitation then the full MRTR confirmation flow&lt;/li&gt;
&lt;li&gt;Client doesn't then the tool accepts an optional &lt;code&gt;confirm: true&lt;/code&gt; argument, and without it, it returns a plain instruction: "Ask the user, then call deploy again with confirm: true"&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 3: The Node entry
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;server.ts&lt;/code&gt;. This is everything Node-specific:&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="c1"&gt;// 👇 server.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&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;@hono/node-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHonoApp&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;@modelcontextprotocol/hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHandler&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;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildServer&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;./bot.js&lt;/span&gt;&lt;span class="dl"&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;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// in production set ALLOWED_HOSTS to your public domain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedHosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&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;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&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;h&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHonoApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedHosts&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;allowedHosts&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="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&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;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="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&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;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;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;3000&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;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;},&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`updated-mcp-spec-bot listening on &amp;lt;http://$&amp;gt;{hostname}:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp`&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's it. &lt;code&gt;createMcpHandler&lt;/code&gt; gives you a standard fetch-style handler, and Hono is just routing. &lt;code&gt;createMcpHonoApp()&lt;/code&gt; validates Host/Origin headers (DNS rebinding protection) and only allows localhost out of the box, so the &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; env var is there for when this runs behind a real domain.&lt;/p&gt;

&lt;p&gt;Everything is env-driven (&lt;code&gt;PORT&lt;/code&gt;, &lt;code&gt;HOST&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;, &lt;code&gt;STATE_KEY&lt;/code&gt;) because that's what a VM or a PaaS like Railway wants. We won't use this file for the Cloudflare deploy, but it's your path if you'd rather run this on Node anywhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: The client
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;client.ts&lt;/code&gt;:&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="c1"&gt;// 👇 client.ts&lt;/span&gt;

&lt;span class="k"&gt;import&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="nx"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;,&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;@modelcontextprotocol/client&lt;/span&gt;&lt;span class="dl"&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;http://127.0.0.1:3000/mcp&amp;gt;&lt;/span&gt;&lt;span class="dl"&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;client&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;Client&lt;/span&gt;&lt;span class="p"&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="s2"&gt;mcp-demooo-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&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="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elicitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;form&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="na"&gt;versionNegotiation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// use 2026-07-28 when the server does&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The elicitation handler: in a real app this renders a confirm dialog.&lt;/span&gt;
&lt;span class="c1"&gt;// Here we auto-accept and log what the server asked.&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;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elicitation/create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;params&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n[elicitation] server asks: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" -&amp;gt; answering yes`&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="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&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;span class="k"&gt;await&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;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`connected, negotiated protocol: &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;getNegotiatedProtocolVersion&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="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listTools&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;ttlMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheScope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttlMs&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;cacheScope&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`tools/list: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&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;t&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`cache hints: ttlMs=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cacheScope=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cacheScope&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="k"&gt;await&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;listTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second listTools served from cache&lt;/span&gt;&lt;span class="dl"&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;before&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&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="s2"&gt;list_deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`list_deployments: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;before&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&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="nx"&gt;text&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="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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&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="s2"&gt;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`deploy: &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;content&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="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&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="nx"&gt;text&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;stats&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&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="s2"&gt;server_stats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`server_stats: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;stats&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&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="nx"&gt;text&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;after&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&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="s2"&gt;list_deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`list_deployments: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;after&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&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="nx"&gt;text&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="k"&gt;await&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;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;⚠️ Don't miss &lt;code&gt;versionNegotiation: { mode: 'auto' }&lt;/code&gt;. Without it, the client negotiates the legacy 2025-11-25 protocol and the MRTR flow fails. This took me half an hour to debug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice the elicitation handler is a completely normal &lt;code&gt;elicitation/create&lt;/code&gt; handler, the same one you'd write for the old flow. The SDK's auto-fulfillment engine routes the embedded MRTR request through it and retries the tool call for you. Your code doesn't even see the round trip.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Run it
&lt;/h3&gt;

&lt;p&gt;In two terminals (better with &lt;a href="https://github.com/tmux/tmux/wiki" rel="noopener noreferrer"&gt;tmux&lt;/a&gt;), run the following:&lt;/p&gt;

&lt;p&gt;In the first terminal:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And in the other:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the kinda output you'd get:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connected, negotiated protocol: 2026-07-28

tools/list: list_deployments, server_stats, deploy
cache hints: ttlMs=30000 cacheScope=public

second listTools served from cache
list_deployments: No deployments yet.

[elicitation] server asks: "Deploy to prod? This will go live." -&amp;gt; answering yes
deploy: Deployed to prod at 2026-08-01T08:02:45.601Z

server_stats: pid=159984 requestsServed=6
list_deployments: prod @ 2026-08-01T08:02:45.601Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every line here demonstrates a spec feature, and I designed it that way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2026-07-28&lt;/code&gt;: we're on the new protocol, not the legacy fallback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ttlMs=30000&lt;/code&gt; + &lt;code&gt;served from cache&lt;/code&gt;: the second &lt;code&gt;listTools()&lt;/code&gt; never touched the network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The elicitation line, then the deploy&lt;/strong&gt;: that was two &lt;code&gt;tools/call&lt;/code&gt; POSTs. First one returned &lt;code&gt;input_required&lt;/code&gt; and closed. Second had the answer plus the sealed &lt;code&gt;requestState&lt;/code&gt;. No stream was ever held open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requestsServed=6&lt;/code&gt;: six requests, six fresh server instances, one process. Under a load balancer, those six could've hit six different machines. How cool is that?&lt;/li&gt;
&lt;li&gt;The final &lt;code&gt;list_deployments&lt;/code&gt;: app state survived even though protocol state didn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the math is here: 4 tool calls, plus 2 &lt;code&gt;listTools()&lt;/code&gt; where only 1 hit the wire, plus 1 extra round for the MRTR retry = 6 server builds.&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%2Foopkg0th0lfdarmt41rx.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%2Foopkg0th0lfdarmt41rx.png" alt="tool call output" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Look at the raw wire
&lt;/h3&gt;

&lt;p&gt;Let's see the "no handshake" thing. One bare curl, with no initialization:&lt;/p&gt;

&lt;p&gt;Run the following command:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3000/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"MCP-Protocol-Version: 2026-07-28"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Mcp-Method: tools/call"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Mcp-Name: list_deployments"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_deployments","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;btw, this curl command was suggested by Claude.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the result you get back:&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;"result"&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;"content"&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;"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;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod @ 2026-08-01T08:02:45.601Z"&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;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"_meta"&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;"io.modelcontextprotocol/serverInfo"&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;"updated-mcp-spec-bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&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;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;Two things worth noticing in that response.&lt;/p&gt;

&lt;p&gt;The result already shows the prod deployment, because I ran this curl against the same server process the client just deployed through.&lt;/p&gt;

&lt;p&gt;A totally separate client, no handshake, no session, and it reads the record the TypeScript client wrote. App state persists, protocol state doesn't.&lt;/p&gt;

&lt;p&gt;Look at those headers. &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; are right there for your gateway to route on. And the &lt;code&gt;_meta&lt;/code&gt; makes the request fully self-describing.&lt;/p&gt;

&lt;p&gt;The DX here is genuinely good.&lt;/p&gt;


&lt;h2&gt;
  
  
  Deploying to Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;We're deploying this to Cloudflare Workers, and it costs nothing: the free plan gives you 100,000 requests a day and a &lt;code&gt;*.workers.dev&lt;/code&gt; subdomain, no credit card needed.&lt;/p&gt;

&lt;p&gt;Why Workers? Because it's the natural way for a stateless MCP server. &lt;code&gt;createMcpHandler&lt;/code&gt; returns a fetch-style handler, and fetch handlers are literally what Workers runs. The entire platform difference fits in one tiny file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Cloudflare has quick-start MCP templates (&lt;code&gt;npm create cloudflare -- --template=cloudflare/ai/demos/remote-mcp-authless&lt;/code&gt;). As of writing, Cloudflare's own docs warn that these still scaffold the deprecated &lt;code&gt;McpAgent&lt;/code&gt; path and say, "Do not use that path for a new server." It's the old stateful world, and it doesn't speak 2026-07-28. Skip the template. &lt;code&gt;createMcpHandler&lt;/code&gt; is the recommended path, and it's what we're already using.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 1: The Worker entry
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;worker.ts&lt;/code&gt;:&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="c1"&gt;// 👇 worker.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&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;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHandler&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;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildServer&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;./bot.js&lt;/span&gt;&lt;span class="dl"&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;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildServer&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;app&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;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&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;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="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&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;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;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Eleven lines. Same &lt;code&gt;buildServer&lt;/code&gt;, same tools, same MRTR flow.&lt;/p&gt;

&lt;p&gt;Two deliberate differences from the Node entry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain &lt;code&gt;new Hono()&lt;/code&gt; instead of &lt;code&gt;createMcpHonoApp()&lt;/code&gt;. The Host validation in &lt;code&gt;createMcpHonoApp&lt;/code&gt; is DNS rebinding protection &lt;em&gt;for&lt;/em&gt; localhost &lt;em&gt;servers&lt;/em&gt;. Behind Cloudflare's edge, it just gets in the way.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;serve(...)&lt;/code&gt;. Workers calls your exported fetch handler itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: The wrangler config
&lt;/h3&gt;

&lt;p&gt;Wrangler is Cloudflare's CLI for Workers. It bundles your TypeScript (no build step needed), runs it locally on the real production runtime, manages secrets, and deploys.&lt;br&gt;
&lt;/p&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; &lt;span class="nt"&gt;-D&lt;/span&gt; wrangler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create &lt;code&gt;wrangler.jsonc&lt;/code&gt;:&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;"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;"updated-mcp-spec-bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"worker.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-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;"compatibility_flags"&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;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;nodejs_compat&lt;/code&gt; flag fills in Node-ish globals so npm packages behave.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Test on the real runtime, locally
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This runs &lt;code&gt;worker.ts&lt;/code&gt; on &lt;strong&gt;workerd&lt;/strong&gt;, the same engine that runs in Cloudflare production, at &lt;code&gt;http://localhost:8787&lt;/code&gt;. Point the client at it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8787/mcp npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same full output as the Node run: &lt;code&gt;2026-07-28&lt;/code&gt; negotiated, cache hints, the MRTR deploy round-trip. Except one line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;server_stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pid=1 requestsServed=6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;pid=1&lt;/code&gt;. That's the edge runtime saying hello. 🫡&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Deploy it
&lt;/h3&gt;

&lt;p&gt;Create a free account at &lt;a href="https://dash.cloudflare.com/sign-up" rel="noopener noreferrer"&gt;dash.cloudflare.com/sign-up&lt;/a&gt; if you don't have one, then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Set the production &lt;code&gt;requestState&lt;/code&gt; secret (this is the shared HMAC key, so every edge instance can verify tokens minted by any other):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32          &lt;span class="c"&gt;# copy the output&lt;/span&gt;
npx wrangler secret put STATE_KEY   &lt;span class="c"&gt;# paste it when prompted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F0ztb609ovf6yanabtixu.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%2F0ztb609ovf6yanabtixu.png" alt="wrangler setup" width="797" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ One gotcha from my own run: the key must be at least 32 bytes or the codec throws at startup. &lt;code&gt;openssl rand -hex 32&lt;/code&gt; gives you 64 hex characters, which is plenty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And ship it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First deploy asks you to pick your free &lt;code&gt;workers.dev&lt;/code&gt; subdomain. Ten seconds later:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://updated-mcp-spec-2026.&amp;lt;your-subdomain&amp;gt;.workers.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your MCP server is live on Cloudflare's global edge.&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%2Fxcja7s8fxhktca8wy70o.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%2Fxcja7s8fxhktca8wy70o.png" alt="mcp server in cloudflare" width="798" height="262"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Verify from the outside
&lt;/h3&gt;

&lt;p&gt;Run the Step 6 curl against the public URL (just swap the host), hit &lt;code&gt;/healthz&lt;/code&gt; in a browser, and then the real proof:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://updated-mcp-spec-2026.&amp;lt;your-subdomain&amp;gt;.workers.dev/mcp npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same output. Except now it's on the internet.&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%2Fzdjl92s674r1jubmq1fn.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%2Fzdjl92s674r1jubmq1fn.png" alt="remote mcp server" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect a real agent to it, with no tunnel and no ngrok:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http updated-mcp-spec-2026 &lt;span class="se"&gt;\&lt;/span&gt;
  https://updated-mcp-spec-bot.&amp;lt;your-subdomain&amp;gt;.workers.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run &lt;code&gt;/mcp&lt;/code&gt; in a Claude Code session to see it connected, then ask it to "deploy to staging". Since Claude Code doesn't declare the elicitation capability yet, our capability-aware fallback kicks in: the tool tells the agent to confirm with you first, you say yes in chat, and the deploy lands.&lt;/p&gt;

&lt;p&gt;Bonus: run &lt;code&gt;npx wrangler tail&lt;/code&gt; while you do it and watch the requests land in your production logs live.&lt;/p&gt;

&lt;p&gt;Here’s a small demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/9xQ5RmU-Hrc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;h3&gt;
  
  
  One caveat
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;deployments&lt;/code&gt; array lives in memory, and on Workers, memory is extra ephemeral: isolates spin up and down per location, so two requests might see different histories. That's not a bug in the demo; it's the whole lesson of the spec, one more time. Protocol state is gone by design, and app state belongs in real storage. On Cloudflare, that's KV, D1, or Durable Objects.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where Composio fits
&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%2F5wnytjebt7lcoblnckp8.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%2F5wnytjebt7lcoblnckp8.png" alt="Composio" width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we just built is one server with three tools. Real agents need Gmail, Slack, Notion, GitHub, Linear, and fifty other things.&lt;/p&gt;

&lt;p&gt;You could build and deploy a similar server for every one of those. Handle each app's OAuth. Keep up with every API change. Run all that infra.&lt;/p&gt;

&lt;p&gt;Or you point your agent at Composio, which gives you 1000+ apps behind a single MCP endpoint:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Build custom MCP servers (like the one we built) for your own domain logic, and let Composio be the app layer for everything else.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The 2026-07-28 spec is a breaking release, and it's the good kind of breaking.&lt;/p&gt;

&lt;p&gt;MCP servers are now boring HTTP services. Deploy them like you deploy everything else: stateless, load-balanced, cacheable, autoscaled. The handshake is gone, sessions are gone, and interactive tools work anyway thanks to MRTR.&lt;/p&gt;

&lt;p&gt;If you're starting a new server today: use SDK v2, use the &lt;code&gt;createMcpHandler(buildServer)&lt;/code&gt; factory pattern, keep resources at module level, seal your &lt;code&gt;requestState&lt;/code&gt;, and split your logic from your platform entry. We went from localhost to Cloudflare's global edge with an eleven-line file, and the same split works for Railway, Render, Fly, or a plain VM through &lt;code&gt;server.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have existing servers: you've got a 12-month window on everything deprecated. Use it.&lt;/p&gt;

&lt;p&gt;Now your MCP server finally gets to be just another web service. 🤌&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%2Fr11gknb9xy4kfo0eq31y.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr11gknb9xy4kfo0eq31y.gif" alt="noiceee" width="450" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ You can find the entire source code in &lt;a href="https://github.com/shricodev/updated-2026-july-mcp-spec-tut" rel="noopener noreferrer"&gt;the repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>mcp</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to connect MCP servers to Slackbot</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Sat, 18 Jul 2026 11:57:47 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-connect-mcp-servers-to-slackbot-1al4</link>
      <guid>https://dev.to/composiodev/how-to-connect-mcp-servers-to-slackbot-1al4</guid>
      <description>&lt;p&gt;Slackbot recently added support for MCP, which means you can now connect it with external apps and let it take actions across your work tools directly from Slack&lt;/p&gt;

&lt;p&gt;But the native app list is still limited. By the time of writing this post, there's just about &lt;strong&gt;20 apps&lt;/strong&gt; that you can connect from the &lt;a href="https://slack.com/marketplace" rel="noopener noreferrer"&gt;Slack marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And for most teams, that's not enough.&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%2Fqlyug2z5afeztlhxrwtd.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlyug2z5afeztlhxrwtd.gif" alt="not enough gif" width="480" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But luckily, Slack allows you to set up or use your custom MCP servers and not have to be limited by the number of apps available in marketplace.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://composio.dev/" rel="noopener noreferrer"&gt;Composio&lt;/a&gt; helps you. It can connect your slack bots to &lt;strong&gt;1000+ apps&lt;/strong&gt; that you can use.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll go through how to connect Slackbot with Composio’s MCP server in 3 steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The steps will be pretty much the same with other MCP servers as well.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;Once this is set up, you can ask Slackbot things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find my latest unread Gmail emails.
Search my Notion workspace for launch notes.
Check my Google Calendar for meetings tomorrow.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And a bunch more. Imagine all the stuff you can do with 1000+ apps. 😵‍💫&lt;/p&gt;

&lt;p&gt;I'll leave the rest to your imagination...&lt;/p&gt;

&lt;p&gt;Slackbot sends the request to Composio Connect, Composio finds the right tool, asks you to connect the app if needed (one time), and then executes the action.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Slack workspace with Slackbot MCP client access (comes with Business+ and Enterprise plan)&lt;/li&gt;
&lt;li&gt;Permission to create or configure a Slack app.&lt;/li&gt;
&lt;li&gt;A Composio account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1: Get the Composio Connect MCP URL
&lt;/h2&gt;

&lt;p&gt;First, you need the MCP server URL from Composio.&lt;/p&gt;

&lt;p&gt;For this setup, use Composio Connect:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is Composio’s hosted MCP server that gives your AI agent access to 1,000+ apps with just &lt;strong&gt;7 meta-tools&lt;/strong&gt; that let the slackbot discover what's available, authorize apps on demand, and execute tools across apps in parallel through a single connection.&lt;/p&gt;

&lt;p&gt;You don’t need to create a custom MCP server for this guide.&lt;/p&gt;

&lt;p&gt;Composio also supports custom MCP servers for more scoped project-specific use cases, but those can require API-key-based auth. For Slackbot, Composio Connect is the simpler path because it works with OAuth-based MCP client flows.&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%2F9r6it1xz4hl4h3ifel7a.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%2F9r6it1xz4hl4h3ifel7a.png" alt="Composio Connect" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Add Composio Connect to a Slack App
&lt;/h2&gt;

&lt;p&gt;Now, we need to register the Composio MCP server inside a Slack app.&lt;/p&gt;

&lt;p&gt;Go to the Slack developer dashboard and create a new app.&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%2F5kkqe4kem5xnfmpyla6r.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%2F5kkqe4kem5xnfmpyla6r.png" alt="Slack new app creation" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the app is created:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your Slack app.&lt;/li&gt;
&lt;li&gt;In the left sidebar, go to &lt;strong&gt;Features&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;MCP Servers&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Fwvranutnruw5ja1w5vhm.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%2Fwvranutnruw5ja1w5vhm.png" alt="Slack MCP Servers button" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Get Started&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&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%2Ff2yx87053fw9upha8l28.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%2Ff2yx87053fw9upha8l28.png" alt="Slack get started button" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now fill in the MCP server details.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: Composio (or anything you wish)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href="https://connect.composio.dev/mcp" rel="noopener noreferrer"&gt;https://connect.composio.dev/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Type&lt;/strong&gt;: Dynamic Client Registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyzro2ycp7xjqhb3gsnw.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%2Fnyzro2ycp7xjqhb3gsnw.png" alt="Slack Add MCP" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the auth type, select &lt;strong&gt;Dynamic Client Registration&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is the right option for MCP servers that support OAuth discovery and client registration. Slack handles the client registration automatically, so you don’t need to manually create OAuth credentials first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, if you click the three dots and then &lt;strong&gt;Tools&lt;/strong&gt;, you should see that it currently cannot fetch the tools because the MCP server uses a dynamic connection and must be installed in your workspace first.&lt;/p&gt;

&lt;p&gt;So, now head over to the &lt;strong&gt;Install App&lt;/strong&gt; tab, and install it to the workspace you selected when creating the app.&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%2Fxwv4sjhjmjvmkiflvhyp.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%2Fxwv4sjhjmjvmkiflvhyp.png" alt="Slack Install App" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your workspace requires approval, send the app request to your admin.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Connect Composio inside Slackbot
&lt;/h2&gt;

&lt;p&gt;Once your Slack app is installed and approved, open a DM with Slackbot.&lt;/p&gt;

&lt;p&gt;Then, just type in a prompt that requires using the app, Slack will use the correct app automatically for you.&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%2Fvp5wb4l3ab4anrfqv8y9.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%2Fvp5wb4l3ab4anrfqv8y9.png" alt="Slack connecting composio" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Connect,&lt;/strong&gt; and you’ll be taken to a confirmation page.&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%2F4f5nvtnc0j1x1wkbv356.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%2F4f5nvtnc0j1x1wkbv356.png" alt="Slack connecting composio confirmation" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on Continue, and then confirm it on the Composio end.&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%2Fljriezhxmemb38w8apqz.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%2Fljriezhxmemb38w8apqz.png" alt="Composio confirmation" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything went well, you should see that your account is connected.&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%2F58g6dsw3n767nztxc4mo.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%2F58g6dsw3n767nztxc4mo.png" alt="Slack final confirmation" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, Slackbot should be able to discover Composio’s MCP tools.&lt;/p&gt;

&lt;p&gt;Start with a simpler test:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What tools are available from Composio?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once that goes through, now try an actual app action.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send a mail to x@y.com saying 'Hi, from Composio 👋 inside Slackbot'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If Gmail is not connected yet, Composio should generate an OAuth link for you to connect it. Once you approve it, the connection persists for future use. So, you don't have to repeat this step again and again.&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%2F5fb169cfpktm2xynzoe3.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%2F5fb169cfpktm2xynzoe3.png" alt="Composio connection link" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Slackbot may ask you to approve the action before it writes data to another app.&lt;/p&gt;

&lt;p&gt;That's expected. Once connected, Slackbot can use that app through Composio.&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%2Fmxlw14j7ed5fi6h0o6dk.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%2Fmxlw14j7ed5fi6h0o6dk.png" alt="Composio MCP in action" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Voilà, you've successfully connected Slackbot to Composio MCP. 🎊&lt;/p&gt;

&lt;p&gt;Here’s a quick workflow for initiating a connection and running an actual app action:&lt;/p&gt;

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


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

&lt;p&gt;Slack's own marketplace is good, and if it covers all the tools you require, you can completely stick to it.&lt;/p&gt;

&lt;p&gt;But for some of you, that's simply not enough. I hope this helps overcome that problem.&lt;/p&gt;

&lt;p&gt;So instead of jumping between different tools, you can ask Slackbot to find information, create tasks, update records, and run actions across your apps from inside Slack.&lt;/p&gt;

&lt;p&gt;This is a much-needed quality-of-life improvement for teams that already live in Slack.&lt;/p&gt;

&lt;p&gt;Slackbot gives you the interface. MCP gives you the protocol.&lt;/p&gt;

&lt;p&gt;And Composio gives you the app layer. 👌&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Opus vs Kombai in 3 Real-World Frontend AI Tests 🚀</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 02 Jun 2026 16:06:41 +0000</pubDate>
      <link>https://dev.to/shricodev/claude-opus-vs-kombai-in-3-real-world-frontend-ai-tests-13a2</link>
      <guid>https://dev.to/shricodev/claude-opus-vs-kombai-in-3-real-world-frontend-ai-tests-13a2</guid>
      <description>&lt;p&gt;Frontend automation has been getting pretty wild lately. 🫠&lt;/p&gt;

&lt;p&gt;A few months ago, this comparison would have been much easier to frame.&lt;/p&gt;

&lt;p&gt;On one side, you had the &lt;strong&gt;Claude Opus&lt;/strong&gt; lineup, one of the strongest coding model lineups available, running through tools like Claude Code with Figma MCP support.&lt;/p&gt;

&lt;p&gt;On the other side, you had &lt;strong&gt;Kombai&lt;/strong&gt;, a frontend-focused AI coding agent that was mostly known for turning Figma files, screenshots, prompts, and existing designs into clean frontend code.&lt;/p&gt;

&lt;p&gt;But Kombai has changed quite a bit since then.&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%2Fu5fj5j52sacbg5j1fgix.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%2Fu5fj5j52sacbg5j1fgix.gif" alt="spiderman meme" width="498" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not just an AI coding agent for frontend anymore. With the newer updates, especially &lt;strong&gt;Design Mode&lt;/strong&gt;, Kombai is now trying to cover a much bigger part of the frontend workflow.&lt;/p&gt;

&lt;p&gt;That changes the comparison a little.&lt;/p&gt;

&lt;p&gt;If we only compare it on code generation, we are kind of testing just one part of what Kombai does now. It can also help create UI designs, iterate on them visually, and then move those designs into code inside the same IDE workflow.&lt;/p&gt;

&lt;p&gt;I will go deeper into that later in the Kombai section, because there is quite a bit to talk about there.&lt;/p&gt;

&lt;p&gt;For this post, though, I still want to focus mainly on the part that matters most in real projects:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤔 Can it work inside an existing codebase and ship good frontend changes without breaking things?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because at the end of the day, that's the actual pain point of frontend.&lt;/p&gt;

&lt;p&gt;If you’re interested, we already compared the older version of &lt;strong&gt;Kombai&lt;/strong&gt; and &lt;strong&gt;Figma MCP&lt;/strong&gt; here: &lt;a href="https://www.freecodecamp.org/news/figma-mcp-vs-kombai-frontend-clone-comparison" rel="noopener noreferrer"&gt;Figma MCP vs Kombai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So in this post, we’ll look at what has changed with Kombai, touch on Design Mode, and then compare it with Claude Opus on 3 real open-source projects.&lt;/p&gt;

&lt;p&gt;Let’s find out which one actually feels more useful. 👇&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%2Fi9zgn89g85nez1b5u6x2.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%2Fi9zgn89g85nez1b5u6x2.gif" alt="Bobs Burger on Fire" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you just want the takeaway, here’s the quick rundown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kombai is not just a Figma-to-code tool anymore.&lt;/strong&gt; It is now more of an all-in-one design and frontend coding agent. You can generate designs, iterate on a canvas, visually edit UI, use Figma as input, work with your real codebase, and then ship great frontend code from inside your IDE.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design Mode is the biggest new change.&lt;/strong&gt; It gives Kombai an infinite canvas, Style Guides, Themes, reusable Blocks, CSS-level visual editing, and a one-click flow from design to code. It is still early, but it changes how you should think about Kombai.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For the coding tests in this post, Kombai was still clearly stronger overall.&lt;/strong&gt; In real open-source projects, it handled frontend implementation, codebase understanding, UI quality, and integration better than Claude Opus in most cases.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4.6 with Figma MCP is still totally fine&lt;/strong&gt;, especially if you already work mostly in the CLI with tools like Claude Code, Codex, or whatever else. You can absolutely stick with that setup and still get solid results. It works well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bigger point is this:&lt;/p&gt;

&lt;p&gt;If you are already comfortable in the CLI, there is no real reason to switch just for this. A good model with Figma MCP is more than enough for a lot of use cases.&lt;/p&gt;

&lt;p&gt;But if you use &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;VS Code&lt;/strong&gt;, &lt;strong&gt;Windsurf&lt;/strong&gt;, &lt;strong&gt;Trae&lt;/strong&gt;, &lt;strong&gt;Antigravity&lt;/strong&gt;, &lt;strong&gt;Kiro&lt;/strong&gt;, or another GUI IDE, you should seriously try Kombai at least once.&lt;/p&gt;

&lt;p&gt;It is just a very good tool for frontend work.&lt;/p&gt;

&lt;p&gt;I’ve been using Kombai for more than &lt;strong&gt;10 months&lt;/strong&gt;, and it still feels kind of wild sometimes. Honestly, I have &lt;strong&gt;Cursor&lt;/strong&gt; installed on my machine for Kombai alone. That’s how useful it has been for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brief on Kombai
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Kombai is an all-in-one design and frontend coding agent for building production-ready frontends&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kombai used to be easy to describe as a frontend AI agent.&lt;/p&gt;

&lt;p&gt;This is still true, but now its just a small part of what it can do.&lt;/p&gt;

&lt;p&gt;The better way to describe it today is this:&lt;/p&gt;

&lt;p&gt;It's an agent specially designed for frontend unlike usual coding agents like Claude Code, Codex or anything else.&lt;/p&gt;

&lt;p&gt;That specialization shows up in a few places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It understands frontend stacks and component libraries&lt;/li&gt;
&lt;li&gt;It can parse Figma designs natively&lt;/li&gt;
&lt;li&gt;It can work with existing components, tokens, hooks, and design systems&lt;/li&gt;
&lt;li&gt;It can visually inspect and edit the browser output&lt;/li&gt;
&lt;li&gt;It can generate UI designs before writing code&lt;/li&gt;
&lt;li&gt;It focuses more on production-ready frontend changes, and not a generic app generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest recent update is the &lt;strong&gt;Design Mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With Design Mode, Kombai can generate UI designs directly inside your IDE. You can start from a prompt, image, website, or Figma reference, and Kombai creates editable UI designs that you can change visually.&lt;/p&gt;

&lt;p&gt;Here’s a quick demo of Kombai’s Design Mode in action by &lt;a href="https://www.youtube.com/@beau" rel="noopener noreferrer"&gt;Beau Carnes&lt;/a&gt; (core team member of &lt;strong&gt;freeCodeCamp&lt;/strong&gt;).&lt;/p&gt;

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

&lt;p&gt;It also has proper design-system-ish primitives now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Style Guides&lt;/strong&gt; for controlling the overall visual direction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes&lt;/strong&gt; for reusable design tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocks&lt;/strong&gt; for reusable design elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants&lt;/strong&gt; for trying multiple design directions&lt;/li&gt;
&lt;li&gt;A CSS editor for visual tweaks&lt;/li&gt;
&lt;li&gt;and many more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These designs live as &lt;code&gt;.canvas&lt;/code&gt; files in your repo, which means they can be version-controlled with git like your code.&lt;/p&gt;

&lt;p&gt;Once you like a design, you can hit Code design, and Kombai moves it into Code Mode, reads your project, understands your stack, reuses relevant components and tokens, and codes it into working frontend.&lt;/p&gt;

&lt;p&gt;That is what makes it feel different from tools like v0, Lovable, or Bolt.&lt;/p&gt;

&lt;p&gt;Those tools are great for generating prototypes or apps from scratch. Kombai is more focused on your &lt;strong&gt;existing frontend repo&lt;/strong&gt;. It works inside your editor and tries to build the thing using the stack you already have.&lt;/p&gt;

&lt;p&gt;Check out this intro demo Kombai 2.0. The first AI design engineer. &lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2061825199247614316-192" src="https://platform.twitter.com/embed/Tweet.html?id=2061825199247614316"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2061825199247614316-192');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2061825199247614316&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;With Kombai, you can not only create designs, but also get the safety of knowing that no backend code is &lt;strong&gt;ever touched&lt;/strong&gt;, which ensures your business logic is not mistakenly changed.&lt;/p&gt;

&lt;p&gt;You can add it right inside your editor. It works with &lt;strong&gt;VSCode&lt;/strong&gt;, &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;Windsurf&lt;/strong&gt;, and &lt;strong&gt;Trae&lt;/strong&gt;. Just grab it from the extension marketplace, launch it, and you’re ready to go.&lt;/p&gt;

&lt;p&gt;With Kombai, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Generate UI designs from scratch using Design Mode.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn Figma designs into code without setting up Figma MCP separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn Figma designs into code (React, HTML, CSS, etc.) using the component library your project already uses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use screenshots, images, websites, or natural language as input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reuse existing components, hooks, design tokens, and frontend conventions from your repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Visually inspect and edit your UI through the Kombai Browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Work with a frontend-smart engine that understands 30+ libraries, including Next.js, MUI, and Chakra UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Most importantly, preview the changes in a sandbox so you can approve or reject the change before committing it to the files.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Head to the &lt;a href="https://docs.kombai.com/get-started/welcome" rel="noopener noreferrer"&gt;docs&lt;/a&gt; to get started and find the setup for your editor.&lt;/p&gt;

&lt;p&gt;You can be up and running in under a minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the extension for your editor&lt;/li&gt;
&lt;li&gt;Sign in and connect your project&lt;/li&gt;
&lt;li&gt;Pick the mode you need: Code, Plan, Design, etc.&lt;/li&gt;
&lt;li&gt;Paste a Figma link or describe what you want to build&lt;/li&gt;
&lt;li&gt;Paste a Figma link, describe what you want, attach an image, or work from your existing code&lt;/li&gt;
&lt;li&gt;Review the output and commit your code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spend most of your time on the frontend, this is a &lt;strong&gt;no-brainer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now comes with 100 award winning landing pages that you can make your own.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2061825201730695550-647" src="https://platform.twitter.com/embed/Tweet.html?id=2061825201730695550"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2061825201730695550-647');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2061825201730695550&amp;amp;theme=dark"
  }



&lt;/p&gt;




&lt;h2&gt;
  
  
  One Important Note Before the Tests
&lt;/h2&gt;

&lt;p&gt;Kombai now has a much bigger features and design support than it did before.&lt;/p&gt;

&lt;p&gt;But this post is still mainly a frontend coding and implementation comparison.&lt;/p&gt;

&lt;p&gt;So, I am not going to deeply test Design Mode here. That deserves its own separate post because the right comparison there would probably be against tools like v0, Lovable, Bolt, and maybe Figma-based workflows.&lt;/p&gt;

&lt;p&gt;For this post, the test is still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can it understand a real codebase?&lt;/li&gt;
&lt;li&gt;Can it preserve functionality?&lt;/li&gt;
&lt;li&gt;Can it implement a feature cleanly?&lt;/li&gt;
&lt;li&gt;Can it match or improve UI quality?&lt;/li&gt;
&lt;li&gt;Importantly, can it work in a large real-world existing project?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly what we are going to test here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Workflow
&lt;/h2&gt;

&lt;p&gt;In this model test, I’ll be using &lt;strong&gt;Claude Opus 4.6&lt;/strong&gt; with everyone’s favorite CLI coding agent, &lt;strong&gt;Claude Code&lt;/strong&gt;, along with Figma MCP support.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Just in case you're interested in how to add MCP support to Claude Code, you can view the guide &lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Kombai, the choice mostly comes down to the IDE, so I’ll go with Cursor. It does not really matter much which IDE you use, though. VS Code would work just fine too.&lt;/p&gt;

&lt;p&gt;We’ll test both tools on three decently complex tasks in &lt;strong&gt;real-world open-source projects&lt;/strong&gt; with hundreds of thousands of LOC.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A frontend-heavy task with Figma&lt;/li&gt;
&lt;li&gt;A frontend + backend task with more implementation complexity&lt;/li&gt;
&lt;li&gt;A task that relies more on codebase understanding than implementation complexity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ll compare both tools on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time to complete the task&lt;/li&gt;
&lt;li&gt;quality of the code&lt;/li&gt;
&lt;li&gt;how closely the final output matches the given design or feature intent&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; I’ll share the source code changes for each task from both tools in a &lt;code&gt;.patch&lt;/code&gt; file. That way, you can easily reproduce them on your local system by cloning the repository and applying the patch with &lt;code&gt;git apply &amp;lt;path_file_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; As I’m using a Claude plan and not API usage, price is roughly estimated based on the output tokens.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Real-World Coding Comparison
&lt;/h2&gt;

&lt;p&gt;The entire test in this blog is going to be on top of real-world open-source projects that are used by thousands of people, not toy projects, but ones with &lt;strong&gt;thousands of LOC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All three tests are going to be on 3 different projects, all open-source, of course!&lt;/p&gt;

&lt;p&gt;Let's start with an easier one, a shadcn template repository with Figma MCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1: Rebuild an Open-Source Project UI (with Figma MCP)
&lt;/h2&gt;

&lt;p&gt;For this test, I'll be comparing both of them on a Figma file, giving them access to a Figma MCP server.&lt;/p&gt;

&lt;p&gt;You can find the Figma design template for this test here: &lt;a href="https://www.figma.com/design/xfzmzKLBkvvY6BPteZWukK/-FREE--Food-Delivery-Admin-Dashboard--Community-?node-id=0-1&amp;amp;p=f&amp;amp;t=GmIYunPopZ2pO4SK-0" rel="noopener noreferrer"&gt;Dashboard&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the provided Figma dashboard design in this existing Next.js + shadcn/ui dashboard codebase.

&amp;lt;figma_url&amp;gt;

Constraints:

1. Preserve all existing functionality exactly.
2. Do not break routing, state, existing interactions, or responsiveness.
3. Replace the current dashboard presentation layer with a UI that closely matches the Figma design.
4. The Figma is food-delivery themed. Translate only the content domain, not the visual system.
5. Keep the layout, structure, and styling language of the Figma as intact as possible.
6. Replace food-specific labels and data with content that fits a generic admin dashboard.
7. Reuse existing logic and data bindings wherever possible.
8. Avoid adding fake backend logic.
9. Keep the implementation production-quality and componentized.

Focus areas:

- sidebar
- header/top bar
- summary metric cards
- chart section
- table/list section
- filters/search/actions if present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/ec49249394524a94139a5af16250f2df" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus did a pretty good job here.&lt;/p&gt;

&lt;p&gt;Given the Figma itself was not even made for a generic admin dashboard in the first place, there was obviously some room where it had to improvise a bit on its own.&lt;/p&gt;

&lt;p&gt;But the main thing I was testing for here was not just whether it could make the frontend look close enough. The actual ask in the prompt was to preserve and support the existing functionalities as well. That part just was not really there. It did not add the interactivity support properly, which was the whole point of the test after all.&lt;/p&gt;

&lt;p&gt;So visually, sure, it looks alright. But if the interaction layer is missing, that is a pretty big miss for this kind of task. If it had nailed that part too, this would have been a really strong result. But it didn’t.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.125&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 9 minutes 1 second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~8K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 6 files changed, 420 insertions(+), 422 deletions(-)&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%2Faa8rfsl6zzzdn8ivt4us.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%2Faa8rfsl6zzzdn8ivt4us.png" alt="Opus 4.6 task completion" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/6b734610378fbbed6e524f137618d15f" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kombai nailed this one. Even better how accurately it finds out the tech stack.&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%2Fkld3s939tqgrvjtx43qg.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%2Fkld3s939tqgrvjtx43qg.png" alt="Kombai figuring out tech stack" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The frontend turned out really good. More importantly, the interactivity is there, which is the main thing being tested other than how close it comes to the Figma design. It actually respected the fact that this is an existing app, with behavior that still needs to work.&lt;/p&gt;

&lt;p&gt;And if I compare both the UI itself and the overall build quality from a production POV, Kombai clearly did better here.&lt;/p&gt;

&lt;p&gt;That said, there was one issue I noticed while working with Kombai.&lt;/p&gt;

&lt;p&gt;After it finishes the implementation, it has this nice default feature where it opens up a browser preview and lets you chat there to fix smaller things quickly. In theory, that sounds great. In practice, for apps that require authentication, which this one did, it falls apart. Google OAuth simply flags it as unsafe, so you cannot log in there at all.&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%2Fwj5n0kq6b23lyfgqfncy.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%2Fwj5n0kq6b23lyfgqfncy.png" alt="Kombai authentication problem in the test browser" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So yeah, they definitely need to work on that.&lt;/p&gt;

&lt;p&gt;Still, overall, the user experience and the actual result were top notch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~12 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 12 files changed, 940 insertions(+), 532 deletions(-)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I noticed the project also has standalone Kanban board support, so why not quickly test it on this as well?&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Completely refactor the Kanban board UI in this open source project based on this Figma:

&amp;lt;figma_url&amp;gt;

This should be a real UI redesign, not minor styling tweaks. Study the Figma closely and make the board feel much more polished, modern, clean, and cohesive. Improve layout, spacing, typography, hierarchy, cards, columns, controls, interaction states, and responsiveness.

Preserve functionality, but refactor components and styling where needed so the code is cleaner and the design is more consistent. Focus heavily on UI quality and make the final result feel much closer to the Figma overall.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/19c1e6f9f640975f22363f4ad9aa7346" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus did pretty well here too.&lt;/p&gt;

&lt;p&gt;The good part is that functionality was not broken. The board still works, and that matters a lot for a refactor like this. The redesign itself is also nice. It is clearly better than before.&lt;/p&gt;

&lt;p&gt;That said, if I look closely at the actual design match, it seems to miss a little here and there. It does not feel quite as locked in to the Figma as the best result should.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; negligible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 5 minutes 26 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 5 files changed, 162 insertions(+), 56 deletions(-)&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%2Fskrb2gjprh475bvt76t4.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%2Fskrb2gjprh475bvt76t4.png" alt="Ous 4.6 task completion" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/583d31c69823cb772c7a7188c4b89f73" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again, Kombai was excellent here.&lt;/p&gt;

&lt;p&gt;Honestly, this one came out awesome. It matches the Figma really well, preserves the expected behavior, and feels like a proper redesign.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 362 insertions(+), 72 deletions(-)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~6 minutes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test 2: Add a Feature in Uptime Kuma
&lt;/h2&gt;

&lt;p&gt;There's this open-source project that's super popular for the self-hosted monitoring service &lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;, with over &lt;strong&gt;84K stars&lt;/strong&gt; on GitHub.&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%2F41bm2kdpxp4q7gsrbgqt.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%2F41bm2kdpxp4q7gsrbgqt.png" alt="Uptime Kuma" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's an existing issue on the project that we will try to build: &lt;a href="https://github.com/louislam/uptime-kuma/issues/1984" rel="noopener noreferrer"&gt;Calendar Graph&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a heatmap-style uptime history section to the public status page in this existing Uptime Kuma codebase.

Constraints:

1. Keep all existing functionality working.
2. Do not break the public status page, responsiveness, or current monitor behavior.
3. Build this as a proper feature inside the existing architecture.
4. Reuse existing logic and data flow wherever possible.
5. Avoid fake backend logic or hardcoded mock data.
6. Make the heatmap work independently for each service.
7. Keep the UI consistent with Uptime Kuma’s existing style.
8. Make the implementation clean and production-ready.

Focus areas:

- status page
- per-service uptime history
- heatmap UI
- state management
- backend integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/f6e66c056cd2bb1c868e640b25f2a953" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus actually got the overall feature in place, and to be fair, the core thing does work.&lt;/p&gt;

&lt;p&gt;That said, there is a bug.&lt;/p&gt;

&lt;p&gt;When you try to change the monitoring duration for one service, it also changes it for all the others. That is obviously not how this should behave, and for a feature like this, that kind of state handling bug is a big issue, of course!&lt;/p&gt;

&lt;p&gt;Other than that, there is not a whole lot to complain about. The core functionality works, just with some caveats, and that feels fair. I do not think it is realistic to expect the model to get every detail perfectly right in one shot on a non-trivial codebase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 13 minutes 5 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~7.5K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 784 insertions(+)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/423f45e00bf9e9c65e202992c8cf2a6f" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kombai did this one properly.&lt;/p&gt;

&lt;p&gt;The feature is implemented correctly, the behavior is right, and even the bug that showed up in the Claude Opus implementation is not there at all.&lt;/p&gt;

&lt;p&gt;That part matters because this is not just about working on the UI. It's about putting the feature into an existing project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~10 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 809 insertions(+), 1 deletion(-)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test 3: Add a Feature to Chatwoot
&lt;/h2&gt;

&lt;p&gt;This is a little different from the other two tests.&lt;/p&gt;

&lt;p&gt;This was more of a test to see how well the model actually understands the codebase and less about generating or working on the UI heavily. The code change isn't going to be huge, but it tests the two on how good they are at understanding the codebase and adding a feature on top.&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%2Far87ngghworzc90e6amb.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%2Far87ngghworzc90e6amb.png" alt="Chatwoot" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a new "Participating" tab to the chat/conversation list in this existing Chatwoot codebase.

The goal is to let users quickly view conversations they are participating in, while keeping the implementation fully aligned with how Chatwoot already handles conversations, tabs, filtering, permissions, and dashboard state.

Constraints:

1. Preserve all existing functionality exactly.
2. Do not break existing conversation list behavior, routing, filters, permissions, or dashboard interactions.
3. Add "Participating" as a proper tab in the existing chat list UI, not as a separate temporary view.
4. Make sure the tab only shows conversations the current user is participating in.
5. Reuse existing backend, frontend, and store patterns wherever possible.
6. Avoid hacks, fake data, or disconnected logic.
7. Keep the implementation production-quality and consistent with the current Chatwoot UI.
8. Ensure permissions and visibility rules continue to work correctly.
9. Make the feature feel like a native part of the product.

Focus areas:

- chat list tab integration
- conversation filtering
- backend query support
- participation-based logic
- store/state updates
- correct tab placement in the UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/314fa9f6d90859d6146d49d040eecf4e" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus nailed this one.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of test where you are not making massive frontend changes, but instead need a solid understanding of how the app actually works so the feature fits naturally into the existing codebase.&lt;/p&gt;

&lt;p&gt;The only issue I noticed was some UI flickering. But that looked more like a minor issue than anything wrong with the feature implementation itself.&lt;/p&gt;

&lt;p&gt;Other than that, it worked perfectly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.02&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 12 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~1.3K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 13 files changed, 52 insertions(+), 11 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

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

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/5f52c2ae2274a734d0eed68fccfffc15" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was no flickering here, and Kombai also placed everything where it actually made sense. Claude got the logic right too, but the tab placement felt a bit off. Kombai just tied it together better, and the whole thing felt a bit cleaner.&lt;/p&gt;

&lt;p&gt;So overall, both did well here, but Kombai’s result felt cleaner and better integrated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 7 files changed, 30 insertions(+), 9 deletions(-)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~8 minutes&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%2Fif5cvarx091xh9emzb0l.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%2Fif5cvarx091xh9emzb0l.png" alt="Kombai task completion" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Verdict
&lt;/h2&gt;

&lt;p&gt;So, what’s the takeaway?&lt;/p&gt;

&lt;p&gt;After testing both on real tasks in real codebases, Kombai was clearly stronger for frontend work.&lt;/p&gt;

&lt;p&gt;That is not to say Claude Code with Opus 4.6 is bad. Far from it.&lt;/p&gt;

&lt;p&gt;It is one of the strongest coding models available right now, and it can do some serious work. In some cases, especially the Chatwoot test, it understood the codebase really well and shipped something that was genuinely solid.&lt;/p&gt;

&lt;p&gt;Claude Opus lineup is too good for general coding.&lt;/p&gt;

&lt;p&gt;Kombai is a frontend specialized tool.&lt;/p&gt;

&lt;p&gt;And for frontend-heavy work, that specialization really shows.&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%2Fym68r25yz6fowfyulu7y.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%2Fym68r25yz6fowfyulu7y.gif" alt="too good gif" width="400" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That said, I do not think you should take my word for it blindly.&lt;/p&gt;

&lt;p&gt;Also, this does not mean Kombai will win every project or every workflow.&lt;/p&gt;

&lt;p&gt;If you are doing backend-heavy work, infra changes, full-stack architecture, or mostly CLI-based development, Claude Code still makes a ton of sense.&lt;/p&gt;

&lt;p&gt;The design side is also getting more interesting now. I did not fully test Design Mode in this post, because that deserves a separate comparison.&lt;/p&gt;

&lt;p&gt;Honestly, the best way to judge tools like this is to try them yourself. A comparison like this can give you a rough idea, but it really clicks only when you use them on your own codebase and see how they actually feel.&lt;/p&gt;

&lt;p&gt;That’s all for this one. Thank you for reading! ✌️&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Code vs. OpenCode without the hype</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Thu, 21 May 2026 13:55:18 +0000</pubDate>
      <link>https://dev.to/composiodev/claude-code-vs-opencode-without-the-hype-j1f</link>
      <guid>https://dev.to/composiodev/claude-code-vs-opencode-without-the-hype-j1f</guid>
      <description>&lt;p&gt;Everyone wants a coding agent now.&lt;/p&gt;

&lt;p&gt;Not a chatbot that explains code.&lt;/p&gt;

&lt;p&gt;An actual agent that can read your repo, edit files, run commands, use tools, and keep moving while you supervise.&lt;/p&gt;

&lt;p&gt;Claude Code and OpenCode are two of the most interesting takes on that idea.&lt;/p&gt;

&lt;p&gt;Claude Code is the polished Anthropic-native route.&lt;/p&gt;

&lt;p&gt;OpenCode is the open-source route for people who want more model choice, more control, and a setup they can tweak.&lt;/p&gt;

&lt;p&gt;And that difference matters more than it looks.&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%2F5bcj3t5mypqia64m30wr.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%2F5bcj3t5mypqia64m30wr.png" alt="distracted man GIF" width="687" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Open-source coding agent with model and tool control&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k02x3jygu2mwj40jz44.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%2F2k02x3jygu2mwj40jz44.png" alt="OpenCode" width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenCode is an open-source coding agent for developers who want more control over their AI coding setup.&lt;/p&gt;

&lt;p&gt;It runs in the terminal, IDE, and desktop, and lets you bring your own model instead of &lt;strong&gt;locking you into one provider&lt;/strong&gt;. Claude, GPT, Gemini, local models, and 75+ other providers are supported. That is probably the biggest reason people care about it.&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%2Fkem0kiznc0fm4g9gyvw9.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%2Fkem0kiznc0fm4g9gyvw9.png" alt="OpenCode tweet" width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also comes with the things you expect from a serious coding agent now: LSP support, multi-session workflows, project memory through &lt;code&gt;AGENTS.md&lt;/code&gt;, MCP tools, custom agents, plugins, and editor support, and maybe a bunch more.&lt;/p&gt;

&lt;p&gt;So the pitch is not just “AI in your terminal.”&lt;/p&gt;

&lt;p&gt;That undersells it.&lt;/p&gt;

&lt;p&gt;OpenCode is closer to a &lt;strong&gt;coding-agent workbench&lt;/strong&gt;. You bring the model, the provider, the editor, the agents, and the workflow. OpenCode gives you the open layer that ties it all together.&lt;/p&gt;

&lt;p&gt;Not everyone needs that level of control.&lt;/p&gt;

&lt;p&gt;But some developers absolutely do.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 OpenCode is for developers who want to tweak every single detail of their coding agent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is what makes it interesting next to Claude Code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Claude Code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Anthropic’s polished coding agent for your terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivw9mx75nhkdloyczjr5.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%2Fivw9mx75nhkdloyczjr5.png" alt="Claude Code" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is Anthropic’s coding agent that lives in your terminal.&lt;/p&gt;

&lt;p&gt;The idea is pretty same here, it can read your codebase, edit files, run commands, handle Git stuff, and all through prompts.&lt;/p&gt;

&lt;p&gt;The big difference is that Claude Code is built around Claude.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it matters.&lt;/p&gt;

&lt;p&gt;You are not coming here to mix and match ten different model providers. You are coming here because you trust Anthropic’s models, and you want the cleanest experience around them.&lt;/p&gt;

&lt;p&gt;Claude Code also comes with a lot of serious agent features: project memory through &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands, permissions, hooks, MCP, plugins, custom subagents, and IDE integrations.&lt;/p&gt;

&lt;p&gt;Claude Code is closer to a Claude-native coding environment. The model, the agent loop, the tool use, the permissions, and the workflow all come from the same Anthropic-shaped box.&lt;/p&gt;

&lt;p&gt;Less DIY.&lt;/p&gt;

&lt;p&gt;But there is also a small shift happening.&lt;/p&gt;

&lt;p&gt;Some developers are starting to move from Claude Code to OpenCode or OpenAI’s Codex for one simple reason: &lt;strong&gt;usage limits&lt;/strong&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%2Fi4uxa5o6uxb1s9x87cgw.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%2Fi4uxa5o6uxb1s9x87cgw.png" alt="Claude Code Usage Limit meme" width="800" height="830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is great, but when you are deep in a coding session, hitting limits feels brutal. And for heavier users, even the &lt;strong&gt;$200 Claude Max plan&lt;/strong&gt; does not always feel like enough.&lt;/p&gt;

&lt;p&gt;That is why OpenCode and Codex are tempting. Also read: &lt;a href="https://composio.dev/content/claude-code-vs-openai-codex" rel="noopener noreferrer"&gt;Claude Code vs. Codex: Detailed breakdown&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When Claude hits the wall, people still need a way to keep shipping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If you're an Anthropic fanboy, and don't care about other models, stick to Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;blockquote&gt;
&lt;p&gt;ℹ️ How both the agents work&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At a high level, both OpenCode and Claude Code follow the same basic agent loop.&lt;/p&gt;

&lt;p&gt;You give it a task.&lt;/p&gt;

&lt;p&gt;It looks at the repo.&lt;/p&gt;

&lt;p&gt;It decides what files, commands, or tools it needs.&lt;/p&gt;

&lt;p&gt;It takes an action.&lt;/p&gt;

&lt;p&gt;Then it reads the result and keeps going.&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%2Fcv9tejzbqvy4whr7jcj2.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%2Fcv9tejzbqvy4whr7jcj2.png" alt="Coding Agent architecture" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is the highest-level architecture of a coding agent. A few details change from tool to tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That loop is the boring part.&lt;/p&gt;

&lt;p&gt;The interesting part is everything around it.&lt;/p&gt;

&lt;p&gt;Here is a tiny example of that loop in practice.&lt;/p&gt;

&lt;p&gt;I gave Claude Code and OpenCode the same small task in a demo word-count repo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a &lt;code&gt;--json&lt;/code&gt; flag to a word-count CLI, update the tests, and run them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The interesting part is not the feature. It is watching both agents go through the same shape: understand the repo, plan the change, edit the files, and run the tests.&lt;/p&gt;

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

&lt;p&gt;Claude Code wraps that loop in Anthropic’s own product system. You get Claude, project memory through &lt;code&gt;CLAUDE.md&lt;/code&gt;, permissions, hooks, MCP, &lt;a href="https://composio.dev/content/top-claude-code-plugins" rel="noopener noreferrer"&gt;plugins&lt;/a&gt;, &lt;a href="https://composio.dev/content/top-claude-skills" rel="noopener noreferrer"&gt;Claude skills&lt;/a&gt;, and subagents in one single setup.&lt;/p&gt;

&lt;p&gt;OpenCode takes a more open route. It gives you the agent runtime, but lets you bring different models, providers, agents, tools, and workflows. Its docs split agents into primary agents and subagents, and let you configure specialized assistants with custom prompts, models, and tool access.&lt;/p&gt;

&lt;p&gt;So architecturally, the difference is not that one is an agent and the other is not.&lt;/p&gt;

&lt;p&gt;They both are.&lt;/p&gt;

&lt;p&gt;The real difference is who controls the harness around the agent.&lt;/p&gt;

&lt;p&gt;Claude Code gives you Anthropic’s harness.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a harness you can inspect, and configure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context, memory and tool use
&lt;/h2&gt;

&lt;p&gt;Both Claude Code and OpenCode are doing the same basic thing: they build a giant prompt, stuff it with repo context, tool definitions, memory files, recent messages, and tool results, then ask the model what to do next.&lt;/p&gt;

&lt;p&gt;The difference is how much of that system you control.&lt;/p&gt;

&lt;p&gt;Claude Code is more vertically integrated here. It is built around Anthropic models, so it can take advantage of Anthropic-specific stuff like prompt caching, native tool calls, and Claude’s own long-context behavior.&lt;/p&gt;

&lt;p&gt;That matters.&lt;/p&gt;

&lt;p&gt;Tool definitions, system prompts, and &lt;code&gt;CLAUDE.md&lt;/code&gt; can be cached between turns, which makes long coding sessions cheaper and faster than they would be if Claude had to re-read everything from scratch every single time.&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%2Fphuqzyrqslhkmlnh7ah3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphuqzyrqslhkmlnh7ah3.jpg" alt="compaction in a coding agent" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenCode takes a different route.&lt;/p&gt;

&lt;p&gt;It does not assume one model or one provider. Instead, it &lt;strong&gt;reads the model’s context limit&lt;/strong&gt; from the provider metadata and builds the session around that. So the same OpenCode setup can run with Claude, GPT, Gemini, Qwen, local models, or whatever else you plug in.&lt;/p&gt;

&lt;p&gt;That flexibility is the whole point.&lt;/p&gt;

&lt;p&gt;But it also means OpenCode has to normalize all the weird provider differences: tool call IDs, cache support, model limits, and tool-calling parts.&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%2F75v9hwsvotu1qmz1k8p0.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%2F75v9hwsvotu1qmz1k8p0.png" alt="OpenCode support for multiple providers" width="799" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code gets to optimize deeply for Claude.&lt;/p&gt;

&lt;p&gt;OpenCode has to work with everyone.&lt;/p&gt;

&lt;p&gt;Memory works the same way.&lt;/p&gt;

&lt;p&gt;Claude Code uses &lt;code&gt;CLAUDE.md&lt;/code&gt; as the main project memory file. It can also load nested &lt;code&gt;CLAUDE.md&lt;/code&gt; files, user-level memory, and auto-memory. So it feels more like the agent has a built-in memory system.&lt;/p&gt;

&lt;p&gt;OpenCode uses &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is more portable. You can commit it to the repo, share it with the team, and use it as a general agent instruction file instead of something tied to one vendor. OpenCode can even fall back to &lt;code&gt;CLAUDE.md&lt;/code&gt;, which makes migration easier.&lt;/p&gt;

&lt;p&gt;At some point, every agent runs out of context.&lt;/p&gt;

&lt;p&gt;Claude Code handles this by compacting the conversation. Older tool outputs are cleared first, then the session gets summarized if needed. That is why Claude Code has commands like &lt;code&gt;/context&lt;/code&gt; and &lt;code&gt;/compact&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OpenCode is a bit more explicit. It checks whether the session is close to the model’s context limit, keeps a buffer for output, and then prunes old tool outputs before doing a full summary. The important bit is that OpenCode stores the raw history in &lt;strong&gt;SQLite&lt;/strong&gt;, so pruning does not mean the data is gone forever.&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%2F7om4vrfngpmqdjsh3phe.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%2F7om4vrfngpmqdjsh3phe.png" alt="OpenCode flexibility" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tool use follows the same pattern.&lt;/p&gt;

&lt;p&gt;Claude Code gives you a polished default toolbelt: read, write, edit, grep, glob, bash, web fetch, todo tracking, MCP, hooks, skills, and subagents.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a smaller but more configurable tool system: read, write, edit, patch, bash, grep, glob, web fetch, task, todo, &lt;a href="https://composio.dev/content/10-best-opencode-skills-that-are-actually-useful-in-2026" rel="noopener noreferrer"&gt;skills&lt;/a&gt;, MCP, custom tools, and experimental LSP support.&lt;/p&gt;

&lt;p&gt;The difference is who controls the tool layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subagents and task delegation
&lt;/h2&gt;

&lt;p&gt;Subagents are basically how coding agents avoid stuffing everything into one giant conversation.&lt;/p&gt;

&lt;p&gt;Instead of making the main agent do every task itself, it can delegate a smaller job to another agent with its own context window, prompt, tools, and permissions.&lt;/p&gt;

&lt;p&gt;Claude Code and OpenCode both follow the same basic pattern here.&lt;/p&gt;

&lt;p&gt;The parent agent calls a &lt;code&gt;Task&lt;/code&gt; or &lt;code&gt;task&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;A child agent spins up.&lt;/p&gt;

&lt;p&gt;It does the work in isolation.&lt;/p&gt;

&lt;p&gt;Then it returns one final message back to the parent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Agent
  |
  | calls Task / task
  v
Subagent
  - own context window
  - own prompt
  - own tools
  - own permissions
  |
  | returns final result only
  v
Main Agent continues...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That part is important. The parent usually does not see the full subagent conversation. It gets the result, not the whole reasoning.&lt;/p&gt;

&lt;p&gt;Claude Code has the more polished version of this.&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%2Fmgfuv1jw1x2rt22d0i57.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%2Fmgfuv1jw1x2rt22d0i57.png" alt="Claude Code approach to subagents" width="799" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It ships with built-in agents like &lt;code&gt;Explore&lt;/code&gt;, &lt;code&gt;Plan&lt;/code&gt;, and &lt;code&gt;general-purpose&lt;/code&gt;. &lt;code&gt;Explore&lt;/code&gt; is mostly read-only and useful for repo research. &lt;code&gt;Plan&lt;/code&gt; helps gather context during planning. &lt;code&gt;general-purpose&lt;/code&gt; is for broader work.&lt;/p&gt;

&lt;p&gt;You can also define custom agents in &lt;code&gt;.claude/agents/&lt;/code&gt; with YAML frontmatter for things like &lt;code&gt;tools&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;permissionMode&lt;/code&gt;, &lt;code&gt;maxTurns&lt;/code&gt;, &lt;code&gt;skills&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means you can do stuff like:&lt;/p&gt;

&lt;p&gt;Use a fast Haiku-style agent for repo search.&lt;/p&gt;

&lt;p&gt;Use a stronger model for code review.&lt;/p&gt;

&lt;p&gt;OpenCode has a similar shape, but it is more transparent.&lt;/p&gt;

&lt;p&gt;It has primary agents and subagents. Primary agents handle the main chat, while subagents are called through the &lt;code&gt;task&lt;/code&gt; tool or &lt;code&gt;@&lt;/code&gt; mentions.&lt;/p&gt;

&lt;p&gt;Custom agents can live in &lt;code&gt;.opencode/agents/*.md&lt;/code&gt; or inside &lt;code&gt;opencode.json&lt;/code&gt;, with fields like &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;steps&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, and &lt;code&gt;permission&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is that OpenCode stores subagents as real child sessions in &lt;strong&gt;SQLite&lt;/strong&gt;. So delegation is not just a hidden prompt trick. It is represented in the session model with its own messages, permissions, and snapshots.&lt;/p&gt;

&lt;p&gt;That fits OpenCode’s whole philosophy.&lt;/p&gt;

&lt;p&gt;Claude Code gives you a cleaner subagent experience.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a more inspectable one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Permissions, safety, and control
&lt;/h2&gt;

&lt;p&gt;This is where the two are very different.&lt;/p&gt;

&lt;p&gt;Claude Code is more conservative by default. It has permission modes, allow/ask/deny rules, hooks, and sandboxing around &lt;strong&gt;Bash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So you can allow boring commands like tests, deny obvious footguns like .env reads or &lt;code&gt;curl | sh&lt;/code&gt;, and ask before anything risky.&lt;/p&gt;

&lt;p&gt;The important part is that Claude Code has multiple safety layers.&lt;/p&gt;

&lt;p&gt;Permissions decide what Claude is allowed to do.&lt;/p&gt;

&lt;p&gt;Something like:&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;"permissions"&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;"allow"&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;"Bash(npm run test *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&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;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./secrets/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl *)"&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;p&gt;Hooks can intercept tool calls before or after they run and sandboxing gives Bash an OS-level boundary.&lt;/p&gt;

&lt;p&gt;OpenCode is simpler.&lt;/p&gt;

&lt;p&gt;Most of the control lives in one permission object inside &lt;code&gt;opencode.json&lt;/code&gt;. You can set rules for &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;webfetch&lt;/code&gt;, and other tools from the same place.&lt;/p&gt;

&lt;p&gt;That is clean, but OpenCode is also more permissive by default. You are expected to configure the rules yourself.&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%2Fe8i1czi8c1lgv8ujg9l8.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%2Fe8i1czi8c1lgv8ujg9l8.png" alt="OpenCode permissions" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something like:&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;"permission"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bash"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"git status *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"git push *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rm *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&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;"edit"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packages/web/src/**/*.tsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&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;p&gt;It does have some smart checks, especially for Bash. OpenCode parses shell commands with tree-sitter (the same thing you have inside NeoVim), so it can detect risky commands like &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, or paths outside the project more carefully than plain string matching.&lt;/p&gt;

&lt;p&gt;But there is no &lt;a href="https://www.anthropic.com/engineering/claude-code-sandboxing" rel="noopener noreferrer"&gt;native sandbox&lt;/a&gt; like Claude Code.&lt;/p&gt;

&lt;p&gt;The bigger OpenCode power feature is plugins. Plugins can intercept tool execution, add custom tools, and change agent behavior.&lt;/p&gt;

&lt;p&gt;That makes OpenCode way more hackable.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the Claude Code leak tells us
&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%2F7h7v5xmi6q45a33xjk2v.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%2F7h7v5xmi6q45a33xjk2v.png" alt="Claude Code leak" width="799" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interesting part of the leak is what it showed about coding agents.&lt;/p&gt;

&lt;p&gt;A lot of Claude magic is in the harness around the model: context management, tool descriptions, prompt caching, permissions, compaction, subagents, and the agent loop.&lt;/p&gt;

&lt;p&gt;OpenCode does pretty much the same. It is not trying to clone some impossible model-level feature. It is trying to build a different harness around similar idea.&lt;/p&gt;

&lt;p&gt;OpenCode’s advantage is that the harness is open, inspectable, and replaceable.&lt;/p&gt;

&lt;p&gt;Another thing that's clear is that the future is not just about better models, but the system around them.&lt;/p&gt;

&lt;p&gt;Better context control.&lt;/p&gt;

&lt;p&gt;Better tool boundaries.&lt;/p&gt;

&lt;p&gt;Better memory.&lt;/p&gt;

&lt;p&gt;Better permissions.&lt;/p&gt;

&lt;p&gt;That is why this comparison is even interesting. Claude Code and OpenCode are not just two CLIs. They are two different answers to the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❓ How much of the agent stack should be final, and how much should developers be able to control?&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  So, which should you pick?
&lt;/h2&gt;

&lt;p&gt;There is no clever answer here.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;Claude Code&lt;/strong&gt; if you want the cleanest Claude-native coding agent experience.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;OpenCode&lt;/strong&gt; if you want more control.&lt;/p&gt;

&lt;p&gt;Personally, I still love Claude Code.&lt;/p&gt;

&lt;p&gt;I really do.&lt;/p&gt;

&lt;p&gt;Anthropic models are banger, especially for coding. The problem is that the limits have started to piss me off. When you are deep in a coding session and the limit hits, it completely breaks the flow.&lt;/p&gt;

&lt;p&gt;But there is some relief now.&lt;/p&gt;

&lt;p&gt;On May 6, Anthropic announced a new compute partnership with &lt;strong&gt;SpaceX&lt;/strong&gt; and doubled Claude Code’s 5-hour limits for &lt;strong&gt;Pro&lt;/strong&gt;, &lt;strong&gt;Max&lt;/strong&gt;, &lt;strong&gt;Team&lt;/strong&gt;, and &lt;strong&gt;seat-based Enterprise users&lt;/strong&gt;. They also removed peak-time limits for Pro and Max users.&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%2Fqosm66pi1fq6pu4k76t0.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%2Fqosm66pi1fq6pu4k76t0.png" alt="Claude Code increase in usage limit" width="799" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That makes Claude Code a lot easier to recommend again.&lt;/p&gt;

&lt;p&gt;I am personally still mostly stuck with Claude Code because the experience is just that good.&lt;/p&gt;

&lt;p&gt;But I use OpenCode when I want to try newer models like Kimi, OpenAI models, or local models. That is where OpenCode makes more sense to me. And by no means, it is to say that you can't use Anthropic models in OpenCode, you can, and that makes it even better.&lt;/p&gt;


&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Claude Code and OpenCode are both useful, but for different reasons.&lt;/p&gt;

&lt;p&gt;Claude Code is the one I’d pick if I just want the agent to work without thinking too much about setup. It feels cleaner, and better for getting into a repo quickly.&lt;/p&gt;

&lt;p&gt;OpenCode is more for when you want control. Different models, different providers, more ways to shape the workflow around how you actually code.&lt;/p&gt;

&lt;p&gt;I wouldn’t overthink it.&lt;/p&gt;

&lt;p&gt;If you hate setup and love Anthropic, use Claude Code.&lt;/p&gt;

&lt;p&gt;If you want more flexibility and less vendor lock-in, use OpenCode.&lt;/p&gt;

&lt;p&gt;That’s really the whole comparison.&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%2F4d09dor9q6cwva8zlg5o.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%2F4d09dor9q6cwva8zlg5o.gif" alt="steve jobs meme" width="422" height="237"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>cli</category>
    </item>
    <item>
      <title>Kimi K2.6 vs. Claude Opus 4.7 in a Weird Game Coding Test ✅</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 05 May 2026 13:04:03 +0000</pubDate>
      <link>https://dev.to/composiodev/kimi-k26-vs-claude-opus-47-in-a-weird-game-coding-test-2ck3</link>
      <guid>https://dev.to/composiodev/kimi-k26-vs-claude-opus-47-in-a-weird-game-coding-test-2ck3</guid>
      <description>&lt;p&gt;Kimi K2.6 has been getting a lot of love lately, especially from devs who want a strong coding model without paying premium model prices every time they run a big prompt.&lt;/p&gt;

&lt;p&gt;So I wanted to see how good this model actually is. But this time, I wanted to compare it with something much heavier, the developers darling &lt;strong&gt;Claude Opus 4.7&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On paper, Claude Opus 4.7 and Kimi K2.6 are very different models.&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%2F9me1tc37trrs3wur8p6o.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%2F9me1tc37trrs3wur8p6o.png" alt="Kimi K2.6 and Opus 4.7 benchmarks" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One is a premium frontier model from Anthropic. The other is Moonshot AI's much cheaper open model for coding and agentic tasks.&lt;/p&gt;

&lt;p&gt;The pricing difference is pretty wild too. Claude Opus 4.7 costs $5/M input tokens and $25/M output tokens. Kimi K2.6 is listed at $0.95/M input tokens and $4/M output tokens, with cached input going even lower at $0.16/M tokens.&lt;/p&gt;

&lt;p&gt;That is a pretty big gap.&lt;/p&gt;

&lt;p&gt;So in this article, we'll see how the cheaper model, Kimi K2.6, does against Claude Opus 4.7.&lt;/p&gt;

&lt;p&gt;For the test, I gave both models the same coding task: build a small Minetest (similar to Minecraft) bounty board with a TypeScript backend, then extend it with Google Sheets logging through Composio.&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%2F4zpwug27ph8tc9d8dti6.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%2F4zpwug27ph8tc9d8dti6.gif" alt="Angrybird interacting with a telescope" width="480" height="260"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you want the quick take, Claude Opus 4.7 clearly won this test, but it was painfully expensive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus was better at the real task.&lt;/strong&gt; The local build was cleaner, and it was the only one that got the real Google Sheets integration working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kimi did pretty well in Test 1.&lt;/strong&gt; It got the local bounty board working for way less money, but it needed more debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2 changed the whole comparison.&lt;/strong&gt; Opus was expensive, but it finished. Kimi just could not put it all together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost difference was wild though.&lt;/p&gt;

&lt;p&gt;For the first local bounty board test, Opus cost around &lt;strong&gt;$3.59&lt;/strong&gt;, while Kimi came in at around &lt;strong&gt;$0.39&lt;/strong&gt;. That is a huge gap. For the basic version, Kimi honestly did pretty well for the price.&lt;/p&gt;

&lt;p&gt;But once the task got a little more real, the gap became way more obvious.&lt;/p&gt;

&lt;p&gt;Opus got it working, even though it needed a little back and forth. The Google Sheets sync worked, and the project was modular enough that I could test the whole flow with two &lt;code&gt;curl&lt;/code&gt; requests without even opening the game.&lt;/p&gt;

&lt;p&gt;The painful part is that the Composio run alone cost &lt;strong&gt;$16&lt;/strong&gt; and took around &lt;strong&gt;28min 52sec API time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Kimi, on the other hand, burned &lt;strong&gt;135k+ tokens&lt;/strong&gt;, took around &lt;strong&gt;25 minutes&lt;/strong&gt;, cost around &lt;strong&gt;$5.03&lt;/strong&gt;, and still did not really get any closer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀 So yeah, Kimi K2.6 is a usable and interesting cheaper model. But in this test, it could not really come close to Opus 4.7 for real-world coding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Evaluation
&lt;/h2&gt;

&lt;p&gt;I treated this like a real project, not a benchmark chart. Both models got the same prompts, and I compared the results based on whether it actually worked, how clean the code was, how much debugging it needed, how long it took, and how much it cost. That last one matters a lot here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same tasks and prompts&lt;/strong&gt; for both models (Test 1: local-only bounty board, Test 2: real Composio Google Sheets sync).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same target architecture&lt;/strong&gt;: Minetest/Luanti Lua mod + TypeScript backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same success criteria&lt;/strong&gt;: &lt;code&gt;/bounty&lt;/code&gt; flow works in-game, backend APIs behave correctly, and in Test 2 the completion is appended to Google Sheets via Composio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I measured
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Functional correctness (most important):&lt;/strong&gt; Did it work end-to-end with real verification?

&lt;ul&gt;
&lt;li&gt;Local run: could a player generate, progress, and complete bounties without breaking state?&lt;/li&gt;
&lt;li&gt;Backend: did &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/api/bounty/generate&lt;/code&gt;, &lt;code&gt;/api/bounty/complete&lt;/code&gt;, and &lt;code&gt;/api/leaderboard&lt;/code&gt; return the expected shapes?&lt;/li&gt;
&lt;li&gt;Test 2: did the Google Sheets append succeed, and could I validate it from the API without needing to be in the game?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Code quality and structure:&lt;/strong&gt; modularity, clarity, and whether the repo was easy to reason about and test.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Debug burden:&lt;/strong&gt; how many follow-ups were needed, how confusing the failure modes were, and whether issues were “real bugs” vs. “misconfiguration traps.”&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Time:&lt;/strong&gt; API time and wall time for each run.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cost and token usage:&lt;/strong&gt; input, output, cache behavior, and total run cost.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Practical ergonomics:&lt;/strong&gt; whether I could validate quickly (for example, testing the full backend + Composio flow with &lt;code&gt;curl&lt;/code&gt;).&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I verified outcomes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test 1:&lt;/strong&gt; ran the backend locally, joined a local Minetest world, used &lt;code&gt;/bounty&lt;/code&gt;, and confirmed task tracking, rewards, and leaderboard persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2:&lt;/strong&gt; verified the end-to-end sync by generating and completing a bounty via the backend API, and confirming a successful Google Sheets append through Composio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scoring approach
&lt;/h3&gt;

&lt;p&gt;This was not a “unit test leaderboard” benchmark. It was a real build-and-ship check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A model “wins” when the project works with minimal intervention.&lt;/li&gt;
&lt;li&gt;A model “loses” when it cannot reach a working state in reasonable time/cost, even if parts of the code look promising.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Coding Test
&lt;/h2&gt;

&lt;p&gt;For this test, I used the following CLI coding agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4.7:&lt;/strong&gt; Claude Code, Anthropic's terminal-based agentic coding tool&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%2Fvc5bfm8a9ump0ebp5f7o.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%2Fvc5bfm8a9ump0ebp5f7o.png" alt="Claude Opus 4.7 model in CC" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kimi K2.6:&lt;/strong&gt; OpenCode via OpenRouter&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%2Fxbg1utbhx6wenvgpszln.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%2Fxbg1utbhx6wenvgpszln.png" alt="Kimi K2.6 in OpenRouter" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is a practical coding test, so both models get the same prompt. I will compare time taken, code quality, token usage, cost, and all that stuff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What are we building?
&lt;/h3&gt;

&lt;p&gt;For this test, I wanted something small enough to verify properly, but still weird enough to show how each model handles an unusual idea.&lt;/p&gt;

&lt;p&gt;So, we're building a simple Minetest/Luanti bounty board.&lt;/p&gt;

&lt;p&gt;A player can join a local world, run &lt;code&gt;/bounty&lt;/code&gt;, get a task like mining dirt or placing torches, and receive a reward after completing it.&lt;/p&gt;

&lt;p&gt;After that, the backend records the completion, logs it to Google Sheets through Composio.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I get it, the concept is a little unusual on purpose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Test Prompts
&lt;/h3&gt;

&lt;p&gt;Both models received the same prompts for each test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test 1 Prompt:&lt;/strong&gt; &lt;a href="https://gist.github.com/shricodev/a9fa4512142e74c72afc0e8b6b121384" rel="noopener noreferrer"&gt;Local Bounty Board Prompt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2 Prompt:&lt;/strong&gt; &lt;a href="https://gist.github.com/shricodev/5e8f4803f1a9af152403e246b7bcd617" rel="noopener noreferrer"&gt;Real Composio Integration Prompt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test 1: Local Bounty Board
&lt;/h3&gt;

&lt;p&gt;This first test is the basic version of the idea.&lt;/p&gt;

&lt;p&gt;No external tools, no Composio. Just the game, the backend, and the local bounty flow working properly.&lt;/p&gt;

&lt;p&gt;The goal was simple. A player runs &lt;code&gt;/bounty&lt;/code&gt;, gets a task, completes it inside the game, and the backend tracks the progress without everything falling apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Opus 4.7
&lt;/h3&gt;

&lt;p&gt;Claude Opus 4.7 handled the first test really well.&lt;/p&gt;

&lt;p&gt;The local bounty board worked end to end. It built the TypeScript backend, the Minetest/Luanti Lua mod, the command flow, progress tracking, rewards, and leaderboard persistence without needing a bunch of follow-up fixes.&lt;/p&gt;

&lt;p&gt;The file structure also felt nice, which I had specifically asked for in the prompt:&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%2F1ldggdwrwcrk42jgswb0.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%2F1ldggdwrwcrk42jgswb0.png" alt="Claude Opus 4.7 created file structure" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The backend was built with Express, Zod, and Vitest. It also handled the boring stuff properly, which honestly matters a lot here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; passed with &lt;strong&gt;11/11 tests&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; passed cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/api/bounty/generate&lt;/code&gt;, &lt;code&gt;/api/bounty/complete&lt;/code&gt;, and &lt;code&gt;/api/leaderboard&lt;/code&gt; returned the right response shapes&lt;/li&gt;
&lt;li&gt;incomplete bounty completions returned a clean &lt;code&gt;400&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It created the Lua files cleanly, used &lt;code&gt;minetest.request_http_api()&lt;/code&gt;, handled &lt;code&gt;secure.http_mods&lt;/code&gt;, tracked digging and placing, stored player bounty state, and handled inventory rewards properly.&lt;/p&gt;

&lt;p&gt;The whole run took around &lt;strong&gt;12 minutes of API time&lt;/strong&gt;, with about &lt;strong&gt;23 minutes wall time&lt;/strong&gt;. That is a bit longer than a quick web app build test, but for this kind of cross-stack project, it felt fair.&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%2Frwsc4lkcx6f9o43eujev.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%2Frwsc4lkcx6f9o43eujev.png" alt="Claude Opus 4.7 time taken for the build" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cost came out to &lt;strong&gt;$3.59&lt;/strong&gt;, which is definitely not cheap. But to be fair, the output was actually useful. It added a lot of code, but most of it was real implementation, not random filler like &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, &lt;code&gt;INSTALLATION.md&lt;/code&gt;, and all those extra files models sometimes create for no reason.&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%2Fgj4v5gc4wfmy9bov64qq.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%2Fgj4v5gc4wfmy9bov64qq.png" alt="Claude Opus 4.7 cost for the build" width="800" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/04fe23a6f6eb363ef7cfb6af9e0e91993568a718" rel="noopener noreferrer"&gt;Claude Opus 4.7 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$3.59&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 12min 3sec API time, 23min 53sec wall time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +1,688 lines, 0 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; 65&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; 54.8k&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache read:&lt;/strong&gt; 2.8M&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache write:&lt;/strong&gt; 129.8k&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It worked end to end without much tweaking. I only had to configure &lt;code&gt;~/.minetest/minetest.conf&lt;/code&gt; and add this line:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secure.http_mods = bountyboard
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Pretty much everything else was smooth. Great quick MVP.&lt;/p&gt;

&lt;p&gt;I noticed one small issue: &lt;code&gt;mine_node&lt;/code&gt; bounties can be farmed by placing and then re-mining the same blocks, because vanilla Minetest does not track who placed a node.&lt;/p&gt;

&lt;p&gt;But that's fine. That is not really a code issue here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Kimi K2.6
&lt;/h3&gt;

&lt;p&gt;The core idea worked. Kimi created the TypeScript backend, the Minetest/Luanti mod, the bounty commands, task tracking, completion flow, rewards, and leaderboard logic.&lt;/p&gt;

&lt;p&gt;The backend side looked solid enough. It used Express, Zod, and Vitest, and the main routes were there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/bounty/generate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/bounty/complete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/leaderboard&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also created the Lua mod files properly and handled the basic &lt;code&gt;/bounty&lt;/code&gt; flow inside Minetest. The code was not bad either. I just felt like it was not as clean or modular as what Opus 4.7 wrote.&lt;/p&gt;

&lt;p&gt;But there was one really irritating issue.&lt;/p&gt;

&lt;p&gt;Somehow, Kimi wrote the global Minetest config in &lt;code&gt;~/.minetest/minetest.conf&lt;/code&gt; with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;secure.http_mods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;bountykimi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But then it also created a world config and added a different mod name there.&lt;/p&gt;

&lt;p&gt;So when I loaded the world, Minetest used the world-level config. That basically overrode the global config behavior I was expecting. Because of that, the HTTP API was not enabled for the actual mod that was running.&lt;/p&gt;

&lt;p&gt;This took me more than half an hour to debug.&lt;/p&gt;

&lt;p&gt;And honestly, because I do not have much experience with Minetest config behavior, this was super annoying.&lt;/p&gt;

&lt;p&gt;The run itself was much cheaper and faster than Opus. Kimi used around &lt;strong&gt;52k context tokens&lt;/strong&gt;, took about &lt;strong&gt;9 minutes 27 seconds&lt;/strong&gt;, and cost around &lt;strong&gt;$0.39&lt;/strong&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%2Fxi3s6ic25sy6620np9wf.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%2Fxi3s6ic25sy6620np9wf.png" alt="Kimi K2.6 token usage and time for the build" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That price difference is pretty wild. Opus cost around $3.59 for the first test, while Kimi came in under $0.40.&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/5c9033f77f2dff7792578b3fa126f7343643b5d1" rel="noopener noreferrer"&gt;Kimi K2.6 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$0.39&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~9min 27sec&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +4,671 lines, 0 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Used:&lt;/strong&gt; 52,073 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Window Used:&lt;/strong&gt; 20%&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The local bounty board idea worked, the code was usable, and the model clearly understood the Lua + TypeScript setup. But if you notice, Kimi wrote more than twice as much code as Opus 4.7.&lt;/p&gt;

&lt;p&gt;The main problem was the Minetest config mess. It added &lt;code&gt;secure.http_mods = bountykimi&lt;/code&gt; globally, but then created another world-level config with a different mod name, which made debugging way more painful.&lt;/p&gt;

&lt;p&gt;So yeah, Kimi passed the first test, but not as smoothly as Opus.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Test 2: Real Composio Integration
&lt;/h3&gt;

&lt;p&gt;Now this is where the actual test, and the fun, begins.&lt;/p&gt;

&lt;p&gt;The custom mod is ready, so now it is time to integrate Composio and give the game a quick agentic touch.&lt;/p&gt;

&lt;p&gt;The idea is simple. As players progress through the game, their bounty completions get logged into Google Sheets with Composio.&lt;/p&gt;
&lt;h3&gt;
  
  
  Claude Opus 4.7
&lt;/h3&gt;

&lt;p&gt;Claude Opus 4.7 did manage to add the real Composio integration, but this one was not as smooth as Test 1.&lt;/p&gt;

&lt;p&gt;The backend could sync bounty completions to Google Sheets. The nice thing is that I did not even need to open Minetest to test whether it was working. Because the project was structured cleanly, I could test the whole backend flow with just two &lt;code&gt;curl&lt;/code&gt; requests.&lt;/p&gt;

&lt;p&gt;First, generate a bounty:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/api/bounty/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"player":"singleplayer","availableTasks":["collect_item"]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/b.json | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then complete it:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/api/bounty/complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--argjson&lt;/span&gt; b &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq .bounty /tmp/b.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--arg&lt;/span&gt; ts &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s1"&gt;'{player:"singleplayer", bounty:$b, progress:{current:$b.target.count, required:$b.target.count}, completedAt:$ts}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And if everything is configured correctly, the second response looks like this:&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"Bounty completed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"leaderboard"&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;"player"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"singleplayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completedBounties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&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;"sync"&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;"googleSheets"&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;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;"Google Sheets row appended."&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;p&gt;This is one of the things I love about Opus the most. It usually creates a pretty modular setup. The game mod, backend logic, and external sync were separated well enough that I could test the Composio part directly from the API without needing to run around inside the game every time.&lt;/p&gt;

&lt;p&gt;It did run into a dev server issue where the &lt;code&gt;tsx&lt;/code&gt; command was parsing &lt;code&gt;watch&lt;/code&gt; incorrectly and treating it like the entry file.&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%2F3mdm9m29o5a7q6mq10oe.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%2F3mdm9m29o5a7q6mq10oe.png" alt="Claude Opus 4.7 build error" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a bit of back and forth, it fixed the error. It eventually built a small runtime env loader and adjusted the config import so the backend could read the environment properly before the rest of the app booted.&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%2Fe4e3gju1m8fore381kvx.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%2Fe4e3gju1m8fore381kvx.png" alt="Claude Opus 4.7 build error fix" width="800" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, the build worked and the Google Sheets sync started working.&lt;/p&gt;

&lt;p&gt;But that cost was painful. It literally cost me around &lt;strong&gt;$16&lt;/strong&gt;. Like, actually :(. If you are not watching usage, this thing can make you broke real fast.&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%2Ffe8rgfuafxakh1oucp9j.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%2Ffe8rgfuafxakh1oucp9j.png" alt="Claude Opus 4.7 time and cost" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It took &lt;strong&gt;28min 52sec API time&lt;/strong&gt;, and about &lt;strong&gt;1hr 17min wall time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Apart from that, the code did work. But it cost way more than I expected for one run.&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/0c9c0fa3641796fd97890530c4116e05912b04f4" rel="noopener noreferrer"&gt;Claude Opus 4.7 Code - Composio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; $16.03&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 28min 52sec API time, 1hr 17min 40sec wall time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +1,848 lines, 507 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; 100.2k with Claude Haiku 4.5, plus Opus usage shown in the session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; 3.2k with Claude Haiku 4.5, 123.3k with Claude Opus 4.7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache read:&lt;/strong&gt; 22.3M&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache write:&lt;/strong&gt; 269.3k&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Opus 4.7 got the real Composio integration working, especially the Google Sheets logging.&lt;/p&gt;

&lt;p&gt;How cool is that? You add a custom agentic feature inside a game. A literal public game.&lt;/p&gt;

&lt;p&gt;So yes, it worked. But $16 for this one run hurt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Kimi K2.6
&lt;/h3&gt;

&lt;p&gt;Kimi K2.6 did not do well on this test. It was pretty much busted.&lt;/p&gt;

&lt;p&gt;From the start, it ran into a bunch of errors. The dev server broke, tests were failing, and even after a little handholding, it only managed to fix part of the test situation.&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%2Fj3viy07x6cen4f8a6lxh.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%2Fj3viy07x6cen4f8a6lxh.png" alt="Kimi K2.6 error in the build" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It eventually got past some of those failures, but the bigger problem was the actual Composio implementation. It did not seem fully sure how to wire the integration cleanly into the existing backend.&lt;/p&gt;

&lt;p&gt;I had to stop and help again and again, but it still could not make meaningful progress with the build.&lt;/p&gt;

&lt;p&gt;After spending more than &lt;strong&gt;25 minutes&lt;/strong&gt; and burning over &lt;strong&gt;130k tokens&lt;/strong&gt;, there was still no real progress. At that point, I had to stop the run.&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%2Flpehtobp7tgmpolmyhr4.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%2Flpehtobp7tgmpolmyhr4.png" alt="Kimi K2.6 build error" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why on earth is it reading a &lt;code&gt;version.txt&lt;/code&gt; file?&lt;/p&gt;

&lt;p&gt;So yeah, I am calling this one a fail for Kimi K2.6.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$5.03&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~25min&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt; 135,109+&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kimi K2.6 failed this test.&lt;/p&gt;

&lt;p&gt;It got stuck around tests, build issues, and the real Composio implementation. Even with manual help, it could not get the integration into a clean working state.&lt;/p&gt;

&lt;p&gt;For the local bounty board, Kimi was surprisingly usable. But once the task moved into real external integration, it struggled a lot more.&lt;/p&gt;
&lt;/blockquote&gt;


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

&lt;p&gt;Both Claude Opus 4.7 and Kimi K2.6 were pretty solid in this test, at least for the local version.&lt;/p&gt;

&lt;p&gt;The task was not that simple either. It had Lua, TypeScript, SDK docs, backend logic, game commands, and the full flow had to work end to end.&lt;/p&gt;

&lt;p&gt;Plus, the idea itself is not that common. Building an AI agent concept inside a custom game mod is not easy, and it is definitely not a one-shot thing, so props to both models.&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%2Fu9zolwkatuaoztum6991.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%2Fu9zolwkatuaoztum6991.gif" alt="Clap" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus 4.7 did better overall. The code was cleaner, and as usual, Anthropic models are pretty good at that.&lt;/p&gt;

&lt;p&gt;The only thing I hate with Anthropic models is the session limit.&lt;/p&gt;

&lt;p&gt;I absolutely hate how little session usage you get. Opus 4.7 especially just eats through it completely in like 3 to 5 prompts.&lt;/p&gt;

&lt;p&gt;Kimi K2.6 is an interesting model. Open models have not always been the best in my experience with real-world projects, but with every new model, my expectations rise a little.&lt;/p&gt;

&lt;p&gt;Let's see where Kimi K2.6 goes from here.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>programming</category>
      <category>gamedev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My speaker broke, so I built a LAN speaker</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:29:00 +0000</pubDate>
      <link>https://dev.to/shricodev/my-speaker-broke-so-i-built-a-lan-speaker-19nc</link>
      <guid>https://dev.to/shricodev/my-speaker-broke-so-i-built-a-lan-speaker-19nc</guid>
      <description>&lt;h2&gt;
  
  
  What's happening?
&lt;/h2&gt;

&lt;p&gt;This was around a year back when I started the project, after a small speaker that I had broke out of nowhere. Won't connect.&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%2Fd5hyoru6a4xegzrkukgq.jpeg" 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%2Fd5hyoru6a4xegzrkukgq.jpeg" alt="Broken speaker" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used it to listen to music every night before going to sleep (not sure if anybody else does the same, but it's one of my only fixed routines since childhood :D).&lt;/p&gt;




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

&lt;p&gt;Okay, so what's the idea?&lt;/p&gt;

&lt;p&gt;I had a thought. Why not build something resembling a speaker? Although I had never worked on a project that required working with audio and all that, I knew what I'd use to implement something like that, which was WebSocket (I'll explain why I chose it in a moment).&lt;/p&gt;

&lt;p&gt;And I did start on it.&lt;/p&gt;

&lt;p&gt;The irony is that I had just finished &lt;a href="https://go.dev/tour/" rel="noopener noreferrer"&gt;Tour of Go&lt;/a&gt;, &lt;a href="https://lets-go.alexedwards.net/" rel="noopener noreferrer"&gt;Let's Go&lt;/a&gt;, &lt;a href="https://lets-go-further.alexedwards.net/" rel="noopener noreferrer"&gt;Let's Go Further&lt;/a&gt;, and had just started on 100 Go Mistakes and How to Avoid Them.&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%2Fadv8xgc17bm3oficmzps.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%2Fadv8xgc17bm3oficmzps.png" alt="LinkedIn post on golang books" width="741" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So... I had to do this in Golang.&lt;/p&gt;

&lt;p&gt;I had quite a good idea of how to work with Golang, so it had to be done with it, also because I wanted to get away from the JavaScript ecosystem (the same Node.js, React, Next.js, yada yada was just too much).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why WebSocket
&lt;/h2&gt;

&lt;p&gt;So, let's come to the plan. Why WebSocket?&lt;/p&gt;

&lt;p&gt;The main reason is that when I had the idea, it was WebSocket that came to my mind, and it's what I thought of before even starting to code the project.&lt;/p&gt;

&lt;p&gt;And WebSocket kind of makes sense as well. This was my thought process.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lets you connect multiple connections from one device? &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Is synchronous enough when streaming over multiple devices in LAN? &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Something I've worked with a lot? &lt;strong&gt;Totally Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's pretty much the reason I chose WebSocket. It might not be the best option, but it's what I had and something I was confident with when I started.&lt;/p&gt;

&lt;p&gt;One more thing I'll add here: since I'm streaming audio frames sequentially (PCM chunks, one after another), I actually need them to arrive in order. &lt;/p&gt;

&lt;p&gt;WebSocket runs over TCP, so that's handled for me. I don't have to think about it. That alone made it feel like the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not WebRTC or UDP?
&lt;/h3&gt;

&lt;p&gt;Let me tell you something pretty frankly: I never even knew there was something like WebRTC. I came across this term while building the project halfway through it.&lt;/p&gt;

&lt;p&gt;So, why not UDP?&lt;/p&gt;

&lt;p&gt;Okay, so UDP is connectionless, which means there's no guarantee that every packet arrives, and more importantly, no guarantee they arrive in order. For something like video calls or games, that's totally fine. You drop a frame, move on, nobody cares.&lt;/p&gt;

&lt;p&gt;But for audio? If a chunk goes missing or arrives out of order, you either get silence or a glitch.&lt;/p&gt;

&lt;p&gt;And since I'm streaming raw PCM (basically just a stream of bytes that represent sound), every single chunk matters. You can't just skip them.&lt;/p&gt;

&lt;p&gt;So UDP was out as well.&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%2Fpllsruo4m6s36xvej2cs.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%2Fpllsruo4m6s36xvej2cs.gif" alt="Out GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And WebRTC? Also, WebRTC is mostly built around browser-to-browser, peer-to-peer stuff. My setup is a server broadcasting to multiple clients over LAN, which isn't really what WebRTC is designed for. So even if I knew about it from day one, I'm not sure it would've been the right fit anyway.&lt;/p&gt;

&lt;p&gt;WebSocket was fine. It worked. And sometimes that's enough. I didn't want to overengineer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀 I learned about all these new terms like &lt;strong&gt;PCM&lt;/strong&gt;, &lt;strong&gt;WebRTC&lt;/strong&gt;, and all that stuff during the build. So, I might say something wrong. I'm not really that familiar with them. So, just hit me in the comments if so.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Okay, so the high level is pretty simple.&lt;/p&gt;

&lt;p&gt;There's a server and there are clients. The server is where the music is stored, and the clients are the devices that play it (could be the same device).&lt;/p&gt;

&lt;p&gt;Here's what actually happens when you press play:&lt;/p&gt;

&lt;p&gt;The server takes the MP3 file, decodes it into raw PCM (basically just bytes of sound data), and starts broadcasting those bytes over WebSocket to every connected client. No client-side decoding. The server does all of that.&lt;/p&gt;

&lt;p&gt;Here's a high level architecture:&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%2Foi47bhzovugarytvtmyw.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%2Foi47bhzovugarytvtmyw.png" alt="Gophercast architecture" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tricky part is sync. If you just start streaming, each client will start playing at slightly different times, and it'll sound like a little echo.&lt;/p&gt;

&lt;p&gt;So what I did is, before playback starts, the server sends a timestamp to all clients saying, "Start playing at exactly this moment in time." Every client gets that timestamp, buffers the audio frames, and waits. When the clock hits that time, everyone starts together.&lt;/p&gt;

&lt;p&gt;It works because on a LAN with &lt;strong&gt;NTP&lt;/strong&gt;, all the device clocks are usually within a millisecond or two of each other. Close enough that you can't tell the difference (much).&lt;/p&gt;

&lt;p&gt;That's the whole thing, honestly.&lt;/p&gt;

&lt;p&gt;Server decodes -&amp;gt; broadcasts -&amp;gt; clients sync -&amp;gt; play&lt;/p&gt;




&lt;h2&gt;
  
  
  What I still don't know
&lt;/h2&gt;

&lt;p&gt;Okay, so during the build, I ran into something I didn't fully understand.&lt;/p&gt;

&lt;p&gt;Turns out your speaker doesn't play audio the exact moment you write data to it. There's some time it spends sitting in a system buffer before it actually comes out. And that delay varies by OS and audio system. On Linux, it's something around 50ms, I feel.&lt;/p&gt;

&lt;p&gt;Honestly, I had no idea there was something like this that you need to account for. This thing was completely debugged by &lt;strong&gt;GPT-5.4&lt;/strong&gt;. There's a hardcoded 50ms constant in the code that is counted when clients actually start playing.&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%2Fa7zha09z3aitxrfz1i05.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%2Fa7zha09z3aitxrfz1i05.gif" alt="I don't know gif" width="480" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also something in the code that keeps checking, roughly every second, whether the audio playing on your device is slightly ahead or behind.&lt;/p&gt;

&lt;p&gt;If it is, it quietly adds or removes a tiny bit of audio to bring it back in line. So small you'd never hear it.&lt;/p&gt;

&lt;p&gt;Both of these kind of work. I tested it. I just couldn't tell you exactly why the numbers are what they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I usually run it
&lt;/h2&gt;

&lt;p&gt;Since my plan is to use my whole laptop as a "speaker", I usually have the server and client on the same system (my personal laptop).&lt;/p&gt;

&lt;p&gt;And I usually have it like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gophercast serve &lt;span class="c"&gt;# now follow the TUI setup....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Connect clients (each in a tmux split). Usually, I have around 2 clients when running on the same machine. More than 2 kind of distorts the audio quality.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gophercast play &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;ip_from_serve&amp;gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"client 1"&lt;/span&gt;
gophercast play &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;ip_from_serve&amp;gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"client 2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The steps are the same when connecting from multiple machines. Just make sure that all of them are connected to the same LAN.&lt;/p&gt;

&lt;p&gt;Here's a quick demo on a single machine, working as the speaker.&lt;br&gt;
(Ignore the audio quality)&lt;/p&gt;

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


&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;I'm pretty happy with how this worked out.&lt;/p&gt;

&lt;p&gt;This was my very first time working with Charm's &lt;a href="https://github.com/charmbracelet/bubbletea" rel="noopener noreferrer"&gt;Bubbletea&lt;/a&gt; and the whole audio stuff.&lt;/p&gt;

&lt;p&gt;I started this project just because I wanted to DIY. It was just the perfect time: my speaker broke, and I had somehow finished learning basic Go programming.&lt;/p&gt;

&lt;p&gt;There's probably still a ton of bugs. I've tried to test most of it. I just picked the tool I knew and figured it out as I went.&lt;/p&gt;

&lt;p&gt;It plays music across multiple devices in sync, or the same one if you connect through multiple terminals. My laptop, another computer, whatever's connected, all playing together.&lt;/p&gt;

&lt;p&gt;That's what I wanted. That's what it does.&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%2Ftk8fu13vlk1izssz5n7l.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%2Ftk8fu13vlk1izssz5n7l.gif" alt="go on GIF" width="480" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(I haven't tested how well it works on Mac, so I can't tell there. Also, I'm not sure Windows will work either due to oto's limitation and our hardcoded 50ms delay.)&lt;/p&gt;

&lt;p&gt;You can find the repo here: &lt;a href="https://github.com/shricodev/gophercast.git" rel="noopener noreferrer"&gt;shricodev/gophercast&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 Want to build such cool stuff? You learn hands on here: &lt;a href="https://app.codecrafters.io/join?via=shricodev" rel="noopener noreferrer"&gt;CodeCrafters&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>go</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
