Agentic Coding Mistakes: Lessons From a Real App build
August 20, 2026 0 comments
Agentic coding tools build the most conventional version of whatever you ask for. We asked for an invoicing system and got textbook accounting software, including a void feature that would have carried a GST liability on unpaid export invoices. Two diff-backed incidents from that build show why the fix is domain knowledge in the spec, not more hours spent reviewing generated code.
In this post
- The app an AI builds is the app in its training data
- Incident one: the agent built a void feature and made it law
- Incident two: numbering that was right everywhere except India
- The mistake that never happened
- The MCP connector: agentic defaults trust too much
- Review catches what you know. Specs prevent what you don’t.
- The bottom line, and the code
The app an AI builds is the app in its training data
The standard advice for AI-assisted development is simple: let the agent write the code, then review it. We follow a stricter version of that ourselves and wrote it up as controlled AI coding. But this build taught us where that advice runs out. Review catches the mistakes you already know to look for. It does nothing about the mistakes that look like correct code.
Some context, kept short. We run a software-export business in India, and export invoicing follows rules that generic tools don’t model. Exports of services are zero-rated under Section 16 of the IGST Act, payment must arrive in convertible foreign exchange, the bank issues a FIRC (Foreign Inward Remittance Certificate) as proof, each remittance carries an RBI purpose code, and invoice numbers run gapless per financial year, which in India starts on April 1. This is our working understanding, not tax advice; confirm specifics with your chartered accountant.
Because no off-the-shelf tool ties a FIRC to the invoice it settles, we built our own system, with Claude Code doing most of the typing. The agent was fast, confident, and produced clean PHP. It also produced two failures that never threw an error, never failed a test we had at the time, and would have passed any code review that judged the code as code.
Incident one: the agent built a void feature and made it law
Every accounting tutorial, every open-source invoice app, every SaaS product the model has ever seen handles a wrong invoice the same way: void it, keep the number, never delete. So the agent built exactly that. Not a suggestion, a complete feature: a Void button, a confirm dialog, a reinstate action, and a comment declaring the behavior as rule “§1.10” of its own spec. From invoice-view.php at commit 99d6730:
|
1 2 3 4 5 6 |
/* Tax-invoice document (§2). Born paid — status is `issued` or `void`, * voided, never deleted (§1.10). */ if ($action === 'void' && $inv['status'] === 'issued') { $DB->prepare("UPDATE invoices SET status='void' WHERE id=?")->execute([$id]); flash('Invoice voided. The number is retained (never reused).'); } |
As generic accounting software, this is correct. As software for an Indian exporter, it is a liability generator. In our reading of GST, an issued tax invoice creates a tax liability whether or not you later mark it void. A wrongly issued invoice has to be removed and its proforma reverted, so the liability never exists on paper. We caught it in the functional-gap review, and the fix went in as commit c9f4107, whose message states the reasoning better than any paragraph I could write here:
“invoice-view: replace Void with ‘Undo conversion’ — deletes a wrongly converted invoice, reverts its proforma to Paid, frees the FY number (an invoice that exists is a GST liability, so wrong ones are removed).”
This was a jurisdiction error made confidently. The agent generated the correct feature for the wrong country, then wrote it into its own spec as a rule.
That last part deserves a beat. The agent didn’t hedge. It codified the pattern as canonical, complete with a section number, and every later piece of generated code would have treated void as settled behavior. A wrong assumption an agent writes into its own spec compounds with every subsequent prompt.
Incident two: numbering that was right everywhere except India
The first version of next_invoice_number(), from the initial commit f6b7863, made two default choices in one function:
|
1 2 3 |
$st = $db->prepare('SELECT prefix, next_seq FROM companies WHERE id = ? FOR UPDATE'); // ... return sprintf('%s-%s-%03d', $row['prefix'], date('Y', strtotime($issueDate)), $seq); |
One global counter per company, and date('Y'): the calendar year. Reasonable everywhere the model’s training data comes from. Wrong in India, where invoice sequences reset on April 1 for the new financial year, and where our proforma and its resulting tax invoice must share a sequence number so the books line up. The catch, again, came from a review question, not from the code: “every business year, April 1st, we change the invoice numbers.” That single sentence forced a rebuild into per-company, per-document-type, per-FY gapless sequences.
Then the second layer, and this one is the more interesting failure. The rebuilt version used INSERT IGNORE plus SELECT ... FOR UPDATE plus UPDATE, a sequence that deadlocks under InnoDB when two transactions create invoices at the same moment. No human review caught this. It was caught by a concurrency test that existed for one reason: someone asked “what if a team member creates an invoice on their machine at the same time?” The question generated the test; the test caught what eyes could not. The fix was a single atomic statement.
That is the arc worth remembering: naive, then domain-corrected, then concurrency-corrected, each step with a diff. And the mechanism differed. The FY miss was caught by knowledge. The deadlock was caught by a test that a question produced. If your process only has “review the diff,” you get the first catch and miss the second.
The mistake that never happened
I expected a third incident. Agentic tools routinely model payment as a boolean, an is_paid flag on the invoice row, because that’s what tutorial invoice apps do. It never happened here. The very first schema, commit f39c0b2, already had a payments table with amount, currency, and FIRC linkage as a first-class record.
Why? Because the FIRC requirement was in the spec before the agent generated a line of code. When the spec says every foreign payment must carry its remittance certificate and purpose code, a boolean cannot satisfy it, so the wrong shape never gets generated. Nobody had to catch this mistake, because the constraint made it impossible to make.
| What the agent defaults to | What an Indian exporter needs | How it was handled |
|---|---|---|
| Void a wrong invoice, keep the number | Remove the invoice, revert the proforma, free the FY number | Built wrong, caught in review |
| One counter, calendar year | Gapless per-FY sequences resetting April 1, proforma and invoice matched | Built wrong, caught in review, then again by a test |
An is_paid boolean |
Payments as first-class records with FIRC and purpose code | Never built wrong: the constraint was in the spec |
Three rows, one pattern. The only mistake that cost nothing was the one the spec prevented.
The MCP connector: agentic defaults trust too much
The system has an MCP connector, so an accountant can open Claude Desktop and type “create and send an invoice to Acme Ltd for the March retainer” without ever logging into the app. The agentic default for this kind of connector is full write access, because that’s the frictionless demo. We went the other way. The connector is a thin layer over the app’s own token-authenticated API: writes are draft-first and require confirmation, every AI-initiated change is audit-logged, and the token is role-scoped so the AI cannot see or touch more than that accountant could. It’s stateless, single-request JSON, which lines up with where the 2026-07-28 MCP specification took the protocol. The AI holds no business logic. The invoice system stays the single source of truth, which matters for the same reason everything above matters: an agent that improvises accounting logic is an agent that improvises liabilities.
Review catches what you know. Specs prevent what you don’t.
Agentic tools never say “that’s unwise.” A human developer with export clients would have asked about the financial year before writing a numbering function. The agent shipped date('Y') without a flicker of doubt, and it would ship the void feature again tomorrow. We’ve written before about keeping AI-generated code clean and about why vibe coding needs guardrails before production, but this build sharpened the point: cleanliness was never the problem. Every one of these mistakes was clean.
So put the domain in writing before the agent starts. For anything with statutory weight, our pre-build spec now answers:
- Which jurisdiction’s rules govern this system, and which documents are statutory records?
- What is the full lifecycle of each document, including what may be voided, deleted, reverted, or never touched?
- What are the numbering rules: sequence scope, gaplessness, and which fiscal calendar they follow?
- What proof documents (FIRC, purpose codes, certificates) must exist as first-class records, not fields?
- What happens when two people do the same thing at the same time, and which test proves it?
- If an AI connector exists, what is its write model: scope, confirmation, and audit trail?
Every “what if” question in that list is a test waiting to be written. Ask it early and the agent generates the right shape from the first commit, the way the payments table proved.
The bottom line, and the code
The agent was worth it. It typed the app in a fraction of the time it would have taken us by hand, and both incidents were cheaper to fix than a single GST notice would have been to receive. But the value of the build lived in the two moments a human said “not in India” and the one moment a spec made the question unnecessary. Speed came from the agent. Correctness came from the domain.
We’re releasing the system under MIT for other Indian software exporters and freelancers with the same compliance shape: a single-business kit that self-installs on ordinary PHP and MySQL shared hosting, with your data in your own database. It is deliberately narrow. Built around Indian GST and export-of-services rules, it is not a fit for domestic billing at other tax rates or for non-Indian firms without changes. The repository is on GitHub; if you find a rule we got wrong, an issue telling us so is the most useful contribution you can make.
Shipping AI-built code to production?
We pair agentic speed with 24 years of review discipline, on React, PHP, and WordPress builds where a wrong default has real-world costs.
Related Posts
-
April 9, 2025
What is Generative Engine Optimization (GEO) and Why It Matters?
You know, it feels like just yesterday we were all scrambling to figure out the best keywords and how to build those all-important backlinks. But, as they say, the times they are a-changin'. The world of search is evolving at an astonishing pace, driven by the rapid advancements in artificial intelligence. And
AI, Gen AI, GEO0 comments -
February 25, 2025
Laravel Cloud & Beyond: A User’s Perspective on the Game-Changing Releases
Well, folks, Feb 21st, 2025 was quite a day for the Laravel developer community. As users of this incredible framework here at Macronimous, we're still buzzing from the announcements that dropped from the Laravel team. It felt like a birthday, Christmas, and the release of a crucial security patch all rolled into


