The £35,000 lesson: when AI generated code meets production

TL;DR

Difficulty: Intermediate

Two vendors let AI write their code and shipped it unreviewed. In one month it cost my clients more than £35,000.

  • Project one: no tests, response shapes changing by the day, an order history endpoint with no authentication, endpoints moving without notice.
  • Project two: seven discount calculations in three weeks, validation that changed its mind, ambiguous dates.
  • The cost: £18,500 of my time, £8,200 in outages, £4,800 reconciliation, £2,100 compensation, £1,400 security audit so far.
  • Your defence: check every response against an agreed contract at your border, quarantine what fails, and write versioning and deprecation notice into the contract.

Over one month, two third party vendors who let AI write their code unreviewed cost my clients more than £35,000. This is the post mortem: what the integrations looked like, what each part cost, the code smells I now look for, and how to stop a vendor's chaos at the edge of your own system.

One month, two vendors, £35,000

Over the past month I have watched two separate projects slide into chaos because third party vendors decided that AI generated code was good enough for production. The result so far: more than £35,000 in direct costs to my clients, a great many hours spent debugging someone else's invented logic, and the reason I built a small tool to flag code that needs a closer look.

This is not a theoretical piece about whether AI can write code. It is a post mortem of what happens when an organisation treats AI generated code as a shortcut instead of a starting point. The projects are real, and I have changed or left out anything that would identify them.

Two projects, the same problem

I am working on two large eCommerce integrations: one for a jewellery retailer in the US, one for a fashion platform in the UK. Both depend on third party vendors for critical APIs and services. And both vendors, it turns out, read "AI assisted development" as "let the AI write everything and hope for the best".

The symptoms were different. The cause was the same.

Project one: a US jewellery platform

This one is a scaling project. As I grow the integration, the vendor has to grow with it. They describe their way of working as a rapid development framework, which as far as I can tell means an AI model writes the code and it ships without anyone looking. (That is not what the term means. I wrote about what rapid development frameworks actually are the week before.)

Nothing is tested

Not "the coverage is a bit low". There are no tests at all. The AI generated the implementation, someone glanced at it, and it went to production.

When I asked about their testing strategy, I was told that AI generated code is inherently more reliable because it is based on patterns from millions of codebases. That is not how any of this works.

The practical effect: every part of my integration has to allow for their API returning something unexpected. I have seen one product endpoint return the price as a string, an integer, a float and, once, a nested object with the price three levels down. All from the same endpoint, in the same week.

Responses change shape from one day to the next

This is the tell of code that is being regenerated instead of maintained.

  • Monday: /api/products/{id} returns { "name": "Diamond Ring", "price": 1299 }
  • Wednesday: the same endpoint returns { "product_name": "Diamond Ring", "price_cents": 129900 }
  • Friday: { "title": "Diamond Ring", "pricing": { "amount": 1299, "currency": "USD" } }

No versioning, no deprecation notice. The schema changes because someone asked the AI to "improve" the endpoint and it decided that meant restructuring the response. My integration now handles all three, because I cannot know which one I will get.

The API is not secure

The authentication has the hallmarks of generated security code nobody reviewed. API keys are checked on the client in some endpoints and on the server in others. One endpoint accepts a user_id and returns that user's full order history, payment details included, with no authentication check at all. The "admin" endpoints are protected by checking whether a request header says X-Admin: true. That is the entire security model.

The user_id endpoint is a textbook case of what the OWASP API Security Top 10 lists first: broken object level authorisation, where an API trusts an object ID sent by the client and never checks that the caller may see that object.1 The admin header is broken authentication in its plainest form.2

I have flagged these issues several times. The answer: "We will have the AI review the security implementation." The same AI that wrote it.

Endpoints move without warning

Last Tuesday half my integration broke because /api/v2/inventory became /api/inventory/v2. No redirect, no deprecation period, no notice. The explanation was that the routing had been "refactored for better organisation", on the AI's suggestion. I now check endpoint availability every five minutes, because I cannot trust that today's URLs will exist tomorrow.

Project two: a UK fashion platform

Here scaling is not the problem. The problem is that I cannot rely on any input or output, because the logic underneath keeps changing.

Logic that changes between deploys

The discount endpoint is the clearest example. Over three weeks I documented seven different calculation methods:

  1. Week one: percentage off the list price.
  2. Later that week: percentage off after VAT.
  3. Week two: a fixed amount off, rounded down.
  4. Later that week: a fixed amount off, rounded to the nearest penny.
  5. Week three: percentage off, capped at £50.
  6. Later: percentage off, with a minimum discount of £5.
  7. Later still: back to percentage off the list price, but with VAT calculated differently.

None came with documentation, a changelog or a notice. Someone asked the AI to "fix the discount calculation" and it rewrote the calculation from scratch according to what seemed reasonable that day.

Validation that changes its mind

Input that worked yesterday returns a 400 error today. Input that should fail is accepted and corrupts the data. I have seen:

  • email validation that accepted not-an-email on Monday and rejected valid@example.com on Tuesday, because a new pattern demanded a two letter top level domain,
  • price fields that accept negative numbers, so an order could pay the customer to buy,
  • date parsing that read 01/02/2025 as 2 January on some days and 1 February on others, depending on which generated parsing function was deployed.

The code smells I keep seeing

Both codebases show the patterns that made me build my detector. The snippets below illustrate them.

Comments that restate the obvious:

# This function calculates the total price by adding up all the item prices
def calculate_total_price(items):
    # Initialize the total to zero
    total = 0
    # Loop through each item in the items list
    for item in items:
        # Add the item's price to the total
        total += item.price
    # Return the calculated total
    return total

A perfect docstring on a trivial function:

def add(a, b):
    """
    Adds two numbers together.

    Args:
        a: The first number to add.
        b: The second number to add.

    Returns:
        The sum of a and b.
    """
    return a + b

Error handling that catches everything and handles nothing:

try:
    result = process_order(order)
except ValueError:
    pass  # Handle value error appropriately
except Exception:
    pass  # Handle any other errors appropriately

A TODO above code that is already there:

# TODO: Implement order processing
def process_order(order):
    validate_order(order)
    calculate_totals(order)
    process_payment(order)
    send_confirmation(order)
    return order

None of these is proof on its own. A careful person can write any of them. Together, and everywhere, they tell you that nobody read the code before it shipped.

What it has cost so far

Here is what this has cost my clients over the past month:

ItemCost
My time debugging, building workarounds and defensive code, and handling incidents caused by upstream changes£18,500
Three significant outages caused by unexpected API changes: emergency response, customer communication, manual order processing£8,200
Data reconciliation: order totals in my system did not match theirs, and reconciling a month of transactions took two people three full days£4,800
Customer compensation: wrong prices, deliveries that failed when address validation changed mid week, wrong products when SKU handling changed£2,100
Security audit by a third party after the authentication findings, still running£1,400 so far
Total£35,000, and rising

That is 18,500+8,200+4,800+2,100+1,400=£35,00018{,}500 + 8{,}200 + 4{,}800 + 2{,}100 + 1{,}400 = £35{,}000, and it leaves out the features I could not build while firefighting.

Why I built a detector

After the third week, I needed a quick way to judge whether a new code drop from these vendors was likely to be AI generated. Not because AI generated code is bad in itself, but because AI generated code that nobody has reviewed, tested or understood is a liability.

So I built ai-code-dedector, a heuristic tool, open on GitHub.3 It looks for the tells:

  • comment style: verbose, restating the obvious, hedging phrases such as "you may want to consider",
  • docstring saturation: every function documented, however trivial,
  • naming patterns: long, fully spelled out names everywhere,
  • error handling: overdefensive patterns and empty except blocks that promise to "handle appropriately",
  • structural uniformity: every function shaped the same way,
  • giveaways: a TODO sitting above a working implementation.

It is not perfect. It cannot catch AI code that a person has carefully edited, and a short snippet gives it little to work with. But it is useful for deciding which code needs extra scrutiny before I trust it anywhere near production.

The risks behind the invoice

The costs above are the visible part. The risks underneath worry me more.

Debt that multiplies. A vendor who regenerates code is not maintaining a codebase, they are generating a new one each time. Nobody knows why things are the way they are, and each "fix" introduces inconsistencies somewhere else.

Security as an afterthought. Ask a model to "add authentication" and you may get something that looks like authentication. Without review by someone who understands security, the flaws go straight to production, as they did here.

A knowledge gap. The people maintaining these systems cannot debug them, because they never understood them. They can only regenerate and hope.

Lock in. Once you are integrated with a vendor who works like this, leaving is expensive and staying is expensive.

I have written the general case at length elsewhere, with the studies behind it, so I will not repeat it here. AI writes the code. Who is responsible for it? covers what goes wrong and who answers for it, and will AI like ChatGPT ever replace developers looks at what the controlled studies say about AI and developer productivity.

Protecting your side of an integration

You cannot fix a vendor's process. You can make sure their chaos stops at your border.

Worked example: a contract check at the border

Lismore Home is an invented homeware shop in Waterford, at lismorehome.example, that pulls products from a supplier's API. Its integration does not trust the response. Every product passes through one small class that knows the agreed shape, and anything else is quarantined instead of reaching the shop:

class SupplierProduct
  Result = Struct.new(:ok, :name, :price_cents, :error, keyword_init: true)

  def self.parse(json)
    name = json["name"]
    price = json["price_cents"]

    return Result.new(ok: false, error: "missing name") unless name.is_a?(String) && !name.empty?
    return Result.new(ok: false, error: "price is not a whole number of cents") unless price.is_a?(Integer)
    return Result.new(ok: false, error: "price is not positive") unless price.positive?

    Result.new(ok: true, name: name, price_cents: price)
  end
end

result = SupplierProduct.parse(response_json)
if result.ok
  ProductSync.upsert(result)
else
  SupplierQuarantine.record(response_json, reason: result.error) # and alert a person
end

Run the three shapes from project one through it, with an agreed contract of name and price_cents:

  1. Monday's { "name": "Diamond Ring", "price": 1299 }: there is no price_cents, so it is quarantined with "price is not a whole number of cents".
  2. Wednesday's { "product_name": "Diamond Ring", "price_cents": 129900 }: the price key happens to match, but there is no name, so it is quarantined with "missing name".
  3. Friday's nested pricing object: no name, quarantined.

The shop keeps selling at yesterday's correct price, and a person gets an alert on the first bad response instead of a customer complaint on the hundredth. The same idea covers the date problem: agree a format that cannot be misread, such as ISO 8601's 2025-02-01, and reject anything else.

What to put in the contract

Ask how the vendor develops, reviews and tests. Put stability in writing: versioned endpoints, a notice period before anything is removed, and a machine readable signal when it happens. The IETF has published specifications for that signal: the Deprecation header from RFC 9745 says a resource is or will be deprecated,4 and the Sunset header from RFC 8594 says when it will stop responding.5 A vendor who sends neither has told you how they will treat you.

What needs to change

I am not against AI. I use AI tools in my own development every day. But there is a large difference between using AI as a tool and using it as a replacement for understanding.

For vendors: AI generated code is a first draft, not a finished product. It needs review, tests and, above all, understanding. If the people maintaining the code cannot explain why it works, it does not belong in production.

For businesses integrating with third parties: ask about development practices, write stability into the contract, build integrations that assume upstream instability, and monitor for changes.

For the industry: expectations, not restrictions. Code review is still necessary. Tests are still necessary. Documentation that explains intent, not just what the code does but why, is still necessary.

What I planned next for the detector

When I wrote this, I planned to extend the detector with a web interface, so nobody has to clone the repository and use a terminal, a GitHub integration to scan whole repositories, analysis of git history, because a sudden change in style or commit patterns is itself a tell, and per file reports showing which parts of a codebase look most suspicious.

The goal is not to remove AI from development. It is to find where AI generated code needs a person's attention before it causes the kind of month I have just had.

Meanwhile, I maintain defensive code against two APIs that may change their behaviour at any moment, and watch the costs climb. £35,000 in one month, for code that nobody at those vendors actually understands.

The projects described are real. Identifying details have been changed or left out. The costs are accurate as of the date of writing.

Footnotes

  1. OWASP Foundation, API1:2023 Broken Object Level Authorization, OWASP API Security Top 10 2023, https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/

  2. OWASP Foundation, API2:2023 Broken Authentication, OWASP API Security Top 10 2023, https://owasp.org/API-Security/editions/2023/en/0xa2-broken-authentication/

  3. Georg Keferboeck, ai-code-dedector, GitHub repository, created 13 April 2026, https://github.com/keferboeck/ai-code-dedector

  4. S. Dalal and E. Wilde, RFC 9745: The Deprecation HTTP Response Header Field, IETF, March 2025, https://www.rfc-editor.org/rfc/rfc9745.html

  5. E. Wilde, RFC 8594: The Sunset HTTP Header Field, IETF, May 2019, https://www.rfc-editor.org/rfc/rfc8594.html

If a vendor's API keeps breaking your shop, or you are not sure what is in the code you have been handed, bring it to the free workshop. We will find where your integration needs a border, and the first two weeks of work are on me.