Just Opening an Order Was Spending a Credit
Bakery Buddy used to have a card on the order detail page that called Claude twice the moment the page rendered. No button, no ask, nothing. It sat under the order details, keyed off the order's id in a `useEffect`, and ran a pricing check plus a short AI summary before anyone had read a single field. Looking at an order to check on it was quietly billing for the privilege.
What the card actually did
`BuddySuggestionsCard` fired `checkPricing()` and `generateSummary()` from that mount effect every time an order page opened. Scroll through a handful of orders in a shift, the ordinary way anyone checks on orders, and you'd made that many unasked-for AI calls without ever touching chat.
`ai_call_logs` had 21 `order_summary` rows that traced back to exactly this: page loads, not requests. On a plan with a real monthly allowance for AI use, that isn't a rounding error. It's a meaningful slice of a month's budget spent on a screen someone only meant to glance at.
Why it read as harmless in the code
The same card also called a second endpoint, `pricing-comparison`, that really was free: pure SQL over past orders, no model, no cost, no `ai_call_logs` row. It sat right beside the two paid calls in the same component, doing the same kind of "load this automatically so it's ready" work.
A free fetch next to a paid one makes the paid one look just as harmless. Reading the component top to bottom, all three calls fire from the same effect, on the same trigger, for the same reason: show something useful without making anyone ask for it. Nothing in the code marks one as free and the others as billed. You'd have to already know the difference to see it, and the whole point of the bug is that nobody did, until someone went looking at the logs instead of the component.
The fix, and the rule behind it
The card is gone. What replaced it reads fields the order page already has instead of asking Buddy to reconstruct them, no separate fetch of any kind. The rule that came out of it is short: no AI endpoint gets called from a mount, a tab switch, or a route load. Only from something a person did on purpose, a send, a click, a submit.
The acceptance test isn't a build passing. Compiling proves the call is well typed, not that it's cheap, and a mount-time call compiles and types exactly as cleanly as a button-triggered one. The real test is a query: note the newest row in `ai_call_logs`, open the page a handful of times, and confirm nothing new landed. That's the only check that would have caught the original bug, and it's the one that ships with every page that touches AI now.
The part that actually stuck
Rules like this usually get written down once and then get broken again somewhere else, in a file nobody connected to the first mistake. This one didn't, and it's worth being specific about why, because the reason isn't "everyone remembered."
Months later, a different part of the app needed its own AI call: a short bench tip for whoever's mid-shift with flour on their hands, on the kitchen task planner. The function that makes that call carries this in its own comment, in the file itself, not in a separate rules page:
"the ONLY paid thing on /tasks. It exists behind a button press, never a mount, a tab open or a drawer, because the page's sibling taught this feature the expensive way: `BuddySuggestionsCard` fired from a `useEffect` and simply OPENING an order spent a credit, 21 times over."
The same planner has its own free SQL fetch sitting right next to that paid call, the exact shape that caused the original confusion. Its route handler says so out loud too, almost word for word: "a free fetch sitting beside a paid one made the paid one look equally harmless." A third file on the same feature, the panel that renders both, repeats the same 21 again, this time attached to its own acceptance test: open it three times, confirm zero new rows. A fourth does the same for a related endpoint on the same screen.
Four separate files, across a feature shipped months after the original bug, each independently deciding to name the old incident by number before doing the new thing safely.
Why this is the rule that survived
A warning in a docs page competes with everything else in that docs page. Nobody re-reads it at the exact moment they're about to add a `useEffect` to a component that has nothing to do with the file where the warning lives. A warning sitting inside the function that would repeat the mistake, in the exact spot where the next person is deciding whether this call is safe to fire on mount, gets read at precisely the moment it matters, because it's the code they're already editing, not a separate page they'd have to remember exists and go check.
The rule didn't survive because it was correct. Plenty of correct rules in this codebase have been broken again anyway. It survived because it was placed where the temptation actually shows up, restated by name, with the actual number attached, every single time a new feature came close to the same shape.
What generalizes past AI calls
Any feature that costs money per use has this exact shape available to it. A mount effect, a tab switch, an automatic refresh, all of them look like good UX, and all of them quietly turn "someone looked at this" into "someone paid for this," with no line anywhere that says so.
If you're building something that bills per action, the automatic call and the deliberate one need to look different in the code, not just feel different in your head. And if you've already been burned by this once, fixing the one file that caused it isn't the whole job. The rest of the job is writing the warning into the next file that will be tempted to do the same thing, in that file, before anyone gets there.
Frequently Asked Questions
What was actually wrong with the order page?
A card on the order detail page called Claude twice from a useEffect the moment the page loaded, running a pricing check and an AI summary with no button press and no request from anyone. ai_call_logs showed 21 order_summary rows that traced back to ordinary page loads, not to anyone asking for a summary.
Why didn't anyone catch it just by reading the code?
The same component also called a genuinely free endpoint, pure SQL with no model call and no cost, right next to the two paid ones. All three fired from the same effect for the same visible reason, so the free call made the paid calls look equally harmless. Only checking the actual logs surfaced the difference.
What's the fix?
The card was removed and replaced with a version that reads fields the order page already has, with no separate fetch at all. The standing rule is that no AI endpoint may be called from a mount, a tab switch, or a route load, only from an explicit action like a send or a click, and the acceptance test is checking ai_call_logs for zero new rows after opening a page, not a passing build.
Why did this rule stick when similar written-down rules in the same codebase got broken again?
Because it didn't just live in a separate rules document. It got restated, by name and with the actual number of unwanted calls attached, inside the new files that later came close to repeating it, right at the point where the mistake would have been made again. A warning inside the file someone is actively editing gets read; a warning in a page they'd have to remember to check usually doesn't.
I build Bakery Buddy for my wife Lindsay's cake studio, Marin Cake Studio, and the rule this post is about exists because simply opening one of her orders used to cost something.
Ready to put this into practice?
Join the Waitlist