Inspiration

I'm on a university CubeSat research team. We don't have a satellite in orbit yet, but when we do, we'll get the same thing every small team gets: a public feed of predicted close approaches, and nobody whose job it is to read it.

CelesTrak's SOCRATES screens the whole tracked catalog, and one weekly run lists 183,612 predicted close approaches. Big operators have flight dynamics teams and a control room. A student team has a grad student and a spreadsheet. Most warnings are noise, but you can't safely ignore any of them.

So I asked: what if a small team had a calm controller who reads the list for them, throws out the false alarms, and just tells them, out loud, which one matters tonight?

What it does

Orbit Watch is a Grok voice copilot for space traffic. It pulls real close-approach warnings from CelesTrak SOCRATES, ranks them, shows them on a 3D globe, and briefs you by voice.

Our demo satellite is SwissCube (NORAD 35932), a 1U CubeSat built by EPFL students, with no thrusters. Its real warning list for this week had 21 entries:

21 screened → 19 dismissed → 2 kept. Each dismissal has a reason: 1 co-orbiting (BEESAT-1, its launch sibling, drifting at only 0.078 km/s), 16 below the probability threshold, 2 duplicate passes. Top threat: SL-8 DEB (NORAD 19831), Soviet rocket debris. Closest approach tonight, Sun Oct 4 at 9:26 PM ET (2026-10-05 01:26:28 UTC). Miss distance 621 m, head-on at 13.881 km/s, max probability 5.614 × 10⁻⁶. Flagged Act as the one the team should look at. SL-8 R/B is kept as Watch. What you can do in the app:

Heads-up alert. A banner with a live countdown tells you about tonight's threat as soon as you open the page. Play alert reads it out loud, and Replay alert plays it again. Brief me. One tap for a short spoken briefing that streams in a couple of seconds. Talk to Grok. Hold the mic and ask "What's the biggest threat tonight?" or "Why did you dismiss the others?" Grok Voice answers in real time. It calls tools for every number, so it never makes up a miss distance, and it can fly the globe to the pass it's talking about. 3D globe. Both orbits around the encounter, a pulsing closest-approach marker, a side label, colored orbits, Show dismissed, Reset view, and a scrubber. Grok Imagine renders. An artist's rendering of the selected encounter, built from the real orbital data and clearly labeled "not a photograph." Verify on CelesTrak. Every threat links to CelesTrak's own report, so judges can check our numbers. Our own math. We propagate both orbits ourselves with SGP4 and get 691 m, close to CelesTrak's 621 m. The gap comes from using orbit data published later. Any CubeSat. Type a NORAD number (or pick from the Known list), choose 24 hours, 72 hours, or 7 days, hit Track, and the cards, globe, and voice all switch to that satellite. Back to SwissCube returns to the demo. Share this alert and a voice on/off switch. Live mission room (SpacetimeDB). Everyone on the team sees the same live state: a status pill on each threat (Watching / Act / Dismissed), a note thread per threat, an "N watching" count, and a team feed where Grok's briefings and alerts post for everyone. Orbit Watch is a triage and explanation tool. It is not an operational collision-avoidance system and never gives maneuver orders.

How we built it

Cursor, with cloud agents running in parallel. All of the code was written in Cursor. Instead of one long session, we ran several Cursor cloud agents at once, each on its own branch and pull request (globe, voice, Brief me speed, Share, CubeSat lookup, globe polish, mission room). Agents committed small and often, and we merged PRs #1 through #16 over one night (14 merged; 2 duplicates closed). Before each merge, the branch pulled in the latest main so nothing conflicted.

Real space data from CelesTrak. The warning list is CelesTrak's SOCRATES Plus screen for SwissCube (data as of 2026-10-03 00:19 UTC). Orbits come from CelesTrak GP data in OMM JSON format, propagated with satellite.js (json2satrec, SGP4). We follow CelesTrak's usage policy: cache everything, check for new data at most once an hour, and stop on the first error with no retries. The demo runs from a committed snapshot of the real data so it works reliably during judging.

A ranker tuned for CubeSats. Each warning gets a 0–100 score from probability, miss distance, closing speed, time to closest approach, and whether the other object can be coordinated with. Out of the box, a CubeSat-scale probability (around 10⁻⁶) looked harmless, so we rescaled it. Now a sub-kilometer head-on pass rises to the top (SL-8 DEB scores 84.0), and slow co-orbiting "threats" get dismissed.

Grok Voice. Push-to-talk runs on the xAI realtime API (grok-voice-latest) with short-lived tokens minted on the server, so the API key never reaches the browser. Grok has four tools: ranked warnings, encounter details, explain dismissed, and focus the globe. Brief me uses a fast non-reasoning Grok model plus streamed text-to-speech.

Grok Imagine. grok-imagine-image-quality renders each selected encounter. The SwissCube / SL-8 DEB render is committed, so the demo never waits on a live image call.

CesiumJS globe. Next.js + Resium, with bundled imagery, so there's no external map token.

SpacetimeDB: the core real-time backend for the mission room. The shared mission room isn't a side log. SpacetimeDB is the only place the team's shared state lives:

Four tables in a TypeScript module (spacetime/): threat_status (Watching / Act / Dismissed and who set it), note (the per-threat note thread), presence (who's connected), and feed (system lines and Grok's briefings). Every change goes through a reducer (setStatus, addNote, heartbeat, postFeed), and each reducer checks its input first: allowed status values, length limits, valid names, no control characters. The feed also drops duplicates and keeps the latest 40 lines. No polling. Every open dashboard subscribes to the tables, so when one operator sets Act or adds a note, SpacetimeDB pushes it to every other screen instantly. Presence comes from the database itself. A disconnect hook removes you when you close the tab, and stale heartbeats get cleaned up, which is how the "2 watching" count stays honest. Grok writes into it. When someone hits Brief me or the heads-up alert fires, Grok's line posts to the shared feed, so the whole team hears the same briefing. The module is published to SpacetimeDB Maincloud as orbit-watch, and we checked the live tables with spacetime sql and the SpacetimeDB dashboard. The room is switched on with two public settings. When they're unset, the app looks exactly as it did before, so the solo demo can't break.

Challenges we ran into

Real data that wouldn't load. CelesTrak timed out over HTTPS from our build machines. Rather than ship fake orbits, we found that plain HTTP worked, kept the usage-policy limits, and swapped every placeholder for real data. The ranker ignored the real threat. A textbook probability scale made a 621 m head-on pass look like noise for a CubeSat. We had to recalibrate it for CubeSat-scale odds, and still dismiss the slow launch-sibling false alarm. Brief me was too slow. 18–50 seconds from click to sound. Switching to a fast non-reasoning model, streaming the audio, and writing a brief-only prompt fixed it. Two numbers for one miss. Our own SGP4 run says 691 m and CelesTrak says 621 m. We decided to show both and explain the gap honestly instead of hiding it. Many agents, one repo. Parallel agents mean parallel conflicts. We had each branch merge main before every merge, so 14 PRs landed in one night without breaking anything. Voice that sticks to the right satellite. After a lookup, the voice still said "SwissCube." We fixed it so every label, briefing, and tool follows the satellite on screen.

Accomplishments that we're proud of

It works on a real threat happening tonight, and anyone can check every number against CelesTrak. 21 warnings come down to one clear decision, with a reason for every dismissal. Our own orbit math agrees with CelesTrak's screening (691 m vs 621 m). Grok's voice is grounded: it calls a tool for every number and never gives a maneuver order. A real-time mission room on SpacetimeDB, built and verified in one night. A full product, built in Cursor by cloud agents working in parallel and coordinated through Grok Bot, over one night (first PR ~6:30 PM Saturday, last merge ~4 AM Sunday ET).

What we learned

Probability thresholds built for big spacecraft don't fit CubeSats, so the context changes what "dangerous" means. Voice needs to be fast and grounded. A slow or made-up briefing is worse than none. A real-time database turns a solo dashboard into a team tool with surprisingly little code, because the database handles the sync. Running Cursor agents in parallel works if you keep PRs small and always merge main first.

What's next for Orbit Watch

Mission room on the live site for every team, plus history of how a threat's status changed over time. Fresher data automatically: re-screen as the pass gets closer, and flag when orbit data is stale. Alerts by text or email when a new threat shows up for your satellite. "What can we actually do?" guidance for teams with and without propulsion. Day one for new teams: when our own CubeSat launches, we'll be watching from the first orbit.

Built With

  • celestrak
  • cesiumjs
  • cursor
  • cursor-cloud-agents
  • grok
  • grok-bot
  • grok-imagine
  • grok-voice
  • next.js
  • react
  • resium
  • satellite.js
  • sgp4
  • socrates
  • spacetimedb
  • tailwindcss
  • typescript
  • vercel
  • vitest
  • xai-api
Share this project:

Updates

Submission history