Vibe Coding vs Writing the Diff Yourself: Where the Speed Stops Paying for Itself
Devon Walsh
September 30, 2026
I built the first version of Brushline in a weekend without reading most of the code. Brushline is a small booking and reminder tool for independent dog groomers: a calendar, SMS reminders, a deposit on booking so no-shows cost the customer instead of the groomer. My sister-in-law runs a one-chair grooming shop and was losing roughly two appointments a week to people who simply did not turn up. I described what she needed to a coding agent, accepted almost everything it produced, clicked around the result, and described the next thing. By Sunday night she had a working link to send customers.
That is vibe coding in the honest sense of the phrase. You describe outcomes, the agent writes the code, you judge the result by using it rather than by reading it. It was the most productive weekend of building I have had since I left product management. It was also the start of a debt I paid off, with interest, about five weeks later.
This is not a piece about whether vibe coding is good or bad. I still do it. It is about where the speed stops paying for itself, because for me that line turned out to be surprisingly specific, and I only found it by crossing it.
What the weekend actually bought
Let me give the approach its due first. In about fourteen hours of work I had:
- A Next.js front end with a booking calendar that respected the shop’s opening hours.
- A Postgres schema for groomers, customers, pets and appointments.
- Stripe Checkout for a fifteen-dollar deposit at booking.
- Twilio reminders at 24 hours and 2 hours before an appointment.
- A basic admin page where the groomer could see the day and mark no-shows.
If I had written each diff myself, carefully, the way I would have wanted to as a PM reviewing an engineer’s work, that is two or three weeks of evenings. More importantly, I would have spent those evenings on things that did not matter yet. I did not know whether groomers would pay for this. I did not know whether customers would tolerate a deposit. Vibe coding let me find out in days instead of a month.
For the first three weeks the tool mostly worked. No-shows at my sister-in-law’s shop dropped from about two a week to one every couple of weeks. Two other groomers she knew asked to try it. I kept going the same way: describe the feature, let the agent build it, click through it, ship it.

The week it stopped working
In week five, a customer at one of the new shops was charged the deposit twice. Then another. Then a third customer paid a deposit, cancelled within the free cancellation window, and was never refunded.
I did what I had been doing all along. I described the problem to the agent and asked it to fix it. It found a plausible cause, the webhook handler was not checking whether it had already processed an event, and added a check. I clicked through a test booking. It looked fine. Two days later, another double charge.
Over the next evening I asked the agent to fix the payment flow four more times. Each time it produced a confident explanation and a change. Each change touched a different file. By the fourth round I realised something that should have been obvious: I did not know how a booking became a payment in my own product. I could not have drawn the flow on a napkin. There was a webhook route, a server action that also created payments, a retry wrapper somebody (the agent, weeks earlier) had added around the Stripe calls, and a cron job that “cleaned up” pending bookings. Each piece made sense on its own. Together they formed a system nobody had designed, including me.
The double charges came from the retry wrapper. When the Stripe API was slow, the server action timed out on my side and retried creating a Checkout session, while the first attempt had in fact succeeded. The missing refunds came from the cleanup cron, which deleted cancelled bookings before the refund job ran, so the refund job never saw them. Neither bug lived in the file the agent kept editing.
Where the line actually is
When I look back at what changed between weeks one to four and week five, it is not that the code got worse. It is that three things became true at once.
The code was touching money. Before week five, a bug meant a reminder text went out at the wrong time or a calendar slot displayed oddly. Annoying, visible, cheap. Once payments were involved, bugs cost real people real money, and they were invisible until someone complained. The feedback loop that makes vibe coding work, use it and see if it looks right, cannot catch a double charge that only happens when an external API is slow.
The system had more than one path to the same state. Booking creation happened in two places. Payment state could change from a webhook, a server action, or a cron job. Vibe coding is excellent at adding paths and terrible at noticing that you now have several. Every “fix” I requested added a guard in one path without anyone asking whether the paths should exist.
I had to fix it myself, under time pressure, with people waiting. Three groomers were relying on the tool. When the agent’s fixes stopped working, I was the only engineer, and I was reading code I had never read, written in patterns I had never chosen, at eleven at night with a customer emailing about a refund.
Any one of these on its own is survivable. All three together is where the weekend’s speed turned into a week of unpaid debt.
What writing the diff yourself actually means
“Write the diff yourself” does not have to mean typing every character with no AI involved. For me it now means three things on any code that crosses the line above.
I decide the design before any code exists. For the payments rewrite, I wrote a half-page note: one place creates Checkout sessions, keyed by booking ID so Stripe deduplicates retries; one webhook handler owns all payment state transitions; nothing else changes payment state; refunds are issued before any booking record is deleted. That note is the part I refuse to delegate.
I ask for small changes I can read in one sitting. Instead of “fix payments,” the requests became “move Checkout session creation into lib/payments/createDepositSession.ts and pass the booking ID as the idempotency key.” Then I read the diff, all of it, before accepting. Each change was small enough that I could explain every line to my sister-in-law if she asked, which she did not, but that is the test.
I own the tests for the paths that matter. The agent wrote many of the tests, but I specified the cases: a slow Stripe response followed by a retry; a cancellation inside the free window; a webhook delivered twice; a webhook delivered before the redirect. I read each test and checked it would have failed on the old code.
This took about four evenings. Slower than the original weekend, much faster than writing the whole thing from scratch, and at the end I understood how a booking becomes a payment. I also added a nightly job that compares Stripe’s record of deposits with our bookings and flags any mismatch, because I no longer trusted that webhooks alone would keep them in sync.

My working rules now
I have not stopped vibe coding. I have put fences around it. These are the rules I actually follow on Brushline and the two side projects since.
Vibe code anything you would happily delete. Landing pages, internal admin screens, one-off data scripts, prototypes to test whether an idea is worth building. If the worst case is “I throw it away and ask again,” let the agent run.
Stop vibing at the first bug you cannot explain. The four failed payment fixes were the clearest signal I have ever had and I ignored three of them. My rule now: if I ask an agent to fix a bug and the fix does not hold, I do not ask again. I go and read the code until I can say why it happened. The second attempt at a fix without understanding is where hours disappear.
Anything touching money, auth, or deleting data gets a written design and readable diffs. These are the areas where bugs are invisible to casual use and expensive when they surface. Vibe coding’s feedback loop is “does it look right when I click it,” and none of these fail in a way you can see by clicking.
If two features change the same state, stop and look. The moment a second code path starts modifying payment status, booking status, or a user’s account, it is time to read and restructure, not to add another feature on top.
Re-read the whole module once a week while it is still small. Twenty minutes on a Sunday reading what the agent has built that week is cheap. Five weeks of unread code is not.
The real cost was not the bug
The refunds were small. I issued them by hand the next morning, with an apology and a free groom voucher for each customer that my sister-in-law generously covered. The actual cost was different: for about a week, I could not answer the simplest question about my own product, “what happens when someone books?”, and the tool I had relied on to build it could not answer it either, because it had no more design intent than I did. It had only ever done what I asked, one request at a time.
That is the heart of it. Vibe coding is fast because it skips the part where somebody decides how the pieces fit. On a prototype, skipping that is the point. On code that holds money and has users depending on it, somebody has to do that deciding eventually, and the longer you wait, the more pieces there are to fit.
Brushline now has seven groomers on it and has not double-charged anyone in four months. The calendar UI is still largely vibe-coded, and I am fine with that. The payment module I could draw on a napkin. That split, knowing which parts I have to understand and which parts I am allowed not to, is the whole skill. The speed was real. It just came with an invoice, and the invoice arrives the first time you need to fix something you never read.