sherifbutt.dev · cardiff, uk
$ cat writing/show-me-a-mockup.md #ai #founder #technical

The prompt I never wrote

One continuous ink line: a dense knot of scribble on the left that unspools rightward and resolves into the clean torn edge of a solid cream shape

None of the things I say when I want an image contain the word image. "Show me a mockup for this." "Draw me a diagram of what we're discussing." "Imagine the person who'd actually use this." I wanted Claude — which has already read my code — to notice that, write a better prompt than I would have, and hand me the file. Getting that right turned out to create every other problem in this post.

3 min 20 s, eight chapters. Narrated in a cloned voice, generated locally with pocket-tts for $0.00 — by the same engine this post is about. Transcript.

01 · the pullThe prize was never the image

An agent that only writes text hands you a description of the thing. An agent that can make assets closes the loop: it writes the landing page and the hero that sits on it, drafts the outreach and the voice that reads it, builds the App Store listing and the six screenshots that go with it.

The pull is that Claude has context I'd otherwise have to retype. It has read the code. It knows what the persona is for, what the diagram is explaining, what the mockup is a mockup of. A prompt written by something that has read your repo is better than a prompt written by you at 11pm, and it costs you nothing to obtain.

02 · the barWhat I wanted

Four things, and one of them turned out to matter more than the rest: that Claude picks the model per request rather than me once at install time; that a draft mockup routes somewhere cheap and a final hero routes somewhere good; that odd aspect ratios — 9:16, and much longer — reach the provider intact; and that I never write the prompt.

The last one is the important one, though I didn't understand why until months later.

03 · the first attemptWhat I tried first

I went looking for something that already existed, and found guinacio/claude-image-gen. It is a real project — 59 stars, wired into Claude Code as both a Skill and an MCP server, and it worked.

It also got the hard part conceptually right. Its author deliberately named the MCP tool media-pipeline / create_asset rather than gemini-image-gen / generate_image, so that Claude would reach for it on intent rather than on the literal word "image." That is the same instinct I had. Someone else had already noticed that the request never says image.

04 · the diagnosisWhy it didn't work

In April its README was titled Gemini Image Generation, and it was Gemini only. That was survivable. What wasn't survivable was how the model got chosen.

Configuration was one environment variable — GEMINI_DEFAULT_MODEL, defaulting to gemini-3-pro-image-preview. Available models were fetched from the API at runtime and validated, which is more than most tools do. But there was one default, set once, in config.

I wanted Nano Banana Pro for finals. I also wanted something cheap for the twenty throwaway mockups I'd generate on the way there. With a single default I could have one or the other, and switching meant editing config and restarting — which is exactly the kind of involvement the whole idea was supposed to remove.

The one decision that had to be made per call was the one decision fixed at install.

A human picking the model at config time is fine when the human is also writing the prompt. The moment the agent writes the prompt, the agent is the only party that knows whether this particular request is a throwaway or a deliverable — and it had no way to say so.

I sat with that one night and decided to build it instead. The next morning the repo went from empty at 09:03 to a multi-provider server with cost tracking at 10:55, budget enforcement at 10:59, and provider failover by 11:31.

05 · the road not takenWhat else was on the table

Forking guinacio's would have meant retrofitting a multi-provider architecture into something built around one, which is usually slower than starting over. Routing everything through OpenRouter was tempting — one key, several image models — but it is a passthrough with a margin on top, it does no text-to-speech, and it has no opinion about what anything costs. A plain library or CLI was the obvious cheap answer, and it fails the only test that matters: you have to remember a library exists. Claude reaches for what's in its tool list.

06 · the pivotWhat changed

The fix was a single knob — small | mid | pro — that means the same thing on every provider, and that the agent sets per call. Aspect ratio became a real parameter, routed differently per provider because they disagree about how to accept it. Nothing about the prompt code changes when the provider changes.

35×
small → pro, same request
$0.006
gpt-image-2, small
$0.211
gpt-image-2, pro
30×
video draft → top tier

Twenty throwaway mockups at the wrong tier cost $4.22. At the right tier they cost twelve cents.

Then two consequences arrived that I had not planned for, both caused by the thing working.

The first is that ambient generation is pleasant, and pleasant things get used more. When you don't have to write the prompt, you ask for more images. Volume rises because the friction is gone, which means a runaway loop is now a billing event rather than an annoyance. That's why the budget cap was in the plan before I had spent anything: hard daily, weekly and monthly limits, warning at 80%, blocking at 100%, checked before the call rather than after the charge. The default is $5 a day.

You cannot reproduce a prompt you never wrote.

A week later I'd want that same character, slightly older. That same hero, slightly warmer. The prompt was never mine — it was somewhere in a chat log, if it still existed at all. My actual workflow became handing the PNG back to Claude and asking it to reverse-engineer the prompt from the image. I was doing forensics on my own artwork.

So every output now writes a hidden .regenerate.json sidecar next to itself carrying the whole recipe — prompt, provider, model, tier, params, cost, lineage. regenerate re-runs it. iterate re-runs it with an adjustment and threads parent to child. "Same one, warmer" went from an investigation to a command.

07 · the lieSuccess is not evidence of success

I handed a video model 35.44s of audio and it gave me back 20.02s of video, with succeeded in the response.

No error. No warning. No field anywhere indicating the audio had been cut. I found out because I watched the clip.

The model caps at twenty seconds. It reaches the cap, stops, and reports success, because from its point of view the request completed — there is no field for "I did most of this". The only detector available was a human looking at the output.

That is the same shape as the prompt problem, and it has the same cause. When you compose the call, you are also the thing that checks the result. Hand the call over and succeeded becomes the only signal you have — and succeeded means the HTTP request completed, not that anything worked.

So the engine stopped trusting it. Cloning is now verified by cloning a single token and feeding the result back as its own reference before the real run — a broken setup gets caught at the start rather than discovered in the finished cut.

08 · the reusable ideaWhere to put the intelligence

This is the part worth stealing if you're building anything on top of an agent. The temptation is to teach each skill about models and prices. Don't. Keep one engine that owns that knowledge, and let every skill call it with intent expressed in ordinary words.

The skill decidesThe engine decides
That something is needed at allWhich model serves it
What it's for, and whyWhich quality tier that implies
Intent in plain words — "high quality", "transparent PNG", a styleSeconds per scene and cost per second, for video
The creative briefPaid or free voice, and which one
 What it will cost, before the call is made

Skills declare intent. The engine resolves intent into a model and a price. A skill saying "high quality, transparent background, 9:16" doesn't need to know that transparency requires gpt-image-1 because gpt-image-2 rejects it — the engine knows, and catches it before the call.

"show me a mockup for this screen" skill decides it needs one engine resolve tier · small mid pro pick model · price it check budget · pre-call refuse, with the £ call provider .regenerate.json + ledger entry the receipt intent in words
The division of labour. Everything left of the orange box is expressed in ordinary words; everything inside it is a decision about models and money.

09 · honestlyThe tradeoffs

curl in a Bash callSingle-provider pluginThis engine
Working in the next 60 secondsyesyesno
Model chosen per call by the agentnonoyes
Cost known before the callnonoyes
Reproducible from the artifact alonenonoyes
Free local optionnonoyes
Reusable as an engine by other skillsnopartlyyes
Things that can break1few8 providers, 38 price entries

The first row is real and I'd defend it. If you need one image this afternoon, write the curl. The last row is also real: more providers means more surface, and the pricing table is 38 hand-maintained entries that warn when they're 30 days stale, because they do go stale.

10 · the objectionBut you are deleting MCP servers

This is the objection I would raise, and it deserves the body of the article rather than a footnote.

"My MCP servers were eating a third of my context before I typed anything. Every connected server loads its full tool schema into context on every message, whether I touch those tools that session or not." r/ClaudeAI
"in one month i reached 11 MCP servers and more than 128 Tools […] so i had to start disabling some." r/mcp

Both are true and neither is answered by me being enthusiastic. Three honest responses.

The first is the arithmetic in the section above, run backwards. Four skills need generation. One engine they share is one server; four skills each carrying their own image and voice code is four. The engine is how the count goes down.

The second is that tool schemas are increasingly fetched on demand rather than loaded up front — this very session is running that way. I have seen a figure quoted for what that saves and I have not measured it, so I am not going to repeat it as though I had.

The third is that if you only ever need one image, none of this applies to you. Write the curl. It genuinely is faster.

11 · the billWhat it cost

One day of this was a total loss. On 21 April I shipped v0.7.0 through v0.7.7 — eight releases, six different approaches — trying to make Claude Code's ${user_config.*} substitution pass configuration into the plugin's MCP server. Every attempt failed the same way: the server never spawned, and nothing anywhere said so. v0.7.8 reverted the manifest to plain shell environment variables and kept every line of the day's actual engineering.

What I gave up was the install-time prompt that asks for your API key and stores it in the keychain. Instead you export shell variables before launching Claude Code, which is worse, and which I document apologetically. Also still open: Gemini TTS batch, multi-chunk caption timestamps, and that pricing table's drift.

12 · the accidentThe waterfall

I set out to build an image generator and accidentally built a dependency.

Four of the ten skills in my ~/.claude/skills now declare a hard dependency on it — ad-studio, designer, stager, and writer. An App Store screenshot generator. A writing skill. And demo-reel, a separate plugin that records narrated demo videos, which shells out to the engine's CLI whenever a scene needs a talking-head avatar or a generated plate, resolving whichever plugin version is newest on the machine. None of them contains code for choosing a model or pricing a call. They ask, in words, and something else decides which one and what it costs.

That is the part I'd encourage anyone to copy. One place that gets updated and feeds everything else, rather than every skill growing its own half-implementation.

13 · unfinishedWhat's still open

The free tier is the thing I want to fix next. Adding pocket-tts was meant to stop $0 being a single point of failure, and it works well — until you try it on the Intel Mac I write on, where it needs torch>=2.5.0 and PyTorch hasn't shipped a macOS x86_64 wheel since 2.2.2. So on my own machine, free is currently theoretical. A local escape hatch is only an escape hatch on hardware someone still builds for, and I don't yet know whether the answer is a container, a second machine, or accepting that the free tier is for other people.

This article is an instance of everything above. The words were drafted by the writer skill, which depends on the engine. The narration at the top of this page was rendered through it, in a cloned voice, on a local model, for nothing. Somewhere in that chain a model got picked, priced, and written down — and I didn't write the prompt for any of it.

claude-image-tts-gen is MIT licensed. v0.13.0 — 88 commits since 18 April, eight providers, one budget, one ledger.