MASCOT FACTORY

About

The Mascot Factory

You type a sentence about yourself and get a mascot. That part takes a few seconds and is meant to be fun. The reason it exists is underneath: this is a small, complete, working example of an outcome factory, where the deterministic work runs as code and a model spends its judgment only where the rails genuinely run out.

Everything on this page is honest about the version that came first, which was worse and cost more. That failure is the most useful thing here.

The whole idea, in one picture

There are two kinds of work in almost every agentic system, and they get billed very differently. One kind has a right answer that never changes: where the pixels go, what the palette is, how a hat sits on a head. The other kind is genuine judgment: reading a person and deciding that they are more of a rust-and-wrench sort than an amber-and-balloon sort.

The first kind belongs in code, forever. The second is what you actually want to pay a frontier model for.

A diagram in two halves. On the left, labeled judged, one model call: a sentence about yourself goes into a trait selector agent, which is constrained by a trait catalog of eight colors, eight eyes, eight hats, six held items, six grounds and four effects. On the right, labeled paved, zero model calls: six enum values cross into compose.ts, a measured pixel grid in code, which produces the mascot as SVG in the browser, 73,728 outcomes and all of them free.

The model never draws anything. It returns six values, and every one of them is an enum in a strict output schema, so an illegal choice is not caught by a validator afterward. It is impossible to express. Then compose.ts turns those six values into SVG in your browser: no network, no image model, no storage, no wait. Same six values, same picture, every time, on any machine.

That is what makes the catalog interesting rather than limiting. Six slots over the canon library is 73,728 distinct mascots, before counting anything paved, and all of them already exist in the sense that matters. Nobody has to generate them and nobody pays for them.

The version that came first, and was bad

The first build did the obvious thing: it asked an image model for a mascot on every request. It was slow, it cost real money per picture, it needed storage and a billing adapter, and the results were inconsistent enough to be unusable. The crab had a different number of legs each time.

The diagnosis is not that the model was bad. It is that we had put an intelligence to work on a task with a right answer, and asked it to rediscover that answer on every single run. That is the figure-it-out cost, paid on repeat, forever, by every user. The fix was to go measure the grid once, write it down, and hand the drawing to a pure function.

The Factory Manager is the shape that came out: an agent that generates almost nothing fresh per run and spends its judgment only at the edges. Deleting the image model made the product faster, free, more consistent, and genuinely better looking. It is worth being blunt about that, because the intuition runs the other way: it feels like more model should mean more quality.

The full architecture

Nothing here is exotic. That is somewhat the point.

An architecture diagram split into two zones. The left zone, you run this, contains generators in git holding the measured grid, guards and an install map, which install canon.json into compose.ts; a Next.js app on Vercel handling /api/generate with auth, quota and rate limiting; and Firestore holding characters, paved traits, trait stats and quotas. The right zone, Anthropic runs this, contains a trait selector agent using strict structured output at one call per mascot, a trait paver agent that runs only when nothing fits, and a shared backend that is claude --print in development and the Anthropic API in production.

Six layers, bottom to top:

  1. Canon. What the mascot is. The grid was measured out of reference screenshots by run-length analysis rather than guessed, so the torso, the leg positions, and the eye columns are numbers with a provenance instead of taste applied repeatedly. It lives in two generators in git, with an install map that pushes the built canon.json into this app so the two cannot drift.
  2. Guards. Every constraint that cost somebody a cycle to discover is now an assertion that fails loudly plus a written rule at the point of use. A malformed pixel pattern raises instead of rendering a balloon as a diamond, which is a real bug that shipped once. A contrast rule refuses an invisible mascot. The rules are written where the next agent will read them, and they say plainly that they are settled, because a constraint that lives only in a chat log gets rediscovered and re-broken. This is the difference between a loud failure and a silent one.
  3. The factory. A pure function from six values to SVG. Runs in the browser, costs nothing, takes no time, and is covered by ordinary unit tests because it is ordinary code.
  4. The manager. One model call that reads a sentence about a person and returns six enum values with a short rationale. This is a one-shot: no conversation, no tools, no retries in the happy path. It is also the whole argument for the effort dial, since the expensive reasoning is spent on the only step where two careful people would disagree.
  5. The paver. When someone describes themselves and nothing in the catalog fits, a second agent authors the missing trait, validates it mechanically, and adds it to the shared library. More on this below.
  6. The signal. Every generation records which traits were offered and which survived to the saved mascot. Keeping a mascot is an upvote for each of its traits, and swapping one out is a downvote for the one you removed.

Paved paths benefit everyone

The catalog is not fixed, and this is the part worth watching. Describe yourself in a way the library cannot express, and the paver agent authors the missing piece: it first sweeps the existing library for something close enough to reuse, and only when nothing fits does it write a new pixel pattern. That pattern then runs a gauntlet of pure checks before it is allowed to exist: grid bounds, palette legality, fill ratio, anchor alignment, and a render smoke test.

If it passes, it goes into the shared library and everyone who arrives afterward can pick it, at no cost and no wait. The person who needed a watering can paid for the watering can once, on behalf of every gardener who shows up later. That is paving in miniature: the desire path gets walked first, and only then does it get paved. You can see everything that has been paved so far on the paved traits page.

The loop that improves the factory

Because the library is data and the signal is real, the factory can be improved by an agent rather than only by us.

A six-step loop. Someone arrives and types a sentence about themselves. The selector picks from the catalog and only from it. The paver extends the catalog only when nothing fits, paid once by whoever needed it. Signal accrues as offered versus kept, with nobody voting. An improver agent proposes promotions, retirements and inventions on a schedule. The canon absorbs the change through a reviewed pull request, and the next visitor gets a better catalog.

The last step is a scheduled deployment on Managed Agents: one versioned agent, fired by a weekly cron, with the generator repository mounted into its sandbox. It reads the keep rates and the gap records, and proposes three kinds of change. Promote a paved trait into canon because people keep choosing it. Retire one that is offered often and kept rarely. Invent one to fill a hole that keeps showing up in what people write about themselves.

The gap records are the half that is easy to miss. Keep rates can only tell you about traits that exist; nothing in them can describe the trait nobody could offer. So every time the selector says nothing in a layer fits, that gets written down with what the person actually wrote, including the times the paver tried and failed. Those failures are the loudest evidence in the whole payload, and they used to be a line in a log nobody read.

The signal it reads is handed over deliberately unranked and unclustered. Computing a promotion score and having the agent approve it would be this project’s original mistake in miniature: paving the judgment and leaving the arithmetic to the model. Deciding that a 71 percent keep rate over fourteen offers beats a perfect record over three, or that four people asking for different garden tools are one missing trait, is the contested call the agent is actually there to make.

It proposes. It does not merge. Each firing edits one file, the trait catalog in the generator repository, regenerates the bundle consumers install, renders a proof sheet, and opens a single pull request capped at about five changes, because a diff nobody finishes reading gets rubber-stamped and a rubber-stamped gate is not a gate. Most firings should open no pull request at all, and that is the system working rather than failing.

That is the human gate, and it is deliberate rather than cautious: taste is the one thing here that should not be automated. The permanent rule about how the eyes may be drawn exists because a human looked at a batch and said no. No keep-rate metric would ever have produced that judgment. Somebody has to be the taste holder.

Status, because this page is otherwise about being honest: the improver ships with this site, its method is committed to the generator repository, and its weekly schedule is defined in scripts/improver/provision.mjs. It has not fired yet. Until it opens its first pull request, this section describes a system that is built rather than one that has run.

Retiring a trait is safe here for a specific reason worth stating: every saved mascot freezes its own trait definitions at the moment it was made, so a retired trait keeps rendering correctly on every piece that already uses it. The improver removes things from the menu, not from history. That property is what makes an unattended agent editing the catalog a reasonable idea at all.

Notice what is absent: there is no voting, no leaderboard, and no engagement number rendered anywhere on this site, on purpose. The system learns from what people actually keep, which is a truer signal than what they would click, and it costs the visitor nothing to give.

Where the managed agents fit

Two of the three agents here are one-shot classification and authoring, which is a plain Messages API call with a strict schema, and reaching for a stateful agent runtime would be the wrong tool. Managed Agents earn their place at the third one, the scheduled improver, because that job is long running, stateful across runs, and has to hold context about what it has already proposed. The usage rule there is agent once, session every run: one versioned config created a single time, then a fresh session per cycle. Creating an agent inside the request path is the documented anti-pattern.

It is worth saying plainly that being able to tell these apart is most of the skill. The same product could have been built with a stateful agent doing every step, and it would have been slower, more expensive, and harder to test, while looking more sophisticated.

How this was tested without spending API credits

The trait selector and the paver sit behind one interface with two implementations. In development it shells out to headless Claude Code on a subscription; in production it uses the Anthropic API with a key. Same prompt, same schema, same code path, no API spend while iterating. That pattern is Subscription-Local, Platform-Production, and the important half is the boundary: subscription auth is for Claude Code and Claude.ai, so a third-party product in production uses an API key. The local backend is a development convenience and is not what serves you now.

Pave your own

The move that produced this site is packaged as a skill you can point any agent at, hosted openly so nothing has to be installed:

Pave the Pathor read what a hosted skill is

Call it right after a long agentic run that succeeded, which is exactly when you are least likely to look back at it. It reads what the run actually wrote rather than your memory of it, finds the improvisations that will certainly recur, and builds them into the system so the next run cannot repeat them. The bar it holds every candidate to is one sentence: I hand-rolled X, and the next invocation that needs X is Y. If you cannot name Y, you do not pave.

Further reading

Everything above is drawn from the Takeoff with Claude wiki. The concepts this demo leans on hardest:

The diagrams on this page are generated from committed specs by the wiki’s diagram emitter and installed here, rather than drawn by hand. Fixing one means editing its spec and re-running the install, which is the same discipline the page is about.