<?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: anthony papoti</title>
    <description>The latest articles on DEV Community by anthony papoti (@anthony_builds).</description>
    <link>https://dev.to/anthony_builds</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%2F4085352%2Feb14361d-b624-43c3-8ab6-f19dbde36eb7.jpg</url>
      <title>DEV Community: anthony papoti</title>
      <link>https://dev.to/anthony_builds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anthony_builds"/>
    <language>en</language>
    <item>
      <title>I asked an LLM to fix misspelled names. It renamed the product instead.</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:43:49 +0000</pubDate>
      <link>https://dev.to/anthony_builds/i-asked-an-llm-to-fix-misspelled-names-it-renamed-the-product-instead-523e</link>
      <guid>https://dev.to/anthony_builds/i-asked-an-llm-to-fix-misspelled-names-it-renamed-the-product-instead-523e</guid>
      <description>&lt;p&gt;&lt;em&gt;My AI clipper corrects proper nouns in transcripts. On one video it swapped an unknown model name for a real competitor's. Why it happened, and the code fix.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Katto is an AI video clipper. You drop in a long video, it cuts the good moments into vertical shorts, and it writes the captions for you. I build it on my own, in public. This is about a feature I advertise on the pricing page, which I caught quietly doing the opposite of its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was solving
&lt;/h2&gt;

&lt;p&gt;Speech-to-text is very good at words and very bad at names. Whisper hears a brand it has never met and writes it phonetically. "Trump" comes out "trompe". "Suárez" comes out "Suares". "ChatGPT" comes out "chatgépété". On a French football video I once counted ninety-eight of these in a single transcript.&lt;/p&gt;

&lt;p&gt;Captions with mangled names look careless, and they are the most visible part of a clip. So I added a correction pass: after transcription, the text goes to a small language model with one instruction, find proper nouns that were spelled phonetically and fix the spelling. It costs about a cent per video. It fixed the ninety-eight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;Last week a user clipped a French video about large language models. The speaker kept referring to a model called JEV. Whisper heard it and wrote "Jev", which is close enough that nobody would have blinked.&lt;/p&gt;

&lt;p&gt;The clips came out titled "Why Jamba is overrated" and "Jamba: revolution and overrated at once". The captions said Jamba. The descriptions said Jamba.&lt;/p&gt;

&lt;p&gt;Jamba is a real large language model, made by AI21. It is simply not the one in the video.&lt;/p&gt;

&lt;p&gt;I went to look at what the pipeline had stored. The archived transcript said "Jev" twice and "Jamba" zero times. Every stage after it said "Jamba" and never "Jev": the scored candidates, the clip metadata, the clip pool, twenty-four occurrences in all. The transcription had been right. The thing I added to protect it had broken it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the model did it
&lt;/h2&gt;

&lt;p&gt;It did exactly what a helpful assistant does. The video is about language models. "Jev" is not a model it knows. "Jamba" is a model it knows, and the two are not far apart if you squint. In context, replacing one with the other looks like a correction rather than an invention.&lt;/p&gt;

&lt;p&gt;That is the failure mode nobody warns you about with this kind of task. A spell-checker that does not recognise a word leaves it alone. A language model that does not recognise a word reaches for the nearest word it does recognise, and the more domain knowledge it has, the more confident and the more wrong that reach becomes. My corrector was worse at rare names precisely because it was good at common ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that stung
&lt;/h2&gt;

&lt;p&gt;I had already thought of this. The prompt said, in capitals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ONLY fix spelling and capitalization. NEVER replace a name with a DIFFERENT name. Inventing a plausible-but-wrong name is WORSE than leaving an unusual one unchanged. […] Do NOT guess a better-known name from context.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three sentences, unambiguous, written specifically to prevent what happened. The model read them and renamed the product anyway.&lt;/p&gt;

&lt;p&gt;I do not think the prompt was badly worded, and I no longer think a better one would have helped. Asking a model not to do the thing it is inclined to do is a request, not a constraint. It holds most of the time, which is worse than never holding, because you stop checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;The rule moved out of the prompt and into the code. The model still proposes corrections; a function now decides whether each one is a spelling fix or a rename, and silently drops the renames.&lt;/p&gt;

&lt;p&gt;The test is deliberately crude: strip case and accents from both strings, then measure how much of the original survives in the replacement. Above 0.6, it is the same name spelled differently. Below, it is a different name.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.90 · rousse peter → rouspéter · kept&lt;/li&gt;
&lt;li&gt;0.83 · Suares → Suárez · kept&lt;/li&gt;
&lt;li&gt;0.82 · chatgépété → ChatGPT · kept&lt;/li&gt;
&lt;li&gt;0.73 · trompe → Trump · kept&lt;/li&gt;
&lt;li&gt;0.53 · nouillène → Nguyen · dropped&lt;/li&gt;
&lt;li&gt;0.31 · mistral → Gemini · dropped&lt;/li&gt;
&lt;li&gt;0.25 · jev → Jamba · dropped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every correction in my test set survives. Some legitimate fixes will not: that fifth line is a real one. A French speaker saying "Nguyen" is heard as "nouillène", the two strings share almost nothing once you strip the accents, and the guard now throws that correction away. The same will happen to romanised Japanese names and to acronyms spelled out letter by letter in another language.&lt;/p&gt;

&lt;p&gt;That is the side I chose. An odd name left alone is visible, and a reader can work out what was meant. A wrong name is not visible, and nobody can. Rejections are logged with both strings, because a rejection means the model just tried to rename something, and that is worth seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;p&gt;A rule a model can ignore is not a rule. If a constraint matters, it belongs in code that runs after the model, not in a paragraph the model reads. Prompts express intent. They do not enforce it.&lt;/p&gt;

&lt;p&gt;Watch the direction of a fix, not just its presence. I had metrics on this pass: how many corrections it made per video. Ninety-eight looked like a healthy number. Nothing measured whether a correction moved the text closer to the truth or further from it, and a bad correction is indistinguishable from a good one if all you count is the total.&lt;/p&gt;

&lt;p&gt;A pipeline that stores the input and ships the output can lie to you twice. The archived transcript said "Jev" while every delivered clip said "Jamba". If I had only read the archive I would have concluded the pipeline was fine. The check that matters is the one on what left the building.&lt;/p&gt;

&lt;p&gt;Confidence is a feature of the failure. A corrector that leaves an unknown name alone produces a visible, harmless oddity. One that replaces it produces clean, fluent, wrong text that nobody questions, including me, until the person who made the video reads their own captions.&lt;/p&gt;

&lt;p&gt;The guard shipped the same day. If you clip a video about something obscure and Katto leaves an unusual name exactly as it heard it, that is now deliberate.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Replaced My TTS Engine. The API Call Was the Easy Part.</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Sun, 13 Sep 2026 13:22:28 +0000</pubDate>
      <link>https://dev.to/anthony_builds/i-replaced-my-tts-engine-the-api-call-was-the-easy-part-49cm</link>
      <guid>https://dev.to/anthony_builds/i-replaced-my-tts-engine-the-api-call-was-the-easy-part-49cm</guid>
      <description>&lt;p&gt;Katto already had AI dubbing. A creator could take a short clip, choose a language, and get a translated version back. The first version used Kokoro and covered eight languages.&lt;/p&gt;

&lt;p&gt;Recently, I moved the primary speech engine to Google Cloud Text-to-Speech with Chirp 3 HD. The goal looked straightforward: improve the voices and add more languages. Katto now exposes 19 dubbing languages and 12 curated voices in the editor.&lt;/p&gt;

&lt;p&gt;I expected the migration to be mostly an API swap.&lt;/p&gt;

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

&lt;p&gt;Generating a natural sentence is easy. Making that sentence begin and end inside the exact space left by another speaker is the actual dubbing problem.&lt;/p&gt;

&lt;p&gt;If the original speaker says something in 2.4 seconds and the translation takes 3.2 seconds, a good TTS model does not save the clip. The voice runs into the next sentence, the captions drift, or the speech has to be accelerated until it sounds rushed.&lt;/p&gt;

&lt;p&gt;The TTS request turned out to be the smallest part of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline I ended up with
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cached word timings
        |
        v
phrase segments with fixed time slots
        |
        v
length-aware translation
        |
        v
Google Chirp 3 HD, one phrase at a time
        |
        v
bounded audio alignment with FFmpeg
        |
        +----&amp;gt; translated captions from the same segments
        |
        v
final MP4 remux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each stage exists because a simpler version failed in a different way.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the original timing as data
&lt;/h2&gt;

&lt;p&gt;Katto already has word timestamps from its transcription pipeline. I do not send the whole transcript to translation and TTS as one block. I group words into phrases.&lt;/p&gt;

&lt;p&gt;A phrase ends at sentence punctuation, after a pause longer than 0.6 seconds, or when it would exceed six seconds. Every segment retains its original start and end. Those numbers become the contract for every later stage.&lt;/p&gt;

&lt;p&gt;Synthesizing the full transcript may sound smoother, but it removes the anchors needed to put speech back on the video timeline. Phrase-level synthesis preserves enough context for a natural voice while keeping useful timing boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Translate for spoken duration, not only meaning
&lt;/h2&gt;

&lt;p&gt;A correct written translation can be a bad dubbing translation. Some languages need more syllables to express the same idea. A literal version may preserve every nuance and still be unusable because it does not fit its slot.&lt;/p&gt;

&lt;p&gt;For each group of phrases, I ask Claude Haiku for a natural spoken translation that prefers shorter phrasing and fewer syllables. The response preserves every source segment index.&lt;/p&gt;

&lt;p&gt;The prompt is part of the audio system. Its job is not only linguistic accuracy. It also reduces the time stretching needed later.&lt;/p&gt;

&lt;p&gt;This remains a heuristic. An LLM cannot guarantee duration because the selected voice also changes pacing. The audio layer still measures the generated WAV. An in-process cache keyed by target language and source segments lets repeat renders reuse the translation while the worker remains alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Synthesize one phrase at a time
&lt;/h2&gt;

&lt;p&gt;Each translated phrase goes to the Google Cloud Text-to-Speech REST API as 24 kHz LINEAR16 audio. Voice names follow the locale-Chirp3-HD-speaker format documented by Google.&lt;/p&gt;

&lt;p&gt;Google supports more Chirp 3 HD locales than Katto currently exposes. I deliberately limited the product to 19 languages wired through the complete path: UI, locale mapping, fonts, captions and fallback behavior. An API claiming a language does not mean the surrounding product supports it correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Fit speech into the slot without destroying it
&lt;/h2&gt;

&lt;p&gt;After synthesis, I compare the generated duration with the original slot.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target_duration = segment["end"] - segment["start"]
ratio = generated_duration / target_duration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the speech is slightly too long, FFmpeg's atempo filter speeds it up while preserving pitch. I cap the speedup at 1.3x.&lt;/p&gt;

&lt;p&gt;That cap matters. An earlier 1.6x limit made more phrases fit, but some voices sounded unnaturally hurried. Perfect alignment is not useful if the result is unpleasant to hear.&lt;/p&gt;

&lt;p&gt;Each adjusted phrase is placed at its original offset in a master track. Short phrases leave a natural pause. Longer phrases get a bounded correction. This does not solve every pathological translation. It enforces a more useful rule: preserve intelligibility before chasing mathematically perfect timing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Give speech and captions the same clock
&lt;/h2&gt;

&lt;p&gt;The synthesis response gives me audio, not reliable word timestamps for the newly spoken text. Inventing word-level timing would make the captions look precise while being wrong.&lt;/p&gt;

&lt;p&gt;Dubbed clips therefore use phrase-level captions generated from the same translated segments used for speech. Their start and end times already match the audio placement.&lt;/p&gt;

&lt;p&gt;The renderer also selects fonts that cover the target script. Latin fonts are not enough for Japanese, Chinese, Hindi or Arabic. Intro titles and AI hooks follow the same localization path, so the screen does not speak one language and display another.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordinary production problems mattered too
&lt;/h2&gt;

&lt;p&gt;Not every curated speaker worked in every locale I tested. If a selected voice is unavailable, Katto retries once with a known default for the chosen gender. The editor's live preview calls the same Google engine. A sample from another model would let the user choose a voice they never receive.&lt;/p&gt;

&lt;p&gt;The production API key is restricted to the VPS IPv4 address, but the VPS preferred IPv6 when reaching Google. Valid requests were rejected until the TTS call was forced through IPv4. The API, key and code were correct. The network path was not.&lt;/p&gt;

&lt;p&gt;Dubbing produces a separate video variant. If translation, synthesis, caption rendering or remuxing fails, the original clip remains intact. For the original eight languages, Kokoro remains available when the Google path is disabled or unconfigured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, and what did not
&lt;/h2&gt;

&lt;p&gt;The visible result was an expansion from 8 to 19 dubbing languages, plus selectable voices and live previews.&lt;/p&gt;

&lt;p&gt;The deeper improvement was architectural. Translation, speech, captions, overlays and video rendering now share one temporal unit: the phrase segment.&lt;/p&gt;

&lt;p&gt;The system does not clone the original speaker, perform lip sync or invent generated word timestamps. Very long translations can still exceed their ideal slot. Those limits are better than claiming precision the pipeline cannot deliver.&lt;/p&gt;

&lt;p&gt;I thought I was replacing a speech engine. I ended up tightening the contract between almost every stage of the media pipeline.&lt;/p&gt;

&lt;p&gt;The API call was the easy part. Making the result belong on the original timeline was the work.&lt;/p&gt;

&lt;p&gt;Katto turns long videos into short vertical clips and can now dub those clips into 19 languages. If you are building multilingual video, I would be interested to hear how you handle timing without making the voice sound rushed.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/text-to-speech/docs/chirp3-hd" rel="noopener noreferrer"&gt;Google Cloud Text-to-Speech: Chirp 3 HD voices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;Katto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>googlecloud</category>
      <category>ffmpeg</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>The Cloudflare KV race that broke our MCP OAuth at random (and how we killed it)</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:35:38 +0000</pubDate>
      <link>https://dev.to/anthony_builds/the-cloudflare-kv-race-that-broke-our-mcp-oauth-at-random-and-how-we-killed-it-a1g</link>
      <guid>https://dev.to/anthony_builds/the-cloudflare-kv-race-that-broke-our-mcp-oauth-at-random-and-how-we-killed-it-a1g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Originally published on the Katto blog: &lt;a href="https://katto.tech/blog/mcp-oauth-cloudflare-kv-race" rel="noopener noreferrer"&gt;https://katto.tech/blog/mcp-oauth-cloudflare-kv-race&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Katto is an AI video clipper. You give it a long video, it finds the best moments and cuts them into captioned vertical clips, and you can drive the whole thing from an AI agent through our official hosted MCP server at mcp.katto.tech. I build Katto on my own, in public, so here is a bug that cost me an evening and the fix that actually ended it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Connecting Katto to Claude over MCP sometimes failed with "Authorization expired or was not approved. Please start over." The confusing part: the consent page had already shown "Connecting your MCP client...", which only happens after the user approves and the key is minted. So the approval clearly worked, yet the final step claimed it had not. And it was intermittent. Some connections went through cleanly, others died on the same account minutes apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the flow is built, and why
&lt;/h2&gt;

&lt;p&gt;The security rule for our hosted MCP is that the API key must never travel through the browser. So the OAuth handoff is server to server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The worker's &lt;code&gt;/authorize&lt;/code&gt; stores the OAuth request in Cloudflare KV under a transaction id.&lt;/li&gt;
&lt;li&gt;The consent page on katto.tech authenticates the user, mints a scoped key, and POSTs it to the worker's &lt;code&gt;/oauth/deposit&lt;/code&gt; over an HMAC-signed channel.&lt;/li&gt;
&lt;li&gt;The browser is then bounced to the worker's &lt;code&gt;/oauth/callback&lt;/code&gt;, which reads that deposit back and completes the grant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The false lead
&lt;/h2&gt;

&lt;p&gt;My first guess was an HMAC secret mismatch between the app and the worker. It was not. A bad signature makes &lt;code&gt;/oauth/deposit&lt;/code&gt; reject with a visible error, and the user would have seen that. Instead they saw "Connecting...", which means the deposit POST returned 200. The key was minted and deposited fine. The failure came one hop later, at the callback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cause: eventually consistent KV
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers KV is eventually consistent. &lt;code&gt;/oauth/deposit&lt;/code&gt; writes the deposit on whichever edge location served that request. The browser is then meta-refreshed, with zero delay, to &lt;code&gt;/oauth/callback&lt;/code&gt;, which is a separate request that can land on a different edge location and read that key before it has propagated. It reads null, and the callback declares the transaction dead. When the two requests happen to hit the same location, it works. When they do not, you get "Authorization expired." It is a read-after-write across two requests, which KV explicitly does not guarantee.&lt;/p&gt;

&lt;p&gt;That is why it looked random. It was random, in the sense that it depended on which edge served each of the two requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mitigations that reduced it but did not fix it
&lt;/h2&gt;

&lt;p&gt;The first patches were the obvious ones: retry the callback read a few times (ten attempts, 500ms apart) and add a one second delay to the meta-refresh so KV gets a head start. That cut the failure rate sharply. It did not remove it. As long as the callback reads a key that another request just wrote, propagation can always take longer than your retry budget. Those were band-aids on a design flaw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: stop reading a fresh key in a second request
&lt;/h2&gt;

&lt;p&gt;The real fix was to remove the cross-request read entirely. The &lt;code&gt;/oauth/deposit&lt;/code&gt; request already holds everything it needs: the OAuth request (written at &lt;code&gt;/authorize&lt;/code&gt;, long since propagated) and the freshly minted key, in hand, in memory. So it completes the grant right there and returns the final redirect URL. The browser goes straight to the client with the authorization code. It never round-trips through the callback to re-read a key that was written moments ago.&lt;/p&gt;

&lt;p&gt;We kept the old callback as a fallback and shipped it backward compatible: the consent page uses the returned redirect URL when the worker provides one, and otherwise falls back to the callback, so an old and a new deploy can never break each other during a rollout. After the change, fresh connections stopped failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;If a value has to be available to the very next step, do not write it to eventually consistent storage and read it back in a separate request. Complete the work in the request that already holds the data, or use a strongly consistent store. Retries and delays hide the race; removing the cross-request read is what ends it. We found this by dogfooding our own MCP the way a reviewer would, before a reviewer did.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>serverless</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Connect Katto to Claude in 60 Seconds (Official MCP Server)</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Sat, 05 Sep 2026 08:33:02 +0000</pubDate>
      <link>https://dev.to/anthony_builds/connect-katto-to-claude-in-60-seconds-official-mcp-server-2ihc</link>
      <guid>https://dev.to/anthony_builds/connect-katto-to-claude-in-60-seconds-official-mcp-server-2ihc</guid>
      <description>&lt;p&gt;Katto is an AI video clipper, and it ships an &lt;strong&gt;official MCP server&lt;/strong&gt;. That means you don't have to open the app to use it: connect it to Claude once, then ask in plain English — &lt;em&gt;"clip the best moments of this YouTube video"&lt;/em&gt; — and Katto downloads, transcribes, scores and renders vertical captioned clips, then hands them back ranked by a virality score.&lt;/p&gt;

&lt;p&gt;No install, no API key pasted into a config file. Here is the whole thing, start to finish.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://katto.tech/mcp" rel="noopener noreferrer"&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%2F94natinpd57yg6ymvg5m.png" alt="Katto MCP running inside Claude: a completed job with the top clips ranked by score" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;▶ &lt;a href="https://katto.tech/mcp" rel="noopener noreferrer"&gt;Watch the 60-second demo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect in 60 seconds
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In Claude, open &lt;strong&gt;Settings → Connectors&lt;/strong&gt; and choose &lt;strong&gt;Add connector&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Paste the hosted endpoint: &lt;code&gt;https://mcp.katto.tech/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Allow&lt;/strong&gt; to sign in with your Katto account (OAuth). That's it — no key to copy, nothing stored on disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Prefer a local setup, or using Cursor, VS Code, Windsurf or ChatGPT? The &lt;a href="https://katto.tech/mcp" rel="noopener noreferrer"&gt;per-client guides&lt;/a&gt; cover each one, including the &lt;code&gt;npx&lt;/code&gt; command and the API-key header if you'd rather not use OAuth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then just ask
&lt;/h2&gt;

&lt;p&gt;Once it's connected, you talk to Katto in normal language. A first prompt to try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clip the best 3 moments of https://www.youtube.com/watch?v=YOUR_VIDEO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude calls Katto, you get back a job id, and you can poll it until the clips are ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What's the status of that job? Show me the clips with their scores.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each clip comes with its MP4 url, an SRT caption file, a title, and a 0–100 virality score, ranked highest first. When a clip's &lt;code&gt;hd&lt;/code&gt; field turns true, its 1080p render is final; the job-level &lt;code&gt;hd_ready&lt;/code&gt; flag tells an agent when every clip is done, so it never caches a link that's about to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do (16 tools)
&lt;/h2&gt;

&lt;p&gt;The connector is not a single "make clips" button — it exposes the whole pipeline as tools an agent can compose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create &amp;amp; control jobs&lt;/strong&gt; — submit a video URL, list your jobs, cancel a running job (and get the video credit refunded instantly).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the output&lt;/strong&gt; — job status and progress, the finished clips, the timestamped transcript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-render, free&lt;/strong&gt; — re-render a clip with a different reframe layout or caption style, or dub it into any of 8 languages, without spending quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discover &amp;amp; configure&lt;/strong&gt; — list the supported sources, the clip-length buckets, and the caption-style presets, plus your plan, quota and saved brand kit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It works from your own code, too
&lt;/h2&gt;

&lt;p&gt;The MCP server is a thin layer over Katto's &lt;a href="https://katto.tech/docs/api" rel="noopener noreferrer"&gt;public REST API&lt;/a&gt; — same endpoints, same responses. So whether you drive it from Claude, from Cursor, or from a script with a &lt;code&gt;sk_live_&lt;/code&gt; key, you get the same jobs, the same scored clips, idempotent retries and signed webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Connect Katto to Claude at &lt;code&gt;https://mcp.katto.tech/mcp&lt;/code&gt;, read the &lt;a href="https://katto.tech/mcp" rel="noopener noreferrer"&gt;MCP overview&lt;/a&gt; and the &lt;a href="https://katto.tech/docs/api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;, or &lt;a href="https://katto.tech/signup" rel="noopener noreferrer"&gt;start with a free account&lt;/a&gt;. If you kick the tires and something surprises you, tell us — we read every reply.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>api</category>
      <category>marketing</category>
    </item>
    <item>
      <title>I shipped a second render engine to make preview match export. It made things worse.</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Thu, 03 Sep 2026 17:04:58 +0000</pubDate>
      <link>https://dev.to/anthony_builds/i-shipped-a-second-render-engine-to-make-preview-match-export-it-made-things-worse-23p0</link>
      <guid>https://dev.to/anthony_builds/i-shipped-a-second-render-engine-to-make-preview-match-export-it-made-things-worse-23p0</guid>
      <description>&lt;p&gt;Katto is an AI video clipper. You drop in a long video, it cuts the good moments into vertical shorts, and an editor lets you fix the framing, captions and layout before you export. I build it on my own, in public. This is the story of a mistake I made in that editor, and what fixing it taught me about "what you see is what you get."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was solving
&lt;/h2&gt;

&lt;p&gt;A clip editor lives or dies on one promise. What you see in the preview is exactly what you download. If the caption sits a pixel higher in the export, if the crop drifts, if a color is off, the user stops trusting the tool. And trust is the whole product.&lt;/p&gt;

&lt;p&gt;My preview is a React composition rendered in the browser. My exports were rendered on the server with FFmpeg. Two different renderers drawing the same clip. They drifted, as you would expect. Caption fonts, emoji rendering, the exact crop on a split screen. The classic "preview does not match export."&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that felt right (and wasn't)
&lt;/h2&gt;

&lt;p&gt;So I did the obvious thing. I made the same React composition that draws the browser preview also render the export on the server, through a headless Chromium (Remotion). One component, one source of truth. Preview equals export, guaranteed.&lt;/p&gt;

&lt;p&gt;This is not a knock on Remotion. It is excellent at what it is built for. The mistake was mine. I bolted it on as a second export engine next to an existing FFmpeg pipeline.&lt;/p&gt;

&lt;p&gt;My initial clips, the ones the AI generates before you ever open the editor, were still rendered by FFmpeg. It is fast, it is proven, it runs the whole pipeline. I was not going to rip that out.&lt;/p&gt;

&lt;p&gt;So now I had two engines producing files. FFmpeg for the generated clip, Chromium and Remotion for the edited re-export. The exact divergence I set out to kill was now worse. The clip you saw in your results list (FFmpeg) and the same clip re-exported after a tiny edit (Remotion) could come out subtly different. I had turned one renderer into two and called it parity.&lt;/p&gt;

&lt;p&gt;On top of that, the Chromium render was slow. Three to four minutes per clip, against roughly 30 seconds for FFmpeg. And it failed now and then. When it failed, it silently fell back to FFmpeg and handed the user a file with their edits quietly dropped. The worst possible failure. Wrong, and quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The realization
&lt;/h2&gt;

&lt;p&gt;"Preview equals export" is not achieved by making the preview be the renderer. It is achieved by having one source of truth for the render, and letting the preview be a faithful approximation of it.&lt;/p&gt;

&lt;p&gt;A browser Player is a great preview. It should never have become a second export engine. The moment it did, I had two implementations of the same geometry, drifting on every change.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FFmpeg is the one engine that produces files.&lt;/strong&gt; Generation and re-export run through the same pipeline. Exports for a typical sub-60s clip dropped to about 30 seconds (I measured 32s), down from the three to four minutes the Chromium path took.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The browser Player is back to what it is good at,&lt;/strong&gt; the interactive preview. It renders nothing that ships.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I ported the flourishes only the browser could do&lt;/strong&gt; into FFmpeg, the per-word color-cycling captions and the stacked layout's adjustable split ratio, so the fast path covers the common cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The silent fallback is dead.&lt;/strong&gt; If a render cannot be produced faithfully, the user gets a visible error, never a quietly wrong file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One coordinate set, the last piece I am finishing.&lt;/strong&gt; When you drag a crop in the preview, those exact rectangles should be what FFmpeg uses, not a second geometry the server recomputes on its own. This is the subtle one. Even after unifying the engine, the preview computed crop rects one way and the export computed them another. I am closing it now, making FFmpeg consume the exact rects from the editor state, because the lesson is the same one level down. Any second computation of the same thing is a source of drift.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Make the preview the renderer" is seductive and usually wrong.&lt;/strong&gt; You end up with two renderers the day you have any other render path. One source of truth for the output. The preview approximates it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A second engine "for parity" becomes a hidden third source of divergence.&lt;/strong&gt; If two code paths compute the same geometry or format, they will drift. The only question is when.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent fallbacks are the cardinal sin.&lt;/strong&gt; A slightly wrong file with no warning costs more trust than an honest error. Fail loud.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify the artifact, not the logs.&lt;/strong&gt; My logs said "rendered stacked layout." I only found the real bug by pulling the actual exported MP4 off storage and looking at a frame. The pixels are the truth.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am a solo founder shipping fast, in public. This one cost me a well-intentioned detour. If you are building any editor with a preview and an export, the shortest path to trust is boring. One renderer, honest errors, and check the frames.&lt;/p&gt;

&lt;p&gt;Katto is at &lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;katto.tech&lt;/a&gt;. If you build video tooling, I would genuinely love to compare notes on preview and export parity.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>ffmpeg</category>
      <category>webdev</category>
      <category>marketing</category>
    </item>
    <item>
      <title>You can now clip video from inside your AI agent: the state of MCP for video in 2026</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Sat, 22 Aug 2026 07:03:41 +0000</pubDate>
      <link>https://dev.to/anthony_builds/you-can-now-clip-video-from-inside-your-ai-agent-the-state-of-mcp-for-video-in-2026-2jek</link>
      <guid>https://dev.to/anthony_builds/you-can-now-clip-video-from-inside-your-ai-agent-the-state-of-mcp-for-video-in-2026-2jek</guid>
      <description>&lt;p&gt;Six months ago, "AI video editing" meant opening a web app, pasting a link, and clicking around. Today you can stay in the tool you already talk to (Claude, Cursor, ChatGPT) and just say: "clip the best moments from this podcast and reframe them for TikTok." The clips come back. No tab-switching, no dashboard.&lt;/p&gt;

&lt;p&gt;The thing that made this possible is the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;. If you have heard the acronym but tuned out, here is the one-line version: MCP is a standard way for an AI agent to call a real tool. The agent does not guess how your API works. It reads a list of typed tools, picks one, fills in the arguments, and gets structured data back.&lt;/p&gt;

&lt;p&gt;I have been building a video clipper (&lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;Katto&lt;/a&gt;) with an MCP server as a first-class surface, not an afterthought, so I went and looked at where the whole clipping category actually stands on this. Here is what I found, honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why video clipping is a near-perfect MCP use case
&lt;/h2&gt;

&lt;p&gt;Most SaaS features are awkward to expose to an agent because they need a lot of back-and-forth UI. Clipping is the opposite. The whole job is: &lt;strong&gt;input a long video, get short clips out.&lt;/strong&gt; That maps cleanly to a handful of tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a clip job from a URL or an upload&lt;/li&gt;
&lt;li&gt;poll it&lt;/li&gt;
&lt;li&gt;fetch the finished clips (the MP4s, the captions, the virality score)&lt;/li&gt;
&lt;li&gt;optionally re-render one with a different layout, or dub it into another language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a workflow an agent can drive end to end. You give it a YouTube link in a sentence, it hands you back five captioned vertical clips.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landscape (as of August 2026)
&lt;/h2&gt;

&lt;p&gt;MCP is becoming surprisingly common among clipping tools. OpusClip ships a hosted server with OAuth. Reap and Submagic expose MCP endpoints. Even smaller players like Whipscribe (more transcription-and-clip-search focused) run a public MCP with a local install option.&lt;/p&gt;

&lt;p&gt;So I do not think "we have an MCP server" is much of a differentiator anymore, at least for tools targeting developers and agent workflows. What still varies a lot is &lt;em&gt;how&lt;/em&gt; it is done. Here is the factual picture, checked against each vendor's own docs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Hosted endpoint&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;Local option&lt;/th&gt;
&lt;th&gt;MCP access tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpusClip&lt;/td&gt;
&lt;td&gt;mcp.opus.pro/mcp&lt;/td&gt;
&lt;td&gt;OAuth, no API key&lt;/td&gt;
&lt;td&gt;hosted (mcp-remote for stdio)&lt;/td&gt;
&lt;td&gt;free trial, then Pro (metered per-minute)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reap&lt;/td&gt;
&lt;td&gt;mcp.reap.video/mcp&lt;/td&gt;
&lt;td&gt;OAuth, workspace-scoped&lt;/td&gt;
&lt;td&gt;hosted (mcp-remote for stdio)&lt;/td&gt;
&lt;td&gt;paid plan with API access (from ~$9.99/mo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Submagic&lt;/td&gt;
&lt;td&gt;api.submagic.co/mcp&lt;/td&gt;
&lt;td&gt;Bearer key&lt;/td&gt;
&lt;td&gt;via mcp-remote bridge&lt;/td&gt;
&lt;td&gt;shares REST API credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whipscribe&lt;/td&gt;
&lt;td&gt;whipscribe.com/mcp&lt;/td&gt;
&lt;td&gt;Bearer key (guest tier allowed)&lt;/td&gt;
&lt;td&gt;open-source stdio client&lt;/td&gt;
&lt;td&gt;free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Katto&lt;/td&gt;
&lt;td&gt;mcp.katto.tech/mcp&lt;/td&gt;
&lt;td&gt;OAuth 2.1 + DCR, or bearer&lt;/td&gt;
&lt;td&gt;npx katto-mcp&lt;/td&gt;
&lt;td&gt;all paid plans, shared quota&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Checked 2026-08-22 against vendor documentation. Things move fast in this space, if a cell is out of date, tell me and I will fix it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually differs between video clipping MCPs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Access and pricing
&lt;/h3&gt;

&lt;p&gt;This is where implementations diverge the most. Some tools meter agent usage: OpusClip offers a free trial and then a Pro plan priced per render-minute, and Submagic's MCP draws from the same credit pool as their metered REST API. Others include it lower down: Reap ships theirs on a paid plan with API access, and Whipscribe's works from a free tier.&lt;/p&gt;

&lt;p&gt;On Katto, the MCP and API are included on &lt;a href="https://katto.tech/pricing" rel="noopener noreferrer"&gt;every paid plan&lt;/a&gt;, and calls draw from the same monthly video quota with no per-minute surcharge. I did that on purpose, and I am not the only one who thinks agent access should not be a tax. But check the table against your own budget; "included" means different things at different price points.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;In practice, I have seen two useful authentication patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hosted, zero-install:&lt;/strong&gt; point an OAuth client (Claude web or desktop, or any client that supports OAuth) at the server URL and sign in. No key stored on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local key:&lt;/strong&gt; a one-line npx install with an API key, for Cursor, CI, and scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not everyone offers both. OpusClip and Reap are OAuth through their hosted endpoints; Submagic and Whipscribe authenticate with Bearer keys. Katto's hosted endpoint (&lt;code&gt;https://mcp.katto.tech/mcp&lt;/code&gt;) does OAuth 2.1 with Dynamic Client Registration so the key never touches your machine, and the same endpoint also accepts a bearer key for automation. That is why I wanted Katto to support both patterns rather than forcing one authentication model on every use case.&lt;/p&gt;

&lt;p&gt;One note on safety, because it applies to every server in the table: an MCP gives your agent real capabilities on your account. Before connecting any of them, look at what the tools can actually do, and prefer servers that keep secrets and destructive actions out of the agent's reach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool coverage
&lt;/h3&gt;

&lt;p&gt;A lot of MCP servers stop at "create job / get job." The more useful ones expose the whole pipeline. Katto currently exposes &lt;a href="https://katto.tech/mcp" rel="noopener noreferrer"&gt;15 tools&lt;/a&gt;, covering the full workflow from creating a job to transcripts, re-rendering with a different layout or caption style, dubbing into eight languages, and quota checks. The point is not the number, though. I wanted an agent to be able to do something useful &lt;em&gt;after&lt;/em&gt; the first render instead of stopping at "job complete."&lt;/p&gt;

&lt;p&gt;OpusClip goes wider still: their catalog is larger than mine and includes things Katto's MCP does not do today, like scheduling posts to connected social accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the MCP cannot tell you
&lt;/h3&gt;

&lt;p&gt;Nothing in that table tells you whether the clips are any good. That is the part that actually matters, and it is the part a tool list cannot show.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Katto is still behind
&lt;/h2&gt;

&lt;p&gt;Honesty cuts both ways, so here is the other side. OpusClip has a much more mature product around its MCP: a bigger tool catalog, social scheduling from inside the agent, and a growing pile of third-party tutorials. Katto is younger and much smaller, and some of the polish that comes from years of users banging on a product simply is not there yet. If you want the most established end-to-end agent workflow today, that is a fair reason to pick OpusClip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I actually care about
&lt;/h2&gt;

&lt;p&gt;Here is the honest bit. The MCP is not the product. It is a doorway to the product. What matters is what happens behind the tool call.&lt;/p&gt;

&lt;p&gt;For Katto specifically, I have spent most of my time on videos where the interesting moment is not spoken: sports, gameplay, trailers, b-roll. Many clipping pipelines still rely heavily on transcript signals to decide what matters. That works well for podcasts and interviews, but it gets much harder when the hook is visual rather than verbal. So Katto reads the pixels too: scene cuts, faces, motion, on-screen text, instead of relying only on the transcript. That is the capability the MCP exposes; the MCP is just how an agent reaches it.&lt;/p&gt;

&lt;p&gt;So my advice, whether you use Katto or not: do not pick a clipping tool by its MCP tool count. Pick it by what the clips actually look like on &lt;em&gt;your&lt;/em&gt; kind of footage, then check that it has a clean, ungated, OAuth-capable MCP so your agents can drive it. Both matter. But only one is the reason someone keeps using the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in one line
&lt;/h2&gt;

&lt;p&gt;If you already have an MCP client, point it at &lt;code&gt;https://mcp.katto.tech/mcp&lt;/code&gt; and sign in with Katto (no key on disk). Or run it locally:&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;"mcpServers"&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;"katto"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"katto-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"KATTO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk_live_your_key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then just ask your agent to clip something. That is the whole point.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am building Katto in public. If you are doing anything with agents and media, I would genuinely like to hear what tools you wish existed. Find me at &lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;katto.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>buildinpublic</category>
      <category>video</category>
    </item>
    <item>
      <title>I Built a SaaS. Then I Asked ChatGPT to Recommend It. It Had Never Heard of Me.</title>
      <dc:creator>anthony papoti</dc:creator>
      <pubDate>Thu, 20 Aug 2026 02:56:47 +0000</pubDate>
      <link>https://dev.to/anthony_builds/i-built-a-saas-then-i-asked-chatgpt-to-recommend-it-it-had-never-heard-of-me-1dfc</link>
      <guid>https://dev.to/anthony_builds/i-built-a-saas-then-i-asked-chatgpt-to-recommend-it-it-had-never-heard-of-me-1dfc</guid>
      <description>&lt;p&gt;&lt;em&gt;A solo founder's distribution reckoning, in real numbers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This morning I opened my analytics dashboard already convinced my product was dead. Nobody's signing up, I told myself. I'd been refreshing the same screen for an hour, watching a number that barely moved, feeling the whole thing slip away.&lt;/p&gt;

&lt;p&gt;Then I did the one thing I'd been avoiding: I stopped staring at the realtime counter and actually read the data.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;Katto&lt;/a&gt;, an AI tool that turns long videos into short vertical clips. It's pre-launch, no ad spend, no marketing budget, just me. And here is what the database actually said, once I looked past a slow Tuesday morning:&lt;/p&gt;

&lt;p&gt;129 users. 58 of them in the last seven days. All organic.&lt;/p&gt;

&lt;p&gt;That is not a dead product. So why did it feel like one? Because I'd been judging a half-empty morning instead of the trend, and because two silent leaks were quietly undoing everything the traffic brought me. Finding those two leaks was the real work of the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak one: the first run was failing
&lt;/h2&gt;

&lt;p&gt;The first gut punch was the failure rate. On one recent day, close to six out of ten jobs failed.&lt;/p&gt;

&lt;p&gt;I dug into the actual reasons instead of panicking, and the picture split cleanly in two. About half the failures were the download step choking on YouTube's bot wall: a user pastes a link, and the video never comes down. The other half were free-plan users bringing videos longer than the plan allows and hitting a hard cap.&lt;/p&gt;

&lt;p&gt;The first half is a bug. The second half is a signal (people are showing up with real long-form content and getting a wall instead of an upsell), but that is a story for another post.&lt;/p&gt;

&lt;p&gt;The download failures are the ones that hurt, because they are invisible to me and fatal to the user. Someone hears about your tool, signs up, brings their video, and the very first thing they try just dies. They don't file a bug. They don't email you. They leave, and they never come back, and they certainly never pay.&lt;/p&gt;

&lt;p&gt;So I spent the day on unglamorous infrastructure: I migrated the backend to new infra and the download failures stopped. I did not take that on faith. I pulled the exact timestamps to make sure I wasn't fooling myself, and the last failure was the day before, with zero since the fix. Boring work. But the gap between a user who stays and one who's gone often lives in exactly this kind of boring work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak two: even when it works, nobody finds it
&lt;/h2&gt;

&lt;p&gt;The second gut punch was bigger, and no infrastructure fix touches it.&lt;/p&gt;

&lt;p&gt;I opened Google Search Console. I rank on page two or three for basically everything. My own brand name ranks around position 28, because "katto" is a common word and I haven't earned the right to own it yet. (Curiously, "katto ai" ranks near the top, which tells me something about which name to lean on.)&lt;/p&gt;

&lt;p&gt;Then I ran the test that actually matters in 2026. I asked ChatGPT, "best AI clipping tools 2026." It confidently listed ten tools. I was not one of them. I tried again with a few phrasings. Same result. The AI that a growing share of my potential users now ask for recommendations has no idea I exist.&lt;/p&gt;

&lt;p&gt;That stung more than the failure rate. My product is good. People who try it mostly like it. And it does not matter, because the people who would try it never hear the name.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not an indexing problem, it's a penetration problem
&lt;/h2&gt;

&lt;p&gt;Here is the reframe that turned the despair into a plan.&lt;/p&gt;

&lt;p&gt;I am fully indexed. Google has my pages. The problem is not that &lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;Katto&lt;/a&gt; is hidden. The problem is that &lt;a href="https://katto.tech" rel="noopener noreferrer"&gt;Katto&lt;/a&gt; is not written about anywhere that counts.&lt;/p&gt;

&lt;p&gt;The tools an AI recommends, and the tools that rank, are the ones that appear across third-party pages: the "best OpusClip alternative" roundups, the review blogs, the directories, the creators who mention them. That is what a search engine and a language model both read as "this thing is real and people vouch for it."&lt;/p&gt;

&lt;p&gt;A competitor I had genuinely never heard of has something like 1,300 referring domains pointing at it. I have maybe three. That single gap explains almost everything about why they surface in a generic answer and I don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan: 3 to 10 to 25 to 50
&lt;/h2&gt;

&lt;p&gt;The mistake would be to look at 1,300 and try to chase it. That way lies buying garbage links off Fiverr and torching whatever trust I've built.&lt;/p&gt;

&lt;p&gt;The honest target for a young product is not 1,300. It's 3 to 10 to 25 to 50 real referring domains. The rule of thumb people repeat is that somewhere around 25 to 50 genuine domains a new site stops looking new. I can't prove that exact number, but the direction is obviously right, and the first ten are gettable by hand, without paying anyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A handful of honest directory listings (a few done, a few to go). The first concrete one on my list is AlternativeTo, where the OpusClip alternatives page lists over fifty tools, submissions are free and community-driven, and there's even an "EU-based" filter with almost nothing in it. French founder, euro pricing: I'd be nearly alone in a niche no competitor can claim.&lt;/li&gt;
&lt;li&gt;Two or three real reviews from people who actually use the tool.&lt;/li&gt;
&lt;li&gt;A link in a genuinely neutral "best AI clipping tools" article. This one is harder than it looks, because most of those roundups turn out to be a competitor quietly ranking itself first. The truly neutral ones are rarer (a few independent blogs, community sites). So the other half of this move is writing my own honest roundup, which, awkwardly, is exactly what every tool that ranks already does, and exactly what the language models read.&lt;/li&gt;
&lt;li&gt;My own founder presence, building in public, generating mentions over time.&lt;/li&gt;
&lt;li&gt;One or two creators who try it and talk about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I am deliberately not doing: buying backlink packages, and paying a hundred dollars a month for an SEO tool to confirm a problem I already understand. That money goes into outreach and into getting the product in front of real creators instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned today
&lt;/h2&gt;

&lt;p&gt;Three things, mostly about myself.&lt;/p&gt;

&lt;p&gt;Your product being good is rarely the problem. Being found is. I spent months polishing features when the real gap was that almost nobody was arriving to see them.&lt;/p&gt;

&lt;p&gt;Stop staring at the realtime counter. It is noise. A slow morning is not a trend, and doom-refreshing a dashboard is a way to feel productive while learning nothing. Look at the weekly number, once a day, and get back to work.&lt;/p&gt;

&lt;p&gt;And distribution is not a thing you do after the product. For an indie founder with no budget, it is the product's twin, and it starts at three referring domains and a lot of unglamorous outreach.&lt;/p&gt;

&lt;p&gt;The traffic is already coming, quietly, on its own. My job now is to stop losing the people who arrive, and to slowly earn my way onto the pages that decide who gets recommended.&lt;/p&gt;

&lt;p&gt;I'll report back at ten domains.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>buildinpublic</category>
      <category>saas</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
