I Asked for a Conversion Audit and Got an Unauthenticated Endpoint
// 2026-06-30 · Frederic Haddad · 9 min read
I asked a question about money and got an answer about security.
Back at the end of June I'd spent several days iterating fast on a customer-facing money flow inside one of my businesses — the part where a charge goes out, a customer pushes back on it, and the system responds with an offer. Every change had been reviewed on the way in. Nothing was broken. So I asked for a step-back audit: forget whether it works, tell me whether this whole flow is actually optimized for conversion and revenue, from the customer's side and the admin side.
The audit came back with 45 findings. The worst of them had nothing to do with conversion.
What I actually asked for
The framing matters. I did not ask for a security review. I did not ask anyone to think about authentication. I asked a commercial question: is this flow leaving money on the table.
I ran three deep audits in parallel — two across the code from different angles, one against the live third-party system where the automated follow-up messages live — and had them merged into a single ranked report I could open and read like a document rather than a wall of chat. That merge step is not cosmetic. Three separate audits give you three overlapping lists and no way to tell which item is the same item wearing different words.
The severity breakdown:
- 8 critical. Security or revenue integrity.
- 12 high, 11 medium, and 14 minor hygiene items — copy, tracking, small inconsistencies.
Forty-five is a lot for a flow that had just been reviewed change by change. That number alone was the first useful finding. Everything had been inspected as it went in, and the pile still existed.
The two that actually mattered
Two of the eight criticals were the kind you don't sleep well after reading.
The first was an access-control gap. Several self-serve customer endpoints — the ones you reach from a link in a message, without logging in — did not check who was calling them. Hold someone else's record identifier and you could act on their account. Not your own. Theirs. The same family of endpoint would also accept whatever discount code it was handed, including ones that took the price below what the thing costs me to deliver. It was closed the same day, in the coordinated fix I'll get to below.
That is a textbook flaw with a boring name, and I had shipped it while thinking about conversion rates.
The second was a revenue-integrity bug, and it's the funnier of the two in a bleak way. The whole flow exists to prevent payment disputes: a customer pushes back, gets offered a lower price, accepts, everyone's happy. Except when the charge actually went out, the system billed the full amount anyway. The flow built to prevent disputes was manufacturing the exact dispute it existed to prevent.
Nobody's individual change did that. The offer was written correctly. The charge was written correctly. They just disagreed with each other, and no single code review had both of them on screen at the same time.
Defects live in the seams
This generalizes far past my code.
Think about a renovation done one room at a time. Each room is inspected and signed off. Every room is fine. Then someone walks the whole floor and finds the hallway has no smoke detector, two doors open into each other, and the wiring from the third room feeds a socket in the first. Nobody built anything badly. The defects accumulated in the seams between well-built pieces, and a room-by-room inspection is structurally incapable of finding them.
Fast iteration produces exactly this. Every commit is small, sensible, reviewed. The bug is the relationship between commit 4 and commit 19. It's the same reason 8,600 green tests can still hide 31 real bugs: a passing suite proves each piece agrees with its author's beliefs, and says nothing about the piece next to it.
Which is why the framing of my request turned out to be an accident worth repeating. A review scoped to the question you're asking will only look where you pointed. I asked broadly — customer view, admin view, whole flow, tell me what's wrong — and the breadth is what surfaced an authentication hole that a conversion-focused test plan would never have gone near. If I'd asked for a security review, I'd have gotten security findings and no mismatch between the offer and the charge. Asking a wide, slightly vague question is underrated.
There's a mechanical reason the wide question beats a checklist. A checklist contains only the failures someone already thought of, so the seam between two correct pieces is the one entry it never has. You pay in noise — 14 of the 45 items were hygiene — and it's a good trade as long as somebody reads all 45.
I did not accept the AI's ranking
The tempting version of this story is "the AI found 45 things and I fixed 45 things." That is not what happened.
I went through the list line by line, as the person who owns the business consequences. Some findings I approved on sight — everything in the critical tier went into one coordinated fix, immediately, no debate. Others I explicitly declined, and wrote down why:
- One recommendation was to display a particular price much more prominently earlier in the flow. Reasonable on paper. I declined it, because the automated follow-up messages already tell customers exactly what they're being charged, and I judged that sufficient to keep dispute risk down. Prominence there costs conversion.
- One of the offers in the flow got flagged as suboptimal. I left it alone for now. It's a real finding; it isn't a June problem.
A severity ranking produced by a model is an input, not a verdict. It has no idea what a change costs you in conversion, in support load, or in engineering time you don't have this month. The ranking tells you what's wrong; only you can say what's worth fixing. Deferring something on the record, with a reason, is a decision. Ignoring it is not.
Fixes go out in waves, not in a swarm
The obvious move once the list was agreed was to fan out a pile of parallel agents and fix everything at once. I didn't, and the reason is dull and practical.
Too many of the accepted fixes touched the same core payment-handling file. Parallel workers editing one file is how you get a merge mess and a fix that quietly undoes another fix. So I split the work into sequential waves by code area instead:
- Payments and security first — the critical tier, in one coordinated change.
- Event tracking second.
- Customer-facing UI last.
Within each wave, parallelism was fine, because the workers weren't standing on each other's feet. It's the same rule you'd use with human contractors: you don't put the plumber and the electrician in the same crawlspace at the same time. Split by area, not by ambition. The ordering is not arbitrary either — the thing that can lose money or leak data goes first, while you still have the attention for it.
The unrelated hole in the backlog
There's a second half to this, from later the same day.
A backlog of bug reports from manual testing had piled up during the fast-iteration period, and nobody knew which of them were real, already fixed, working as designed, or feature requests in disguise. Untriaged bugs are worse than no bugs — they make prioritization impossible and they rot.
Instead of walking the list one ticket at a time, I checked out the exact code deployed to staging — not the latest branch, the code customers were actually touching — and ran four agents in parallel, each on a different functional area, each instructed to verify every open item against that real deployed code and write a verdict ready to be posted as a comment on the ticket. 12 tickets verified in one sitting, with the reasoning written into the tracker rather than delivered verbally and forgotten.
And again, the interesting finding was the one nobody was looking for. Customer login had zero rate-limiting or lockout rules — while the admin login next door was already throttled to five attempts a minute. The account behind that customer login holds real customer records and stored payment details. It got caught because the triage swept the whole project instead of just the tickets someone assumed were related, and it went onto the fix list as a confirmed bug the same morning it was found.
One door was hardened, the door right next to it wasn't, and nobody had ever looked at both at once. That asymmetry is the normal state of a codebase that grew fast — and it's why a dedicated adversarial pass at the end of any big build isn't a formality. Agents optimizing their own slice will not find the defect that belongs to no slice.
What this means if you run a business, not a codebase
You don't need to care about my endpoints. You should care about three things this pattern implies:
- Individually reviewed changes still accumulate defects. If your team ships incrementally and reviews every change — good — you have not covered whole-flow risk. Nobody has read the flow end to end since it changed.
- Commission the audit outside the frame of the question. Ask "what's wrong with this whole thing, from every angle" at least as often as you ask "does this feature work." Scope is the variable that decides what gets found.
- Do this after speed, not instead of it. Fast iteration isn't the problem. Fast iteration with no periodic step-back review is.
I asked a commercial question and got a security answer, and the security answer was the valuable one. For companies here in Dubai and across the UAE, the sharp edge is that a lot of the fastest-moving teams are shipping customer support portals, finance-operations tooling, HR self-service and procurement approvals built exactly this way — quickly, competently, one reviewed change at a time — under a data-protection regime where an access-control gap stops being merely embarrassing and starts being reportable. The cheapest moment to find it is on your own schedule.
If you've been iterating fast on something that handles customers' money or personal data, a whole-flow audit — commissioned outside the frame of the question, so it looks where you wouldn't have pointed — is a good use of a consulting day. Book a consulting day or send me an inquiry first if you'd rather talk before booking.