<?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: Toheeb Olanrewaju Olagoke</title>
    <description>The latest articles on DEV Community by Toheeb Olanrewaju Olagoke (@olacode).</description>
    <link>https://dev.to/olacode</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%2F4071341%2F75bb00aa-e115-4643-a902-f5bd13744dfc.png</url>
      <title>DEV Community: Toheeb Olanrewaju Olagoke</title>
      <link>https://dev.to/olacode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olacode"/>
    <language>en</language>
    <item>
      <title>I let three ghosts into my terminal. One of them remembered what I was doing three months ago tonight.</title>
      <dc:creator>Toheeb Olanrewaju Olagoke</dc:creator>
      <pubDate>Fri, 30 Oct 2026 21:56:23 +0000</pubDate>
      <link>https://dev.to/olacode/i-let-three-ghosts-into-my-terminal-one-of-them-remembered-what-i-was-doing-three-months-ago-2kh5</link>
      <guid>https://dev.to/olacode/i-let-three-ghosts-into-my-terminal-one-of-them-remembered-what-i-was-doing-three-months-ago-2kh5</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hf26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://youtu.be/OPQT0wRBPeI" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Haunted Shell is A Christmas Carol for your terminal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In Dickens, three spirits visit Scrooge in one night: the Past, the Present and the Yet to Come. In Haunted Shell they visit your shell, and everything they know comes from your own machine: your git history, your shell history, the repo you're standing in. All of it is read and narrated by an open-weight model running on my laptop, with the Wi-Fi off.&lt;/p&gt;

&lt;p&gt;☾ &lt;strong&gt;The Ghost of Commits Past&lt;/strong&gt; walks through every git repository on your computer and finds what &lt;em&gt;you&lt;/em&gt; were doing on this same day in earlier months and years. It shows you the real commits first, then takes you by the hand.&lt;/p&gt;

&lt;p&gt;▌ &lt;strong&gt;The Ghost of Commits Present&lt;/strong&gt; is Mortimer, a Unix sysadmin who died in 1987 in a freezing server room while a tape backup was running. He lives under your prompt and whispers about the commands you run. Fail a command, force-push, &lt;code&gt;rm -rf&lt;/code&gt; something, or type &lt;code&gt;gti status&lt;/code&gt; at 2 a.m., and he has something to say.&lt;/p&gt;

&lt;p&gt;⧗ &lt;strong&gt;The Ghost of Commits Yet to Come&lt;/strong&gt; never jokes. Run it inside a project and it points at what you've been neglecting: the oldest TODOs (dated with &lt;code&gt;git blame&lt;/code&gt;), forgotten stashes, uncommitted files, dead branches. Then it prophesies, and ends the way Dickens did.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;haunt carol&lt;/code&gt; brings all three, one after another. With &lt;code&gt;haunt voice on&lt;/code&gt;, they whisper out loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I read the theme:&lt;/strong&gt; Halloween is about ghosts. Día de los Muertos is about remembering. The most honest ghost living inside my machine turned out to be &lt;em&gt;me&lt;/em&gt;: months of commit messages written at one in the morning that I had completely forgotten. Haunted Shell lets that ghost speak.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it landed
&lt;/h2&gt;

&lt;p&gt;The first time I ran &lt;code&gt;haunt past&lt;/code&gt; for real, this is what came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;☾  The Ghost of Commits Past
   Here is what you were doing on the 30th of earlier months:

  30 Jul 2026  01:03  undertone         Add submission Q&amp;amp;A
  30 Jul 2026  01:03  undertone         Add submission copy
  30 Jul 2026  01:05  undertone         Add contribution description
  30 Jul 2026  18:33  awesome-phone-ca  feat: add pharmacy-stock-check skill
  30 Jul 2026  22:02  rxfind            RxFind: pharmacy stock check agent on CALL-E
  30 Jul 2026  23:27  rxfind            fix: handle semicolon field separator in summary parsing

☾ I take your hand, and we step into the shadows of yesterday's code.
  Three months ago, on Thursday 30 July 2026 at 01:03, you sat before a
  computer screen with the message "Add submission Q&amp;amp;A" etched in its
  digital heart. At 01:05, just two minutes later, your fingers danced
  across the keyboard again, this time with the words "Add contribution
  description". Later that evening, as night descended, at 18:33, you
  conjured a new feature for awesome-phone-call-agents.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three months ago tonight, at one in the morning, I was finishing the submission for &lt;strong&gt;Undertone&lt;/strong&gt;. Undertone measures the colour of your skin, hair and eyes, works out which seasonal palette you belong to, scores a garment catalogue against that palette with a perceptual colour-difference metric, and then shows you the best matches on your own photo.&lt;/p&gt;

&lt;p&gt;By that evening I had moved on to &lt;strong&gt;RxFind&lt;/strong&gt;, a pharmacy stock-check agent on CALL-E. It came from a problem everyone has had: you need a specific medication &lt;em&gt;today&lt;/em&gt;. Which pharmacy actually has it, at what price, and will they hold it for you? The only way to find out is to call them one at a time and ask the same four questions. At 23:27 I was still fixing its parser.&lt;/p&gt;

&lt;p&gt;When the ghost brought that night back, my first reaction was: wow. I couldn't believe it. I had to go and check my git log to believe it, and every line was real. I was shocked, and it was amazing. A ghost I built had brought back the night I shipped Undertone, while I was writing the post for this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the spirits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ▌ The Ghost of Commits Present
&lt;/h3&gt;

&lt;p&gt;Mortimer is a little too observant. These are real whispers from the day I set the project up and pushed it to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git remote add origin https://github.com/Olacode01/haunted-shell.git
&lt;span class="go"&gt;▌ Your little joke about the Dickens classic won't fetch any applause,
&lt;/span&gt;&lt;span class="gp"&gt;  I'm afraid. The GitHub world is all code now;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;what&lt;span class="s1"&gt;'ll be next,
&lt;/span&gt;&lt;span class="go"&gt;  a git pull of existential dread?

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Keep post draft local"&lt;/span&gt;
&lt;span class="go"&gt;▌ They forgot the `git push` part, didn't they? The remote's still
  holding onto that draft, a spectral reminder of their oversight...

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt;~ls
&lt;span class="go"&gt;zsh: command not found: ls~ls
▌ Your haste has brought forth the specter of syntax errors. Did you
  mean to summon 'grep', or was it a desperate cry for help?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;He was right about the &lt;code&gt;git push&lt;/code&gt;. I ran it next.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;haunt present&lt;/code&gt; also gives you a review of your day so far. Here is his summary after I'd spent half an hour hunting for a folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;▌  The Ghost of Commits Present (Mortimer)
  • They have been at the terminal since 10:28 today, about 27 minutes, and have run 18 commands.
  • 4 commands failed. The last one to fail was: find ~ -maxdepth 4 -type d -name rxfind 2&amp;gt;/dev/null
  • They keep running the same command: cd ~/Downloads/haunted-shell (4 times).

▌ You've been staring at this screen for nearly half an hour now, 27 minutes
  of futile typing. Four commands have withered away in the darkness, one of
  them a desperate search for something called 'rxfind'. You keep returning
  to that haunted-shell folder, four times already, but it remains empty
  and unforgiving.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ⧗ The Ghost of Commits Yet to Come
&lt;/h3&gt;

&lt;p&gt;Pointed at RxFind, the hooded spirit found a file I'd left uncommitted for a month and a half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⧗  The Ghost of Commits Yet to Come
   It raises one finger toward rxfind:

  † The last commit was 46 days ago: fix: recover fields from prose summaries; document the unstable format
  † 2 uncommitted changes sit in the working tree; the oldest, server.py, has waited 46 days without being committed

⧗ The unrecovered fields will be lost forever. The field descriptions will no
  longer accurately reflect the data. Server.py will remain unchanged and its
  functionality unsaved. These are shadows of what may be. You could commit
  those changes today.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;a href="https://github.com/Olacode01/haunted-shell" rel="noopener noreferrer"&gt;https://github.com/Olacode01/haunted-shell&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole thing is &lt;strong&gt;one Python file using only the standard library&lt;/strong&gt; (it runs on the &lt;code&gt;python3&lt;/code&gt; that ships with macOS), a 30-line zsh plugin, and an Ollama &lt;code&gt;Modelfile&lt;/code&gt;. No pip installs, no server, no framework. The model is &lt;strong&gt;Llama 3.2 3B&lt;/strong&gt;, running locally through &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ghost of Commits Present&lt;/strong&gt; uses zsh &lt;code&gt;preexec&lt;/code&gt;/&lt;code&gt;precmd&lt;/code&gt; hooks to catch each command and its exit code. Before calling the model, &lt;code&gt;haunt&lt;/code&gt; checks for &lt;em&gt;omens&lt;/em&gt; (&lt;code&gt;rm -rf&lt;/code&gt;, force-pushes, exit 127, Ctrl-C, running the same command twice, the witching hour) and writes them into the prompt in plain English, like "they force-pushed, rewriting history that others may have seen". That makes a 3B model far more specific than just showing it a raw command. Replies stream from Ollama's local API and type themselves out under your prompt, with the odd c̷u̸r̶s̵e̷d̸ letter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ghost of Commits Past&lt;/strong&gt; walks your home folder for git repos (skipping &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;Library&lt;/code&gt; and friends) and reads &lt;code&gt;git log --all&lt;/code&gt; from each, keeping only commits where the author is you. On my machine that was 177 footprints across 35 repositories. It looks for the same date in earlier years, widening to ±1, ±3 and ±7 days; for younger histories like mine it falls back to the same day of earlier months. Once a day it also knocks on its own, with one pale line under your prompt: &lt;em&gt;"Something you wrote on this day, 3 months ago, is knocking. Let it in: haunt past."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ghost of Commits Yet to Come&lt;/strong&gt; is plain git plumbing: &lt;code&gt;git grep&lt;/code&gt; for TODO/FIXME/HACK, &lt;code&gt;git blame --porcelain&lt;/code&gt; to date the oldest ones, &lt;code&gt;git stash list&lt;/code&gt;, &lt;code&gt;git status --porcelain&lt;/code&gt; with file modification times, and &lt;code&gt;git for-each-ref&lt;/code&gt; for branches nobody has touched in 60 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The voice&lt;/strong&gt; is the &lt;em&gt;Whisper&lt;/em&gt; voice built into macOS, played with &lt;code&gt;say&lt;/code&gt;, so it's offline too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping a small ghost honest
&lt;/h3&gt;

&lt;p&gt;This was the hardest part, and the part I'm proudest of.&lt;/p&gt;

&lt;p&gt;The first time the Ghost of Commits Past spoke, it told me I'd committed to a project in &lt;strong&gt;2024&lt;/strong&gt;. My history starts in 2026. In the same breath it made up a company name, said I'd done it "for the team", and narrated its own stage directions: &lt;em&gt;(pauses)&lt;/em&gt;. A 3B model will happily invent a touching past for you, and for a tool about &lt;em&gt;your&lt;/em&gt; memories, that's the one thing it must never do.&lt;/p&gt;

&lt;p&gt;So the spirits now work like a séance with receipts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The evidence is printed first&lt;/strong&gt;, exactly as it is on disk, before the model says a word. You can always check the ghost against it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model never does date math.&lt;/strong&gt; My code had been telling it a commit was "0 years ago", so it guessed a year. Now Python works out "3 months ago" and the model is told to use only that exact phrase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It sees less.&lt;/strong&gt; The screen shows every memory; the model gets at most four.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stage directions are stripped&lt;/strong&gt; as the text streams, and the spirits run at a lower temperature than Mortimer's quick whispers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test suite (26 tests) builds throwaway git repos with backdated commits, including someone else's commit on the same date that must be ignored, and runs everything against a fake Ollama server, so it needs no model to run.&lt;/p&gt;

&lt;p&gt;I built Haunted Shell with an AI coding assistant, then ran it on my own history, found where the ghost lied, and kept tightening it until it didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters for What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This project should not exist on a cloud API.&lt;/strong&gt; To work, it reads my entire git history and my shell history: every project name, every server I've SSH'd into, every half-finished idea at 1 a.m., and probably an API key I pasted once. I would never upload that to someone else's server to get a ghost story. With an open-weight model running on my laptop, I don't have to: nothing leaves &lt;code&gt;127.0.0.1&lt;/code&gt;. On top of that, Haunted Shell redacts tokens, passwords and &lt;code&gt;user:pass@&lt;/code&gt; URLs from commands &lt;em&gt;and&lt;/em&gt; commit messages, ignores any command that starts with a space, and keeps everything in two small files I can delete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with the lights and the internet off.&lt;/strong&gt; In the demo, I turn the Wi-Fi off first. The ghosts don't notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing, so it can haunt every command.&lt;/strong&gt; Mortimer is called dozens of times a day for one-sentence replies. That's a terrible fit for a metered API and a perfect fit for a 3B model sitting warm in memory. On my Mac a whisper starts in under a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can change the ghost itself.&lt;/strong&gt; Because the model is mine, I tuned it with a &lt;code&gt;Modelfile&lt;/code&gt;, and anyone can move the ghosts into a different open-weight model with one command (&lt;code&gt;haunt model qwen2.5:3b&lt;/code&gt;), or use a bigger model just for the carol while the whispers stay on the small, fast one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memories should outlive the service that holds them.&lt;/strong&gt; That's the part that felt right for Día de los Muertos. My commit messages turned out to be a diary I didn't know I was keeping. A tool that helps you remember shouldn't depend on a company that could change its prices or shut down. These ghosts live on my machine, and they stay as long as I want them to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it (macOS)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Olacode01/haunted-shell &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;haunted-shell
./install.sh
&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%2Ffbj0w8j2zg9puhiom2v5.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%2Ffbj0w8j2zg9puhiom2v5.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then open a new terminal tab, mistype something, and listen. When you're ready: &lt;code&gt;haunt carol&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building Co-Shop: A Shared Cart for Humans and AI Agents with WebMCP</title>
      <dc:creator>Toheeb Olanrewaju Olagoke</dc:creator>
      <pubDate>Wed, 09 Sep 2026 21:43:01 +0000</pubDate>
      <link>https://dev.to/olacode/building-co-shop-a-shared-cart-for-humans-and-ai-agents-with-webmcp-5p8</link>
      <guid>https://dev.to/olacode/building-co-shop-a-shared-cart-for-humans-and-ai-agents-with-webmcp-5p8</guid>
      <description>&lt;p&gt;&lt;strong&gt;The problem with "agentic" web apps today&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask most AI agents to shop for you online right now, and here's roughly what happens under the hood: the agent opens a headless browser, loads the page, tries to parse the DOM, guesses which  is the "Add to Cart" one, clicks it, waits, re-parses, and hopes nothing changed in the layout since the model was trained on how that site "usually" looks.&lt;/p&gt;


&lt;p&gt;It works sometimes. It's also slow, brittle, and completely opaque to the human sitting there. You ask an agent to "add a few vegetarian dinners to my cart," it goes away for a minute, and you get a summary back: "Done! I added 3 items." You had no idea what it was doing while it did it, and if it clicked the wrong thing, you find out after the fact.&lt;/p&gt;

&lt;p&gt;WebMCP is a proposed web standard that removes the guesswork entirely. Instead of an agent reverse engineering your UI, your page tells the agent what it can do:&lt;/p&gt;

&lt;p&gt;document.modelContext.registerTool({&lt;/p&gt;

&lt;p&gt;name: "add_to_cart",&lt;/p&gt;

&lt;p&gt;description: "Add a product to the cart",&lt;/p&gt;

&lt;p&gt;inputSchema: {&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type: "object",

properties: {

  productId: { type: "string" },

  quantity: { type: "number" },

},

required: ["productId"],
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;},&lt;/p&gt;

&lt;p&gt;async execute({ productId, quantity }) {&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// your actual cart logic
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;},&lt;/p&gt;

&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;An agent visiting your page right now, in ChatGPT's WebMCP-enabled desktop browser, or in Chrome with the #enable-webmcp-testing flag can call add_to_cart directly. No clicking, no scraping, no guessing.&lt;/p&gt;

&lt;p&gt;I wanted to see what this actually unlocks beyond "agent can now click buttons faster," so I built Co-Shop for OpenAI's WebMCP Challenge: a grocery storefront where a human and an AI agent share the literal same cart, live, in the same browser tab.&lt;/p&gt;

&lt;p&gt;Here's what I learned building it.&lt;br&gt;
The core idea: one state, two actors&lt;br&gt;
The easy version of this project would have been "agent has its own cart-building tool, human has a UI, sync them somehow." I didn't want that. The whole point of WebMCP, I think, is that there doesn't need to be a sync step at all if the agent's tool and the human's UI both mutate the same piece of state, they're never out of sync in the first place.&lt;/p&gt;

&lt;p&gt;So in Co-Shop, app/page.js is a single client component holding the cart in React state, and both the UI's click handlers and the WebMCP tools' execute functions call the exact same helper functions:&lt;/p&gt;

&lt;p&gt;function addToCart(productId, quantity, actor) {&lt;/p&gt;

&lt;p&gt;const product = PRODUCTS.find((p) =&amp;gt; p.id === productId);&lt;/p&gt;

&lt;p&gt;if (!product) return { ok: false, error: &lt;code&gt;No product with id "${productId}".&lt;/code&gt; };&lt;/p&gt;

&lt;p&gt;const qty = Math.max(1, Math.floor(quantity || 1));&lt;/p&gt;

&lt;p&gt;setCart((prev) =&amp;gt; {&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const existing = prev.find((i) =&amp;gt; i.productId === productId);

if (existing) {

  return prev.map((i) =&amp;gt;

    i.productId === productId ? { ...i, quantity: i.quantity + qty, addedBy: actor } : i

  );

}

return [...prev, { productId, quantity: qty, addedBy: actor }];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;});&lt;/p&gt;

&lt;p&gt;logActivity(actor, &lt;code&gt;added ${qty}× ${product.emoji} ${product.name} to the cart&lt;/code&gt;);&lt;/p&gt;

&lt;p&gt;return { ok: true, product: product.name, quantity: qty };&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The only difference between a human clicking "Add" and an agent calling the add_to_cart tool is the actor string passed in: "human" vs "agent". That one parameter is what drives the purple "AGENT" badge, the pulse animation, and the activity feed entry. Everything else the state, the total, the render is identical.&lt;br&gt;
Registering the tools&lt;br&gt;
All seven tools are registered in a single useEffect, using an AbortController so cleanup is automatic if the component unmounts:&lt;/p&gt;

&lt;p&gt;_useEffect(() =&amp;gt; {&lt;/p&gt;

&lt;p&gt;if (typeof window === "undefined" || !("modelContext" in document)) {&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setWebmcpStatus("unsupported");

return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;const controller = new AbortController();&lt;/p&gt;

&lt;p&gt;const opts = { signal: controller.signal };&lt;/p&gt;

&lt;p&gt;async function registerAll() {&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await document.modelContext.registerTool(

  {

    name: "search_products",

    description: "Search the Co-Shop grocery catalog...",

    inputSchema: {

      type: "object",

      properties: {

        query: { type: "string" },

        category: { type: "string" },

        tags: { type: "array", items: { type: "string" } },

        maxPrice: { type: "number" },

      },

    },

    async execute({ query, category, tags, maxPrice } = {}) {

      const results = findProducts({ query, category, tags, maxPrice });

      return { content: [{ type: "text", text: JSON.stringify({ count: results.length, results }) }] };

    },

  },

  opts

);

// ... six more tools registered the same way

setWebmcpStatus("ready");
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;registerAll();&lt;/p&gt;

&lt;p&gt;return () =&amp;gt; controller.abort(); // unregisters everything&lt;/p&gt;

&lt;p&gt;}, []);_&lt;/p&gt;

&lt;p&gt;Two details tripped me up here, and I think they're worth calling out for anyone building their first WebMCP tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Stale closures. Tools are registered once, on mount. If your execute function captures cart directly from a useState value at registration time, it'll keep reading that original empty cart forever. React state updates don't retroactively update already-created closures. The fix is to route every read and write through setCart (the function, not the value) and derive snapshots on demand rather than trusting a captured variable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Return shape matters. WebMCP's execute functions are expected to return a content-array shape:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;{ content: [{ type: "text", text: "..." }] }&lt;/p&gt;

&lt;p&gt;I originally just returned plain objects. It technically "worked" in my own manual testing (since I controlled both ends), but once a real agent called the tools, structured JSON in that text field not a bare string is what let the agent actually reason over the result (e.g., knowing exactly which 5 of 7 requested dinners fit a budget, and why).&lt;br&gt;
The tool that mattered most: compound actions&lt;br&gt;
The single tools (add_to_cart, remove_from_cart, get_cart) are the "hello world" of WebMCP pretty much the exact example in OpenAI's own challenge brief. The one that actually made the demo interesting was a compound tool:&lt;/p&gt;

&lt;p&gt;async execute({ days, dietary, avoidTags, maxBudget }) {&lt;/p&gt;

&lt;p&gt;return textResult(planDinners({ days, dietary, avoidTags, maxBudget }));&lt;/p&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;plan_dinners takes a natural-language-shaped request "7 days, vegetarian, under $40" and internally does the filtering, picks distinct matching meals, and calls addToCart for each one, all in a single tool round-trip. This is the difference between an agent doing one thing at a time forever, and an agent actually executing a plan. When I tested it against ChatGPT's in-app browser, asking it to "plan a week of vegetarian dinners under $40" resulted in exactly this response, generated by the tool itself:&lt;/p&gt;

&lt;p&gt;"Added 5 vegetarian dinners to the cart for $39.50. Seven don't fit under the $40 limit."&lt;/p&gt;

&lt;p&gt;That sentence isn't the LLM improvising it's structured output straight from plan_dinners' return value, which the model then relayed. That's the part of WebMCP I find genuinely different from prompt-and-hope agent behavior: the agent isn't guessing what happened, it's told exactly what happened, in a format it can act on.&lt;br&gt;
Testing against a real agent&lt;br&gt;
Two ways to verify a WebMCP integration actually works, in increasing order of "does this prove anything real":&lt;/p&gt;

&lt;p&gt;document.modelContext exists in the console. Proves the API surface is there. Doesn't prove your tools are being called correctly.&lt;br&gt;
A stubbed registerTool in a headless test. I did this with Playwright before ever touching a real agent stub document.modelContext.registerTool to just store the tool object, then call .execute() directly in a script. This caught real bugs (a quantity-update edge case) with instant feedback, no agent round-trip needed.&lt;br&gt;
An actual agent client. For me, that was Chrome with chrome://flags/#enable-webmcp-testing enabled (confirms registration my status pill flipped to "Agent tools live") and, more meaningfully, ChatGPT's desktop app, which has a built-in WebMCP-aware browser. Opening my deployed URL there and typing a plain-English request was the first time I saw an actual model decide, on its own, which tool to call and with what arguments.&lt;/p&gt;

&lt;p&gt;That third step is the one I'd tell anyone building a WebMCP project not to skip, even under deadline pressure. Stubbed tests prove your code is correct. They don't prove an agent will actually choose to call your plan_dinners tool instead of, say, five separate add_to_cart calls, or prove your tool descriptions are clear enough for the model to pick the right one at all.&lt;br&gt;
What I'd do differently&lt;br&gt;
If I kept building this, the next thing I'd add is a compare_products tool right now an agent can search and filter, but can't ask "which of these is the better value per serving," which feels like the natural next compound action after plan_dinners. I'd also want to test with more than one agent client side-by-side, since tool-calling behavior (how eagerly a model reaches for a compound tool vs. chaining primitives) seems to vary meaningfully between clients.&lt;/p&gt;

&lt;p&gt;Try it yourself&lt;br&gt;
Live app: co-shop-gules.vercel.app&lt;br&gt;
Source (MIT licensed): github.com/Olacode01/co-shop&lt;/p&gt;

&lt;p&gt;If you have a WebMCP-enabled browser handy, open the live link and ask an agent to "plan a week of vegetarian dinners under $40." Watching your own cart fill up live, tagged by who added what, is a much better way to understand what WebMCP changes than reading about it.&lt;/p&gt;


</description>
      <category>javascript</category>
      <category>webmcp</category>
      <category>nextjs</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building VeraMove: An AI That Calls Three Movers, Catches Hidden Fees, and Negotiates a Better Deal.</title>
      <dc:creator>Toheeb Olanrewaju Olagoke</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:38:14 +0000</pubDate>
      <link>https://dev.to/olacode/building-veramove-an-ai-that-calls-three-movers-catches-hidden-fees-and-negotiates-a-better-deal-lm7</link>
      <guid>https://dev.to/olacode/building-veramove-an-ai-that-calls-three-movers-catches-hidden-fees-and-negotiates-a-better-deal-lm7</guid>
      <description>&lt;p&gt;28 million Americans move every year, in a $20B+ market made up of over 16,000 small moving companies. Real quotes for one identical 45-mile move have been documented ranging from $1,158 to $6,506, a 5.6x spread for the same job. Sight-unseen phone estimates are 40% more likely to end in a bill above the original quote. That's the problem our four-person team set out to solve at a recent Hack-Nation × ElevenLabs hackathon, and this is the story of how we built VeraMove: a voice-and-document intake pipeline that locks a single move specification, calls three vendors with it, catches the fees they don't mention up front, negotiates one quote against another, and hands back a ranked, evidence-backed recommendation. The core loop: The whole product boils down to one sequence, and we treated it as the single thing that had to work before anything else mattered:&lt;/p&gt;

&lt;p&gt;The full source is public: &lt;a href="https://github.com/zukhriddingit/VeraMove" rel="noopener noreferrer"&gt;github.com/zukhriddingit/VeraMove&lt;/a&gt;; it's a mock-first hackathon starter, no API keys or accounts required to run it yourself.&lt;/p&gt;

&lt;p&gt;Voice or document intake&lt;/p&gt;

&lt;p&gt;→ confirmed, version-locked JobSpec&lt;/p&gt;

&lt;p&gt;→ three parallel vendor calls&lt;/p&gt;

&lt;p&gt;→ itemized quotes with hidden-fee detection&lt;/p&gt;

&lt;p&gt;→ negotiation using a verified competing quote as leverage&lt;/p&gt;

&lt;p&gt;→ ranked recommendation with transcript and recording evidence. Everything else  UI polish, awards strategy, video scripts- was explicitly secondary. "A working call beats a polished interface" became something close to a team motto. Architecture: mock-first, contract-driven FastAPI owns the canonical Pydantic contracts and generates the OpenAPI schema. APP_MODE=mock wires in an in-memory repository and deterministic synthetic fixtures, so the entire loop- three vendor calls, quote generation, negotiation, recommendation- runs without a single external API key or account. That decision mattered enormously for a 24-hour build: nobody was blocked waiting on ElevenLabs quota or an OpenAI key while the demo loop got proven out.&lt;/p&gt;

&lt;p&gt;The frontend's rule, spelled out in the repo's AGENTS.md, was strict: one API client, types generated from FastAPI's OpenAPI schema via openapi-typescript, never handwritten parallel domain models. Any contract change meant: update the Pydantic models and backend tests, re-export the OpenAPI JSON, regenerate the TypeScript types, update call sites, run the full check suite, then get sign-off from both the backend and frontend owners. That discipline is what let four people build in parallel without four different mental models of what a Quote looks like. Four conversation-design requirements, made visible. Because "moving services negotiator" is a fairly obvious vertical for a hackathon like this, differentiation had to come from execution depth, not novelty. One deliberate choice: build a visible checklist in the UI for the four things the challenge brief actually cared about in a voice agent.&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%2Fn0vryqppbgp47efykgyu.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%2Fn0vryqppbgp47efykgyu.png" alt=" " width="799" height="630"&gt;&lt;/a&gt;&lt;br&gt;
Disclosure: every call opens by stating it's an AI calling on the customer's behalf, and answers "are you a robot?" honestly. Friction survival: at least one call has to survive real friction (hold music, a rushed dispatcher) and land a structured callback instead of a dead end. The honesty line: the agent can use a real verified competing quote as leverage, but must never invent inventory or fabricate a competitor's number. Structured close: no call ends on a vague answer; every call closes as an itemized quote, a callback commitment, or a documented decline.&lt;/p&gt;

&lt;p&gt;We built this as a small four-item card that lights up based on real call outcome data rather than being pure set dressing; two items are static guardrail statements (things baked into every synthetic call by design), and two are computed live from each call's actual outcome type. Honest &amp;gt; flashy, especially when honesty is literally what the requirement is testing for. Hidden fees and red flags, from the schema up. The vendor persona design does a lot of work here. Three synthetic vendors, three distinct negotiation styles: a transparent one that itemizes everything up front, a cheap-headline one that reveals fees only under direct questioning, and a premium one that moves meaningfully when shown a better verified quote.&lt;/p&gt;

&lt;p&gt;On the data side, every fee line item on a quote carries a disclosed_upfront: boolean. Any fee where that's false is, by definition, a hidden fee caught after the fact, which meant the frontend didn't need any fuzzy logic to surface this, just a filter. Red flags work the same way: a red_flags: string[] field lives directly on both the quote and the final recommendation ranking, populated by the backend's own rules (e.g. a quote 30%+ below the median gets flagged, never presented as a clean win without a caveat). The mid-build pivot: Vite/React to Lovable Partway through, after a role swap put me on frontend ownership, we shipped a fully working Vite + React + TypeScript implementation of the loop, intake, confirm, calls, negotiate, report, all wired to the real mock backend, with hidden-fee and red-flag surfacing, a full JobSpec review screen, and test coverage for the new logic. It passed the full CI-equivalent check (Ruff, pytest, OpenAPI export, typecheck, Vitest, production build) and went up as a PR.&lt;/p&gt;

&lt;p&gt;Then the team decided to move the frontend to Lovable. That decision came with a real lesson in the cost of a mid-build tool switch:&lt;/p&gt;

&lt;p&gt;Rebuilding on hearsay contracts. Lovable's agent can't run openapi-typescript against a live backend the way our Vite setup could; it works from whatever you tell it. My first pass described the API contract from memory, and it quietly diverged from the real schema in three ways: money fields (original_total, negotiated_total, deposit) are serialized by FastAPI as decimal strings, not JSON numbers; stairs is a numeric count, not a boolean; and version fields are the literal string "1.0", not a number. All three got caught and fixed, but only because we'd separately verified the real schema earlier in the Vite build; without that, they'd have shipped wrong. Verifying against real payloads, not assumptions. When we needed the shape of a call record for the conversation-design checklist, I initially had Lovable invent field names based on partial information. The actual backend owner sent a real JSON payload back, and the real shape looked meaningfully different: a full Vendor object nested in multiple places instead of a {id, name} shorthand, a status field, and started_at/completed_at timestamps. Type-correcting against a real payload took minutes; building on the wrong assumption and finding out later would have cost a lot more. Unrelated git histories. Lovable auto-provisions its own GitHub repo per project with its own root commit. Pushing that as a new branch onto the real repository worked at the git level, but GitHub's compare view came back with "there isn't anything to compare, main and this branch are entirely different commit histories." The fix was mechanical once diagnosed: branch from the real main, copy the Lovable-exported files into the correct directory, commit fresh on top of the real history, and push that instead. A five-minute problem once you know unrelated-history diffs are a known GitHub limitation, a confusing one if you don't. CORS and reachability. A cloud-hosted frontend can't call &lt;a href="http://127.0.0.1:8000" rel="noopener noreferrer"&gt;http://127.0.0.1:8000&lt;/a&gt;; that address only means something on the machine running it. Anything built in a hosted tool needs either a publicly reachable backend or a tunnel, and the backend's CORS policy needs to explicitly allow the new frontend's origin. Easy to forget when you've been developing against localhost for the whole build.&lt;/p&gt;

&lt;p&gt;None of these were hard problems. All of them were the direct cost of losing the single source of truth (a generated, verified contract) the moment the frontend moved to a tool that couldn't generate types from the live API itself. The fix in every case was the same instinct: stop guessing, go get the real payload, verify against it. What we'd tell another team doing this: Freeze your contracts before writing feature code, and freeze them first between backend and frontend, not last. Build the ugliest possible version of the full loop before touching visual polish. A working call beats a polished interface, every time. If you have to build in a tool that can't generate types from your live API, verify field-by-field against a real payload before you build on top of assumed ones; the difference between "grounded in the generated contract" and "hand-typed from memory" is exactly the gap where subtle, expensive bugs live.&lt;/p&gt;

&lt;p&gt;VeraMove is a hackathon starter, not a production product, mock mode only, synthetic data throughout, no real vendor calls. But the loop it proves one locked spec, three comparable quotes, one negotiation backed by real leverage, one evidence-linked recommendation is the whole idea, and getting that loop working end to end, twice, in two different frontend stacks, taught us more about contract discipline than either build alone would have.&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%2Fm1zye2ozq0mh8sk6c60z.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%2Fm1zye2ozq0mh8sk6c60z.png" alt=" " width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the code:&lt;/strong&gt; &lt;a href="https://github.com/zukhriddingit/VeraMove" rel="noopener noreferrer"&gt;github.com/zukhriddingit/VeraMove&lt;/a&gt;. Clone it, run python scripts/&lt;a href="http://bootstrap.py" rel="noopener noreferrer"&gt;bootstrap.py&lt;/a&gt;, then python scripts/&lt;a href="http://dev.py" rel="noopener noreferrer"&gt;dev.py&lt;/a&gt;, and you'll have the full loop running locally in about five minutes no credentials needed&lt;/p&gt;

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