Car rental · Saudi Arabia

One price engine.
Website, chatbot, API.

A pricing service that blends a machine-learning baseline with business rules, clamps the result between a cost floor and the scraped market band, and serves the identical number to every channel — with the full breakdown logged for retraining.

FastAPI · Firestore · ONNX Runtime · Playwright · AWS App Runner

Ten signals per quote

  • Weather

    Open-Meteo, live

  • Competitors

    4 providers, daily

  • Utilisation

    fleet, per branch

  • Demand index

    SARIMA seasonal forecast

Season, weekend flags, duration bucket, city, vehicle class and base rate make up the rest. Every quote's inputs and outputs are written to an audit collection.

  • 36

    REST endpoints

  • 10

    features per price

  • 11

    chatbot states

  • 4

    competitors scraped daily

  • 9

    cities with weather coverage

  • 60/40

    rules to model blend

The problem

Static rates leave money on the table — and channels disagree.

Rental pricing is usually set by hand and updated rarely, so rates drift out of line with demand and the market. Worse, the website, the call centre and the chatbot often quote different numbers for the same car on the same dates, which is the kind of thing customers screenshot.

This computes one price from live signals — weather, season, fleet utilisation, demand and scraped competitor rates — and serves it from a single endpoint to every channel. Guardrails keep it above cost and inside the market band, and every decision is logged with its full breakdown.

What it does

Six things the engine guarantees.

  • One price, every channel

    The website, the chatbot and external API clients all call the same endpoint, so a quote in chat equals a quote on the site.

  • Model and rules, blended

    A gradient-boosting baseline, informed by a SARIMA forecast of seasonal demand, combines 60/40 with a business-rules price for stability.

  • Competitor-aware

    A daily headless-browser scrape of four Saudi rivals sets the market band, with hot routes refreshed every six hours.

  • Profit-first guardrails

    Never below cost plus 15%, never above 10% over the market median. When the two conflict, profit wins.

  • Booking assistant

    A guided eleven-step flow books a car end to end and holds the vehicle transactionally for fifteen minutes.

  • Hot-reloadable models

    A new model version is picked up from the registry within a minute and swapped in without a redeploy or a restart.

Under the hood

How a number gets built, and what stops it going wrong.

Build a quote

A mid-size sedan in Riyadh. Change the duration and the pickup point and watch the number move through each stage of the pipeline.

Duration
Pickup

Base SAR 240/day · cost SAR 150/day · market median SAR 210/day. Illustrative inputs; the arithmetic is the engine's.

  1. ML baseline

    ONNX gradient-boosting over 10 features, incl. a SARIMA demand forecast

    SAR 238
  2. Rule price

    base × 0.95 duration × 1.10 airport premium

    SAR 251
  3. Blend · 60% rules / 40% model

    rules weighted higher for stability

    SAR 246
  4. Guardrails

    SAR 173SAR 231

    Floor is the higher of cost + 15% and 70% of the market median; ceiling is 110% of it. Blended price exceeded the ceiling and was pulled down.

Quoted rate

Snapped to a 5-SAR step inside the band

SAR 230/day

Three decisions worth defending

Market reference

The median, not the average.

Competitor scrapes are noisy — one mispriced luxury listing drags an average hard, and pricing would chase it. The engine takes the median of the four providers precisely because it resists outliers, and pairs it with a hard cost floor. Together that means a single bad scrape can neither push a price below cost nor make the fleet follow a junk rate off a cliff.

PRICING_SYSTEM_README.md · Phase 7

Conversation design

The chatbot is a state machine, not an agent.

Booking runs through eleven explicit states with forward-only transitions; intent is matched by keyword with no model involved. The LLM is called for exactly one job — mapping 'I want something small' to a real category — with temperature 0.1, a 16-token output cap, and a reply that is re-validated against the allowed list in code. It cannot inject text into the flow, so the prompt-injection surface is close to nothing.

chatbot/orchestrator.py:527

Consistency, and its cost

The chatbot calls the same endpoint the website does.

Rather than reimplement pricing for chat, the assistant makes an HTTP call to the app's own /pricing/unified-price — so the two channels cannot drift apart, ever. The honest trade-off is that this is a localhost round-trip: an extra serialisation hop and a second FastAPI request per quote, which is the top latency bottleneck in the system. The right fix is calling the pricing function in-process while keeping it the single source of truth.

chatbot/orchestrator.py:719

Stack

Backend
Python 3.11FastAPI 0.110Pydantic 2.6UvicornAPSchedulerslowapi
ML
ONNX Runtime 1.25scikit-learn GradientBoostingSARIMA demand forecastingLightGBM 4.3skl2onnx
Data
Firebase FirestoreFirebase AuthFirebase Storage (model registry)
Signals
Playwright + BeautifulSoupOpen-Meteo9 cities4 competitors
Frontend
React 18Vite 5TypeScript 5TanStack Query 5Tailwind 3
Infra
Dockernginx + supervisorAWS App RunnerECRSecrets Manager

Honest limitations: no trained model file ships with the repo, so a fresh deployment uses the fallback formula until training runs. There are no published accuracy or latency benchmarks, and it runs as a single-worker container — real scale needs a shared cache and a shared rate limiter.