Skip to main content
Glama

Velocity

A self-hosted live map of aircraft, ships and satellites that records what it sees, so you can rewind to any past moment. No API keys, no account.

License Version Tests No keys required

git clone https://github.com/AndrewCTF/velocity.git && cd velocity
docker compose up          # then open http://localhost:8080

Why it exists

Free trackers only let you look back a little: Flightradar24 gives 7 days of replay on the free tier, MarineTraffic 24 hours, and ADS-B Exchange dropped its free API. Velocity pulls the same public feeds and writes every position to your own disk, so the history is yours.

History you can replay

Self-hosted

Account / API key

Who owns the archive

Velocity

As much as your disk holds

Yes

None

You

Flightradar24

7 days on the free tier

No

Account

Them

MarineTraffic

24 hours free (down from 72)

No

Account

Them

ADS-B Exchange

Free API tier discontinued

No

Paid API

Them

Free tiers as of July 2026; each cell is checkable on the vendor's pricing page.

Related MCP server: digitraffic-ais

What it does

  • Records history. Every position goes into SQLite by default, or Postgres/TimescaleDB if you set HISTORY_PG_DSN. The default keeps a rolling window; set ARCHIVE_MODE=1 to keep everything. A scrubber at the bottom rewinds the map to any moment you recorded.

  • Says where each contact came from. Every aircraft and ship shows which feeds reported it, how many agreed, and how old the position is, so a stale fix doesn't look live. /api/status/provenance gives the same per feed.

  • Puts more on the same map. GPS jamming, ships with AIS switched off, flight restrictions, military bases, naval warnings, earthquakes, internet outages and conflict events.

  • Keeps evidence. Captures get a SHA-256 hash and an append-only custody log, and a case exports to an HTML or PPTX report that cites its sources.

  • Talks to AI agents. It runs as an MCP server, so an agent can ask about live traffic instead of guessing. Anything automated is labelled as such.

Quick start · The apps · Take the tour · Query it from an AI agent

Limits

  • It's built for one person, not a team.

  • Coverage is whatever the public feeds see. It's good over Europe and North America and thin elsewhere; ship data is densest around Northern Europe. The map tells you where coverage is thin.

  • Some live state (current incidents, area selections, briefs) clears on restart. History, saved objects, cases and the evidence log stay on disk. See Scope and limits.

  • The 3D satellite view needs a lot of GPU memory.

Use at your own risk. No warranty; the authors take no responsibility for how you use this tool or the data it pulls. Velocity pulls from third-party sources that have not authorised it: their terms apply to you, the requests come from your IP, and rate limits, bans and legal exposure are yours. Feeds can be incomplete, delayed or spoofed, and AI summaries can be wrong. Don't use it for navigation, safety-of-life, or anything where being wrong hurts someone. Read DISCLAIMER.md before you run it. Security self-assessments: ISO/IEC 27001:2022 controls, OWASP ASVS 5.0 L2.

Prerequisites

Honestly, not much. There are two ways to run it, pick whichever you're set up for:

  • The easy way, with Docker. If you have Docker (24 or newer, with the compose plugin that ships with Docker Desktop), that's the whole list. One command and you're up.

  • Running the pieces yourself. If you'd rather not use Docker, you'll need Node 20+, pnpm 9 (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.

Quick start

Docker is the short road. It brings up the API, the web app and nginx together:

git clone https://github.com/AndrewCTF/velocity.git
cd velocity
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 — and position history is already recording to a Docker volume, so it's still there the next time you run docker compose up (set ARCHIVE_MODE=1 in .env for an open-ended archive instead of the default rolling window; see Scope and limits). The first time in, a short tour points out where things live; you can pull it back up whenever from ⚙ Settings.

The time dock in replay: the window set to one hour, the incident and signals density strips populated across it, and the header reading "Replay · 03:00 to 04:00 Z" with a return-to-live button

That dock along the bottom is the archive. Pick a window (1h, 6h, 24h, 3d, 7d), click anywhere on the density strip, and the globe rewinds to that moment; the header swaps from Live to Replay, and one button puts you back. Playback runs at 1x through 3600x. The dated day-picker and the recording since … · GB · fixes ownership chip live in the 2D console at /2d, which reads the same /api/history/coverage archive.

The Play button drives the clock through half-hour tar1090 replay chunks with a 24-entry cache, decoding readsb's 16-byte heatmap entries in a Web Worker and appending each contact's samples to its SampledPositionProperty. Aircraft reach back to 2024 through the public readsb heatmap archives the backend proxies and caches (GET /api/history/upstream/chunk|tracks|coverage); one decoded adsb.lol chunk (2024-06-01 12:00 UTC) held 180 slices of 10 s and 3,953 aircraft tracks over Europe. Vessels have no public equivalent, so they replay only as far back as your own archive goes.

Local (open) mode. The compute-heavy endpoints — the LLM-backed Reports tabs, recon, and OSINT enrichment — fail closed on a keyless box so a public deployment can't be made to spend GPU/credits by an anonymous caller. The dev docker compose up sets ALLOW_UNAUTHENTICATED=1 for you (the stack is loopback-only), so those features work out of the box and the UI shows an open-mode banner. Configure API_KEY or Supabase in .env and real auth takes over (the flag becomes irrelevant). For an internet-facing box use docker-compose.prod.yml, which leaves the flag unset. The globe, replay, evidence capture, and all keyless feeds never needed the flag.

Open it on your phone

The stack listens on 127.0.0.1 only, on purpose: the dev stack runs in open mode, so anything that can reach it can use it. To reach your own instance from your phone, put it on your tailnet instead of your LAN:

tailscale serve --bg 8080   # https://<this-machine>.<your-tailnet>.ts.net

Then add that origin to .env and restart, or the API refuses the new Host header (the DNS-rebinding guard in app/origin_guard.py):

CORS_ORIGINS=http://localhost:8080,https://<this-machine>.<your-tailnet>.ts.net

Only devices signed in to your tailnet can open it. Don't use tailscale funnel, which publishes it to the whole internet. For anything wider than your own devices, run docker-compose.prod.yml with Supabase sign-in configured (docs/security/auth-and-sessions.md); it fails closed without a credential.

make 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 API_PROXY_TARGET if the backend isn't on http://127.0.0.1:8000; the dev server proxies /api, /tiles and /ws there, so the browser stays same-origin. (VITE_API_URL is different: it makes the browser call the API directly, cross-origin, and is only for builds with no proxy in front.) API_KEY is for servers, the CLI and the MCP server. Never build it into a hosted web bundle, because anyone who loads the page can read it. Browsers sign in with Supabase instead (docs/security/auth-and-sessions.md).

The apps

Velocity isn't one mega-dashboard; it's a workspace of eighteen apps that all share the same selection and time context, so switching never loses the object you had selected. The launcher in the top bar groups them:

Group

Apps

What they're for

Live

Map · Sim

The Cesium globe (always mounted behind everything else) and a browser war-game overlay.

AI

AI · Models

The analyst agent, automated watch and alerts, and a registry of local models (installed, catalog, presets, engine).

Analyze

Explorer · Graph · Investigate · Targeting · Video · Country · Markets · Quiver · Vertex

Filter the live object store, grow a link-analysis graph, run digital OSINT (domains/people/wallets), a notional kill-chain board, FMV/ground recon, per-country World-Bank/UN stat dossiers, indices/commodities/FX with a stress index, time-series cards over the recorded archive, and a system graph of sources, feeds and layers.

Data

Foundry · Workflows · Workshop

Bring your own data through governed pipelines; automate the live feeds with a node-graph editor; compose modules from live widgets.

Product

Reports

Case files, watch-officer briefs, dossiers, and case→report export.

3D

City 3D

Gaussian-splat city scenes built from a keyless satellite AOI.

The rest of this README tours the ones you'll live in. Everything below is a real screenshot of the current build (v1.0.1), taken against live feeds on 2026-08-08, not a mockup. node scripts/screenshot-readme.mjs re-shoots the whole set.

Take the tour

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; dense areas cluster into count badges you can drill into.

Global view of the Velocity globe with live air, sea and incident traffic

2. Zoom anywhere, toggle everything. Drag into a region and traffic, labels and coastlines fill in. The left rail is the whole intel stack in one tree — air, maritime, space, ground hazards, signals and 18 infrastructure layers — each with its live count. A right-side map toolbar adds the analyst verbs on top of the globe: measure a running great-circle distance, area-select a box and search every object inside it, annotate with dropped markers, or move a marker — plus one-click camera reset and zoom.

The layer tree over a live European air picture, air/maritime/space/hazards toggles with counts

3. Click anything. Select an aircraft or vessel and the right panel fills with its dossier: identity, kinematics, the flight/route, ACARS, a pattern-of-life baseline, GPS integrity and the raw fields — while a magenta line traces its recent track on the globe. An AI assessment block (labelled, and grounded in the observed track) summarises what the dossier fields imply. Click empty space and it all clears.

A selected aircraft over the North Atlantic with its full dossier panel open — identity, kinematics, integrity, freshness, provenance, sanctions screening — and its magenta track drawn back across the ocean

4. Or query the whole store at once. The Explorer app is every live object in one filterable, sortable table — tens of thousands of tracks across vessels, aircraft and quakes, each tagged with the feed it came from and how long ago it was seen. Facet it, save the search, export the result to CSV.

Explorer: every live object in one filterable table, faceted by kind and tagged by source

5. Chokepoints and AOIs, watched continuously. A curated library of the world's maritime chokepoints — Hormuz, Bab-el-Mandeb, Malacca, the Taiwan Strait, the English Channel, undersea-cable corridors — each with live transit counts and the daily oil-flow figures that make it matter. Standing detections fire against them in the background.

Global chokepoint watch: Hormuz, Bab-el-Mandeb, Suez, Panama and Malacca with their daily transit and oil-flow figures

6. Airspace, bases, ports and airports — operational context, still keyless. Toggle the TFR/Airspace layer and live FAA temporary flight restrictions draw as real polygons, coloured by reason with their altitude bands. Alongside them: 7,183 military bases (air, naval and army, from Wikidata), NGA NAVAREA broadcast warnings with mine areas flagged, 3,804 ports from the NGA World Port Index (harbour size and type, shelter, repairs, dry dock, pier and channel depths), and airports enriched with runways (length, surface, lighting, per-end ILS category from FAA NASR), tower/ground/ATIS frequencies, live METAR with flight category, and a LiveATC linkout. A basemap picker swaps between nine keyless map styles (Carto dark, 3D satellite + terrain, Apple satellite, Esri imagery/topo/dark, OpenTopoMap, USGS, Sentinel-2 cloudless), each probed live before it's offered and each carrying its required attribution.

Live FAA temporary flight restrictions drawn as polygons over the US, with SIGMET hazards and the airport and port layers on

7. Space, with real physics. ~16k satellites from CelesTrak, propagated in-browser with actual SGP4 orbital mechanics and enriched from SATCAT with owner, launch date, radar cross-section and orbit class.

The globe wrapped in the live satellite constellation

8. The watch officer works the inbox. An autonomous watch officer fuses jamming, dark vessels, military air activity and conflict events into ranked, cited alerts — GPS spoofing, jamming cells, dark-vessel candidates — each with an [inferred] reasoning line and a one-click slew to. It runs detect_deception and deep_analyze against the live picture, so the inbox fills itself on a fresh keyless boot with no operator setup.

Watch-officer inbox: queued alerts carrying spoofing, jamming and dark-vessel badges alongside the reasoning behind each one

9. …and writes the brief. Every couple of minutes it promotes the convergences — "vessels went dark near reported activity", "spoofed tracks co-located with an AIS gap" — into ranked incident cards with a written, cited summary and a slew to button. Actionable incidents are auto-promoted into the investigation graph, so the ontology grows itself.

Watch-officer intel brief: ranked, cited cross-domain incidents with slew-to

10. Keep it as evidence. Anything worth keeping goes into the chain-of-custody evidence locker: capture a URL snapshot, upload a file, grab a screenshot, or freeze the current feed state, and each artifact is hashed with SHA-256, written to ./data/evidence, and logged to an append-only custody chain in the local ontology store. Attach it to a situation, re-verify the hash any time, or roll a whole case into a signed hash-of-hashes manifest. Fully keyless. From there, Reports exports the case (see Export) as a JSON bundle, a self-contained HTML report with per-claim provenance footnotes, or a PPTX brief — with any AI narrative confined to a clearly labelled block.

11. A tool that draws on the map gets its own window. The annotation editor, the watchbox rules and the field kit open as free-floating, draggable, resizable windows over the globe rather than stealing the rail from the layer tree — arm a tool from the map toolbar, set up what it draws beside the thing you are drawing on, and Window → Re-dock every floating panel clears them all. Positions persist for the session.

Two floating windows over the live globe: the annotation editor with its shape, colour and style controls, and the watchbox panel with its enter/exit/loiter trigger rules

12. 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, union, window, pivot, dedup, cast, sort…), 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 pipeline graph flags exactly which outputs went stale.

Foundry lineage pipeline: raw datasets on the left feeding five transforms into derived outputs, with raw, derived and stale colour-coded

13. Automate it — the Workflows tab. A node-graph editor over the same live feeds and ontology: wire sources (aircraft, vessels, quakes, alerts, datasets, ontology, countries) through sandboxed Python/SQL/LLM transform blocks into sinks (alerts, ontology objects, datasets, persistent memory), then run it on a schedule. Control blocks can reach outward too — webhooks to your own server, or a MAVLink bridge for drone tasking that rehearses log-only without a vehicle.

A Workflows DAG: live aircraft through Python and SQL into an alert

Scope and limits

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:

  • It's built for one analyst. One optional API key, no accounts or roles.

  • What persists vs what clears. Durable under ./data/ or the Timescale service: the position-history archive (a Postgres + TimescaleDB hypertable by default; SQLite at ./data/history.db remains the keyless fallback when no HISTORY_PG_DSN is set, with one boot warning naming the switch), the local ontology store (ontology objects, case files, situations; intel/ontology_local.py, ./data/ontology.db), plus separate SQLite files for Foundry, Workflows, alert rules, cached news and the Instability Index scorer (foundry.db, workflows.db, alert_rules.db, news_history.db, instability.db), and a keyless-local audit log (audit_log.db — only materializes when no SUPABASE_URL is configured; a keyed deployment writes audit rows to Supabase instead and never creates the file). That's 8 SQLite files, not one — back up the whole ./data volume, not a single DB. Also durable: the evidence locker (blobs under ./data/evidence, custody chain in the ontology store's append-only assertions table), a disk tile cache (./data/tilecache) and any downloaded local models (./data/models). Volatile, and cleared on backend restart: the live incident list, transient AOI selections, and generated watch-officer briefs. The Docker Compose volumes mean the durable stores survive docker compose down, not just a process restart.

  • Retention, honestly. The dev compose sets HISTORY_RETENTION_HOURS=48 (2 days), but that's a ceiling, not a promise: HISTORY_MAX_BYTES (2 GB by default) binds first, and at measured global ADS-B+AIS ingest (~3 GB/h on a keyless box pulling full-breadth feeds) the byte cap fills well before the hour window matters, leaving well under 2 hours of effective depth out of the box — not 48. The hour window only becomes the binding limit once disk is large enough for it to be the tighter cap; a genuine 48h global archive needs on the order of 150 GB (~3 GB/h * 48h). Raise HISTORY_MAX_BYTES, or set ARCHIVE_MODE=1 with HISTORY_DISK_BUDGET_GB sized to what you're willing to spend on disk (the production compose profile does this, 5 GB to start). /api/history/coverage reports the ACTUAL current depth (oldest_ts), not the configured ceiling, and the 2D console's day-picker and ownership chip render it, so you can see what you're really getting. Depth answers now come from the hourly positions_hourly aggregate; the old SQLite coverage scan took 73 s over 78M fixes, which is why the rollup exists.

  • AIS runs keyless and global (~50k vessels tracked, MMSI-deduped across ShipXplorer, MyShipTracking, Digitraffic and Kystverket), but coverage is densest over Northern Europe and the Baltic and thins out elsewhere; an AISStream key fills in the gaps. Sparse regions still lean on the radar (SAR) layer.

  • The two headless-Chrome sidecars are degraded under Docker, not crashed. ADS-B (OpenSky + the httpx tar1090 mirrors + ShipXplorer AIS) and the regional Kystverket/Digitraffic AIS feeds are direct HTTP calls and run at full strength in the container. But the tar1090 ADS-B sidecar and MyShipTracking (the enabled primary keyless wide-area AIS source) drive a real Chromium via Node, which the slim API image doesn't ship — they log a warning and sit idle rather than crash the backend, so you lose their extra coverage, not the app. Run bare-metal (bash scripts/run-api.sh) for full sidecar coverage.

  • Some Analyze apps are notional by design — the Targeting kill-chain board and the Video/FMV sensor HUD carry a "NOTIONAL // SIMULATED" banner and are driven by the Sim overlay, not a live weapons or ISR feed.

  • The 3D satellite view will eat your VRAM. The default 2D dark map runs on a laptop; check System requirements before switching the heavy mode on.

None of it needs an API key to start. Keys only add reach.

What is enforced

Not promises, checks in the tree. Each line names the route or test that holds it:

  • Clearance on reads and writes. One predicate, intel/ontology.visible_to, filters local reads (get, list_by_kind, search, links, assertions) and the situations, maps and evidence routes by the caller's level and compartments; keyless callers are clearance 0, and a write above clearance or onto a hidden row answers 403. → apps/api/tests/test_clearance_local_reads.py

  • Custody-chained evidence. Every capture is hashed with SHA-256 and appended to an immutable custody log; GET /api/evidence/{sha}/verify re-checks the bytes. → apps/api/tests/test_evidence.py

  • Hash-chained audit. Mutations append to action_log.db; GET /api/audit/verify walks the chain. → apps/api/tests/test_asvs_fix_a_logging.py

  • Citations withheld when ungrounded. LLM_REQUIRE_CITATIONS=1 (default) withholds a selection or country brief that cites none of its own ids; GET /api/ai/guardrails reports the posture. → apps/api/tests/test_citations_hard.py

  • Human-in-the-loop write-back. A write-back action is operator-only and runs only from a proposal the operator approves; the audit row carries the payload's SHA-256, never the payload. → apps/api/tests/test_writeback_action.py

What it pulls in

Live numbers measured off a running keyless backend on 2026-08-08; they move around through the day:

Feed

Typical live count

Where it comes from

Aircraft (ADS-B)

12–15k (14,436 measured)

OpenSky + airplanes.live + adsb.lol + FR24

Military aircraft

~30–60

adsb.lol + airplanes.live

Vessels (AIS)

~50k tracked, global

ShipXplorer + MyShipTracking + Digitraffic + Kystverket

GPS jamming

~220 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

16,095, SATCAT-enriched

CelesTrak

Airspace (TFRs)

live restriction polygons

FAA

Military bases

7,183 (+1,330 MIRTA/Wikidata)

Wikidata + DIA MIRTA

Naval broadcast warnings

~800 plotted points

NGA NAVAREA

Ports

3,804, with harbour detail

NGA World Port Index

Airports

runways + ILS + frequencies + live METAR

FAA NASR, NOAA, LiveATC

Earthquakes

~290/day

USGS + EMSC

News + fact-check

~370 articles (off by default)

publisher RSS

Internet outages

country level

IODA, Cloudflare

Submarine cables

~700

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

Per-country toolkits

53 countries

open-data / registries / sanctions links

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.

Use your own receiver

If you run an ADS-B receiver (readsb, ultrafeeder, tar1090), Velocity folds its aircraft into the same picture as the public feeds and records them to your archive like everything else. Point ADSB_FEED_URLS in .env at your tar1090's aircraft.json. The variable replaces the default list, so keep the defaults in it if you still want them:

ADSB_FEED_URLS=http://192.168.1.20:8080/data/aircraft.json,https://globe.theairtraffic.com/data/aircraft.json,https://skylink.hpradar.com/data/aircraft.json,https://api.adsb.lol/v2/point/0/0/20000

Contacts are merged by ICAO address, freshest fix wins. Inside Docker, localhost is the API container, so use the receiver's LAN address.

For a small desk display of what is overhead (ESP32, e-ink), GET /api/overhead returns the nearest aircraft as compact JSON from the same snapshot, with no upstream call of its own: see docs/desk-display.md.

MCP server: query the live console from an AI agent

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 94 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.

detect_deception / locate_emitter

Flag spoofed/co-located tracks; triangulate a jamming source from degraded-integrity aircraft.

whats_changed / incident_history

Delta since a timestamp; the incident timeline for an area.

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 same fusion powers the in-app AI selection brief: click an entity and POST /api/ai/selection/brief fuses its registry enrichment and pattern-of-life dossier into a selection-tier model prompt — every claim cites the dossier field it came from, an uncited answer is withheld rather than shown, and it degrades to the raw props if no model is configured.

Install as a Claude Code plugin (skill + commands + agent)

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).

Point your agent at your own instance

The MCP server is mounted into the backend at /mcp (streamable-HTTP), so once you're running Velocity yourself there's nothing extra to install to query it from an agent. Register it with any MCP client against your own host:

claude mcp add --transport http osint-geoint \
  http://localhost:8000/mcp \
  --header "Authorization: Bearer $VELOCITY_TOKEN"

$VELOCITY_TOKEN is only required if you've set API_KEY on the backend; a keyless local instance needs no header at all.

Self-host / develop

# 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.

Export

Two ways out, for two jobs:

The live picture. GET /api/export?fmt=geojson|csv|kml&kinds=aircraft,vessels&bbox=min_lon,min_lat,max_lon,max_lat&limit=N downloads the current 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.

A finished case. POST /api/situations/{id}/export?fmt=json|html|pptx walks a situation's children, its sourced assertions and every attached piece of evidence into a shareable artifact: a self-describing JSON bundle, a self-contained HTML report with a provenance footnote on each claim, or a PPTX brief. Any AI-written narrative is confined to one labelled block — the rest is your cited evidence.

System requirements

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:

  • 2D-dark (default basemap): light. The globe is a proxied 2D raster basemap plus the entity layers (aircraft/vessels). Runs on integrated graphics / ~2–4 GB VRAM, which is the right mode for modest hardware.

  • 3D-sat (satellite imagery + world terrain + OSM 3D buildings, optional Google Photorealistic 3D): VRAM-heavy. CesiumJS streams terrain meshes, high-res imagery, and 3D-tile building/photogrammetry sets, and it caches into whatever VRAM is available, measured at 20+ GB on a 32 GB card. Tilesets are now individually cache-capped (Google 3D ~1.5 GB, OSM buildings ~0.5 GB) and MSAA is off (FXAA instead), but with terrain + global imagery + a high-DPI/4K canvas the resident set is still large. On a card with less VRAM, Cesium evicts and re-fetches more aggressively (lower fps, more pop-in) and still runs.

Tier

GPU

RAM

Display

What you get

Edge / lite

WebGL2 integrated (Iris Xe / Vega / M1)

8 GB

1080p

OSINT_PROFILE=lite (default). 9k aircraft, 32k vessels, 2D-dark. Backend fits in ~1.5 GB; the adaptive governor sheds low-priority layers to keep frames under budget.

Recommended

Discrete ≥8 GB VRAM (RTX 3060 / RX 6700 / M-Pro)

16 GB

1080p–1440p

Full profile with all feeds. 2D-dark smooth; 3D-sat usable at city scale.

Full / 3D-sat

RTX 4070+/16 GB VRAM or better

32 GB

up to 4K

All feeds + browser sidecars + 3D-sat terrain + buildings; best frame rate.

These tiers come from a cgroup-isolated benchmark (tools/perf/bench_ram.sh) that constrains backend + Vite + Chrome inside a single MemoryMax and measures real fps, renderMs, entity counts, and OOM kills at each RAM level. 3D-sat genuinely wants a lot of VRAM, and the low-VRAM minimum only holds for the 2D-dark map; switch on 3D-sat and you'll want a discrete card with headroom.

Backend (server): Python 3.12, outbound HTTPS. The lite profile (default) disables browser sidecars, history, and the FR24/OpenSky-gaps tiers, measuring ~530 MB steady-state RSS — comfortably fits an 8 GB box alongside the browser. The full profile with all direct feeds warm measures ~3.2 GB. The optional headless-Chrome coverage sidecars are the real memory line item: the ADS-B scraping tier measured ~11 GB RSS across its ~50-process Chrome tree and the AIS tier ~3-6 GB across its ~20-36 (both measured 2026-07-24 at full coverage), so budget roughly 15 GB on top of the backend when both are enabled — or keep lite and use the direct feeds.

Stack

  • Frontend: Vite + React 18 + TypeScript + CesiumJS + MapLibre GL JS v5.24 + Tailwind + Zustand

  • Backend: FastAPI (Python 3.12) + httpx + websockets. Live derived state is in-process (bounded observation store + disk tile cache); durable state (the position-track replay archive, the ontology store: objects / case files / situations, and the evidence locker's custody chain) persists to Postgres + TimescaleDB or SQLite, on Docker volumes in Compose, with a rolling window by default or an open-ended disk-budgeted archive via ARCHIVE_MODE

  • Agent access: Model Context Protocol server (app.mcp_server, MCP SDK) + optional local Ollama analysis

  • Data: PostgreSQL 16 + TimescaleDB backs the position archive today (6 hour chunks, columnar compression, hourly positions_hourly rollup, SQLite as the keyless fallback). PostGIS + Redis remain planned.

  • Infra: Docker Compose, nginx reverse proxy

Layout

osint/
├── apps/web/                 # React + Cesium console (14-app workspace)
│   └── src/
│       ├── globe/            # Cesium globe, layers, map toolbar (measure/area/annotate)
│       ├── entity-panel/     # right-rail dossier / inspector
│       ├── evidence/         # chain-of-custody evidence locker UI
│       ├── foundry/          # FOUNDRY surface (datasets, pipeline DAG, builds, ontology)
│       └── shell/            # app switcher + detachable floating panels
├── apps/api/                 # FastAPI backend
│   └── app/
│       ├── intel/            # agent-facing analytics, local ontology store, evidence, case export
│       ├── foundry/          # BYO-data layer: datasets, transforms, builds, checks, binding
│       ├── routes/intel.py   # /api/intel/* deep-query JSON API
│       ├── routes/evidence.py# /api/evidence/* capture / verify / attach / manifest
│       ├── routes/foundry.py # /api/foundry/* datasets/pipelines/checks/bindings
│       └── mcp_server.py     # Model Context Protocol server
├── packages/shared/          # Shared TS types (LayerDescriptor, Observation)
├── docs/                     # design notes, decisions.md, foundry-plan.md
└── infra/                    # Docker, nginx, db init

Tests

# from the repo ROOT (running from apps/api makes .env auth resolve → a wall of 401s)
OSINT_DISABLE_BACKGROUND=1 apps/api/.venv/bin/pytest apps/api -q   # 3276 passed + 21 skipped
pnpm -r test                          # vitest (web, shared)
pnpm -r typecheck
bash scripts/verify.sh                # typecheck + lint + web unit + api tests in one shot
# manual MCP integration drivers (need backend on :8000):
#   apps/api/.venv/bin/python tests/mcp_client_check.py   # stdio handshake
#   apps/api/.venv/bin/python tests/mcp_full_check.py     # tools end-to-end + Ollama

Phase status

This is the internal build log, in order shipped — not the pitch (that's above). Legend: ✅ shipped · 🚧 in progress

  • ✅ Phase 0 — Foundation

  • ✅ Phase 1 — MVP: live ADS-B / AIS / quakes / GPS-jamming layers on the globe

  • ✅ Phase 2: Replay + drill-in. The timeline scrubber and the history archive ship with a Postgres + TimescaleDB backend (48h rolling window by default, or an open-ended disk-budgeted archive with ARCHIVE_MODE), SQLite as the keyless fallback, and a tar1090-style half-hour chunk replay pipeline that carries aircraft back to 2024 from public readsb heatmap archives; drilling into any past moment works today. PostGIS + Redis for the rest of the platform's state are a deferred scaling upgrade, not a blocker

  • ✅ Phase 3 — Fusion engine + alerts (correlation rules) + 2D mirror

  • 🚧 Phase 4 — Advanced sensors + agent access. MCP server + intel API (now with a Claude Code plugin and detail=short|long tool variants), Sentinel-1 SAR dark-vessel detection, an autonomous watch-officer that writes cited incident briefs and auto-populates the ontology graph from them, a keyless infra/domain OSINT layer, a places/airspace layer (FAA TFR polygons, military bases, NGA naval warnings, World Port Index port detail, NASR/METAR airport enrichment, an 8-way keyless basemap picker), a chain-of-custody evidence locker and case→report export (JSON/HTML/PPTX with per-claim provenance), a globe map toolbar (measure/area-select/annotate) and detachable floating panels, a photo-geolocation pipeline, a City 3D Gaussian-splat viewer, optional local-GPU (Ollama) inference, and a first-run onboarding tour. More sensors and deeper analysis are ongoing.

  • 🚧 Phase 5 — Foundry: a keyless, local, single-operator data-integration loop. Upload → transform (governed 13-step DSL with lineage) → build (dependency DAG, staleness, cycle rejection) → data-health checks → bind into the local ontology graph. In: immutable versions + rollback, row-level quarantine/dead-letter, freshness/schema-drift SLAs, window/pivot analytics, entity resolution. Deliberately out of scope (single-operator identity): multi-tenant MLS, distributed compute, streaming CDC, connector catalogs. Next: ontology Actions (audited write-back) and dataset branches.

  • 🚧 Phase 6 — Workflows: a node-graph automation layer over the same live feeds and ontology. 20 blocks — sources (aircraft, vessels, quakes, alerts, datasets, ontology, countries), transforms (op.python/op.sql/op.llm sandboxed subprocesses, op.geo, op.http, op.steps), sinks (alert, ontology, dataset, persistent memory), and external-actuation control blocks that reach out of the platform: control.webhook to any server, and control.drone/control.device to command a UAV or hardware via a JSON envelope. A first-class MAVLink bridge (app.mavlink_bridge, ArduPilot/PX4, log-only without a vehicle so you can rehearse) ships in-repo. See docs/workflows-control-blocks.md.

See docs/ for the per-feature design notes and pipeline writeups.

License

AGPL-3.0-or-later covers Velocity's source code. Upstream data carries its own licenses; several feeds are non-commercial / academic (e.g. ACLED, adsb.fi, OpenSky). See NOTICE for per-source attribution and terms, and verify each upstream's current terms before any commercial or redistributive use.

No warranty, and no liability for how you collect or use the data — see DISCLAIMER.md, which also covers scraping, upstream terms, personal data, and prohibited uses.

Available Tools

94 tools
adsb_by_callsignC

Live aircraft by callsign (e.g. 'UAL123').

Inputs: cs (required). Returns: object

adsb_by_hexB

Live aircraft by ICAO24 hex code (e.g. 'a1b2c3').

Inputs: icao (required). Returns: object

adsb_by_registrationB

Live aircraft by tail/registration number (e.g. 'N12345').

Inputs: reg (required). Returns: object

adsb_by_typeB

Live aircraft by ICAO type designator (e.g. 'B738', 'A320').

Inputs: type_code (required). Returns: object

adsb_laddA

Aircraft on FAA's Limiting Aircraft Data Displayed (LADD) list.

Inputs: no named parameters. Returns: object

adsb_piaC

Aircraft using Privacy ICAO Address (PIA) — randomized hex codes.

Inputs: no named parameters. Returns: object

aircraft_densityA

Aircraft density over an area as a grid of cells (count, by category, GNSS-degraded per cell) plus totals, peak cell, and in-area vessel count.

Give either a centre (lat, lon [, radius_nm]) or an explicit bbox (min_lon, min_lat, max_lon, max_lat). cell_deg sets grid resolution.

Inputs: lat, lon, detail, max_lat, +5 more. Returns: object

aircraft_dossierB

Pattern-of-life dossier for one aircraft (ICAO24 hex or callsign): recent track, gaps, derived speed profile, GNSS-integrity, emergency/military flags, and which live incidents it appears in.

Inputs: ident (required), detail. Returns: object

air_qualityB

Air quality (US AQI + PM2.5/PM10) sampled across major world cities — a proxy for industrial activity, smoke drift, and chemical release.

Inputs: detail. Returns: object

anomaliesB

Fused anomaly report for an area (or global if no coords): emergency aircraft, GPS-jamming hotspots, dark-vessel candidates, recent fusion alerts, plus a triage threat_level (low|elevated|high) and score.

Inputs: lat, lon, detail, max_lat, +4 more. Returns: object

answerA

Named questions answered with a verdict, its rule, and the evidence age.

The one call that returns a CONCLUSION rather than data to interpret. Each answer carries verdict (open / reduced / closed / unknown), threshold (the rule that produced it, in words), data_lag_s (how old the newest evidence is), and stale.

Two contracts worth relaying to a user verbatim rather than paraphrasing: unknown is a real answer and its detail says why we cannot answer yet, and data_lag_s of null means NO EVIDENCE WAS OBSERVED - never "fresh". Report stale alongside any verdict you quote.

Omit question_id to list every registered question.

Inputs: question_id. Returns: object

aoi_imageryA

Building imagery for a location at two dates — set time + place directly.

Give a location (lat/lon + radius_km, OR an explicit min/max bbox) and a before + after date (YYYY-MM-DD). Returns what imagery is available for each date WITHOUT downloading: Maxar Open Data VHR (~0.3-0.5 m, event-gated — only where a disaster/conflict event covers the AOI) and Sentinel (10 m, global, any date). best_source says which to use. Reconstruction downloads the scenes to a temp dir on demand and discards them — nothing is stored.

Args: before, after: dates as YYYY-MM-DD. lat, lon, radius_km: centre + radius (km) of the AOI. min_lon/min_lat/max_lon/max_lat: explicit bbox (overrides lat/lon). window_days: ± days around each date to search Maxar (events are sparse).

Inputs: after (required), before (required), lat, lon, +6 more. Returns: object

approve_proposalA

Approve and EXECUTE a queued proposal (POST /api/actions/proposals/{id}/approve).

Only call this when the operator has told you to. The receipt carries the audit row the execution wrote. 404 for an unknown or expired proposal (the queue has a 15-minute TTL); 403 when the action is operator-only and the caller is not the operator; 401 on a multi-user deployment by design — see propose_action.

Inputs: proposal_id (required). Returns: object

area_baselineB

Is this normal? Current vessel / dark-vessel / jamming / military counts z-scored against a rolling baseline, with anomalies called out (e.g. "dark vessels +5σ", "traffic -3σ"). Global uses the background sampler; polling an AOI repeatedly builds that area's baseline. Distinguishes a real shift from a normal day.

Inputs: lat, lon, detail, radius_nm. Returns: object

aviation_sigmetC

AIRMET/SIGMET hazard polygons (turbulence, icing, IFR, volcanic ash, convective) constraining the aircraft layer.

Inputs: detail. Returns: object

cisa_kevC

CISA Known Exploited Vulnerabilities catalog — what's actively being exploited in the wild right now.

Inputs: detail. Returns: object

climate_anomaliesC

Temperature/precipitation anomalies (vs. a 5-year same-window baseline) over conflict-dense country centroids.

Inputs: detail. Returns: object

contact_provenanceA

Which sources are seeing the sky right now, and how much they agree.

Per tier: how many contacts it reported and how many ONLY it reported, plus the share of contacts with two or more independent observers and the confidence rule in force.

The exclusive count is the column that matters. Crowd-sourced ADS-B accepts whatever is uploaded to it, so a contact only one tier can see is not corroborated, and a tier contributing thousands of exclusive contacts is either unique coverage or an unverifiable claim. Say which when you report it, and never present an uncorroborated contact as confirmed.

Inputs: no named parameters. Returns: object

create_watch_ruleA

Create a standing watch rule (POST /api/alerts/rules).

A rule needs a gate: either an identity pin (icao24/mmsi/callsign — follows that one entity globally, no AOI required) or a complete AOI (lat, lon; radius_nm defaults to 50 nm if omitted). kinds filters which signal kinds fire it — jamming/dark_vessel/military_air/military_vessel/incident/quake/ fire/ais_gap/rendezvous/loiter (omit for all). channel is 'inapp'|'discord'|'webhook' ('discord'/'webhook' require sink_url). The route validates and 400/422s on a bad gate, channel, sink_url, or kind — this wrapper does not re-validate, it passes the error through.

Inputs: label (required), lat, lon, mmsi, +8 more. Returns: object

data_sourcesA

Which feeds are always-on vs key-gated, and the configured Ollama host/model. Use to explain coverage gaps (e.g. fires need a FIRMS key).

Inputs: no named parameters. Returns: object

deep_analyzeA

Have a reasoning model reason deeply over the live intel.

Gathers the relevant data (global situation, plus a focused area bundle + jamming + anomalies when lat/lon given) and feeds it to a real reasoning model — DeepSeek (deepseek-reasoner) when configured, else a local Ollama model. Heavy reasoning runs off-context; only the conclusion enters the agent's context. Falls back to returning the raw structured data (analysis=null) if no LLM backend is reachable.

Args: question: what to analyse (e.g. "Is there coordinated GPS jamming near the Baltic, and which aircraft are affected?"). lat, lon, radius_nm: optional area to focus on (loaded PRIMARY). model: optional Ollama model override used only on the Ollama fallback. tier: reason (deepseek-reasoner, default — judgement) or fast (deepseek-chat — quicker, shallower).

Inputs: question (required), lat, lon, tier, +2 more. Returns: object

deepstate_firmsC

FIRMS fire hotspots within the Ukrainian theater (DeepState).

Inputs: detail. Returns: object

deepstate_radiation_stationsC

Live radiation monitoring stations in Ukraine (DeepState, ~565 stations).

Inputs: detail. Returns: object

deepstate_war_newsC

Geolocated war-news events from the DeepState map.

Inputs: detail. Returns: object

delete_watch_ruleB

Delete a standing watch rule by id (DELETE /api/alerts/rules/{rule_id}).

Inputs: rule_id (required). Returns: object

detect_deceptionA

Denial & deception — "am I being fed?". Flags MANIPULATED tracks distinct from jamming: AIS duplicate-MMSI (one identity, two hulls) and impossible teleports; ADS-B GPS spoofing (many aircraft snapped to one false position) and kinematic position-injection. Run before trusting a feed in a contested area. Omit coords for global.

Inputs: lat, lon, detail, radius_nm. Returns: object

disaster_alertsB

GDACS severity-scored global disasters (earthquake/cyclone/flood/volcano/ drought/wildfire), each Green/Orange/Red by modelled impact.

Inputs: detail. Returns: object

displacementB

UN OCHA HAPI country-level IDP and refugee figures, latest reporting period per country.

Inputs: detail. Returns: object

esri_waybackC

ESRI Wayback time-travel manifest — every historical imagery version since 2014.

Inputs: no named parameters. Returns: object

fact_checkA

Adjudicate one free-text claim against current world-news headlines.

Returns {verdict: true|false|misleading|unverified, reasoning, supporting_sources, confidence}. Use it to sanity-check a statement before treating it as fact.

Inputs: claim (required). Returns: object

fema_disastersC

Recent FEMA disaster declarations across US states and territories.

Inputs: detail. Returns: object

fire_perimetersA

Active wildfire burn-area polygons (NIFC/WFIGS) — the AREA behind FIRMS hotspot points, with incident name + size in acres.

Inputs: detail. Returns: object

focus_areaA

Load a region PRIMARY and return a full intel bundle for it in one call.

This is the headline tool. It triggers a DEDICATED, always-fresh fetch for just this area (bypassing global rate limits) and registers it for ongoing priority refresh. The bundle contains: fresh aircraft summary + sample, aircraft-density grid, GPS-jamming assessment, vessels, and fused anomalies (emergencies, jamming hotspots, dark vessels, alerts) with a threat level.

Args: lat, lon: centre of the area of interest. Required unless polygon is given, in which case they are ignored. radius_nm: radius in nautical miles (1–250; default 200). label: optional human name for the AOI (e.g. "Kaliningrad"). cell_deg: density grid cell size in degrees (0.1–10; default 1.0). polygon: optional irregular region as ONE ring of [lon, lat] pairs (longitude FIRST; 3–500 vertices, closed or open; no holes, no antimeridian crossing). Overrides lat/lon/radius_nm: its bounding circle (max 250 nm, else polygon_truncated) is loaded PRIMARY and every record outside the ring is dropped. Grid cells and jamming hexes are kept by centre point, so use a small cell_deg for a small ring. A total the capped samples cannot give for the ring comes back as _in_circle (the bounding circle's figure).

Inputs: lat, lon, label, detail, +3 more. Returns: object

gdelt_articlesC

GDELT DOC 2 API — article-level search across global news.

Inputs: q. Returns: object

get_situationA

Global situational snapshot — the cheap first call to orient.

Returns total aircraft (airborne/ground, by category), GNSS-degraded count, active emergency squawks, the worst GPS-jamming cells worldwide, tracked vessel counts by category, and recent fusion-alert counts. A few hundred tokens describing the whole planet.

detail='short' (default) trims the jamming/vessel/alert samples to their top few; detail='long' returns the full situation bundle.

Inputs: detail. Returns: object

gleif_lookupC

Look up a company's Legal Entity Identifier (LEI) via the GLEIF API.

Inputs: q (required). Returns: object

gps_jammingA

GPS/GNSS jamming assessment (GPSJam method: ADS-B NACp<8 / NIC<7 binned into 1° cells). Returns flagged cells ranked by severity, counts of high/medium cells, and a sample of affected aircraft. Omit all coordinates for a global view; otherwise pass a centre or a bbox to scope it.

Inputs: lat, lon, detail, max_lat, +4 more. Returns: object

hdx_datasetsC

Search humanitarian datasets on HDX (data.humdata.org).

Inputs: q. Returns: object

history_diffA

What changed inside a box between two moments: arrived, departed, stayed.

Answers from OWNED history, so it works for any past window still retained rather than only the present instant. at_a and at_b are epoch seconds (at_b defaults to now); each is widened by window_sec because a stored position is a sample rather than a continuous truth.

recorded: false means nothing was in the archive for that box and window - which is NOT the same as "nothing changed". Do not report an empty archive as a quiet period.

Inputs: at_a (required), lamax (required), lamin (required), lomax (required), +4 more. Returns: object

incident_historyA

Timeline of how each incident built up over the recent window — per incident, a compact series of [time, threat_level, score] points. Reveals sequence (e.g. jamming first, then dark vessels, then a reported event). Global uses the background watch history; an AOI uses your prior watch calls.

The full window can hold many incidents; this returns the limit most active ones (default 25, alias max_incidents) with each series downsampled to its threat-level transitions, and reports incident_count / returned / truncated so you know if more exist. Raise limit or narrow with lat/lon/hours to drill in.

Inputs: lat, lon, hours, limit, +3 more. Returns: object

intel_briefA

Cross-domain INCIDENT brief — the headline analytic tool.

Fuses the raw layers into ranked, cited INCIDENTS instead of returning signals you have to correlate yourself. An incident is a CONVERGENCE: signals within link_km across >=2 domains (GPS-jamming, dark/AIS-off vessels, military air, AIS gaps, emergencies, geocoded events, quakes) — or a single critical/high signal. Each incident carries a rule-based narrative, a threat_level, the contributing evidence (with IDs), and recommended follow-up queries. Omit coordinates for a global brief; pass a centre+radius or bbox to scope it. Start here, then drill into an incident's centroid with query_vessels / gps_jamming / deep_analyze.

Inputs: lat, lon, detail, link_km, +6 more. Returns: object

kiwisdr_stationsC

Map of public KiwiSDR software-defined radio receivers worldwide.

Inputs: detail. Returns: object

list_focus_areasA

List the priority areas currently loaded PRIMARY (with fetch stats and whether each is served by a dedicated fetch or the degraded snapshot fallback).

Inputs: no named parameters. Returns: object

list_proposalsA

Pending action proposals awaiting operator approval, oldest first (GET /api/actions/proposals). 401 on a multi-user deployment by design — see propose_action.

Inputs: no named parameters. Returns: object

list_watch_rulesC

List your standing watch rules (GET /api/alerts/rules).

Inputs: detail. Returns: object

locate_emitterA

Estimate a GPS jammer/spoofer LOCATION from the degraded-ADS-B footprint (severity-weighted centroid + CEP + confidence). Turns "jamming somewhere here" into "emitter ~here ±N km". Footprint-centroid estimate (~tens of km), not RF direction-finding — stated in the response. Scope with lat/lon.

Inputs: lat, lon, detail, radius_nm. Returns: object

lookup_aircraftA

Look up one aircraft by ICAO24 hex (exact) or callsign (substring). Returns its current state, registration, an integrity/threat assessment, and the latest server-held fix.

Inputs: ident (required). Returns: object

marine_buoysC

NDBC marine buoy observations — wave height, wind, pressure, water temp.

Inputs: detail. Returns: object

maritime_chokepointsB

Vessel congestion at named straits (Hormuz/Suez/Malacca/Bosphorus/…), derived live from the AIS union: vessel + stationary counts per chokepoint.

Inputs: detail. Returns: object

maritime_coverageA

Where keyless AIS actually reaches: a dense 36x72 grid of live-vessel counts in 5 degree cells (row 0 = 90S..85S, col 0 = 180W..175W). A zero cell means no feed reports there, not that the sea is empty. Returned whole; a trimmed grid would be unreadable.

Inputs: max_age_s. Returns: object

maritime_pipeline_statsA

Health of the keyless AIS pipeline: vessels per source, the OpenWaters sweep (tiles failed/truncated, whether it completed), fix-age percentiles, and the standing note that keyless AIS is coastal, not open-ocean.

Inputs: no named parameters. Returns: object

maritime_satellite_positionsA

Unidentified satellite AIS positions: where ships are in OPEN OCEAN, beyond coastal AIS. Each is a position, a type class and a heading only; upstream publishes no MMSI, name or position age for them, so do not present one as a tracked, identified vessel. bbox = lon0,lat0,lon1,lat1 (optional). count is the true total even when the list is trimmed.

Inputs: bbox, detail. Returns: object

maritime_vessels_bboxB

Vessels in a box from the freshness-wins AIS store, freshest first, each with its fix age and every source that reported it. bbox = lon0,lat0,lon1,lat1 (lon0 > lon1 crosses the antimeridian). count is the true number in the box even when the list is trimmed.

Inputs: bbox (required), limit, detail, max_age_s. Returns: object

markets_snapshotB

Batched keyless market quote snapshot: indices, commodities, fx, crypto.

Inputs: detail. Returns: object

market_stressC

Composite market-stress score from equity drawdown, gold/oil moves, USD flight-to-safety, and BTC drawdown.

Inputs: detail. Returns: object

mineral_sitesA

USGS MRDS mineral deposit sites inside a bbox.

bbox is REQUIRED for data (lon_min,lat_min,lon_max,lat_max): the whole catalogue is ~300k sites, so with no viewport this answers an empty collection. commodity matches the MRDS CODE ("CU", "AU"), not a name.

Inputs: bbox, detail, commodity. Returns: object

ms_global_buildingsB

Microsoft Global Buildings manifest — 1.3 billion footprint polygons, download links per quadkey region.

Inputs: no named parameters. Returns: object

nas_statusB

FAA National Airspace System ground stops, ground delays, arrival/ departure delays, and airport closures, as points at the affected airport.

Inputs: detail. Returns: object

news_analysisA

Cross-source, debiased world-news intelligence.

Scrapes ~12 outlets (BBC, Reuters, AP, Al Jazeera, Guardian, CNN, Fox, …), then a reasoning model strips bias/propaganda and separates VERIFIED FACTS (corroborated by ≥2 independent outlets) from ATTRIBUTED CLAIMS and rhetoric. A leader promising "the war will end soon" is flagged as rhetoric, never reported as fact. Returns events with neutral_summary, verified_facts, attributed_claims, bias_flags, propaganda_techniques, rhetoric_flags and a confidence. May take ~30s when the cache is cold (it reasons over the feed).

Inputs: no named parameters. Returns: object

news_briefA

Latest assembled morning brief: top story per category plus a short synthesis paragraph tying the leads together. Built on a ~20h cadence by the background news refresher, not on-demand — use news_analysis or the edition route for the freshest per-story detail.

Inputs: detail. Returns: object

osm_military_objectsC

Military objects from OpenStreetMap via Overpass API. bbox = south,west,north,east.

Inputs: bbox (required), detail. Returns: object

power_plantsB

WRI global power plants (≥200 MW) — name, fuel, capacity, country. Static energy-infra targets to correlate strikes/outages against.

Inputs: detail. Returns: object

propose_actionA

Queue a governed action for the operator to approve (POST /api/actions/proposals).

This does NOT run the action. name is one of the registered actions (GET /api/actions lists them and their params): flag_entity, promote_incident, nominate_target, add_watch, writeback. params is that action's own param object — validated here, so a malformed proposal is rejected now rather than at approval time. confidence (0-1) is your own reported confidence and is recorded with the proposal.

writeback writes a record OUT to a system the operator runs (an allow-listed HTTP endpoint, or a Foundry sql connection) and is operator-only: send dry_run: true first, which validates the target and the SSRF policy without sending anything.

Multi-user deployments: this server authenticates with an INTERNAL token that is deliberately never a user (apps/api/CLAUDE.md, Auth), so on a Supabase deployment these four tools answer 401 by design — a write-back has to be attributable to a person, and the internal token is not one. They are for the single-operator box, where the operator IS the one user.

Inputs: name (required), params, confidence. Returns: object

quakes_nearA

Earthquakes (USGS) within radius_km of a point (GET /api/eq).

lat, lon, and radius_km must ALL be given together — the route 422s on a partial set rather than silently falling back to the unfiltered global feed (agent-safe: pass all three or none). range is 'hour'|'day' (default)|'week'|'month'. For a severity-scored global disaster view instead, use disaster_alerts().

Inputs: lat (required), lon (required), radius_km (required), range, +1 more. Returns: object

query_aircraftA

Filtered aircraft query against the live snapshot. Returns matched_total

  • a capped list of compact records.

category ∈ airliner|private|helicopter|glider|military|emergency. Combine with area (centre or bbox), squawk, callsign_contains, altitude band (metres), emergency / gnss_degraded / on_ground booleans. limit ≤200.

Inputs: lat, lon, limit, detail, +13 more. Returns: object

query_vesselsB

Vessels (AIS) in an area, classified (cargo/tanker/fishing/passenger/ military/sailing/pleasure/tug). dark_only=True returns only dark-vessel candidates (moving with no static identity). Scoped by centre or bbox.

Inputs: lat, lon, limit, detail, +6 more. Returns: object

radiationC

Recent Safecast crowd radiation measurements (value + unit + location) — a nuclear-incident detection layer.

Inputs: detail. Returns: object

radio_reportsA

Amateur-radio reception reports from PSKReporter, plotted at the transmitter.

Each feature is an emitter that an independent receiving station decoded in the last minutes, carrying callsign, Maidenhead grid, band, mode and SNR. Positions are grid-square CENTRES, so read properties.precision_km before treating one as a location.

Inputs: detail, minutes. Returns: object

reject_proposalA

Drop a queued proposal without executing it (POST /api/actions/proposals/{id}/reject). 401 on a multi-user deployment by design — see propose_action.

Inputs: proposal_id (required). Returns: object

relief_disastersC

ReliefWeb active humanitarian disasters, geocoded to the affected country.

Inputs: detail. Returns: object

sar_scenesA

Recent keyless Sentinel-1 SAR scenes over a box, for water AIS does not reach. bbox = lon0,lat0,lon1,lat1. By default a SCENE INVENTORY (id, time, footprint, public download URL). detect=True also returns LOCATED radar contacts for a box up to one degree a side, each classified against AIS: 'dark' only where enough nearby AIS makes absence meaningful, otherwise 'unknown'. Read note: it says when detection did not run and why.

Inputs: bbox (required), days, limit, detail, +1 more. Returns: object

satnogs_ground_stationsC

Map of active SatNOGS ground stations worldwide.

Inputs: detail. Returns: object

satnogs_observationsC

Recent satellite downlink observations from the SatNOGS ground station network — real decoded telemetry from space.

Inputs: detail. Returns: object

satnogs_transmittersB

Known satellite transmitter frequencies from the SatNOGS database. Pass a NORAD catalog ID to filter to one satellite.

Inputs: detail, satellite. Returns: object

shodan_ipA

Shodan InternetDB lookup for one IPv4 address — ports, vulns, hostnames. NO API key needed.

Inputs: ip (required). Returns: object

source_catalogB

Master catalog of all data sources the platform knows about — tile providers, SAR archives, SDR directories, splat sources, streaming endpoints, and more. Filter by category.

Inputs: category. Returns: object

space_weatherB

SWPC space weather — solar X-ray flares, active alerts, and the auroral oval. The causal layer under GPS/HF degradation.

Inputs: detail. Returns: object

spc_storm_reportsC

Today's SPC severe weather reports — tornado, wind, and hail events.

Inputs: detail. Returns: object

system_doctorA

What is configured, what is missing, and the exact line that fixes it.

Use this when a layer looks empty before concluding the world is quiet: a blank layer is far more often an unconfigured optional source than an absence of activity. Every capability listed is optional - the console runs keyless - so a long problems list is not a fault list. Never report a missing optional key as a defect.

Inputs: no named parameters. Returns: object

telegram_channelC

Scrape recent messages from a Telegram OSINT channel.

Inputs: channel. Returns: object

tinygs_packetsC

Recent LoRa satellite packets from the tinyGS community network.

Inputs: detail. Returns: object

track_historyB

Historical position track for ONE entity (GET /api/history/track).

id is 'aircraft:' / 'vessel:', or a bare id whose shape is unambiguous (6-char ICAO24 hex or 9-digit MMSI) — the route infers the kind from it. from_ts/to_ts are unix seconds (default: the last hour). A id the route can't resolve returns its 422 message as-is (e.g. ambiguous shape — pass a 'kind:id' prefix).

Inputs: id (required), to_ts, detail, from_ts. Returns: object

travel_advisoriesC

Official country-level travel advisories pooled from US State, UK FCDO, and Australia Smartraveller — each country's highest reported level.

Inputs: detail. Returns: object

tropical_cyclonesB

Active tropical cyclones (NHC) with center fix, classification, intensity and pressure.

Inputs: detail. Returns: object

ukraine_alertsC

Live air-raid alert status across Ukraine's 25 oblasts (alerts.com.ua).

Inputs: detail. Returns: object

unhcr_refugeesC

UNHCR refugee/IDP population figures by country.

Inputs: year. Returns: object

vessel_dossierA

Pattern-of-life dossier for one vessel (MMSI): recent track, AIS gaps, derived speed profile (loiter / transit / loiter-then-dash), area covered, which live incidents it appears in, and a behaviour assessment. The track is the store's ~1h retention window.

Inputs: mmsi (required), detail. Returns: object

volcanoesB

Smithsonian GVP Holocene volcanoes — location, type, elevation, last eruption year.

Inputs: detail. Returns: object

weather_sondesC

Live weather balloon (radiosonde) positions worldwide from SondeHub.

Inputs: detail. Returns: object

whats_changedA

Standing watch — what CHANGED since the last check, not the full picture.

Returns incidents that are NEW, ESCALATED, DE-ESCALATED, or RESOLVED. Global (no coords) reflects the background watch loop (recomputed ~every 60s); an AOI (lat/lon) diffs against YOUR previous whats_changed call for that area, so you can poll one region and be told only what moved. Use this to monitor instead of re-reading the whole brief each time.

Inputs: lat, lon, detail, radius_nm. Returns: object

worldpopC

WorldPop population grid datasets metadata. Filter by ISO3 country.

Inputs: iso3. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.8/5.0

    Scored across 94 tools

    Disambiguation2/5

    The tool set contains many overlapping analytic entry points: get_situation, focus_area, anomalies, intel_brief, area_baseline, whats_changed, deep_analyze, and answer all return situation/intel conclusions with subtle boundaries. Aircraft and vessel access is also split across several near-equivalent tools (query_aircraft/lookup_aircraft/adsb_by_*/fr24_search; query_vessels/maritime_vessels_bbox/maritime_satellite_positions), making misselection likely.

    Naming Consistency3/5

    Almost all names are lowercase snake_case, which is consistent at the formatting level. However, the naming convention itself is mixed: some tools use verb_noun (query_aircraft, create_watch_rule, detect_deception), while many are bare nouns or domain labels (anomalies, intel_brief, gps_jamming, tinygs_packets, weather_sondes).

    Tool Count1/5

    94 tools is an extreme surface for a single MCP server, far beyond the 3-15 well-scoped range. While the platform covers many data domains, the count is so large that agents must navigate a sprawling catalogue rather than a focused tool set.

    Completeness4/5

    The surface covers an unusually broad OSINT/geospatial domain: situation awareness, aircraft/vessel queries, anomalies, history, watch rules, proposals, imagery, disaster feeds, cyber feeds, and many specialist sources. Minor lifecycle gaps exist, such as no update_watch_rule, but core workflows and many optional layers are represented.

    Maintenance

    ActivityMaintained
    ResponsivenessUsually handled

    Related MCP Connectors

    Related MCP Servers