
Open source Baltic Sea shadow fleet tracker. 1200+ vessels, live AIS, cable proximity alerts. No cloud, no subscription, runs locally
Baltic Sea shadow fleet monitoring via live AIS data. Watches 1200+ vessels from the Ukrainian GUR War&Sanctions catalogue against the AISStream WebSocket feed, plots positions on a self-updating map, flags proximity to undersea cables, and detects Russia↔West transshipment patterns.
Free, open source, runs locally. No cloud, no subscription beyond a free AISStream API key.
Part of the Former Lab sovereign intelligence toolchain.
Shadow Fleet Tracker Light is built and maintained by the Former Lab team — sovereign computing, building open, privacy-first tools on old hardware with no VC backing.
If this tool is useful to you, consider supporting on Patreon. A free tier is available, with a 7-day trial on paid tiers. Always support Ukraine!
git clone https://github.com/FormerLab/shadow-fleet-tracker-light.git
cd shadow-fleet-tracker-light
pip install -r requirements.txt
export AISSTREAM_API_KEY=your_key_here # free at aisstream.io
python shadow_tracker.py &
uvicorn webserver:app --host 0.0.0.0 --port 8000
Open http://localhost:8000 — the map is at /map.
Windows users: see the Setup section for PowerShell/cmd environment variable syntax.
Linux / macOS users can use start.sh. Windows users can use start.bat or start.ps1 — both open tracker and dashboard in separate windows and launch the browser automatically.
Vessels1.db)filtered_cables.kml)Dark CartoDB basemap with cable routes overlaid in green. Vessel markers update every 3 minutes with a live countdown to the next refresh — the page auto-reloads. Popups show MMSI, IMO, flag, speed, heading, destination, and deep links to MarineTraffic, VesselFinder, and War&Sanctions. OpenSanctions sanctions status shown if an API key is configured.
Known vessel names from Vessels1.db are shown immediately on first appearance without waiting for an AIS static message.
On startup the map is pre-populated from the last 24 hours of the position log. Vessels not yet seen in the current session are shown as grey markers with a "last seen Xh ago" label — they transition to live colour as new pings arrive.
shadow_tracker.py Main process — WS consumer, map renderer, DB writes
loitering_module.py Loitering detection and map annotation
transshipment_module.py Port call detection and Russia↔West transshipment flagging
webserver.py FastAPI dashboard
gur_scrape.py One-shot scraper — builds IMO→GUR-ID mapping + full vessel catalogue
check.py Preflight checks — Python version, deps, API key, network
start.sh Launcher for Linux / macOS
start.bat Launcher for Windows (Command Prompt)
start.ps1 Launcher for Windows (PowerShell)
requirements.txt Dependencies
Vessels1.db Watchlist — 1200+ vessels (MMSI + IMO + name where known)
vessel_data_log.db Position log — runtime, append-only
vessel_static.db AIS static data cache (name, destination, flag history)
transshipment.db Port call log and transshipment events
loitering_events.db Loitering events
filtered_cables.kml Baltic Sea undersea cable geometry
cables.kml Full cable dataset (pre-filter source)
gur_mapping.json IMO→GUR-ID mapping (produced by gur_scrape.py, optional)
gur_vessels_full.json Full GUR catalogue — GUR-ID→{imo, mmsi, name, flag} (produced by gur_scrape.py)
Vessels1.db is the canonical list of tracked vessels. 1200+ entries sourced from the Ukrainian GUR War&Sanctions catalogue, covering tankers and cargo vessels identified as part of the Russian shadow fleet or implicated in Baltic hybrid activity.
The list includes vessels recently seized or intercepted by Baltic authorities:
| Vessel | IMO | Event |
|---|---|---|
| EAGLE S | 9329760 | Finland seized Dec 2024 — Estlink-2 cable sabotage, spy equipment found |
| EVENTIN | 9308065 | Germany seized Jan–Mar 2025 — drifted off Rügen, 100,000t Russian crude confiscated |
| KIWALA | 9332810 | Estonia detained Apr 2025 — flagless, EU/UK sanctioned |
| JAGUAR | 9293002 | Estonia intercepted May 2025 — Russia scrambled Su-35 to escort it |
| KIRA K | 9346720 | Wagner/GRU crew confirmed aboard Dec 2025 |
| QENDIL | 9310525 | Wagner/GRU crew Sep 2025 — Ukrainian drone strike Dec 2025 |
| FITBURG | 9250397 | Finland seized 31 Dec 2025 — Helsinki-Tallinn cable sabotage |
| CAFFA | 9143611 | Sweden seized 6 Mar 2026 — stolen Ukrainian grain, false flag |
| SEA OWL I | 9321172 | Sweden seized 12 Mar 2026 — EU sanctioned, false Comoros flag |
MMSIs are corrected against current AIS data — shadow fleet vessels reflag frequently. The update_vessels_2026_03.sql file documents all changes with sources.
The watchlist is intentionally open. Add vessels directly to Vessels1.db while the tracker is running — they will be picked up within 5 minutes without a restart.
Linux / macOS:
sqlite3 Vessels1.db "INSERT OR IGNORE INTO vessels (mmsi, imo, name) VALUES ('123456789', '9999999', 'VESSEL NAME');"
Windows (no sqlite3 CLI needed):
import sqlite3
conn = sqlite3.connect("Vessels1.db")
conn.execute("INSERT OR IGNORE INTO vessels (mmsi, imo, name) VALUES ('123456789', '9999999', 'VESSEL NAME')")
conn.commit()
conn.close()
Vessels1.db — watchlist
vessels(mmsi TEXT PK, imo TEXT, name TEXT, destination TEXT)
vessel_data_log.db — position log
vessel_data_log(timestamp, mmsi, name, imo, destination, speed, heading, latitude, longitude, cable_alert)
Append-only. Written on every map render cycle.
vessel_static.db — AIS static data cache
vessel_static(mmsi TEXT PK, name TEXT, imo TEXT, destination TEXT, flag TEXT)
flag_history(id, mmsi, flag, timestamp)
Populated from live ShipStaticData AIS messages. Takes priority over Vessels1.db names once received. flag_history records every flag change with a timestamp — reflagging events are logged and displayed on the per-vessel page.
loitering_events.db — loitering log (created at runtime)
loitering_events(id, mmsi, timestamp, latitude, longitude, near_cable)
transshipment.db — port call and transshipment log (created at runtime)
port_calls(id, mmsi, name, port, port_type, entry_ts, exit_ts, min_speed)
transshipment_events(id, mmsi, name, direction, from_port, from_exit_ts,
to_port, to_entry_ts, days_between, detected_ts)