# Project export: anchor: real-time + graph NN phone scam defense for elders

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Anchor is an ambient device that protects vulnerable older adults from phone scams, running real-time detection on an NVIDIA Jetson and a graph NN that learns risky relationship patterns over time.
- Devpost: https://devpost.com/software/anchor-real-time-phone-scam-defense-for-older-adults
- GitHub: https://github.com/hanshaunlee/anchor
- Demo: https://treehacks-anchor-demo-website.onrender.com/
- Video: https://www.youtube.com/embed/_-pfLnr00gM?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — hanshaun (18 commits), Cursor (16 commits)

## Devpost submission (written by the team)

### Inspiration

Phone scams are a uniquely high-pressure channel: the victim has to respond in the moment. In 2025, people over 60 reported $5B in losses across 150K complaints to the FBI. Elder abuse is also massively underreported, with one estimate suggesting only 1 in 24 cases are reported to authorities, in part due to victims' shame. As AI voice tools improve, phone-based scams get even more convincing, raising the stakes for real-time intervention for vulnerable populations. We wanted to build something that prioritized users' dignity, that was immediately useful in a home after 36 hours, and that was hard enough to stretch us. One-sentence value proposition Anchor is an ambient phone-call safety layer that detects scam pressure in real time and prompts verification, without requiring an app, broadband, or surveillance. What we built Anchor is a physical device that sits near an older adult’s phone and listens only to their side of the conversation. It streams transcriptions and voice stress signals into an on-device risk pipeline and, when risk is high (e.g. "You need the code sent to my phone?"), interrupts with a calm voice prompt that encourages verification before money or credentials leave the home. We treated “no app, no training, no broadband” as a product requirement, not a nice-to-have. We also prioritized dignity: the goal is a real time auditory intervention, not shaming or constantly notifying family. The edge device runs a five-stage ZeroMQ microservices pipeline: audio capture with real-time resampling (44.1kHz → 16kHz), Whisper small.en for speech-to-text (~700ms per 2.6s window), a two-tier threat detection system combining exact phrase matching against FBI/FTC scam databases with semantic similarity scoring via sentence-transformer embeddings (all-MiniLM-L6-v2), a quantized LLM (Qwen2.5-0.5B-Instruct, Q4 GGUF, ~500ms inference) for context-aware warning generation, and Piper neural TTS for natural voice output. If the home has reliable internet, Anchor also supports Connected Mode: a cloud Independence Graph with a Heterogeneous Graph Transformer (HGT) that links events over time, because scams are rarely one-off interactions. The cloud backend scores entity-level risk via HGT plus rule fusion with calibrated thresholds. Connected Mode can alert a trusted loved one when a concerning pattern occurs. Risk scoring runs in-process in the API/worker; Modal is used for GPU-based GNN training (multi-seed structured synthetic sweeps), not live inference, and Claude is used to generate short risk narratives and plain-language explanations from graph motifs and timelines when configured. Architecture (two layers) Edge baseline (offline-first): Microphone capture → on-device speech recognition → stress/activation signals → tactic scoring Runs on NVIDIA Jetson Orin Nano (6-core ARM, 1024 CUDA cores, 8GB unified memory) with total pipeline memory footprint of ~2.1GB and power draw of ~6W Two-tier threat detection: Tier 1 uses regex/substring matching against 100+ known scam phrases for instant (<1ms) high-confidence triggers; Tier 2 computes cosine similarity between transcript embeddings and 50+ scam scenario descriptions Real-time loop ends in a voice intervention (not a text notification the elder won’t see) Connected Mode (cloud graph): Converts “concerning event summaries” into nodes/edges in an Independence Graph Uses a Heterogeneous Graph Transformer (HGT) over the same schema (entity, session, event, utterance nodes; co_occurs, next_event, mentions edges) to score longitudinal risk Fuses rule-based motif scoring with calibrated HGT outputs (with optional conformal decision bands) to drive bounded escalation Generates bounded, action-oriented alerts to a trusted contact (help, not surveillance) User flow (example: “bank security” scam) 1) Anchor is ambient: It’s a physical device by the older adult’s phone. It listens only to the older adult’s side. 2) On-device detection (Jetson): During a call, the Whisper model transcribes the elder's speech in real-time while the two-tier detection system analyzes each utterance—Tier 1 flags exact scam phrases instantly, Tier 2 computes semantic similarity against known manipulation patterns. Anchor detects risk signals from the older adult's words and voice state (e.g., "a verification code just came to my phone," "should I read it to you?" + rising stress/urgency). 3) Dignity-first interruption: When risk exceeds the intervention threshold, the Qwen LLM generates a context-specific warning (e.g., tailored to gift card vs. tech support vs. government impersonation tactics), and Piper TTS speaks it through the device's speaker. Anchor calmly inserts a “pause to verify” prompt: “Quick safety check—before sharing any code, let’s hang up and call the bank using the number on your card.” 4) Help without surveillance: If risk is severe or the older adult asks for help, Anchor sends a bounded alert to a designated loved one with suggested next steps (no raw audio sharing). 5) Connected Mode (cloud Independence Graph + HGT): With reliable internet, Anchor uploads bounded event summaries to the cloud, where an HGT scores entity-level risk across linked sessions (repeat contact, escalating urgency, isolation → payment pressure), and rule + model fusion determines escalation. User-driven decisions, focusing on users' dignity first We designed around real constraints: many older adults won’t maintain an app, and they don’t want to be surveilled. In Pew’s most recent broadband tracking (June 2025), only 70% of adults 65+ report having home broadband, and adoption drops to 54% in households earning under $30k, which is exactly the cohort for whom a single scam can be financially devastating (so Anchor can’t depend on always-on internet). That led to: Offline-first safety path on the device One-sided listening as a privacy boundary Minimal disclosure: alerts focus on “what to do next,” not full call content Roadblocks + what we changed We had to trade off accuracy and latency across distributed hardware. Early on we were too ambitious with overlapping agents; they duplicated work and struggled to reach consensus under real-time constraints. On the edge side, we initially tried running a larger LLM (Qwen2.5-1.5B) which took 13+ seconds per inference. We decomposed the threat detection into 4 stages (described in the architecture section), and we found template-completion prompting with a 0.5B model reduced latency to ~500ms while preserving context-awareness. We also discovered that semantic similarity alone produced false positives on benign phrases like "gift card for grandson's birthday," so we added explicit benign context pattern matching as an override layer. We refocused on user needs and separated responsibilities cleanly through fast on-device detection + a cloud graph layer for longitudinal patterns. We also separated training (Modal GPU HGT runs, structured synthetic data sweeps) from in-process inference (rule + HGT fusion in the API/worker) to keep latency predictable and infrastructure simpler. Privacy approach We avoided building a monitoring tool. Anchor is designed around minimum necessary disclosure: No raw audio retention No full transcript sharing to third parties Only bounded summaries for concerning events (and only to a designated trusted contact) What we’d do next Finish the edge → cloud integration so the device can reliably stream event summaries into the Independence Graph and the GNN can improve longitudinal detection. We’d also expand scenario coverage and tune the intervention ladder to reduce false positives without humiliating the older adult.

## README (from the GitHub repository)

# Anchor

Backend and dashboard for an edge voice companion that helps protect elders from fraud. The edge sends **structured event packets** (transcripts, intents, financial events—no raw audio). The backend ingests them, builds a household **Independence Graph**, scores risk (GNN + rules), explains via motifs and subgraphs, and surfaces **risk signals**, **watchlists**, and recommendations. **Read-only for money:** it flags and recommends; it does not execute financial transactions.

```
Edge (batch) → POST /ingest/events → API (FastAPI) + Worker
  → LangGraph pipeline: ingest → normalize → graph_update → Financial Agent
  → risk_score (shared service: HGT or rule fallback) → explain → consent_gate → watchlist → persist
  → Supabase (source of truth) | PyG in-memory (GNN) | Neo4j (optional viz)
  → Next.js dashboard (alerts, protection, agents, graph, replay)
```

| Stack | Role |
|-------|------|
| **Supabase** | Postgres + Auth; sessions, events, entities, risk_signals (fingerprint upsert), watchlists, rings, calibration, agent_runs |
| **FastAPI** | REST + WebSocket `/ws/risk_signals`; routers: households, sessions, alerts, risk_signals, protection, explain, ingest, investigation, agents, outreach, etc. |
| **LangGraph** | Single pipeline: normalize (deterministic) → graph → Financial Agent → risk_score → explain → persist |
| **PyG** | HGT (entity risk + embeddings); GraphGPS/FraudGT for experiments/Elliptic only |
| **Next.js** | Dashboard: auth, protection, alerts (timeline, graph, similar incidents, explain), Run Investigation, agents (catalog, trace), replay |
| **Modal** | Training (HGT, Elliptic); not API/pipeline |

- **Risk:** One place—`domain/risk_scoring_service.py`. Returns calibrated_p, optional rule_score, fusion (0.6×calibrated + 0.4×rule). Conformal bands when calibrated; drift invalidates conformal until recalibration. Rule-only fallback when GNN unavailable.
- **Agents:** Supervisor (INGEST_PIPELINE, NEW_ALERT, NIGHTLY_MAINTENANCE), Financial Security, Graph Drift, Evidence Narrative, Ring Discovery, Calibration, Red-Team, Recurring Contacts, Caregiver Outreach. Status/trace via `GET /agents/status`, `GET /agents/trace`.
- **Graph:** `domain/graph_service.build_graph_from_events`; Independence Graph with MIS-based `independence_violation_ratio` used in rule scoring.

**Quick start**

```bash
pip install -e ".[ml]"   # from repo root
./scripts/run_api.sh     # → http://127.0.0.1:8000
cd apps/web && npm i && npm run dev   # → http://localhost:3000
```

- Pipeline once: `./scripts/run_worker.sh --once --household-id <uuid>`
- Train HGT: `make train` or `make modal-train`
- Test: `make test`

**Docs:** [SETUP.md](SETUP.md) — full setup (Supabase, env, Neo4j). [README_EXTENDED.md](README_EXTENDED.md) — file-by-file reference, schema, event packet, API contracts, agents, tests.

**Repo:** `apps/api/` (FastAPI, pipeline, domain), `apps/worker/` (jobs, persist), `apps/web/` (Next.js), `ml/` (models, graph, train, Modal), `config/` (settings, graph schema), `db/` (bootstrap, migrations 001–024), `scripts/`, `tests/`.

*Python 3.11, FastAPI, Supabase, LangGraph, PyTorch/PyG, Next.js 14, Modal.*


## Detected evidence (automated analysis)

Indexed codebase: 362 recognized source files, 1904 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- LangChain (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 392)

```
.gitignore
apps/api/api/__init__.py
apps/api/api/agents/__init__.py
apps/api/api/agents/financial_agent.py
apps/api/api/broadcast.py
apps/api/api/config.py
apps/api/api/deps.py
apps/api/api/graph_state.py
apps/api/api/main.py
apps/api/api/neo4j_sync.py
apps/api/api/pipeline.py
apps/api/api/routers/__init__.py
apps/api/api/routers/agents.py
apps/api/api/routers/alerts.py
apps/api/api/routers/capabilities.py
apps/api/api/routers/connectors.py
apps/api/api/routers/device.py
apps/api/api/routers/explain.py
apps/api/api/routers/graph.py
apps/api/api/routers/households.py
apps/api/api/routers/incident_packets.py
apps/api/api/routers/ingest.py
apps/api/api/routers/investigation.py
apps/api/api/routers/maintenance.py
apps/api/api/routers/outreach.py
apps/api/api/routers/playbooks.py
apps/api/api/routers/protection.py
apps/api/api/routers/rings.py
apps/api/api/routers/risk_signals.py
apps/api/api/routers/sessions.py
apps/api/api/routers/summaries.py
apps/api/api/routers/system.py
apps/api/api/routers/watchlists.py
apps/api/api/schemas.py
apps/api/domain/__init__.py
apps/api/domain/action_dag.py
apps/api/domain/agents/__init__.py
apps/api/domain/agents/base.py
apps/api/domain/agents/caregiver_outreach_agent.py
apps/api/domain/agents/continual_calibration_agent.py
apps/api/domain/agents/evidence_narrative_agent.py
apps/api/domain/agents/financial_security_agent.py
apps/api/domain/agents/graph_drift_agent.py
apps/api/domain/agents/incident_response_agent.py
apps/api/domain/agents/model_health_agent.py
apps/api/domain/agents/recurring_contacts_agent.py
apps/api/domain/agents/registry.py
apps/api/domain/agents/ring_discovery_agent.py
apps/api/domain/agents/supervisor.py
apps/api/domain/agents/synthetic_redteam_agent.py
apps/api/domain/capability_service.py
apps/api/domain/claude_risk_narrative.py
apps/api/domain/connectors/__init__.py
apps/api/domain/connectors/bank_connector.py
apps/api/domain/consent.py
apps/api/domain/entities/__init__.py
apps/api/domain/entities/display.py
apps/api/domain/explain_service.py
apps/api/domain/explainers/__init__.py
apps/api/domain/explainers/pg_service.py
apps/api/domain/graph_service.py
apps/api/domain/ingest_service.py
apps/api/domain/langchain_utils.py
apps/api/domain/ml_artifacts.py
apps/api/domain/notify/__init__.py
apps/api/domain/notify/providers.py
apps/api/domain/rings/__init__.py
apps/api/domain/rings/fingerprint.py
apps/api/domain/rings/service.py
apps/api/domain/risk_scoring_service.py
apps/api/domain/risk_service.py
apps/api/domain/risk_signal_persistence.py
apps/api/domain/rule_scoring.py
apps/api/domain/similarity_service.py
apps/api/domain/utils/__init__.py
apps/api/domain/utils/time_utils.py
apps/api/domain/watchlist_service.py
apps/api/domain/watchlists/__init__.py
apps/api/domain/watchlists/normalize.py
apps/api/domain/watchlists/service.py
apps/web/.eslintrc.json
apps/web/.gitignore
apps/web/components.json
apps/web/next.config.mjs
apps/web/package.json
apps/web/postcss.config.mjs
apps/web/public/fixtures/agents_catalog.json
apps/web/public/fixtures/agents_status.json
apps/web/public/fixtures/archive/graph_evidence.json
apps/web/public/fixtures/archive/README.md
apps/web/public/fixtures/archive/scenario_replay.json
apps/web/public/fixtures/capabilities_me.json
apps/web/public/fixtures/consent_me.json
apps/web/public/fixtures/household_me.json
apps/web/public/fixtures/protection_overview.json
apps/web/public/fixtures/protection_reports.json
apps/web/public/fixtures/protection_ring_detail.json
apps/web/public/fixtures/protection_rings.json
apps/web/public/fixtures/protection_watchlists.json
apps/web/public/fixtures/risk_signal_detail.json
apps/web/public/fixtures/risk_signals.json
apps/web/public/fixtures/session_events.json
apps/web/public/fixtures/sessions.json
apps/web/public/fixtures/summaries.json
apps/web/public/fixtures/watchlists.json
apps/web/src/app/(auth)/login/page.tsx
apps/web/src/app/(auth)/logout/page.tsx
apps/web/src/app/(auth)/onboard/page.tsx
apps/web/src/app/(auth)/signup/page.tsx
apps/web/src/app/(auth)/signup/success/page.tsx
apps/web/src/app/(dashboard)/agents/page.tsx
apps/web/src/app/(dashboard)/alerts/[id]/alert-detail-content.tsx
apps/web/src/app/(dashboard)/alerts/[id]/page.tsx
apps/web/src/app/(dashboard)/alerts/page.tsx
apps/web/src/app/(dashboard)/dashboard/page.tsx
apps/web/src/app/(dashboard)/elder/page.tsx
apps/web/src/app/(dashboard)/error.tsx
apps/web/src/app/(dashboard)/graph/page.tsx
apps/web/src/app/(dashboard)/ingest/page.tsx
apps/web/src/app/(dashboard)/layout.tsx
[272 more files omitted for size]
```

### Dependencies

- apps/web/package.json: @radix-ui/react-label@^2.1.8, @radix-ui/react-scroll-area@^1.2.10, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.8, @radix-ui/react-slot@^1.2.4, @radix-ui/react-switch@^1.2.6, @radix-ui/react-tabs@^1.1.13, @supabase/supabase-js@^2.95.3, @tanstack/react-query@^5.90.21, @types/node@^20, @types/react@^18, @types/react-dom@^18, @xyflow/react@^12.10.0, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^8, eslint-config-next@14.2.35, framer-motion@^12.34.0, lucide-react@^0.564.0, next@14.2.35, postcss@^8, react@^18, react-dom@^18, recharts@^3.7.0, tailwind-merge@^3.4.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5, zod@^4.3.6, zustand@^5.0.11
- pyproject.toml: fastapi@>=0.109.0, httpx@>=0.26.0, langchain@>=0.1.0, langchain-core@>=0.1.0, langgraph@>=0.0.40, modal@>=0.64.0, numpy@>=1.24.0, pandas@>=2.0.0, psycopg2-binary@>=2.9.0, pydantic@>=2.5.0, pydantic-settings@>=2.1.0, pytest@>=7.4.0, pytest-asyncio@>=0.23.0, python-jose[cryptography]@>=3.3.0, ruff@>=0.1.0, scikit-learn@>=1.3.0, scipy@>=1.11.0, supabase@>=2.3.0, torch@>=2.2.0, torch-cluster, torch-geometric@>=2.5.0, torch-scatter, torch-sparse, uvicorn[standard]@>=0.27.0, websockets@>=12.0
- requirements.txt: anthropic@>=0.39.0, fastapi@>=0.109.0, httpx@>=0.26.0, langchain@>=0.1.0, langchain-core@>=0.1.0, langgraph@>=0.0.40, neo4j@>=5.0.0, pydantic@>=2.5.0, pydantic-settings@>=2.1.0, pytest@>=7.4.0, pytest-asyncio@>=0.23.0, python-dotenv@>=0.21.0, pyyaml@>=6.0, ruff@>=0.1.0, supabase@>=2.3.0, uvicorn[standard]@>=0.27.0, websockets@>=12.0

### Recent commits (newest first)

- Add anchor-web service with rootDir apps/web for Render monorepo
- Add HEAD support for / and /health, add /healthz for Render health checks
- docs: concise README, detailed README_EXTENDED
- Add Render deployment: render.yaml and DEPLOY_RENDER.md
- Add consolidate_sweep_seeds script
- tests: update test suite, remove tests README
- scripts: add analyze_structural_significance, export_supabase_for_gnn, generate_foreign_dataset, run_structured_sweep_local; archive stress_supervisor_matrix
- ml: update train/inference/modal scripts; add modal_structured_sweep, run_utils, synthetic generator, export_whitepaper_artifacts; bump requirements
- web: update dashboard pages, components, API client; add agent-strip, explainable-ids, implemented-agents-card; archive fixtures, remove invalidateAfterInvestigation
- api/domain: update agents, explain and risk services, watchlists; add recurring_contacts_agent, claude_risk_narrative, risk_signal_persistence
- api: update main, pipeline, schemas, routers; add explain router
- db: add risk_signals fingerprint migration and archive, remove setup_alerts_one_household
- config: update settings, remove config README
- docs: update README and README_EXTENDED
- docs: remove obsolete docs, add SETUP.md and WHITEPAPER_CHECKLIST.md
- Protection rings, ML registry & retrain, embedding 128, contract/flow tests
- Protection UI, alerts, agents, and migrations
- docs: update schema, migrations 011-014, capability/action_dag, agents narrative_reports
- Major agentic worflow edits
- Backend upgrade + frontend pairing: single risk scoring, idempotent ingest, model_available, pgvector/similar incidents, embedding-centroid, model evidence, six agents (financial + drift/narrative/ring/calibration/redteam), agents page other-agents run + trace

## Key source files (fetched from GitHub, selected and truncated for size)

### DEPLOY_RENDER.md

```markdown
# Deploy Anchor on Render

## 1. API (FastAPI)

### One-time: Connect repo and create Web Service

1. Go to [dashboard.render.com](https://dashboard.render.com) → **New** → **Web Service**.
2. Connect your GitHub account and select the **Anchor** repo.
3. Render can auto-detect `render.yaml`. If it does, it will create **anchor-api** from the blueprint. Otherwise configure manually:
   - **Name:** `anchor-api` (or any name).
   - **Region:** Oregon (or your choice).
   - **Branch:** `main`.
   - **Runtime:** Python 3.
   - **Build command:** `pip install -r requirements.txt`
   - **Start command:** `PYTHONPATH=.:apps/api uvicorn api.main:app --host 0.0.0.0 --port $PORT`
   - **Health check path:** `/health` (optional but recommended).

### Required environment variables (API)

Set these in the service **Environment** tab:

| Variable | Description |
|----------|-------------|
| `SUPABASE_URL` | Supabase project URL (Project Settings → API) |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase service_role key (keep secret) |

Without these, the API returns 503 Supabase not configured.

### Optional environment variables

- `DATABASE_URL` — for migrations (if you run them from elsewhere).
- `NEO4J_URI`, `NEO4J_USER`, `NEO4J_PASSWORD` — Neo4j for graph view sync.
- `ANTHROPIC_API_KEY` — for Explain API plain-language descriptions.
- `OPENAI_API_KEY` — optional Evidence Narrative LLM.
- `ANCHOR_ML_CONFIG`, `ANCHOR_ML_CHECKPOINT_PATH` — GNN scoring (omit for rule-only mode).

### Deploy

- **Blueprint:** After connecting the repo, use **Apply** or **New** → **Blueprint** and point to this repo; Render will create the web service from `render.yaml`.
- **Manual:** After saving the Web Service, Render will build and deploy. Each push to `main` will auto-deploy if **Auto-Deploy** is on.

Your API URL will be like `https://anchor-api-xxxx.onrender.com`. Use it as `NEXT_PUBLIC_API_BASE_URL` for the web app.

---

## 2. Web dashboard (Next.js) — optional

To run the Next.js dashboard on Render:

1. **New** → **Web Service**.
2. Connect the same repo; set **Root Directory** to `apps/web`.
3. **Runtime:** Node.
4. **Build command:** `npm install && npm run build`
5. **Start command:** `npm start` (or `npx next start`).
6. **Environment:**  
   - `NEXT_PUBLIC_API_BASE_URL` = your Anchor API URL (e.g. `https://anchor-api-xxxx.onrender.com`)  
   - `NEXT_PUBLIC_SUPABASE_URL` = your Supabase URL  
   - `NEXT_PUBLIC_SUPABASE_ANON_KEY` = your Supabase anon key  

---

## 3. Worker (background jobs)

The worker (`apps/worker`) runs pipeline jobs (e.g. ingest, risk scoring). On Render you can run it as a **Background Worker**:

- **Build:** same as API from repo root: `pip install -r requirements.txt`
- **Start:** `PYTHONPATH=.:apps:apps/api python -m worker.main --poll` (polls `processing_queue`; or use `--once --household-id <uuid>` for a one-off run).
- Set the same env vars as the API (Supabase, optional ML, etc.).

Cron-style scheduling is usually done with Render cron job
[truncated — 62 more characters]
```

### SETUP.md

```markdown
# Anchor: Setup

Single guide to get the **API**, **Supabase**, **web app**, and optional services working. Do not commit real secrets; use `.env` (gitignored) or your deployment env.

---

## 1. Supabase: project and schema

### 1.1 Create project

1. [supabase.com/dashboard](https://supabase.com/dashboard) → **New project** (org, name, **save database password**).
2. Wait for the project to be ready.

### 1.2 Run bootstrap (once)

1. **SQL Editor** in the dashboard.
2. Open **`db/bootstrap_supabase.sql`** from this repo and run it **in full** (creates enums, tables 001–007, RLS, `user_household_id()`, etc.).
3. If you already ran an older bootstrap and only need newer objects, skip this and run the migrations below instead.

### 1.3 Run post-bootstrap migrations (in order)

Bootstrap does **not** include migrations **008–024**. Run these **after** bootstrap, in **numeric order**, in SQL Editor (or via `scripts/run_migration.py` if `DATABASE_URL` is set):

| Order | File | Purpose |
|-------|------|--------|
| 1 | `008_pgvector_embeddings.sql` | pgvector + similarity search (similar incidents when enabled) |
| 2 | `009_rings.sql` | `rings`, `ring_members` (Ring Discovery) |
| 3 | `010_household_calibration_params.sql` | `household_calibration` for calibration report |
| 4 | `011_role_consent_helpers.sql` | `user_can_contact()` for outreach RLS |
| 5 | `012_outbound_actions_caregiver_contacts.sql` | `outbound_actions`, `caregiver_contacts` |
| 6 | `013_action_playbooks_capabilities_incident.sql` | `household_capabilities`, `action_playbooks`, `incident_packets` |
| 7 | `013_outbound_contact_safe_display.sql` | Safe display / RLS for outbound (run after 012) |
| 8 | `014_narrative_reports.sql` | `narrative_reports` (Evidence Narrative "View report") |
| 9 | `015_outbound_actions_conformal_auto_send.sql` | Conformal auto-send and outreach columns |
| 10 | `016_rpc_alert_page_and_investigation_context.sql` | RPCs for alert page and investigation |
| 11 | `017_performance_indexes.sql` | Performance indexes |
| 12 | `018_processing_queue.sql` | `processing_queue` (enqueued investigation) |
| 13 | `019_processing_queue_dedupe_retry.sql` | Dedupe and retry for processing_queue |
| 14 | `020_watchlist_items.sql` | `watchlist_items` |
| 15 | `021_rings_fingerprint_canonical.sql` | Rings fingerprint and canonical view |
| 16 | `022_embedding_vector_128.sql` | 128-dim embedding vector support |
| 17 | `023_protection_rings_watchlist_columns.sql` | Protection page rings/watchlist columns |
| 18 | `024_risk_signals_fingerprint.sql` | `risk_signals.fingerprint` for compound upsert |

**How to run:** Open each file under `db/migrations/`, copy contents, run in SQL Editor. Or from repo root with `DATABASE_URL` set: `python scripts/run_migration.py 008_pgvector_embeddings`, then 009 … through 024 in order.

If a migration fails with "already exists", skip that statement or run the rest of the file.

**Shortcut for alerts/outreach only:** You can run **`db/run_mig
[truncated — 11246 more characters]
```

### requirements.txt

```
# Anchor: install with pip install -r requirements.txt
# For ML (PyG, torch): pip install -e ".[ml]"
fastapi>=0.109.0
pyyaml>=6.0
uvicorn[standard]>=0.27.0
supabase>=2.3.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
python-dotenv>=0.21.0
langgraph>=0.0.40
langchain-core>=0.1.0
langchain>=0.1.0
anthropic>=0.39.0
httpx>=0.26.0
websockets>=12.0
neo4j>=5.0.0
ruff>=0.1.0
pytest>=7.4.0
pytest-asyncio>=0.23.0

```

### docker-compose.yml

```yaml
# Supabase local (run via supabase start) + Neo4j for visualization/investigation only
# Per README_EXTENDED.md §2.4: Neo4j = visualization + investigative queries; PyG = training/scoring; Supabase = source of truth.
# See README: supabase start, then run migrations
version: "3.8"
services:
  neo4j:
    image: neo4j:5
    environment:
      NEO4J_AUTH: neo4j/password
    ports:
      - "7474:7474"
      - "7687:7687"
    volumes:
      - neo4j_data:/data
volumes:
  neo4j_data: {}

```

### pyproject.toml

```
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "anchor"
version = "0.1.0"
description = "Anchor: Independence Graph backend + ML pipeline for edge voice companion"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
dependencies = [
    "fastapi>=0.109.0",
    "uvicorn[standard]>=0.27.0",
    "supabase>=2.3.0",
    "pydantic>=2.5.0",
    "pydantic-settings>=2.1.0",
    "python-jose[cryptography]>=3.3.0",
    "langgraph>=0.0.40",
    "langchain-core>=0.1.0",
    "langchain>=0.1.0",
    "torch>=2.2.0",
    "torch-geometric>=2.5.0",
    "numpy>=1.24.0",
    "scipy>=1.11.0",
    "scikit-learn>=1.3.0",
    "pandas>=2.0.0",
    "httpx>=0.26.0",
    "websockets>=12.0",
    "ruff>=0.1.0",
    "pytest>=7.4.0",
    "pytest-asyncio>=0.23.0",
    "modal>=0.64.0",
]

[project.optional-dependencies]
ml = [
    "torch-scatter",
    "torch-sparse",
    "torch-cluster",
]
db = [
    "psycopg2-binary>=2.9.0",
]

[tool.setuptools.packages.find]
where = ["."]
include = ["apps*", "ml*", "db*"]

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests", "apps/api/tests", "apps/worker/tests", "ml/tests"]

```

### apps/web/package.json

```
{
  "name": "web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-label": "^2.1.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@supabase/supabase-js": "^2.95.3",
    "@tanstack/react-query": "^5.90.21",
    "@xyflow/react": "^12.10.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "framer-motion": "^12.34.0",
    "lucide-react": "^0.564.0",
    "next": "14.2.35",
    "react": "^18",
    "react-dom": "^18",
    "recharts": "^3.7.0",
    "tailwind-merge": "^3.4.0",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^4.3.6",
    "zustand": "^5.0.11"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.35",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### apps/worker/main.py

```python
#!/usr/bin/env python3
"""
Worker entrypoint: run pipeline for a household or listen for jobs.
Usage: python -m worker.main [--household-id UUID] [--once]
"""
import argparse
import logging
import os
import sys

# Add repo root and apps/api for imports
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, ROOT)
sys.path.insert(0, os.path.join(ROOT, "apps", "api"))

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--household-id", type=str, default=None)
    parser.add_argument("--once", action="store_true", help="Run pipeline once then exit")
    parser.add_argument("--poll", action="store_true", help="Poll processing_queue every N seconds and run jobs")
    parser.add_argument("--poll-interval", type=int, default=30, help="Seconds between queue polls (default 30)")
    args = parser.parse_args()

    try:
        from supabase import create_client
        url = os.environ.get("SUPABASE_URL", "")
        key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
        if not url or not key:
            logger.warning("SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY not set; pipeline will use placeholder data")
        supabase = create_client(url, key) if url and key else None
    except Exception as e:
        logger.warning("Supabase client not available: %s", e)
        supabase = None

    if args.once and args.household_id:
        from worker.worker.jobs import run_pipeline
        result = run_pipeline(supabase, args.household_id)
        logger.info("Pipeline result: %s", list(result.keys()))
    elif args.poll and supabase:
        from worker.worker.jobs import process_one_processing_queue_job
        import time
        logger.info("Polling processing_queue every %s seconds", args.poll_interval)
        while True:
            if process_one_processing_queue_job(supabase):
                logger.info("Processed one queue job")
            time.sleep(args.poll_interval)
    else:
        logger.info("Worker idle (use --household-id and --once to run once, or --poll to poll processing_queue)")


if __name__ == "__main__":
    main()

```

### apps/api/api/main.py

```python
"""
Anchor API: FastAPI backend, Supabase, LangGraph pipelines.
Auth: Supabase Auth; household-scoped RLS.
"""
from pathlib import Path

# Load .env from repo root so ANTHROPIC_API_KEY etc. are available (run_api.sh cd's to root)
try:
    from dotenv import load_dotenv
    root = Path(__file__).resolve().parents[3]  # apps/api/api/main.py -> repo root
    load_dotenv(root / ".env")
except ImportError:
    pass

from contextlib import asynccontextmanager
from typing import AsyncGenerator

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware

from api.broadcast import add_subscriber, broadcast_risk_signal, remove_subscriber
from api.config import settings
from api.routers import agents, alerts, capabilities, connectors, device, explain, graph, households, incident_packets, ingest, investigation, maintenance, outreach, playbooks, protection, risk_signals, rings, sessions, summaries, system, watchlists


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    yield


app = FastAPI(
    title="Anchor API",
    description="Independence Graph backend: sessions, events, risk signals, watchlists, device sync",
    version="0.1.0",
    lifespan=lifespan,
    docs_url="/docs",
    redoc_url="/redoc",
)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(households.router)
app.include_router(graph.router)
app.include_router(sessions.router)
app.include_router(risk_signals.router)
app.include_router(alerts.router)
app.include_router(capabilities.router)
app.include_router(playbooks.router)
app.include_router(incident_packets.router)
app.include_router(connectors.router)
app.include_router(protection.router)
app.include_router(explain.router)
app.include_router(watchlists.router)
app.include_router(rings.router)
app.include_router(device.router)
app.include_router(ingest.router)
app.include_router(summaries.router)
app.include_router(investigation.router)
app.include_router(maintenance.router)
app.include_router(system.router)
app.include_router(agents.router)
app.include_router(outreach.router)


@app.websocket("/ws/risk_signals")
async def websocket_risk_signals(websocket: WebSocket) -> None:
    """Push new risk_signals to subscribed clients. UI: connect for realtime alerts."""
    await websocket.accept()
    add_subscriber(websocket)
    try:
        while True:
            await websocket.receive_text()
    except WebSocketDisconnect:
        pass
    finally:
        remove_subscriber(websocket)


@app.api_route("/", methods=["GET", "HEAD"])
def root() -> dict:
    """Root: links to docs and no-auth demo. Auth-required endpoints return 401 without a valid JWT."""
    return {
        "name": "Anchor API",
        "version": "0.1.0",
        "docs": "/docs",
        "redoc": "/redoc",
        "health": "/health",
        "demo_no_auth": "/agents/financial/demo",
    }


def _health_body() -> dict:
    return {"status": "ok"}


@app.api_route("/health", methods=["GET", "HEAD"])
def health() -> dict:
    return _health_body()


@app.api_route("/healthz", methods=["GET", "HEAD"])
def healthz() -> dict:
    """Kubernetes/Render-style health check; same response as /health."""
    return _health_body()

```

### apps/web/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import "@xyflow/react/dist/style.css";
import { QueryProvider } from "@/providers/query-provider";
import { AuthProvider } from "@/providers/auth-provider";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });

export const metadata: Metadata = {
  title: "anchor – Elder companion & risk engine",
  description: "Offline-first elder companion and graph risk engine",
  icons: {
    icon: { url: "/icon.png", type: "image/png" },
    apple: { url: "/icon.png", type: "image/png" },
  },
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en" suppressHydrationWarning className={inter.variable}>
      <body className="min-h-screen bg-background font-sans antialiased">
        <QueryProvider>
          <AuthProvider>{children}</AuthProvider>
        </QueryProvider>
      </body>
    </html>
  );
}

```

### apps/web/src/app/page.tsx

```typescript
import Link from "next/link";

export default function HomePage() {
  // In a real app we'd check auth server-side; for now we redirect to dashboard
  // and let middleware or client handle login redirect
  return (
    <div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8 bg-anchor-warm">
      <img src="/logo.png" alt="Anchor" className="h-20 w-auto max-h-24 sm:h-24 sm:max-h-28 object-contain" />
      <h1 className="sr-only">Anchor</h1>
      <p className="text-muted-foreground text-center max-w-md">
        Elder companion & graph risk engine. Offline-first, privacy-aware.
      </p>
      <div className="flex flex-wrap gap-4 justify-center">
        <Link
          href="/signup"
          className="rounded-2xl bg-primary px-6 py-3 text-sm font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90"
        >
          Create account
        </Link>
        <Link
          href="/login"
          className="rounded-2xl border border-border bg-background px-6 py-3 text-sm font-medium shadow-sm transition hover:bg-accent"
        >
          Sign in
        </Link>
        <Link
          href="/dashboard"
          className="rounded-2xl border border-border bg-background px-6 py-3 text-sm font-medium shadow-sm transition hover:bg-accent"
        >
          Dashboard
        </Link>
      </div>
      <Link href="/replay" className="text-sm text-muted-foreground hover:underline">
        Scenario Replay (demo)
      </Link>
    </div>
  );
}

```

[351 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]