Build on NE-PULSE
The network's earthquake events and live state are open: no key, no sign-up, free. Use them for an alert bot, a school display, a map, a research notebook, or connect your own sensor and make the network denser.
Experimental research network, not an official warning service. Events can be wrong or retracted; show that to your users. For safety decisions follow the national seismological service and the Ministry of Emergency Situations.
Quick start
Everything is plain HTTPS + JSON. Try it:
shellcurl https://waiver-fans-edward-scsi.trycloudflare.com/api/v1/events?limit=5
curl https://waiver-fans-edward-scsi.trycloudflare.com/api/v1/networkbase URL: https://waiver-fans-edward-scsi.trycloudflare.com
Endpoints
| Method | Path | What you get |
|---|---|---|
| GET | /api/v1/events?limit=50 | Recent events, newest first (limit 1–200; add &all=1 to include candidates). |
| GET | /api/v1/events/{id} | One event with its full decision log: every check the network made and why. |
| GET | /api/v1/events/{id}/traces | Short anonymous waveform snippets recorded around that event. |
| GET | /api/v1/network | Live state: connected and listening phones, active areas (10 km, only if ≥3 phones), noise trace, active events. |
| GET | /api/v1/history?hours=24 | Per-minute network statistics (phones, samples, triggers, events). |
| GET | /api/v1/config | The detection settings in force (levels, thresholds, algorithm version). |
| WS | /ws/public | Live push: the network state about every second, and every event update the moment it happens. |
Live feed
Open one WebSocket and you get two message types: `network` (the same body as /api/v1/network) and `event` (an event was created or changed level). No polling needed.
javascript// Live: every event update, the moment it happens
const ws = new WebSocket("wss://waiver-fans-edward-scsi.trycloudflare.com/ws/public");
const seen = new Set();
ws.onmessage = (m) => {
const msg = JSON.parse(m.data);
if (msg.type !== "event") return; // "network" = live state
const e = msg.event;
if (e.level === "confirmed" && !e.retracted && !seen.has(e.id)) {
seen.add(e.id);
const t = new Date(e.originT * 1000).toLocaleTimeString();
console.log(`Earthquake confirmed ${t} near ${e.lat}, ${e.lng} (±${e.locUncKm} km)`);
// post to your Discord webhook / Telegram channel / school display here
}
};Event object
The fields you will use most:
- id
- Stable event id, e.g. NP-20261011-091500-1
- level
- candidate → corroborated → confirmed (only confirmed should alert people)
- originT
- Estimated start time, Unix seconds (±originUncSec)
- lat, lng
- Estimated epicentre, rounded (±locUncKm); depthKmAssumed is a model value, not measured
- levelTimes
- When each level was reached, Unix seconds
- cells
- Areas (10 km) that felt it: first arrival time, peak acceleration, intensity
- retracted
- true if the event was withdrawn; show it, don't hide it
Examples
A tiny alert bot: print (or post to Discord/Telegram) every newly confirmed earthquake.
↑ Live feed
Research: load the last day of network statistics into pandas.
pythonimport requests, pandas as pd
API = "https://waiver-fans-edward-scsi.trycloudflare.com"
h = requests.get(f"{API}/api/v1/history", params={"hours": 24}).json()
df = pd.DataFrame(h["minutes"])
df["time"] = pd.to_datetime(df["t"], unit="s")
print(df[["time", "connected", "eligible", "samples", "picksAccepted", "events"]].tail())
events = requests.get(f"{API}/api/v1/events", params={"limit": 200}).json()["events"]
print(pd.DataFrame(events)[["id", "level", "originT", "lat", "lng", "locUncKm"]])Just want alerts? The Telegram bot does it already, no code needed: @ne_pulse_bot
Connect your own sensor
Anything with an accelerometer can join: an ESP32 + MPU6050 (~$5), a Raspberry Pi, an old phone. It must lie still and fixed; that is what makes a good sensor. Three steps:
- 1Register once: GET /api/v1/challenge, solve the small proof-of-work (find a nonce so that SHA-256(challenge + ":" + nonce) starts with `difficulty` zero bits), then POST /api/v1/devices. Keep the deviceId and secret.
- 2Open a WebSocket to /ws/device and send a hello with your 1 km area (row/col of the 1 km grid; the exact location is never sent).
- 3Send a batch every second: sample times as offsets from t0 (0.1 ms units) and x, y, z in mm/s² with gravity included, at 50–100 Hz. The server filters, detects and votes exactly as for phones.
python# pip install requests websockets
import asyncio, hashlib, json, math, time, requests, websockets
API = "https://waiver-fans-edward-scsi.trycloudflare.com"
def solve(challenge, bits): # proof-of-work, ~1 s
n = 0
while True:
h = hashlib.sha256(f"{challenge}:{n}".encode()).digest()
if int.from_bytes(h, "big") >> (256 - bits) == 0:
return str(n)
n += 1
def cell_1km(lat, lng): # same grid as the site
kmdeg = 2 * math.pi * 6371.0088 / 360
dlat = 1 / kmdeg
row = math.floor((lat + 90) / dlat)
latc = -90 + (row + 0.5) * dlat
n = max(1, round(360 * kmdeg * max(math.cos(math.radians(latc)), 1e-3)))
return row, min(n - 1, math.floor((lng + 180) / (360 / n)))
# 1. register once (store these two values)
ch = requests.get(f"{API}/api/v1/challenge").json()
dev = requests.post(f"{API}/api/v1/devices", json={
"challenge": ch["challenge"], "nonce": solve(ch["challenge"], ch["difficulty"]),
"platform": "other", "consent": "v1"}).json()
def read_accel(): # <- your sensor here, m/s^2 incl. gravity
return 0.0, 0.0, 9.81
async def main(lat, lng):
row, col = cell_1km(lat, lng)
async with websockets.connect(API.replace("http", "ws", 1) + "/ws/device") as ws:
# 2. hello with the 1 km area
await ws.send(json.dumps({"type": "hello", "deviceId": dev["deviceId"], "secret": dev["secret"],
"platform": "other", "client": "my-sensor/0.1", "location": {"grid": 1, "row": row, "col": col}}))
seq = 0
while True: # 3. one batch per second, ~100 Hz
t0 = time.time() * 1000
dt, x, y, z = [], [], [], []
while time.time() * 1000 - t0 < 1000:
ax, ay, az = read_accel()
dt.append(round((time.time() * 1000 - t0) * 10))
x.append(round(ax * 1000)); y.append(round(ay * 1000)); z.append(round(az * 1000))
await asyncio.sleep(0.01)
seq += 1
await ws.send(json.dumps({"type": "batch", "seq": seq, "t0": t0, "dt": dt,
"x": x, "y": y, "z": z, "clock": {"offset": 0, "rtt": 0}}))
asyncio.run(main(41.31, 69.28)) # your approximate locationReady firmware for ESP32 + MPU6050 is in the source code under firmware/esp32-station.
Rules
- Free. No key needed for reading. Please cache: events change at most every few seconds.
- Rate limits apply per IP; if you get HTTP 429, slow down.
- Privacy is built in: positions are only published as 10 km areas with at least 3 phones; no device ids, no raw phone data are public.
- Say where the data comes from ("NE-PULSE phone network") and that it is experimental.
- Don't connect a sensor that moves, and don't fake data; the network rejects inconsistent sources and that only hurts everyone's alerts.
Coming next
- • Webhooks: register a URL and get a POST the moment an event is confirmed (for school bells, displays, channels), with a free key.
- • Open research datasets: anonymised noise statistics per area and labelled recordings from participants who agreed.
Not built yet. Tell us what you would make with them.