<?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: Mashi Mashi</title>
    <description>The latest articles on DEV Community by Mashi Mashi (@mashi_mashi_3092153ef1ab5).</description>
    <link>https://dev.to/mashi_mashi_3092153ef1ab5</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%2F4059575%2F809d79a4-e8ef-4e41-89f8-f82b24829807.png</url>
      <title>DEV Community: Mashi Mashi</title>
      <link>https://dev.to/mashi_mashi_3092153ef1ab5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mashi_mashi_3092153ef1ab5"/>
    <language>en</language>
    <item>
      <title>Why "Accuracy" Is the Wrong Metric for Probabilistic Prediction Models</title>
      <dc:creator>Mashi Mashi</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:29:09 +0000</pubDate>
      <link>https://dev.to/mashi_mashi_3092153ef1ab5/why-accuracy-is-the-wrong-metric-for-probabilistic-prediction-models-4e85</link>
      <guid>https://dev.to/mashi_mashi_3092153ef1ab5/why-accuracy-is-the-wrong-metric-for-probabilistic-prediction-models-4e85</guid>
      <description>&lt;p&gt;If you build a model that predicts the probability of an outcome — a race result, a match winner, a medical risk score, anything where the output is meant to be a probability rather than a hard label — "accuracy" is almost always the wrong number to optimize for, and often the wrong number to even report. I run &lt;a href="https://ai-race.jp/" rel="noopener noreferrer"&gt;ai-race.jp&lt;/a&gt;, which produces probabilistic predictions for horse races, and the gap between "the model is accurate" and "the model is useful" shows up constantly in that domain. This isn't a post about picking winners; it's about why the standard classification metric misleads you once your output is a probability instead of a category, and what to look at instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why accuracy breaks down for probabilistic output
&lt;/h2&gt;

&lt;p&gt;Accuracy answers one question: for the class you predicted as most likely, how often were you right? That's a fine question when your model outputs a single label — spam or not spam, cat or dog. It's a bad question when your model's actual product is a probability distribution over many possible outcomes, because accuracy throws away everything except whether the top pick happened to win.&lt;/p&gt;

&lt;p&gt;Take a race with twelve runners. A model that assigns the eventual winner a 9% probability, barely ahead of eleven other horses clustered near 8%, gets full credit under "accuracy" the same as a model that assigned that horse 60% and put daylight between it and the field. Both models "got it right." But the second model expressed real, calibrated confidence and the first one barely distinguished the winner from noise. If you only track accuracy, you cannot tell these two models apart, even though one of them is obviously doing something much more useful. Worse, a model can improve its calibration and honesty — start admitting more uncertainty in genuinely uncertain races — and accuracy won't move, or can even look slightly worse, because admitting uncertainty sometimes means not concentrating probability mass on the single most likely outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log loss: the metric that actually rewards honest probabilities
&lt;/h2&gt;

&lt;p&gt;Log loss (cross-entropy) scores a prediction by how much probability mass the model assigned to the outcome that actually happened, penalizing confident wrong answers heavily and rewarding confident right answers, with a smooth gradient in between for everything less certain. If the true outcome gets assigned probability &lt;em&gt;p&lt;/em&gt;, the loss contribution is &lt;em&gt;-log(p)&lt;/em&gt;. A model that says "5% chance" for the actual winner pays a much steeper penalty than one that said "25% chance," even if both technically ranked that horse outside their top pick.&lt;/p&gt;

&lt;p&gt;This matters because log loss can't be gamed by just picking a favorite and hoping. To score well on log loss across many races, a model has to spread probability sensibly across plausible outcomes and reserve high confidence for situations that genuinely warrant it. It directly rewards the property you actually want from a probabilistic model: does the number mean what it says it means. Two models can have identical accuracy — same set of "correct" top picks — and meaningfully different log loss, and the one with lower log loss is the one whose probabilities you should trust more when making any downstream decision that depends on the actual probability value, not just the ranking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calibration: does 30% actually happen 30% of the time
&lt;/h2&gt;

&lt;p&gt;Log loss is a single aggregate number; calibration is what you check to understand why that number is good or bad. A model is calibrated if, among all the predictions where it said "30% chance," the outcome actually happened close to 30% of the time. You check this with a reliability diagram: bucket predictions by their stated probability, and for each bucket plot the stated probability against the observed frequency. A perfectly calibrated model sits on the diagonal.&lt;/p&gt;

&lt;p&gt;Systematic miscalibration shows up in predictable shapes. Overconfidence looks like a curve that sags below the diagonal at high stated probabilities — when the model says 70%, the true rate is more like 55%. Underconfidence does the opposite. Favorite-longshot bias, a well-documented pattern in betting markets broadly, tends to show up as heavy favorites being slightly underpriced in probability terms while longshots are systematically overpriced — bettors and models alike tend to overvalue a small chance at a big outcome. Checking calibration separately from log loss matters because it tells you where a model is wrong, not just how wrong it is on average: a single log loss number can look mediocre for a model that's excellent everywhere except one badly miscalibrated segment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why probability, not a hard pick, is the honest product
&lt;/h2&gt;

&lt;p&gt;There's a specific reason this distinction is not academic in horse racing. Every race has a fixed pool of money and a payout structure — the "控除率" (deduction rate, commonly around 20–30% depending on bet type in Japan's pari-mutuel system) — meaning the pool structurally pays back less than it takes in, before you even get to prediction quality. Any tool that frames its output as "the winner" rather than "here is the estimated probability distribution over outcomes" is overstating what a model can honestly claim, given that even a well-calibrated model is operating inside a negative-sum structure. Reporting probabilities, log loss, and calibration is the honest framing: it tells you how good the estimates are as estimates, without implying that following them is a path to guaranteed profit. Nothing here is a suggestion to bet, and a low log loss does not offset the structural deduction rate — it just means the probabilities themselves are trustworthy as probabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check instead of accuracy
&lt;/h2&gt;

&lt;p&gt;If you're building or evaluating a probabilistic model, a reasonable checklist looks like: report log loss (or Brier score, which is similar but uses squared error instead of log) as your primary aggregate metric; plot a reliability diagram and check for systematic over- or under-confidence at different probability bands; and if you must report something accuracy-like for a general audience, pair it with the calibration data so a "70% confident and right" claim can actually be verified against the model's track record at that confidence level. None of this is exotic — it's standard practice in weather forecasting and quantitative finance, both fields that learned the accuracy trap decades ago because the cost of a badly calibrated probability is direct and measurable. Prediction modeling for any real-world stochastic event deserves the same discipline.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>statistics</category>
      <category>python</category>
    </item>
    <item>
      <title>Shipping a Next.js App on Cloudflare Workers with OpenNext + D1</title>
      <dc:creator>Mashi Mashi</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:26:19 +0000</pubDate>
      <link>https://dev.to/mashi_mashi_3092153ef1ab5/shipping-a-nextjs-app-on-cloudflare-workers-with-opennext-d1-33bk</link>
      <guid>https://dev.to/mashi_mashi_3092153ef1ab5/shipping-a-nextjs-app-on-cloudflare-workers-with-opennext-d1-33bk</guid>
      <description>&lt;p&gt;I recently shipped &lt;a href="https://souzoku-baton.jp/" rel="noopener noreferrer"&gt;souzoku-baton.jp&lt;/a&gt;, a Next.js app, on Cloudflare Workers using OpenNext instead of Vercel. The motivation was mostly cost and consolidation — I already run a handful of other properties on Cloudflare and didn't want a second billing relationship and a second set of DNS quirks just to host one more app. OpenNext's Cloudflare adapter has matured a lot, but there were three areas where the defaults quietly did the wrong thing, and I want to write them down before I forget the exact symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenNext instead of just Pages
&lt;/h2&gt;

&lt;p&gt;Cloudflare Pages can serve a Next.js static export directly, but the moment you need server-side rendering, API routes with real backend logic, or middleware that does more than redirect based on a cookie, a plain static export stops being enough. OpenNext (&lt;code&gt;@opennextjs/cloudflare&lt;/code&gt;) compiles a standard Next.js app into a Cloudflare Worker, mapping the Next.js server runtime onto Workers' request/response model and giving you access to bindings — D1, KV, R2, Durable Objects — directly inside your Next.js server code. That binding access was the actual reason I picked it: the app needed a real relational store for inheritance/estate-planning records, and D1 was the natural fit given everything else was already on Cloudflare.&lt;/p&gt;

&lt;p&gt;The setup itself is close to the documented quick start: &lt;code&gt;create-cloudflare&lt;/code&gt; scaffolds the project, &lt;code&gt;wrangler.jsonc&lt;/code&gt; declares the D1 binding, and &lt;code&gt;open-next.config.ts&lt;/code&gt; controls how the adapter builds the output. Where things got interesting was after the first deploy actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: the D1 migrations ledger silently diverges from reality
&lt;/h2&gt;

&lt;p&gt;D1 tracks applied migrations in a &lt;code&gt;d1_migrations&lt;/code&gt; table that it manages itself, and &lt;code&gt;wrangler d1 migrations apply&lt;/code&gt; is supposed to be the only thing that touches it. Early on, while iterating quickly, I ran a couple of schema changes with &lt;code&gt;wrangler d1 execute --file&lt;/code&gt; directly against the remote database — faster to type than generating a proper migration file when I was just testing a column rename. That was the mistake. &lt;code&gt;d1_migrations&lt;/code&gt; only gets a row inserted when you go through &lt;code&gt;migrations apply&lt;/code&gt;, so after those direct executes, the ledger no longer matched the actual schema: the table structure had my changes, but the migrations system had no record of them.&lt;/p&gt;

&lt;p&gt;The failure mode was confusing rather than loud. &lt;code&gt;wrangler d1 migrations list&lt;/code&gt; reported every migration as unapplied, including ones whose effects were clearly already live in production. Running &lt;code&gt;apply&lt;/code&gt; at that point would have tried to re-run migrations whose &lt;code&gt;CREATE TABLE&lt;/code&gt; and &lt;code&gt;ALTER TABLE&lt;/code&gt; statements would immediately fail against a schema that already had those columns. The fix was to manually reconcile the ledger: for each migration file whose changes were already present in production, I ran an &lt;code&gt;INSERT OR IGNORE INTO d1_migrations (id, name, applied_at) VALUES (...)&lt;/code&gt; matching the migration's expected id and filename, effectively telling D1 "trust me, this one is already done." After that, &lt;code&gt;migrations list&lt;/code&gt; and &lt;code&gt;migrations apply&lt;/code&gt; behaved correctly again. The lesson was simple in hindsight: never touch a D1 database with raw &lt;code&gt;execute&lt;/code&gt; once migrations are in play, even for something that feels like a one-line fix — the ledger has no way to know about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: KV-backed rate limiting has real latency and consistency limits
&lt;/h2&gt;

&lt;p&gt;I used Workers KV to implement a basic per-IP rate limiter on a couple of public form endpoints — read the current count, check it against a threshold, write the incremented count back. This is a well-known anti-pattern for KV specifically because KV is eventually consistent and optimized for high-read/low-write workloads, not for fast read-modify-write cycles. Under light traffic it worked fine. Under a short burst — someone refreshing a form repeatedly, or a bot hammering an endpoint — the count reads and writes raced against each other enough that the limiter under-counted requests by a meaningful margin, letting more through than the configured threshold.&lt;/p&gt;

&lt;p&gt;I didn't need to rearchitect the whole thing; I moved just the rate-limiting counter to a Durable Object, which gives you a single consistent point of coordination per key (per IP, in this case) instead of KV's distributed eventual-consistency model. The Durable Object holds the counter in memory and persists it to its own storage, and because each DO instance is single-threaded with respect to its own state, the race condition disappears by construction rather than by careful timing. KV is still fine for things like feature flags or cached config that tolerate staleness; it's just the wrong tool for a counter that needs read-then-write correctness under concurrent hits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: Next.js metadata's &lt;code&gt;openGraph&lt;/code&gt; fields don't inherit the way you'd expect
&lt;/h2&gt;

&lt;p&gt;Next.js's metadata API lets you define an &lt;code&gt;openGraph&lt;/code&gt; object in a parent layout and have child routes inherit and extend it, which works nicely for things like &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;. What surprised me is that &lt;code&gt;openGraph.images&lt;/code&gt; does not merge the way nested metadata fields usually do when a child route defines its own partial &lt;code&gt;openGraph&lt;/code&gt; object — if a page-level &lt;code&gt;generateMetadata&lt;/code&gt; returns an &lt;code&gt;openGraph&lt;/code&gt; key at all, Next.js treats it as a full replacement for the object rather than a shallow merge on top of the layout default, and any field you don't re-specify (including &lt;code&gt;images&lt;/code&gt;) is simply gone rather than falling back to the parent's value.&lt;/p&gt;

&lt;p&gt;In practice this meant several inner pages exported an &lt;code&gt;openGraph&lt;/code&gt; object to set a page-specific &lt;code&gt;title&lt;/code&gt;, and as a side effect silently lost the default sharing image the layout had defined, so social previews for those pages showed a blank or default browser card instead of the intended graphic. The fix was to stop treating the layout's &lt;code&gt;openGraph&lt;/code&gt; as something child routes could partially override, and instead centralize a &lt;code&gt;buildOpenGraph(overrides)&lt;/code&gt; helper that always spreads the full default object first and applies only the specific keys a page needed to change. That guarantees &lt;code&gt;images&lt;/code&gt; and other shared fields survive even when a page customizes just the title.&lt;/p&gt;

&lt;h2&gt;
  
  
  Net take
&lt;/h2&gt;

&lt;p&gt;None of these are OpenNext bugs exactly — they're gaps between how the underlying Cloudflare primitives behave and what you'd naively assume coming from a more traditional Node/Vercel deployment model. D1's migration ledger, KV's consistency model, and Next.js's metadata merging rules are all documented, just not in a way that surfaces itself until you've already hit the edge case. Running on Workers with OpenNext has been solid otherwise — cold starts are noticeably better than serverless functions I've run elsewhere, and having D1, KV, and Durable Objects available in the same request without extra network hops made a few things simpler than they would have been split across separate services.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>nextjs</category>
      <category>serverless</category>
      <category>database</category>
    </item>
    <item>
      <title>Migrating 10 WordPress Sites to Cloudflare Pages: What Broke</title>
      <dc:creator>Mashi Mashi</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:23:58 +0000</pubDate>
      <link>https://dev.to/mashi_mashi_3092153ef1ab5/migrating-10-wordpress-sites-to-cloudflare-pages-what-broke-5dom</link>
      <guid>https://dev.to/mashi_mashi_3092153ef1ab5/migrating-10-wordpress-sites-to-cloudflare-pages-what-broke-5dom</guid>
      <description>&lt;p&gt;A few months ago I moved a batch of WordPress sites off a shared LAMP host and onto Cloudflare Pages as static exports. The pitch is obvious: no PHP process to keep patched, no MySQL to babysit, effectively free hosting, and a CDN in front of everything by default. What the pitch doesn't tell you is how many small, boring things break on the way there. This post is a rundown of what actually went wrong migrating a set of ten WordPress sites — one of them is &lt;a href="https://burningtribe.tokyo/" rel="noopener noreferrer"&gt;burningtribe.tokyo&lt;/a&gt;, which I'll use as the concrete example — and how I fixed each issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;p&gt;The migration itself is conceptually simple: crawl the live WordPress site, save every URL as a static HTML file plus its assets, and serve that tree from Cloudflare Pages. I used a combination of &lt;code&gt;wget --mirror&lt;/code&gt; and a custom crawler for a couple of sites where &lt;code&gt;wget&lt;/code&gt; choked on query-string-based pagination. The static output then gets pushed with &lt;code&gt;wrangler pages deploy&lt;/code&gt;. No build step, no framework, just files.&lt;/p&gt;

&lt;p&gt;That simplicity is exactly why it seemed low-risk. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: relative canonical tags pointed everything at the homepage
&lt;/h2&gt;

&lt;p&gt;The first thing I noticed after deploying was that Google Search Console started reporting most inner pages as "duplicate, Google chose different canonical" — and the canonical it picked was the homepage. The cause was almost funny once I found it: the WordPress theme emitted &lt;code&gt;&amp;lt;link rel="canonical" href="/"&amp;gt;&lt;/code&gt; as a relative path in a few cached page fragments, instead of an absolute URL like &lt;code&gt;https://burningtribe.tokyo/some-post/&lt;/code&gt;. On the original WordPress install this didn't matter because the page itself resolved the relative reference correctly at the point of caching. Once the HTML was frozen and served statically from a different origin structure (Pages serves everything from the apex), that relative canonical collapsed to the site root for every single page that had it.&lt;/p&gt;

&lt;p&gt;The fix was a straightforward but tedious pass: grep every exported HTML file for &lt;code&gt;rel="canonical"&lt;/code&gt;, and rewrite any relative or root-relative href into a fully qualified absolute URL matching that page's own path. I wrote a small script that walked the export directory, parsed the canonical tag, and replaced it based on the file's own location relative to the site root. Running that across all ten sites turned up the same bug in three of them — it wasn't a burningtribe.tokyo-specific quirk, it was a plugin combination that a few sites shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: no sitemap.xml survived the export
&lt;/h2&gt;

&lt;p&gt;WordPress sitemap plugins (I was using a common SEO plugin's built-in XML sitemap) generate their sitemap dynamically from a PHP endpoint, not as a static file anywhere in the theme or uploads directory. A mirror crawl that only follows &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; links will never discover &lt;code&gt;/sitemap.xml&lt;/code&gt; unless it's explicitly linked from a page, which it usually isn't — it's referenced from &lt;code&gt;robots.txt&lt;/code&gt; and submitted directly to Search Console instead.&lt;/p&gt;

&lt;p&gt;The result: after cutover, sitemap.xml on the new static site was either missing entirely or, worse, still referencing the old dynamic URLs that no longer existed as generated endpoints. For a couple of sites this went unnoticed for almost two weeks because the sites still ranked on already-indexed pages; the failure mode is silent. I ended up regenerating sitemaps after the fact by walking the exported file tree directly (every &lt;code&gt;.html&lt;/code&gt; file becomes a &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; entry) rather than trying to preserve the plugin's output, since the plugin's output no longer had anything running to produce it. That also meant re-submitting fresh sitemaps in Search Console for each property, which is worth doing explicitly rather than assuming the crawler will pick up the change organically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: enqueued JS and CSS 404s
&lt;/h2&gt;

&lt;p&gt;WordPress uses &lt;code&gt;wp_enqueue_script&lt;/code&gt; and &lt;code&gt;wp_enqueue_style&lt;/code&gt; to load most theme and plugin assets, and those functions frequently append a version query string, like &lt;code&gt;theme.js?ver=6.4.2&lt;/code&gt;. A naive mirror tool that treats &lt;code&gt;theme.js&lt;/code&gt; and &lt;code&gt;theme.js?ver=6.4.2&lt;/code&gt; as different resources will save one and reference the other, or save the query-string version and leave the extensionless base path unresolved depending on crawl order. I saw a batch of these appear as 404s in the Pages deployment logs for image lazy-load scripts and one analytics snippet, which quietly broke lazy-loaded images on a couple of pages until I caught it in a manual QA pass.&lt;/p&gt;

&lt;p&gt;The fix was to strip version query strings during the export step and de-duplicate on the base filename, then verify with a link checker (&lt;code&gt;linkchecker&lt;/code&gt; worked fine for this) run against the deployed Pages URL rather than the local export — some of these only showed up once served over HTTPS with Cloudflare's own caching layer in front, because local file serving during testing was more forgiving about missing query strings than the production edge was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: internal redirects had nowhere to live
&lt;/h2&gt;

&lt;p&gt;The original WordPress sites had a handful of accumulated 301 redirects — old permalink structures, a couple of merged categories, one full domain change years back that was still being redirected from an old slug. Those redirects lived in a redirection plugin's database table, which obviously doesn't come along in a static export at all.&lt;/p&gt;

&lt;p&gt;Cloudflare Pages supports a &lt;code&gt;_redirects&lt;/code&gt; file for exactly this case, and that's where these rules ended up. The process was: export the plugin's redirect rules (most redirect plugins have a CSV or JSON export), then transform that list into the &lt;code&gt;_redirects&lt;/code&gt; file format (&lt;code&gt;/old-path /new-path 301&lt;/code&gt;) with a short script, and commit that file at the root of the Pages project. It's a five-minute fix once you know it's needed, but it's easy to forget entirely if you're focused on "does the site render" rather than "does the old URL still resolve," and a couple of these old redirects were still receiving meaningful traffic from long-lived backlinks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;If I were starting this migration over, I'd build the sitemap and &lt;code&gt;_redirects&lt;/code&gt; file generation into the export pipeline from the start rather than patching them in reactively after Search Console complained. I'd also run the canonical-tag check as an automated assertion in CI before any deploy, since it's cheap to check and expensive to have silently wrong for weeks. None of these problems are exotic — they're all a version of the same root cause: WordPress plugins generate output dynamically at request time, and a static export only captures what happened to be requested during the crawl, not the underlying logic that produced it. Once you internalize that, most of the migration checklist writes itself.&lt;/p&gt;

&lt;p&gt;The sites are stable now and load meaningfully faster than they did on the old host, so the migration was worth it. But "just export it as static files" undersells how much of WordPress's behavior is implicit and request-time-dependent rather than baked into the files themselves.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>wordpress</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What I Learned Shipping 90+ Mobile Apps with AI Coding Agents</title>
      <dc:creator>Mashi Mashi</dc:creator>
      <pubDate>Sun, 02 Aug 2026 21:40:35 +0000</pubDate>
      <link>https://dev.to/mashi_mashi_3092153ef1ab5/what-i-learned-shipping-90-mobile-apps-with-ai-coding-agents-13lk</link>
      <guid>https://dev.to/mashi_mashi_3092153ef1ab5/what-i-learned-shipping-90-mobile-apps-with-ai-coding-agents-13lk</guid>
      <description>&lt;p&gt;Over the last year I've shipped more than ninety mobile apps to the App Store and Play Console, almost all of them built with heavy help from AI coding agents (Claude Code, mostly, with a mix of other tools along the way). That number sounds absurd until you understand what most of these apps actually are: small, focused utilities and niche games built by a team of one, not ninety separate startups with ninety roadmaps.&lt;/p&gt;

&lt;p&gt;I want to write down what actually changed in my workflow, because most of the "AI coding agent" content I read online is either breathless hype or reflexive skepticism. The reality, at least for me, was messier and more useful than either extreme.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unlock wasn't code generation
&lt;/h2&gt;

&lt;p&gt;The first thing people assume is that AI agents are valuable because they type code faster than you. That's true, but it's not the interesting part. The interesting part is that a coding agent can hold an entire small app's context in working memory and act on it consistently — rename a variable across twelve files, update a schema and every place that touches it, or run a build, read the error, and fix it without you copy-pasting a stack trace into a chat window.&lt;/p&gt;

&lt;p&gt;Once I stopped treating the agent as an autocomplete engine and started treating it as a junior engineer who never gets tired of grunt work, the portfolio approach became possible. A single person cannot manually maintain ninety codebases. A single person plus an agent that can be pointed at a repo and told "the build is broken, fix it" can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store submissions are where the real complexity lives
&lt;/h2&gt;

&lt;p&gt;Writing the app was never the bottleneck. App Store Connect and Google Play Console submissions were. Both platforms have dozens of small, easy-to-miss requirements that have nothing to do with whether your code works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A subscription app needs an EULA link in its description, or it gets auto-rejected by a script before a human ever looks at it.&lt;/li&gt;
&lt;li&gt;Free trials that are only configured for some territories will pass review in one region and throw a purchase error in another.&lt;/li&gt;
&lt;li&gt;Deleting an app record on App Store Connect doesn't free up the bundle ID — it's reserved permanently, which matters a lot when you're iterating fast.&lt;/li&gt;
&lt;li&gt;Google Play's classifier will flag things like "reads Android system logs" language even when the underlying permission usage is completely benign, and the appeal path is not obvious.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this shows up in a tutorial about building your first app. It only shows up after you've been rejected a few dozen times and started keeping a checklist. I eventually had the agent maintain that checklist as a literal pre-submission gate script, because I stopped trusting myself to remember eighteen rules across ninety apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are excellent at finding the boring bug you'd never look for
&lt;/h2&gt;

&lt;p&gt;The bugs that actually hurt were never the ones in business logic. They were things like: a retired ad SDK dependency left behind after a plugin was removed, causing a crash only on cold start on specific Android versions. Or a locale check that worked on every simulator I tested but returned the wrong result on real Japanese devices because the JS engine's &lt;code&gt;Intl&lt;/code&gt; implementation doesn't localize the way you'd expect.&lt;/p&gt;

&lt;p&gt;What made agents genuinely useful here wasn't cleverness — it was persistence and breadth. I could ask an agent to scan all ninety repos for a specific anti-pattern (an unreferenced dependency, a hardcoded staging URL, a duplicate icon mismatch between the launcher and the store listing) and get a real answer in minutes instead of spending a weekend grepping by hand. The value compounds specifically because the portfolio is large — the same technique on three apps would just be "using find-and-replace with extra steps."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I still don't trust the agent alone
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the limits, because overclaiming here helps no one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anything involving real money — in-app purchase configuration, subscription pricing, Stripe webhooks — gets a manual review pass every time. Agents are good at pattern-matching prior mistakes, not at catching a wrong price point unless you tell them exactly what to check.&lt;/li&gt;
&lt;li&gt;Destructive operations (force pushes, database migrations, deleting review-blocking records) are never delegated fully. I treat "the agent proposes, I approve" as a hard rule for anything irreversible.&lt;/li&gt;
&lt;li&gt;Long-running or flaky external processes — a build that intermittently fails because of a shared temp directory, or a CDN cache that lies about propagation — still need a human to notice the pattern isn't random.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest summary is that AI coding agents didn't replace the parts of shipping software that require judgment. They replaced the parts that required patience: reading every line of a hundred-line diff, remembering which of ninety apps still uses a deprecated permission, re-running the same fifteen-step submission checklist without skipping step eleven because you're tired.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually made this sustainable
&lt;/h2&gt;

&lt;p&gt;Two habits mattered more than any specific tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Writing down failure modes as I hit them&lt;/strong&gt;, in plain files the agent could read back later, rather than trusting memory. The second time a submission failed for the same reason, that was a process bug, not a one-off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating verification as non-negotiable.&lt;/strong&gt; An agent reporting "build succeeded" is not proof of anything except that a compiler didn't complain. I learned to require an actual artifact check — did the binary get uploaded, does the store listing reflect it, does the app open — before calling anything done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're exploring this space and want a more structured way to actually practice building with an AI coding agent rather than just reading about it, I've been collecting what I've learned into a small practical resource at &lt;a href="https://www.aicraftcampus.jp/" rel="noopener noreferrer"&gt;AI Craft Campus&lt;/a&gt; — it's aimed at people who want to go from "I asked an AI to write some code" to "I run a small, working software portfolio with an AI agent doing the repetitive parts." It's a work in progress, same as the rest of this.&lt;/p&gt;

&lt;p&gt;Ninety apps is not a finish line, it's just where the counter happens to be right now. The workflow that got me here — small repos, aggressive automation of the boring parts, and a healthy distrust of "it built successfully" as a definition of done — is the part I'd actually recommend adopting, independent of which specific tool you use.&lt;/p&gt;

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