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:

  1. Deterministic code extracts potential numeric anchors from user input (dates, ages).
  2. The TypeScript engine draws the remaining numbers using crypto-randomness and validates that the line doesn’t collide with recent historical draws.
  3. 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:

  1. Never Infer: If a ticket image does not explicitly contain a legible printed date, the system immediately returns draw_date = null and prompts the user to retake the photo.
  2. 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.
  3. 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.

TL;DR

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.