diff --git a/.example.env b/.example.env
index 897f13a..d38c29f 100644
--- a/.example.env
+++ b/.example.env
@@ -9,3 +9,7 @@ LANGFUSE_SECRET_KEY=
LANGFUSE_HOST=https://cloud.langfuse.com
EVINCTA_API_KEY= # required for write routes in production
ALLOWED_ORIGINS=http://localhost:5173
+
+# web/ — set in Vercel (same value as EVINCTA_API_KEY for approve/override)
+# VITE_API_URL=http://localhost:8000
+# VITE_EVINCTA_API_KEY=
diff --git a/Dockerfile b/Dockerfile
index f7ab599..e4f65a3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,8 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY services/ services/
COPY api/ api/
-# Demo claims (committed). Mount or bake data/generated/ for the full seeded set (issue #47).
+# Demo claims (committed). Pre-baked index survives Render redeploys.
+COPY data/demo_claims_index.json data/claims_index.json
COPY data/samples/ data/samples/
# NOTE: no .env, no secrets copied — injected at runtime by Render
diff --git a/README.md b/README.md
index 7483b85..8d86ae7 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ An evidence-backed recommendation — with its reasoning, precedents, and audit
-[](https://YOUR-APP.vercel.app)
+[]([Evincta](https://evincta.vercel.app))
[](.github/workflows/eval.yml)
@@ -31,7 +31,7 @@ An evidence-backed recommendation — with its reasoning, precedents, and audit
---
> [!NOTE]
-> **Live demo:** [YOUR-APP.vercel.app](https://YOUR-APP.vercel.app) → click **“Enter as demo adjuster.”**
+> **Live demo:** [https://evincta.vercel.app](https://evincta.vercel.app) → click **“Enter as demo adjuster.”**
> The API runs on a free-tier container that sleeps when idle, so the **first load may take ~30s to wake** — after that it's fast. Browsing and deciding claims costs nothing (recommendations are pre-computed and cached).
diff --git a/api/auth.py b/api/auth.py
index 1b9973f..614b862 100644
--- a/api/auth.py
+++ b/api/auth.py
@@ -5,5 +5,7 @@
def require_key(x_api_key: str = Header(None)):
- if not API_KEY or x_api_key != API_KEY:
+ if not API_KEY:
+ return # local dev — no key configured
+ if x_api_key != API_KEY:
raise HTTPException(401, "invalid or missing API key")
diff --git a/api/claims.py b/api/claims.py
index 749e5d0..1589ef6 100644
--- a/api/claims.py
+++ b/api/claims.py
@@ -2,6 +2,7 @@
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel
from services.agents.run import start_claim, resume_claim
+from services.agents.audit import audit_node
from services.ingest.extractor import extract_claim
from fastapi import Depends
from .auth import require_key
@@ -44,7 +45,8 @@ def _run():
def list_claims():
# compact rows for the queue view
return [{"claim_id": c["claim_id"], "status": c["status"],
- "decision": c["recommendation"]["decision"],
+ "recommendation": c["recommendation"]["decision"],
+ "human_decision": c.get("human_decision"),
"fraud_risk": c["recommendation"]["fraud_risk"]}
for c in store.all_claims()]
@@ -77,7 +79,14 @@ async def decide(claim_id: str, body: DecisionIn):
else c["recommendation"]["decision"])
def _resume():
- resume_claim(c["thread_id"], human_decision=final, approver=body.approver)
+ try:
+ resume_claim(c["thread_id"], human_decision=final,
+ approver=body.approver)
+ except Exception:
+ # baked demo index may not have a matching LangGraph checkpoint
+ audit_node({"claim_id": claim_id,
+ "recommendation": c["recommendation"],
+ "human_decision": final, "approver": body.approver})
await run_in_threadpool(_resume)
store.upsert(claim_id, status="decided",
diff --git a/data/demo_claims_index.json b/data/demo_claims_index.json
new file mode 100644
index 0000000..0c3a3c7
--- /dev/null
+++ b/data/demo_claims_index.json
@@ -0,0 +1,114 @@
+{
+ "claim_0001": {
+ "thread_id": "3de5e4ee-ad20-4576-99e5-c13b37dc8590",
+ "status": "pending",
+ "recommendation": {
+ "decision": "approve",
+ "payout_low_usd": 6500.0,
+ "payout_high_usd": 26000.0,
+ "fraud_risk": "low",
+ "confidence": 0.83,
+ "rationale": "Applying adjudication rules in order: (1) Coverage is confirmed - covered is true, so no denial on coverage grounds. (2) Fraud risk is low with no flags, so no investigation required. (3) Cost midpoint ($17,250) exceeds deductible ($1,000), so no denial on deductible grounds. (4) Therefore, we approve the claim. Payout range is calculated as repair cost estimates ($7,500-$27,000) minus the $1,000 deductible, with the high end capped at the coverage limit of $26,000. Similar precedent claim_0012 involved vandalism and was approved with a comparable payout of $8,750.",
+ "cited_precedents": [
+ "claim_0012",
+ "claim_0013",
+ "claim_0023"
+ ],
+ "policy_basis": "Claim meets all approval criteria: incident is covered under policy, fraud risk is low, and estimated repair costs exceed the deductible amount. Payout is subject to $1,000 deductible and $26,000 coverage limit as specified in the policy terms."
+ },
+ "evidence": {
+ "incident_type": "collision",
+ "damaged_parts": [
+ "front end",
+ "hood",
+ "front driver side",
+ "windshield",
+ "front bumper",
+ "driver door"
+ ],
+ "severity": "severe",
+ "visible_pre_existing_damage": false,
+ "image_inconsistency": true,
+ "policy_number": "AUTO-42953",
+ "deductible_usd": 1000.0,
+ "coverage_limit_usd": 26000.0,
+ "notes_summary": "Insured reported vandalism at junction with severe damage. However, the photo evidence shows extensive collision damage consistent with a high-speed impact rather than vandalism. The front end is severely crushed, hood crumpled, windshield shattered, and driver door damaged - all typical of a collision. Adjuster notes mention 'severe dmg to unknown' suggesting uncertainty about the incident type. Discrepancy exists between claimed vandalism and visible collision damage.",
+ "extraction_confidence": 0.65
+ },
+ "claim_id": "claim_0001"
+ },
+ "claim_0000": {
+ "thread_id": "b9fcefa6-51b0-4e57-9e1c-f5436a4d11f0",
+ "status": "pending",
+ "recommendation": {
+ "decision": "approve",
+ "payout_low_usd": 80.0,
+ "payout_high_usd": 1400.0,
+ "fraud_risk": "low",
+ "confidence": 0.92,
+ "rationale": "Coverage is confirmed and the claim passes all checks. The cost midpoint ($990) exceeds the deductible ($250), satisfying the threshold requirement. Fraud risk is assessed as low with no flags present. All precedent cases with similar image characteristics were approved with no fraud issues. The estimated repair costs fall well within the policy coverage limit of $26,000. Payout range calculated as cost estimates ($330-$1,650) minus the $250 deductible.",
+ "cited_precedents": [
+ "claim_0000",
+ "claim_0014",
+ "claim_0032",
+ "claim_0072",
+ "claim_0062"
+ ],
+ "policy_basis": "Claim is covered under policy with $250 deductible and $26,000 coverage limit. Cost estimate midpoint of $990 exceeds deductible threshold. No fraud indicators or coverage exclusions apply."
+ },
+ "evidence": {
+ "incident_type": "theft",
+ "damaged_parts": [
+ "rear bumper",
+ "quarter panel"
+ ],
+ "severity": "minor",
+ "visible_pre_existing_damage": false,
+ "image_inconsistency": true,
+ "policy_number": "AUTO-74686",
+ "deductible_usd": 250.0,
+ "coverage_limit_usd": 26000.0,
+ "notes_summary": "Reporting party reports theft incident occurred on driveway with minor damage to unknown parts. Image shows blue vehicle with scrapes and damage to rear bumper/quarter panel area.",
+ "extraction_confidence": 0.65
+ },
+ "claim_id": "claim_0000"
+ },
+ "claim_0007": {
+ "thread_id": "af646410-16fa-4592-97df-b65693ad59a2",
+ "status": "pending",
+ "recommendation": {
+ "decision": "approve",
+ "payout_low_usd": 6750.0,
+ "payout_high_usd": 24950.0,
+ "fraud_risk": "low",
+ "confidence": 0.85,
+ "rationale": "Coverage is confirmed with a deductible of $250 and coverage limit of $26,000. The estimated repair cost ranges from $7,000 to $25,200 (midpoint $16,100), which exceeds the deductible threshold. Fraud risk is assessed as low with no flags identified. Following the decision rules in order: (1) coverage is confirmed as covered, (2) fraud risk is low (not medium or high), (3) the midpoint cost of $16,100 exceeds the $250 deductible. Therefore, the claim should be approved. Similar precedents show approved severe damage claims (claim_0059, claim_0006, claim_0012) with comparable severity patterns. Payout calculation: cost range minus $250 deductible = $6,750 to $24,950.",
+ "cited_precedents": [
+ "claim_0059",
+ "claim_0006",
+ "claim_0012",
+ "claim_0061"
+ ],
+ "policy_basis": "Claim meets coverage requirements under the policy. The incident is covered, fraud risk is low, and repair costs significantly exceed the deductible amount. Policy allows for approval with payout calculated as estimated costs less applicable deductible."
+ },
+ "evidence": {
+ "incident_type": "collision",
+ "damaged_parts": [
+ "rear end",
+ "trunk",
+ "rear bumper",
+ "rear quarter panel",
+ "roof"
+ ],
+ "severity": "severe",
+ "visible_pre_existing_damage": false,
+ "image_inconsistency": true,
+ "policy_number": "AUTO-41195",
+ "deductible_usd": 250.0,
+ "coverage_limit_usd": 26000.0,
+ "notes_summary": "Caller reports collision in car park with severe damage. No injuries reported. Image shows catastrophic damage to entire rear section and roof structure with complete collapse - inconsistent with typical car park collision.",
+ "extraction_confidence": 0.65
+ },
+ "claim_id": "claim_0007"
+ }
+}
\ No newline at end of file
diff --git a/web/src/ClaimDetail.tsx b/web/src/ClaimDetail.tsx
index 7d200a2..23f88a7 100644
--- a/web/src/ClaimDetail.tsx
+++ b/web/src/ClaimDetail.tsx
@@ -28,6 +28,7 @@ export default function ClaimDetail({
}) {
const [c, setC] = useState
(null);
const [busy, setBusy] = useState(false);
+ const [err, setErr] = useState(null);
useEffect(() => {
setC(null);
@@ -43,10 +44,13 @@ export default function ClaimDetail({
async function act(decision: string, override_to?: string) {
setBusy(true);
+ setErr(null);
try {
await api.decide(id, { decision, approver: "atti@evincta", override_to });
await api.get(id).then(setC);
onDecided();
+ } catch (e) {
+ setErr(e instanceof Error ? e.message : "Request failed");
} finally {
setBusy(false);
}
@@ -62,7 +66,7 @@ export default function ClaimDetail({
{c.status}
-