A mobile-first progressive web app with an AI decision layer that turns fuel anxiety into clear next steps — detect location, compare trusted nearby options instantly, and get nudged the moment prices fall below your target.
For many NSW households, fuel is one of the most volatile weekly costs. Two stations a few minutes apart can differ by 20¢+ per litre, and drivers usually discover that gap only after they pull in.
Storyboard illustrating the end-to-end user journey — from price shock at the bowser to confident, informed fill-up decisions via BlazrAI.
The Australian Competition and Consumer Commission called on fuel retailers to account for significant and unexplained price differentials between stations — noting that consumers lack the tools to identify and respond to these variations in real time.
The ABS reported living costs rose for all household types in the twelve months to December 2025, with annual rises ranging from 2.3% to 4.2%. Transport costs — including fuel — remained a top contributor alongside housing and food.
ABS — Living Costs Increase Across All Household Types, Feb 2026 →
“Every week I fill up without knowing if I’m paying $10 more than I should. There’s no easy way to compare — you just drive past and hope.”
— Composite from user research · Western Sydney commuter
This comparison is scoped to real Australian usage patterns. FuelCheck NSW and Google Maps each solve part of the journey in AU, while BlazrAI combines official live pricing, supply context, and predictive guidance into a one-stop fuel decision flow for NSW drivers.
Zero-tap context on first open: BlazrAI can voice out the best nearby price immediately, so drivers get the core business value before interacting with the interface.
Positioning takeaway (AU): BlazrAI is a one-stop fuel decision layer combining live Data.NSW/FuelCheck prices, DCCEEW supply context, and news-informed prediction guidance, while Google Maps remains the broad navigation utility baseline.
Research surfaced three non-negotiables: real-time nearby truth, near-zero friction (no install, no account wall), and proactive nudges that arrive before the expensive mistake.
“I fill up twice a week. Even saving 10¢ a litre adds up to over $150 a year. I just don’t have time to compare.”
“Diesel prices swing wildly. I’ve driven an extra 10km to save money, only to find it had gone up. I need something smarter.”
A progressive web app that behaves like a native product without the app-store tax. It opens straight into a live map, anchors to your location, and uses AI-assisted ranking and intent parsing to surface the best value stations before decision fatigue kicks in.
On mobile, sidebars steal space and break flow. A bottom sheet keeps the map visible, respects thumb reach, and matches familiar iOS/Android behavior so people compare and act without context switching.
Fully deployed at kopunithavelu.com.au/fuelcheck. Grant location access and BlazrAI immediately centers on your area, loads live nearby stations, and ranks options with AI-assisted, decision-ready context.
Allow location for the full experience. If permission is blocked, use the location pill to search your suburb manually.
Each pattern here links intent to execution: the behavioral cue, the empathy behind the wording, and the implementation detail that keeps it trustworthy at runtime. Hover the dots to inspect the trade-offs behind each decision.
The latest build adds system-level refinements in live code: sunset-aware map style sync, verified NSW source disclosure, balanced FAB geometry, improved map/list ergonomics, and route fitting that respects bottom nav + floating controls.
BlazrAI is designed for a fast money decision under real-world pressure. It combines ethical behavioral cues, transparent trust signals, and AI-assisted decision support so users move quickly without feeling manipulated.
BlazrAI stays intentionally light on infrastructure while remaining serious about reliability. The browser handles interaction, PHP protects credentials and shapes data, Cloudflare solves network constraints, and MySQL gives the product memory. Gemini is integrated as a focused intent parser for natural-language search, with deterministic local fallback when AI is unavailable. This keeps AI as a value multiplier in the decision flow, not a brittle dependency. This is the real runtime architecture, not a concept diagram.
/api/parse-intent.php. If Gemini is unavailable, the endpoint and client both fall back to local heuristic parsing so search never blocks.Every serious build hits constraints that no tutorial prepares you for. These three roadblocks forced architecture-level decisions, not cosmetic fixes.
api.nsw.gov.au returned empty-body 404s despite valid DNS and connectivity. Root cause: NSW gateway silently blocked shared-hosting IP ranges.api.nsw.gov.au/Token, which consistently returned 404. Diagnostic tests across seven URL variants confirmed a domain migration.api.onegov.nsw.gov.au based on archived TfNSW forum evidence; token flow and live pricing recovered immediately.Each incident was isolated with a dedicated debug.php endpoint that tested one layer at a time: network, DNS, token URL variants, and database access, then returned structured JSON.
That approach replaced guesswork with proof. Once stable, the debug endpoint was removed from production.
BlazrAI is already live as a production-ready web product solving a real weekly cost problem for NSW drivers, with AI-assisted decision support, completed EV charging coverage, and an operating model now focused on WA, VIC, and QLD rollout.
kopunithavelu.com.au/fuelcheck. NSW drivers can open, allow location, and immediately act on nearby value without install or sign-up friction.BlazrAI is currently live across NSW using the official NSW FuelCheck API. The next rollout plan is focused on WA, VIC, and QLD, each with its own reporting scheme, regional pricing behavior, and fuel-type mix.
The EV charging layer is complete and live, powered by the Transport for NSW EV Charging Locations dataset published on Data.NSW. This keeps the same government-sourced, open-data approach as the fuel pricing layer — no third-party APIs, no billing gates, no vendor lock-in.
2c53cfbd-d9e6-4688-8f24-bb989e85980b
CHAdeMO, CCS_SAE, and Tesla_Fast as separate boolean-style fields — mapping directly to the connector type filter chips in the BlazrAI UI.Charger_rating (kW output) enables the same value-framing logic as petrol: rank by speed-to-range rather than raw proximity.Operator and Opening_hours surface network brand and access times — so drivers know whether a Tesla-only site or a 24h DC fast charger is within range before leaving home.data.nsw.gov.au supports filtered queries by postcode, LGA, and charger type without authentication, consistent with BlazrAI’s infrastructure philosophy.
This section documents the actual deployed source code — every file, endpoint, database table, configuration key, and JS module. It is generated directly from code review, not from memory. Use it as a reference for onboarding, auditing, or extending the codebase.
BlazrAI is a zero-framework progressive web app. There is no React, no Vue, no build step. The frontend is vanilla HTML + modular JS + CSS. The backend is PHP 8 on shared hosting. Data lives in MySQL. Background tasks run on server cron.
All PHP files deploy to shared hosting. The Cloudflare Worker is a separate lightweight edge script that proxies NSW API requests, solving the IP-block constraint. There is no Docker, no container registry, and no CI/CD pipeline — deployment is manual file publish.
try/catch with silent failure. Live price fetches never break because a background feature failed.fuel_token table caches OAuth tokens for up to 12 hours. The token layer retries multiple URL/method variants before throwing, and auto-refreshes on a 401 mid-request.app-core.js, app-utils.js, etc.) loaded via <script> tags in sequence.localStorage, requiring no account system.price_history. The 48h prediction is a local heuristic over this data.The diagrams below show the runtime architecture and data enrichment pipeline extracted directly from the source code. Both reflect actual deployed behaviour.
Complete file tree from the deployed source ZIP. Colour coding: yellow = core config, green = API endpoints, blue = cron, orange = JS modules, purple = CSS.
All API endpoints are PHP files. CORS headers (Access-Control-Allow-Origin: *) are applied by jsonResponse() in client.php. All requests and responses are JSON except the unsubscribe GET which returns HTML.
Returns same enriched result shape as nearby.php except distance_km is null (no GPS origin).
Returns intent object with intent, fuel_type, location_mode, suburb/postcode, confidence, and source metadata. Uses Gemini first, then local heuristic fallback to avoid runtime hard-fail.
Uses multi-transport delivery: tries mail() with sendmail params, falls back to mail() without params, then /usr/sbin/sendmail directly. Logs to feedback_submissions.log regardless.
Increments app_usage_counter on each request (unless ?increment=0). Returns { count, label, baseline, live_count }. Total = VISIT_COUNT_BASELINE + DB live count.
Import database.sql once to create all four tables. Tables are InnoDB, utf8mb4. All PHP files also call ensurePriceHistoryTable() and ensureUsageCounterTable() inline as a safety net.
| Column | Type | Notes |
|---|---|---|
| id | INT UNSIGNED AUTO_INCREMENT | Primary key |
| access_token | TEXT | Raw Bearer token from NSW OAuth |
| expires_at | DATETIME | Set to now() + expires_in - 120s buffer. Indexed. |
| created_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | Audit |
| Column | Type | Notes |
|---|---|---|
| id | INT UNSIGNED | Primary key |
VARCHAR(190) | Alert owner. Indexed with active. | |
| phone | VARCHAR(32) | Legacy field (SMS removed after privacy feedback) |
| fuel_type | VARCHAR(10) | E10 | U91 | U95 | U98 | DL | PDL | LPG |
| price_threshold | DECIMAL(6,1) | Alert fires when cheapest ≤ this. Range: 50–400. |
| suburb / postcode | VARCHAR | Location for alert evaluation. At least one required. |
| radius_km | INT | Allowed values: 2, 5, 10 |
| active | TINYINT(1) | Soft delete flag. 1 = active. |
| last_notified | DATETIME NULL | Used to enforce 6-hour cooldown |
| Column | Type | Notes |
|---|---|---|
| id | BIGINT UNSIGNED | Primary key |
| station_code | VARCHAR(32) | NSW station code |
| fuel_type | VARCHAR(10) | Normalised fuel type |
| station_name, brand, suburb, postcode | VARCHAR | Denormalised for query performance |
| latitude / longitude | DECIMAL(10,7) | Nullable |
| price | DECIMAL(6,1) | Cents per litre |
| reported_at | DATETIME NULL | NSW API's lastupdated field, parsed via multiple date formats |
| captured_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | When we stored this snapshot. Key index for 7-day query. |
| UNIQUE KEY | uq_station_snapshot(station_code, fuel_type, price, reported_at) — prevents duplicate snapshots | |
| Column | Type | Notes |
|---|---|---|
| id | TINYINT UNSIGNED PRIMARY KEY | Always 1 — single-row table |
| visit_count | BIGINT UNSIGNED DEFAULT 0 | Incremented via INSERT ... ON DUPLICATE KEY UPDATE |
| updated_at | TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | Auto-updated on each visit |
All constants are set via config.php. Override any key in config.local.php (gitignored). Environment variables take highest priority — useful for CI/hosting env vars. The resolution order is: ENV var > config.local.php > default in config.php.
| Key | Default | Description |
|---|---|---|
| FUEL_API_KEY | '' | NSW FuelCheck API client key (required for direct mode) |
| FUEL_API_SECRET | '' | NSW FuelCheck API client secret (required for direct mode) |
| FUEL_API_BASE | api.onegov.nsw.gov.au/FuelPriceCheck/v2 | Base URL for all fuel data endpoints |
| FUEL_TOKEN_URL | api.onegov.nsw.gov.au/oauth/… | OAuth token endpoint. Confirmed working URL (not the one in old docs) |
| CLOUDFLARE_WORKER | '' | If set, all NSW API calls route through this Worker URL. Leave empty for direct calls. |
| GEMINI_API_KEY | '' | Gemini API key for NLP intent parser (/api/parse-intent.php). Optional at runtime because fallback parser exists. |
| GEMINI_MODEL | gemini-2.0-flash | Preferred Gemini model for intent parsing. Endpoint can fallback to alternate models. |
| DB_HOST / DB_NAME / DB_USER / DB_PASS | localhost / fuelcheck / fueluser / '' | MySQL credentials for PDO connection |
| TWILIO_SID / TWILIO_TOKEN / TWILIO_FROM | '' | Legacy only. SMS alert flow was removed after user privacy feedback. |
| ALERT_FROM_EMAIL / ALERT_FROM_NAME | alerts@example.com | From address used in alert emails |
| APP_URL | http://localhost/fuelcheck | Used to build unsubscribe links in alert emails |
| SNAPSHOT_ENABLED | true | Enable/disable the history snapshot cron job |
| SNAPSHOT_LOCATIONS | ['Glenwood 2768', 'Parramatta 2150', 'Sydney 2000'] | Suburb + postcode pairs to snapshot on each cron run |
| SNAPSHOT_FUEL_TYPES | ['E10','U91','U95','U98','DL','PDL','LPG'] | Fuel types to snapshot per location |
| VISIT_COUNT_BASELINE | 856 | Added to live DB count for displayed total. Set to account for pre-DB visits. |
No framework, no bundler. Each JS file is a flat module of global functions, loaded in order by index.html. State is global variables + localStorage.
activeSheet, currentView). Handles NLP search flow via /api/parse-intent.php and fail-soft local parsing when AI intent is unavailable. Controls sunrise/sunset theme sync, map/list view switches, verified-source popover, fuel strip, promo banner, and visit counter. Entry point is initMap() called from app.js on DOMContentLoaded.stationCardMarkup() and renderStationList() renderers. All monetary calculations happen here.renderSummary()) and full filter pipeline (applyFilters()). Also owns brand filter chip rendering and watchlist filter toggle.gtag(). AdSense slot rendering — lazy-loads the adsbygoogle script only if a valid ca-pub-* client ID is configured via window.FUELCHECK_ADSENSE. PayPal HostedButtons SDK loader with promise deduplication. Manages donation panel rendering.iconSvg(name, size) returns inline SVG markup. hydrateIcons() replaces data-icon placeholder elements post-render, enabling icon use inside dynamically injected HTML.in-memory globals (reset on reload)
localStorage keys (persist across sessions)
The buildPrediction48h() function in client.php runs over the 7-day history_7d array for each station. It does not call any external AI service. Gemini is used only for user-query intent parsing, not price forecasting.
Both cron jobs are configured via the hosting scheduler. They run PHP scripts directly and log output to files for debugging.
stationPriceResults() which calls persistPriceHistory() via INSERT IGNORE into price_history.
API cost3 locations × 7 fuel types × 4 runs/day = 84 NSW API calls/day. Well within 2,500/month free tier.
Trend maturityHigh-confidence predictions (5+ data points) available after ~30 hours. Medium after ~18 hours.
active=1 alert rows from price_alerts.
Step 2Group alerts by fuel_type|location key to minimise NSW API calls.
Step 3For each group, fetch live prices via /fuel/prices/location. Find cheapest price.
Step 4If cheapest ≤ threshold AND last_notified was >6h ago → send email alert → update last_notified.
Cooldown6-hour per-alert cooldown prevents notification spam even when prices stay low.
These gaps were identified during source code review. They are documented here to support informed decisions about production hardening, not as blockers to the current production deployment.
alerts.php validates that radius must be one of [2, 5, 10]. Any 15 km submission will be rejected with a 400 error. Fix: remove 15 km from the UI chip options or add it to the backend allowlist.phone/Twilio references are legacy cleanup items that can be removed in a later schema tidy-up./api/nearby.php until the 2,500/month quota is exhausted. Mitigation: add Cloudflare rate limiting rules at the Worker level, or add a simple per-IP counter in PHP before NSW API calls.debug.php should be removed from productionapi/debug.php, which was used during development to test individual layers (DNS, token, DB). This file exposes diagnostic information and should be deleted or gated behind authentication before any public deployment.feedback.php and check-alerts.php use PHP's mail() function with sendmail fallback. On shared hosting, mail deliverability varies significantly. SPF/DKIM records are required for reliable inbox delivery. Consider switching to a transactional email provider (Postmark, Mailgun, SendGrid) for production-grade alert reliability.SNAPSHOT_LOCATIONS suburbs accumulate multi-day trend history. Stations in other areas will show single-point history until a user in that area triggers a live request (which also writes to price_history). This means 48h predictions in unconfigured suburbs start at "low confidence" until organic usage generates data.Download the full dataset as a flat CSV file. No authentication required. Filename versioned by date (ev_chargers_consolidated_sep25.csv). Best for batch import into the BlazrAI database on a scheduled cron.
Query the dataset in real time via the CKAN DataStore API. Supports filters, q (full-text), limit, and offset. No API key required. Useful for proximity lookups by postcode or operator.
| Field name | Type | BlazrAI usage |
|---|---|---|
| OBJECTID | text | Unique row identifier — use as stable station key |
| Station_name | text | Display name on map marker and charger card |
| Station_address | text | Street address for directions CTA |
| Opening_hours | text | Shown on charger detail card — helps users avoid closed sites |
| Operator | text | Network brand badge (NRMA, Tesla, Chargefox, etc.) — enables operator filter chip |
| Number_of_stations | text | Physical bay count — displayed as "X bays" on card |
| Number_of_plugs | text | Total plug count — shown alongside bays for capacity context |
| Charger_type | text | AC (destination) or DC (fast) — drives the charger-type filter and marker style on map |
| Charger_rating | text | kW output — enables sorting by speed and "minutes to 80%" calculation |
| CHAdeMO | text | Boolean-style field — maps to CHAdeMO connector filter chip |
| CCS_SAE | text | Boolean-style field — maps to CCS2 connector filter chip (most common in new vehicles) |
| Tesla_Fast | text | Boolean-style field — maps to Tesla Supercharger filter chip |
| Latitude | text | GPS latitude — direct input to Leaflet marker and haversineKm() distance calc |
| Longitude | text | GPS longitude — same as above |
| LGANAME | text | Local Government Area — used for regional grouping and alert scoping |
| PCODE | text | Postcode — used as CKAN filter key for proximity queries |
ev_chargers| Column | Type | Source field | Notes |
|---|---|---|---|
| id | INT UNSIGNED PK | OBJECTID | Stable unique identifier from TfNSW dataset |
| station_name | VARCHAR(255) | Station_name | Display label for map marker and card |
| address | VARCHAR(255) | Station_address | Street address for directions CTA |
| operator | VARCHAR(120) | Operator | Indexed — supports operator filter queries |
| opening_hours | VARCHAR(120) | Opening_hours | Freetext — shown on card |
| charger_type | ENUM('AC','DC') | Charger_type | Drives map marker colour and type filter |
| charger_rating_kw | DECIMAL(6,1) | Charger_rating | Parsed from text; enables speed sorting |
| num_stations | TINYINT | Number_of_stations | Physical bay count |
| num_plugs | TINYINT | Number_of_plugs | Total plug capacity |
| has_chademo | TINYINT(1) | CHAdeMO | Boolean connector flag |
| has_ccs | TINYINT(1) | CCS_SAE | Boolean connector flag — most common in new EVs |
| has_tesla | TINYINT(1) | Tesla_Fast | Boolean connector flag |
| latitude | DECIMAL(10,7) | Latitude | Indexed alongside longitude for haversine queries |
| longitude | DECIMAL(10,7) | Longitude | Same |
| lga_name | VARCHAR(120) | LGANAME | Regional grouping for alert scoping |
| postcode | VARCHAR(10) | PCODE | Indexed — matches CKAN filter key for live queries |
| imported_at | TIMESTAMP DEFAULT CURRENT_TIMESTAMP | — | Last batch import timestamp for freshness display |
snapshot-history.php) that refreshes the local ev_chargers table on a schedule aligned to the dataset update cadence.Charger_rating field is a text column. The import script will need to extract the numeric kW value (e.g. "50kW" → 50.0) before storing in charger_rating_kw DECIMAL for sorting and speed calculations.