A cheap model built me a working service for $8.33, and its passing tests hid the two bugs that mattered most

I run a lot of projects for one person. For a while I’ve wanted a cheap way to hand a bounded idea to an autonomous builder and get something real back: a service that runs, has tests, and can be judged. Cheap enough that a dead end costs lunch money instead of a weekend.

So I ran it. A cheap model, a bounded spec, a $20 budget. It built the thing, and that mostly worked. Where it got interesting was what “done” turned out to contain, and how little it cost me to find out.

The whole run came to $8.33.

Picking the stack

The model was the easy call: GLM-5.2. Cheap, and on paper strong enough to write a small backend service. Everything after that was a chain of constraints, each one knocking out the obvious choice.

I didn’t want to send my code to Chinese hosting. GLM is a Chinese model, but it’s an open-weight model so I don’t have to run it in China; GMI Cloud serves it from a US endpoint behind an OpenAI-compatible API. That fixed hosting and created the next problem. OpenAI-compatible means not Anthropic-compatible, which rules out Claude Code, my usual harness, because it speaks the Anthropic API which z.ai supported but GMI did not. I needed an agent that talks to a generic OpenAI endpoint. Goose, Block’s open agent, does, and it supports the thing I care about for autonomous work: spawning subagents and running tools. So the stack was GLM-5.2, hosted on GMI, driven by Goose.

The configuration fight

Getting Goose to talk to GMI took longer than I’d like to admit. The goose configure wizard wants to fetch the provider’s model list to confirm the connection, and that probe kept failing against GMI. Network errors. A 405 on a doubled /v1/v1/models path. The model I wanted, zai-org/GLM-5.2-FP8, missing from whatever list it did manage to pull. The wizard is built for the well-known providers and does not cope with anything off that list.

What worked was skipping the wizard and setting the provider by hand:

bash
export OPENAI_API_KEY="<your-gmi-key>"
export OPENAI_HOST="https://api.gmi-serving.com"
export GOOSE_PROVIDER="openai"
export GOOSE_MODEL="zai-org/GLM-5.2-FP8"

Then, when Goose still couldn’t find the model in its fetched list, typing the name in by hand (“enter a model not listed”). After that it connected and stayed connected. If you ever point Goose at a non-standard OpenAI-compatible host, set the env vars yourself and skip the wizard.

The target and the guardrail

The build target was deliberately boring: a small OpenAI-compatible LLM router. A proxy that takes chat-completion requests and routes them to configured backends, with fallback, cost accounting, and streaming passthrough. (Yes, I used an LLM harness to build an LLM router. I know.) I chose it because it is small, self-contained, testable without real API keys, and because its acceptance rubric is crisp enough to score as a number instead of a vibe. The router itself was disposable. What I wanted was a read on the stack.

The guardrail was the $20 budget and one instruction that does more than any other to keep a cheap run cheap: stop when the acceptance tests pass, and don’t gold-plate. An open-ended “make it great” just wanders, and the wandering is what runs up the bill.

The build

Three prompts. Scaffold the repo. Read the spec and write an integration plan to a professional bar, then stop and show me. Execute the plan, and fan out where it helps.

Two things from the run stood out. When it went to create the GitHub repo, its token lacked repo-creation rights, so it failed and started reaching for an OAuth device-code flow to get them. Instead of handing a cheap autonomous agent broader credentials, I created the repo myself and told it to continue. My autonomous system has limited permissions but high autonomy. My laptop has high permissions but limited autonomy. That split has worked well for me: I hold the credentials and do the steps that can’t be undone. It also decided on its own to commit and push straight to main when it finished. On a throwaway benchmark repo that’s fine, but I noted it. (yes I’d normally set up branch protections but again, throwaway repo…)

Twelve minutes of wall-clock later it reported done. Clean FastAPI service, modules split sensibly, Decimal cost math, structured JSON logs, streaming, 45 passing tests, ruff clean. It looked like something a competent engineer would open a PR with.

The catch

So I did what I do with any PR that looks done: I reviewed it. The green suite was hiding two bugs, both in the streaming path.

The first was that streaming cost was always $0. The code computed the cost of a streamed response before the stream had been consumed, off a usage object that was still all zeros, and billed that. Token counts came out right, because they were read from the same object after it filled in, so the ledger looked populated. But the money stayed at zero for every streamed request. For a router whose whole job is cost accounting, that is the one number that probably shouldn’t be wrong every time.

The other significant issue was broken SSE framing. Server-sent events are separated by blank lines. The passthrough stripped them and re-emitted each data: line with a single newline instead of the double newline that delimits events. A real OpenAI SDK streaming client frames on that blank line, so it would read the whole response as one malformed event and fail to parse it. Drop-in compatibility, the entire selling point, did not hold for streaming.

Both bugs survived because the tests were green. The streaming test checked token counts but never the cost. The passthrough test read the stream line by line instead of by event, a weaker check than any real client performs. The suite passed most confidently in the two spots it wasn’t really looking.

The model takes the shortest path that satisfies the criteria you actually wrote. “Has tests” isn’t a real criterion, so it writes tests that confirm the code it just wrote, using the same assumptions that produced the bug. The test and the bug come from the same head, so a self-authored test inherits its author’s blind spot. There’s an easy lever, though: have a clean, context-free session write the tests, separate from the one that wrote the code, and the bias drops.

The tell

I wanted to know whether that was catchable without me hand-tracing async code, so I gave Goose a lazy one-liner (in a new context-free session), “do a staff-level review, look for bugs, concurrency issues, and security concerns,” and it found both as well, with its own list attached.

The lazy result is a useful one. No hand-holding and no pointing, and a cheap model surfaced the two worst bugs in its own output. These bugs are not subtle race conditions. They are ordering mistakes that jump out the moment someone reads the streaming code, and a cheap review pass also found them. That’s solid for an MVP-quality bar. Run two or three and combine the findings and you have a working gate for pocket change. Building is the commodity now. The review is where you decide whether to trust the thing, and that turned out to be cheap too.

Fix everything

For the last step I gave it the most ambiguous instruction I had, “fix everything,” and watched what it did with the rope. That prompt is a bit of a trap, because it pulls against “don’t gold-plate,” and an eager model could start bolting on improvements that were not required for the MVP. I wanted to be sure it just fixed the issues it had raised and didn’t just go wild and waste tokens.

It didn’t. It fixed the findings and stopped, with no speculative abstractions. And on the one hard call it did something I’d be happy to see from a real engineer. An HTTP response header is flushed before the body streams, so the true cost of a streamed response cannot go in a header, because it isn’t known yet. The dumb fix would buffer the whole stream to learn the cost, which defeats streaming. It didn’t do that. Instead it set the header to pending, made the ledger the source of truth, computed the cost after the stream drained, and left a comment explaining why. It also updated the cost test to assert the real number. 56 tests, still clean.

One gap slipped through. The framing fix is correct, but nothing tests it, which is the same blind spot that hid the bug in the first place.

The bill

Build, review pass, and the fix round came to $8.33, under half the budget. The router works. Not sure what I’d use it for but hey. Now I have an MVP-quality llm-router in my pocket. What matters more is that the loop closed cheaply end to end. The model built a working service, the review caught the bugs its passing suite was hiding, and the fix landed them, the hard one included, while I did nothing but very casual steering.

What I took from it

The cheap frontier is real. A model that costs well under a dollar a run, in a competent harness, produced a bounded backend service at a bar I’d accept in a PR. For throwaway prototyping, that changes the calculation on whether an idea is even worth trying which was my goal. I’m building a system where I can dictate an idea and get back a list of user stories that I can tweak and get back a spec that I can review and say “go” and get back an MVP with real code to evaluate. All while walking the dog. I love it.

Building is now cheap. Validation still requires either trust, process + trust, or proper attention.

And it handled ambiguity better than I expected. Which was my real hope. “Fix everything” produced restraint and one real architectural judgment, not a thrash. That surprised me more than the bugs did.

And keep controls and fleshy human hands on anything you can’t undo: credentials, pushes, deletes. Cheap and autonomous is a great pairing right up until it isn’t.

For eight dollars I got two things: a working service, and a clear map of where a cheap autonomous builder gets things wrong without telling you. Both were cheap to get. The second is the one I was hoping for.

My next task for it will be a $40 budget and tasking it to do something stupid large. Curious to see how that’ll work out.