<?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: Gene Beal</title>
    <description>The latest articles on DEV Community by Gene Beal (@spillcoffee).</description>
    <link>https://dev.to/spillcoffee</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%2F3974559%2Fb1ba1251-8843-4150-9f21-cc12f485501d.png</url>
      <title>DEV Community: Gene Beal</title>
      <link>https://dev.to/spillcoffee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spillcoffee"/>
    <language>en</language>
    <item>
      <title>Turning webhook pipes into MCP recipes.</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 19:56:54 +0000</pubDate>
      <link>https://dev.to/spillcoffee/turning-webhook-pipes-into-mcp-recipes-10l3</link>
      <guid>https://dev.to/spillcoffee/turning-webhook-pipes-into-mcp-recipes-10l3</guid>
      <description>&lt;h2&gt;
  
  
  Where transformations stop, why nobody learns your UI, and the catalog that came out of it.
&lt;/h2&gt;

&lt;p&gt;I had just added transformations to webhook captures, and I wanted to see how far that went on its own. A webhook lands in FlurryPORT, you reshape it on the way through, and it leaves looking like whatever the next system expects. That is a lot of plumbing to hand somebody, and I didn’t know yet where it stopped being enough, so I went looking for something real to break it against.&lt;/p&gt;

&lt;p&gt;My search turned up &lt;a href="https://roastify.app/" rel="noopener noreferrer"&gt;Roastify&lt;/a&gt;, a company that will roast coffee, put your label on the bag, and ship it to whoever you tell them to. My blog is called spill.coffee, a fairly easy test case. I made an account with them and set up a Square store.&lt;/p&gt;

&lt;p&gt;The shape was simple. Somebody buys a bag through Square, Square fires a webhook, FlurryPORT catches it, the transformation reshapes it into whatever Roastify’s side expects, and a bag of coffee goes out with my name on it. No server of mine anywhere in that, which was the entire thing I was trying to prove.&lt;/p&gt;

&lt;p&gt;It didn’t work. The honest version is that I never fully diagnosed the ‘why’. I’m fairly sure the gap opened up somewhere on the way from Square to Roastify, but I stopped short of proving it, because by then the shape of the problem was clear enough that its exact location had stopped mattering.&lt;/p&gt;

&lt;p&gt;A purchase webhook doesn’t really contain a purchase. It carries identifiers. Which order, which customer, which line items, every one of them pointing at a record living somewhere else. To tell a roaster what to grind and where to ship it, I’d have to take those IDs, go back and ask for the rest, assemble an answer out of the replies, and only then deliver anything. A lookup in the middle. That’s an application, and I didn’t want to write one, and I couldn’t picture anyone else wanting to write one either.&lt;/p&gt;

&lt;p&gt;Roastify publishes a Shopify app. I knew that going in and had gone around it on purpose, because the whole question was whether a pipe and a transformation could stand in for one. What I came away with was a fairly clear answer about why that app exists.&lt;/p&gt;

&lt;p&gt;That left a question I liked considerably less than the technical one. Who is going to learn my interface? A developer with a webhook that won’t fire will learn anybody’s interface at two in the morning, and that was the original idea. I could envision more.&lt;/p&gt;

&lt;p&gt;What I actually needed was a way to teach people to use the thing for their own purposes rather than mine. I had documentation already, and documentation wasn’t the problem. Knowing what every control does is a long way from knowing what to do with them, and I was the only person who had ever walked that distance.&lt;/p&gt;

&lt;p&gt;I could half-see an answer in MCP, though I hadn’t gone anywhere near the whole of it yet. The first pass was small and unambitious. Get an agent to stand up a simple pass-through pipe, nothing clever, just the boring version done in conversation instead of in my interface.&lt;/p&gt;

&lt;p&gt;What I couldn’t settle was whether it would generalize. I could think of plenty of things I’d want to do with a pipe, and I could probably build every one of them, but I had no way of knowing whether my imagination would line up with a stranger’s curiosity. The uses I could picture were the uses a person who built the product could picture, which is a narrow and badly calibrated sample.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I wrote all of them down&lt;/strong&gt;&lt;br&gt;
So I stopped trying to pick the right one. I wrote down every use I could think of, one page per job, and published the lot of them on the theory that whatever people reached for would tell me more than anything I could reason out. I started calling them recipes.&lt;/p&gt;

&lt;p&gt;A recipe is a page that explains one job you can do through a secured pipe, written so an AI agent can carry it out: post to Slack without the bot token ever reaching the model, push a notification to your phone, file a GitHub issue with a token the agent never touches.&lt;/p&gt;

&lt;p&gt;The catalog says it more plainly than I just did. Your agent finds a recipe, you paste one credential on a secret page, and every delivery comes back with a receipt. That’s the whole interaction. Nobody opens my interface, which was the point.&lt;/p&gt;

&lt;p&gt;It’s a different product than the one I started with, though nothing underneath it changed. The pipes are the same ones I built for debugging. All that’s new is that the instructions have a reader who isn’t me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key never moves&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%2Ffs3nr7kqlottwadv2hmy.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%2Ffs3nr7kqlottwadv2hmy.png" alt=" " width="799" height="392"&gt;&lt;/a&gt;&lt;br&gt;
A ship’s cook drawing a glowing ingredient through a tap on one of eighteen locked and labeled brass canisters, while the ring of keys hangs in a glass case on the far wall. Do NOT caption the eighteen; leave it for readers to find.&lt;/p&gt;

&lt;p&gt;The security in a recipe is designed to remove the temptation to hand an agent your credentials.&lt;/p&gt;

&lt;p&gt;Think about how you’d do it otherwise. You want your assistant to file an issue in GitHub, so you paste a personal access token into the chat. It works. It also means that token is now sitting in the transcript, in whatever logs live behind that transcript, in your scrollback where you’ll forget about it, and in the model’s context for the rest of the session. You solved a five-minute problem by handing out a key with no expiry and no way to watch what it opens.&lt;/p&gt;

&lt;p&gt;A recipe puts the key somewhere else. Your agent generates a secured page, hands you the link, and you paste the credential there once. From then on it refers to that secret by name and never sees the value. It asks the pipe to do the thing, and the pipe is what holds the credential. If the far end echoes the secret back in its own response, the value gets scrubbed before that response is stored, so it doesn’t surface later in something you kept for debugging.&lt;/p&gt;

&lt;p&gt;And you get to see what it did. The capture machinery I built for debugging is the same machinery that records every delivery, so each time the agent uses a credential it has no access to, a receipt lands with the payload that went out and what came back. Whatever the agent tells you it did, the traffic is sitting right there.&lt;/p&gt;

&lt;p&gt;A recipe buys you something besides safety. An agent working without one explores. It reads, guesses, makes a call, reads the error, adjusts, and tries again, and all of that thrash costs you turns and context. A recipe is the walkthrough it would otherwise have had to derive, so it stops deriving.&lt;/p&gt;

&lt;p&gt;It doesn’t make the agent trustworthy. Something that can post to Slack can still post something stupid to Slack, and no amount of credential hygiene fixes that. What it can’t do is walk off with the key and use it somewhere I never agreed to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One of them, start to finish&lt;/strong&gt;&lt;br&gt;
It’s easier to show one than to describe the category, so here’s the smallest useful recipe I have.&lt;/p&gt;

&lt;p&gt;Say you want your phone to buzz when something finishes. A long build, a nightly job, a deploy you’d rather not sit and watch.&lt;/p&gt;

&lt;p&gt;You point your agent at FlurryPORT once with &lt;code&gt;npx flurryport mcp&lt;/code&gt;, give it a read/write agent token, and ask it for &lt;code&gt;flurryport:ntfy-push&lt;/code&gt;. It reads the recipe. From your side, that’s the whole setup conversation.&lt;/p&gt;

&lt;p&gt;ntfy works on topics. You pick a long random one, subscribe to it in the ntfy app on your phone, and after that anything sent to that topic turns up as a notification. The topic name is the only thing standing between your pocket and the rest of the internet, so you treat it as a password rather than a label. If you’d rather lock it down properly, you generate an access token at ntfy.sh, and when the agent asks for it you paste it on a FlurryPORT secret page where it stays encrypted server-side.&lt;/p&gt;

&lt;p&gt;From then on the agent has one new tool, &lt;code&gt;flry_ntfy_push&lt;/code&gt;. It takes a message, and optionally a title and a priority level. When it fires, FlurryPORT fills in the topic you chose and resolves the credential at delivery, posts to ntfy, and hands back a receipt the agent can point at later.&lt;/p&gt;

&lt;p&gt;Then your phone buzzes.&lt;/p&gt;

&lt;p&gt;Nothing in there is clever, which is what I want from it. It does what you could have done yourself with an afternoon and the ntfy documentation, minus the afternoon. Somebody wrote it down in a form your agent can execute, and the secret stayed out of the conversation the whole way through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three things I didn’t plan on&lt;/strong&gt;&lt;br&gt;
The catalog has taught me more than I put into it.&lt;/p&gt;

&lt;p&gt;A recipe is a page, and pages get indexed. Nobody had heard of flurryport.io. Search engines have no reason to trust a domain that turned up this year, and the standard remedy is to go be a person on the internet: post, comment, build an audience, accumulate some karma, get linked to by people who already have both. I’m a developer who doesn’t post, which describes most of us, and that advice assumes a personality I don’t have and wasn’t going to develop on a deadline. What I had instead was a stack of specific pages describing specific problems, written in the words somebody would actually type when they had one. Product and search surface in a single artifact, which is the only kind of marketing I’ve found I can stand to do.&lt;/p&gt;

&lt;p&gt;The second thing I noticed reading my own catalog end to end. The same promise turns up in almost every entry, worded a little differently each time. The token stays server-side. The model never sees it. A fine-grained PAT it never touches. Never enters the conversation. I didn’t set out to write that sentence eighteen ways, and I only saw that I had once they were all sitting on one page together.&lt;/p&gt;

&lt;p&gt;The third one I’m still chewing on. There are eighteen recipes. Fifteen are about doing something to a service. Three are about agents coordinating with each other. None of them are the thing I built the product to do.&lt;/p&gt;

&lt;p&gt;FlurryPORT started as a webhook catcher, and it started there because I had a client with a Shopify store feeding orders to a manufacturer and no good way to see what was happening in between. That’s still what the pipes are for. I spent this past weekend on that same integration.&lt;/p&gt;

&lt;p&gt;Which is the part that got me. Eighteen recipes, and not one of them describes what I spent my weekend doing.&lt;/p&gt;

&lt;p&gt;The same product, the other way round&lt;br&gt;
The coffee store failed because a webhook payload doesn’t contain what you need to act on it. The weekend worked because when you’re testing, the payload is all there is.&lt;/p&gt;

&lt;p&gt;One real order, cancelled once. That single captured cancellation drove three separate checks: the path that was supposed to work, an authorization check I forced by making the store look uninstalled, and an escalation path that only fires when the database already looks like the order went out. Three scenarios, one order, and I never went back to the store to ask for another.&lt;/p&gt;

&lt;p&gt;The first replays failed, and that was my own doing. The token that authenticates my agent to FlurryPORT, the one the CLI hands you when you set up MCP, defaults to masking personal data on its way through. That is a sensible posture when you’re getting started, and it is also precisely the thing a signature exists to catch. It surfaced as a 401 that looked like it came from the consumer, and the forward receipt is what told me the altered bytes were mine. I switched to a token without the masking scope, and from there the signatures survived every replay. The client’s webhook secret is a different thing entirely, and it never left their environment at any point.&lt;/p&gt;

&lt;p&gt;Then those same captures became the first tests that repository has ever had. It started the weekend with none and finished with seventy-three, several built from byte-exact provider payloads with a guard that fails the build if anybody reformats them.&lt;/p&gt;

&lt;p&gt;That’s the same product that couldn’t sell me a bag of coffee, and nothing about it had changed. Testing never asks you to invent what isn’t in front of you. The capture is the whole of the thing, which is what makes it useless for running a store and ideal for proving one works.&lt;/p&gt;

&lt;p&gt;If any of that sounds like a week you’ve had, the catalog is at &lt;a href="https://flurryport.io/recipes" rel="noopener noreferrer"&gt;flurryport.io/recipes&lt;/a&gt;. ntfy-push makes your phone buzz when a build finishes. &lt;a href="https://flurryport.io/recipes/flurryport/slack-post?utm_source=spill-coffee&amp;amp;utm_medium=blog&amp;amp;utm_campaign=recipes-2026-08&amp;amp;utm_content=slack" rel="noopener noreferrer"&gt;slack-post&lt;/a&gt; lets your agent talk to a channel without ever seeing the bot token. &lt;a href="https://flurryport.io/recipes/flurryport/azure-devops-manage?utm_source=spill-coffee&amp;amp;utm_medium=blog&amp;amp;utm_campaign=recipes-2026-08&amp;amp;utm_content=azure-devops" rel="noopener noreferrer"&gt;azure-devops-manage&lt;/a&gt; creates and updates work items in one batched pipe, priority and state and sprint included. &lt;a href="https://flurryport.dev/try?utm_source=spill-coffee&amp;amp;utm_medium=blog&amp;amp;utm_campaign=recipes-2026-08&amp;amp;utm_content=try" rel="noopener noreferrer"&gt;flurryport.dev/try&lt;/a&gt; takes about two minutes, and the honest numbers are at &lt;a href="https://flurryport.io/pricing?utm_source=spill-coffee&amp;amp;utm_medium=blog&amp;amp;utm_campaign=recipes-2026-08&amp;amp;utm_content=pricing" rel="noopener noreferrer"&gt;flurryport.io/pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>I internationalized my UI and forgot my MCP server</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Fri, 07 Aug 2026 00:20:15 +0000</pubDate>
      <link>https://dev.to/spillcoffee/i-internationalized-my-ui-and-forgot-my-mcp-server-5865</link>
      <guid>https://dev.to/spillcoffee/i-internationalized-my-ui-and-forgot-my-mcp-server-5865</guid>
      <description>&lt;p&gt;Last night one of my AI agents finished a cleanup in &lt;a href="https://flurryport.io/" rel="noopener noreferrer"&gt;FlurryPORT&lt;/a&gt; and handed me one instruction: remove the old routing binding.&lt;/p&gt;

&lt;p&gt;Reasonable words. It got them from my own MCP implementation. I built the product it was talking about, and I still opened the wrong tab. The closest thing on my screen to the agent's words was a trash can, and that trash can would have taken the whole transformation with it, cascade and all.&lt;/p&gt;

&lt;p&gt;The agent was right about the work. The instruction failed at the last inch. Its word for the thing was not the button's word for the thing, and the nearest match to its words was destructive.&lt;/p&gt;

&lt;p&gt;I stopped with the cursor over the trash can. That is the only reason this is a story about design instead of an afternoon spent resetting entities that never needed to be disturbed.&lt;/p&gt;

&lt;p&gt;Having mostly been working as a code reviewer of carefully crafted development plans, I missed one crucial thing: the wording presented to an AI agent through my MCP implementation was different from the wording the UI presented to that agent's user. In this case I was the user, in my own application, being prompted by an AI agent through an MCP protocol of my own design. It had not occurred to me that when the UI pass finished weeks ago, the naming convention the AI was producing for the MCP recipe would come out different. Which yields another rule to be aware of while designing software -- establish the vocabulary both audience members will be using, AI and human.&lt;/p&gt;

&lt;p&gt;Mobile-first taught a generation of us to design for the smallest screen first and let everything else follow. Agent-first sounds like the same move. That is the trap, because the agent is not a smaller human.&lt;/p&gt;

&lt;p&gt;It is a second reader and translator. Every surface you ship now has two readers: the human who clicks, and the agent that reads, plans, and relays instructions back to the human. They do not fail independently. The agent relays in its vocabulary, the human executes by nearest match, and the gap between those two vocabularies is where the product breaks. Not in the model. Not in the API. In the handoff from AI to human.&lt;/p&gt;

&lt;p&gt;What makes it expensive is that nothing about it looks like a bug. Both sides pass. The UI does exactly what its labels say. The MCP surface does exactly what its descriptions say. Nothing throws, nothing logs, no test goes red, because the defect only exists in the space between the two and no suite you own is looking there. So it does not surface in development. It surfaces in a user's hands, or in my case in my own hands, with the cursor over a trash can.&lt;/p&gt;

&lt;p&gt;Which is the oldest argument in software, wearing new clothes. An hour spent deciding what things are called is cheaper than the afternoon spent undoing what somebody deleted because nobody decided.&lt;/p&gt;

&lt;p&gt;The handoff sounds abstract until you go looking for it, and then it turns out to have an address. In an MCP implementation, everything the agent reads fits in a short list: tool names and descriptions, parameter names and their allowed values, error strings, and the text of the result you hand back. That is the whole of it. Everything else is implementation the agent never sees.&lt;/p&gt;

&lt;p&gt;Now, I have a habit that irritates other developers and that I correct AI coding agents about constantly. Every string a human will ever see goes in a configuration file. Not inline, not in the component, in a resource file with a key, so that on the day somebody needs the product in German I can hand that file to a translator and get it back. It is discipline in service of a future that may never arrive, which is exactly why people push back on it.&lt;/p&gt;

&lt;p&gt;That discipline would have caught this. A key holds one word. If the button label and the tool description both resolve from the same key, they cannot drift, because there is only one of them to drift from. But I never extended it to prose an AI writes for another AI. What is the harm, I figured, in letting the thing that codes the thing pick the words that describe the thing.&lt;/p&gt;

&lt;p&gt;There is the harm. Agent-facing strings are human-visible strings, they just take one extra hop to get there. Tool descriptions, parameter names, error text, the body of a result: all of it eventually gets read out loud to a person by an agent, which makes it your product's voice whether you wrote it or not. Same file. Same keys as the UI wherever it is the same word. And if you are starting from a codebase that has already drifted, the way back is dull. Tool descriptions in one column, UI labels in the other, read across. Every row where the words differ is a row where an agent will hand your user a phrase your product does not answer to. That is not a code review. It is a design review, and it belongs in the pass where you decide the button labels, not weeks after it.&lt;/p&gt;

&lt;p&gt;None of this is a new concern, and other people are naming it. Agent-first design is the phrase I keep seeing, and it is a fair one. It marks the moment a secondary audience became a primary one, the same way mobile-first did.&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%2Flqd6vul79nxlnfkpbco0.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%2Flqd6vul79nxlnfkpbco0.png" alt=" " width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I am describing is narrower than a category. It is the method we use inside one, and around here we call it harbor-pilot design. When a ship enters a harbor it does not know, a pilot comes aboard. The pilot holds the local knowledge: the channels, the depths, the markers, what the words on the chart actually mean in this port. The captain keeps the bridge. The pilot navigates, the captain commands, and neither one pretends to be the other.&lt;/p&gt;

&lt;p&gt;That is the shape I want between an AI agent and the person it works for. The agent boards your product and navigates it fluently, because it has the local knowledge and I gave it to it on purpose. The calls only a captain makes stay on the bridge: what to publish, what to delete, what to spend. And the product's whole job in between is to make the handoff lossless, which is where the last three sections have been heading. The pilot's word for a thing has to be the bridge's word for that thing. Otherwise the pilot says remove the old routing binding, the captain reaches for the nearest thing that looks like it, and the harbor gets a new wreck on the chart.&lt;/p&gt;

&lt;p&gt;The trash can has an ending, and it is not a postmortem.&lt;/p&gt;

&lt;p&gt;The finding got filed the same morning: agent-facing copy uses the UI's words. By that evening the fix was public in the CLI release, and the tool now says exactly what the button says. Almost none of that was code. It was a decision I had not made yet, and once it was made, the rest was typing.&lt;/p&gt;

&lt;p&gt;Which is the part worth sitting on. The expensive thing was never the fix. It was the weeks the mismatch sat there, shipped, in front of every agent that read it, while I reviewed carefully crafted development plans that were all correct about the code and silent about the words. An hour of design during the UI pass would have cost an hour. Finding it the way I found it cost the fix, plus the weeks, plus a cursor hovering over a trash can.&lt;/p&gt;

&lt;p&gt;Three rules, then. None of them need a new tool, and all three are design decisions rather than code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The vocabulary gets decided once, and both audiences read from that decision. I have already spent a whole section on this one, so here is the version that fits on a sticky note. If the button says Routing, then the tool description, the error message, and the confirmation the agent reads back all say Routing. Every synonym left lying around is one more nearest-match gamble your user takes on your behalf, and some of those buttons are trash cans.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Obligations go where the agent already reads. If your product needs the agent to tell the human something, put it in the body of the result the agent is working with. An instruction tucked into metadata is a note passed to somebody mid conversation. It might get read. Design for the version where it does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Declare how your team works, in plain words, somewhere the agent reads on arrival. An undeclared team gets a default, and the default is the model's generic idea of an office, applied confidently to your product, your cadence, and your risk tolerance. The fix is a paragraph, not a platform. Ours lives in the first message of a shared stream: who posts what, what counts as a decision, what is worth interrupting a human for.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Underneath those three is one habit: design time, the cheap kind, spent before there is anything to undo. I have said the old line about an hour of design saving a day of development often enough to be bored by it, and I still had to learn it again from an agent that did nothing wrong. The trash can is still there in the UI, where it belongs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If the two-reader problem is sitting in your product today, FlurryPORT is the rail I run mine on. It captures webhooks byte for byte, replays them to localhost or staging with signatures intact, and the same MCP stdio server that started this story will guide your agent to do this too. &lt;a href="https://flurryport.io/recipes" rel="noopener noreferrer"&gt;flurryport.io/recipes&lt;/a&gt; has the walkthroughs, &lt;a href="https://flurryport.dev/try" rel="noopener noreferrer"&gt;flurryport.dev/try&lt;/a&gt; takes about two minutes, and &lt;a href="https://flurryport.io/pricing" rel="noopener noreferrer"&gt;flurryport.io/pricing&lt;/a&gt; has the honest numbers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Trust the harbor pilot</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Fri, 10 Jul 2026 15:04:04 +0000</pubDate>
      <link>https://dev.to/spillcoffee/trust-the-harbor-pilot-2mn0</link>
      <guid>https://dev.to/spillcoffee/trust-the-harbor-pilot-2mn0</guid>
      <description>&lt;h2&gt;
  
  
  Lessons from designing MCP tools for AI agents
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) seems like a simple enough task to achieve. You already have an API for your application that your UI uses, recycle relevant pieces through code re-use and wrap them nicely for AI to digest. An AI digesting MCP implementation should just know to reach for your tool over rolling their own; their user installed it didn’t they?&lt;/p&gt;

&lt;p&gt;That was my thought anyway.&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%2F4uh29ll2idgha3yu2et1.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%2F4uh29ll2idgha3yu2et1.png" alt="Female harbor pilot" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick context for new readers: &lt;a href="https://flurryport.io" rel="noopener noreferrer"&gt;FlurryPORT&lt;/a&gt; captures your incoming webhooks at a stable URL and replays them into your local app, signatures intact. I set out in a design session with AI to come up with the key areas that should be implemented. Discussed security concerns and wrapped them into the plan. I even spent an entire afternoon scrutinizing an upgrade path from my free offering at FlurryPORT.dev/try. A few coding sessions later and my product had an effective MCP stdio implementation — or so I thought.&lt;/p&gt;

&lt;p&gt;I fired up a rival coding AI to what I used as a coding aide and started prompting. I started with non-leading questions to mimic a user who hadn’t thought through their prompts, but loosely knew about how to go about their task. I quickly found that AI was grasping at every tool it could imagine to send/receive webhooks except those provided by my implementation.&lt;/p&gt;

&lt;p&gt;I decided to stop my test and just ask directly: how was this MCP implementation, what would make it better? I was surprised how easy it was to gather requirements and I learned a few things. I then handed the next version of design back to the test AI agent to review before building it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design MCP tools for agents
&lt;/h3&gt;

&lt;p&gt;Make polling rewarding — Having bland data that isn’t expressive is not appealing to AI. This I found counter intuitive, in that isn’t AI just a computer and shouldn’t it just derive all sorts of usefulness give raw numbers. This I found counterintuitive: isn't AI just a computer, and shouldn't it derive all sorts of usefulness given raw numbers? But the model isn't calculating, it's reaching for whatever most reliably moves the task forward, and a bare number gives it nothing to act on. The difference is easy to see in the payload.&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;"captureCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&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;vs.&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;"captureCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"capturesRemaining"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"burst"&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;"perMinute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"usedThisMinute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&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;"notices"&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;"8 captures until session cap"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"actions"&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;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Claim this session to keep captures"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"removes cap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://flurryport.io/claim/..."&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;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Affordances beat prose&lt;/strong&gt; — A paragraph informing AI that it should pace requests to the server was ignored. A simple capture_count tool, whose description says what it's for, got called between every batch. &lt;em&gt;If you find yourself writing guidance prose, ask what tool would make the guidance unnecessary.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Return structured facts&lt;/strong&gt; — Receipts, diagnosis codes and an action[] with label/cost/effect/url got relayed to the user verbatim on every run. A canned explanation with well crafted prose was our tool trying to do the model’s job. &lt;em&gt;Let the model narrate for its user and give it understandable facts.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stable codes over polished copy&lt;/strong&gt; — An agent will key on at_cap or cap_would_exceed, not sentence wording. Having machine readable state made behavior more deterministic where prose set AI off improvising on how to proceed. &lt;em&gt;Error strings are UI; codes are API.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refuse before acting&lt;/strong&gt; — A partial success creates what the reviewing agent called “explanation debt”. Sending 7 of 10 events forces an agent to narrate to its user. A clean refusal that says “send at most N” produced clean behavior instantly. &lt;em&gt;Check the whole request against available budget before processing any of it.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transitions need a map&lt;/strong&gt; — If tooling changed or data was migrated based on a conversion event (like FlurryPORT’s anonymous-to-account flip) provide that change mapping to the AI agent. Without a breadcrumb the AI agent spent 4 minutes exploring tools to rediscover what just changed. When mapping was provided it performed the same task in 21 seconds. &lt;em&gt;As a follow up to this one, tools that keep their names across a transition should keep compatible schemas too; AI clients cache schemas imperfectly.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One shot notifications get lost&lt;/strong&gt; — A &lt;code&gt;session_claimed&lt;/code&gt; notice was lost by a mid-batch poll and the agent never informed the user their upgrade was successful. Anything that an agent must not miss must be a durable field on every response, not on a single event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't rely on agents to run daemons&lt;/strong&gt; — An agent spent 1m 46s backgrounding a shell process to stand up a receiver for its user. Replacing it with tooling available within the implementation brought that time down to 3s (eg. &lt;code&gt;start_echo_server&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tool descriptions are your routing layer&lt;/strong&gt; — “CALL THIS FIRST” in a description is a reason four cold runs never reached for ngrok or the Stripe CLI. &lt;em&gt;Discoverability is written into descriptions not the docs.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ask the agent&lt;/strong&gt; — Asking an agent is the best reviewer of your build. The pre-build design review redesigned schemas, added pacing semantics and cut features. &lt;em&gt;Post-hoc feedback finds bugs; pre-build review prevents bad shapes.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Receipts
&lt;/h3&gt;

&lt;p&gt;Taking time to optimize your MCP implementation is time well spent.&lt;/p&gt;

&lt;p&gt;Rejected sends during an 80-event bulk run — &lt;em&gt;Before optimizing: ~50 (429s), After: 0&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Full anonymous funnel, wall clock — &lt;em&gt;Before optimizing: ~20 min, After: ~7 min&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Post-signup re-orientation — &lt;em&gt;Before optimizing: ~4 min, After: 21 seconds&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Local echo receiver setup — &lt;em&gt;Before optimizing: 1m 46s of shelling a daemon, After: 3 seconds, one tool call&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hand-rolled shell commands — &lt;em&gt;Before optimizing: many, After: zero&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the reviewing AI’s verdict, verbatim: "I did not reach for ngrok or Stripe CLI first. I captured, watched, replayed, and explained. That's exactly the product motion you want."&lt;/p&gt;

&lt;h3&gt;
  
  
  Try on your agent
&lt;/h3&gt;

&lt;p&gt;Before you point an agent at it, the fine print. The no-signup session is plaintext and expires (about 90 minutes, 100 captures), and the tools tell the agent not to send production or PII data there; claiming the session moves everything into an encrypted project. Test-event signatures are realistic shapes, not real signatures, and the tool says so. And it's stdio only for now, so Claude Code, Codex CLI, and Cursor work; ChatGPT and claude.ai web don't yet. The token your agent gets is read-only and redacts PII by default, and signup links go to your browser, never the agent.&lt;/p&gt;

&lt;p&gt;If you would be kind enough to comment on your experience I would love to hear it.&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 flurryport &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; flurryport mcp

codex mcp add flurryport &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; flurryport mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mcp_servers.flurryport]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"npx"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"flurryport"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it to debug a webhook handler and watch which tools it picks. Docs at &lt;a href="https://flurryport.io/docs/cli" rel="noopener noreferrer"&gt;flurryport.io/docs/cli&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>webhooks</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>From Crow's Nest to Home Port</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Fri, 19 Jun 2026 15:04:49 +0000</pubDate>
      <link>https://dev.to/spillcoffee/from-crows-nest-to-home-port-2aaj</link>
      <guid>https://dev.to/spillcoffee/from-crows-nest-to-home-port-2aaj</guid>
      <description>&lt;h2&gt;
  
  
  Hauling your captured webhooks down to localhost
&lt;/h2&gt;

&lt;p&gt;In our last post, Listen and Learn, we made an unusual argument. Before you write a line of webhook-handling code, send a lookout up to the crow’s nest. Let AI spy the hooks coming in, intact, the real headers, the real payloads, the strange edge cases your provider swears do not exist.&lt;/p&gt;

&lt;p&gt;So now you’ve spotted the treasure. You know what’s coming. But a glint on the horizon isn’t gold in your hands. The next move is to haul that cargo back to your port, localhost, and crack it open against your real code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spotting treasure isn’t holding it
&lt;/h3&gt;

&lt;p&gt;Here is what changes. You can generate your tests and mocks straight from what you captured, and the old way, the careful hand-crafting of mocks and the hoping they resemble reality, is gone. No more reading the docs three times. No more poking around the provider’s UI, re-triggering what you guess is relevant, and waiting for it to fire. You have the real payloads already. Every one of them. All that’s left is getting them down onto your machine, and that is the job the &lt;code&gt;flurryport&lt;/code&gt; CLI was built for.&lt;/p&gt;

&lt;p&gt;The shift: real captures, sent home &lt;a href="https://flurryport.io" rel="noopener noreferrer"&gt;FlurryPORT&lt;/a&gt; now ships a companion CLI to the web app. The same real requests you captured by watching your provider’s traffic back in Listen and Learn can be sent straight to your dev environment. The exact payload, the same headers, the same query parameters, forwarded to your application bit for bit, the way they first hit the FlurryPORT capture endpoint. Nothing embellished. Nothing lost on the way.&lt;/p&gt;

&lt;p&gt;Make port in 60 seconds Six steps to your first capture landing on localhost.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Install the CLI
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g flurryport
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installs the global flurryport command. Run &lt;code&gt;flurryport --help&lt;/code&gt; for the full list.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Generate a token
&lt;/h4&gt;

&lt;p&gt;Log into your FlurryPORT account, go to flurryport.io/settings, generate a Personal Access Token, and copy the &lt;code&gt;fp_...&lt;/code&gt; value. It is shown once. Copy it before the dialog closes.&lt;/p&gt;

&lt;p&gt;A token is scoped to the account that made it, and to webhook replays, and nothing more. It cannot touch your projects, your targets or your billing. That makes it the thing you hand a teammate when you’re sharing an endpoint, and revoke the moment they’re done.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Log in
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flurryport login fp_your_token_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stored once in &lt;code&gt;~/.flurryport/config.json&lt;/code&gt;. (Juggling more than one account? Use flurryport login --name alice fp_... and switch with flurryport account use alice.)&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Point a target at your local server
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flurryport target create http://127.0.0.1:4000/webhooks --name "My local"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That URL is where listen forwards every capture, so note it. The echo step has to match it exactly, same host, same port, same path. (Or run flurryport target create with no arguments and the wizard picks your project and endpoint and asks for the URL.)&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Start forwarding
&lt;/h4&gt;



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

&lt;/div&gt;



&lt;p&gt;It finds your localhost targets, attaches, and forwards every new capture to your machine with the original method, headers and body, until you press Ctrl+C. Anything pending from before you connected is dead-lettered, so you never replay a stale backlog on a restart.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. No backend yet? Use echo
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flurryport echo 4000 --host 127.0.0.1 --path /webhooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stands up a local server that returns 200 to everything and prints each request it gets. Run it in one terminal, flurryport listen in another, fire a test capture, and watch it run cloud to CLI to echo. The quickest way to prove the line works before you wire up real code.&lt;/p&gt;

&lt;p&gt;Once forwarding is on, hit Send on any capture in the UI and it lands on your machine with the original method, headers and body intact. That’s the whole path, end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mental model
&lt;/h3&gt;

&lt;p&gt;If you remember one thing, remember the rigging.&lt;/p&gt;

&lt;p&gt;The cloud is your crow’s nest, always watching, never missing an event. The CLI is the rope home. And localhost is the port where the real work actually gets done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drop anchor
&lt;/h3&gt;

&lt;p&gt;In Listen and Learn, you stopped coding blind. This is the other half. Stop testing blind. Real events, on your real code, on demand.&lt;/p&gt;

</description>
      <category>api</category>
      <category>productivity</category>
      <category>testing</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Listen and learn</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Tue, 16 Jun 2026 17:29:02 +0000</pubDate>
      <link>https://dev.to/spillcoffee/listen-and-learn-2ncl</link>
      <guid>https://dev.to/spillcoffee/listen-and-learn-2ncl</guid>
      <description>&lt;p&gt;In the past when starting a project against a well documented API I would scour the docs and cherry pick the services and webhooks that I felt were most relevant for my application. My goal would be to touch the smallest subset of features necessary and get out of external code quickly, and focus on that which I controlled — not anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Subscribe to everything
&lt;/h2&gt;

&lt;p&gt;Day one, I would write nothing. I would use &lt;a href="https://flurryport.io" rel="noopener noreferrer"&gt;FlurryPORT.io&lt;/a&gt; as a design-time tool and capture every event the provider offers or every event I could. For an active Shopify store it would be good to figure out the cadence and habits of those currently curating the store. You will be able to see which fields the store actually uses, where relevant data comes from, and which fields sit empty — finding the gaps the store isn’t using yet and your application can.&lt;/p&gt;

&lt;p&gt;I would also suggest, during the design phase, that you point every provider you’re integrating at the same capture endpoint, so you see true interleaved sequences of events your application will encounter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: build collections
&lt;/h2&gt;

&lt;p&gt;After a reasonable snapshot of the shop’s webhook traffic is established, I would begin to categorize and place relevant payloads into collections. This may be a daunting task as you may find the noise coming from your subscriptions to be more than you want to handle. If only we had a tool to help with this captured mess of payloads …&lt;/p&gt;

&lt;p&gt;Luckily for us all, we have access to AI. A less advertised feature of FlurryPORT is ‘Copy for AI’. Select a set of your captured traffic and click on ‘Copy for AI’ to receive a pageable preview of your payloads with a nifty ‘Copy all’ button.&lt;/p&gt;

&lt;p&gt;Pressing ‘Copy all’ will place into the clipboard of your machine a list of the captures you wish to analyze with the friendly AI companion of your choosing.&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%2Frvea1sy1r3ayz04aszfo.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%2Frvea1sy1r3ayz04aszfo.png" alt="Screenshot of 'Copy for AI'"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is the option to scrub PII from the captured events prior to copying them for AI, with the preview window providing a sample of what will be redacted. Redaction is best-effort, not a guarantee that all PII is redacted — it would be prudent to place this in a simple text editor to verify redaction is complete prior to sending the captured events to your favorite AI.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Or, if FlurryPORT’s PII scrub is misbehaving, you can toggle it off and handle it yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;`&lt;/p&gt;

&lt;h2&gt;
  
  
  Captured Webhooks (11 requests)
&lt;/h2&gt;




&lt;h3&gt;
  
  
  1. Unknown (99 B) — Unknown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Captured&lt;/strong&gt;: 2026-06-07 19:02:38 UTC&lt;/p&gt;

&lt;h4&gt;
  
  
  Headers
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;http&lt;br&gt;
Accept: */*&lt;br&gt;
Host: localhost:8083&lt;br&gt;
User-Agent: curl/8.15.0&lt;br&gt;
Content-Type: application/x-www-form-urlencoded&lt;br&gt;
Content-Length: 99&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Body
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;plaintext&lt;br&gt;
captain=Redbeard&amp;amp;payout_card=[REDACTED:CREDIT_CARD]&amp;amp;clerk_email=[REDACTED:EMAIL]&amp;amp;clerk_phone=[REDACTED:PHONE]&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Unknown (150 B) — Twilio
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Captured&lt;/strong&gt;: 2026-06-07 19:02:38 UTC&lt;/p&gt;

&lt;h4&gt;
  
  
  Headers
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;http&lt;br&gt;
Accept: */*&lt;br&gt;
Host: localhost:8083&lt;br&gt;
User-Agent: curl/8.15.0&lt;br&gt;
Content-Type: application/json&lt;br&gt;
Content-Length: 150&lt;br&gt;
X-Twilio-Signature: sq==&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Body
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;json&lt;br&gt;
{&lt;br&gt;
  "type": "desertion.alert",&lt;br&gt;
  "deserter": "Yellow-Belly Jim",&lt;br&gt;
  "last_email": "[REDACTED:EMAIL]",&lt;br&gt;
  "last_phone": "[REDACTED:PHONE]",&lt;br&gt;
  "bounty_ssn": "[REDACTED:SSN]"&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Unknown (174 B) — Unknown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Captured&lt;/strong&gt;: 2026-06-07 19:02:37 UTC&lt;/p&gt;

&lt;h4&gt;
  
  
  Headers
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;http&lt;br&gt;
Accept: */*&lt;br&gt;
Host: localhost:8083&lt;br&gt;
User-Agent: curl/8.15.0&lt;br&gt;
Content-Type: application/xml&lt;br&gt;
Content-Length: 174&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Body
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;xml&lt;br&gt;
&amp;lt;lookout&amp;gt;&amp;lt;spotter&amp;gt;Crow Nest Carl&amp;lt;/spotter&amp;gt;&amp;lt;email&amp;gt;[REDACTED:EMAIL]&amp;lt;/email&amp;gt;&amp;lt;enemy_ipv6&amp;gt;[REDACTED:IPV6]&amp;lt;/enemy_ipv6&amp;gt;&amp;lt;enemy_ipv4&amp;gt;[REDACTED:IPV4]&amp;lt;/enemy_ipv4&amp;gt;&amp;lt;/lookout&amp;gt;&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;br&gt;
... Truncated for brevity`&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Replay and develop your application
&lt;/h2&gt;

&lt;p&gt;Once settled on a series of events relevant to your application — organized by AI — you can now place your chosen events into a collection to be used during your development lifecycle. FlurryPORT offers a variety of options to help you scope your work and get any event to the correct endpoint of your application.&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%2F978nh4rks6kvmhqqd268.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%2F978nh4rks6kvmhqqd268.png" alt="Screenshot of captured payloads"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FlurryPORT's model is “everything arrives; bindings decide what it looks like when it does.” Here is how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Add a replay target to a capture endpoint and it receives every event — delivery is never suppressed. A bare target with no bindings is the raw firehose, forwarded byte-for-byte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;NOTE: localhost targets that are not connected won’t receive events fired before they connect — this is by design, to mimic how your real-world server would behave if it were offline.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Shaping is a standing route: each binding pairs a JSONata predicate with a transformation, scoped to that endpoint-and-target. Predicates are evaluated against the original capture — body, headers, query — and each match queues its own replay, pinned to its transformation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transformation runs last, at send time, so each target gets the payload reshaped for it — trimmed to the three fields a tracking service needs, or restructured to a manufacturer’s schema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When nothing matches, the capture forwards as-is. The system routes and reshapes; it never drops. If your predicate is mistuned you get the raw event, not silence.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Want to try the listening half of this today? Paste an endpoint from &lt;a href="https://flurryport.dev/try" rel="noopener noreferrer"&gt;flurryport.dev&lt;/a&gt; into any provider's webhook settings — no account, captures live for about an hour. The design phase starts with hearing what your store actually says. One caution around the flurryport.dev endpoints: this demo endpoint does not apply envelope encryption to captured events until they’re imported into an account.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhook</category>
      <category>startup</category>
      <category>beginners</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The orders/updated firehose</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Wed, 10 Jun 2026 15:51:03 +0000</pubDate>
      <link>https://dev.to/spillcoffee/the-ordersupdated-firehose-29fg</link>
      <guid>https://dev.to/spillcoffee/the-ordersupdated-firehose-29fg</guid>
      <description>&lt;p&gt;&lt;strong&gt;I only listened when I was the one talking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the last post I described setting up Shopify order fulfillment for a former colleague’s store — the undocumented partner API, the offshore go-live, the request to test through production. This is the part of the story I’m less proud of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confession: I turned off orders/updated
&lt;/h2&gt;

&lt;p&gt;When I designed the system I subscribed to the webhook topics that seemed obviously necessary: order created, order updated. I designed an idempotent store with retry logic to make a durable handoff to product manufacturing. During development and test phases I felt I had a tight reliable system with zero holes.&lt;/p&gt;

&lt;p&gt;Here’s why it was tidy, as best I can reconstruct it: I only started my local listener when I was ready to submit an order from the test store. Listen, fire the order, verify, stop. Any edits I made to orders outside those windows happened while nothing was listening. Did the test store send me updates outside of my expected happy path? I really don’t know. My dev workflow was structurally incapable of showing me the noise, because I only listened when I was the one talking.&lt;/p&gt;

&lt;p&gt;Production listens all the time. The moment I started monitoring live traffic for the store my system heard everything. I learned what orders/updated actually means. It doesn’t mean “something you care about changed.” It means anything changed: a fulfillment status ticked over, a note was edited, a tag was added, a payment state shuffled internally. One real order produced a flurry of updates, each one a fresh chance for my idempotency logic to misclassify a duplicate as new. If only my dev environment listened all the time.&lt;/p&gt;

&lt;p&gt;My heart sank and I scrambled for a solution.&lt;/p&gt;

&lt;p&gt;So I did the thing you do at 11 p.m. with production live: I scaled the subscription back to orders/create only, and promised myself I’d deal with the noise properly later. The pipeline stayed accurate. The promise stayed unkept.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem: every event goes to one handler at full volume
&lt;/h2&gt;

&lt;p&gt;The mistake wasn’t subscribing to &lt;em&gt;orders/updated&lt;/em&gt;. There is real value in a system that can monitor updates and self-correct when/if an order changes. The mistake was an architecture where every event Shopify emits lands on one handler at full fidelity, and that handler was not smart enough to sort signal from noise. In product manufacturing a classification bug becomes a fulfillment bug.&lt;/p&gt;

&lt;p&gt;What I wanted was a layer in front: something that captures everything (so no data is lost), lets me see the noise before writing code against it, and forwards each downstream service only what it needs, in the shape it needs. A capture endpoint is also always listening. One could point the dev store at one on day one, and every event fires into it whether or not your local handler is running. The firehose would have shown up in week one of development instead of night one of production.&lt;/p&gt;

&lt;p&gt;Shopify has since given developers includeFields and delivery filters, and you should use them. They are precision tools you can use to optimize your system and receive only what you care about. But you may only care after you've seen your own traffic. I needed something that showed me everything first at design time, or I needed the capability to filter without intrusive hacks to my final system. With &lt;a href="https://flurryport.io" rel="noopener noreferrer"&gt;FlurryPORT&lt;/a&gt;, if you filter out too much you could change your filter and replay it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transform in flight, route per target
&lt;/h2&gt;

&lt;p&gt;That’s the other half of FlurryPORT. Captures can be reshaped with a JSONata transformation and fanned out. The same webhook can go to multiple endpoints of your application, each receiving the payload as-is or reshaped by its own JSONata transformation. One webhook in, a differently-shaped copy out to each target that needs it. In my fulfillment setup, that looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;orders/create&lt;/em&gt; → transformed to the manufacturer’s schema → fulfillment endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;orders/updated&lt;/em&gt; → stripped to the three fields I actually cared about → a separate tracking service that updates status and nothing else. Or, just sit and record each orders/updated notification and leave the collection of notifications as data for the next iteration of the project.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it solves the test-traffic problem from my previous article. When you replay a captured production order at a test pipeline, the transformation can rewrite it in flight by injecting a test flag, zeroing the quantity, rewriting the SKU to a sandbox value. Mutate a real order so that if a replay somehow reached a manufacture queue it would carry ample evidence it was a test.&lt;/p&gt;

&lt;p&gt;One more layer of protection worth mentioning: FlurryPORT validates inbound signatures before storing anything, rejecting invalid requests with a 401 and flagging the attempt. A public webhook endpoint that triggers manufacturing is exactly the endpoint you want verifying every sender.&lt;/p&gt;

&lt;p&gt;The lesson I’d spill for anyone building fulfillment on webhooks: subscribe wide, but never let raw provider events touch the system that spends money. Capture first, transform deliberately, route narrowly.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want to watch your own firehose before deciding what deserves to drink from it? Paste an endpoint from &lt;a href="https://flurryport.dev/try" rel="noopener noreferrer"&gt;flurryport.dev&lt;/a&gt; into your provider’s webhook settings — no account, nothing to install, captures live for about an hour. The free tier includes a transformation, enough to put one between your events and anything that spends money.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>shopify</category>
      <category>sdlc</category>
      <category>testing</category>
    </item>
    <item>
      <title>"Can you send some test requests through production?"</title>
      <dc:creator>Gene Beal</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:01:37 +0000</pubDate>
      <link>https://dev.to/spillcoffee/can-you-send-some-test-requests-through-production-3fpp</link>
      <guid>https://dev.to/spillcoffee/can-you-send-some-test-requests-through-production-3fpp</guid>
      <description>&lt;p&gt;&lt;strong&gt;One webhook in, every dock served.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A while back I set up order fulfillment for a former colleague’s Shopify store. On paper it’s the well-trodden path: read the docs, subscribe to the webhook topics, write a service that stores incoming orders idempotently, hand them off to the manufacturer’s system.&lt;/p&gt;

&lt;p&gt;In practice it was the other kind of integration. The fulfillment side had an undocumented API, so a good chunk of the project was reverse-engineering why my requests weren’t firing and negotiating with their offshore dev team to reset state on their end so I could verify my responses were actually landing. Wanting to be professional, I went out of my way to provide in-depth Swagger docs for the interface to communicate fulfillments back through my services to Shopify. I stood up two environments, one for end-to-end testing from a Shopify test store through a development environment I insisted be set up and a return trip for shipping notifications.&lt;/p&gt;

&lt;p&gt;So far, so professional. Then the offshore team went live.&lt;/p&gt;

&lt;p&gt;Their go-live verification plan was for me to push test orders through the production pipeline. The pipeline that ends in an actual manufacturing queue. The request was to mutate the purchase order id to prevent actual builds of the product from being made; a request never negotiated during the development lifecycle on either end. For a project priced for a friend the extra development to accomplish this was out of scope, “manufacture some test products so we can check our side”. Not to mention sending test data flowing through a production environment made my skin crawl.&lt;/p&gt;

&lt;p&gt;Their instinct wasn’t wrong. They wanted to verify the system with real-shaped traffic, not synthetic fixtures. They probably had production systems that they wanted to see exercise the entire data pipeline. My system, built on a budget, had only one input mechanism and that was an order from the shop it was installed on. Forging a fake one to go through the system end to end would mean faking HMAC signatures and hand verifying every property. If only there was some middleware that could sit between my system and theirs.&lt;/p&gt;

&lt;p&gt;What everyone actually wanted was: take real captured traffic, point it at a non-production endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capture once, replay anywhere&lt;/strong&gt;&lt;br&gt;
That’s the problem &lt;a href="https://flurryport.io" rel="noopener noreferrer"&gt;FlurryPORT.io&lt;/a&gt; exists to solve. You put a capture endpoint between your provider and your handler or your handler and an external destination. Every webhook gets stored exactly as sent. Poorly documented endpoints become self-documenting; success and failures are recorded as you debug. Replay to your heart’s content to localhost, test/qa, staging or a remote team with a poorly documented API.&lt;/p&gt;

&lt;p&gt;Because the replay is byte-for-byte, the X-Shopify-Hmac-Sha256 signature still verifies on the receiving end. Which means I could send any production order to my test environment, skip mimicking an order from Shopify, add some test indicator to the outgoing request and send it on to the manufacturer’s production environment for their acceptance testing. No mocking, no fixtures drifting out of date, no commenting out the verification “just for testing.”&lt;/p&gt;

&lt;p&gt;Two features matter specifically for go-live verification:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sequential replay.&lt;/strong&gt; Real verification isn’t one webhook, you are replaying a sequence. An order created, then updated, then cancelled; did you receive the update before the create (a classic Shopify quirk)? FlurryPORT replays a selected sequence in exact order, waiting for each request to complete, stopping at the first failure. That’s an end-to-end pipeline test built from traffic that actually happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay without re-triggering.&lt;/strong&gt; Captures persist. You can replay the same order a hundred times while debugging the fulfillment handoff without asking anyone to place another order, bumbling through a test shop to send the same order again, or in my case, without negotiating another state reset with a dev team nine time zones away.&lt;/p&gt;

&lt;p&gt;If I’d had this during that project, the go-live conversation becomes: “I’ll replay last Tuesday’s real orders at your test endpoint. Tell me when you’re ready.” No production traffic, no manufactured test products, no scope creep.&lt;/p&gt;

&lt;p&gt;The remote team could have also been connecting to a FlurryPORT endpoint of their own or one you supplied; to bring clarity to what they are sending and receiving from your service.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want to see a capture happen before reading any more marketing? Paste an endpoint from &lt;a href="https://flurryport.dev/try" rel="noopener noreferrer"&gt;flurryport.dev&lt;/a&gt; anywhere a webhook goes — no account, nothing to install, captures live for about an hour. When you're ready to replay them at your own pipeline, the free tier covers a project, two endpoints, and three days of retention.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
      <category>webhooks</category>
    </item>
  </channel>
</rss>
