World Monitor gathers public signals about the state of the world into one screen. Curated news feeds from global and regional outlets are summarised into AI briefs, and a 3D globe (globe.gl) or a WebGL flat map (deck.gl) carries layers for conflict, military flights, shipping chokepoints, cyber threats, climate events, energy and infrastructure. Panels track stock exchanges, commodities, crypto and prediction markets, and a Country Instability Index scores 31 countries with approximate 24-hour movement. The same codebase builds tech, finance, commodity, energy and good-news variants, a Tauri desktop app, a terminal dashboard (npx worldmonitor), SDKs and an MCP server for AI agents.
It comes from Elie Habib and grew unusually fast: created in January 2026, it has about 89,700 stars, 171 contributors, more than 9,000 commits and 50 releases, the latest (v2.11.6) on 25 October 2026, with commits landing daily. The hosted site at worldmonitor.app is free to browse, and a Pro plan sells API keys for the REST API, CLI and MCP tools.
The code is AGPL-3.0. You can self-host, fork and use it commercially, but if you run a modified copy as a service you have to publish your changes; the README offers a separate commercial licence for teams that cannot. The app starts with no environment variables, and AI summaries can run on a local Ollama model, so API keys are only needed for the feeds that require them.
- Repository: github.com/koala73/worldmonitor
- Licence: AGPL-3.0 (GNU Affero General Public License v3.0)
- Language: TypeScript. Stars: 89.7K. Forks: 13.8K. Last push: Oct 28, 2026.
- Scan: safe, Oct 28, 2026, commit 91c50b1
Who it is for
Analysts, journalists and researchers who follow geopolitics or markets and want one situational view, OSINT hobbyists, and developers who want a large, well-tested open-source dashboard or an agent-ready source of world data.
Getting started
1. Try the terminal dashboard without installing anything (no key needed)
npx worldmonitor2. Or clone the web app (Node.js and npm)
git clone https://github.com/koala73/worldmonitor.git && cd worldmonitor3. Install the dependencies
npm install4. Start it, then open http://localhost:3000
npm run devNothing is required to start; some feeds need their own credentials, listed in .env.example. A self-hosted copy still loads its analytics script from abacus.worldmonitor.app, although the tracker only records visits on the official worldmonitor.app hostnames.
Safety scan
We cloned koala73/worldmonitor at commit 91c50b1 on Oct 28, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- A large codebase: 8,945 files and about 2.4 million lines, mostly JavaScript and TypeScript. The one secret hit is a fake ghp_ token in tests/railway-reconcile-manifest.test.mjs, written to prove that token-like names are rejected. The 37 bare-IP URLs are all tests using documentation and reserved ranges (TEST-NET, 93.184.216.34) to prove the webhook and proxy guards refuse them.
- The four api.telegram.org hits are the alert feature sending to the bot token you configure, and the curl | sh hit is a hint message in the build prerequisite check suggesting rustup. The root postinstall hook runs a local script that writes an inventory facts file, with no network access; src-tauri/build.rs is the standard one-line Tauri build script.
- 3,657 locked packages across 12 lockfiles, with 24 known advisories: none critical, and the only two high ones (mysql2 and deepmerge-ts) are in docker/umami/runtime, the project's own analytics server, not the dashboard. The main package-lock has five moderate or low; most of the Rust desktop findings are notices that old crates are unmaintained.
- Telemetry, read from the source: Sentry error reporting is off unless you set VITE_SENTRY_DSN (empty in .env.example), Umami is limited to the worldmonitor.app hostnames, and Vercel Web Analytics only reports on a Vercel deployment. The network hosts are the news, market and data sources the panels read.
- 53 workflows. contributor-trust.yml uses pull_request_target, but the scanner's checkout match is a warning comment: the job never checks out PR code, it only looks up the author's name and adds a label. All 19 third-party actions are pinned to commits. Security policy, Dependabot, CodeQL, licence, contributing guide and code of conduct all present.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | 1 candidate found and read; see the notes above. |
| Suspicious code | 5 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 2 npm lifecycle scripts. 1 Cargo build script. 2 installer scripts |
| Committed binaries | None. |
| CI workflows | 53 workflows. 1 uses pull_request_target, 1 of which check out the pull request head. 0 of 19 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often worldmonitor.app, www.worldmonitor.app, news.google.com, api.worldmonitor.app. 37 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 24 advisories across 3,657 pinned packages: 0 critical, 2 high, 11 moderate, 1 low, 10 unrated. blog-site/package-lock.json: 329 packages, 0 advisories; consumer-prices-core/package-lock.json: 236 packages, 0 advisories; demos/verification-desk/package-lock.json: 49 packages, 0 advisories; docker/umami/runtime/pnpm-lock.yaml: 252 packages, 6 advisories; integrations/zapier/package-lock.json: 940 packages, 0 advisories; package-lock.json: 1,795 packages, 5 advisories; pro-test/package-lock.json: 603 packages, 5 advisories; scripts/package-lock.json: 342 packages, 2 advisories; sdk/rust/Cargo.lock: 132 packages, 1 advisories; src-tauri/Cargo.lock: 525 packages, 11 advisories; workers/api-cors-preflight/package-lock.json: 91 packages, 0 advisories; workers/railway-reconcile-control/package-lock.json: 91 packages, 0 advisories. |
| Project hygiene | Has security policy, automated dependency updates, CodeQL, licence file, contributing guide. |
| OpenSSF Scorecard | Not scored: the project is not in Scorecard's weekly index. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Secret candidates (1, redacted)
| Where | Rule | Match |
|---|---|---|
| tests/railway-reconcile-manifest.test.mjs:132 | github-token | ghp_ab…789 (40 chars) |
Pattern hits (5)
| Where | Rule | Match |
|---|---|---|
| convex/http.ts:734 | exfil-host | await fetch(`https://api.telegram.org/bot${botToken}/${method}`, { |
| scripts/check-build-prereqs.mjs:95 | download-piped-to-shell | hint: "Install via rustup: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh", |
| scripts/notification-relay.cjs:596 | exfil-host | const res = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, { |
| scripts/seed-digest-notifications.mjs:1206 | exfil-host | `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMediaGroup`, |
| scripts/seed-digest-notifications.mjs:1234 | exfil-host | `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, |
URLs to bare IP addresses (37)
| Where | Rule | Match |
|---|---|---|
| server/__tests__/shipping-register-user-key.test.ts:114 | ip-literal-url | const payload = { callbackUrl: 'https://93.184.216.34/hook', chokepointIds: ['suez'], alertThreshold: 50 }; |
| server/__tests__/shipping-registration-storage.test.ts:58 | ip-literal-url | const payload = { callbackUrl: 'https://93.184.216.34/hook', chokepointIds: ['suez'], alertThreshold: 50 }; |
| src-tauri/sidecar/local-api-server.test.mjs:2101 | ip-literal-url | 'http://100.64.0.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2102 | ip-literal-url | 'http://198.18.0.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2103 | ip-literal-url | 'http://192.0.0.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2104 | ip-literal-url | 'http://192.0.2.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2105 | ip-literal-url | 'http://192.88.99.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2106 | ip-literal-url | 'http://198.51.100.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2107 | ip-literal-url | 'http://203.0.113.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2108 | ip-literal-url | 'http://240.0.0.1/secret', |
| src-tauri/sidecar/local-api-server.test.mjs:2216 | ip-literal-url | const upstream = await fetch('https://93.184.216.34/data'); |
| tests/notification-teams.test.mjs:220 | ip-literal-url | const result = await teams.sendTeamsPayload('https://20.42.65.92/workflows/x/triggers/manual/paths/invoke', {}, { resolveHostname: resolvePublic }); |
| tests/seed-cyber-provider-profiles.test.mjs:203 | ip-literal-url | urls: [{ url: 'https://192.0.2.44/payload', country: 'FR', date_added: new Date(NOW - 90 * DAY).toISOString() }], |
| tests/shipping-v2-handler.test.mjs:211 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:258 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:273 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:284 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:295 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:327 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:346 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:361 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:372 | ip-literal-url | callbackUrl: 'https://93.184.216.34/wm', |
| tests/shipping-v2-handler.test.mjs:383 | ip-literal-url | await registerWebhook(proCtx(), { callbackUrl: 'https://93.184.216.34/wm', chokepointIds: [], countries: [] }); |
| tests/shipping-v2-handler.test.mjs:393 | ip-literal-url | () => registerWebhook(proCtx(), { callbackUrl: 'https://93.184.216.34/wm', chokepointIds: [], countries: [], eventType: 'country.coup' }), |
| and 13 more | ||
npm lifecycle scripts (2)
demos/verification-desk/package.jsonpostinstall:node scripts/build-avatar.mjs && node scripts/install-deck.mjspackage.jsonpostinstall:npm run inventory:facts
Installer scripts (2)
- .github/scripts/deploy-gate.sh, 615 lines
- scripts/run-seeders.sh, 138 lines
Worst known vulnerabilities (24 of 24)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-ggr8-5vv4-36mx | high | deepmerge-ts@7.1.5 | DeepmergeTS has stack exhaustion when merging recursive object graphs |
| GHSA-3f6p-5ww8-9rcr | high | mysql2@3.15.3 | MySQL2: Auth Plugin Downgrade to mysql_clear_password Leaks Plaintext Credentials |
| GHSA-92pp-h63x-v22m | moderate | @hono/node-server@1.19.11 | @hono/node-server: Middleware bypass via repeated slashes in serveStatic |
| GHSA-frvp-7c67-39w9 | moderate | @hono/node-server@1.19.11 | Node.js Adapter for Hono: Path traversal in `serve-static` on Windows via encoded backslash (`%5C`) |
| GHSA-rgwj-5xj2-c3m3 | moderate | mysql2@3.15.3 | MySQL2: Unbounded zlib inflate in compressed MySQL protocol handler allows decompression-bomb DoS |
| GHSA-5qjj-4xww-7phc | moderate | valibot@1.2.0 | Valibot: record() issue paths can make flatten() throw for inherited Object property names |
| GHSA-528h-pc64-c93x | moderate | stream-json@1.9.2-worldmonitor.1 | stream-json: pick/ignore/filter/replace filters are O(depth²) on nested input - small crafted JSON blocks the event loop… |
| GHSA-hqr4-qq8f-hg3x | moderate | stream-json@1.9.2-worldmonitor.1 | stream-json: JSONC parser and verifier re-scan the whole accumulated comment on every input chunk |
| GHSA-mjw6-4jj6-33hc | moderate | stream-json@1.9.2-worldmonitor.1 | stream-json has a prototype pollution issue: Assembler writes this.current[this.key] on plain objects |
| GHSA-w5hq-g745-h8pq | moderate | uuid@8.3.2 | uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided |
| GHSA-w5hq-g745-h8pq | moderate | uuid@11.1.0 | uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided |
| GHSA-qwgh-2vcv-g2f7 | moderate | block-buffer@0.10.4 | block_buffer: panic corrupts inline buffer position |
| GHSA-wrw7-89jp-8q8g | moderate | glib@0.18.5 | Unsoundness in `Iterator` and `DoubleEndedIterator` impls for `glib::VariantStrIter` |
| GHSA-gcqx-w48m-w2h4 | low | deepmerge@4.3.1 | deepmerge mergeObject() allows prototype poisoning of returned objects |
| CVE-2026-111943 | unknown | rustls-webpki@0.103.15 | Certificate verification may accept distrusted intermediates |
| RUSTSEC-2024-0370 | unknown | proc-macro-error@1.0.4 | proc-macro-error is unmaintained |
| RUSTSEC-2025-0081 | unknown | unic-char-property@0.9.0 | `unic-char-property` is unmaintained |
| RUSTSEC-2025-0075 | unknown | unic-char-range@0.9.0 | `unic-char-range` is unmaintained |
| RUSTSEC-2025-0080 | unknown | unic-common@0.9.0 | `unic-common` is unmaintained |
| RUSTSEC-2025-0100 | unknown | unic-ucd-ident@0.9.0 | `unic-ucd-ident` is unmaintained |
| RUSTSEC-2025-0098 | unknown | unic-ucd-version@0.9.0 | `unic-ucd-version` is unmaintained |
| RUSTSEC-2026-0371 | unknown | zbus_names@3.0.0 | `zbus_names` is unmaintained |
| RUSTSEC-2026-0370 | unknown | zvariant@4.2.0 | `zvariant` is unmaintained |
| RUSTSEC-2026-0372 | unknown | zvariant_derive@4.2.0 | `zvariant_derive` is unmaintained |
Workflows worth a look
- .github/workflows/contributor-trust.yml: pull_request_target, checks out the PR head
By the numbers
| Stars | 89.7K |
|---|---|
| Forks | 13.8K |
| Contributors | 171 |
| Commits | 9,005 |
| Open issues | 240 |
| Open pull requests | 103 |
| Releases | 50 |
| Latest release | v2.11.6 |
| Licence | AGPL-3.0 |
| Main language | TypeScript |
| Project age | 9 months |
| Last push | Oct 28, 2026 |
| Tracked files | 8,945 |
| Lines of code | 2.4M |
| Checkout size | 182 MB |
Lines by language: JavaScript 913.9K, TypeScript 636.8K, JSON 520.3K, Markdown 159.4K, YAML 83.7K, CSS 40.4K.
Questions
Is World Monitor free?
Yes. The website and its variants are free to browse, the desktop app and terminal dashboard are free downloads, and the source is AGPL-3.0, so self-hosting costs nothing beyond your own server. A paid Pro plan sells API keys for the REST API, CLI and MCP server, and a commercial licence exists for teams that cannot meet the AGPL's terms.
Where does World Monitor's data come from?
From public and attributed upstream sources: news outlets and Google News, official statistics such as the World Bank, IMF and St. Louis Fed, market data, prediction markets, ADS-B flight data from Wingbits, and conflict, cyber and climate feeds. The project publishes a data sources catalogue with each provider's licence posture. AI summaries are machine-written, so check the linked articles before relying on them.
Is it legal to use World Monitor for research?
Yes. It shows aggregated public information about countries, markets and infrastructure, not individuals, so it suits journalism, risk analysis and teaching. Each upstream source keeps its own terms, which matter if you republish its data, and if you run a modified copy as a public service the AGPL requires you to publish your source code.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
