๐ŸŒ EarthGrid

Distributed storage and openEO processing for Earth observation data. No vendor lock-in. Community-driven.

โ€”
Active Nodes in Grid
โ€”
Total Storage across Grid
โ€”
Items across Grid
โ€”
Data across Grid
โ€”
Requests from Grid (30d)
โ€”
GB Delivered
โ€”
GB Ingested

๐ŸŒ Network Economy

โ€”
Loading...

๐Ÿ›ฐ๏ธ Spatial Coverage

Loading...

๐Ÿ“ฅ Data Ingest History

Data fetched from upstream sources (Element84, CDSE, etc.)

Loading...

๐Ÿ“Š Uptake Statistics

Loading...

๐Ÿ† Leaderboard

Loading...

๐Ÿ… Active Challenges

Loading...

๐ŸŽ–๏ธ Achievements

Loading...

๐Ÿ“ก Live Activity

Loading...

โšก Processing Operations

Loading...

๐Ÿ”‘ Key Features

๐Ÿงฉ

Content-Addressed

Every chunk identified by SHA-256 hash. Tamper-proof by design โ€” corrupted or fake data is automatically rejected.

๐Ÿ”„

Auto-Sync

New data automatically propagates to all registered nodes. No manual intervention needed.

๐Ÿข

Idle Resources

Runs at lowest CPU and I/O priority (nice 19, ionice idle). Never competes with your other workloads.

๐Ÿ›ฐ๏ธ

STAC Native

Built-in STAC catalog. Every item is discoverable, searchable, and interoperable with the EO ecosystem.

๐Ÿงฎ

Built-in Processing

NDVI, NDWI, EVI, cloud masking, band math โ€” process data where it lives, no data movement needed.

๐ŸŒ

Federation

Beacons coordinate, data flows peer-to-peer. No single point of failure. Works across firewalls via HTTPS.

๐Ÿ“– Usage Examples

# Fetch latest Sentinel-2 for an area (distributed across the grid)
earthgrid fetch --bbox 12.4,55.6,12.6,55.7

# Date range, multiple scenes
earthgrid fetch --bbox 12.4,55.6,12.6,55.7 --start 2025-06-01 --end 2025-06-30 --limit 10

# Specific bands only
earthgrid fetch --bbox 12.4,55.6,12.6,55.7 --bands B04,B08,SCL

# Verify data integrity
earthgrid verify
earthgrid verify --heal    # auto-fix corrupted chunks

# Check node status
earthgrid status
import requests, matplotlib.pyplot as plt
from collections import defaultdict
from datetime import datetime

BASE = "http://localhost:8400"
LON, LAT = 12.57, 55.68  # Frederiksberg Gardens

# Search all scenes at this point
items = requests.get(f"{BASE}/stac/search", params={
    "bbox": f"{LON-0.01},{LAT-0.01},{LON+0.01},{LAT+0.01}",
    "collections": "sentinel-2-l2a", "limit": 500,
}).json()["features"]

# Group by date
dates = defaultdict(dict)
for item in items:
    band = item["id"].rsplit("_", 1)[-1]
    dt = item["properties"]["datetime"][:10]
    dates[dt][band] = item

# Extract NDVI time series
ndvi_ts = []
for dt in sorted(dates):
    if "B04" not in dates[dt] or "B08" not in dates[dt]:
        continue
    try:
        red = requests.get(f"{BASE}/point/{dates[dt]['B04']['collection']}/{dates[dt]['B04']['id']}",
                           params={"lon": LON, "lat": LAT}).json()["value"]
        nir = requests.get(f"{BASE}/point/{dates[dt]['B08']['collection']}/{dates[dt]['B08']['id']}",
                           params={"lon": LON, "lat": LAT}).json()["value"]
        if nir + red > 0:
            ndvi_ts.append((datetime.strptime(dt, "%Y-%m-%d"), (nir - red) / (nir + red)))
    except: continue

# Plot
d, v = zip(*ndvi_ts)
plt.figure(figsize=(14, 5))
plt.fill_between(d, v, alpha=0.3, color="#3fb950")
plt.plot(d, v, "o-", color="#3fb950", markersize=4)
plt.title(f"NDVI Time Series โ€” Copenhagen ({LAT}ยฐN, {LON}ยฐE)")
plt.ylabel("NDVI"); plt.grid(alpha=0.2)
plt.savefig("ndvi_timeseries.png", dpi=150)
import openeo

conn = openeo.connect("http://localhost:8400")

cube = conn.load_collection("sentinel-2-l2a",
    spatial_extent={"west": 12.4, "south": 55.6, "east": 12.7, "north": 55.75},
    temporal_extent=["2025-06-01", "2025-06-30"],
    bands=["B04", "B08"])

cube.ndvi(red="B04", nir="B08").save_result("GTiff").download("ndvi.tif")
library(httr); library(terra); library(jsonlite)

base <- "http://localhost:8400"

# Search
items <- fromJSON(content(GET(paste0(base, "/stac/search"),
  query = list(bbox = "12.4,55.6,12.7,55.75",
               collections = "sentinel-2-l2a", limit = 100)
), "text"))$features

# Download helper
dl <- function(band) {
  item <- items[grepl(band, items$id), ][1, ]
  url <- paste0(base, "/download/", item$collection, "/", item$id)
  tmp <- tempfile(fileext = ".tif")
  writeBin(content(GET(url), "raw"), tmp)
  rast(tmp)
}

red <- dl("B04"); nir <- dl("B08")
ndvi <- (nir - red) / (nir + red)

plot(ndvi, main = "NDVI โ€” Copenhagen",
     col = colorRampPalette(c("brown", "yellow", "darkgreen"))(100),
     range = c(-0.2, 0.8))
library(openeo)

con <- connect("http://localhost:8400")
p <- processes()

cube <- p$load_collection("sentinel-2-l2a",
    spatial_extent = list(west=12.4, south=55.6, east=12.7, north=55.75),
    temporal_extent = c("2025-06-01", "2025-06-30"),
    bands = c("B04", "B08"))

result <- p$save_result(p$ndvi(cube, red="B04", nir="B08"), format="GTiff")
compute_result(result, "ndvi.tif")

library(terra)
plot(rast("ndvi.tif"),
     col = colorRampPalette(c("brown","yellow","darkgreen"))(100))
const fs = require("fs");
const { execSync } = require("child_process");
const BASE = "http://localhost:8400";

// Search
const resp = await fetch(
  `${BASE}/stac/search?bbox=12.4,55.6,12.7,55.75&collections=sentinel-2-l2a&limit=100`
);
const items = (await resp.json()).features;

// Download B04 and B08
async function download(band) {
  const item = items.find(i => i.id.includes(band));
  const r = await fetch(`${BASE}/download/${item.collection}/${item.id}`);
  const path = `/tmp/${band}.tif`;
  fs.writeFileSync(path, Buffer.from(await r.arrayBuffer()));
  return path;
}

const b04 = await download("B04");
const b08 = await download("B08");

// Compute NDVI with GDAL
execSync(`gdal_calc.py -A ${b08} -B ${b04} --outfile=ndvi.tif \
  --calc="where((A+B)>0, (A-B)/(A+B), 0)" --type=Float32`);
console.log("Saved: ndvi.tif");
use reqwest::blocking::Client;
use std::fs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let base = "http://localhost:8400";
    let client = Client::new();

    // Search
    let items: serde_json::Value = client
        .get(format!("{base}/stac/search"))
        .query(&[("bbox", "12.4,55.6,12.7,55.75"),
                  ("collections", "sentinel-2-l2a"),
                  ("limit", "100")])
        .send()?.json()?;

    let features = items["features"].as_array().unwrap();

    // Download bands
    for band in &["B04", "B08"] {
        let item = features.iter()
            .find(|i| i["id"].as_str().unwrap().contains(band))
            .unwrap();
        let col = item["collection"].as_str().unwrap();
        let id = item["id"].as_str().unwrap();
        let bytes = client
            .get(format!("{base}/download/{col}/{id}"))
            .send()?.bytes()?;
        fs::write(format!("{band}.tif"), &bytes)?;
        println!("Downloaded {band}: {:.1} MB",
                 bytes.len() as f64 / 1e6);
    }

    println!("Compute NDVI with gdal_calc.py or the gdal crate");
    Ok(())
}
using HTTP, JSON3, ArchGDAL, Plots

base = "http://localhost:8400"

# Search
resp = HTTP.get("$base/stac/search", query=Dict(
    "bbox" => "12.4,55.6,12.7,55.75",
    "collections" => "sentinel-2-l2a", "limit" => 100))
items = JSON3.read(resp.body).features

# Download helper
function dl(band)
    item = first(filter(i -> contains(String(i.id), band), items))
    r = HTTP.get("$base/download/$(item.collection)/$(item.id)")
    path = tempname() * ".tif"
    write(path, r.body)
    ArchGDAL.read(path) do ds
        Float64.(ArchGDAL.read(ds, 1))
    end
end

red, nir = dl("B04"), dl("B08")
ndvi = @. ifelse((nir + red) > 0, (nir - red) / (nir + red), 0.0)

heatmap(ndvi[end:-1:1, :], c=:RdYlGn, clims=(-0.2, 0.8),
        title="NDVI โ€” Copenhagen", size=(800, 800))
savefig("ndvi.png")
# Search the STAC catalog
curl -s "http://localhost:8400/stac/search?bbox=12.4,55.6,12.7,55.75" | jq '.features[].id'

# Download bands
curl -o B04.tif "http://localhost:8400/download/sentinel-2-l2a/S2C_33UUB_20250614_0_L2A_B04"
curl -o B08.tif "http://localhost:8400/download/sentinel-2-l2a/S2C_33UUB_20250614_0_L2A_B08"

# Compute NDVI with GDAL
gdal_calc.py -A B08.tif -B B04.tif --outfile=ndvi.tif \
  --calc="where((A+B)>0, (A.astype(float)-B)/(A+B), 0)" --type=Float32

# List collections
curl "http://localhost:8400/collections"

# Network status
curl "http://localhost:8400/nodes"

๐Ÿง  MCP โ€” AI Agent Access

Give AI coding tools direct access to the grid via Model Context Protocol.

# Claude Code / Codex / Cursor โ€” add to ~/.mcp.json:
{
  "mcpServers": {
    "earthgrid": {
      "command": "earthgrid-mcp",
      "args": ["--api-key", "your-key", "--api-url", ""]
    }
  }
}

# Then in your AI tool:
# "Search the grid for Sentinel-2 data over Sรผdtirol"
# "Enqueue a fetch for tile 32TPS, 2024-2026"
# "Show me the current coverage map"

๐Ÿš€ Get Started

Want to run your own node? Check the installation guide and join the network.

๐Ÿ“– Installation Guide on GitHub