I wanted to build something that mattered. Climate change is one of those topics where awareness is the first barrier — most people don't know their actual footprint. So I built Carbon.Ledger: a full-stack web app that lets you log activities, see your CO₂ impact, and get AI-powered tips to cut back.
Here's how I built it, what went wrong, and the one engineering decision I'm most proud of.
mistralai/mistral-large-3-675b-instruct-2512
The app is built around one simple loop:
Understand → Track → Reduce
Users can read lesson articles, browse a glossary (CO₂e, Scope 1/2/3, Net Zero), and ask free-text climate questions via an LLM-powered Q&A — rate limited to 10 questions/day.
An activity logging form lets users log transport, energy, food, and goods. CO₂ is auto-computed on save using emission factors stored in the database — no LLM involved here. The dashboard shows a 30-day breakdown with a Chart.js donut chart and progress against a monthly goal.
Rule-based insights compare the user's totals against national average benchmarks. The top emission category drives filtered recommendations. And once per day, the LLM generates a personalized tip — cached 24h per user.
This is the part I'm most proud of.
LLMs make up numbers. That's a real problem when you're talking about emissions data — if the model says "beef produces 5 kg CO₂ per kg" when the real figure is 27, that's actively harmful misinformation.
My solution in services/llm.py
:
def _contains_hallucinated_numbers(response: str, context: str) -> bool:
"""
Extract all numbers from the LLM response.
Check every one of them exists in the context we provided.
If not → hallucination detected.
"""
import re
response_numbers = set(re.findall(r'\d+\.?\d*', response))
context_numbers = set(re.findall(r'\d+\.?\d*', context))
return not response_numbers.issubset(context_numbers)
The flow:
timeout=10.0
and max_retries=1
are explicitly set to prevent hung Gunicorn worker threads on slow LLM responses.Key principle: the LLM is never used for calculations. All math is Python/DB.
The activity log form has a dependent dropdown — selecting a category dynamically loads the relevant emission factors. I used htmx for this instead of writing custom JavaScript:
<select name="category"
hx-get="/factors/"
hx-target="#factor-select"
hx-trigger="change">
One attribute. No JS file. The server returns a partial HTML snippet with the filtered factors. This is exactly what htmx is built for.
Render makes Django deployment straightforward. A few Django-specific things worth noting:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = not DEBUG
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
Render terminates TLS at the proxy level, so SECURE_PROXY_SSL_HEADER
is essential — without it Django won't recognize HTTPS requests and will redirect loop forever.
Also: randomize your admin URL via env var:
ADMIN_URL = env('ADMIN_URL', default='admin/')
Stops bots from hammering the default /admin/
endpoint.
Async LLM calls. Right now the LLM calls are synchronous — a slow NVIDIA NIM response blocks a Gunicorn worker. The fix is Celery + Redis for background tasks. I skipped it for the MVP but it's the first thing I'd add.
Audited emission factors. The 22 factors I seeded are approximations from public sources, not peer-reviewed figures. For a production app, you'd want to integrate a verified dataset like the UK Government GHG conversion factors.
Email verification. Currently anyone can sign up with any email. Not a problem for an MVP, but a real gap.
If you're building something with Django + LLMs, the hallucination guard pattern is worth stealing. And if you have thoughts on better emission factor datasets, I'd love to hear them in the comments.