Deterministic Code vs. Generative AI: Why We Took Math & Decisions Away from the LLM
When you start building software with modern Large Language Models, there is an intoxicating urge to make the AI do everything.
Need to scan a lottery ticket? Let Gemini read the image and validate the draw rules. Need to parse unstructured scrape tables? Let an LLM parse the HTML. Need to generate personalized lucky numbers? Prompt an LLM with the userβs birthday and astrological sign.
In the early prototypes of Lotto Check, this approach worked surprisingly well in isolated demos. But as soon as we deployed to thousands of daily active users, the architecture began to buckle in bizarre and costly ways.
In consumer software, especially when users have real money on the line, trust is binary. If an app tells a user their ticket is a loser when it won, or checks a ticket against the wrong draw date, user trust is destroyed forever.
Over several painful production postmortems, we arrived at an uncompromising architectural rule: LLMs are strictly engines of perception and language. They must never be trusted with math, validation, or business decisions.
Here are three real-world production failures that forced us to fire the AI from making decisions.
1. The Day Gemini Handed the Exact Same Numbers to 29 Users
One of our early engagement features was an AI Lucky Number Generator. The premise was playful: users could type in personal memories (βMy daughterβs graduation in Halifaxβ, βOur first family road tripβ), and Gemini would generate a customized set of Lotto Max numbers paired with a thoughtful astrological/horoscope-style reading.
In our prompt, we instructed Gemini:
βPick 7 unique numbers between 1 and 50 based on the semantic anchors in the userβs memory, ensuring mathematical randomness and avoiding common patterns.β
We launched the feature. The readings were poetic and delightful. But two weeks later, when we ran an analytical query on generated_number_sets, we discovered something horrifying:
Gemini had handed the exact same Lotto Max sequence [4, 11, 19, 28, 33, 42, 49] to 29 different users.
Why did this happen? LLMs are probabilistic token predictors, not random number generators. Despite temperature settings, when prompts share similar semantic framing or latent embeddings, the modelβs highest-probability numerical tokens converge toward familiar clusters. Even worse, for general prompts, the model defaulted to evenly spaced arithmetic progressions across the number grid.
Imagine the PR catastrophe if that set had hit the $70 Million jackpot, only for 29 users of our app to discover they were splitting a single prize tier because an AI couldnβt generate uniform random numbers.
Language models predict the most plausible next token; they are fundamentally incapable of true mathematical randomness.
The Fix: Separation of Concerns
We completely stripped Gemini of number selection. We wrote a deterministic, cryptographically secure number engine in TypeScript (numberEngine.ts):
// Deterministic crypto-random selection with collision ceilings
export function generateLotteryLine(game: LotteryGame, userAnchors: number[]): number[] {
const line = new Set<number>(userAnchors.filter(n => n >= 1 && n <= game.maxNumber));
while (line.size < game.pickCount) {
const randomByte = crypto.getRandomValues(new Uint8Array(1))[0];
const candidate = (randomByte % game.maxNumber) + 1;
line.add(candidate);
}
return Array.from(line).sort((a, b) => a - b);
}
Now, the architecture operates as a pipeline:
- Deterministic code extracts potential numeric anchors from user input (dates, ages).
- The TypeScript engine draws the remaining numbers using crypto-randomness and validates that the line doesnβt collide with recent historical draws.
- Only then is the finalized number array passed to Gemini with a single, focused instruction: βWrite a 2-sentence creative reading interpreting why these numbers connect to the userβs memory.β
The model writes prose. The code does the math.
2. βTicket Scanning Never Guessesβ: Stripping AI Date Repair
When users snap photos of lottery tickets, the images are rarely studio quality. They are taken under kitchen fluorescent lights, creased from pockets, or slightly blurry.
Our initial Gemini Vision extraction pipeline included extensive βauto-healingβ heuristic prompts:
- If the ticket date read
OCT 16, but the model knew Lotto Max draws are on Tuesdays and Fridays, the prompt allowed the model to βinfer the most plausible draw yearβ or βshift off-schedule dates to the nearest upcoming draw.β - If the model read a day of the week as
THU, but the numeric date was a Friday, the model attempted to score which token had higher optical confidence.
In October 2026, our production monitoring flagged an alarming anomaly: about 16 out of 71 failed scans were completely valid tickets sold in Ontario and Atlantic Canada that had been outright rejected or misclassified.
The cause? The physical tickets did not print a day of the week, only 16 OCT 26. The AI model, trying to be helpful, hallucinated that October 16 was a Thursday and rejected the ticket as βoff-schedule.β
We made an immediate, foundational pivot recorded in docs/decisions.md: A failed scan is an inconvenience; a wrong scan is a disaster.
In mission-critical user workflows, a clear scan failure message is a minor inconvenience, but a silent incorrect guess destroys user trust forever.
We completely eradicated every AI date repair, year-scoring, and schedule auto-heal heuristic:
- Never Infer: If a ticket image does not explicitly contain a legible printed date, the system immediately returns
draw_date = nulland prompts the user to retake the photo. - Zero Day-of-Week Guessing: Extraction prompts strictly forbid the model from calculating days of the week. If a weekday isnβt physically printed in ink, the model outputs
day_of_week: null. - Targeted Literal Re-reads: If a date fails mathematical validation against our Postgres schedule tables, we invoke a targeted, isolated crop re-read that only accepts the date if it achieves 100% agreement between the printed text, the physical schedule, and the initial extraction.
By taking inference away from the AI, our scan failure rate dropped, and false-positive mismatches fell to absolute zero. Catching these architectural blindspots before they reach production requires disciplined adversarial testing (covered in detail in our breakdown of operating live production apps with coding agents).
3. Scraper Inversion: From 3,000 LOC of AI Headless Crawling to 380 LOC of Clean REST
When aggregating lottery winning numbers and prize breakdowns across five Canadian regional crown corporations (OLG, BCLC, WCLC, Loto-QuΓ©bec, ALC), websites frequently update their layouts, add Cloudflare bot gates, or format numbers in nested dynamic HTML tables.
Initially, we deployed headless browser scrapers backed by AI extractors (using Firecrawl and LLM parsing). The idea was that AI could βunderstandβ any webpage layout change without breaking.
In practice, it was a maintenance nightmare:
- Scrapers burned thousands of API credits re-scraping unchanged tables.
- LLM parsers would occasionally confuse a guaranteed $1M Gold Ball prize with an auxiliary secondary draw.
- The scraper codebase sprawled across 7 Edge Functions and over 3,000 lines of brittle parsing code.
We audited the network payloads of every provincial lottery website and realized that virtually every regional portal fetches its verified results from unauthenticated internal JSON endpoints (like BCLCβs PlayNow REST API).
Before building complicated AI scrapers to parse dynamic web pages, inspect the network tab to see if clean backend REST payloads already exist.
We executed a complete Scraper Simplification:
- Deleted all headless AI scrapers and markdown extractors.
- Replaced them with direct, unauthenticated REST calls wrapped in strict TypeScript schemas.
- Enforced hard invariant checks: e.g., Lotto 6/49 prize breakdown payloads must contain exactly 7 tiers, or the scrape transaction is aborted and queued for retry.
The result? The prize breakdown scraper collapsed from 1,503 LOC down to 383 LOC, credit costs dropped to zero, latency dropped from 12 seconds to 150ms, and data ingestion reliability reached 100%.
The Golden Rule for AI Product Architecture
If you are building an application with coding agents and LLM APIs, memorize this division of responsibility:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β THE LLM β
β β’ Visual perception (OCR, image classification) β
β β’ Natural language synthesis (summaries, readings) β
β β’ Semantic search & unstructured text extraction β
βββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β Raw tokens & perception
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DETERMINISTIC CODE β
β β’ Math, probability, and random number generation β
β β’ Invariant validation (dates, tier counts, schemas) β
β β’ Database transactions, RLS, and security gates β
β β’ Network calls and idempotent state updates β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
When you let the LLM operate within its strength, transforming messy real-world inputs into structured data, and let deterministic code enforce the rules, your app becomes unshakeable.
LLMs excel at perception and creative prose, but fail catastrophically at math, date inference, and randomness. In this post, we explored three real-world production failures in Lotto Check that led us to strip the AI of decision-making, replacing it with deterministic TypeScript math engines, zero-inference OCR verification, and lightweight REST ingestion pipelines.


