A real-time ambient stress index for cities.
Combines live weather, seismic, air-quality, mobility, macro and news signals into one explainable 0–100 score.
Why · How · Scoring · Screenshots · Run · Deploy · Contribute
Every city has a dozen streams of real-time data — a magnitude 3 event 40 km out, a thunderstorm warning, a stalled motorway, a currency dip, a spike in negative headlines. Individually they're easy to ignore. Stacked together, they shape what a day feels like.
StrainScape turns those streams into one number with an audit trail. Not a mental-health metric, not a prediction — an ambient conditions readout, like a barometer.
A typical request fans out to up to 12 real data sources in parallel — Open-Meteo weather / air quality / pollen / marine / flood, USGS and EMSC earthquake catalogs, NOAA SWPC geomagnetic Kp, NOAA NWS severe weather alerts (US), MeteoAlarm official EU alerts (40+ countries), OpenSky airspace activity, and optionally GDELT 2.0 news tone — plus synthetic fallback providers for traffic, FX and news.
No API keys required. Clone, run, and you get a working score with real live data immediately.
| New York — Elevated | Tel Aviv — High |
![]() |
![]() |
| Paris — Elevated | World Overview |
![]() |
![]() |
request ──▶ geocode ──▶ providers (parallel) ──▶ factor scorers ──▶ dynamic weights ──▶ ASI ──▶ explanation
- The HTTP layer resolves a city or coordinate pair via Open-Meteo geocoding.
- The provider aggregator fans out to 12 sources in parallel. Each one is non-fatal — failures degrade to neutral defaults and are flagged in the response.
- Six factor scorers each produce a 0–100 sub-score with human-readable drivers.
- A dynamic weighting pass boosts whichever factor is clearly dominant (active hazard, war zone, extreme heat), then renormalizes weights to 1.0.
- The final score, its level band, and an analyst-style explanation are rendered to HTML or served as JSON.
Everything runs in a single Go binary (~12 MB) with templates and static assets embedded. No external cache, no background workers.
| Factor | Base Weight | Signals |
|---|---|---|
| Hazard Load | 0.25 | Seismic (USGS + EMSC), severe weather alerts (WMO / NWS / MeteoAlarm), GloFAS river discharge, rough seas, geomagnetic Kp, geopolitical country risk |
| Environmental Load | 0.20 | Thom discomfort index, apparent temperature, wind, rain, European AQI, PM2.5, UV index, pollen (CAMS) |
| Mobility Friction | 0.15 | Traffic congestion, commute delay, rush-hour timing, airspace activity (OpenSky) |
| Information Pressure | 0.15 | News negativity and volume — GDELT 2.0 when enabled, deterministic synthetic fallback otherwise, geopolitical floor |
| Economic Pressure | 0.10 | Short-term FX volatility and inflation pressure (per-country tiers) |
| Context Pressure | 0.15 | Rush-hour / late-night timing, clustering amplification |
Scores land in five bands: Calm (0–24) · Elevated (25–44) · High (45–64) · Intense (65–79) · Critical (80–100).
Dynamic weighting boosts hazard + information for active conflict zones, mobility during rush-hour congestion, and environmental during extreme temperatures. Full model: docs/scoring-model.md.
| Provider | Type | What it feeds |
|---|---|---|
| Open-Meteo Weather | Live | Temperature, humidity, wind, precipitation, WMO alerts, local hour |
| Open-Meteo Air Quality | Live | PM2.5, PM10, European AQI, UV index |
| Open-Meteo Pollen (CAMS) | Live | Grass, birch, alder pollen (Europe) |
| Open-Meteo Marine | Live | Wave height (coastal cities) |
| Open-Meteo Flood (GloFAS) | Live | River discharge anomaly |
| USGS Earthquake Catalog | Live | Seismic events, magnitude, distance |
| EMSC Seismological Centre | Live | EU/Med earthquakes (USGS complement) |
| NOAA SWPC | Live | Planetary K index (geomagnetic storms) |
| NOAA NWS | Live | Severe weather alerts (US only) |
| MeteoAlarm (EUMETNET) | Live | Official EU severe weather alerts (40+ countries) |
| OpenSky Network | Live | Airspace activity (mobility proxy) |
| GDELT 2.0 | Live (opt-in) | News tone + volume (GDELT_ENABLED=true) |
| Geopolitical tier | Curated | Country conflict/tension level (120+ countries) |
| Traffic | Synthetic | Congestion model (megacity list + rush-hour curve) |
| Economic / FX | Synthetic | Per-country volatility tiers |
| News | Synthetic | Baseline news pressure (fallback when GDELT is off) |
Every response includes a providers array showing each source as live, synthetic, or unavailable so the UI is always honest about what it saw.
Full details: docs/data-sources.md.
GET /api/score?city=Tel+Aviv
{
"location": {
"city": "Tel Aviv",
"country": "Israel",
"countryCode": "IL",
"latitude": 32.0853,
"longitude": 34.7818,
"timezone": "Asia/Jerusalem"
},
"score": 61.2,
"level": "High",
"factors": [
{
"key": "hazard",
"label": "Hazard Load",
"score": 73.6,
"weight": 0.34,
"contribution": 25.0,
"drivers": ["active severe weather alert", "active armed conflict in country"],
"narrative": "Recent natural-hazard signals are materially raising background pressure."
}
],
"explanation": {
"headline": "Tel Aviv, Israel — multiple signals stacking into a noticeably tense atmosphere right now.",
"topDrivers": [
"Hazard Load — active severe weather alert and active armed conflict in country",
"Information Pressure — coverage dominated by active conflict",
"Mobility Friction — 89% congestion and +35 min typical delay"
]
},
"providers": [
{ "name": "open-meteo", "status": "live" },
{ "name": "usgs-earthquake", "status": "live" },
{ "name": "emsc-earthquake", "status": "live" },
{ "name": "noaa-swpc", "status": "live" },
{ "name": "opensky", "status": "live" },
{ "name": "geopolitical-tier", "status": "synthetic" }
]
}git clone https://github.com/aykutsp/strainscape
cd strainscape
go run .
# Open http://localhost:8080go test ./... -racedocker build -t strainscape .
docker run --rm -p 8080:8080 strainscapeA render.yaml is committed at the repo root. To put this on the public internet:
- Push the repo to GitHub.
- In Render, create a new Blueprint and point it at the repo.
- Render picks up
render.yaml, builds the Dockerfile, and publishes with a/healthzcheck.
The same image runs on Fly.io, Railway, Google Cloud Run, or any container host. The only required config is PORT (which every platform sets for you).
| Variable | Purpose |
|---|---|
STRAINSCAPE_CACHE_TTL |
Snapshot cache TTL (seconds or Go duration) |
STRAINSCAPE_HTTP_TIMEOUT |
Per-provider request timeout |
TOMTOM_API_KEY |
Enables real traffic adapter |
EXCHANGERATE_API_KEY |
Enables real FX / macro adapter |
NEWSAPI_KEY |
Enables real news adapter |
GDELT_ENABLED |
true to enable GDELT 2.0 (rate-limited) |
None are required. Weather and seismic paths hit Open-Meteo and USGS with no auth.
strainscape/
├── main.go # entry point, embeds templates + static
├── internal/
│ ├── domain/ # shared types (Signals, FactorScore, …)
│ ├── config/ # weights, level bands, env loading
│ ├── scoring/ # pure scoring engine (one file per factor)
│ │ ├── hazard.go
│ │ ├── environmental.go
│ │ ├── mobility.go
│ │ ├── information.go
│ │ ├── economic.go
│ │ ├── context.go
│ │ ├── weights.go # dynamic weight adjustment
│ │ ├── explain.go # analyst-style explanation builder
│ │ └── engine.go # orchestrator
│ ├── providers/ # 18 adapter files (geocoding to geopolitical)
│ ├── cache/ # TTL in-memory snapshot store
│ └── server/ # HTTP routes, template funcs, middleware
├── web/
│ ├── templates/index.html # server-rendered UI
│ └── static/ # CSS + ~120 lines vanilla JS (autocomplete, geolocation)
├── docs/
│ ├── architecture.md
│ ├── scoring-model.md
│ ├── data-sources.md
│ ├── research-notes.md
│ └── screenshots/
├── Dockerfile # two-stage distroless build
├── render.yaml
├── Makefile
└── CONTRIBUTING.md
- Everything is explicit. No trained model, no ML inference, no hidden state. Weights, band boundaries and factor logic are all in source. If a score surprises you, the cause is one
go testrun away. - Providers are adapters. Each one talks to its API, normalizes to
Signals, returns a status. The scoring engine is pure and never touches the network. - Failure is first-class. The
providersarray shows which sources were live, synthetic, or unavailable, so the UI is always honest. - Geopolitical realism. A curated per-country conflict tier ensures active war zones (Ukraine, Gaza, Yemen, Sudan) consistently outscore peaceful megacities regardless of the day's synthetic news baseline.
- One binary, zero deps. The Go binary embeds templates and static assets. No external cache, no build step for the frontend, no Node.js.
Pull requests welcome. Good first changes:
- Real adapters for traffic, FX, and news (replacing synthetic fallbacks)
- Additional factor families (noise, air traffic delays, public transit reliability)
- More scenario tests in
internal/scoring/engine_test.go - Historical trend endpoint backed by a persistence layer
See CONTRIBUTING.md and docs/architecture.md.
StrainScape estimates ambient environmental conditions around a location. It is not a medical device, not a diagnostic tool, and not a measurement of any individual's psychological state. Treat the score as a barometer, not a verdict.
MIT License · Scoring Model · Data Sources · Architecture



