DEV Community

Cover image for How I Built a Carbon Footprint Tracker with Django + NVIDIA NIM
Divyansh
Divyansh

Posted on

How I Built a Carbon Footprint Tracker with Django + NVIDIA NIM

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.


The Stack

  • Backend: Django 5
  • Frontend: Tailwind CSS + htmx + Chart.js
  • LLM: NVIDIA NIM — mistralai/mistral-large-3-675b-instruct-2512
  • Database: SQLite (dev) / PostgreSQL (prod)
  • Cache: Redis (prod)

- Deployed on: Render.com

The Three Pillars

The app is built around one simple loop:

Understand → Track → Reduce

Understand

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.

Track

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.

Reduce

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.


The Hardest Part: LLM Hallucination Guard

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)
Enter fullscreen mode Exit fullscreen mode

The flow:

  1. Call LLM with context (user's actual data)
  2. Extract all numbers from the response
  3. Check every number exists in the context
  4. If not → retry with explicit "do not use numbers" instruction
  5. If still fails → return a safe static fallback message On top of that: 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.


htmx for the Category Dropdown

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">
Enter fullscreen mode Exit fullscreen mode

One attribute. No JS file. The server returns a partial HTML snippet with the filtered factors. This is exactly what htmx is built for.


Deployment on Render

Render makes Django deployment straightforward. A few Django-specific things worth noting:

# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = not DEBUG
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
Enter fullscreen mode Exit fullscreen mode

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/')
Enter fullscreen mode Exit fullscreen mode

Stops bots from hammering the default /admin/ endpoint.


What I'd Do Differently

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.


Results

  • 146 tests, 99% coverage
  • Deployed and live
  • Built in under a week

🔗 Live demo: carbon-footprint-awareness-4qpf.onrender.com

🐙 GitHub: github.com/Divyansh0208/Carbon-Footprint-Awareness


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.

Top comments (0)