AMBER Angels

A distributed sensor network
for missing-persons response.

Architecture, scoring engine, FEMA pipeline, mobile runtime, and dashboard.

Backend
FastAPI · PostgreSQL · PM2
Edge
Expo · OpenALPR · YOLOv8
Web
Next.js 14 · Mapbox
Status
Pilot · Carrollton, GA
01 / 15
System Overview

Idle by default. Activated by IPAWS.

Three sources feed the alert pipeline — FEMA IPAWS CMAS (5 min), FEMA IPAWS EAS (2 min), and NCMEC RSS (30 min). Between alerts, the inference pipeline is fully idle — no continuous capture, no always-on scanning. When a qualifying alert fires, volunteers in the affected area are notified and the pipeline activates per-mission.

01 · POLL

FEMA IPAWS

CAP XML polled every 5 min. Plate + vehicle profile parsed and written to watchlist.

02 · DISPATCH

Volunteers notified

Push to enrolled volunteers in alert polygon. Mount phone, tap START.

03 · INGEST

Unified worker

Drone RTMP + phone foreground service feed shared frame queue. ALPR + YOLO run per frame.

04 · SCORE

Composite confidence

5-second window. ALPR + YOLO corroboration. HIGH / PROBABLE / raw classification.

05 · ESCALATE

Coordinator gate

Discord webhook fires with full verdict. Human review before any LE notification.

AAAMBER Angels
02 / 15
Architecture

Three planes. Strict boundaries.

Edge Plane
expo · android+ios

Mobile client

React Native 0.81. Foreground Service for background scanning. GPS at ~1Hz.

drone · rtmp

Drone stream

nginx exec_push pipes JPEG frames into the unified worker. Same path as phone.

phase 2 · dji msdk

DJI MSDK 5.17

Native Kotlin module. Mavic 3, Mini 4 Pro, Air 3, Avata.

Coordination Plane
fastapi · py 3.10

API + ingestion

POST /ingest/frame. JWT auth. slowapi rate limiting. Async SQLAlchemy.

unified_worker.py

Frame pipeline

One process. Shared queue across all sources. ALPR + YOLO + scoring.

postgresql

Detection store

Events + watchlist + telemetry. Time-windowed retention. No raw video.

Integration Plane
inbound · alerts

2 Active + 1 Staged

FEMA IPAWS CMAS (5 min) · FEMA IPAWS EAS (2 min) · NCMEC RSS (30 min). amber.alert.gov staged — DNS unresolvable.

outbound · alerts

Discord webhook

Full verdict embed: plate + confidence + frame + vehicle match.

web · next.js 14

Coordinator dashboard

Mapbox dark. /map · /admin · /missions · /leaderboard · /debrief.

Hard constraint: there is no write path to disk for raw frames. Frames hold in RAM only for inference duration; only structured detection metadata persists past the worker.
AAAMBER Angels
03 / 15
The Unified Worker

One process. One frame queue.

Every source — drone RTMP, phone background service, phase-two DJI MSDK — feeds into the same in-process queue. No duplicated inference paths, no per-source forks. Non-matching frames are deleted from disk immediately after inference. When a HIGH_CONFIDENCE watchlist match fires, one golden evidence frame is saved and attached to the coordinator Discord alert.

Sources

Drone RTMP
nginx exec_push
→ frames/drone/
Phone Camera
Android Foreground Svc
POST /ingest/frame
DJI MSDK 5.17
native Kotlin module
phase 2

unified_worker.py

pid:worker · pm2-managed
01
Frame queue (asyncio)

Shared across all sources. Backpressure-aware.

02
OpenALPR — plate text + confidence

Local inference. Fuzzy-matched against watchlist.

03
YOLOv8-nano — body type + dominant color

HSV K-means on upper 60% of bbox; confidence per frame.

04
Plate Recognizer (opt-in cloud)

Make/model enrichment on high-confidence reads only.

05
AggregationService → composite score

5-second window. ALPR + YOLO corroboration. Classification.

Outputs

Postgres
events table
raw_summary jsonb
Discord
webhook embed
frame attached
Dashboard
Next.js /map
live event feed
AAAMBER Angels
04 / 15
The Differentiator

Composite confidence — not just plate text.

Plate-only ALPR is roughly 30% false-positive in real-world conditions. We score every detection against two independent signals: ALPR text quality and YOLO vehicle corroboration. Both must agree before we open a HIGH_CONFIDENCE event.

Stored on every event as raw_summary.vehicle_corroboration — coordinators inspect exactly what contributed to the score.

# aggregation_service.py aggregate_confidence = 0.50*max_alpr + 0.30*mean_alpr + 0.10*median_alpr + repetition_bonus # +5 / +10 (2 / 3+ frames) + consistency_bonus # +5 (≥75% agree) quality_penalty # blur, skew, partial + yolo_corroboration # color + type + conf · max +7
Component
Weight
Highest single-frame ALPR confidencemax(alpr.confidence)
50%
Mean ALPR confidencemean(alpr.confidence)
30%
Median ALPR confidencemedian(alpr.confidence)
10%
Repetition bonus+5 (2 frames) / +10 (3+ frames)
additive
Consistency bonus+5 if ≥75% reads agree
additive
Quality penaltymotion_blur, skew, partial_plate, far_distance
subtractive
YOLO color consistency≥75% frames agree on color
+2
YOLO body-type consistency≥75% frames agree on type
+2
YOLO confidence signal(avg_conf − 0.5) × 6
±3
AAAMBER Angels
05 / 15
Aggregation Window

Five-second window. Three classifications.

Detections grouped by drone and plate-prefix get fuzzy-matched together inside a five-second sliding window. The window decides whether the event opens for coordinator review, fires Discord, or stays a raw observation.

HIGH_CONFIDENCE

≥ 90.0
Min score90.0
Min frames3
Dominant ratio≥ 75%
ActionDiscord fire

PROBABLE

≥ 75.0
Min score75.0
Min frames2
Dominant ratio≥ 60%
ActionEvent opened

RAW

< 75.0
Score floor55.0
Min frames1
Dominant ratio
ActionLogged only

OCR confusion pairs · baked into fuzzy matcher

O0 I1 B8 S5 Z2 G6 + single insertion / deletion tolerance
AAAMBER Angels
06 / 15
Alert Ingestion · National + State + Child Cases

Three polling sources — two active, one staged.

Three background async tasks cover every layer of the US missing-persons alert system. FEMA IPAWS CMAS (5-min poll, fema_connector.py) and FEMA IPAWS EAS (2-min poll, amber_alert_poller.py) — both federal CAP XML, different endpoints, share a single Postgres-backed dedup table so the same alert never fires twice. NCMEC RSS (30-min poll) — all 50 state feeds individually; fires a Discord cross-reference when a new NCMEC case overlaps an active FEMA vehicle target in the same state. amber.alert.gov is implemented but staged — the hostname is currently unresolvable, so it's dark.

Both FEMA pollers share a processed_alerts dedup table backed by Postgres — survives PM2 restarts. Re-poll uses COALESCE upsert; a bare-plate entry gets backfilled with vehicle profile data on a later poll.

SOURCE
PROGRAM / COVERAGE
INTERVAL
FEMA IPAWS CMAS
AMBER · Levi's · Silver · Mattie's · Purple · MIPA · EMA · Blue — federal CAP XML
5 min
FEMA IPAWS EAS
Same CAP XML schema, separate federal endpoint — higher frequency, shared dedup table
2 min
NCMEC RSS
50 individual state feeds — missing-child cases + cross-ref to active vehicle targets
30 min
amber.alert.gov
DOJ national registry — staged only, DNS currently unresolvable
STAGED
fema_connector.py · amber_alert_poller.py · ncmec_poller.py
# FEMA CMAS — every 300 s (fema_connector.py)
async def fema_background_loop():
  for alert in parse_cap(await http.get(FEMA_CMAS_URL)):
    await watchlist.upsert(plate, color, body, make)
    await dispatcher.notify_volunteers(polygon, cap_code)

# FEMA EAS — every 120 s (amber_alert_poller.py)
async def amber_background_loop():
  for alert in parse_cap(await http.get(FEMA_EAS_URL)):
    await watchlist.upsert(alert)  # shared dedup table

# amber.alert.gov — STAGED (DNS unresolvable · AMBER_GOV_URLS = [])
# async def poll_ambgov(): ...  ← dark until DNS resolves

# NCMEC — every 1800 s, 50 state feeds in parallel
async def poll_ncmec():
  for state in US_STATES:
    cases = parse_feed(await http.get(NCMEC_URL.format(state)))
    await db.upsert_cases(cases)
    await cross_ref_vehicle_targets(cases, state)
AAAMBER Angels
07 / 15
Active Alert

Push lands → mission start in two taps.

Start mission

IPAWS-driven · geofenced · single action

When IPAWS issues an alert in the volunteer's polygon, an Expo push notification fires. They see the suspect descriptor, alert ID, last-known direction — and one tap to start.

  • 01Geofenced push — only volunteers inside the alert polygon are notified expo-notifications
  • 02Suspect descriptor — vehicle make/color/body parsed from CAP and shown verbatim
  • 03Single-action start — no menus; tap to enter CameraScreen with foreground service running
  • 04Stand-down on alert clear — IPAWS cancellation deactivates watchlist and fires a Discord stand-down embed; coordinator coordinates wind-down
AAAMBER Angels
08 / 15
Mobile Runtime

Lock the phone. Keep scanning.

Background mode

Android Foreground Service · persistent notification

The flagship piece of the mobile stack. modules/phone-camera runs Camera2 + GPS + upload as an Android Foreground Service so the user can drive with their navigation app open. AA does not need to be visible.

  • 01Foreground service — Camera2 + expo-location stay alive when screen off / app backgrounded
  • 02Persistent notification — live frame count + one-tap Stop without unlocking
  • 03Per-frame upload — POST /ingest/frame with GPS attached at ~1Hz
  • 04Out-of-range warning — fires when volunteer drifts outside the alert polygon
AAAMBER Angels
09 / 15
Phase 2 · Drone Swarm

Relinquish your drone. Coordinator takes control.

DJI connection

Power on at home → swarm join → coordinator dispatches

A pilot who can't physically respond still contributes. They power on their drone, open AutonomousMissionScreen, and walk away. The coordinator sees the drone on the map at its home GPS, clicks anywhere on the map to place an observation pin, checks the ADS-B airspace advisory, and dispatches. Pilot taps Accept & Launch — the DJI SDK uploads the waypoint and executes. No pilot on scene required.

  • 01Map-click dispatch — coordinator clicks road/corridor on map to drop obs pin; no coordinate typing. ADS-B advisory lists aircraft within 5 nm below 3,000 ft before confirm.
  • 02Mission lifecyclepending → dispatched → uploading → executing → completed | aborted. Timeout: 30 min unaccepted, 4 h executing.
  • 03VLOS / BVLOS — VLOS enforces 400 m radius (Part 107). BVLOS Tactical unlocks extended range per drone when admin records FAA waiver number.
  • 04Same inference pipeline — drone RTMP → unified worker → ALPR + YOLO → composite score
AAAMBER Angels
10 / 15
Coordinator Dashboard

Live mission map. Dispatch from coverage gaps.

Live mission map

Next.js 14 · Mapbox dark · role-tiered · drone dispatch layer

Coordinators see two layers Flock can't give them: (1) exactly where fixed cameras have zero coverage, and (2) which volunteer drones are online right now and where. Click a drone icon → click anywhere on the map to place the observation pin → check the ADS-B airspace advisory → dispatch. Flock DroneSense costs $3 k–$6 k / drone / year and requires an operator on scene. Ours costs $0 and the pilot can be at home.

  • 01Map-click obs placement — click any point on the map to set obs pin; no coordinate entry. ADS-B advisory (OpenSky) lists live air traffic before dispatch confirms.
  • 02Swarm layer — amber drone icons at home GPS; grayed after 5 min offline. Click icon to open dispatch modal.
  • 03Mission panel — live sidebar panel: every active/recent mission with status, progress bar, and fly-to button
  • 04Coverage gap targeting — Flock bucket = 0 cells highlighted inside alert area; NCMEC cross-ref shown in sidebar
AAAMBER Angels
11 / 15
Integrations

What we depend on, and what we own.

Every external dependency was chosen so the system is auditable and self-hostable. The ML stack is open-source end-to-end. Self-hostable on any Ubuntu 20.04+ VPS with PostgreSQL, Node 18+, Python 3.10+.

inbound · alerts

FEMA IPAWS

CMAS (5 min) + EAS (2 min). CAP XML. AMBER, Silver, Mattie's, Purple, MIPA, Blue.

CAP 1.2 · no auth
inbound · ncmec

NCMEC RSS

50 individual state feeds every 30 min. Missing-child cross-reference to active vehicle targets.

RSS · no auth
inbound · airspace

OpenSky Network

Live ADS-B air traffic. Airspace advisory in dispatch modal — lists aircraft within 5 nm below 3,000 ft.

REST · public API
edge · ml

OpenALPR + YOLOv8-nano

Local ALPR + body-type classification. HSV K-means for color. All open-source.

AGPL · self-hosted
edge · ml

Plate Recognizer (opt-in)

Cloud make/model enrichment on high-confidence reads. Quota-conserved.

REST · throttled
edge · drone

DJI MSDK 5.17

Native Kotlin. Mavic 3, Mini 4 Pro, Air 3, Avata. Part 107 gated. iOS pending.

Android first
mobile

Expo + React Native 0.81

Bare workflow. EAS Build cloud — no Mac required for iOS. OTA via expo-updates.

EAS · OTA
outbound · ops

Discord webhooks

Coordinator alerts. Embed: plate + confidence + golden frame + vehicle match verdict.

webhook · per-channel
outbound · sms

Twilio SMS (opt-in)

HIGH_CONFIDENCE escalation path. Silently skips if env vars absent. Configured via TWILIO_* vars.

optional · REST
backend

FastAPI + PostgreSQL

Async SQLAlchemy. JWT auth. slowapi rate limit. PM2 supervised.

Python 3.10
web

Next.js 14 + Mapbox

Mapbox GL dark. 7 toggleable map layers. Mission dispatch + swarm panel.

App Router
infra

DigitalOcean + nginx

Single Droplet, self-hostable. nginx reverse-proxy. Let's Encrypt TLS. PM2 supervised.

Ubuntu 20.04
AAAMBER Angels
12 / 15
Privacy Architecture

Enforced by the topology, not by policy.

Non-matching frames are never written to disk. They pass through RAM for inference and are deleted immediately — no accumulation of plate sightings, no non-alert vehicle data. The platform is event-triggered; between alerts, the inference pipeline is fully idle.

When a HIGH_CONFIDENCE watchlist match fires, one golden evidence frame is saved — attached to the coordinator Discord alert inline so they can see exactly what the drone saw. That frame is stored with the detection record. Telemetry purges after 90 days. Detection records and golden frames purge after 1 year. The watchlist holds only plates from active government alerts, never our own sightings database.

Edgenon-matching frame
Clouddenied · deleted after inference
WorkerHIGH_CONFIDENCE match frame
Storagegolden frame · coordinator Discord
Pipelineface / biometric
Clouddenied · not in pipeline
Edgevolunteer location
Cloudself-only · 90d retention
Workerplate from non-alert vehicle
Storagedenied · watchlist-gated
Workerwatchlist-matched event
Discordallowed · structured embed
Coordinatorreviewed + confirmed event
LE Partnerallowed · post human gate
AAAMBER Angels
13 / 15
System in Action

Real logs.
June 7, 2026.

Demo AMBER Alert injected. Two volunteers — one phone, one drone — both hit 99% peak confidence on plate YVJ024.

47 s
alert-to-hit · phone
4 s
stream-to-hit · drone
99%
peak confidence · both
193
alerted events · session
ambers-angels-api · 2026-06-07 UTC
── Phone Pipeline ─────────────────────────────────────────────
14:15:57ALERTFEMA IPAWS — AMBER Alert ingested  plate=YVJ024 · white sedan · Carroll Co., GA
14:15:57NOTIFY3 volunteers in coverage area — mission push sent
14:16:00MISSIONphone-1 · mission opened · camera scanning active
14:16:20SCAN/ingest/frame · 1 frame/sec · OpenALPR + YOLO
14:16:43READplate=YVJ024 · conf=90.2%→95.15% composite · MATCH  watchlist hit · vehicle: white sedan ✓
14:16:43ALERTDiscord dispatched · frame attached · ← 47 s alert-to-hit
14:16:48PEAKconf=99.00% · aggregation window saturated
── Drone Pipeline (DJI Avata, same session) ───────────────────
15:09:00STREAMDJI Avata → rtmp://amberangels.org/live/avata
15:09:05PROCrtmp_monitor → ffmpeg spawned · 3 fps frame extraction
15:09:14READplate=SYVJ0Z4 · conf=86.8% · PROBABLE  fuzzy: len=7≠6 · Z↔2 OCR variant · dismissed
15:09:19READplate=YVJ024 · conf=86.81% · MATCH  len=6 ✓ · 0-char delta · escalating
15:09:20AGGaggregation window filling · composite confidence rising...
15:09:34PEAKconf=99.00% · sustained · 193 alerted events · 5m 42s
15:09:34ALERTDiscord dispatched · frame attached · ← 4 s stream-to-alert
AAAMBER Angels
14 / 15
AMBER Angels

Composite scoring,
evidence-only frame retention,
privacy by topology.

FastAPI · PostgreSQL · OpenALPR · YOLOv8 · FEMA IPAWS · Expo · React Native · DJI MSDK · Next.js 14 · Mapbox · Discord · PM2
Founder
Grant Lindberg
Email
info@amberangels.org
Web
amberangels.org
15 / 15