An AI-powered decision comparison tool that surfaces hidden costs and systemic impacts invisible in traditional comparisons.
Enter two to six options (the API accepts up to ten) and your intent (e.g. "buying a house vs. renting, optimizing for 10-year financial outcome"). Prism suggests a depth tier, researches the options via web search (Medium and Deep tiers), then uses Claude to generate scored criteria covering financial, environmental, social, and long-term factors you might not have considered. The Deep tier also surfaces hidden systemic properties: second-order effects of each option.
export ANTHROPIC_API_KEY=sk-ant-...
export TAVILY_API_KEY=tvly-...Optional:
export DAILY_SPEND_CAP_USD=5.0 # Default $5/day cap on Claude API costs
export LLM_BACKEND=api # api (default) or cli; see belowWith LLM_BACKEND=cli, Prism sends every Claude call through Claude Code's headless mode (claude -p) instead of the Anthropic API, so it runs on your logged-in Claude subscription and ANTHROPIC_API_KEY isn't needed. This is for running Prism locally for yourself only; don't use it for a shared or hosted instance.
- Install Claude Code with the native installer and log in (
claude, then/login). The backend refuses to start ifclaudeisn't onPATH, or if it's an npm.cmdshim on Windows (which would garble the prompts). - If
ANTHROPIC_API_KEYis set,claude -pbills the key instead of the subscription. Prism removes it, plusANTHROPIC_AUTH_TOKENand theCLAUDE_CODE_USE_BEDROCK/_VERTEX/_FOUNDRYswitches, from theclaudesubprocess's environment. - CLI mode isn't supported in Docker. Run the backend directly on the host with
uvicorn(step 2); the Docker setup keeps using the API key. - CLI calls are logged in
usage_metrics.jsonwithbackend: "cli"and the costclaude -preports, but that cost isn't billed and doesn't count towardDAILY_SPEND_CAP_USD./api/monitoringreports it separately ascli_notional_cost_usd; its token totals and per-feature counts include CLI calls, each of which adds about 24k cache-creation tokens of Claude Code overhead. - The cap is still checked when starting an analysis, so if earlier API spend reached it today, CLI mode gets
429too until tomorrow (or raiseDAILY_SPEND_CAP_USD). - Each call has a fixed overhead of about 1 to 2 seconds.
max_tokensisn't applied in CLI mode.
cd backend
pip install -r requirements.txt
uvicorn app.main:app --reload # Runs on http://localhost:8000Verify: curl http://localhost:8000/health should return {"status":"ok"}.
cd frontend
npm install
npm run dev # Runs on http://localhost:5173Open http://localhost:5173 in your browser.
If the backend runs on a different host or port, set this before starting the frontend:
export VITE_API_BASE=http://your-host:8000Docker Compose auto-loads docker-compose.override.yml, so the bare command is the dev stack
(hot reload, source bind-mounted, backend on 8000, Vite on 5173):
cp .env.example .env # then fill in the two API keys
docker compose up --buildProduction (nginx on port 80 serving the built SPA and proxying /api, backend not exposed)
must skip the override file explicitly. UI and API are served from one origin; the CSP and
backend CORS settings assume this, so a split frontend/backend deployment is not supported:
docker compose -f docker-compose.yml up --build -dIn production, analysis data lives in the named Docker volume prism-data (Compose prefixes it with the project name; find it with docker volume ls --filter name=prism-data, copy out with docker cp). In dev it stays in backend/data/ on the host. Requires Docker Compose v2.24.4 or newer (for the !override tag).
Prism has no authentication. Every endpoint, including /api/monitoring (usage and cost stats), is open to anyone who can reach the server, and every analysis spends your Anthropic and Tavily credit. Run it on localhost or a private network only. DAILY_SPEND_CAP_USD is not access control and only partly limits spending (see Spending guard below). If you expose it beyond that, put it behind an authenticating reverse proxy first.
To report a security problem, please open a private security advisory rather than a public issue.
- Create a decision: options plus intent. Claude Haiku suggests a tier (Quick, Medium or Deep) with a rationale.
- Run an analysis at the chosen tier. The backend returns
202with ananalysis_idand works in a background task. - Watch progress over Server-Sent Events while the frontend polls for the result.
- Read the result: a winner with reasoning, scored criteria with sources, and (Deep only) hidden properties.
| Tier | Approx. cost | Approx. time | What it does |
|---|---|---|---|
| Quick | ~$0.002 | ~5s | Claude's own knowledge, no web search |
| Medium | ~$0.04 | ~45s | Tavily web search per option, then criteria with cited sources |
| Deep | ~$0.18 | ~3min | Medium, plus discovery of hidden systemic properties (best-effort: a failure here doesn't fail the analysis) |
Costs and times are rough guides, not measurements.
Browser (React) --HTTP/SSE--> FastAPI backend --> Tavily (web search)
--> Claude (analysis)
--> data/ (JSON files)
The backend persists everything as JSON files; there is no database. Decisions own analyses:
data/
decisions/
{decision_id}.json Decision (options, intent, suggested tier)
{decision_id}/analyses/
{analysis_id}.json AnalysisResult (written as "processing", then "completed" or "error")
usage_metrics.json Token usage and cost per Claude call
Writes are atomic (tempfile, fsync, os.replace), and IDs are validated as UUIDs before any file access.
backend/app/
main.py FastAPI app, CORS, router registration
models.py Pydantic v2 models
storage.py JSONStorage: atomic, UUID-validated reads and writes
routers/
comparison.py Decisions, analyses, criteria, weights
progress.py SSE progress stream
monitoring.py Usage and daily spend
health.py Readiness probe
services/
orchestrator.py run_analysis(): routes the quick / medium / deep pipeline
suggest_tier.py Tier suggestion (Claude Haiku)
claude_service.py Criteria generation (Claude Sonnet, prompt caching)
hidden_service.py Hidden property discovery (Deep only)
criteria_service.py Go deeper, regenerate weights
tavily_service.py Web search with a 24h in-memory cache
spending_guard.py Daily cost cap
usage_tracker.py Token and cost tracking, persisted to disk
progress_store.py In-memory SSE message buffer
config.py API client singletons
frontend/src/
App.tsx Single page; hash routing (#decisionId:analysisId); history sidebar
lib/api.ts apiGet / apiPost helpers and all domain types
lib/scoring.ts Client-side weighted scoring
features/
comparison/ DecisionForm: create decision, then start analysis
progress/ SSE consumer and progress view
results/ Winner card, criteria, hidden properties; polling hook
history/ Past decisions in the sidebar
- No database. JSON files keep the stack free of infrastructure; atomic writes prevent corruption on crash.
- Async with polling. Analyses take seconds to minutes, so the API answers
202at once and the client polls (with SSE for progress messages). - Deduplication. Starting an analysis at a tier that already has a completed analysis for the same decision returns the existing
analysis_idinstead of spending again. - Spending guard. A daily cap (default $5,
DAILY_SPEND_CAP_USD) rejects new analyses with429once reached; requests that match a completed analysis still get it back, since they cost nothing. It rebuilds today's total fromusage_metrics.jsonon startup, so it survives restarts. Calls made withLLM_BACKEND=cliare not counted (see above). Limits: only starting an analysis checks the cap, so go-deeper, regenerate-weights and the tier-suggestion call on every new decision are counted but still run past it; Tavily usage is not counted; analyses already running can overshoot it. - Confidence. On Medium and Deep, Claude labels each cited source as peer-reviewed, government, industry, news or blog (stored in
source_quality), and code grades each criterion from its sources: high needs at least 2 sources including a peer-reviewed or government one; medium needs at least 1 source of news quality or better (industry or news, or a single peer-reviewed or government source); anything else (no sources, or only blog or unclassified sources) is low. Only URLs from the web research count. Go-deeper criteria are graded the same way. Quick-tier criteria (no sources) and user-added criteria are not graded (confidence: null): they get no badge and don't count toward the winner card's overall confidence, which is the share of graded criteria rated high. Quick results show "Based on AI knowledge only, no sources" instead of a badge. Analyses saved before this change keep their oldlowgrades. Deep-tier hidden properties get their confidence from Claude.
The web UI uses the decision, analysis and progress endpoints. The weights, criteria, go-deeper and regenerate-weights endpoints are API-only for now. Full schemas are at http://localhost:8000/docs while the backend runs.
| Method | Path | Description |
|---|---|---|
POST |
/api/decisions |
Create a decision; returns decision_id, suggested_tier, rationale |
GET |
/api/decisions |
List decisions, newest first |
POST |
/api/decisions/{decision_id}/analyses |
Start an analysis at a tier (202 + analysis_id) |
GET |
/api/decisions/{decision_id}/analyses |
List a decision's analyses |
GET |
/api/decisions/{decision_id}/analyses/{analysis_id} |
Get a result; poll until status != "processing" |
GET |
/api/progress/{analysis_id} |
SSE stream (progress / complete / error events) |
| Method | Path | Description |
|---|---|---|
PATCH |
.../analyses/{analysis_id}/weights |
Persist weight overrides |
POST |
.../analyses/{analysis_id}/go-deeper |
Add criteria from cached research (Medium/Deep) |
POST |
.../analyses/{analysis_id}/regenerate-weights |
Let Claude re-weight the criteria |
POST |
.../analyses/{analysis_id}/criteria |
Add a criterion |
PATCH |
.../analyses/{analysis_id}/criteria/{criterion_id} |
Edit a criterion |
DELETE |
.../analyses/{analysis_id}/criteria/{criterion_id} |
Delete a criterion |
... is /api/decisions/{decision_id}.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Readiness check |
GET |
/api/monitoring |
Token usage and cost stats (unauthenticated; see Security) |
| Layer | Technology | Version |
|---|---|---|
| Backend | Python | 3.11+ |
| FastAPI | 0.128 | |
| Pydantic | 2.12 | |
| Anthropic SDK | 0.75 | |
| Tavily SDK | 0.7 | |
| Frontend | React | 19 |
| Vite | 7 | |
| TypeScript | 5.9 | |
| TanStack Query | 5 | |
| Tailwind CSS | 4 |
# Backend tests. Most mock Claude and Tavily; a few live tests also call the real
# Anthropic API when ANTHROPIC_API_KEY is set, so unset it to stay offline.
cd backend
pip install -r requirements.txt -r requirements-dev.txt
pytest tests/
# Frontend
cd frontend
npx tsc -b # type check (tsconfig.json is references-only; plain `tsc --noEmit` checks nothing)
npm run lint
npm test # vitest
npm run build # output in frontend/dist/