I entered Vibeathon 2026 with one self-imposed rule: I would hand-code FuelSense's backend and calculation engines, even though the competition was built around AI-assisted coding. I also entered VibeUI 26, a separate design competition running at the same time, from 8:00 AM to 2:00 PM on the same floor.
FuelSense ended up winning Vibeathon, becoming my 17th hackathon victory. But the part worth documenting is the engineering: how I built a reproducible telemetry pipeline, separated numerical decisions from AI-generated language, and kept the application usable under a six-hour deadline.
This is a technical teardown of what I implemented, what was deliberately simulated, and what I would change before deploying the same architecture against real vehicle and pricing data. The source code, scenario data pack, API/state contract, and original competition case study are public.
1. Engineering problem and design constraints
FuelSense is a mobile-first driving and refueling decision system. Its job is to transform vehicle-like telemetry into understandable outputs:
- Driving efficiency, idle behavior, estimated fuel cost, and CO₂.
- A deterministic BUY / WAIT refueling recommendation based on fuel reserve and price direction.
- AI explanations and a chat interface grounded in the backend's calculated state.
A six-hour event changes the engineering priorities. I needed predictable inputs, comparable outputs, inspectable calculations, and a presentation that would not depend on unstable hardware or network conditions.
The backend runs real calculations over simulated CAN-style telemetry frames. It does not connect to a physical CAN bus or OBD-II adapter. Fuel-price contexts are deterministic scenario fixtures, not a live Malaysian fuel-price feed. The application demonstrates the decision pipeline; it is not a production automotive integration.
The stack was intentionally modest: FastAPI, SQLAlchemy + SQLite, a React/Vite frontend, and a DeepSeek-backed explanation layer with deterministic local fallbacks.
2. Architecture: deterministic computation, generative explanation
The most important architectural boundary was between computing a decision and explaining a decision. The model can describe the metrics and recommendation, but it should not be responsible for producing either.
| Layer | Implementation | Responsibility |
|---|---|---|
| Scenario inputs | simulation/scenario_definitions.py | Four driving profiles and fixed signal patterns |
| Telemetry | simulation/can_generator.py | Generate time-indexed vehicle-like frames |
| Persistence | models.py, database.py | Store session progress and telemetry samples |
| Analytics | services/analytics_engine.py | Integrate fuel/distance, produce scenario metrics |
| Decision | services/decision_engine.py | Apply fuel-reserve and price rules |
| Orchestration | simulation/scenario_engine.py | Rebuild state, switch scenarios, publish snapshots |
| Explanation | services/ai_service.py | Explain supplied numbers with AI or local templates |
| Interface | frontend/src/App.jsx | Poll snapshots, render results, stream explanations |
SimulationState is the data contract between computation and presentation. The React dashboard consumes the snapshot rather than recomputing the Eco Score, fuel cost, or BUY/WAIT result.
3. Simulating a CAN-style telemetry feed
A real CAN integration would introduce hardware availability, protocol decoding, noise, and vehicle-specific signal mappings. None of those were necessary to test the decision engine during the hackathon. Instead, I made telemetry a pure function of scenario and time step.
The generator in can_generator.py selects from a ten-sample pattern using step % 10, then calculates the fuel level from elapsed time, the scenario's hourly burn rate, and tank capacity.
1index = step % 102speed_kmh = profile["speed_kmh"][index]3rpm = profile["rpm"][index]4 5liters_used = step * burn_rate_lph / 3600.06fuel_pct = max(7 0.0,8 initial_fuel_pct - (liters_used / tank_capacity_l) * 100.0,9)The complete frame contains timestamp, speed_kmh, rpm, throttle_pct, engine_load_pct, fuel_level_pct, driving_mode, and fuel_burn_rate_lph. The profiles and sample arrays live in scenario_definitions.py.
Four testable driving behaviors
| ID | Profile | Initial fuel | Burn rate | Target Eco Score | Target efficiency |
|---|---|---|---|---|---|
| A | Urban Congestion | 72% | 9.2 L/h | 35 | 12.0 L/100km |
| B | Highway Efficiency | 68% | 5.1 L/h | 85 | 5.8 L/100km |
| C | Aggressive Driving | 60% | 14.8 L/h | 22 | 14.5 L/100km |
| D | Mixed Weekly Driving | 75% | 7.3 L/h | 65 | 8.2 L/100km |
Each profile lasts 180 seconds, with distinct throttle, RPM, speed, and engine-load patterns. These values are scenario targets and parameters, not measured vehicle benchmarks. That distinction matters when evaluating the analytics that follow.
4. The analytics engine: measured integrals versus target-driven indicators
The analytics implementation in analytics_engine.py has two different types of outputs. Mixing them up would make the system sound more physically predictive than the code actually is.
4.1 Integrating samples
For the one-second samples used by the simulator, distance and fuel consumed are integrated as discrete sums:
Distance (km) = Σ speed_kmh[i] / 3600Fuel used (L) = Σ fuel_burn_rate_lph[i] / 3600These expressions assume one sample represents one second. With a variable sampling interval, each term would also need its own delta_seconds multiplier.
As an example, two one-second frames at 60 km/h and 120 km/h, with fuel burn rates of 6 L/h and 12 L/h, yield:
Distance = (60 + 120) / 3600 = 0.050 kmFuel = (6 + 12) / 3600 = 0.005 LThese are the exact numerical expectations asserted by the analytics unit/integration test.
4.2 Why the dashboard metrics are stable
The displayed Eco Score, efficiency, idle ratio, and aggressive-event indicators do not come from a fully calibrated vehicle physics or behavior model. Instead, the implementation starts from scenario-specific targets and applies a small deterministic sine wave:
The implementation also clamps values into profile-specific ranges and scales aggressive-event counts toward their targets. This is a practical demonstration-oriented model: the distance and fuel-volume integrals come from frames, while several dashboard indicators are intentionally constrained to show consistent differences between driving styles.
This made the presentation reproducible. It does not establish that the Eco Score is calibrated against real-world fuel economy, or that its value can be inferred from arbitrary live CAN traffic.
4.3 Converting efficiency into cost and emissions
The engine derives cost per kilometer using a fixed demonstration price of RM2.05/L, and monthly spend using a 1,200 km/month assumption:
cost_per_km = (fuel_efficiency_l_per_100km / 100.0) * 2.05monthly_spend_myr = cost_per_km * 1200.0co2_kg = fuel_burned_liters * 2.31For a target efficiency of 5.8 L/100km, that is approximately RM0.119/km or RM142.68/month before rounding conventions. The CO₂ term, however, is calculated from the session's integrated fuel consumption. It is not a monthly total.
The critical lesson was to keep units, assumptions, and provenance explicit. A dashboard can display values that look equally precise while those values have very different evidentiary bases.
5. Refuel optimization: a decision tree, not an LLM judgment
The refueling engine in decision_engine.py receives the tank level, tank capacity, and fuel-price context. It returns a recommendation, a reason, and estimated savings.
The first decision is a hard reserve constraint: below 15% fuel, return BUY regardless of price movement. A level from 15% through 40% also results in BUY, including when prices are falling, because the prototype prioritizes avoiding a low reserve.
| Fuel reserve | Price trend | Result |
|---|---|---|
| Below 15% | Any | BUY — critical override |
| 15–40% | Falling | BUY — reserve too low to delay |
| 15–40% | Other | BUY — low reserve |
| Above 40% | Falling | WAIT — potential lower future price |
| Above 40% | Rising/other | BUY — protect against higher prices |
The 30-day rolling average influences the wording of the rising-price rationale, but both rising-price branches for a healthy tank recommend BUY.
The remaining fill volume is determined by the tank rather than assumed:
remaining_liters = tank_capacity_l * (fuel_level_pct / 100.0)fill_liters = tank_capacity_l - remaining_literspotential_savings = abs(future_price - current_price) * fill_litersFor the demo, fuelpriceservice.py provides two fixed price contexts: a falling trend for scenario B (RM2.05 current; RM2.10 average), and a rising trend for the other scenarios (RM2.05 current; RM2.00 average). The engine models a RM0.08/L decrease when waiting, a RM0.10/L increase for a low-reserve rising-price case, and a RM0.05/L increase for the healthy-reserve rising-price case.
These are scenario assumptions, not forecast outputs. The savings number is therefore a what-if estimate, not a proven market prediction. In a production version, the price context and forecast error would need independent validation.
The AI cannot silently change a BUY into WAIT or override the low-fuel rule. The backend makes and publishes the decision first; AI is only allowed to provide an explanation of the supplied result.
6. State orchestration: rebuildable snapshots instead of a permanent tick process
FuelSense uses SQLAlchemy models for both session metadata and telemetry samples. The central orchestrator in scenario_engine.py then builds a complete SimulationState containing telemetry, analytics, refuel decisions, fuel-price context, and AI insights.
A particularly useful design choice was elapsed-time catch-up. The current implementation does not require a continuously running per-session background loop. When state is requested, it can derive how far the 180-second scenario should have progressed, generate missing frames, persist them in a batch, and recalculate the snapshot.
5for step in range(last_stored_step + 1, target_step + 1):6 frames_to_insert.append(generate_frame(step))7 8save_batch(frames_to_insert)9rebuild_and_publish_snapshot()At a loop boundary, the actual code updates the start timestamp and clears the previous loop's samples. This is well suited to reproducible demos because elapsed time drives simulation progress, even after a pause between HTTP requests. It does not make SQLite persistence itself globally stateless.
Snapshot contract and frontend synchronization
The API contract specifies a full-state response rather than a stream of partial field updates:
{ "session_id": "active", "state_version": "1.0", "simulation_state": { "scenario_id": "B", "status": "RUNNING", "current_step": 42, "telemetry": {}, "analytics": {}, "refuel_decision": {}, "fuel_price_context": {}, "ai_insights": {} }, "meta": { "state_hash": "...", "is_stale": false, "server_time": 0 }}The abbreviated object above illustrates the envelope; a real response contains all required nested fields. The backend calculates state_hash from the scenario ID, current step, and status. It is a version/change marker, not a checksum of every nested analytics value.
In App.jsx, React polls /scenario/state every three seconds and replaces its main simulation state from the returned snapshot. The frontend doesn't integrate telemetry or determine refueling actions itself. Refueling explanations are handled separately through a streamed request.
Locking and transition scope
Scenario starts, stops, and resets use a per-session asyncio.Lock. It serializes cooperating coroutines within the current Python process, reducing overlap between conflicting scenario-control requests.
The start path commits a RESETTING state, waits one second, then enters a second locked section to transition to RUNNING. This is not one database transaction spanning the entire transition, and an asyncio.Lock is not a multi-process or cross-thread lock. The current router also uses the shared session ID active for the presentation. These are reasonable prototype boundaries, not general multi-user isolation guarantees.
7. Streaming AI without putting a language model in the critical path
The DeepSeek integration in ai_service.py receives context from the already computed state. Prompts instruct the model to explain, rather than invent or recalculate, the metrics and decisions.
The current streaming endpoints use daemon worker threads plus queue.Queue, not an asyncio executor-based implementation. The route retrieves the authoritative decision immediately, then streams its explanation in chunks while a worker handles blocking model I/O.
The refuel endpoint begins with JSON-lines metadata, for example a decision and estimated_savings, followed by chunk records containing pieces of the explanation. It is an HTTP StreamingResponse with newline-delimited JSON, not WebSocket messaging. The chat route also streams text, using a thread-backed queue.
Crucially, the application includes local scenario explanation templates. When the API key is absent, demo mode is enabled, or the external request fails, those templates provide a deterministic fallback. That preserved a useful explanatory interface without making model availability a prerequisite for calculation.
The prompts constrain model behavior, but prompting alone cannot prove every generated sentence is correct. For a safety-sensitive production advisor, the displayed quantitative facts would need programmatic validation as well.
8. Verification: test the contracts and the arithmetic
The repository includes eight pytest test functions in test_e2e.py. They cover scenario listing, scenario setup, status and state envelopes, simulation start, chat response, refueling rules, and sample-integration arithmetic.
python -m pytest backend/tests/test_e2e.py -vThe most concrete checks assert that:
- Fuel below 15% results in
BUY. - Fuel above 40% during a falling price trend results in
WAIT. - A rising trend with price below the rolling average results in
BUY. - Two synthetic one-second frames integrate to 0.05 km and 0.005 L.
/scenario/stateincludes a versionedSimulationStateenvelope and key nested objects.
These are repository-defined tests; they are not evidence of real-vehicle calibration, load testing, multiple concurrent users, or verified production latency. I haven't published performance numbers for this build because the project documentation's latency figures are targets, not benchmark measurements.
9. What I would harden beyond the hackathon
The current system is a convincing prototype precisely because it minimizes external uncertainty. The same design also makes its next engineering steps clear:
- Data provenance: Replace scenario-based CAN-style patterns with a validated OBD-II/CAN adapter, proper signal decoding, and timestamp-aware sampling.
- Analytics calibration: Derive efficiency and behavior scores from recorded driving sessions instead of target-based clamping; document uncertainty and unit conversions.
- Price forecasting: Replace fixed RM offsets and trend fixtures with timestamped price observations and a separately evaluated forecasting model.
- Isolation and concurrency: Introduce real session ownership and stronger transaction/state-version controls across processes, especially around a scenario switch or late AI response.
- AI verification: Keep rule-based decisions authoritative, verify generated numerical claims against the snapshot, and handle cancellation, timeouts, and stale streamed responses.
- Testing: Add boundary tests at 15%/40%, scenario-switch races, streaming interruptions, and end-to-end measurements under load.
Those are future engineering requirements, not claims about what shipped during the six-hour event.
10. The constraints behind the implementation
I chose this challenge after 16 previous hackathon wins because following my usual build-and-pitch formula had become predictable. Running two competitions at once forced me to prioritize interfaces, reproducibility, and failure recovery.
While FuelSense's deterministic backend was taking shape, I was crossing the hallway to work on VibeUI with the Google Antigravity coding agent. That session exhausted its token quota and entered a four-hour lockout, leaving its generated UI in a broken state. I used the interruption to finish presentation material and continue manual implementation. With 15 minutes remaining, I discarded the unusable VibeUI frontend and rebuilt a minimal HTML/CSS/JavaScript site. The pitches overlapped; I presented one project, ran across the hallway, pitched FuelSense, and returned.
VibeUI did not place. After hearing third runner-up, second runner-up, and first runner-up announced without my name, I expected the split focus to have cost me both competitions. Instead, FuelSense was announced as Vibeathon 2026 Champion.
The result did not demonstrate that manually written code is inherently better than AI-generated code. The more useful conclusion was architectural: I could explain and recover every critical part of FuelSense's computation without relying on a coding agent, while still using an LLM for the layer where natural-language generation was genuinely valuable.
The engine makes the decision. The AI explains it. And when the tooling fails, I still need to understand every part of the system.
For the full implementation details, see the GitHub repository, scenario data pack, API/state-sync specification, and competition case study.
