<?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: Sunitha Eswaraiah</title>
    <description>The latest articles on DEV Community by Sunitha Eswaraiah (@esunitha).</description>
    <link>https://dev.to/esunitha</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%2F4022627%2Fad76c9c0-8ac3-43ff-8aeb-c6a51210819b.jpg</url>
      <title>DEV Community: Sunitha Eswaraiah</title>
      <link>https://dev.to/esunitha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/esunitha"/>
    <language>en</language>
    <item>
      <title>An epilogue to the game factory — I changed every model and the cost moved</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:28:38 +0000</pubDate>
      <link>https://dev.to/esunitha/an-epilogue-to-the-game-factory-i-changed-every-model-and-the-cost-moved-p55</link>
      <guid>https://dev.to/esunitha/an-epilogue-to-the-game-factory-i-changed-every-model-and-the-cost-moved-p55</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 9 of 9 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I said I was done. Post 8 ended with the factory parked — functional, proven, not something I was going to keep polishing. Then I came back, changed every model in the pipeline, and learned something I'd had backwards.&lt;/p&gt;

&lt;p&gt;The thing that pulled me back was a number. One agent, on a single run, sent about 780,000 tokens &lt;em&gt;into&lt;/em&gt; the model and got roughly 5,000 back. Input to output, a ratio of 150 to 1. That agent was the Builder — the same one that fought me for months. By this point it barely used the model at all. So why was it burning three-quarters of a million tokens a run, and why was it the most expensive model I owned?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the factory landed
&lt;/h2&gt;

&lt;p&gt;The short version, if you didn't read the rest of the series: six agents turn a one-sentence theme into a deployed, playable slot machine. Designer writes a spec, the image agents make icons and a background, the Builder rewrites the code, the Tester plays it in a browser, the Deployer ships it.&lt;/p&gt;

&lt;p&gt;The Builder was the one I eventually turned back into a program. After the redesign it was roughly 99% deterministic — mirror the source, then swap colors, fonts, and strings in plain Python. The only thing left for the model was optional cosmetic CSS: nudge a glow, adjust a keyframe. A few small edits per build.&lt;/p&gt;

&lt;p&gt;That's the setup for the question I hadn't asked yet. If the model in an agent is doing almost nothing, why is it the same top-tier model I use for the agent that does the most?&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching the model to the work
&lt;/h2&gt;

&lt;p&gt;So I made a map. For each agent, how much of the work is the model actually &lt;em&gt;thinking&lt;/em&gt;, and how much is plumbing?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Designer&lt;/strong&gt; — generates a spec from a conversation. This is the creative one. Taste shows up here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image-Gen / Background-Gen&lt;/strong&gt; — the creative work happens inside the image model. The agent wrapped around it just loops over prompts and saves files. Plumbing around a creative core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builder&lt;/strong&gt; — 99% deterministic Python, 1% cosmetic CSS. Plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tester&lt;/strong&gt; — half deterministic (drive a browser, take screenshots), half judgment (a vision model looking at the result).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployer&lt;/strong&gt; — almost entirely deterministic. Plumbing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The map made the decision obvious. Keep the strong model where the agent thinks. Try a cheaper, faster one where it's plumbing.&lt;/p&gt;

&lt;p&gt;These are the assignments I landed on, from what Bedrock offered me in late 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Designer&lt;/td&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;The creative step. Taste shows up here, so it keeps the strong model.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image-Gen / Background-Gen&lt;/td&gt;
&lt;td&gt;Claude Haiku 4.5, driving Stability&lt;/td&gt;
&lt;td&gt;The judgment is in the image model. The agent around it just drives a short tool loop, so the cheapest Claude does.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Builder&lt;/td&gt;
&lt;td&gt;Qwen3-Coder-480B&lt;/td&gt;
&lt;td&gt;A long code-editing tool loop — a coding-specialized model, and a much cheaper one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tester (loop)&lt;/td&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td&gt;Executes a fixed checklist. No creativity required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tester (vision)&lt;/td&gt;
&lt;td&gt;Pixtral Large&lt;/td&gt;
&lt;td&gt;Scores screenshots against a rubric. Needs to actually see.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployer&lt;/td&gt;
&lt;td&gt;Qwen3-32B&lt;/td&gt;
&lt;td&gt;Runs SAM and CLI steps in a fixed order.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Worth being precise about one thing, because "I moved the cheap agents to a cheap model" hides two different decisions. The Builder went to a &lt;em&gt;coding&lt;/em&gt; model, because it edits code in a tool loop. The Tester and Deployer went to a plain, smaller general model, because they don't edit anything — they run checklists. And the Tester's vision model is the exception to the whole cost story: Pixtral Large is the second most expensive model in the pipeline, because "does this look like ancient Egypt" is a judgment call and that's the one place I'm buying judgment.&lt;/p&gt;

&lt;p&gt;One practical note that isn't in any pricing table: when I ran this, the only region my setup could invoke Qwen3-Coder-480B from was &lt;code&gt;us-west-2&lt;/code&gt;, so the Builder talks to a different region than every other agent in the pipeline. Regional availability moves around — AWS has since added others — but that's the point. Where a model is callable from is part of choosing it, not a footnote, and it's the part most likely to have changed by the time you read this.&lt;/p&gt;

&lt;p&gt;The Builder got fast. Suspiciously fast — the kind of fast that makes you check whether it actually ran. It had. And then I looked at the token bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  780,000 in, 5,000 out
&lt;/h2&gt;

&lt;p&gt;The output was tiny, and that's the point. 5,000 tokens covers everything the model emitted across the whole run — its plan, its tool calls, and the handful of small CSS patches inside them. That confirmed "99% deterministic": the model was hardly generating anything. The 780,000 was all input. The cost of &lt;em&gt;looking&lt;/em&gt;, not the cost of &lt;em&gt;writing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So where does 780,000 tokens of input come from when the instructions are barely 2,000?&lt;/p&gt;

&lt;p&gt;I logged every call's input size across one run. The shape told the whole story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;call   input tokens   what changed
  1        3,017       system prompt + tool specs (baseline)
  3        3,360       still just reading the spec
  4       31,398       +28k: it read one CSS file into the conversation
 ...      ~33,000      each — that same file, re-sent every turn
 17       64,604       +28k: it read a second big file
 ...      ~65,000      each — now both files, re-sent every turn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jump at call 4 is the model reading a single stylesheet so it could edit it. The file is 66 KB, and it arrived as about 28,000 tokens. CSS tokenizes worse than prose — all that punctuation and all those hex codes land nearer 2.4 characters per token than the 4 you'd assume from English. To patch a rule, the edit tool needs the file's exact current text, and the lazy way to get that is to hand the model the whole file.&lt;/p&gt;

&lt;p&gt;Here's the part I'd had backwards. The file was read &lt;em&gt;once&lt;/em&gt;. But in an agent loop, every call re-sends the entire conversation so far. That stylesheet landed in the context on call 4 and then rode along on all seventeen calls after it. Read once, paid for eighteen times. That one file is around 500,000 of the 780,000 tokens by itself.&lt;/p&gt;

&lt;p&gt;I hadn't burned 780,000 tokens thinking. I'd burned them re-reading the same two files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch I didn't see coming
&lt;/h2&gt;

&lt;p&gt;At this point Qwen looked like a clear win: same near-empty output, a fraction of the price per token. Then I noticed a column in my usage log that was zero for every single call — cached tokens.&lt;/p&gt;

&lt;p&gt;Qwen didn't support prompt caching on the Bedrock path I was using. Claude Sonnet 5 did, and my own integration only ever enabled caching for Anthropic model ids — one &lt;code&gt;is_anthropic()&lt;/code&gt; check gating the whole code path. Bedrock supports caching on more than just Anthropic models, so this is a fact about my two candidates and my implementation, not a rule about the platform.&lt;/p&gt;

&lt;p&gt;And prompt caching is exactly the thing that fixes a loop like this: the big, stable prefix you re-send every turn gets billed at a small fraction after the first time. That repeated stylesheet — the 500,000 tokens — is the ideal shape for a cache, and on Qwen none of it was cached, so every re-send was full freight.&lt;/p&gt;

&lt;p&gt;So the comparison I care about isn't "cheap model versus expensive model." It's:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On published rates, and assuming the cache-hit pattern I'd expect from a prefix this stable, Sonnet 5 &lt;strong&gt;with&lt;/strong&gt; caching lands in roughly the same range on this workload as Qwen3-Coder &lt;strong&gt;without&lt;/strong&gt; it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I want to be clear that this is an estimate and not a measurement. I never ran this build on Sonnet to compare — every number in my logs is a Qwen run with a cache column of zero. Cache hits also aren't free money: they depend on where you place checkpoints, whether the prefix really is byte-stable, and whether you hit the TTL before the next call. The point isn't a precise figure. It's that a price gap that looks like an order of magnitude on the pricing page can mostly close once caching is in play, so the sticker rate isn't the comparison you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Qwen stayed anyway
&lt;/h2&gt;

&lt;p&gt;Cost was a wash. Three other things weren't.&lt;/p&gt;

&lt;p&gt;The first is speed. Qwen3-Coder decodes quickly, and on an agent producing 5,000 tokens of real output, fast is what you feel. Builds that used to make me wait now finish before I've switched windows.&lt;/p&gt;

&lt;p&gt;The second is a constraint I walked into sideways. Qwen3-Coder-480B gave me a 128K context window with a 16K cap on output, and on an agent whose context grows by 28,000 tokens every time it opens a file, that ceiling is closer than it looks. I had to bound the conversation history explicitly and reserve room for the reply, or the loop would overflow on input alone and the call would just fail. Context limits are shared between input and output generally — this isn't a Qwen peculiarity — but the headroom I had here was tight enough that it stopped being an accounting detail. The context bloat wasn't only expensive. On this model it was also the thing most likely to break the build.&lt;/p&gt;

&lt;p&gt;The third is that the model isn't thinking here. Look at that output again: 5,000 tokens across twenty-one calls. The Builder isn't reasoning about architecture; it's making a few cosmetic edits and calling tools. When an agent is mostly plumbing, you're not giving up much quality by running a plainer model, because there's little quality being exercised in the first place.&lt;/p&gt;

&lt;p&gt;The whole decision fits in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Sonnet 5&lt;/th&gt;
&lt;th&gt;Qwen3-Coder-480B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Quality on hard reasoning&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (with caching in play)&lt;/td&gt;
&lt;td&gt;competitive&lt;/td&gt;
&lt;td&gt;competitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;slower&lt;/td&gt;
&lt;td&gt;fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;larger; headroom to spare here&lt;/td&gt;
&lt;td&gt;128K, 16K max output — tight for this loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doing hard reasoning &lt;em&gt;here&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When the bottom row is "no," the top row stops mattering, and speed wins. So the plumbing agents keep Qwen. The Designer, where the bottom row is "yes," keeps Sonnet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually fix
&lt;/h2&gt;

&lt;p&gt;The neat ending would be "pick the right model per agent." That's true, but it buries the real lesson, which is that &lt;strong&gt;the model was never the expensive part&lt;/strong&gt;. The expensive part was shipping a 66 KB file into the context and re-sending it twenty times.&lt;/p&gt;

&lt;p&gt;The model choice saved some money and a lot of wall-clock. Not shipping the whole file would save far more — the Builder touches maybe six rule blocks, a few hundred tokens, not twenty-eight thousand.&lt;/p&gt;

&lt;p&gt;The two stylesheets account for about 646,000 of the 779,474 tokens, just by riding along in the context: one across eighteen calls, the other across five. Strip them out and what's left — system prompt, tool specs, spec lookups, tool results, the model's own replies, all of it re-sent every turn — is roughly 133,000. Hand the model extracted rule blocks instead of whole files and you land somewhere near 140,000. That's a five-fold cut, and it has nothing to do with which model is behind the endpoint.&lt;/p&gt;

&lt;p&gt;(Not a twentyfold cut, which is what I'd have guessed before doing the arithmetic. The baseline you re-send every turn is itself most of what remains.)&lt;/p&gt;

&lt;p&gt;I haven't shipped that one yet. I'm writing it down because it's the more useful lesson: I went looking for savings in the model and found them in my own context management.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure input against output before you optimize a model.&lt;/strong&gt; A 150-to-1 ratio isn't a smart agent; it's an agent re-reading. The token count measures how much you &lt;em&gt;sent&lt;/em&gt;, not how much the model &lt;em&gt;thought&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the model to what the agent actually does.&lt;/strong&gt; Strong model where it reasons, plain model where it plumbs. Map your agents honestly — in this pipeline, most of them turned out to be plumbing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching can beat a cheaper sticker price.&lt;/strong&gt; A premium model that caches a big, stable prefix can land in the same range as a budget model that re-sends everything. Check whether your cheap candidate supports caching at all before you price the swap — not every model on a platform does. Compare the workload, not the per-token rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cheapest token is the one you don't send twice.&lt;/strong&gt; Before swapping models, look at what's sitting in your context getting re-billed every turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most reliable agent I built was the one I turned back into a program. The cheapest one will be the same agent once I stop handing it files it doesn't need. Same lesson as the whole series, one layer down: keep taking work away from the model until the only thing left is the thing a model is for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is an epilogue to the &lt;a href="https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4"&gt;game-factory series&lt;/a&gt;. The most related earlier posts are &lt;a href="https://dev.to/esunitha/the-builder-agent-the-code-modifier-that-fought-me-5ch0"&gt;the Builder agent&lt;/a&gt; and &lt;a href="https://dev.to/esunitha/what-i-learned-building-an-agent-pipeline-and-where-i-stopped-3efk"&gt;the lessons&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>llm</category>
    </item>
    <item>
      <title>What I learned building an agent pipeline — and where I stopped</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:29:08 +0000</pubDate>
      <link>https://dev.to/esunitha/what-i-learned-building-an-agent-pipeline-and-where-i-stopped-3efk</link>
      <guid>https://dev.to/esunitha/what-i-learned-building-an-agent-pipeline-and-where-i-stopped-3efk</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 8 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The runaway
&lt;/h2&gt;

&lt;p&gt;One afternoon I watched an agent spend three hours and 138 dollars trying to edit a single file.&lt;/p&gt;

&lt;p&gt;It was the Builder — the agent whose job was to take a theme spec and turn my slot machine's React code into a themed variant. It had opened the main game component, a 1,959-line file, and started making small surgical edits. One of them broke the syntax. The model ran the compiler itself, read the error, tried to fix it, broke something else, and kept going — nothing in the loop forced it to stop on a failed build. By the time I killed it, it had made 868 calls to the model. The file was a wreck: two &lt;code&gt;export default&lt;/code&gt; statements, duplicate &lt;code&gt;return (...)&lt;/code&gt; blocks, orphaned JSX floating between them.&lt;/p&gt;

&lt;p&gt;Nothing about that failure was exotic. The agent did exactly what I told it to do. The problem was what I told it to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the factory was
&lt;/h2&gt;

&lt;p&gt;The short version, if you didn't read &lt;a href="https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4"&gt;post 1&lt;/a&gt;: I built a real slot machine on AWS to learn the iGaming domain, then turned the theming into a pipeline of agents. Describe a theme in a sentence, and six agents in sequence design a spec, generate icons and a background, rewrite the code, test it in a browser, and deploy it. Designer, Image-Gen, Background-Gen, Builder, Tester, Deployer.&lt;/p&gt;

&lt;p&gt;Five of those six behaved. The Builder was the one that kept me up. Every other agent in the pipeline had a clean output — a spec, a set of images, a test report, a deployed URL. The Builder's output was a code change to a large, interconnected React app, and code changes are where a model's mistakes compound instead of just being wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that mattered most
&lt;/h2&gt;

&lt;p&gt;The Builder's task was: take the casino's &lt;code&gt;SlotGame.js&lt;/code&gt; and edit it to match the theme. The file was 1,959 lines. The model couldn't do it reliably — it guessed at whitespace, patched the wrong one of twenty near-identical calls, broke the parse with unescaped apostrophes, and each miss compounded.&lt;/p&gt;

&lt;p&gt;The insight I was slow to reach: the model was bad at editing that file because &lt;em&gt;the file was hard to edit&lt;/em&gt;. So I stopped trying to make the model better at the edit and changed the thing being edited.&lt;/p&gt;

&lt;p&gt;Two changes, in this order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I modularized the casino.&lt;/strong&gt; &lt;code&gt;SlotGame.js&lt;/code&gt; went from 1,959 lines to 715, with its concerns moved into focused modules — five hooks (&lt;code&gt;useAudio&lt;/code&gt;, &lt;code&gt;useAuth&lt;/code&gt;, &lt;code&gt;useLeaderboard&lt;/code&gt;, &lt;code&gt;useReelAnimation&lt;/code&gt;, &lt;code&gt;useResponsibleGaming&lt;/code&gt;) and five presentational components (&lt;code&gt;RiskGauge&lt;/code&gt;, &lt;code&gt;TutorialModal&lt;/code&gt;, &lt;code&gt;ResponsibleGamingModal&lt;/code&gt;, &lt;code&gt;ProfileDropdown&lt;/code&gt;, &lt;code&gt;WinEffects&lt;/code&gt;). &lt;code&gt;TournamentGame.js&lt;/code&gt;, which had been a 90% copy-paste of &lt;code&gt;SlotGame.js&lt;/code&gt;, dropped from 1,441 lines to 459 by consuming the same hooks. After the refactor, no file in the tree exceeded 234 lines.&lt;/p&gt;

&lt;p&gt;That last number was the design target, not a side effect. Not "cleaner code" — a file size the agent could read, hold, and patch without losing its place.&lt;/p&gt;

&lt;p&gt;The refactor had one hard constraint: the casino is the real game, the one people actually play, and it had to behave identically afterward. Same UI, same animations, same deploy. So the split moved code without changing behavior, and "the game still plays the same" was the acceptance test. Deduplicating &lt;code&gt;TournamentGame.js&lt;/code&gt; onto shared hooks paid for the work on its own — every bug fix had previously needed applying twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I took the theming away from the model.&lt;/strong&gt; Everything mechanical became a Python pass over a &lt;em&gt;copy&lt;/em&gt; of the tree. The Builder mirrors the casino frontend into &lt;code&gt;output/&amp;lt;theme_id&amp;gt;/&lt;/code&gt;, then runs its passes over the copy in a fixed order: background image, spin button, color palette, font families, UI strings, structured theme content, &lt;code&gt;theme_id&lt;/code&gt; injection, admin-surface strip.&lt;/p&gt;

&lt;p&gt;Swapping every casino hex for the spec's palette used to be 30 to 40 model patch calls per build. Now it is a function call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# walks every .css / .js / .html file in the output tree
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_color_palette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror is a denylist, not an allowlist — two admin files are excluded and everything else flows through. When the casino gains a hook or a component, themed games inherit it with no Builder change.&lt;/p&gt;

&lt;p&gt;The order of those passes is load-bearing in a way I didn't anticipate. The background and spin-button passes anchor on the casino's &lt;em&gt;original&lt;/em&gt; hex values, so they have to run before the color pass rewrites those same hexes. Get the sequence wrong and the anchors silently stop matching. Deterministic code has dependencies too; they're just dependencies you can unit-test.&lt;/p&gt;

&lt;p&gt;The result: the model's editable surface is now CSS, and only CSS. Every &lt;code&gt;.js&lt;/code&gt; and &lt;code&gt;.jsx&lt;/code&gt; write or patch is refused by a guard before it reaches disk. The model's budget is about six patches, spent on the one thing the passes genuinely cannot do — restyling keyframes, glows, and panel shapes so the game's &lt;em&gt;mood&lt;/em&gt; matches the theme. That remains the flakiest step in the pipeline, and it is now also the smallest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "deterministic" didn't mean
&lt;/h2&gt;

&lt;p&gt;Moving a swap into Python made it reliable. It did not automatically make it correct.&lt;/p&gt;

&lt;p&gt;The themed strings come from the spec, and they get substituted into JavaScript source. One theme's tagline was &lt;code&gt;Every Spin's a Touchdown!&lt;/code&gt;. A plain &lt;code&gt;str.replace()&lt;/code&gt; put it where the casino's original tagline lived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// casino source&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS Fortune Awaits!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after a naive swap&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMessage&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every Spin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;Touchdown&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;);
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The apostrophe closes the string literal. &lt;code&gt;SyntaxError&lt;/code&gt;. The compile gate caught it, but the model couldn't repair it — by then its patch anchors no longer matched the file either.&lt;/p&gt;

&lt;p&gt;The fix was to make the substitution aware of the quote context it was landing in: escape for the delimiter that encloses each occurrence, whether that's &lt;code&gt;'&lt;/code&gt;, &lt;code&gt;"&lt;/code&gt;, or a backtick, and insert verbatim when the match isn't inside a string at all. Obvious in hindsight. The lesson isn't about escaping, though — it's that pulling work out of the model moves the correctness burden onto you. The model would have gotten the quoting right. My &lt;code&gt;str.replace()&lt;/code&gt; didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I observed across all six agents
&lt;/h2&gt;

&lt;p&gt;Each agent taught something specific (posts 3-7 cover those), but patterns emerged across the whole pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Editing got reliable when I changed the file, not the model.&lt;/strong&gt; The Designer generating a spec from a conversation: reliable. Image-Gen producing icons from prompts: reliable. The Builder patching a 1,959-line interleaved file with an exact-string-match tool: fragile, and no amount of prompt work fixed it. What fixed it was splitting that file until nothing exceeded 234 lines, and then deleting most of the edits by making them mechanical. Better tooling would have helped too — AST-aware edits, bounded context, validation loops. But the cheapest lever I had was the shape of the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit, inspectable handoffs made debugging possible.&lt;/strong&gt; Every stage's output was a file on disk — a JSON spec, a set of PNGs, a test report. When a build came out wrong, I could open the intermediate files and see exactly where the pipeline diverged. The requirement isn't "files specifically" — it's that each handoff is durable, immutable, and something you can inspect without running anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human gates belong where mistakes are costly.&lt;/strong&gt; Three approval gates in the pipeline: the spec, the build plan, and the deploy. The spec because a bad spec poisons five stages downstream. The build plan because code modifications are hard to inspect after the fact. The deploy because shipping has external impact and rollback — while possible — carries risk. Every other stage ran without asking, because its output was cheap to redo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching changes the economics.&lt;/strong&gt; The Builder's 868-call runaway cost $138 at Sonnet's list rates, &lt;em&gt;with&lt;/em&gt; Bedrock's prompt caching enabled — each call paid a fraction of the full input cost for the conversation prefix it had already seen. Without caching, the same run would have been far more expensive. Caching made agent loops economically viable for normal runs. It also masked how expensive a runaway was becoming until an hour had passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn caps are the first safety measure, not the last.&lt;/strong&gt; Before I understood why the Builder was failing, I added a sixty-turn cap. It didn't make the agent smarter — it made its worst case bounded. The capped runs that hit the limit aborted in minutes instead of hours and cost single digits instead of triple. Bound the loop before you trust the loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work outside the model is work you can test.&lt;/strong&gt; Every deterministic pass is a pure function over a directory, so every one of them has unit tests — the color swap, the string swaps, the mirror, the quote-escaping, the pass ordering, and the guards that keep the model out of JavaScript. The suite runs in about three seconds and costs nothing, because it never calls a model. That is the under-sold reason to pull work out of a model's reach: not just that deterministic code fails less, but that you can write a test that proves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest ceiling
&lt;/h2&gt;

&lt;p&gt;The pipeline runs end to end. It produces themed, deployed, playable games from a sentence. That's real.&lt;/p&gt;

&lt;p&gt;It's also not a product. Here's what's still true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Icon consistency isn't solved.&lt;/strong&gt; Each image generation call is independent. Thirty icons come back in thirty slightly different styles. Prompt engineering narrows the range; it doesn't eliminate the variance. The games look themed. They don't look art-directed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Tester's navigation is fragile.&lt;/strong&gt; Vision-as-judge works — a model looking at a screenshot can tell you whether the game looks right. But the model guessing CSS selectors to drive Playwright breaks often enough to produce false negatives. Accessible labels and stable test hooks would fix this. I haven't shipped that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cosmetic CSS pass still wobbles.&lt;/strong&gt; Even after shrinking the model's job to "optionally adjust shadows and glows," it still hits the turn cap on some themes. The deterministic build is the floor. The model polish is unreliable upward pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I stopped there. The deterministic Builder was proven — it produced a themed, compiling build with no model involvement. The lessons had stopped being about slot machines and started being about agents in general.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;p&gt;Five things, distilled from months of building this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restructure the target before you blame the model.&lt;/strong&gt; If a file is too big for an agent to edit, that is a fact about the file. Split it, then convert what's left from surgery into generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bound every model loop before you trust it.&lt;/strong&gt; Unbounded &lt;code&gt;while&lt;/code&gt; plus a paid API is a liability, not a feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make every handoff explicit and inspectable.&lt;/strong&gt; Files, versioned objects, whatever — as long as you can look at what one stage produced without running the next one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactor the target for the agent, but never let the agent near the original.&lt;/strong&gt; Making the casino agent-friendly was a behavior-preserving refactor of a live game. Making it safe was structural: the factory mirrors the casino into an output tree and edits only the copy, so no build can touch the real thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know when the lessons have outpaced the project.&lt;/strong&gt; That's the signal to bank them and move on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The game factory was a learning project. What it taught me about scoping agent work, managing cost, and designing for inspectability carries into whatever I build next. The factory itself stays where it is — functional, proven for what it does, but not something I'm polishing further.&lt;/p&gt;

&lt;p&gt;The next thing is something different: applying what I learned here to a problem that isn't themed slot machines. The agent patterns transfer. The domain won't.&lt;/p&gt;

&lt;p&gt;The most reliable agent I built was the one I gradually turned back into a program. The best thing I did for it was to keep taking work away from the model until the only thing left was the thing a model is actually good at.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is the final post in the game-factory series. The earlier posts cover each agent individually: &lt;a href="https://dev.to/esunitha/how-a-single-game-evolved-into-a-game-factory-5763"&gt;the factory overview&lt;/a&gt;, &lt;a href="https://dev.to/esunitha/the-designer-agent-a-spec-from-a-conversation-4nba"&gt;Designer&lt;/a&gt;, &lt;a href="https://dev.to/esunitha/the-image-agents-prompt-to-png-34d0"&gt;Image agents&lt;/a&gt;, &lt;a href="https://dev.to/esunitha/the-builder-agent-the-code-modifier-that-fought-me-5ch0"&gt;Builder&lt;/a&gt;, &lt;a href="https://dev.to/esunitha/the-tester-agent-visual-qa-with-playwright-and-a-vision-model-256m"&gt;Tester&lt;/a&gt;, &lt;a href="https://dev.to/esunitha/the-deployer-agent-and-the-multi-tenant-backend-1h63"&gt;Deployer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>lessons</category>
    </item>
    <item>
      <title>The Deployer agent and the multi-tenant backend</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:14:07 +0000</pubDate>
      <link>https://dev.to/esunitha/the-deployer-agent-and-the-multi-tenant-backend-1h63</link>
      <guid>https://dev.to/esunitha/the-deployer-agent-and-the-multi-tenant-backend-1h63</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 7 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate that says no
&lt;/h2&gt;

&lt;p&gt;The Deployer is the last thing in the pipeline. Every other agent builds toward it: the Designer produces a spec, Image-Gen generates icons, Builder patches the React code, Tester plays the built game in a real browser. All of that work flows into a single question: does the Deployer run?&lt;/p&gt;

&lt;p&gt;The answer isn't always yes. The Deployer reads the Tester's report before it touches anything. If the Tester flagged the build as broken — spin didn't work, win didn't register, login failed — the Deployer refuses to start. Not with an error. Just a refusal. The build exists on disk but it won't ship.&lt;/p&gt;

&lt;p&gt;That refusal is the thing I'm most glad I built. Every automated pipeline I've seen either skips pre-deploy testing entirely or tests in a way that doesn't check whether the product actually works. The Tester runs a real browser, plays the game, and looks at screenshots to decide if it passed. The Deployer treats that judgment as a hard gate.&lt;/p&gt;

&lt;p&gt;What follows, if the gate opens, is the part that involves AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;The Deployer takes a built React app and ships it to AWS using SAM — the Serverless Application Model, which is CloudFormation with a better syntax for serverless resources. The frontend ends up behind CloudFront, served from S3. The backend it points to is a serverless stack: API Gateway in front, Lambda functions handling game logic, DynamoDB storing player data and leaderboard entries.&lt;/p&gt;

&lt;p&gt;It doesn't deploy silently. Before anything changes, it creates a changeset — CloudFormation's dry-run mechanism, which calculates what will be modified without actually modifying it — and shows it to me. If the changeset looks wrong, I can say no. This is the same "agent proposes, human approves" pattern that runs through the whole pipeline: the Designer waits for approval on the spec, the Builder shows its plan before touching code, the Deployer confirms before it ships.&lt;/p&gt;

&lt;p&gt;The deploy has two halves. The backend goes through &lt;code&gt;sam build&lt;/code&gt; and &lt;code&gt;sam deploy&lt;/code&gt; — I show the changeset first, then run with the flag that skips the interactive confirmation, because the review already happened. The frontend is a separate step: &lt;code&gt;aws s3 sync&lt;/code&gt; uploads the React build to the theme's S3 bucket, then a CloudFront invalidation busts the CDN cache so the new version is immediately live.&lt;/p&gt;

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

&lt;p&gt;The obvious outcome is live games at real URLs. Two themed variants are deployed as of this writing: ancient-egypt-slots and norse-mythology-slots, both playable, both with functioning leaderboards and real DynamoDB tables behind them.&lt;/p&gt;

&lt;p&gt;The less obvious outcome — and the one that took more work — is the backend architecture underneath both of them.&lt;/p&gt;

&lt;p&gt;The original casino had a single-tenant backend. Every Lambda function, every DynamoDB table, every API Gateway route was built for one game. When I started generating themed variants, the straightforward path would have been to deploy a separate backend stack per theme: ancient-egypt-slots gets its own Lambdas, norse-mythology-slots gets its own Lambdas, every future theme gets another full stack.&lt;/p&gt;

&lt;p&gt;That's expensive and it's unmanageable. Five themes means five stacks to maintain, five DynamoDB configurations to keep in sync, five deployments every time I fix a bug in the spin logic. The better structure was a shared backend with every read and write keyed by a &lt;code&gt;theme_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I forked the original casino backend into a multi-tenant version: one serverless stack — deployed to eu-west-1 — that serves every themed game at once. A new theme doesn't need its own backend. It registers as another tenant of the shared one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;Two categories of failure. They're quite different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The multi-tenancy migration was a real migration.&lt;/strong&gt; "Add a &lt;code&gt;theme_id&lt;/code&gt; field" sounds like an afternoon. It's not, when the system you're adding it to was designed around one tenant and every path through it assumes that implicitly. I found cases where &lt;code&gt;theme_id&lt;/code&gt; was being passed correctly to Lambda but wasn't making it into the DynamoDB query, so a spin in one game would sometimes read credit data from another. I found cases where the validation checked that the field was present but not whether it matched any known theme. Finding those cases meant writing tests that ran both themes simultaneously and checked for data bleed — not a standard integration test shape.&lt;/p&gt;

&lt;p&gt;The migration also carried its own risk, separate from any theming work. DynamoDB doesn't let you alter an existing table's primary key, so I created new tables with &lt;code&gt;theme_id&lt;/code&gt; as the partition key and migrated the application's queries to use them. On a development backend with no real player data, that was straightforward. In production with existing players, the same migration would mean careful data backfill and dual-read logic during the transition.&lt;/p&gt;

&lt;p&gt;The lesson: making a system multi-tenant is a migration, not a configuration. Plan for it the way you'd plan for any schema change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CloudFormation fails in its own language.&lt;/strong&gt; The Deployer is the stage where the application code becomes irrelevant to the failure. A bug in the React build surfaces in the Tester, not here. If the Deployer fails, the failure is CloudFormation's, and CloudFormation doesn't speak Python or JavaScript.&lt;/p&gt;

&lt;p&gt;I hit two of these. The first was a duplicate logical ID: two resources in the same SAM template with the same name. CloudFormation's error for this doesn't say "you have duplicate logical IDs." It surfaces as a schema validation error, and tracing it back to the specific duplicate took longer than it should have. The second was a &lt;code&gt;ResourceExistenceCheck&lt;/code&gt; — a pre-flight check that validates whether a referenced resource exists before CloudFormation even creates a changeset. I had a reference to a resource the stack expected to already be there, and the check failed before a single resource was created or modified. The error arrived at changeset creation time rather than at deployment, which was useful, but still required reading CloudFormation event logs to understand.&lt;/p&gt;

&lt;p&gt;Neither failure was in the application code. Both required CloudFormation documentation rather than anything else in the project's stack. The deploy step speaks a different dialect than every other step in the pipeline.&lt;/p&gt;

&lt;p&gt;The good part: the test-report gate held every time. One build had a broken win state — reel symbols were rendering but win evaluation wasn't triggering. The Deployer refused it. I fixed the Builder's output, the Tester passed it, and the Deployer ran. The gate did exactly what it was supposed to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gate on real QA output, not on compilation.&lt;/strong&gt; "The build compiled" is not evidence that the product works. A Tester that runs a browser, plays the actual game, and produces a structured report — which checks passed, which failed, screenshots for the failures — gives the Deployer something meaningful to block on. That check takes a few minutes. A broken game on a live URL is worse than a deploy that didn't run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key a shared backend by tenant ID — for low-stakes variants.&lt;/strong&gt; For these themed games — same code, same owner, no compliance boundaries — one backend stack keyed by &lt;code&gt;theme_id&lt;/code&gt; is simpler to operate than five isolated stacks. Every request carries the tenant key, every read and write is scoped to it, and adding a new variant is a registration step rather than a stack provisioning step. This wouldn't be the right call if tenants were separate customers with different security requirements. For variants of one game under one operator, it's the lighter path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget separately for infrastructure failures.&lt;/strong&gt; The Deployer is the first stage in this pipeline where the failure mode isn't in the Python or the React. CloudFormation errors don't look like application errors, don't point to line numbers in your code, and debugging them means learning CloudFormation's event model and pre-flight check behavior. That's a different skill than everything else in the pipeline requires. If you're building an automated deploy step, expect to spend time on this that feels orthogonal to the application work. It is orthogonal. It's also unavoidable.&lt;/p&gt;

&lt;p&gt;The pipeline ends here. The chain runs end-to-end — one sentence in, one deployed game out, with a human approving three decisions along the way. Whether the result is good enough to call a product is a different question, and the answer is honest: not yet. The lessons from why are the subject of the final post.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/the-tester-agent-visual-qa-with-playwright-and-a-vision-model-256m"&gt;the Tester agent&lt;/a&gt;. Next: the final post — observations and lessons.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>cloudformation</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Tester agent — visual QA with Playwright and a vision model</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:04:43 +0000</pubDate>
      <link>https://dev.to/esunitha/the-tester-agent-visual-qa-with-playwright-and-a-vision-model-256m</link>
      <guid>https://dev.to/esunitha/the-tester-agent-visual-qa-with-playwright-and-a-vision-model-256m</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 6 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I ran the ancient Egypt variant through the pipeline for the first time and it passed everything. The Builder produced a clean build. The icons loaded. The themed strings were in the code. Then I ran the Tester, and it told me the login form was broken.&lt;/p&gt;

&lt;p&gt;The form wasn't broken.&lt;/p&gt;

&lt;p&gt;That failure told me more about where automated testing goes wrong than months of reading could have. It's where this post starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Tester agent is
&lt;/h2&gt;

&lt;p&gt;The Tester is the QA stage in a pipeline: Designer, Image-Gen, Background-Gen, Builder, Tester, Deployer. Its job is to take the freshly built game, run it in a real browser, and decide whether it works before the Deployer touches anything. The Deployer reads the test report and refuses to proceed if the verdict is &lt;code&gt;fail&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mechanically: the agent spins up the built game as a local subprocess. It runs &lt;code&gt;npm install&lt;/code&gt;, then &lt;code&gt;npm start&lt;/code&gt; in the output directory, waits for port 3000 to respond, and launches a Playwright browser session. From there it runs a fixed battery of eleven functional checks: navigate to the app, complete the auth flow, place spins, verify themed strings appear, check for console errors, confirm that every API call carries the right theme ID. Then three visual checks.&lt;/p&gt;

&lt;p&gt;The unusual part is how it judges those visual results. After the functional checks, it takes three screenshots — idle page, mid-spin, settled-after-spin — and sends each to a vision-capable model with a rubric:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does the title and color palette visibly match the spec? Are the symbols and patterns panel readable? Is the tutorial modal absent?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The vision model scores the screenshot from 1 to 5 and lists any issues it can see. Playwright can assert on visibility and image dimensions, but my existing assertions didn't cover aesthetic defects — a background that loaded as a flat fallback color, a color palette that was technically present but visually wrong. A model looking at the actual screenshot catches those without writing a specific assertion for each one.&lt;/p&gt;

&lt;p&gt;Two models share the work. Haiku handles the main orchestration loop: executing the checklist, clicking buttons, reading console output, filling forms. Sonnet handles the nuanced visual judgment on the idle screenshot, where theme fidelity matters most. The simpler visual checks (mid-spin, post-spin) also use Haiku, because the bar there is lower.&lt;/p&gt;

&lt;p&gt;At the end the agent calls a terminal tool, &lt;code&gt;mark_test_complete&lt;/code&gt;, with a verdict and a summary. That gets written to &lt;code&gt;.test-report.json&lt;/code&gt;:&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"functional"&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;"passed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&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;"visual"&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;"average_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"evaluations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;"findings"&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;If verdict is &lt;code&gt;fail&lt;/code&gt;, the Deployer reads that and stops. The game doesn't ship.&lt;/p&gt;

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

&lt;p&gt;On a working build, the Tester runs the full happy path. It registers a fresh smoke-test user with a unique email each run (to avoid conflicts with previous runs), logs in, dismisses the tutorial modal, places a spin, waits for the animation to settle, and checks that a win or no-win message appears. It reads console output for errors and reads network traffic to verify every backend call carries the right theme ID. It runs up to 20 spins watching for themed strings: "Pharaoh's Fortune" for ancient Egypt, "Valhalla Jackpot" for Norse mythology.&lt;/p&gt;

&lt;p&gt;The vision evaluation caught things I didn't expect. On one early build, the theme colors were correct but the background image hadn't loaded. The page was technically functional, the DOM had no errors, but the game looked wrong. The vision model flagged it with a score of 2 and the specific note that the background was a flat fallback color. A DOM assertion would have passed that build.&lt;/p&gt;

&lt;p&gt;That's what the Tester is genuinely good at: looking at the actual rendered page and deciding whether it looks right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;The failure mode was in the navigation, not the judgment.&lt;/p&gt;

&lt;p&gt;To drive the game, the agent had to find and interact with elements: fill the registration form, click the spin button, read result messages. The model did this by guessing selectors. It tried placeholder text, role attributes, visible text labels, CSS class patterns. Most of the time it got close enough. Sometimes it didn't.&lt;/p&gt;

&lt;p&gt;The registration form was the worst offender. The form has five fields, and two of them are plain &lt;code&gt;input[type="text"]&lt;/code&gt; — one for player name, one for country. There is nothing in the element's type to distinguish them. To fill both correctly, the agent has to know to target &lt;code&gt;input[type="text"]&lt;/code&gt; with &lt;code&gt;nth=0&lt;/code&gt; for the first and &lt;code&gt;nth=1&lt;/code&gt; for the second. If it guesses wrong, the wrong field gets filled, the form submits with country empty, the browser's HTML5 validation silently blocks the submit, and nothing happens. No error. No network request. Just silence — which the agent records as a test failure.&lt;/p&gt;

&lt;p&gt;Twice the run reported "couldn't find field X" when field X was there and working. Both times the automation had broken on its own selector guess, not on anything wrong in the product.&lt;/p&gt;

&lt;p&gt;This is the worst kind of test failure. A real failure tells you something is broken in the product. A false negative tells you something is broken in the test infrastructure. Because the output looks the same, you don't always know which you're looking at immediately. I spent time debugging the auth form before I realized the selector was wrong.&lt;/p&gt;

&lt;p&gt;The pattern matters: vision-as-judge was the reliable half; LLM-guessed-selector navigation was the fragile half.&lt;/p&gt;

&lt;p&gt;Looking at a screenshot and deciding whether a slot machine matches an ancient Egypt theme is exactly the kind of ambiguous judgment a language model handles well. It can reason about color fidelity, symbol legibility, layout, atmosphere. Reverse-engineering a CSS selector for a button it has never seen the source of is a different task. It's brittle, failure-prone, and not something the model is particularly good at.&lt;/p&gt;

&lt;p&gt;The fix is structural. The form fields should have accessible labels — &lt;code&gt;aria-label&lt;/code&gt; or associated &lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt; elements — which also makes the game more accessible. Where a stable user-facing locator doesn't exist, the Builder should stamp &lt;code&gt;data-testid&lt;/code&gt; attributes: &lt;code&gt;data-testid="spin-button"&lt;/code&gt;, &lt;code&gt;data-testid="credit-display"&lt;/code&gt;. Then the Tester queries by label or role first, falls back to testid, and only guesses as a last resort. That gives the automation stable hooks across every theme variant.&lt;/p&gt;

&lt;p&gt;I haven't shipped that yet. It's on the backlog. The evidence for it is sitting in the test logs: two false negatives on builds that were actually working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;Use a vision model as a complementary judge of a rendered UI. It catches aesthetic defects that specific assertions weren't written for: wrong colors, layout that's technically valid but visually broken, a background that loaded as a fallback. It's not a replacement for deterministic assertions — it's probabilistic and occasionally inconsistent. But it covers the gap between "the DOM is correct" and "the game looks right."&lt;/p&gt;

&lt;p&gt;But don't make the model navigate by guessing selectors. That's where it breaks. Navigation requires knowing the exact shape of the DOM, and a model with no source access is guessing. Every wrong guess is a potential false negative.&lt;/p&gt;

&lt;p&gt;The design that works: give the automation stable, semantic hooks — testid attributes, ARIA roles, predictable form structure — and let the model focus on the judgment calls. Make the thing easy to drive, and reserve the model's cleverness for the evaluation.&lt;/p&gt;

&lt;p&gt;For any pipeline where one agent builds an artifact and the next agent tests it, the cleanest version of this is a contract between the two. The Builder promises to produce certain testable hooks. The Tester relies on them. Both agents get simpler. Failures become easier to localize.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/the-builder-agent-the-code-modifier-that-fought-me-5ch0"&gt;the Builder agent&lt;/a&gt;. Next: the Deployer agent.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>playwright</category>
      <category>llm</category>
    </item>
    <item>
      <title>The Builder agent — the code-modifier that fought me</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:51:02 +0000</pubDate>
      <link>https://dev.to/esunitha/the-builder-agent-the-code-modifier-that-fought-me-5ch0</link>
      <guid>https://dev.to/esunitha/the-builder-agent-the-code-modifier-that-fought-me-5ch0</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 5 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three hours and no result
&lt;/h2&gt;

&lt;p&gt;I was watching the terminal scroll when I noticed the agent had been running for over an hour. Build attempts, compiler rejections, patch attempts. Same file, same error pattern, a new proposed fix each time.&lt;/p&gt;

&lt;p&gt;By the time I stopped it, the call count was 868. Three hours had elapsed. The file it had been working on had two &lt;code&gt;export default&lt;/code&gt; statements, two &lt;code&gt;return (...)&lt;/code&gt; blocks — one of them orphaned in JSX that didn't close — and a mix of correctly-themed and original-themed strings sitting next to each other like the model had started several attempts and stopped mid-thought. The React compiler had been rejecting it for the last hundred calls at least.&lt;/p&gt;

&lt;p&gt;At list rates for the model, that run cost about $138.&lt;/p&gt;

&lt;p&gt;The Builder hadn't done anything wrong by its own rules. There were no rules that said stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Builder is
&lt;/h2&gt;

&lt;p&gt;The Builder is the fourth agent in the pipeline. By the time it runs, the Designer has produced a spec, Image-Gen has generated themed icons, and Background-Gen has produced a background image. The Builder's job is to take all of that and wire it into the actual code.&lt;/p&gt;

&lt;p&gt;The Builder runs a flat &lt;code&gt;while&lt;/code&gt; loop. Call the model, dispatch the tool it chooses, feed the result back, repeat until the model calls &lt;code&gt;mark_build_complete&lt;/code&gt; or something goes wrong enough to abort. The loop itself is eight lines. What makes it interesting is the toolset inside it.&lt;/p&gt;

&lt;p&gt;The Builder has more tools than any other agent in the pipeline. It can read a file, write a file, list the files in a directory, copy a file, patch a file (find an exact string and replace it), and call the terminal tool. Six operations, each corresponding to a real step in the job. The variety matters: most of the build is copy operations, some of it is writes, a small slice is targeted patches.&lt;/p&gt;

&lt;p&gt;There are two human gates. Before the agent touches any code, it proposes a plan and waits for approval. When the build finishes, it presents the output and waits again.&lt;/p&gt;

&lt;p&gt;In the version that burned $138, there was no mandatory compile gate. The model could see compiler output — it was running &lt;code&gt;npm run build&lt;/code&gt; itself via the terminal tool, reading the errors, and attempting patches — but nothing &lt;em&gt;stopped the loop&lt;/em&gt; when compilation failed. The model just kept trying. I added a hard compile gate later: compilation runs after the model signals done, errors feed back for a bounded number of fix rounds, and if it doesn't converge, the build rolls back. But that structure didn't exist during the failure I'm about to describe.&lt;/p&gt;

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

&lt;p&gt;The casino codebase is a React application backed by serverless AWS functions. Every symbol in the game, every color, every win message is baked into the source code. The Builder's job was to fork it into a themed variant: copy the structural files unchanged, swap the colors and fonts and API endpoints to match the spec, and patch the main game component so the reels, win messages, and tutorial reflected the theme.&lt;/p&gt;

&lt;p&gt;For an ancient Egypt variant: gold and lapis tones replacing the original palette, hieroglyph names replacing cloud service names in the symbol configuration, the win message changed from something about AWS architecture to something about pharaohs. Same code structure, different content surface.&lt;/p&gt;

&lt;p&gt;The copy-and-patch pattern worked well for small, isolated files. The CSS config, the sound file manifest, the theme metadata — all clean boundaries. The Builder handled them without complaint.&lt;/p&gt;

&lt;p&gt;The main game component was a different case entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The file was too big to edit safely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SlotGame.js&lt;/code&gt; was about 1,900 lines. It held the spin logic, the win-calculation state machine, the reel animation handling — and scattered throughout all of that: theme-specific strings. Win messages embedded in conditional branches. Tutorial copy baked into JSX. Symbol references in a dozen different places, each with slightly different surrounding syntax.&lt;/p&gt;

&lt;p&gt;I had already moved the color swap, the font swap, and the static string substitutions into plain Python — deterministic passes that ran before the model got involved. That reduced the patch surface significantly. It wasn't enough.&lt;/p&gt;

&lt;p&gt;The remaining patches still ran into the problem the patch tool created: to change a line, the model had to reproduce the surrounding block exactly. Every bracket, every indentation level, every blank line before and after the target. On a 1,900-line file, the model didn't have a reliable mental model of every whitespace detail. It guessed. Sometimes correctly. Often close enough to fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Themed names broke the parser.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Symbol names in a spec sometimes contained apostrophes. A symbol called &lt;code&gt;Chef's Trio&lt;/code&gt; sounds reasonable in a design document. Dropped into a single-quoted JavaScript string, it breaks the parse. The model didn't consistently escape these. The compiler would reject the file, the agent would read the error, and in fixing the apostrophe it would shift an indentation level, which broke a different thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Each failed patch made the file worse.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every edit left the file slightly different from what the model remembered. The model patched based on what it had seen from a prior read — but after each change, the file no longer matched that snapshot. So the model was proposing patches against a version that no longer existed on disk. The exact-string match would fail because the surrounding context had changed, and the model — not knowing what the file currently looked like — would guess at what the block might be now and try again, often wrong.&lt;/p&gt;

&lt;p&gt;There was a way to bound this: force a full re-read of the current file before every patch. I hadn't done that. The token cost of re-reading 1,900 lines on every turn seemed wasteful on the runs that were going well. On the runs that weren't, it would have caught the drift early.&lt;/p&gt;

&lt;p&gt;Duplicate structures were the signature failure. When the exact-match patch tool rejected the model's target string, the model would fall back to rewriting larger sections of the file via the write tool — sometimes inserting the replacement alongside the original instead of replacing it, producing two copies of the same block. Duplicate &lt;code&gt;export default&lt;/code&gt; statements. Two &lt;code&gt;return (...)&lt;/code&gt; calls. Orphaned JSX floating between structural blocks that no longer connected to anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop had no ceiling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;868 calls. Three hours. Over 413 million cached input tokens as the model re-read the same growing conversation on every call. About $138 at list rates for Sonnet — and that was &lt;em&gt;with&lt;/em&gt; prompt caching enabled. Bedrock's caching meant each call charged a fraction of the full input cost. Without it, the bill would have been an order of magnitude worse. Caching made the Builder viable on normal runs; it also made the worst-case run cheap enough per-call that I didn't notice it was spiraling until an hour in.&lt;/p&gt;

&lt;p&gt;I knew abstractly that an unbounded loop backed by a paid API was a risk. Knowing it abstractly and watching it happen are different experiences.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the fix looked like, in outline
&lt;/h2&gt;

&lt;p&gt;The eventual solution wasn't about making the model more careful. It was about removing the reason to edit that file at all.&lt;/p&gt;

&lt;p&gt;The component was fragile to patch because it mixed structural code with content. Theme-specific strings were not isolated — they were woven into the spin logic and the win-calculation branches. The fix was to separate them: move the theme-specific text into a config file the component reads at runtime, so the Builder's job for that component becomes generating a JSON file rather than patching 1,900 lines of JSX.&lt;/p&gt;

&lt;p&gt;Generating a JSON config from a spec is ordinary Python — a function reads the spec fields and serializes them into the shape the component expects. No model involvement, no string matching, no file editing. The component reads the config at runtime. The model never touches the component at all.&lt;/p&gt;

&lt;p&gt;I'm not going to explain that fix in full here — the full redesign and what it teaches about which parts of a pipeline should involve a model at all is the subject of the final post in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;Two things, both short.&lt;/p&gt;

&lt;p&gt;A code-modifying agent is only as safe as the size and shape of what you ask it to edit. Small files with a clean boundary between structure and content are workable targets. Large files with interleaved concerns are not. If your agent is patching a big file and behaving badly, examine the file before you examine the model.&lt;/p&gt;

&lt;p&gt;Any model loop backed by a paid API needs a hard turn cap before you trust it with a real run. Not added later as a safety measure — as a condition of running at all. The cap doesn't improve the agent's reasoning. It makes the worst-case run affordable rather than surprising.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/the-image-agents-prompt-to-png-34d0"&gt;the image agents&lt;/a&gt;. Next: the Tester agent. The full redesign and the general lesson — about what should and shouldn't be an agent — is the final post in this series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>react</category>
    </item>
    <item>
      <title>The image agents — prompt to PNG</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:34:29 +0000</pubDate>
      <link>https://dev.to/esunitha/the-image-agents-prompt-to-png-34d0</link>
      <guid>https://dev.to/esunitha/the-image-agents-prompt-to-png-34d0</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 4 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Icons are what people look at
&lt;/h2&gt;

&lt;p&gt;You can theme fonts, colors, win messages, and sound effects. Change all of it and the game still reads like the original with a skin on it. Swap the icons — the actual symbols spinning in the reels — and it reads like a different game. The casino template I built by hand uses cloud service logos. Replace those with golden scarabs and ankhs and it becomes an Egyptian game. Keep the logos and give it an Egyptian color scheme and it doesn't.&lt;/p&gt;

&lt;p&gt;A full theme has around thirty symbols. Each needs to be small enough to read at reel size, distinctive enough to tell apart mid-spin, and consistent enough that they look like they came from the same place. Getting that by hand for every theme is exactly what I wanted to avoid.&lt;/p&gt;

&lt;p&gt;So two agents handle the visual layer: Image-Gen and Background-Gen. They're the shortest story in the pipeline — almost identical code, real results, and one failure mode I still haven't fully solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two agents, one loop
&lt;/h2&gt;

&lt;p&gt;The Designer's spec carries everything the image agents need. Each symbol entry has an &lt;code&gt;icon_prompt&lt;/code&gt; — a short text description the Designer wrote to describe that symbol's appearance. The spec also carries a single &lt;code&gt;background_prompt&lt;/code&gt; for the full-page background.&lt;/p&gt;

&lt;p&gt;Image-Gen reads the spec, loops through every symbol, and for each one calls Stable Image Core on Bedrock with the symbol's prompt. It gets a PNG back, resizes it to 256×256 (the size the reels expect), and writes it into the app's &lt;code&gt;public/images&lt;/code&gt; folder. After the icons are approved, it seeds DynamoDB — putting each symbol into a table the game queries at runtime to know which icons to load.&lt;/p&gt;

&lt;p&gt;Background-Gen does the same process exactly once, for the background image.&lt;/p&gt;

&lt;p&gt;That's the scope. I grouped them in one post because splitting them into two would mean writing the same agent story twice. They share the same architecture, the same failure modes, and the same lessons. The only thing different is the count of outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs
&lt;/h2&gt;

&lt;p&gt;The happy path is simple. Stable Image Core returns the image as base64; I decode it, resize it, write it to disk. A thirty-symbol run takes a few minutes.&lt;/p&gt;

&lt;p&gt;The resize step looks like a footnote but it isn't. Stable Image Core returns full-resolution PNGs — much larger than the 256×256 the reel layout expects. At native size the images overflow their containers and the game renders incorrectly. No model parameter controls the output dimensions. It's a plain post-download resize call in Python, on every image.&lt;/p&gt;

&lt;p&gt;The fallback is deliberate. Image generation calls fail: timeouts, model errors, prompts that get rejected. A single failed icon shouldn't crash a thirty-symbol run. A single broken icon means a known gap you can regenerate later. So the agent catches per-call failures, logs them, substitutes a placeholder image, and keeps going. The build finishes with a gap rather than a crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it broke
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Consistency across a set is the real problem.&lt;/strong&gt; Each call to the model is independent. It has no memory of the previous call. Generate thirty icons in a loop and you get thirty independent draws from the same distribution — which means thirty different art styles.&lt;/p&gt;

&lt;p&gt;The scarab might come back painterly and textured. The ankh lands flat and geometric. The pyramid looks like concept art from a different project. All technically correct against their prompts. None of them obviously from the same game.&lt;/p&gt;

&lt;p&gt;You can reduce this with prompt engineering. I added a style string to every call — "pixel art, dark background, warm palette, consistent line weight" — and it narrows the range. Keeping symbol counts lower helps. But I didn't solve it. Icon visual quality stayed the main limitation of the factory. One good icon is a solved problem. A coherent set of thirty isn't, not with independent calls to a generative model.&lt;/p&gt;

&lt;p&gt;The games ship looking like themed games. They don't look like art-directed games. That's the honest description of the ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The path failure was invisible.&lt;/strong&gt; Generated images land in &lt;code&gt;public/images&lt;/code&gt;. The image agents write them correctly. But when deployed under a non-root path, referencing the background in CSS with a plain &lt;code&gt;url('/images/background.png')&lt;/code&gt; silently fails — the browser doesn't find the file. No error, no 404 in the console; the CSS rule parses fine, the image just doesn't appear.&lt;/p&gt;

&lt;p&gt;The correct way is a JavaScript inline style using &lt;code&gt;process.env.PUBLIC_URL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;backgroundImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`url(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/images/background.png)`&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That resolves correctly across deployment paths. The fix is a one-liner. Finding it cost more time than that, because nothing in the output told me where to look. Technically a Builder problem — but discovered while debugging why generated images weren't showing up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The size mismatch was boring but necessary.&lt;/strong&gt; Images downloaded fine, the folder looked right, and the game was broken. I had to open the game in a browser to find out something was wrong, then trace back to the image dimensions. The resize step was ten minutes of work; discovering it needed to exist took longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;Text-to-image models are strong at generating individual images. They're weak at consistency across a batch. That isn't a limitation of any specific model — it's what you get when each call samples independently. If you're generating a set of related images, plan for this before you start.&lt;/p&gt;

&lt;p&gt;The options: put a strong, specific style string in every prompt and repeat it verbatim; use a fixed seed if the model supports it; generate one reference image first and use img2img for the rest; or set an explicit acceptance that the set will have variance and decide early what "good enough" looks like. Don't let the consistency problem surface when you're looking at thirty finished icons that don't cohere.&lt;/p&gt;

&lt;p&gt;Own the boring post-processing in plain code. Resize, path wiring, fallback handling — none of that belongs in a prompt or a model setting. The model's job is to produce a good image. Your code's job is to put it in the right place at the right size and handle the call that fails. Keep those two responsibilities separate and both stay simple.&lt;/p&gt;

&lt;p&gt;The fallback pattern generalizes beyond image generation: in any pipeline loop over independent, replaceable items, a per-item failure is recoverable. Catch it at the item level, substitute something visible (not silent), and let the build finish. A gap you can fill later is better than a crash you have to start over from — as long as QA can see the gaps.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/the-designer-agent-a-spec-from-a-conversation-4nba"&gt;the Designer agent&lt;/a&gt;. Next: the Builder agent.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>bedrock</category>
    </item>
    <item>
      <title>The Designer agent — a spec from a conversation</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:24:11 +0000</pubDate>
      <link>https://dev.to/esunitha/the-designer-agent-a-spec-from-a-conversation-4nba</link>
      <guid>https://dev.to/esunitha/the-designer-agent-a-spec-from-a-conversation-4nba</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 3 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The only agent that talks back
&lt;/h2&gt;

&lt;p&gt;Every agent in this pipeline reads files and writes files. The Designer is the exception. You describe a theme — "Norse mythology slots" — and it asks you things. What's the tone: whimsical or dark? Should the high-value symbols be characters or objects? Are there specific win phrases that should feel right for the setting? It consults the original casino's design constraints, checks a worked example, and runs back and forth with you until it has enough to produce a single JSON file.&lt;/p&gt;

&lt;p&gt;That file is the contract. It doesn't deploy anything. It doesn't write any code. It describes the game — symbols with weights and prompts, a color palette, winning patterns, UI strings, fonts, sounds, a background prompt — and every later stage reads it. Image-Gen, Background-Gen, Builder, Tester, Deployer: they all start from that file.&lt;/p&gt;

&lt;p&gt;That sounds like a narrow job. It turned out to be the most consequential one in the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent is
&lt;/h2&gt;

&lt;p&gt;The Designer uses the Bedrock Converse API, the conversational format where you maintain a message history and pass it back on every turn. No framework. A Python class, the API, and three tools.&lt;/p&gt;

&lt;p&gt;The tools define the job more precisely than the system prompt does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;generate_game_spec&lt;/code&gt; is the terminal tool. When the model calls it, the conversation ends and the spec is ready for human approval. Its input schema is the contract: &lt;code&gt;theme_id&lt;/code&gt;, &lt;code&gt;theme_name&lt;/code&gt;, &lt;code&gt;symbols&lt;/code&gt; (each with a name, a weight from 1 to 10, a category, and an &lt;code&gt;icon_prompt&lt;/code&gt;), &lt;code&gt;color_palette&lt;/code&gt;, &lt;code&gt;win_conditions&lt;/code&gt;, &lt;code&gt;patterns&lt;/code&gt;, &lt;code&gt;ui_strings&lt;/code&gt;, &lt;code&gt;fonts&lt;/code&gt;, &lt;code&gt;sound_theme&lt;/code&gt;, &lt;code&gt;background_prompt&lt;/code&gt;. I defined that schema once and every downstream agent reads against it.&lt;/p&gt;

&lt;p&gt;The other two tools are read-only. &lt;code&gt;read_blueprint&lt;/code&gt; lets the model inspect the original casino's design constraints without those constraints being inlined in the system prompt. &lt;code&gt;read_example_spec&lt;/code&gt; returns a worked example so the model can see what a correct, complete spec looks like. Both tools exist so the model can look things up on demand. Without them, the system prompt would need to carry several thousand words of context on every single turn — expensive, and stale the moment anything in the casino changes.&lt;/p&gt;

&lt;p&gt;The conversation follows a short arc. You describe a theme. The model asks clarifying questions for anything underspecified. It reads the blueprint and example as needed. When it has enough, it calls &lt;code&gt;generate_game_spec&lt;/code&gt;. The proposed spec appears, you read it, and you approve, reject, or ask for changes. Approved, it goes to disk as &lt;code&gt;specs/&amp;lt;theme_id&amp;gt;-spec.json&lt;/code&gt;. Everything else reads from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the spec is actually doing
&lt;/h2&gt;

&lt;p&gt;The file is a few kilobytes of JSON. It doesn't feel load-bearing until you watch what depends on it.&lt;/p&gt;

&lt;p&gt;Image-Gen reads &lt;code&gt;symbols[].icon_prompt&lt;/code&gt; to generate each reel icon. If those prompts are vague — "a cool wizard thing" instead of "an ornate golden wizard hat against a dark purple sky, fantasy illustration style" — the icons come out generic. Builder reads &lt;code&gt;color_palette&lt;/code&gt; and patches the casino's CSS variables. It reads &lt;code&gt;ui_strings&lt;/code&gt; to replace the displayed text. It reads &lt;code&gt;patterns&lt;/code&gt; to configure which symbol combinations win. Deployer reads &lt;code&gt;theme_id&lt;/code&gt; to name the stack. The whole pipeline is downstream of whatever the Designer committed to disk.&lt;/p&gt;

&lt;p&gt;A bad spec is quiet for a long time. The Designer doesn't know whether the prompts it wrote will produce good icons. Builder doesn't know whether the colors will look right in the browser. Tester is the first automated stage that catches it — and by then you're four stages in.&lt;/p&gt;

&lt;p&gt;I reduced the risk slightly by making the example spec a tool result rather than a fixed block in the system prompt. The model can compare its proposed output against a well-formed reference before calling &lt;code&gt;generate_game_spec&lt;/code&gt;. That helped. It didn't eliminate the problem. Reviewing the spec carefully before approving it — actually reading each symbol prompt, not just skimming the palette — is the single most valuable thing you can do in the whole pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;Three things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The recursion problem.&lt;/strong&gt; The Designer was the first agent I wrote, and I wrote it with a recursive response handler. When the model calls a tool, &lt;code&gt;_process_response&lt;/code&gt; feeds the result back by calling itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Original Designer: recurse on every tool call
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_process_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;generate_game_spec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tool_input&lt;/span&gt;          &lt;span class="c1"&gt;# done
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;append_tool_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_process_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# recurse
&lt;/span&gt;
&lt;span class="c1"&gt;# All later agents: flat while loop
&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_call_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="c1"&gt;# dispatch tool, append result, keep going
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recursion only goes a few levels deep in practice — the model rarely chains more than two or three tool calls in one turn. It never actually crashed. But it made the code harder to reason about, harder to log each step, and harder to interrupt cleanly. Builder, Tester, and Deployer all use the flat &lt;code&gt;while&lt;/code&gt; loop because it's obvious where the iteration happens. The Designer is the one I wrote first and never went back to fix.&lt;/p&gt;

&lt;p&gt;The first agent you build teaches you the shape. You just have to notice the lesson before you ship the next five.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Garbage in, garbage out — delayed.&lt;/strong&gt; The spec is the contract, so the weakest spec produces the weakest game. A symbol with &lt;code&gt;"icon_prompt": "mystical thing"&lt;/code&gt; gets an icon that looks like clip art. A win condition with no context gets an awkward label in the UI. Neither of those failures shows up until three or four stages later, and the root is back in the spec file. There is no automated check between "Designer approved" and "game looks bad" because the intermediate artifacts — icon files, styled code — are all valid even when the prompts that generated them were weak. The only mitigation I found was discipline at the approval gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The approval gate confused the web UI.&lt;/strong&gt; The pipeline originally ran in the terminal. Human-in-the-loop meant &lt;code&gt;input()&lt;/code&gt;. When I later put it behind a web UI, every Designer turn became a form in the browser — and the Designer's loop has two distinct kinds of turns: conversational turns where the model is asking a clarifying question, and approval turns where it has produced a spec. In the terminal, context makes these obvious. In the web UI, both appeared as the same text box, and users kept trying to approve a question as if it were a finished spec. I had to go back and tag each turn type explicitly — &lt;code&gt;CONVERSATION&lt;/code&gt; vs &lt;code&gt;APPROVAL&lt;/code&gt; — so the UI could render them differently and route the response correctly. The lesson: "a human in the loop" has a shape, and &lt;code&gt;input()&lt;/code&gt; hides that shape until you try to put it somewhere real.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this
&lt;/h2&gt;

&lt;p&gt;Two things, both concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the boundary artifact explicit and inspectable.&lt;/strong&gt; The spec file is not a convenience — it's the reason failures are diagnosable. When a game comes out wrong, I open the spec and the problem is usually visible. If the Designer had handed off implicitly — passing state in memory, writing values into a database you'd need tooling to query — inspection becomes a project instead of a glance. A flat file on disk is not sophisticated, and that's the point. When each stage's output is a file, you can look at it, diff it, and hand it to someone else without explaining an API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a flat loop for anything conversational.&lt;/strong&gt; Recursion reads naturally when you're writing it. A &lt;code&gt;while&lt;/code&gt; loop is easier to reason about, has no depth limit, is easier to add logging to, and is easier to interrupt cleanly. I reached for the while loop on every agent after the first. Go back and apply the lesson to the first one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/how-a-single-game-evolved-into-a-game-factory-5763"&gt;the factory overview&lt;/a&gt;. Next: the image agents.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>bedrock</category>
    </item>
    <item>
      <title>How a single game evolved into a game factory</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:09:50 +0000</pubDate>
      <link>https://dev.to/esunitha/how-a-single-game-evolved-into-a-game-factory-5763</link>
      <guid>https://dev.to/esunitha/how-a-single-game-evolved-into-a-game-factory-5763</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 2 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One sentence in, one game out
&lt;/h2&gt;

&lt;p&gt;The game factory is a pipeline of agents that turns a one-sentence theme into a deployed, playable slot machine. I type "ancient Egypt treasure slots," approve a few decisions along the way, and some minutes later there is a themed game on a URL: golden scarab reels, a papyrus background, win messages about pharaohs, a working leaderboard. I didn't draw an icon, write a line of React, or touch a console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4"&gt;Post 1&lt;/a&gt; was the single slot machine I built by hand to learn the iGaming domain. This post is how that one game became a line that produces variants of it on demand — what the six agents are, how they hand off, and where a human still stands in the line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a line of agents, not one
&lt;/h2&gt;

&lt;p&gt;The obvious version is a single clever agent: "here's a theme, here's the codebase, make me a game." I didn't build that, and the reason matters.&lt;/p&gt;

&lt;p&gt;A slot machine variant isn't one task. It's a spec decision, then image generation, then code changes, then a browser test, then a deploy — each with different tools, different failure modes, and different places a human wants to look before continuing. A single agent doing all of it holds too much in its head, fails in ways that are hard to localize, and gives you nothing to inspect between steps. So I split it into a pipeline, where each stage is a small agent with one job and a clean output the next stage can consume.&lt;/p&gt;

&lt;p&gt;An agent here is nothing fancy: a model in a loop, given a set of tools and a condition that ends the loop. Call the model, let it call a tool, feed the result back, repeat until it signals done. No framework — just the Bedrock API and Python, on purpose, so I understood every part. The interesting engineering isn't in the loop. It's in scoping each agent's job so the loop actually terminates well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The six stages
&lt;/h2&gt;

&lt;p&gt;The pipeline runs in order. Each stage reads what came before and writes something the next stage needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                one sentence in
                │
                ▼
        ┌────────────────┐
        │ Designer       │ ──▶ the spec (a JSON contract)
        └────────────────┘
                │
                ▼
        ┌────────────────┐
        │ Image-Gen      │ ──▶ symbol icons
        └────────────────┘
                │
                ▼
        ┌────────────────┐
        │ Background-Gen │ ──▶ the page background
        └────────────────┘
                │
                ▼
        ┌────────────────┐
        │ Builder        │ ──▶ the themed React code
        └────────────────┘
                │
                ▼
        ┌────────────────┐
        │ Tester         │ ──▶ a pass / fail QA report
        └────────────────┘
                │
                ▼
        ┌────────────────┐
        │ Deployer       │ ──▶ a live URL
        └────────────────┘
                │
                ▼
        a deployed, playable game
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Designer.&lt;/strong&gt; You describe a theme; the Designer asks questions and produces a spec — a single JSON file that every later stage reads. Symbols and their weights, a color palette, winning patterns, UI strings, fonts. It's the contract for the whole build:&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;"theme_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ancient-egypt-slots"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"symbols"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scarab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scarab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;"color_palette"&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;"primary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#C8A24B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#1A0F0A"&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;"win_names"&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;"three_of_a_kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PHARAOH'S FORTUNE"&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;&lt;strong&gt;2. Image-Gen.&lt;/strong&gt; For each symbol in the spec, it generates an icon from a prompt, then resizes it to the size the reels expect. The reels stop being cloud-service logos and start being scarabs and ankhs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Background-Gen.&lt;/strong&gt; The same idea, once, for the page background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Builder.&lt;/strong&gt; The one that turns the casino's React code into &lt;em&gt;this theme's&lt;/em&gt; code. This is the code-modification agent, and it's the one that fought me hardest — enough that it gets its own post and a redesign story.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Tester.&lt;/strong&gt; It runs the built game in a real browser with Playwright, takes screenshots at each step, and a vision model judges whether the game actually plays — do the reels spin, does a win display correctly, does the UI look right. Automated QA that judges by looking, not by asserting on the DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Deployer.&lt;/strong&gt; It builds the app and deploys it to AWS — the frontend to CloudFront, wired to a serverless backend — and it refuses to run unless the Tester's report says the game passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the human stands
&lt;/h2&gt;

&lt;p&gt;The line isn't fully autonomous, deliberately. Between stages there are approval gates: the Designer proposes a spec and waits for a yes; the Builder proposes a plan before it changes code; the Deployer confirms before it ships. The pattern is &lt;em&gt;agent proposes, human approves&lt;/em&gt;. It keeps a person in the loop at exactly the points where a wrong decision is expensive to undo, without making them do the work.&lt;/p&gt;

&lt;p&gt;The handoffs are boring on purpose. The spec is a file on disk. The icons and background are files on disk. The test result is a file on disk. Each agent reads the artifacts of the ones before it. Boring handoffs are inspectable handoffs — when a build looks wrong, I can open the spec and the images and see exactly what the next stage was handed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If you're building something that turns a prompt into a real artifact, the shape worth copying isn't "one agent that does everything." It's a &lt;strong&gt;pipeline of narrow agents&lt;/strong&gt;: each with a single job, a plain-file output the next stage reads, and a human gate wherever a mistake is costly. It localizes failure — when a game comes out wrong, I know &lt;em&gt;which&lt;/em&gt; stage to blame, because I can see what it produced. And it lets you replace or harden one stage without touching the others, which is the whole reason the Builder could get torn down and rebuilt later without disturbing the five agents around it.&lt;/p&gt;

&lt;p&gt;Each stage gets its own post next, in order, same three questions each time: what it is, what it did, and what went wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous: &lt;a href="https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4"&gt;the slot machine that became a factory&lt;/a&gt;. Next: the Designer agent.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>aws</category>
      <category>serverless</category>
    </item>
    <item>
      <title>The slot machine that turned into an agentic game factory</title>
      <dc:creator>Sunitha Eswaraiah</dc:creator>
      <pubDate>Thu, 09 Jul 2026 12:39:00 +0000</pubDate>
      <link>https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4</link>
      <guid>https://dev.to/esunitha/the-slot-machine-that-turned-into-an-agentic-game-factory-45i4</guid>
      <description>&lt;p&gt;&lt;em&gt;Post 1 of 8 in the game-factory series.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;There is a slot machine I built and deployed to AWS, and it looks, at first glance, like a novelty. The reels are filled with cloud service icons instead of cherries and sevens, a leaderboard tracks players in real time, and every so often a small quantum badge lights up in the corner of the screen for reasons that aren't obvious until you look under the hood. It has a tournament mode for live events, a hosted "big screen" leaderboard view for a projector, and a real-time risk gauge that most people don't expect to see on a slot machine at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvkjdd0vh0o644ebhmvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvkjdd0vh0o644ebhmvr.png" alt="AWS Slots — reels, responsible gaming monitor, and live leaderboard" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;AWS Slots — reels, responsible gaming monitor, and live leaderboard
 &lt;p&gt;&lt;/p&gt;

&lt;p&gt;Underneath that surface, it behaves like a real one. Random number generation with a defined design, return-to-player math, session tracking, bet escalation detection — all of the mechanics that make a slot machine a slot machine, implemented the way an operator would implement them. What it is not, deliberately, is gambling. There is no real money anywhere in the system: no deposit path, no cash-out, nothing a player can lose beyond a number on a screen. The cloud icons are the tell. Nobody wagers actual currency for the chance to line up three Lambdas.&lt;/p&gt;

&lt;p&gt;That distinction — a slot machine built with the seriousness of a real product, but without the thing that would make it one — is the reason this project exists, and it's where the story actually starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I've worked with several clients in the iGaming space, and I came into that work knowing almost nothing about the industry itself. I had no real opinions about &lt;a href="https://en.wikipedia.org/wiki/Return_to_player" rel="noopener noreferrer"&gt;return-to-player&lt;/a&gt; percentages (the share of wagered money a machine pays back over time), volatility curves, or how a jurisdiction certifies a &lt;a href="https://en.wikipedia.org/wiki/Random_number_generation" rel="noopener noreferrer"&gt;random number generator&lt;/a&gt;. I was walking into a domain with its own vocabulary, its own regulators, and its own math, and I needed to close that gap quickly.&lt;/p&gt;

&lt;p&gt;Reading through documentation and sitting in on product demos told me what the industry talks about, but it didn't tell me how any of it actually works. That gap is where I usually end up building something — I learn a domain by trying to reproduce its hardest problems in code, not by reading about them. So I set out to build a slot machine that behaved like a real one: a random number generator designed the way a regulator would expect, &lt;a href="https://en.wikipedia.org/wiki/Responsible_gaming" rel="noopener noreferrer"&gt;responsible gaming&lt;/a&gt; features (tools and detection systems meant to catch harmful play before it escalates) implemented the way an operator is required to implement them, and session tracking structured the way a behavioral analytics team would actually query it. If I could get all of that working end to end, I would understand the industry from the inside rather than from a deck.&lt;/p&gt;

&lt;p&gt;The AWS icons weren't just theming — they were the mechanic. Winning patterns in the game are architecture patterns: land two matching services and you score, land three of a kind and you score more, and the jackpot is landing a coherent AWS architecture across the reels. The symbols had to mean something for that to work, and cloud services were the natural fit. It also meant the game could teach a little AWS architecture on the side, almost as a byproduct of playing it.&lt;/p&gt;

&lt;p&gt;Once I started, the project kept growing on its own logic. Learning about rule-based responsible gaming detection led me to want to try the ML-based version. Basic session tracking turned into a full analytics pipeline once I understood why operators care about it. A mentor of mine even suggested quantum-seeded randomness and then built the service himself, which gave me a real quantum RNG to integrate into a live spin path. What began as "understand how slot machines work" became "build the smallest complete iGaming platform I can, and treat every layer of it as if it had to survive a real audit."&lt;/p&gt;

&lt;h2&gt;
  
  
  What the game does
&lt;/h2&gt;

&lt;p&gt;Strip away the infrastructure and the game itself is simple to describe: you spin, you bet, you try to land a pattern.&lt;/p&gt;

&lt;p&gt;The basic win is matching symbols — two of a kind pays a little, three of a kind pays more. The interesting win is the jackpot, and it isn't a random combination. It's a real AWS architecture. Land S3 feeding Lambda feeding DynamoDB across the reels, for instance, and you've built something that actually deploys. The game keeps a small library of valid patterns like this, and hitting one is worth more than any three-of-a-kind. That was the whole point of choosing cloud services as symbols in the first place — the jackpot teaches the shape of a real system as a side effect of chasing it.&lt;/p&gt;

&lt;p&gt;Sitting next to the reels is a live risk gauge, and this is the part that surprises people who've never thought about what's actually required of a gambling product. Every spin gets checked against a set of behavioral rules — are you spinning faster than a person normally would, are your bets climbing after a loss, have you been at this for half an hour straight. On top of that rule-based layer, a machine learning model periodically reassesses your session and produces a risk score, and the gauge moves accordingly. Because there's no real money in this version, nothing actually stops you when the gauge goes red — but the mechanism is the same one a real operator would use to intervene, and building it taught me more about responsible gaming than any article could have.&lt;/p&gt;

&lt;p&gt;The badge in the corner is the quantum tell. Most spins draw their randomness from a standard, well-trusted source. Some spins, when a separate service is healthy and responsive, draw it from an actual quantum circuit instead — the service my mentor built, seeding the randomness from &lt;a href="https://aws.amazon.com/braket/" rel="noopener noreferrer"&gt;Amazon Braket&lt;/a&gt; (AWS's managed quantum computing service). I integrated it into the game as an experiment in whether quantum-sourced entropy could serve a live spin without anyone noticing the difference except for the badge itself.&lt;/p&gt;

&lt;p&gt;And the whole thing isn't limited to solo play. Event mode turns it into something you can run in a room: players join with a QR code, a shared leaderboard updates live on a projector, and a tournament structure lets you run timed competitive rounds rather than open-ended play. That mode is what made the game night possible in the first place.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(A full technical writeup — Lambda counts, RNG seeding mechanics, security layers, analytics pipeline — is in the appendix at the end of this post, for anyone who wants the implementation detail.)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I ran the first version at an office game night. Booked a room, put the leaderboard up on a screen, and let people join by scanning a QR code so nobody had to type anything to get in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwii0bs2p9lxc68gj897r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwii0bs2p9lxc68gj897r.jpg" alt="Demoing the tournament mode to a small group, live leaderboard on screen" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Demoing the tournament mode to a small group, live leaderboard on screen
 &lt;p&gt;&lt;/p&gt;

&lt;p&gt;It landed better than I expected. A small crowd gathered around the screen — people watching the reels spin, waiting to see if the badge would light up, watching the leaderboard reshuffle as new players joined and started competing for the top spot. The room reacted to it the way a room reacts to an actual game, which told me the work had crossed some threshold from a personal exercise into something that functioned as a real product.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpnr0au1p1nf7s9exepzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpnr0au1p1nf7s9exepzi.png" alt="The tournament's live leaderboard projected during a later game night" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;The tournament's live leaderboard projected during a later game night
 &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The tournament mode scaled past that first small room, too. I ran a later event with a full leaderboard projected for a much bigger group, timed rounds, and a proper prize structure at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Far4hw9aumzx0t0rycnyb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Far4hw9aumzx0t0rycnyb.png" alt="Presenting final tournament results and prize winners" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;Presenting final tournament results and prize winners
 &lt;p&gt;&lt;/p&gt;

&lt;p&gt;The game is still running today. Real production deployment, a real Cognito user pool, real DynamoDB tables that have held up under actual use. The player count is modest, but every one of them has a genuine account and a genuine leaderboard entry, and the system hasn't needed to be rebuilt to support any of it.&lt;/p&gt;

&lt;p&gt;More importantly, for the reason I started, the learning stuck. I can reason about RNG design, responsible gaming obligations, session tracking, behavioral analytics, and tournament operations from having built each of those pieces myself, not from having read about them. When I'm in a conversation with someone who actually knows this industry, I understand what they mean.&lt;/p&gt;

&lt;p&gt;And there was one more thing I noticed, standing at that first game night watching the reels spin — something that felt obvious the moment I saw it, but hadn't occurred to me at all before then.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came next
&lt;/h2&gt;

&lt;p&gt;Here's the realization, in one sentence: almost everything that made this a &lt;em&gt;slot machine&lt;/em&gt; was infrastructure, and almost nothing that made it &lt;em&gt;this&lt;/em&gt; slot machine was.&lt;/p&gt;

&lt;p&gt;The thirty-four functions, the machine learning, the quantum RNG, the six layers of security, the analytics pipeline — months of work, and none of it specific to this particular game. What was specific to this game was small by comparison: a set of cloud icons that could have been any set of images, a handful of hex codes for the palette, win patterns in a database table, sound files in a folder.&lt;/p&gt;

&lt;p&gt;Which raised an obvious question: if the infrastructure stayed exactly as it was and only the theme changed, could I produce an entirely different slot machine — wizards and wands instead of cloud icons, or iron thrones, or football clubs — without touching any of the hard parts again?&lt;/p&gt;

&lt;p&gt;Doing that by hand would just be a customization service, not a factory, and it wouldn't have taught me anything new. The version I actually wanted to build was one where the variation itself was automatic — where I could describe a theme in a single sentence and have something else produce the specification, generate the artwork, patch the code, test the result, and deploy it, with no manual editing anywhere in the loop.&lt;/p&gt;

&lt;p&gt;That project became game-factory: a pipeline of six agents — Designer, Image-Gen, Background-Gen, Builder, Tester, and Deployer — that takes a theme prompt and turns it into a themed clone of this casino, deployed to its own address. It works. Two themed variants are live as of this writing. Getting there took a lot of wrong turns, and those wrong turns turned out to be the more interesting story.&lt;/p&gt;

&lt;p&gt;The next seven posts in this series are about those wrong turns and the patterns that came out of fixing them. If you're building agents that modify code, generate assets, or check their own work, some of what I learned along the way should transfer directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: technical implementation
&lt;/h2&gt;

&lt;p&gt;The section above stays at the level a player or a curious reader needs. This is the level an engineer would want.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tp64kul8caoxqm1bgvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0tp64kul8caoxqm1bgvb.png" alt="Presenting final tournament results and prize winners" width="800" height="770"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;AWS Slots Casino — solution architecture
 &lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend.&lt;/strong&gt; A React application, hosted on S3 behind CloudFront. Several distinct modes in the same codebase: solo play, tournament mode with a warmup phase and timed rounds, an "event" mode for hosting live sessions, an admin dashboard for credit resets and pattern approvals, and a big-screen leaderboard view for a projector. Fifty-plus AWS service icons make up the symbol pool. Three ways to win — two of a kind, three of a kind, or a valid AWS architecture across the reels for the jackpot — each with its own payout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend.&lt;/strong&gt; Thirty-four AWS Lambda functions written in Python, organized by responsibility: authentication (registration, login, verification, password recovery), game logic (spins, credit resets, leaderboard), machine learning (behavioral event tracking, risk score inference), events and tournaments (creation, joining, spinning within an event, ending a round), and admin (analytics, feedback, pattern approval). Amazon API Gateway sits in front of all of it, with a WAF web ACL applying &lt;a href="https://owasp.org/www-project-top-ten/" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt; rule sets (the industry-standard list of common web application vulnerabilities), IP reputation filtering, and rate limiting, plus its own throttling — 30 req/s sustained, 100 burst. &lt;a href="https://aws.amazon.com/cognito/" rel="noopener noreferrer"&gt;Amazon Cognito&lt;/a&gt; (AWS's managed user identity service) handles identity, tokens, and password recovery. DynamoDB holds players, sessions, behavioral events, tournaments, event participants, icon metadata, and winning patterns, all as configurable data rather than anything hardcoded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RNG design.&lt;/strong&gt; In production, the spin function calls Python's &lt;code&gt;secrets.randbelow&lt;/code&gt;, drawing from the OS's cryptographically secure entropy pool — sufficient for a real product. A second path exists for experimentation, running the quantum service my mentor built. That service runs on ECS Fargate in Rust, and on startup submits 32-qubit circuits to Amazon Braket, using either the SV1 simulator or IonQ hardware when it's available. Each qubit gets a Hadamard gate and then a measurement, and each measurement collapses into a zero or a one — genuine quantum randomness. Those bits seed a &lt;a href="https://datatracker.ietf.org/doc/html/rfc8439" rel="noopener noreferrer"&gt;ChaCha20&lt;/a&gt; stream cipher (a fast, well-vetted algorithm for expanding a small amount of true randomness into a much larger stream of it) that fills a 100MB entropy pool, enough for roughly 12.5M spins, with a background monitor topping it off whenever it drops below 20%. The spin function calls this service with a 500ms timeout and falls back transparently to the standard path if anything is slow or unhealthy. A field in the response, &lt;code&gt;entropySource&lt;/code&gt;, tells the frontend which path served the spin — that's what lights up the quantum badge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsible gaming.&lt;/strong&gt; Two layers running side by side. Rule-based, on every spin: evaluates against six patterns — spins under 6s apart, bets spiking above 5x the running average, 3x bet increases immediately after a loss, credits dropping below 10, sessions past 30 minutes, and long streaks of rapid consecutive spins. Any of those firing writes an event to a behavioral table in DynamoDB. Model-based, every 10 spins: a function pulls the last 30 days of a player's session data, computes 26 behavioral features across three groups — temporal (session duration, frequency), financial (bet sizing, credit depletion), and behavioral (impulsivity, loss-chasing) — and sends them to an &lt;a href="https://xgboost.readthedocs.io/" rel="noopener noreferrer"&gt;XGBoost&lt;/a&gt; model (a widely used, tree-based machine learning algorithm) hosted on &lt;a href="https://aws.amazon.com/sagemaker/" rel="noopener noreferrer"&gt;Amazon SageMaker&lt;/a&gt; (AWS's managed machine learning platform), which returns a risk score from zero to a hundred driving the live gauge, with four levels from low to critical. There's no real money in the system, so the gauge is a demonstration rather than an intervention — in a real product, high or critical scores would trigger deposit limits or a referral to support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security.&lt;/strong&gt; A defense-in-depth model across six layers, arranged so that if one fails, the next one is still standing. Edge: CloudFront and a WAF handling OWASP rules, IP reputation, and HTTPS enforcement, backed by AWS Shield Standard. Network: API Gateway throttling and CORS restricted to the production domain. Identity: Cognito verifying &lt;a href="https://jwt.io/introduction" rel="noopener noreferrer"&gt;JWTs&lt;/a&gt; (signed tokens used to prove a logged-in user's identity on every request) against its JWKS endpoint, brute-force lockout, short-lived access tokens. Application: every credit and win calculation runs server-side, admin endpoints check the caller's JWT against an allowlist, players can only ever read their own data. Data: encryption at rest, per-function IAM roles scoped to exactly the tables each one needs, public access blocked on every S3 bucket. Detection: CloudWatch alarms, X-Ray tracing on every Lambda, CloudFront access logs flowing into S3. None of it arrived at once — each layer got added as the domain's actual threat model became clearer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analytics.&lt;/strong&gt; The behavioral events table is the source of truth. Every night at 2AM UTC, a scheduled export job scans the table and writes the day's events into S3 as Parquet, where Athena queries it with SQL and QuickSight turns it into dashboards — player behavior over time, risk score distributions, tournament participation, session duration trends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tournament operations.&lt;/strong&gt; Before a live event, a warmup script provisions 10 instances of Lambda concurrency on the event-spin function so cold starts don't interrupt play, then tears the concurrency down afterward so the cost disappears with it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next: &lt;a href="https://dev.to/esunitha/how-a-single-game-evolved-into-a-game-factory-5763"&gt;How a single game evolved into a game factory&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>igaming</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
