3D globe correlating live OSINT feeds—flights, ships, outages—for solo analysts.
A 3D globe that pulls a stack of open intelligence feeds into one place: live aircraft, ships, satellites, GPS jamming, dark vessels, earthquakes, internet outages, conflict events and news. It correlates them on the server instead of leaving you to eyeball six tabs. It also runs as an MCP server, so an AI agent can ask it for live data instead of guessing from its training cut-off.
Live demo · Quick start · Take the tour · Query it from an AI agent
Before you get excited: it's a single-analyst tool, state lives in memory (restart = gone), AIS is densest over Northern Europe (global coverage is sparser and terrestrial-biased), and the 3D satellite mode is a VRAM hog. Full caveats in Scope and limits.
Honestly, not much. There are two ways to run it, pick whichever you're set up for:
compose plugin that ships with Docker Desktop), that's the whole list. One
command and you're up.corepack enable picks the right version for you), Python
3.12 for the backend, and uv to pull its dependencies. A plain venv works too
if you don't have uv.Either way, you need a browser that can do WebGL2, so any recent Chrome, Edge or Firefox. The globe leans on your GPU, so on a laptop with switchable graphics do yourself a favour and push it onto the discrete card before you judge the frame rate.
And no keys. The core feeds (planes, ships, quakes, satellites, the basemap) all run without a single API key. Keys only ever add reach; see What it pulls in for what each one buys you.
Docker is the short road. It brings up the API, the web app and nginx together:
git clone https://github.com/AndrewCTF/osint-geospatial-console.git cd osint-geospatial-console cp .env.example .env # optional, leave it empty and it still works docker compose up # api + web + nginx on :8080
Now open http://localhost:8080. It comes up live, planes moving, ships, quakes, the lot, with nothing to configure in between. The first time in, a short tour points out where things live; you can pull it back up whenever from ⚙ Settings.
Local dev without Dockermake install # pnpm install + api venv cd apps/api && .venv/bin/uvicorn app.main:app # backend on :8000 pnpm dev # vite on :5173, proxies /api to localhost:8000
Set VITE_API_URL if the backend isn't on http://localhost:8000. If you set
API_KEY on the backend, build the web app with a matching VITE_API_KEY; it
rides along as X-API-Key on every call.
1. The whole planet, live. Thousands of aircraft plus vessels, satellites, quakes and more on one Cesium globe. Every aircraft and ship renders as its category icon, coloured and rotated to its heading.
2. Zoom anywhere. Drag into a region and traffic, labels and coastlines fill in. Here's Europe and the Med: a few thousand aircraft at a glance, plus the layer rail on the left (toggle aircraft, vessels, jamming, quakes, …) and the timeline along the bottom.
3. Click anything. Select an aircraft or vessel and the panel on the right fills with its dossier: position, a track-history sparkline, GPS integrity, and the raw fields, while a magenta line traces its recent track on the globe. Click empty space and it clears.
4. Bring your own data — the Foundry tab. Upload a CSV/JSON/NDJSON, shape it through a governed pipeline (13 transform steps: filter, derive, join, aggregate, window, pivot, dedup, cast, regex…), gate every version with data-health checks (freshness SLAs, schema-drift, uniqueness…), and bind it into the same ontology graph as the live feeds. Lineage, immutable versions with rollback, and a dead-letter for rows that fail a transform all come along.
The point is fusion. Plenty of sites already do one feed well: Flightradar24 for planes, MarineTraffic for ships, GPSJam for jamming. Velocity goes after the seam between them: AIS plus radar imagery flags a ship that's switched its transponder off; a cluster of aircraft reporting bad GPS integrity becomes a jamming hotspot; when two or more of those line up in the same place and time, they get promoted to a single incident with a written, cited summary.
A few things worth knowing up front, because I'd rather you read them here than be annoyed later:
None of it needs an API key to start. Keys only add reach.
Rough live numbers off a running backend; they move around through the day:
| Feed | Typical live count | Where it comes from |
|---|---|---|
| Aircraft (ADS-B) | 9–13k (~11k typical) | OpenSky + airplanes.live |
| Military aircraft | ~140 | adsb.lol |
| Vessels (AIS) | ~33k, global | ShipXplorer + MyShipTracking + Digitraffic + Kystverket |
| GPS jamming | ~200 flagged 1° cells | ADS-B NACp/NIC, the GPSJam method |
| Dark vessels | radar change-detection | Sentinel-1 SAR |
| Fused incidents | correlation-driven | the correlation engine |
| Satellites | ~16k | CelesTrak |
| Earthquakes | ~250/day | USGS + EMSC |
| News + fact-check | ~370 articles | publisher RSS |
| Internet outages | country level | IODA, Cloudflare |
| Submarine cables | 715 | TeleGeography |
| Conflict events | ~1.5k live | GDELT, EONET, ACLED |
| Wildfires | VIIRS hotspots | NASA FIRMS (needs a key) |
| 3D war damage, imagery, webcams | varies | Sentinel, GIBS, OSM |
Optional keys, if you want more reach: AISStream for global AIS, an OpenSky
login for a bigger ADS-B budget, FIRMS_MAP_KEY for fires, an ACLED key for
conflict events, CLOUDFLARE_TOKEN for outages. GET /api/intel/sources
reports what's actually live versus what's still waiting on a key.
The part I think is genuinely new: the backend doubles as a Model Context
Protocol server, so an AI agent can interrogate the same warm feeds the globe
renders without scraping a dozen sites or flooding its own context. Ask "where
is GPS being jammed right now?" and it answers from the live feed. Full
architecture + /api/intel/* HTTP reference: docs/mcp-server.md.
It exposes 22 tools over app.mcp_server (a representative slice below; run
--list-tools for the full set):
| Tool | What it returns |
|---|---|
get_situation |
Global summary: aircraft by category, GNSS-degraded count, emergencies, worst jamming cells, vessel/alert counts. The cheap first call. |
focus_area(lat,lon,radius_nm) |
Loads a region PRIMARY (dedicated fresh /v2/point fetch + ongoing priority refresh, independent of global rate limits) and returns a full bundle: aircraft + density + GPS jamming + vessels + fused anomalies. |
aircraft_density |
Grid of cells (count, by category, GNSS-degraded) for an area. |
gps_jamming |
GPSJam-method assessment (ADS-B NACp<8 / NIC<7, 1° bins): flagged cells, severity, affected aircraft. Global or scoped. |
query_aircraft |
Filtered query (bbox/centre, category, squawk, callsign, altitude band, emergency / gnss_degraded / on_ground). |
lookup_aircraft(ident) |
One aircraft by ICAO24 or callsign + integrity/threat assessment. |
query_vessels |
AIS vessels in an area, classified; dark_only for dark-vessel candidates. |
anomalies |
Fused report: emergencies, jamming hotspots, dark vessels, alerts + a triage threat level. |
list_focus_areas / data_sources |
Active priority AOIs / feed health. |
deep_analyze(question, lat?, lon?) |
Gathers the relevant intel JSON and has a reasoning model reason over it (DeepSeek when configured, else a local Ollama model), so heavy analysis stays off the agent's context and only the conclusion returns. |
Every tool returns compact, bounded JSON (counts, grids, ≤50-item samples), so
an agent can sweep the planet for a few hundred tokens instead of pulling 15k
features. Heavy tools also take detail='short' (the default digest — top-N
of each list plus a <field>_total) or detail='long' (the full bundle), so
an agent sweeps in short and drills in long. Area-primary loading means the
agent's region of interest stays fresh and dense even while the global firehose is
being rate-limited; the rest of the world keeps streaming from the sticky snapshot.
The repo is also a Claude Code plugin marketplace. One install wires the MCP
server plus an analyst skill (osint-intel), slash commands (/osint-brief,
/osint-watch, /osint-jamming), and a osint-watch-officer agent. Start the
backend (bash scripts/run-api.sh), then in Claude Code:
/plugin marketplace add /path/to/OSINT
/plugin install osint-geoint@osint-velocity
Set repo_dir and python (the repo's venv interpreter) when prompted; the
plugin runs that Python directly, so it works on Windows, macOS, and Linux. The
installer prints the exact commands for your OS: bash plugin/osint-geoint/install.sh (Linux/macOS, -y to register the MCP server) or
plugin\osint-geoint\install.ps1 (Windows, -Run to register).
On the hosted platform the MCP server is mounted into the backend at /mcp
(streamable-HTTP), so there's nothing to install or run. Register it with any
MCP client using your Velocity access token:
claude mcp add --transport http osint-geoint \ https://projectvelocity.org/mcp \ --header "Authorization: Bearer $VELOCITY_TOKEN"
$VELOCITY_TOKEN is your signed-in Velocity (Supabase) access token; the
gateway Worker verifies it and the backend re-checks it, so the endpoint is
gated to your session.
# 1. backend must be running (provides the warm feeds) uv run --project apps/api uvicorn app.main:app --port 8000 # 2a. MCP server over stdio (Claude Code / Desktop / Agent SDK), cross-platform uv run --project apps/api python -m app.mcp_server # 2b. or streamable-HTTP uv run --project apps/api python -m app.mcp_server --http --port 8765 # introspect (no backend needed) uv run --project apps/api python -m app.mcp_server --list-tools
To register the local stdio server with Claude Code, run from the repo root:
claude mcp add osint-geoint -- uv run --project apps/api python -m app.mcp_server
uv run resolves the right interpreter on Linux, macOS, and Windows without
hardcoding a venv path. No uv? Point it at the venv Python directly:
apps/api/.venv/bin/python -m app.mcp_server (Linux/macOS) or
apps\api\.venv\Scripts\python.exe -m app.mcp_server (Windows), run from
apps/api.
Config (env or apps/api/.env): API_BASE, API_KEY, OLLAMA_HOST,
OLLAMA_MODEL (empty picks the smallest installed model; deep_analyze
degrades to returning raw JSON if Ollama is absent). The MCP server never
crashes a tool call: backend down returns a structured backend_unreachable
error; Ollama down falls back to raw intel JSON.
GET /api/export?fmt=geojson|csv|kml&kinds=aircraft,vessels&bbox=min_lon,min_lat,max_lon,max_lat&limit=N
downloads the current live picture (the same snapshot the globe renders) as
GeoJSON (QGIS / kepler.gl / Leaflet), CSV (spreadsheets), or KML
(Google Earth). bbox clips to a viewport; kinds is comma-separated (default
aircraft); limit caps features; vessels are best-effort.
The heavy component is the client, a CesiumJS WebGL2 globe. It is GPU- and
browser-main-thread-bound, and the backend is light. WebGL2 is required
(Chrome/Edge 110+, Firefox 110+). On hybrid-graphics laptops, force the discrete
GPU (chrome://gpu, look for adapter "ACTIVE").
VRAM depends heavily on which mode you run: