American Express · Cross-Channel Journey Stitching
"Everyone else built a better rear-view mirror. We built a flight simulator for the customer relationship."
TimeWeave stitches a customer's fragmented touches across four channels — app, web, call center, in-person — into one identity and one journey, then turns that journey into a decision: it captures the anonymous pre-login signal everyone misses, explains why a customer is at risk, and lets an analyst branch the journey into alternate futures ranked by outcome — honestly labeled as cohort outcome rates, not causal guarantees.
All four seed phases are implemented and the frontend runs off the live backend, with a static offline fallback so the hero demo never breaks.
Each script creates its environment on first run (Python venv + pip install, or npm install), then
starts the server. Run the backend first, then the frontend in a second terminal.
./scripts/run-backend.ps1 # → http://localhost:8000 (docs at /docs)
./scripts/run-frontend.ps1 # → http://localhost:5173 (second terminal)If PowerShell blocks the scripts, either unblock once with
Set-ExecutionPolicy -Scope Process Bypass or run the commands inline:
cd backend; python -m venv .venv; .\.venv\Scripts\python -m pip install -r requirements.txt
.\.venv\Scripts\python -m uvicorn main:app --reload --port 8000
# new terminal:
cd frontend; npm install; npm run dev./scripts/run-backend.sh # → http://localhost:8000 (docs at /docs)
./scripts/run-frontend.sh # → http://localhost:5173 (second terminal)Sanity-check the whole pipeline with no server:
backend/.venv/Scripts/python scripts/seed.py (Windows) or
backend/.venv/bin/python scripts/seed.py (macOS/Linux).
The frontend talks to the backend on :8000 (override with VITE_API_BASE; see
frontend/.env.example). If the backend is down, Priya's hero flow still renders from the static
seed (frontend/src/data/priya.ts).
| Phase | Scope | Status |
|---|---|---|
| 1 | Frontend hero visual — Identity / Journey / Counterfactual, 3 views end-to-end | ✅ done |
| 2 | Synthetic generator (real schemas) + DuckDB store + ingestion → canonical events + FastAPI | ✅ done |
| 3 | Fuzzy identity resolution + churn model + attribution + cohort comparison, wired to the frontend | ✅ done |
| 4 | Review-queue UI, predicted-vs-observed loop, uncertainty bands, population "fix-the-flow", audit export, optional LLM planner | ✅ done |
Three linked views: Identity (channel streams collapse into one golden identity; click the fuzzy
SAME_AS edge for the 91% merge explanation + un-merge; a review-queue badge holds low-confidence
merges), Journey (cross-channel swim-lane timeline + churn gauge + risk attribution), and
Counterfactual (the journey branches into interventions; the gauge drains along the winning
branch; every number labeled "outcome rate for N similar journeys"; Approve → mock execute →
predicted-vs-observed loop).
- Real industry schemas (
backend/data/): events are generated as Adobe XDM ExperienceEvents, Amazon Connect CTRs, ISO 8583 messages and system-derived SLA breaches, then normalized to one canonical event model (backend/ingestion/). Map real Amex feeds onto these shapes and the pipeline runs unchanged. - Embedded, persistent DuckDB store (
backend/data/store.py) — no server; a file atbackend/timeweave.duckdbthat survives restarts (see Data & persistence). ~835 synthetic customers / ~4,200 events across all four channels, plus the authored edge cases. - Feature-driven ground truth (
backend/data/outcomes.py): each customer's churn/retention outcome is drawn from one generative model over the journey's features, so the churn classifier and the cohort comparison are real properties of the data — not numbers invented at display time.
- Explainable identity resolution (
backend/resolution/): deterministic pass (exact account/PAN/phone + ECID cookie-continuation) then a fuzzy pass (rapidfuzz, or a stdlib fallback) that scores anonymous sessions on a weighted feature blend → confidence, matched features, plain-English explanation. Thresholds: ≥0.85 auto-merge · 0.55–0.85 review queue · <0.55 held back. Merges are reversible (un-merge) with an append-only audit history. - Journey graph (
backend/graph/): a NetworkX-style temporal identity + journey graph (Identity / SourceIdentifier / Event / Outcome / Intervention) exposed at/graph/{gid}. - Churn model + attribution (
backend/intelligence/): a logistic classifier trained on the population; per-driver attribution via leave-one-out marginals. Priya lands ≈68% risk, ordered broken-callback > redemption-failure > cancel-research > … with the fee barely registering. - Cohort comparison: observed retention per intervention over the cohort of similar journeys (DuckDB), with a Wilson uncertainty band and mandatory correlational labeling.
- Review-queue approve/reject UI, live un-merge, edge-case toggle in the picker.
- Predicted-vs-observed loop that closes on the synthetic ground truth.
- Uncertainty bands on every cohort number; population "fix-the-flow" business case (churn prevented / calls avoided / value preserved); regulator-ready audit export (JSON).
- Optional LLM planner/explainer (
backend/llm/) — auto-detectsANTHROPIC_API_KEY, sends only IDs + aggregates, and degrades to templated text when absent. The app is fully functional without it.
Edge cases behave correctly: false_positive_trap scores ~0.38 and is not auto-merged;
shared_household (a shared card token) lands in the review queue at 0.71.
API: /identity/{gid} · /journey/{gid} · /churn/{gid} · /interventions/{gid} ·
/recommendation/{gid} · /graph/{gid} · /merges · /review-queue · POST /unmerge ·
POST /remerge · POST /review/{id} · POST /approve · /population/fix-the-flow ·
/population/stats · /system/status · /customers · /events?golden_id= · /health ·
/admin/db · POST /admin/customer · POST /admin/regenerate
Data is synthetic and generated locally — no real data, no external source.
-
Persistent by default. The store is a file-backed DuckDB at
backend/timeweave.duckdb(gitignored). On first boot it generates ~835 customers / ~4,200 events and persists them; on every restart it hydrates from the file instead of regenerating, so the data (and anything you add) survives and can be inspected later. Override the location — or go ephemeral — with theTIMEWEAVE_DBenv var (TIMEWEAVE_DB=:memory:for an in-memory run). -
Inspect it:
GET /admin/dbshows the file path + row counts. You can also openbackend/timeweave.duckdbin any DuckDB client and queryevents,ground_truth,profiles. -
Add your own customer (writes to the DB, then rebuilds resolution + retrains churn so it shows up live in the UI):
curl -X POST http://localhost:8000/admin/customer -H "Content-Type: application/json" -d '{ "profile": {"golden_id":"golden-demo-9","name":"Demo Customer","tier":"Platinum", "tenure_years":7,"renewal_in_days":5,"fee_sensitivity":0, "cohort_key":"redemption_failed+callback_broken@renewal"}, "raw_events": [ {"_source":"xdm","_id":"d9-app-1","timestamp":"2026-07-11T10:00:00Z", "eventType":"application.redemption", "identityMap":{"account":[{"id":"acct-d9","authenticatedState":"authenticated"}]}, "_attributes":{"points":60000},"_outcome":"redemption_failed"}, {"_source":"system","_id":"d9-sla","_customer_ref":"acct-d9", "timestamp":"2026-07-15T15:00:00Z","eventType":"system.sla_breach", "_attributes":{"breached":true},"_outcome":"callback_broken"} ] }'
raw_eventsuse the same source schemas the generator emits (XDM / CTR / ISO 8583 / system). -
Regenerate from scratch:
POST /admin/regeneratewipes and rebuilds the population.
DuckDB is single-writer — run one backend instance against a given DB file at a time.
The optional accelerators — rapidfuzz, networkx, scikit-learn — are exactly that:
optional. Each capability ships with a zero-dependency stdlib fallback and auto-upgrades if the
library is importable (/system/status shows which backend is active):
| Capability | Accelerator | Stdlib fallback |
|---|---|---|
| Fuzzy string matching | rapidfuzz | difflib |
| Identity/journey graph | networkx | internal graph structure |
| Churn classifier | scikit-learn | pure-Python logistic regression (gradient descent) |
| Intervention planner | Anthropic LLM | deterministic templates |
Install the accelerators for a credibility upgrade (pip install rapidfuzz networkx scikit-learn);
nothing changes behaviourally.
TimeWeave does not claim causal uplift. Recommendations are observed retention rates within a cohort of similar journeys — correlational, and labeled as such everywhere they appear (UI text and API alike). Validated causal uplift requires controlled experimentation; that's roadmap, not seed. Every merge and every recommendation carries a plain-English "why," and any (mock) action requires human-in-the-loop approval.
Real causal inference / experimentation, RL policies, production streaming at scale, real PII, clean rooms / PPRL, multi-node graph DB, real channel integrations, a Splink matcher swap-in, merge-drift monitoring, and an agent copilot at point-of-contact.
amex-hackathon/
├─ frontend/ React + Vite + TS + Tailwind + react-flow + framer-motion
│ ├─ src/api/ typed client + response types + offline fallback bundle
│ ├─ src/hooks/ useTimeWeave (loads the selected customer's bundle)
│ └─ src/components/ Identity / Journey / Counterfactual / Population + review queue
├─ backend/ FastAPI + DuckDB (embedded, persistent), all offline
│ ├─ timeweave.duckdb persisted data file (gitignored; created on first run)
│ ├─ schemas/ Pydantic: canonical event, API models
│ ├─ data/ generator · seed_priya · store · features · outcomes
│ ├─ ingestion/ raw XDM/CTR/ISO8583/system → canonical
│ ├─ resolution/ deterministic + fuzzy matcher · review queue · un-merge · audit
│ ├─ graph/ in-memory identity + journey graph (networkx-optional)
│ ├─ intelligence/ logistic churn model · attribution · timeline · cohort comparison
│ └─ llm/ optional planner/explainer (degrades gracefully)
├─ scripts/ run-backend(.ps1/.sh) · run-frontend(.ps1/.sh) · seed.py
└─ seed.md the full project spec