Building an Artemis II Tracker

William McNamara • April 21, 2026

How I tracked humanity's return to the Moon with Python, Streamlit, and a handful of free APIs

When NASA announced that Artemis II would carry the first crewed lunar mission since Apollo 17 in 1972, I wanted to do more than just watch the launch. I wanted to build something — a live dashboard that would let me (and anyone else) follow the mission the way Mission Control does, with real numbers updating in real time. No accounts, no API keys, no paywalls. Just open data and a Python script.


The Idea: What Would Mission Control Actually Show?


The first question I had to answer was: what data is even publicly available for a crewed NASA mission? The answer was more than I thought, but less than I hoped.


On the "more than I thought" side: NASA's Jet Propulsion Laboratory runs a system called Horizons, which is essentially a live database of the positions and velocities of every tracked object in the solar system, including active spacecraft. So cool! Artemis II's Orion capsule has an official JPL target ID: -1024. That means I could query its exact position, distance from Earth, and velocity at any moment, for free, with no authentication required.


On the "less than I hoped" side: crew biometrics, cabin pressure, CO₂ levels, and propellant remaining; all of that is monitored internally by Mission Control in Houston and none of it is released publicly during an active mission. Boo! I made peace with that early and decided to focus on what I could show and show it beautifully.


The final feature list came together around four pillars: orbital tracking from JPL Horizons, space weather and crew radiation data from NOAA, live imagery from GOES-16 and NASA's Solar Dynamics Observatory, and lastly I built a 3D WebGL scene just to make it all feel like a real mission control room.


Setting Up the Project


The stack is deliberately minimal. The whole application is a single Python file, app.py, with three dependencies: Streamlit for the web interface, Plotly for charts, and Requests for HTTP calls. That's it. No database, no background workers, no message queues.


I also wrote a small installer script to handle environment setup cleanly.

Running python3 install.py once creates a self-contained virtual environment and installs everything. After that, launching the dashboard is a single command. I wanted the setup experience to be frictionless for anyone who wanted to run it themselves.


The Heart of It: Querying JPL Horizons


The most technically interesting part of the project was getting live orbital data out of JPL Horizons. The system has a REST API, but its response format is designed for astronomers; it returns a structured text block with headers, separator lines, and a data section bracketed by $$SOE and $$EOE markers.


I queried two objects simultaneously on every refresh: the Orion capsule ( -1024 ) and the Moon ( 301 ). Getting the Moon's live position was important; the distance between Earth and the Moon varies by about 50,000 km over its orbit, and I wanted the "percentage of the way to the Moon" calculation to be accurate rather than using a fixed average.

The quantities 1 and 20 give me the spacecraft's right ascension and declination (its angular position in the sky) plus its range from Earth in astronomical units and its range-rate — the rate at which that distance is changing, which is the radial velocity. From those four numbers I could derive everything else: distance in km and miles, velocity in km/s and mi/s, altitude above Earth's surface, and light travel time.


Parsing the response required a small trick. Horizons sometimes inserts a variable number of *** separator lines between the column header and the data block, which breaks naive parsing. I solved it by walking backwards from the $$SOE marker to find the first line with at least four commas — that's always the header:

With the position and angles in hand, I could convert the spherical coordinates (RA, Dec, distance) into Cartesian XYZ coordinates in kilometers — which I'd need later to place the spacecraft correctly in the 3D scene.

Derived Metrics: Making the Numbers Meaningful


Raw astronomical units and right ascension coordinates aren't what most people want to see on a dashboard. I built a layer of conversions to turn the Horizons output into human-readable metrics:

I also built a mission phase detector that uses the spacecraft's distance relative to the Moon's current distance to determine where in the journey it is:

Space Weather: Protecting the Crew


One thing that makes a crewed lunar mission genuinely different from robotic missions is radiation. Beyond Earth's magnetosphere, the crew is exposed to solar energetic particles and galactic cosmic rays with no natural shielding. Space weather isn't just an interesting sidebar — it's a crew safety issue.


NOAA's Space Weather Prediction Center publishes real-time data from the DSCOVR satellite (parked at the L1 Lagrange point, about 1.5 million km sunward of Earth) and the GOES geostationary satellites. All of it is free JSON, no API key required, updating every minute or two.


I pulled five endpoints on every refresh:


  • Planetary Kp Index — a measure of geomagnetic disturbance, 0–9 scale
  • Solar wind speed and density — from DSCOVR's Faraday cup
  • Interplanetary Bz — the north-south component of the solar magnetic field; a sustained negative Bz drives geomagnetic storms
  • X-ray flux — used to classify solar flares from A through X class
  • Proton flux >10 MeV — the NOAA S-scale radiation storm indicator


The flare classification logic mirrors the official NOAA scale:

And the S-scale radiation storm levels map onto proton flux thresholds from S1 (Minor, >10 pfu) through S5 (Extreme, >100,000 pfu), each rendered with an appropriately alarming color.


Live Imagery


The dashboard pulls two live satellite images that update automatically — no intervention needed.


Earth comes from GOES-16, NOAA's geostationary weather satellite parked over the Americas at 35,786 km altitude. NOAA's NESDIS division publishes the GeoColor full-disk composite at a public URL that updates every ten minutes. It's a single st.image() call.


The Sun comes from NASA's Solar Dynamics Observatory, which has been imaging the Sun continuously since 2010. The 193-angstrom AIA channel shows the solar corona at about 1.5 million degrees Celsius — active regions appear bright, and solar flares are visible as sudden brightenings. NASA publishes the latest image at a stable URL that updates every fifteen minutes.


The 3D Scene: WebGL Inside Streamlit



The most visually striking part of the dashboard is a live 3D rendering of the Earth-Moon system with the Artemis spacecraft shown at its actual position. I built this using Three.js, the JavaScript WebGL library, and embedded it in Streamlit using components.html() .

The trick is passing live Python data into the JavaScript scene. I serialized the orbital positions into a JSON payload and injected it as a JavaScript constant:

Inside the Three.js scene, everything is built procedurally from Canvas 2D textures — no image files required. Earth gets a multi-layer texture with ocean gradients, painted continents, polar ice caps, a cloud layer, and a night-side emissive map showing city lights. The Moon gets a hand-painted texture with maria (the dark basaltic plains), highland regions, and impact craters. Both textures are generated fresh in the browser on every load.


The spacecraft itself is rendered as a triple-layer pulsing orange glow — an outer halo, a mid-layer, and a bright inner core — with a white point at the center and a yellow velocity vector pointing in the direction of travel. 7,000 stars surround the scene, distributed using the golden-angle spiral method for uniform coverage of a sphere, with vertex colors spanning blue-white, white, and warm yellow to approximate the real distribution of stellar spectral types.


Camera control is handled with spherical coordinates. The scene auto-orbits slowly when idle; dragging pauses auto-orbit and gives manual control; scrolling zooms. The camera always looks at the origin (Earth's center), and the zoom is clamped so you can't zoom inside Earth or past the Moon.


Caching: Keeping It Fast and Polite


The dashboard uses Streamlit's @st.cache_data decorator to avoid hammering the APIs on every page interaction. Different data sources have different refresh rates:

This means the dashboard feels instant for users — cached data is served immediately — while API calls happen at a sensible cadence in the background.


Deploying It


I wanted the dashboard to be always-on and publicly accessible, so I deployed it on a $6/month DigitalOcean Droplet running Ubuntu. The deployment stack is:


  • systemd to run Streamlit as a background service that restarts automatically on crashes or reboots
  • Nginx as a reverse proxy, forwarding port 80 to Streamlit's port 8501
  • Certbot for automatic HTTPS via Let's Encrypt


The systemd service file is the critical piece — it's what makes the dashboard "always-on" rather than dependent on a terminal window staying open:

Total monthly cost: $6. No sleeping, no cold starts, no usage limits.


What's Not There (And Why)


The dashboard intentionally includes a section explaining what it can't show. Crew biometrics, cabin telemetry, propellant levels — all of that exists, but it flows through NASA's internal Mission Control systems and has never been made available via a public API. This isn't a gap I could fill with creativity; it would require a direct NASA partnership.


I think it's worth being honest about this on the dashboard itself. A lot of "live tracking" sites paper over data gaps with stale numbers or estimates presented as live data. I'd rather show a clear "not available" than fake precision.


Closing Thoughts


The whole project took a weekend to build, costs $6 a month to run, and requires no API keys or paid data sources. Everything it shows is genuinely live — not simulated, not estimated, not cached from hours ago.


The most satisfying moment was watching the spacecraft position update in real time on the 3D globe and realizing that the numbers on screen represent four actual human beings traveling through deep space, further from Earth than any person has been in over fifty years. That felt worth building.


If you want to run it yourself, the full source is available and setup takes about ten minutes. All you need is Python 3.10+ and a terminal.

By William McNamara September 19, 2025
How I made a critical segmentation algorithm 9x faster with a new approach to clustering
By William McNamara December 11, 2024
Not a whole lot for Trump to undo
By William McNamara October 15, 2024
ColabFold has changed the game for amateur protein folding analysis
By William McNamara February 3, 2024
Getting started with Deepmind's revolutionary model for protein folding
By William McNamara December 22, 2023
A continuation of genome sequencing analysis
By William McNamara November 7, 2023
A good step but not nearly enough
By William McNamara August 1, 2023
Introductory methods for genome sequencing
By William McNamara April 2, 2023
A primer on foundation models: what they are, how they've evolved, and where they're going.
By William McNamara March 19, 2023
Like many music enthusiasts, the most used app on my phone by far is Spotify. One of my favorite features is their daily or weekly curated playlists based on your listening tastes. Spotify users can get as many as six curated ‘Daily Mixes’ of 50 songs, as well as a ‘Discover Weekly’ of 30 songs updated every Monday. That’s more than 2k songs a Spotify user will be recommended in a given week. Assuming an everage of 3 minutes per song, even a dedicated user would find themselves spending more than 15 hours a day to listen to all of that content. That…wouldn’t be healthy. But Spotify’s recommendations are good! And I always feel like I’m losing something when these curated playlists expire before I can enjoy all or even most of the songs they contain. Or at least I did, until I found a way around it. In this articule, I’m going to take you through Spotify’s API and how you can solve this problem with some beginner to intermediate Python skills. Introduction to Spotify’s API Spotify has made several public APIs for developers to interact with their application. Some of the marketed use cases are exploring Spotify’s music catalogue, queuing songs, and creating playlists. You can credential yourself using this documentation guide . I’d walk you through it myself but I don’t work for Spotify and I want to get to the interesting stuff. In the remainder of this article I will be talking leveraging Spotipy , an open source library for python developers to access Spotify’s Web API. NOTE : At the time of writing, Spotipy’s active version was 2.22.1, later versions may not have all of the same functionality available.
By William McNamara January 17, 2023
Google's DeepMind Researchers have proposed a new framework for scaling AI Models that could be a game changer for the space.