My AI Wrote Trash Code. And That Was Exactly What I Wanted

There’s a widespread belief that when you use AI to code, you have to ask it to do things right from the start. Clean architecture, small files, quality tests. And if it doesn’t turn out that way, then the tool is broken or you don’t know how to use it.
The reality is that this can hold you back, especially in exploratory projects. And bad initial code isn’t necessarily a problem.
What I’ve realized—and confirmed in my last two projects—is that the time to ask for clean code is the second step, not the first.
This applies to entire projects, but I also see it clearly for big features within an existing project.
The TL;DR is: your work with code doesn’t end when it works. It ends when the code is maintainable and scalable. And you don’t get that with a perfect first attempt, but with successive iterations of refactoring.
The Beginning: A Burned-Out Server and a Decision
Postiz (the tool I use to publish on social media) fried a 4GB RAM server. I couldn’t even SSH in to stop Docker. I literally lost access while it kept eating up resources.
It was funny how AI helped me recover it. But I’ll save that story for another post 😄
I’d been putting up with its problems for a while: the unusable mobile UI, the sluggishness for solo use, the API inconsistencies. But that day gave me the push I needed to build my own tool.
The first version of what I had in mind was simple: a lightweight API, surrounded by an MCP and a CLI, with a barely-there UI just to review posts. Nothing interactive. Something that could run on a Raspberry Pi.
As I mentioned before, I started the project from my own Openclaw while the kids played at the park.
That night, once I was home and things were quiet, I tried it out and it worked. For now, only on X, which was the MVP I had in mind.
And then I got carried away.
The Moment the Project Gets Complicated All by Itself
I asked Pencil to design something for me. I liked the result so much that I ended up developing a full, 100% responsive interface. What was supposed to be headless became an app with its own UI.
The problem: the API the interface needed started evolving much faster than the CLI and the MCP. I kept adding endpoints for the web without worrying if the other two channels had parity. The debt piled up quietly.
And the code… the main file hit 7,000 lines. One of the test files went over 3,000.
Everything worked.
And it was a mess.
And NOTHING happened.
Phase 2: When You Start Feeling Like an Engineer
This is where a lot of people stop. The project works, but the code is a mess—the kind that gets vibe coding a bad name.
But this is where the engineering phase kicks in.
First up was the architecture. With everything already working, I could analyze what fit best. We ruled out a classic layered architecture because it would have made the app’s domain unnecessarily complicated. We went with hexagonal.
Before starting, I asked it to review the current tests and add any needed to make sure the refactor wouldn’t break the app. The current models are extremely good at refactoring, especially with a project of this size. But I didn’t want to risk it.
Once the app was restructured with the new architecture, we started splitting up the big files. A soft limit of 500 lines was reasonable.
Then came the parity problem. The API had features the CLI and MCP didn’t. We set up a validation in the tests: if a feature exists in the API, the tests fail if it’s not also in the CLI and MCP. That way, the agent knows exactly when it’s done.
I also did a few iterations to improve the web’s accessibility, using both the LLM’s own knowledge and accessibility validation tools.
And finally, the tests themselves. This deserves its own paragraph.
The Problem with AI-Written Tests
AI tends to write white-box tests: it checks that a certain function was called with certain parameters. These tests break as soon as you refactor, even if the behavior hasn’t changed.
In general, they’re useless except for giving you a false sense of coverage.
What I needed were black-box tests: you give it an input, check the output. You don’t care how it’s implemented inside. Those tests survive refactors.
I talked to the agent and told it I preferred this kind of test, and to check if there were tests that just validated one function calling another. There are a lot of those out there, and I suspect its training data makes it lean toward writing them. But they didn’t help me at all.
And yes, I did most of this work from Telegram. In spare moments I used for this instead of scrolling through social media.
What I Want You to Take Away
The chaotic code from phase 1 wasn’t a mistake. It was necessary to get this far in so little time. Without that messy sprint, I might have gotten lost in the initial details and still not have a working app today.
The mistake would be to stay there. Or to think the agent will handle phase 2 on its own, without you making the engineering decisions.
AI is incredibly fast at building. But the technical direction is still yours.
What is Loop Engineering? A practical example with Codex
I Don't Think the Token Subsidy Is Going Away. And That Says a Lot About the Future of AI