Contents
- At a glance
- The OSINT fusion layer
- Country-level risk scoring
- v16 scoring upgrades (locus / actor / cluster-first)
- v17 — Static / Active decomposition
- v18 — Ten-category baseline decomposition
- Risk vs confidence
- Source registry & reliability weights
- Corroboration & clustering
- Recency decay
- Country attribution
- Event types & threat vectors
- Refresh cadence
- Output schema
- Intended use cases
- How to read the GGRI
- Changelog
At a glance
The Global Geopolitical Risk Index (GGRI) is AtlasRisks' country-level risk scoring engine. It fuses public OSINT — wire services, mainstream media, specialist conflict reporting, regional outlets, alternative media, government and humanitarian advisories, optionally GDELT and ACLED — into per-country risk scores, refreshed twice daily and on demand. Every country in the active set carries two numbers:
- Risk — 0 to 95, a composite of the country's baseline condition plus event-driven modifiers from the last seven days of intelligence.
- Confidence — 0 to 100, how well that risk number is supported by source diversity, source reliability, recency, official-source corroboration, and country-attribution clarity.
A score is meaningful when both are high. A high risk with low confidence is a flag to investigate; a low risk with high confidence is a stable signal. The two are reported separately, never collapsed.
Tier mapping
| Tier | Range | Operational meaning |
|---|---|---|
| LOW | 0–39 | Stable. Routine geopolitical activity; no acute threat indicators. |
| MODERATE | 40–59 | Elevated baseline or active news cycle; specific theaters bear watching. |
| HIGH | 60–74 | Severe instability, active sanctions or kinetic events, no rule-of-law collapse. |
| CRITICAL | 75–95 | Active war, state collapse, mass casualty events, or sustained chokepoint disruption. |
The OSINT fusion layer
Every public intelligence pipeline trades coverage against credibility. Wire services break stories first but compress them; specialist conflict outlets verify but lag; regional papers know the ground but carry domestic framing; state-affiliated outlets disclose policy posture but distort facts; humanitarian feeds report disasters with high accuracy but limit themselves to humanitarian topics. No single source class gives you the full picture.
AtlasRisks' fusion approach is to ingest all of them, tag each item with its source class and reliability weight, then score countries from the corroborated picture rather than from any single feed. A wire-service-only signal scores moderately. A wire-service signal corroborated by a regional outlet AND a humanitarian advisory scores higher. A signal that exists only in alternative media or state-affiliated sources, without corroboration, scores low — even if the headline is dramatic.
Country-level risk scoring
For each country with a defined baseline (~70 countries) plus any country with current news activity, the engine computes:
- Baseline risk — a manually curated 0–95 score reflecting structural condition (active war, state collapse, ungoverned space, peer-state confrontation, etc.). Reviewed quarterly.
- Event severity modifier — capped at +10. Each clustered event contributes its raw severity (drawn from the event-type lexicon, e.g. coup=9, airstrike/terrorism=8, mass casualty=8, kidnapping=6, civil unrest=5, sanctions=4) weighted by the average reliability of its sources and amplified by corroboration count.
- Trend modifier — capped at +3. Reflects whether today's event volume on this country exceeds the baseline-quiet condition.
- Corroboration modifier — capped at +6. Adds +2 per event cluster confirmed by three or more independent sources.
- Official advisory modifier — capped at +5. Adds weight when government advisories or humanitarian alerts (NHC, USGS, GDACS, ReliefWeb, future State Department advisories) attribute to the country.
- Breaking modifier — capped at +10. Existing v10 signal preserved for compatibility: high-density armed-conflict keyword matches flagged as breaking news.
- Stale data penalty — up to −2. If all attributed items are more than 48 hours old, the score is dampened so cold news doesn't pretend to be live.
The resulting number is clamped to 0–95 and bucketed into a tier. Every modifier is exposed in the API payload so consumers can audit which factors moved a country today.
v16 scoring upgrades — locus, actor, cluster-first
The v11 scoring model worked, but in operation it had a recognizable failure mode: structurally stable countries that simply appeared in a lot of news stories on a given day got over-elevated. A quiet democracy mentioned in 80 wire stories about a foreign crisis would creep toward HIGH because the engine couldn't distinguish "this country is the locus of the news" from "this country is mentioned in the news about somewhere else." v16 fixes that with five concrete changes.
-
Locus / actor / mention country-role classification.
For every country detected in a headline, the engine now asks: where in the sentence does the country name sit relative to the kinetic verbs? A country whose name appears within 12 words of an attack/strike/coup/raid/explosion verb is classified as the
locus(the place the event happens at). A country mentioned outside that window but tied to government / military / diplomatic actor language is classified as theactor(an involved party). A country whose name appears with no proximity to either is classified as amention(referenced, but not the subject). - Cluster-first event severity from top-5 LOCUS clusters only. Severity is no longer aggregated per article. The engine clusters items, ranks each country's clusters by reliability-weighted severity, and computes the country's event-severity modifier from the top 5 clusters where that country is the locus. A country mentioned 80 times in actor-only or mention-only stories contributes nothing to its own severity score under this rule.
- Reliability-weighted volume. Volume signals (the trend modifier) are now multiplied by the average source reliability of the contributing items. A flood of 50 RT or alt-media items can no longer fake high volume the way 50 wire-service stories can.
-
Baseline-asymmetric modifier cap.
The maximum amount the news can elevate any country's risk above its baseline is now
max(8, round((95 − base) × 0.40)). Concretely: a baseline-90 country (active war zone) can move ±8 from news. A baseline-30 country (stable democracy) is also capped at ±8 by the floor — but more importantly, no amount of news traffic about other countries can drag a baseline-30 country's score above ~38. The structurally stable can no longer be elevated to HIGH purely by being mentioned in dramatic stories. - Cluster-locus filtering. A cluster only contributes to country X's score if at least one of the cluster's items has X as the locus. Mention-only and actor-only items still appear in country X's evidence trail (so analysts can see what was said about it) but they don't move the score.
v16.2 added a related correctness fix to country detection itself: the substring match used since v9 (text.includes("India")) was matching Indiana, Iranian, Chinatown, and South Korean. The v16.2 detector uses a precompiled word-boundary regex ((?<![A-Za-z0-9])needle(?![A-Za-z0-9])) so only whole-word matches count. The hotspot drill-down panel and the daily brief's events list also filter to locus + actor articles per country, so the country brief no longer surfaces stories that happened to mention the country in passing.
Net effect: tier transitions are more conservative and more accurate. Countries don't move into HIGH because of the news cycle alone, and ties at the CRIT ceiling now genuinely reflect parallel real-world conditions (active wars, state collapse, sustained kinetic events) rather than scoring artefacts.
v17 — Static / Active decomposition
Even with the v16 fixes, the highest-baseline countries (Sudan 95, Ukraine 92, Palestine 92, Russia 88, Israel 88) all converged to a tied 95.0 headline score whenever any meaningful news activity registered. The cause was structural: with a 95 ceiling and a baseline-asymmetric modifier cap of max(8, round((95 − base) × 0.40)), every country with baseline ≥ 87 hit the ceiling on its first news event. Sudan literally couldn't move from 95 because its baseline already equalled the cap. Operationally this meant a quiet day in a war zone and an active escalation day produced the same headline number — and the Top Movers list was uninformative when 5 conflict countries tied at 95.
v17 fixes this by reporting two numbers per country instead of one:
risk_static— the curated structural baseline. Slow-moving (reviewed quarterly), reflects active war, state collapse, peer-state confrontation, stable democracy, etc. Range 8–95.risk_active— pure event-driven intensity from the last 7 days of OSINT. Driven entirely by news; a country with zero news activity hasrisk_active = 0regardless of structural condition. Range 0–100, clamped only at the upper bound (no baseline contribution).risk— the headline number.= max(risk_static, risk_active), clamped 0–100. Tier mapping (LOW <40, MOD 40–59, HIGH 60–74, CRIT ≥75) is applied to the headline.
5 × event_severity_modifier (0–10)
+ 4 × breaking_modifier (0–10)
+ 3 × corroboration_modifier (0–6)
+ 3 × official_advisory_modifier (0–5)
+ 4 × trend_modifier (0–3)
+ 2 × volume_bump (0–5)
− 5 × stale_data_penalty (0–2)
)
v17.8 hybrid headline:
if (risk_static ≥ 75)
risk_headline = clamp(0, 100, risk_static + (risk_active − 50) × 0.25)
else
risk_headline = clamp(0, 100, max(risk_static, risk_active))
For conflict countries (baseline ≥ 75), the headline is the curated baseline MODULATED by current news intensity — active=50 is the "average news day" with no nudge to baseline; values above 50 push up (escalation), values below 50 nudge down (quiet day). The 0.25 multiplier means active news can swing the headline ±12.5 from baseline. Sudan, Russia, Ukraine, Israel — all CRIT — now produce differentiated decimal headlines that reflect what's happening today, not just round baseline ceilings.
For non-conflict countries (baseline < 75), the headline is max(risk_static, risk_active) — the static baseline is the floor, but a major incident can escalate the headline. A terror attack in Sweden (static 25, active 78) pushes the headline to 78 / CRIT for the day; a quiet day in Switzerland (static 8, active 0) reads 8 / LOW.
The active modifiers themselves are still computed under the v16 rules (locus-only attribution, cluster-first severity, reliability-weighted volume). v17 only changes how those component modifiers are combined into a final score: instead of being added on top of the baseline and capped tightly, they now produce an independent 0–100 score that competes with (rather than tops off) the baseline.
How to read the two numbers
| Country state | Static | Active | Headline | Operational read |
|---|---|---|---|---|
| Sudan, quiet day | 95 | 35 | 91.3 | Structural CRIT, but news quieter than average — headline modulated down ~3.7 from baseline. |
| Sudan, active escalation | 95 | 88 | 100.0 | News intensity well above average — headline pushed past 95 to ceiling. Operationally escalating. |
| Sudan, mass-casualty event | 95 | 95 | 100.0 | Maximum signal. Operator should treat as catastrophic. |
| Russia, quiet day | 88 | 42 | 86.0 | Borderline CRIT/HIGH. Routine news cycle pulls headline 2 below baseline. |
| Russia, missile exchange day | 88 | 82 | 96.0 | Active news pushes headline 8 above baseline. CRIT, very active. |
| Ukraine, normal week | 92 | 55 | 93.3 | Active slightly above neutral — small upward modulation. |
| Israel, active operations | 88 | 78 | 95.0 | News-driven escalation visible in the headline number, not just secondary fields. |
| Sweden, terror incident | 25 | 78 | 78.0 | Static below 75 → max(static, active) rule applies. Active dominates; headline jumps to CRIT. |
| Sweden, normal week | 25 | 12 | 25.0 | Static dominates — baseline LOW. |
| Switzerland, quiet day | 8 | 0 | 8.0 | Both low. No risk to operate. |
The crucial behavioral change: Top Movers in the daily brief and the dashboard now sort by abs(delta_active_vs_yesterday), not by headline delta. Static doesn't change daily, so headline delta is uninformative for CRIT-pegged countries (Sudan's headline can read 95 every day for a year while the active number swings between 20 and 90). The active delta is what changed in the world; that's what surfaces.
Both numbers are surfaced in the API payload (hotspot.risk_static, hotspot.risk_active, hotspot.risk_active_components with the per-modifier breakdown, and hotspot.delta_active_vs_yesterday). The headline risk field continues to populate exactly as before for backward compat with the v9–v16 dashboard adapters. The dashboard status strip and the daily brief now display Static / Active alongside the headline; the country drill-down panel shows the active component breakdown.
v18 — Ten-category baseline decomposition
v17.8 produced decimal differentiation only for the active component — the static baseline was still a single curated number per country, so Sudan (95), Ukraine (92), Palestine (92), Russia (88), and Israel (88) all reported their round baselines on the headline. Operationally these are wildly different countries: Sudan is state-collapse + security; Russia is sanctions + cyber + political; Israel is security + advisory; etc. v18 surfaces that structural difference by decomposing every country's baseline into ten weighted category subscores.
Categories and weights
| # | Category | Weight |
|---|---|---|
| 1 | Political Stability | 12% |
| 2 | Security and Violence | 18% |
| 3 | Civil Unrest and Social Tension | 10% |
| 4 | Crime and Personal Safety | 8% |
| 5 | Economic and Financial Stability | 10% |
| 6 | Regulatory, Legal, and Corruption | 8% |
| 7 | Infrastructure and Operational | 8% |
| 8 | Cyber and Information | 8% |
| 9 | Health, Environmental, Natural Hazard | 8% |
| 10 | Travel, Diplomatic, Government Advisory | 10% |
| Total | 100% | |
Country profiles + algorithmic seeding
Each country is assigned a profile that defines additive offsets per category. The category subscore for category c equals clamp(0, 100, baseline + offset[c]). Profiles are calibrated quarterly so the weighted sum stays close to the legacy baseline (the structural anchor doesn't move; the operational decomposition does). v18 ships ten profiles: state_collapse_war, peer_war_invader, war_defender, war_zone_active, fragile_authoritarian, fragile_state, criminal_violence, regional_tension, stable_democracy, and a fall-through default.
Worked examples — five CRIT countries that previously tied
| Country | Profile | Pre-v18 baseline | v18 risk_static | Top-3 categories |
|---|---|---|---|---|
| Sudan | state_collapse_war | 95 | 90.8 | security 100, economic 100, advisory 100 |
| Russia | peer_war_invader | 88 | 89.3 | political 93, security 93, regulatory 93 |
| Ukraine | war_defender | 92 | 91.2 | security 98, advisory 97, economic 95 |
| Israel | war_zone_active | 88 | 89.3 | security 94, advisory 93, political 91 |
| Palestine | war_zone_active | 92 | 93.3 | security 98, advisory 97, political 95 |
| Yemen | state_collapse_war | 82 | 77.8 | security 87, economic 87, advisory 87 |
| USA | stable_democracy | 42 | 40.6 | cyber 62, crime 54, unrest 45 |
| Switzerland | stable_democracy | 18 | 16.6 | cyber 38, crime 30, unrest 21 |
Russia and Israel both report risk_static = 89.3, but their top-3 categories are completely different (Russia: political/security/regulatory; Israel: security/advisory/political). The dashboard country drill-down surfaces the per-category breakdown so an operator can see at a glance why two CRIT countries with similar headlines have different operational profiles.
Per-category recency half-life is also category-specific: 7 days for fast-moving categories (security, unrest), 14 days for episodic categories (economic, health), 30 days for slow-moving categories (political, crime, regulatory, infrastructure, cyber, advisory).
The v17.8 hybrid headline rule is preserved unchanged. v18.0 only changes how risk_static is COMPUTED (weighted sum of categories instead of single number). Future v18.x rounds will add per-event scoring formula with business_impact, alert triggers with point uplifts and decay, sector modifiers, and subnational granularity.
Risk vs confidence
Two countries can both be scored 75 and be in very different epistemic situations. Country A might be at 75 because every wire service, ReliefWeb, and a state department advisory all reported the same kinetic event in the last six hours. Country B might be at 75 because a single alternative-media source published one dramatic headline that the engine couldn't corroborate.
The confidence score, on a 0–100 scale, makes the difference visible. It's a weighted blend of:
- Average source reliability (40%) — drawn from the source registry's per-type weights (wire services 0.92, mainstream media 0.85, specialist conflict 0.82, humanitarian 0.80, regional 0.72, alternative 0.45, state-affiliated 0.30, public social signals 0.20).
- Corroboration (25%) — number of independent multi-source clusters attributed to this country.
- Recency (15%) — average freshness across the country's items, with full weight in 0–24h, 60% in 24–48h, 35% in 48–72h, 15% in 3–7 days.
- Official-source bonus (10%) — a binary kicker when at least one government or humanitarian advisory item attributes to the country.
- Country attribution confidence (10%) — average per-item confidence in the country detection itself (full country name 90, capital or institution 80, leader name 75, demonym 65, feed-default fallback 40).
Source registry & reliability weights
v11 organizes sources into a structured registry with per-type reliability weights. The dashboard shows which categories drove today's score; consumers can use the registry to audit how heavily a given source class influenced an event.
| Source class | Weight | Examples in registry |
|---|---|---|
| Official / govt advisory | 1.00 | UK FCDO travel advice, US State travel warnings, Smartraveller (Australia), Canada GAC, EU Council Press, Japan MOFA travel advice. v18.9 added EU Council + Japan MOFA to widen non-Anglo policy coverage. |
| Economic stability | 0.95 | OFAC Recent Actions (sanctions), IMF Press Center, Federal Reserve press releases, ECB press releases, FRED Blog (St. Louis Fed data commentary). v18.9 expanded the tier from 1 → 5 feeds so the v18 economic-category active signal is genuinely live (rate decisions, IMF Article IV reports, Fed/ECB statements) instead of OFAC-only. |
| Humanitarian official | 0.92 | Reserved for direct WHO / UN OCHA / IFRC integration in a future round. |
| Wire service | 0.92 | AP World, Reuters World, AFP English. v18.8 added AFP to close the European-press gap. |
| Mainstream media | 0.85 | BBC (7 regional desks), Guardian (3), NPR (3), Yahoo, CBS, ABC, CNN, Politico, Foreign Policy, Al Jazeera, Deutsche Welle, France 24. |
| Specialist conflict | 0.82 | Institute for the Study of War, Crisis Group, Long War Journal, Al-Monitor (Middle East specialist), Defense One (US defense policy specialist). Optional: ACLED API. v18.9 added Al-Monitor + Defense One to lift the tier from 3 → 5; Janes Defence Weekly remains paywalled, no public RSS. |
| Humanitarian disaster | 0.80 | NOAA NHC (Atlantic + Pacific), USGS Earthquakes, GDACS, ReliefWeb. |
| Regional media | 0.72 | Kyiv Independent (UKR), Times of Israel (ISR/PSE/LBN), Premium Times Nigeria (NGA), The East African (KEN/UGA/TZA), Mail & Guardian (ZAF), Times of India (IND), Nikkei Asia (JPN/pan-Asia), South China Morning Post (HKG/CHN), Folha de São Paulo (BRA), El Universal (MEX), Clarín (ARG). v18.7 added 9 anchors across Africa, Asia, and Latin America. |
| Alternative media | 0.45 | Bellingcat, War on the Rocks, Antiwar.com, Responsible Statecraft, The Intercept. |
| State-affiliated | 0.30 | RT (RU), TASS English (RU), Xinhua English (CN), PressTV (IR), Anadolu Agency English (TR). Always flagged in payload as state_affiliated:true. v18.8 expanded the tier from 1 to 5 outlets to cover the propaganda-surface across the major non-Western state-aligned framings; weight stays at 0.30 so they corroborate but never dominate. |
| Public social signal | 0.20 | Optional GDELT 2.0 DOC API. Treated as soft signal. |
Alternative media, state-affiliated, and public social signals are intentionally low-weighted. They contribute corroboration value when their reporting aligns with higher-weight sources, but cannot by themselves move a country's risk score significantly. State-affiliated items are always flagged in the API payload so downstream consumers can discount or filter them.
Optional environment-gated sources
Two sources are wired in v11 but gated by Netlify environment variables; they are skipped gracefully when keys aren't set.
- GDELT 2.0 DOC API — set
GDELT_ENABLED=true. Public, key-less. Queried for a curated list of threat-related terms (coup, airstrike, missile attack, terrorism, kidnapping, evacuation, embassy alert, state of emergency, border clash, cyberattack on critical infrastructure, port disruption, piracy). Used as a soft trend signal and corroboration layer; never a primary score driver. - ACLED Conflict Events — set
ACLED_API_KEYandACLED_EMAILafter registering at acleddata.com. Pulls last-24h armed conflict events with location, fatalities, and event type. Treated as specialist-conflict tier (weight 0.82).
X / Reddit / Telegram / Google Trends are recorded as reserved env-var slots in the diagnostics object (diagnostics.env_enabled) so the admin console can see whether keys are present, but no fetcher is wired in v11. AtlasRisks does not scrape private platforms, does not bypass authentication, and does not collect personal social media data — these sources will only ever come online via lawful official APIs.
Corroboration & clustering
The engine clusters items by country + event_type + normalized_keywords + date_bucket. A cluster represents one real-world event as reported by one or more sources. For each cluster the engine tracks: source domains (distinct), source types (distinct), source count, highest reliability, average reliability, severity weighted by reliability and corroboration count, first seen and last seen timestamps, and a confidence score.
Scoring rules anchored in clusters:
- One alternative-media or state-affiliated source alone — minimal impact. The cluster's average reliability stays low; the corroboration multiplier is 1.0.
- Two independent reliable sources — moderate impact. Corroboration multiplier rises to 1.2.
- One official source plus media confirmation — strong impact. The official-advisory modifier kicks in.
- Three or more independent source categories — high confidence. Corroboration multiplier rises to 1.4 and the corroboration modifier adds up to +6 to country risk.
Recency decay
News loses operational relevance over time. The engine applies a per-item recency weight to severity scoring:
| Age window | Weight | Treatment |
|---|---|---|
| 0 – 24 h | 1.00 | Full weight in active scoring. |
| 24 – 48 h | 0.60 | Reduced weight; still active. |
| 48 – 72 h | 0.35 | Significant decay; trend context. |
| 3 – 7 d | 0.15 | Trend context only; minor influence. |
| > 7 d | 0.00 | Excluded from active scoring window. |
The 7-day rolling window is preserved as feed_history in the payload so longer-term trend analysis remains available, but items older than seven days no longer contribute to today's risk.
Country attribution
Each headline plus description is run through detailed country detection. The engine returns every matched country with a confidence score and a reason:
- Full country name or capital match (90 confidence) — "Sudan", "Khartoum", "Buenos Aires".
- Institution match (80 confidence) — "Pentagon", "Kremlin", "Hezbollah", "IDF", "RSF".
- Leader match (75 confidence) — "Putin", "Zelensky", "Xi Jinping", "Erdogan".
- Demonym or short match (65 confidence) — "Russian", "Iranian", "British".
- Feed default-country fallback (40 confidence) — region-specific feeds (BBC US & Canada → USA; Kyiv Independent → UKR) catch articles whose text doesn't mention a country by name.
Multi-country articles are attributed to all detected countries. "U.S. strikes Houthi positions in Yemen" attributes to USA + YEM, not just the first match.
Event types & threat vectors
Every item is classified into one or more event types. The classification produces both a primary event type and a union of threat vectors (operational categories that downstream consumers can filter on). v11 ships with the following lexicon:
- Armed conflict (severity 8) — airstrike, missile, artillery, invasion, drone strike, frontline.
- Terrorism (severity 8) — terror attack, IED, suicide bomb, claimed responsibility.
- Mass casualty (severity 8) — massacre, ethnic cleansing, genocide, killed, wounded.
- Coup or regime change (severity 9) — coup, junta, ousted, military takeover, martial law.
- Civil unrest (severity 5) — protests, riots, unrest, looting, clashes with police.
- Political instability (severity 5) — constitutional crisis, government collapse, no-confidence, election fraud.
- Sanctions (severity 4) — OFAC, embargo, designations, frozen assets, export controls.
- Crime / cartel (severity 5) — cartel, narco, gang, organized crime.
- Kidnapping (severity 6) — kidnap, hostage, abduction, ransom.
- Maritime security (severity 6) — Red Sea, Strait of Hormuz, Gulf of Aden, port closure.
- Piracy (severity 6) — pirates, hijacked vessel, boarding.
- Cyber (severity 5) — cyberattack, ransomware, data breach, critical-infrastructure attack.
- Disaster (severity 6) — earthquake, tsunami, flood, hurricane, volcanic eruption.
- Health (severity 5) — epidemic, outbreak, pandemic, quarantine.
- Economic instability (severity 5) — currency collapse, hyperinflation, sovereign default.
- Migration / border (severity 4) — mass migration, border closed, asylum, displaced.
- Infrastructure failure (severity 5) — blackout, grid failure, fuel shortage, port outage.
- Aviation security (severity 6) — airspace closed, aviation incident, shot-down aircraft.
- Travel warning (severity 6) — travel advisory, embassy alert, evacuation.
- Diplomatic crisis (severity 4) — ambassador withdrawn, severed ties, recalled diplomats.
- Supply chain disruption (severity 4) — shipping delays, semiconductor shortage, grain corridor.
Threat vectors aggregate the event types into operational categories — military, terrorism, civil unrest, organized crime, kidnapping, travel, maritime, cyber, natural disaster, political, economic, health, infrastructure, supply chain, aviation security. Each hotspot in the API payload exposes the union of threat vectors triggered for that country.
Refresh cadence
The OSINT pipeline runs on a Netlify scheduled function with cron 0 3,10 * * * UTC — 03:00 UTC (22:00 EST previous day) and 10:00 UTC (05:00 EST). The dashboard's "Pull Now" button additionally allows on-demand refreshes, rate-limited to once per minute per pipeline instance to prevent runaway invocations.
Each refresh writes to two Netlify Blob keys:
intel/latest— the canonical payload served by/api/intel-feed.intel/history— a 30-day rolling map of compact daily snapshots used to compute global and per-hotspot day-over-day deltas.
Output schema
The /api/intel-feed endpoint returns a payload of the following shape. Backward compatibility with v10 is preserved — every v10 field still exists in v11; new fields are additive.
{
// v10-compatible fields (preserved)
"generated_at": "2026-05-08T10:00:14Z",
"source": "Country baseline + structured 40+ source registry...",
"events_24h": 247,
"elevated_count": 12,
"critical_count": 3,
"global_average": 63,
"global_tier": "high",
"tier_counts": { "1": 0, "2": 198, "3": 49, "4": 0 },
"hotspots": [ { ... } ],
"feed": [ { ... } ],
"global_recent": [ { ... } ],
"feed_history": [ ... ],
"window_days": 7,
"delta_vs_yesterday": 1.4,
"yesterday_score": 62,
// v11 additive fields (still current under v17)
"methodology_version": "v17",
"global_static": 51.2, // v17 — average of all hotspots' baselines
"global_active": 38.6, // v17 — average of all hotspots' news-driven score
"delta_active_vs_yesterday": 4.1, // v17 — meaningful day-over-day signal
"events_7d": 1342,
"global_confidence": 78,
"sources_checked": 47,
"sources_successful": 36,
"sources_failed": 11,
"source_mix": { "mainstream_media": 751, "government_advisories": 231, "alternative_media": 160, "state_affiliated": 100, "humanitarian_disaster": 37, "specialist_conflict": 30, ... },
"unassigned": [ ... ], // items not attributable to any country
"diagnostics": {
"run_id": "run_lwf3z9_a8c2f1",
"duration_ms": 9372,
"sources_attempted": 42,
"sources_successful": 39,
"sources_failed": 3,
"skipped_sources": [ { "source": "GDELT 2.0 DOC API", "reason": "GDELT_ENABLED env var not set" } ],
"errors": [ ... ],
"items_fetched_raw": 583,
"items_after_dedupe": 412,
"items_after_country_detection": 387,
"blob_write_status": "ok",
"env_enabled": { "gdelt": false, "acled": false, "social_x": false, "social_reddit": false, "google_trends": false }
}
}
Each hotspot entry exposes the full risk decomposition:
{
"iso": "PAK", "name": "Pakistan", "lat": 30, "lng": 69,
"risk": 67, "tier": "high", "confidence": 82,
"base": 58,
"newsModifier": 9, // legacy v10 aggregate (preserved for compat)
"breakingBump": 0,
"breaking_count": 0,
"eventSeverityModifier": 6,
"trendModifier": 1,
"corroborationModifier": 4,
"officialAdvisoryModifier": 0,
"staleDataPenalty": 0,
"count": 7,
"source_count": 5,
"source_types": ["mainstream_media", "wire_service", "specialist_conflict"],
"corroborated_event_count": 2,
"average_reliability": 0.86,
"average_recency": 0.72,
"signals": { "armed": 6, "unrest": 4, "political": 2, "disaster": 0 },
"threat_vectors": ["military", "civil_unrest", "political"],
"events": [ { "ts": "...", "headline": "...", "src": "...", "url": "...", "tier": 2 } ],
"delta_vs_yesterday": 4
}
Intended use cases
The GGRI is built for security operators, analysts, and decision-makers who need a continuously refreshed country-level view of geopolitical and operational risk. Specifically:
- Executive protection and travel security — daily risk-tier check before travel, with the country brief available for advance planning.
- Maritime operations and logistics — chokepoint and piracy threat vectors surface independently of generic "country risk" so route decisions can react to current conditions.
- Insurance underwriting — country-level baselines plus current-event modifiers, with confidence scores for portfolio-quality assessment.
- Corporate risk and business continuity — supply-chain disruption, sanctions, infrastructure failure threat vectors as standalone signals.
- Geopolitical monitoring — analyst dashboard for tracking emerging crises across multiple theaters in one view.
AtlasRisks is not a government advisory and does not replace professional judgment. The GGRI is a starting point for analysis. Operators should consult official advisories, qualified security professionals, and ground-level information before making operational, travel, or financial decisions.
How to read the GGRI
The GGRI is a direction-of-risk indicator, not a prediction. Read tier transitions (LOW → MODERATE → HIGH → CRITICAL), the day-over-day delta, the confidence score, and the threat-vector breakdown — not last-decimal score moves. A country at risk 75 with confidence 30 is a flag to investigate; a country at 75 with confidence 85 is a stable signal worth acting on.
Use the events list and the cluster-derived corroboration metrics to investigate why today's score is what it is. The score is a starting point for analysis, not a substitute for it.
Changelog
v18.1 — 2026-05-10 (Per-event scoring + asymptotic ceiling + 2-decimal display)
- Per-event scoring formula implemented per v18 §7. Every event now gets
event_score = severity × source_reliability × recency × frequency × business_impact × locus_factor. Severity from EVENT_TYPE_LEXICON (normalized to 1-5); source_reliability from feed tier (0.20-1.00); recency = exp(-ln2 × age / category_half_life); locus_factor 1.00 / 0.60 / 0.20 from the v16 country-role classifier. Frequency and business_impact placeholders at 1.0 in v18.1 — wired to cluster source-count and capital/foreigner-targeted heuristics in v18.2. - EVENT_TYPE_TO_CATEGORY map assigns each of 21 event types to one of the 10 v18 categories. category_active(c) = K_c × Σ event_score over events in c, clamped 0-100. risk_active = Σ (category_active × weight). Replaces the v17.6 lump-sum modifier formula. Transitional v18.1 blend: 70% per-event / 30% legacy lump-sum to smooth the rollout (cuts to 100% per-event in v18.2 once production data confirms calibration).
- Asymptotic ceiling replaces the hard
clamp(0, 100, ...)that let CRIT countries saturate at exactly 100.00. New:asymptoticCompress(x, knee=85, hardMax=100). Inputs ≤85 pass through unchanged; inputs >85 are squashed toward 100 along an exponential curve and never reach it. Sample outputs: input 90 → 89.25; input 95 → 92.31; input 100 → 94.49; input 200 → 99.83; input ∞ → 100.00 (asymptote). Concrete impact: Sudan-mass-casualty was 100.0 under v18.0, now 96.36 — meaningful headroom restored at the top of the band. - Two-decimal display for risk, risk_static, risk_active in the payload (previously single-decimal). Brief renderer + admin diagnostic surface 95.91 not 95.9. Provides operational granularity at the top end where most movement happens.
- METHODOLOGY_VERSION bumped "v18" → "v18.1". Backward compat preserved — hotspot.risk = max(static, active) hybrid headline rule still applies, just with asymptotic compression layered after.
v18.0 — 2026-05-10 (Ten-category baseline decomposition)
- risk_static is now a weighted sum of ten category subscores per the v18 spec (Political 12% / Security 18% / Civil Unrest 10% / Crime 8% / Economic 10% / Regulatory 8% / Infrastructure 8% / Cyber 8% / Health 8% / Advisory 10%). Pre-v18 risk_static was a single curated number — Sudan/Russia/Ukraine/Israel/Palestine all read their round baselines on the headline. v18 produces decimals like Sudan 90.8, Russia 89.3, Ukraine 91.2 because each category contributes differently to each country's profile.
- Algorithmic seeding via per-country profiles. Ten profiles ship: state_collapse_war, peer_war_invader, war_defender, war_zone_active, fragile_authoritarian, fragile_state, criminal_violence, regional_tension, stable_democracy, default. Each profile defines additive offsets per category, calibrated so the weighted sum stays close to the legacy baseline (the structural anchor doesn't move — the operational decomposition does).
- Per-category recency half-life (7d for security/unrest, 14d for economic/health, 30d for political/crime/regulatory/infrastructure/cyber/advisory). Pre-v18 a single half-life applied to all events.
- API additions: hotspot.profile (string), hotspot.categories[10] with {static, active, score, weight} per category. Backward compat preserved — hotspot.risk = max(static, active) rule still applies via v17.8 hybrid headline. v9-v17 dashboard adapters work unchanged.
- METHODOLOGY_VERSION bumped "v17" → "v18".
- Future v18.x: per-event scoring formula with business_impact, 17 alert triggers with point uplifts + decay, sector modifiers, subnational granularity (per AtlasRisks Risk Methodology v18 §13-§16).
v17.8 — 2026-05-09 (Hybrid headline)
- Replaced pure max(static, active) with a hybrid rule. v17.6's max() worked correctly for non-conflict countries but reverted to the round curated baseline for every CRIT country, losing the differentiation we'd been trying to add.
- For CRIT countries (baseline ≥ 75), headline is now
baseline + (active − 50) × 0.25, clamped 0–100. Active=50 is the "average news day" with no nudge; values above 50 push up (escalation), below 50 nudge down (quiet day). Max swing ±12.5 from baseline. - For non-CRIT countries (baseline < 75), the v17.6 max() rule still applies — a major incident in a normally quiet country can escalate the headline.
- Result: Sudan, Russia, Ukraine, Israel — all CRIT — produce differentiated decimal headlines (Sudan 91.3 quiet vs Sudan 100.0 active escalation, Russia 86.0 quiet vs Russia 96.0 missile-day, etc.) instead of all reading as round baseline values.
- Both
risk_staticandrisk_activestill surface in the payload unchanged with the v17.6 component breakdown. The dashboard Static / Active strip and the brief's per-mover S/A line continue working. - Live-merger (v17.7) keeps working — it just propagates the new decimal headline to the country table, map markers, and Top Movers tile.
v17.7 — 2026-05-09 (Dashboard live-merger)
- Country table, map markers, and Top Movers tile previously read from a hardcoded ~40-country demo array with synthetic deltas. v17.7 merges the live
/api/intel-feedhotspots into the demo array on every refresh: always-override scoring fields (was conditional on h.risk > c.risk, hiding the new lower decimal headlines), live delta capture for the Δ24H column, push pipeline-only entries that aren't in the demo set, re-trigger renderTable + Top Movers + map markers.
v17.6 — 2026-05-09 (Static / Active decomposition)
- Two-number scoring. Each country now reports
risk_static(curated baseline, slow-moving),risk_active(pure event-driven 0–100, no baseline contribution), andrisk = max(static, active). Headline ceiling raised from 95 to 100. - Active formula.
5×severity + 4×breaking + 3×corroboration + 3×official + 4×trend + 2×volume − 5×stale, clamped 0–100. - Top Movers sort by active delta. Headline delta is uninformative for CRIT-pegged countries; active delta is the meaningful day-over-day signal.
- Schema additions:
hotspot.risk_static,hotspot.risk_active,hotspot.risk_active_components(per-modifier breakdown),hotspot.delta_active_vs_yesterday, payload-rootglobal_static+global_active+delta_active_vs_yesterday. History snapshot extended to capture all three so tomorrow's deltas are reproducible. - Backward compatibility: all v9–v16 fields preserved exactly. The legacy
riskfield on each hotspot now equalsmax(static, active), so existing v9/v10/v11/v12/v13/v14/v15/v16 dashboard adapters continue working untouched. - Dashboard status strip surfaces Static / Active alongside the headline. Daily brief shows both per mover (S 95 / A 88 → 95) with active arrows.
v17.5 — 2026-05-09
- Fixed BLOB WRITE STATUS: pending false-positive in admin diagnostics. The diagnostics object was being written to the blob with status="pending" baked in, then only the LOCAL variable was updated to "ok"; the blob was never re-written. Fix: write optimistically with status pre-set to "ok"; setJSON's atomic failure semantics handle the unhappy path. Admin renderer now distinguishes ok / pending (in-flight) / failed:N (with reason inline) tri-state.
v17.0.x — 2026-05-09
- Deliverability hardening. Daily brief Resend payload now includes
List-Unsubscribe(https + mailto),List-Unsubscribe-Post: List-Unsubscribe=One-Click,List-Id, andPrecedence: bulkheaders (RFC 2369 + RFC 8058). Adds the native Gmail / Outlook one-click Unsubscribe button; biases the spam classifier out of Promotions tab. - Brief renderer extracted into shared
_lib/brief-utils.mjsso the cron and the new admin "Send test brief to me" endpoint produce byte-identical sends. - Top-mover delta arrow now correctly renders
•for delta=0 (was misleadingly down-arrow on first-day baseline briefs). - Audit-driven copy fixes: pricing accuracy ($99 Pro, $299 Team, $499 Enterprise, $0 Free); pricing.html Pro CTA repointed from broken /checkout to /signup.html; "41 sources" updated to "45 sources" across 6 pages.
v16.x — 2026-05-08 to 09
- Locus / actor / mention country-role classifier. Distinguishes country-as-event-locus from country-as-mention based on kinetic-verb proximity within 12 words.
- Cluster-first severity. A country's event-severity modifier is computed from the top-5 LOCUS clusters where that country is the place the event happens at, not from per-article aggregation.
- Reliability-weighted volume. Trend modifier multiplies by source-class average reliability so RT / alt-media floods can no longer fake high volume.
- Baseline-asymmetric modifier cap =
max(8, round((95 − base) × 0.40)). Structurally stable countries can no longer be elevated to HIGH by news intensity about other regions. - Cluster-locus filtering. A cluster only contributes to country X's score if at least one of its items is locus for X.
- v16.2 country-detection precision. Substring matcher replaced with word-boundary regex (
(?<![A-Za-z0-9])needle(?![A-Za-z0-9])): "India" no longer matches "Indiana"; "Iran" no longer matches "Iranian"; "China" no longer matches "Chinatown"; "South Korea" no longer matches "South Korean". Hotspot drill-down events now filter to locus + actor articles per country.
v15.x — 2026-05-08
- Staff user management system (12 endpoints under /api/staff/*) with scrypt password hashing, HMAC sessions + one-time tokens, role permissions, audit log, email-based password reset.
- v15.1 — light-theme contrast pass (127 targeted overrides on dashboard.html).
v14 — 2026-05-08
- Edge-function admin gate. Client-side SHA-256 obscurity replaced with a Deno edge function that issues HMAC-signed session cookies after PORTAL_PASSWORD entry.
PORTAL_PASSWORD+ADMIN_SESSION_SECRETenv vars required. Lock Console rewired to a server-side logout endpoint. - Admin diagnostics gain a Server config section (env-var configured/missing booleans, no values exposed).
v13.x — 2026-05-08
- Atom feed parsing added to
parseRssItems— USGS, OFAC, FCDO now functional (they ship Atom not RSS). - government_advisories source category populated: UK FCDO, US State, Smartraveller, Canada GAC.
- economic_stability source category populated: OFAC Recent Actions (sanctions / designations).
- Dashboard: 15-vector threat-vector filter pills (intersected with tier + signal-category filters); per-hotspot confidence pill on country drill-down.
- v13.1 — admin Pipeline Error panel surfaces actual HTTP status / content-type / body excerpt + likely-cause hint instead of silently saying "unavailable."
- v13.2 critical config fix —
netlify.toml/api/* redirects moved above the /* catch-all. The catch-all had been swallowing all /api/* requests since v9; v13.2 is when the GGRI pipeline first becomes reachable in production.
v12 — 2026-05-08
- Admin OSINT diagnostics panel (auto-refreshing, surfaces source health, source mix, env_enabled flags, skipped sources, errors, run_id, blob status).
- Dashboard surfaces methodology version stamp + per-tier confidence pill.
send-daily-brief.mjsdefensively enriches placeholder brief content from intel/latest blob (GGRI, tier, confidence, delta, methodology version, top movers from hotspots).
v11 — 2026-05-08
- Flat feed list replaced by a structured source registry (10 categories, 40 RSS sources + 2 optional API integrations).
- Source reliability weights (0.20–1.00) per source class, applied to severity scoring and confidence calculation.
- Optional GDELT 2.0 DOC API integration (env-gated).
- Optional ACLED API integration (env-gated, free key + email registration).
- New RSS sources: NOAA NHC (Atlantic + Pacific), USGS earthquakes, GDACS, ReliefWeb, Long War Journal, Bellingcat, War on the Rocks, Antiwar.com, Responsible Statecraft, The Intercept.
- Event type / threat vector classification beyond armed/unrest/political/disaster — 21 event types covering terrorism, kidnapping, maritime security, piracy, cyber, sanctions, supply-chain disruption, aviation security, travel warnings, diplomatic crises, and more.
- Country detection with confidence + match-reason metadata; multi-country attribution preserved.
- Event clustering for cross-source corroboration.
- Risk scoring decomposed into transparent modifiers (event severity, trend, corroboration, official advisory, breaking, stale-data penalty).
- Per-country and global confidence scores added.
- Recency decay (24h / 48h / 72h / 7d / older).
- Diagnostics object on each refresh (run_id, duration, source counts, errors, env_enabled).
- 60-second manual refresh cooldown to prevent Pull Now spam.
- Backward compatibility: every v10 payload field preserved exactly.
v10.1 — 2026-05-08
- Live Atlas dashboard: feed ticker bound to live
data.global_recent, Top Movers wired to realdelta_vs_yesterday, signal-category filter pills (Armed/Unrest/Political/Disaster), honest cadence indicator, Pull Now success toast, map-background click closes the right-side detail panel.
v10 — 2026-05-08
- OSINT pipeline expanded to 29 RSS feeds (added AP, Reuters, ISW, Crisis Group, Kyiv Independent, Times of Israel).
v9 — 2026-05-08
- Final logo cleanup. Single canonical
Atlas_Risks_clean_logo.pngin all footer locations.
v5 — v8
- Multi-country attribution, breaking-news flag, day-over-day delta computation, daily snapshot persistence.
— AtlasRisks · GGRI v18 · 2026-05-10