01The wizard walk
The portal's price-list report never returns for this server, which I verified at a 601-second read timeout across every date format and both verbs. Prices come instead from a stateful tour wizard, which I bisected against the live portal into a walk of newTour, getTourList, getTourRouteList, wizardstep, showHotelForm and minprice. getTourList is the strange one: nothing in the portal's own wizard.js suggests the server depends on it, because in a browser it only repopulates a dropdown, but without it wizardstep returns 500.
02A 48 KB stall
Requests from the production server stalled at roughly 48 KB of response body and never resumed, with identical byte counts at 60, 120 and 300 second timeouts and nothing further at 600. Changing the server's IP was off the table, so I fixed it in code: jQuery AJAX headers now ride on every wizard call so the portal returns an 88-byte script instead of the half-megabyte SPA page, and a hard 250 KB cap turns an unbounded hang into a bounded timeout. The portal also tarpits repeated logins, so my adapter logs in once, reuses the cookie, and holds a per-username lock that stops a cold worker firing simultaneous logins.
03Guardrails in code
Every rule in the draft validator started life as a prompt instruction the model followed most of the time, which is the wrong standard for a price sent to a partner. The checks that can be decided from the data are now decided after generation and before storage, each one comparing the reply against a figure or a name that came from a source block. One detail earns the whole file: my money regex originally matched three-digit integer parts, so it read 1414 as 141 and 2124 as 212, silently disarming every check that compared a quoted figure to its source.
04Two scripts, one city
The same email came back from the extractor once as Kuala Lumpur and once as Куала Лумпур, and an unmapped destination means no hotel is priced at all. Rather than trust a free model to translate, I put both scripts in the region table and sorted it longest-first so the full city name wins before the short token. A related bug was subtler: the knowledge base files Langkawi hotels under their state, so two resorts failed in three milliseconds with could not map destination Kedah while the portal had them on file the whole time.
05Free models, routed
A day that burned through more API budget than I expected turned into a routing policy. I now reserve the expensive model for writing partner-facing replies, while sheet extraction, classification, entity extraction, hotel inference and style profiling run through OpenRouter's free models. My router refreshes the live free-model list so new ones are picked up automatically and rotates on failure with per-cause cooldowns, and a double-pass extraction plus a needs-review gate mean a weak free model can only ever raise a verify flag, never produce a wrong answer.