<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>MITM LLC Blog</title>
    <link>https://mitmllc.com/blog/</link>
    <atom:link href="https://mitmllc.com/blog/feed.xml" rel="self" type="application/rss+xml" />
    <description>Notes from MITM LLC on product launches, technical decisions, and lessons learned building indie Mac software.</description>
    <language>en-us</language>
    <lastBuildDate>Wed, 07 Oct 2026 16:14:44 GMT</lastBuildDate>
    <item>
      <title>Sky Critter is now on Android</title>
      <link>https://mitmllc.com/blog/skycritter-android/</link>
      <guid isPermaLink="true">https://mitmllc.com/blog/skycritter-android/</guid>
      <pubDate>Wed, 07 Oct 2026 00:00:00 GMT</pubDate>
      <description>The pixel kitten crossed platforms: Sky Critter is live on Google Play. Same Swift brain on both platforms, one shared critter roster, and the story of how a solo studio shipped the port.</description>
      <author>noreply@mitmllc.com (Rene Zelaya)</author>
      <content:encoded><![CDATA[<p>Sky Critter started on iPhone. As of this morning it&#39;s on Android too, live on <a href="https://play.google.com/store/apps/details?id=com.mitmllc.skycritter">Google Play</a>.</p>
<p>Android was always the plan. The blocker was more mundane than technical: I needed a real Android phone to test on, and I didn&#39;t want to buy one in Europe. So the port waited until I was back in the United States and could pick up an affordable used phone as a test device. Users had been asking, and I also wanted to know whether my way of working could handle a second platform.</p>
<figure><img src="/blog/assets/skycritter-android/android-in-hand.jpg" alt="Ziggy the raccoon handling a clear 75-degree afternoon in Santa Barbara, on Android, in my hand." decoding="async"><figcaption>Ziggy the raccoon handling a clear 75-degree afternoon in Santa Barbara, on Android, in my hand.</figcaption></figure>
<p>It&#39;s the same app on both platforms: a pixel critter lives in your sky and reacts to the real weather outside your window. Soggy in rain, giddy in snow, sun-baked in heatwaves, scared of thunder, and a werewolf on real full moons. Underneath the cuteness is a genuine forecast: hourly temperatures, wind, humidity, pressure, sunrise and sunset, moon phases, and a yesterday-versus-today comparison. The whole critter roster made the trip, too. Tabby ships free; the rest of the crew (the cats, the dogs, the raccoons, one extremely calm capybara) are one-time cosmetic purchases, and weather features are never paywalled on either platform. You can meet everyone in the <a href="https://skycritter.app/critters">critter gallery</a>.</p>
<h2>How a one-person studio ships a second platform</h2>
<p>The Android app isn&#39;t a rewrite, and it isn&#39;t Flutter or React Native. It&#39;s the same Swift codebase as the iPhone app, running on Android via <a href="https://skip.tools">Skip</a>, an open-source toolchain that compiles Swift natively for Android and maps SwiftUI to Jetpack Compose. The app&#39;s brain — the weather logic, the 18-state critter mood engine, the bilingual voice lines, every pixel sprite — lives in one shared Swift package that both platforms consume, with a single test suite that runs on macOS, Linux, and Android to prove the two apps behave identically.</p>
<p>I didn&#39;t guess that this would work. The port started as a one-week feasibility spike with explicit go/no-go criteria. The spike found exactly one hard problem: high-frequency ambient animation (the twinkling stars, the drifting rain and snow) was about 30x too slow through the cross-platform bridge. So that one layer is hand-written Kotlin/Compose on Android, driving the same art. The sky renderer and the billing layer are per-platform by design; everything else is shared. The weather data comes from Apple&#39;s WeatherKit on both platforms, so the Android app reads the same forecast as the iPhone app, with a small cloud service handling the authentication Android can&#39;t do natively.</p>
<p>All in, the port took about ten weeks of part-time work, July to October, and I&#39;m not maintaining two codebases at the end of it.</p>
<h2>The text that made it worth it</h2>
<p>I made a friend in Barcelona who has an Android phone, and it&#39;s been a real bummer that she hasn&#39;t been able to enjoy anything I&#39;ve been building. This morning, after the release went live, I sent her the link. She sent back &quot;you&#39;re the boss&quot; with the little flex-arm emoji.</p>
<p>That&#39;s the thing, really. Regardless of what platform my friends are on, they can now enjoy what I&#39;ve built.</p>
<p>Sky Critter runs on Android 10 and newer, and on iPhone with iOS 17 and newer. Get it on <a href="https://play.google.com/store/apps/details?id=com.mitmllc.skycritter">Google Play</a>, on the <a href="https://apps.apple.com/app/id6771334776">App Store</a>, or poke the sky first at <a href="https://skycritter.app/">skycritter.app</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>I wanted to learn more about my singing voice, so I built a tool to understand it better</title>
      <link>https://mitmllc.com/blog/timbr/</link>
      <guid isPermaLink="true">https://mitmllc.com/blog/timbr/</guid>
      <pubDate>Tue, 06 Oct 2026 00:00:00 GMT</pubDate>
      <description>I play guitar and I record myself playing and singing, and a while back I hit a plateau. What kicked this off was Peter Gabriel&apos;s &quot;In Your Eyes.&quot; I&apos;d been listening to it and loving it, and then I tried it at karaoke and I just couldn&apos;t get it, which bugged me, because I can usually land other singers pretty close, both their notes and their style.</description>
      <author>noreply@mitmllc.com (Rene Zelaya)</author>
      <content:encoded><![CDATA[<p>I play guitar and I record myself playing and singing, and a while back I hit a plateau. What kicked this off was Peter Gabriel&#39;s &quot;In Your Eyes.&quot; I&#39;d been listening to it and loving it, and then I tried it at karaoke and I just couldn&#39;t get it, which bugged me, because I can usually land other singers pretty close, both their notes and their style. I&#39;ve sung songs higher than that one and songs lower than it, so I couldn&#39;t work out why the middle of my range was the part tripping me up, or where my voice was actually failing me. I never studied music, so I didn&#39;t really have a way to answer any of that, or even the vocabulary to describe what I was hearing.</p>
<p>That&#39;s the reason Timbr exists. It&#39;s browser-based (gettimbr.com), and the idea is simple: you record a short clip, it measures what your voice is actually doing (pitch, range, steadiness, vibrato, where you spend your time), and it shows that back to you as a voice map with a plain-language read next to it. The options I had before I built it didn&#39;t really help. An app that just hands you a label like &quot;you&#39;re a tenor&quot; is a black box I can&#39;t check, and a coach costs money and means having someone in the room. I wanted to be able to see it on my own.</p>
<p>One note on scope before you read too much into any of this. It&#39;s a private preview with a handful of users, so there aren&#39;t growth charts to show you, and the paid tier is built but switched off for now. I did the whole thing solo: the research, the product design, the data visualization, and the part I care most about, which was tuning how the model reads the measurements and judging whether the measurements were any good to begin with.</p>
<h2>Why I didn&#39;t just bolt on a chatbot</h2>
<p>The default move right now is to drop a general LLM into your product and call it a feature, and I didn&#39;t want to do that, for a few reasons.</p>
<p>Most of the trouble with LLMs in products comes from the input. If you give users an open text box, they&#39;ll take it wherever they want, which opens the door to prompt injection and a hundred conversations that have nothing to do with the point of the tool. So instead I gave people a set of preloaded questions, the ones I&#39;d found genuinely useful when I was poking at my own voice. That keeps people on task, it keeps my costs under control (this is a free tool, and I wasn&#39;t about to hand out an unlimited chatbot), and it keeps the whole experience narrow and within my control.</p>
<p>The deeper reason is sycophancy. Chatbots get tuned so that people like them, and a lot of that likability comes from flattery, which is fine for a companion app but works against you in a diagnostic one. If the whole point is to tell you something true about your own voice, flattering you doesn&#39;t move you forward, it just feels good for a second. So the model never hears your audio at all. It receives a structured, plain-text summary of the measurements and reasons from those numbers, and the system prompt tells it as much: you&#39;re reasoning over numbers, not audio, and you did not hear the recording. Working only from numbers means it can&#39;t make up a performance it never actually heard.</p>
<h2>The product kept telling people they&#39;d failed</h2>
<p>There were two problems I only found by using the thing myself and watching other people use it.</p>
<p>The first one is a little embarrassing. I build on a MacBook, and most of my users sing on their phones. Someone was using the live tuner on their phone and the whole screen kept bouncing, because every time the readout changed width the row would re-wrap and shove everything underneath it down a bit. I&#39;d never once seen it happen, because on my laptop the layout had plenty of room. The fix was simple enough (reserve the worst-case width and drop the hint text on small screens), but it was a reminder that I&#39;d been designing from the wrong device, and the only reason I caught it was that I put the tool in front of someone who actually lives where my users live.</p>
<p>The second problem was bigger, and it was about what the tool was willing to judge. Early on it treated every single note as though you&#39;d meant to hold it, so I&#39;d sing a phrase and it would come back and tell me I hadn&#39;t hit any of them and that my vibrato was all over the place, when all I&#39;d really done was slide through those notes on my way somewhere, the way you do when you&#39;re singing expressively. If every note gets scored as a sustain, most of a song comes back as a failure, which isn&#39;t how a song works, it&#39;s how practicing a single note works. So I had to teach the tool the difference between a note you&#39;re parking on and a note you&#39;re traveling through. The signal that ended up working was slope: a wide, clean sweep gets read as a glide and is never scored for steadiness, and any note whose pitch is still moving quickly is treated as moving rather than held. I also smooth the vibrato out first so that expression doesn&#39;t get mistaken for a wobble.</p>
<p>That shaped the visualization as much as the scoring. The tool is measuring flatness, not whether you matched a melody, because it has no melody to compare you against, so it&#39;s deliberately blind to that and won&#39;t raise an alarm just because your pitch moved around. On the voice map itself, the vertical bar through each note shows how far that note drifted, the color runs from green to red depending on steadiness, and the size of the dot shows how often you&#39;ve sung it.</p>
<figure><img src="/blog/assets/timbr/voice-map.png" alt="Figure: the Voice Map. Each dot is a held note placed by pitch, the bar through it shows how far that note drifted, the color runs green to red by steadiness, and the dot size shows how often I've sung it." decoding="async" width="1958" height="1000"><figcaption>Figure: the Voice Map. Each dot is a held note placed by pitch, the bar through it shows how far that note drifted, the color runs green to red by steadiness, and the dot size shows how often I've sung it.</figcaption></figure>

<h2>Saying what I don&#39;t know</h2>
<p>The thing I keep coming back to is that I don&#39;t want to flatter people about their voice, because I wouldn&#39;t want anyone doing that to me about mine. The best feedback I&#39;ve ever gotten was clear about its own limits and about where it was coming from, and that kind of integrity does more than just feel right, it gets the user a better outcome and it sets honest expectations for the business too.</p>
<p>So the honesty is built into the data the model works from rather than added on as a polite tone at the end. The thresholds that decide whether a note counts as steady or drifting are hand-set and flagged as provisional in the code, because that&#39;s exactly what they are. They haven&#39;t been validated against a real coach, and the product says so on every screen: &quot;This is an informed starting hypothesis from measurement, not a verdict. Register and voice-type reads are inferred from provisional thresholds. A real voice coach is the ground truth.&quot; Register only ever shows up as a rough zone estimated from your glides, and when the glides disagree with each other, the tool tells you that directly instead of guessing.</p>
<figure><img src="/blog/assets/timbr/glide.jpg" alt="Figure: the Glide tool. Most sweeps come back &quot;no break detected,&quot; one shows a low-confidence break, and the passaggio zone is labeled provisional. The product refuses to state a register it can't stand behind." loading="lazy" decoding="async" width="1184" height="540"><figcaption>Figure: the Glide tool. Most sweeps come back "no break detected," one shows a low-confidence break, and the passaggio zone is labeled provisional. The product refuses to state a register it can't stand behind.</figcaption></figure>

<p>Leaving register out entirely was a deliberate call, and it&#39;s a decent example of how I work with AI. I&#39;m not a vocal expert. The tool harvests your timbre data, the spectral shape of your sound, and the model could easily have handed you a register or a voice type based on it. Early on I wanted to understand how that&#39;s actually determined, so rather than taking the model&#39;s default answer I interrogated it and dug into what that design decision would really mean for a user. What I came away with was that reading someone&#39;s true singing register takes a human in the room, watching multimodal cues like breath and how the body is working while the person sings, and that&#39;s not something I can rebuild from a spectral number. So I left it out, and the tool harvests that data without pretending to know what it means.</p>
<p>Choosing the model was part of the same effort. What I needed was structural honesty: state the measured things plainly, hedge the inferred things, and never hand someone a confident single number for something that&#39;s genuinely fuzzy, so instead of saying &quot;your break is at F4&quot; it says something more like &quot;somewhere around E4 to G4, and the signal there is messy.&quot; One model I tried had great formatting but was far too sure of itself, making claims about your physiology that I had no business making, so I ended up on a cost-efficient model (DeepSeek-V4.1-Flash via DeepInfra, per the project&#39;s docs) and did the real work in the prompt, with evidence tiers that don&#39;t blur together and a rule to report the confidence band rather than its midpoint. The provider sits behind a clean abstraction, so switching to any model with an API key is a single environment variable, which matters a lot more for how fast I can iterate than it does as a feature on a list.</p>
<h2>What this is, if you&#39;re hiring</h2>
<p>I came up through design and research, and I took the initiative to work directly with engineering to build this. I can have the conversation with AI the way I&#39;d have it with an engineer, because I&#39;ve spent years on cross-functional teams, and I&#39;m not shy about pushing back on the AI I&#39;m building with. Since I&#39;m not an expert in vocal physiology, I made a point of understanding the consequences of a design decision before shipping it rather than accepting whatever the model gave me by default. I also think like a product person, in that I have limited bandwidth and rely on research, real usage, feedback, and strategy to decide where it goes. The question sitting underneath the whole project is how to give singers a fuller picture of the data around their voice without being prescriptive about what it means, and the best work I did on Timbr came down to answering that honestly, which mostly meant being disciplined about what not to claim.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The customers who helped me fix my own app</title>
      <link>https://mitmllc.com/blog/customers-who-helped-fix-my-app/</link>
      <guid isPermaLink="true">https://mitmllc.com/blog/customers-who-helped-fix-my-app/</guid>
      <pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate>
      <description>A stranger caught a real bug in my WhisperPad code, and helped me diagnose it with logs. Here&apos;s what happened during launch week.</description>
      <author>noreply@mitmllc.com (Rene Zelaya)</author>
      <content:encoded><![CDATA[<p>Launch was in early June. By Thursday morning of that week I woke up to an email from a paying customer. He&#39;d bought the lifetime license. And his microphone didn&#39;t work.</p>
<p>That was the moment I decided my morning routine could wait.</p>
<p>The first emails about this issue had actually come in earlier, from free-tier users right after launch. I engage with free users too. Why not? They often give the most useful early signal anyway. But when the paying customer wrote in, there was a different kind of pressure. Someone trusted me enough to commit to a lifetime license. I needed to make that good.</p>
<p>When the first free-tier email came in, my honest first reaction was, oh no, this is a nightmare. That instant shock of did I just release something that wasn&#39;t ready. But I took a step back. I&#39;d tested this thing across ten other Macs before launch. So I told myself, okay, hop back in the driver&#39;s seat and start diagnosing.</p>
<p>I&#39;ve done this work before. I used to be a customer support agent, all of it over the phone. This time it&#39;s email, but the empathy and the acknowledging, that part is muscle memory. I could do it in my sleep. The funny thing is it feels better doing it for my own product. When I was getting paid to support someone else&#39;s thing, sure, I had some loyalty to it because I worked there. But it wasn&#39;t mine. And I think a lot of founders and engineers and designers get so far removed from the front line that they forget who they&#39;re actually building for. Sitting on the front line again was humbling. You get much faster signal on what&#39;s broken. You become the pattern detection.</p>
<h2>What Tom did</h2>
<p>I&#39;ll call him Tom. Tom was using the free version, so he didn&#39;t really owe me any debugging time. The first thing he did that I&#39;m thankful for is he reached out at all instead of just bouncing, which is already more than most people do. Then I gave him some troubleshooting steps, and he actually did them. Some of those steps meant punching commands into the terminal, collecting logs, and sending the output back to me.</p>
<p>Here&#39;s the part that made me laugh. Tom opened up his own Claude Code to look at the logs, and his Claude pointed at a theory. So we had this back and forth where he&#39;s telling me, my Claude says it&#39;s this, and from the logs it wasn&#39;t definitive. Then my Claude agent took a look. What confirmed it was an actual bug was a second email, same issue. Then a third.</p>
<p>Even though Tom eventually found a workaround for his own machine, those initial logs gave me enough signal to go looking. And I found a real bug. The Direct-distribution build had shipped without a microphone entitlement. The Mac App Store version got the microphone entitlement automatically through the sandbox settings, but the Direct version disables the sandbox so it can do auto-paste, and I&#39;d forgotten to manually add the entitlements file as a replacement. Without that entitlement, the app technically had no permission to access the microphone. Most people worked fine because macOS was lenient about checking. A few hit stricter enforcement and the app just failed silently. Tom helped me, not just himself.</p>
<h2>The thing I was actually scared of</h2>
<p>Watching it fail in front of the biggest one-day audience I&#39;ve ever had was terrifying. But the fear was specific. I&#39;m putting a product out there and I&#39;m putting a price on it. I don&#39;t want to be embarrassed. I don&#39;t want to ship something pre-baked and burn goodwill by rushing to market. I&#39;ve seen that happen a lot, and as a UX researcher it&#39;s a pattern I criticize constantly: teams ship features without testing and the users pay for it with a bad experience. I did not want to be that guy. I really don&#39;t like making users your guinea pigs unless they actually volunteered, with consent and compensation, not by downloading something they thought was finished.</p>
<p>To be fair, there were close to 200 direct downloads by that point, plus whatever came through the Mac App Store, and only three people hit this. It&#39;s rare. And they were chill about it. Nobody wants to create a bad experience for somebody else, but the people who ran into it gave me a way out.</p>
<h2>Shipping the fix</h2>
<p>The fix itself wasn&#39;t stressful. From the time I sat down to the time it was out the door was about an hour and twenty.</p>
<p>If this had happened to me back when I built websites by hand, it would have been a different night. Stuff like this did happen when I freelanced, and I&#39;d be up at 1am on a customer&#39;s downed site, working for two hours, hunting for the right line of code, trying to calm myself down while something is actively broken. The hardest part was always just finding the file to touch. With Claude Code I find it fast, make the change, and ship. Honestly I spent more time writing emails to customers than I spent in the codebase.</p>
<p>And it wasn&#39;t stressful because I already knew what was wrong. The back and forth with Tom had handed me that. With the logs, I can track a bug down and squash it. Without them it&#39;s just &quot;my mic didn&#39;t work, figure it out, it&#39;s on you.&quot;</p>
<p>To the people who wrote in: you gave my product a chance, and even when there was some turbulence, you reached out to help me fix it. Thank you.</p>
<hr>
<p><em>This post was dictated using <a href="https://mitmllc.com/whisperpad">WhisperPad</a>. The cadence is the way I actually talk.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Apple changed their mind</title>
      <link>https://mitmllc.com/blog/apple-changed-their-mind/</link>
      <guid isPermaLink="true">https://mitmllc.com/blog/apple-changed-their-mind/</guid>
      <pubDate>Tue, 09 Jun 2026 00:00:00 GMT</pubDate>
      <description>Apple reversed the 2.4.5 rejection of WhisperPad. The full version is back on the Mac App Store.</description>
      <author>noreply@mitmllc.com (Rene Zelaya)</author>
      <content:encoded><![CDATA[<p>It was Friday night. I was out with classmates after class, drinks at a bar, the kind of after-week wind-down where you&#39;re not expecting anything important to happen. My phone rang. Caller ID said Apple. I was in the middle of a busy bar and didn&#39;t want to pick up, and honestly part of me was skeptical too. Apple calling me directly seemed like the kind of thing a scam might pretend to be.</p>
<p>I let it go to voicemail.</p>
<p>When I checked later, the auto-transcription Apple does on incoming voicemails gave me the gist. A member of Apple&#39;s App Review team said he wanted to talk about WhisperPad. He didn&#39;t say why. I tried to call back at 9:30 Barcelona time and it was already after his working hours in Pacific. I told my classmates Apple was calling me. It felt unreal. Apple is giving me a phone call. That was the framing in my head all weekend.</p>
<h2>The wait</h2>
<p>I had a long weekend ahead of me wondering what the call was about. I let myself hope it was a reversal. I&#39;d been very public about the original rejection.  Part of me was nervous Apple might be calling for a different reason. Maybe they wanted to communicate something firm in private rather than in writing.</p>
<h2>The call</h2>
<p>On Monday I was in class. They called during a break. I picked up.</p>
<p>The conversation was three or four minutes. The Apple reviewer was calm and friendly. He told me the App Review board had taken a closer look at my original rejection and decided to reverse it. WhisperPad&#39;s use of the Accessibility API was approved. I could resubmit the version they&#39;d previously rejected, using the same method, and reference our conversation in the review notes.</p>
<p>He said: &quot;Sometimes a single reviewer&#39;s call gets revisited by the board, and the decision flips.&quot; I thanked him for his time. End of call.</p>
<h2>What I felt</h2>
<p>Mostly confused, then thankful. I&#39;d been operating for two weeks on the assumption that this path was permanently closed. I&#39;d forked the app into two separate SKUs. I&#39;d stood up direct distribution with Paddle for payments and Sparkle for updates. I&#39;d written architectural decision records explaining why the fork was permanent. And now, with one phone call, all of that had changed.</p>
<p>Ultimately, I&#39;m happy Apple reached out. They approved the app. I&#39;m stoked about it. The story is closed in the best possible way.</p>
<h2>The work of un-doing the fork</h2>
<p>The reversal sounded simple on the phone. Resubmit the version you originally built. In code, it was anything but simple.</p>
<p>When I split WhisperPad into two versions in May, I&#39;d guarded the differences behind a compile flag called <code>MAS_BUILD</code>. The flag had quietly come to mean two unrelated things. One: legitimate, permanent exclusions required by the App Store sandbox, like the in-app crash reporter that reads files the sandbox blocks. Two: feature reduction to dodge the 2.4.5 rejection — the clipboard-only paste path, the disabled transcription modes, the abbreviated onboarding. With the reversal, I needed to keep the first kind and undo the second.</p>
<p>Restoring the full app meant unwinding <code>MAS_BUILD</code> guards across seven files. Core insertion logic. App startup permissions. Settings UI. The onboarding flow. The FAQ. The result panel. Most of the work was deleting code. The compliance changes had added complexity, and the reversal removed it.</p>
<p>Net result: forty-four lines fewer than before.</p>
<h2>The Sparkle dance</h2>
<p>The trickier piece was the auto-updater. Sparkle, the long-standing framework that handles updates for non-App-Store Mac apps, ships helper executables that are incompatible with the sandbox by design. The sandbox exists to keep apps from escaping into the rest of the system. Sparkle&#39;s whole job is to escape and replace the parent app. The App Store will reject any bundle that contains a non-sandboxed executable.</p>
<p>So Sparkle has been toggled in and out of the codebase four times in two weeks. Out for the compliant 1.7. Back in for the direct launch. Out again on Monday, briefly, because the moment I tried to upload the 1.8 App Store build the validation failed with the exact error from before — Sparkle&#39;s helpers were back in the bundle because Sparkle is linked as a Swift Package and embeds automatically. Restored immediately after the MAS upload cleared.</p>
<p>That dance is fragile and manual. The permanent fix is conditional framework linking or revisiting the single-target decision, both of which are on the list. For now, git is the safety net.</p>
<h2>What this means for the studio</h2>
<p>The direct-distribution channel from mitmllc.com still exists. I&#39;m keeping it. People who want to save the App Store markup can buy direct. People who want Apple&#39;s trusted purchase flow can buy from the App Store. The same product, two channels, neither one a compromise.</p>
<p>Without the original rejection, I never would have built the direct channel. Sometimes the workaround you&#39;re forced to build becomes the thing you&#39;re glad you have. I learned a lot in two weeks I wouldn&#39;t have learned in two months of straight App Store work. The infrastructure I now have for direct distribution will outlast WhisperPad. It&#39;s there for whatever I ship next under MITM LLC.</p>
<h2>What I&#39;d say to other developers</h2>
<p>Build your own distribution channel anyway. Not because it&#39;ll force Apple to reverse a rejection ...though, hey, maybe it will, who knows. Build it because it&#39;s an asset you&#39;ll want to have regardless. And triple-check your work when you do. The biggest headache I had during the launch wasn&#39;t anything Apple did. It was that some users downloaded a version with a broken auto-updater and a stale purchase link, and they couldn&#39;t get to my payment page. That cost me real customers and a lot of email apologies. Sparkle, entitlements, license-key delivery etc.</p>
<h2>Closing</h2>
<p>Overall I&#39;m thankful this saga happened. I made real connections answering emails from customers, from people reaching out about RSI in general, from other Mac developers who&#39;d hit the same wall. It was good to see those connections form and to talk with people dealing with similar issues. I learned a lot.</p>
<p>It&#39;s been an interesting two weeks. Super hectic, super fun, and all of it on top of writing a master&#39;s thesis and building my final presentation. June is going to be a busy month.</p>
<p>WhisperPad is now what it was always supposed to be, in both places it was supposed to be. The loop is closed.</p>
<p>If you want to try it, the direct version is at <a href="https://mitmllc.com/whisperpad/">mitmllc.com/whisperpad</a>. The Mac App Store version is also live with the full auto-paste functionality now. The free tier is 120 minutes a month either way. If you want to write to me here: <a href="mailto:contact@mitmllc.com">contact@mitmllc.com</a></p>
<hr>
<p><em>This post was dictated using WhisperPad.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Apple rejected my dictation app for using the accessibility API</title>
      <link>https://mitmllc.com/blog/apple-rejected-my-dictation-app/</link>
      <guid isPermaLink="true">https://mitmllc.com/blog/apple-rejected-my-dictation-app/</guid>
      <pubDate>Wed, 27 May 2026 00:00:00 GMT</pubDate>
      <description>Apple rejected WhisperPad under Guideline 2.4.5 for using the accessibility API to paste transcribed text. After an appeal that ended in another rejection, I split it into two versions — a compliant one on the App Store, and the original auto-paste version, direct.</description>
      <author>noreply@mitmllc.com (Rene Zelaya)</author>
      <content:encoded><![CDATA[<p>I built WhisperPad because I needed it. In the fall of 2024 the joints in my fingers started to hurt when I typed. Maybe the bill came due for spending most of my life on a keyboard: a childhood of video games, then 10 years working in tech. It got worse throughout the winter, and by early 2025, I could not type for sustained stretches without triggering an unsustainable level of pain. It was a progressive injury, so there was no single dramatic moment; just a slow narrowing of how much I could do in a day.</p>
<p>That narrowing arrived at an inconvenient time. I was between jobs and trying to decide what came next, and I had landed on applying to a master&#39;s program in human-computer interaction. My biggest fear about it was not whether I could keep up mentally. It was whether my hands would let me produce the work fast enough to keep pace.</p>
<h2>What WhisperPad does (or did, anyway)</h2>
<p>WhisperPad lives in your menu bar. You press a keyboard shortcut, you talk, it transcribes what you said locally on your Mac, and it places the text into whatever field your cursor is in. Nothing is sent to a server. If you have clicked away by the time it finishes, the text is on your clipboard and you can paste it wherever you want. That is the whole app. The point was to make getting words out of my head and into the computer cost as few hand movements as possible.</p>
<figure>
  <video controls preload="metadata" playsinline poster="/blog/assets/apple-rejected-my-dictation-app/window-indicator-poster.jpg">
    <source src="/blog/assets/apple-rejected-my-dictation-app/window-indicator.mp4" type="video/mp4">
    Your browser does not support the video tag. <a href="/blog/assets/apple-rejected-my-dictation-app/window-indicator.mp4">Download the video</a>.
  </video>
<figcaption>The window indicator that appears while you&#39;re dictating.</figcaption>
</figure>

<p>In April, Apple rejected an update to my Mac dictation app, WhisperPad, under Guideline 2.4.5. Their position was that I was using the accessibility API in a way that wasn&#39;t an accessibility use. The app exists because I have a hand injury. Apple had approved earlier versions doing the same thing. This time they did not.</p>
<p>I had used Apple&#39;s built-in dictation first, and the experience was a particular kind of frustrating. The transcription was close but rarely right, and every correction meant going back in with the keyboard, deleting, retyping. I was hurting my hands to fix the tool that was supposed to be saving them. That is the specific problem I wanted to solve: not &quot;transcribe my voice,&quot; but &quot;transcribe my voice well enough that I am not constantly typing corrections.&quot;</p>
<p>I will be honest about how I approached this. I did not do much market research. There were probably good tools out there already, but most sent your audio to a server somewhere, and I wanted one that didn&#39;t. When I want something, my first question is usually &quot;can I just build it,&quot; not &quot;what can I buy.&quot;</p>
<p>The first version was rough, but I used it every day and kept improving it. I shared it with a couple of classmates and watched them fold it into their own daily workflows. Seeing other people quietly come to rely on it made me want to release it properly.</p>
<h2>The rejection</h2>
<p>Version 1.0 shipped in the winter, and it was free to download from the App Store. Next I added features early users asked for, and I decided to make it a paid app. That update was version 1.5.</p>
<p>Apple rejected it under Guideline 2.4.5. The rejection was specifically about the accessibility permission. WhisperPad uses it to inject transcribed text into other applications, and Apple&#39;s position was that this was not an approved use of that API. Earlier, approved versions of the app already did the identical thing with the identical permissions, so this was hard to swallow.</p>
<p>The rejection explanation had some fair feedback, too. I fixed those issues, but the 2.4.5 issue was the real wall. So I appealed, explaining that WhisperPad functioned as an accessibility tool, that I had a repetitive strain injury, and that the whole reason the app injected text the way it did was to spare the hands of people who, like me, could not afford the extra keystrokes. I asked Apple to look at it in that light.</p>
<p>They responded that they would take a closer look. They told me not to reply in the thread, and said they would come back with a decision. That was April 21st.</p>
<p>Then it went quiet. By May 21st I had heard nothing, so I sent a short, polite note asking about the status. I have worked in customer support. I know tickets fall through the cracks, and I wanted to nudge it back onto the pile.</p>
<p>They responded quickly, and the answer was another rejection.</p>
<h2>The decision to split</h2>
<p>That second rejection is what forced a real decision, and I want to explain, because it would have been easy to make a worse one.</p>
<p>I spent some time weighing two options: comply with Apple&#39;s restrictions and release a version that fit their rules on the App Store (sacrificing the direct paste), or release WhisperPad through direct distribution. I decided I couldn&#39;t sacrifice the reach of the App Store. So I would build a version that complied with their rules and put it there, and at the same time move ahead with direct distribution to deliver the full version I had originally set out to build.</p>
<p>Here is the thing about the constraint. Apple&#39;s guideline, as far as I can tell, is meant to govern apps injecting text into themselves for accessibility, rather than into other apps. That is my assumption, since I never got a perfectly clear explanation. (If you understand this guideline better and can correct me, reach out — I want to hear it.) Either way, an app reaching into every other app on your system is something to be careful about, and I can see how WhisperPad sits in an awkward spot relative to that line.</p>
<p>So I split WhisperPad into two versions.</p>
<p>The version on the Mac App Store does not auto-paste. It puts your transcribed text on the clipboard, and you press Command-V to place it. That takes the core flow from roughly four steps to six. That does not sound like much, and for most people it is not. The entire design goal of the app was to remove hand movements for people who need to be economical with them, so a 50% increase in steps is significant. It is a compromised version of the idea. But it is a real, useful app, and it is on the store where people with or without accessibility needs can stumble onto it. The App Store makes it discoverable.</p>
<p>The original vision, the one that pastes directly where your cursor is, I shipped myself — outside the App Store.</p>
<h2>Shipping it directly</h2>
<p>Shipping software outside the App Store means rebuilding the things the store normally does for you. There&#39;s a new trust hurdle, too. You&#39;re asking people to trust you to process their credit card on your own site.</p>
<p>For payments I went with Paddle, after some research into the options. For updates I am using Sparkle, the long-standing framework for keeping Mac apps up to date outside the store. Licensing is handled with license keys checked against a small server. None of this is exotic, it is the well-worn path for independent Mac software, but it was all new to me, and getting it right took real work. Practically, I forked the app into separate build targets: one configured for the App Store, one for direct release. I finished wiring up the direct release pipeline on May 27, the morning I started writing this.</p>
<h2>What I took from this</h2>
<p>What first looked like an obstacle turned into a constraint that pushed me to build a better-organized product. I now understand my own build configuration, my update path, and my payment flow far better than I did in March, because Apple&#39;s &quot;no&quot; forced me to learn all of it. Apple&#39;s guidelines exist for defensible reasons, even if I wish my particular app had landed on the other side of the line.</p>
<p>When a platform tells you no, you usually have more options than the moment makes it feel like. The choice is rarely &quot;comply or quit.&quot; It is often &quot;comply here, and do the fuller thing somewhere else.&quot; Shipping two versions, each one serving its channel as well as that channel allows, was not a defeat. It was just the next piece of work.</p>
<p>WhisperPad is on the Mac App Store now, with a free tier of 120 minutes a month so you can find out whether it fits how you work before paying for anything. The direct version, with full auto-paste, is available at <a href="/whisperpad">mitmllc.com/whisperpad</a>. If you want to talk to me about it, I am Rene Zelaya, and you can reach me at <a href="mailto:contact@mitmllc.com">contact@mitmllc.com</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
