Team: Shanay Gaitonde · Sahiel Bose · Multi-agent systems · TypeScript · LangChain
A 13-agent, two-phase pipeline that reads an institutional denial letter, retrieves the institution's own published rules, catches where the denial contradicts those rules, drafts a cited appeal, runs the high-stakes math in a sandbox, clears a compliance guardrail, and — after a human approval gate — files the appeal and tracks the deadline. Most insurance denials are never appealed; the ones that are often win. Recourse closes that gap. It runs end-to-end on deterministic mocks with zero API keys.
- Maria Mitchell, 54, HER2+ breast cancer. Her insurer denies Trastuzumab (Herceptin) as "not medically necessary." She photographs the one-page denial letter and uploads it.
- Recourse OCRs the letter and extracts the payer, plan, denial code, and the appeal deadline — a 180-day window with 166 days remaining (computed as real code, not guessed).
- It retrieves the payer's own published medical policy for Herceptin — the exact coverage criteria the plan publishes.
- It pulls Maria's clinical facts and runs a criterion-by-criterion match: HER2+ confirmed (IHC 3+), Stage II, ECOG 1 — she meets all three.
- THE CATCH: the denial said "not medically necessary," but under the payer's own Policy §4.2, Maria qualifies on every listed criterion. The agent surfaces a provable self-contradiction.
- It drafts a formal appeal that quotes the payer's policy number back at them and lists each met criterion.
- The deadline and eligibility math run as real code in an isolated sandbox — never produced by the LLM.
- A compliance guardrail verifies: deadline not blown, every citation resolvable, no PHI leaked, format correct.
- Dr.'s advocate Dana reviews the catch and the letter on the dashboard and clicks Approve & File.
- The filing agent submits and returns a confirmation number — and if the portal is down, it automatically reroutes to fax without losing the case. The deadline goes on watch; a re-denial auto-escalates to external review, and the winning argument is written to memory.
The contradiction engine catches a tired clinician — or a scared patient — would never spot: a denial that violates the insurer's own published policy. Then it proves it with a citation and files the appeal in under a minute.
This is the technical core, and what makes Recourse not "an LLM that writes a letter." Letter-writers are everywhere; an agent that catches an institution violating its own published policy is not.
retrieved rule (the payer's own coverage criteria, parsed into a checklist)
+ extracted fact (the patient demonstrably meets each one)
+ stated reason (the denial claims they don't)
─────────────────────────────────────────────────────────────────────────────
= a provable contradiction → a citable argument
The matcher is pure, deterministic logic — fully real with no API keys — and classifies every catch into one of three patterns:
| Pattern | Meaning | Strength |
|---|---|---|
self_contradiction |
Patient meets every published criterion, yet was denied | Strongest — the demo case |
invented_criterion |
The denial leans on a requirement the policy doesn't list | Strong — insurer invented a hurdle |
admin_error |
Wrong code, wrong plan, or missing records | Correctable |
Because it's deterministic, it's pinned by a labeled eval set (see Benchmarks & Evals) rather than vibes.
Two phases, split by a human approval gate — nothing is filed until a person approves it. Every step is wrapped in retry → fallback orchestration (a failed portal submit reroutes to fax automatically).
flowchart TD
subgraph INTAKE["Patient Intake (:3002)"]
I1["Upload the denial letter"]
end
subgraph DRAFT["API · POST /api/agents/draft — Phase 1: Analyze & Draft"]
A1["1 · Intake & OCR"]
A2["2 · Classifier"]
A3["3 · Context Pull"]
A4["4 · Evidence Retriever"]
A5["5 · Contradiction Engine · THE CATCH"]
A6["6 · Strategy / Memory"]
A7["7 · Drafting"]
A8["8 · Compute & Form-Fill"]
A9["9 · Guardrail"]
end
subgraph GATE["Human Approval Gate · Dashboard (:3003)"]
G1["Advocate reviews the catch + letter, clicks Approve & File"]
end
subgraph FILEP["API · POST /api/agents/file — Phase 2: Verify & Act"]
F10["10 · Identity / Auth"]
F11["11 · Filing (portal → fax fallback)"]
F12["12 · Audit · SHA-256 chain"]
F13["13 · Tracker / Escalator"]
end
subgraph RECORDS["Case & Audit (:3004)"]
AU1["Status timeline + tamper-evident log"]
end
I1 -->|userId| A1
A1 --> A2 --> A3 --> A4 --> A5 --> A6 --> A7 --> A8 --> A9
A9 -->|"needs_human_approval"| G1
G1 -->|approverToken| F10
F10 --> F11 --> F12 --> F13
F13 -->|"tracking / escalated"| AU1
Engineering ideas worth a second look:
- No hallucinated arithmetic — appeal deadlines and eligibility scores are executed as real code (optionally shipped to an isolated sandbox), never produced by the model.
- Immutable, tamper-evident audit — every step is recorded in a SHA-256 hash chain (each event's
prevHash= the previouspayloadHash), with averifyChain()that detects tampering. - Human-in-the-loop by design — the draft → file split means an agent never acts unilaterally.
- Live progress — Phase 1 streams agent-by-agent progress to the UI over NDJSON, with a graceful fallback to a plain request.
- Graceful degradation everywhere — see below.
| # | Agent | Role | Technology |
|---|---|---|---|
| 1 | Intake & OCR | OCR the denial letter; store the original artifact | Nebius + Tigris |
| 2 | Classifier | Letter → structured denial (payer, plan, code, deadline window) | LangChain + Claude |
| 3 | Context Pull | Patient's clinical facts + prior case history | Insforge + Brain2 |
| 4 | Evidence Retriever | Fetch the payer's published policy → structured criteria | Apify |
| 5 | Contradiction Engine (the catch) | Criterion-by-criterion match; classify the contradiction | Rule-based + Daytona |
| 6 | Strategy / Memory | Which argument has beaten this payer before | Brain2 |
| 7 | Drafting | Write the cited appeal letter | LangChain + Claude |
| 8 | Compute & Form-Fill | Sandboxed deadline/eligibility math; fill + validate the form | Daytona |
| 9 | Guardrail | Compliance pass — deadline, citations, PHI, format | Opsera |
| 10 | Identity / Auth | Verify the approver may file on this case | Insforge |
| 11 | Filing (the action) | Submit on the payer portal; fall back to fax | Rtrvr.ai |
| 12 | Audit | SHA-256 hash-chained, tamper-evident trail | Tigris + crypto |
| 13 | Tracker / Escalator | Watch the deadline; escalate on re-denial; learn the outcome | Insforge + Brain2 |
Orchestration (retry → fallback → recovery) wraps every step. Agents 5 (the matcher) and 12 (the hash chain) are pure local logic — fully real regardless of keys.
npm install && npm run dev…boots the API + three UIs and runs the entire pipeline on deterministic mocks with no .env file. Every external integration sits behind one pattern:
export async function fetchPayerPolicy(payer: string, service: string): Promise<PayerPolicy> {
if (process.env.APIFY_TOKEN) {
try { /* REAL: call the provider, parse, return */ } catch { /* fall through to the mock */ }
}
return mockPayerPolicy(payer, service); // deterministic — same output every run
}Drop a real key into .env and that integration activates automatically — no code changes. A bad or expired key never crashes anything; the real call is wrapped in try/catch and falls back to the mock. Each integration becomes an independent, swappable seam, and the project is demoable on any machine.
git clone https://github.com/shanayg15/Recourse.git
cd Recourse
npm install
npm run dev # boots all 4 apps concurrently (Turborepo)| App | URL | Who uses it |
|---|---|---|
| API | http://localhost:3001 | Internal — the agent pipeline + all routes |
| Patient Intake | http://localhost:3002 | Patient uploads a denial letter |
| Dashboard | http://localhost:3003 | Advocate reviews the catch + letter, approves & files |
| Case & Audit | http://localhost:3004 | Status timeline + tamper-evident activity log |
To go live, copy .env.example to .env and add keys one integration at a time — start with ANTHROPIC_API_KEY.
curl -X POST http://localhost:3001/api/agents/draft \
-H "Content-Type: application/json" \
-d '{ "userId": "u_maria", "vertical": "health" }'Streams newline-delimited JSON: one {type:"progress"} frame as each agent starts/finishes, then a final {type:"done", response} frame. Powers the dashboard's live trace.
curl -X POST http://localhost:3001/api/agents/file \
-H "Content-Type: application/json" \
-d '{ "caseId": "case_maria", "approverToken": "advocate_dana" }'Returns a Filing (confirmationNumber, channel: "portal" | "fax") and the full SHA-256 auditTrail.
| Route | Purpose |
|---|---|
GET /api/cases/:id |
Case + documents + tamper-evident audit trail |
POST /api/cases/:id/simulate-response |
Demo-only: re-denial → escalation path |
POST /api/demo/portal |
Demo-only: toggle a portal outage to show the retry → fax fallback |
| Token | User | Role |
|---|---|---|
patient_maria |
Maria Mitchell | Patient |
advocate_dana |
Dana Rivera | Patient advocate (approves & files) |
admin_ops |
Operations | Admin |
Pass as approverToken or Authorization: Bearer <token>.
The contradiction matcher is deterministic, so its quality is pinned by a labeled eval set — no API key required. npm run test runs 11 tests across the workspace:
| Metric | Target | Result |
|---|---|---|
| Self-contradicting denials caught | 5 / 5 | ✅ 5 / 5 |
| False appeals (filed when the patient doesn't qualify) | 0 | ✅ 0 |
| Appeal deadline computed correctly across windows | 3 / 3 | ✅ 3 / 3 |
| Real-vs-mock LLM seam (real branch when keyed, fallback otherwise) | pass | ✅ |
| End-to-end draft route on mocks | pass | ✅ |
Eval cases live in packages/agents/test/eval.test.ts — add your own labeled scenarios.
npm run test # all workspaces (Vitest) — no API key required
npm run typecheck # strict TypeScript across the monorepo
npm run lint # ESLint
npm run build # production build, all 4 appsThe critical test — the catch:
it("flags a self_contradiction when the patient meets every published criterion", async () => {
const { finding } = await run({
caseId, context: MARIA_CONTEXT, policy: DEMO_POLICY,
classification: DEMO_CLASSIFICATION, nowISO: "2026-05-31T00:00:00.000Z",
});
expect(finding.type).toBe("self_contradiction");
expect(finding.citedPolicySection).toBe("4.2");
expect(finding.metCriteria).toHaveLength(3);
});| Layer | Technology |
|---|---|
| Monorepo | Turborepo + npm workspaces |
| Framework | Next.js 15 (App Router) |
| Language | TypeScript 5 (strict everywhere), ESM |
| Agents / LLM | LangChain prompt templates + Claude (@anthropic-ai/sdk) |
| Live progress | NDJSON streaming over fetch (graceful fallback) |
| Sandboxed execution | Daytona (deadline + eligibility math) |
| Data extraction | Apify |
| Compliance guardrail | Opsera (MCP) |
| Web action / filing | Rtrvr.ai |
| Auth / database | Insforge (in-memory store as the keyless fallback) |
| Object storage / audit | Tigris + SHA-256 hash chain |
| Semantic memory | Brain2 |
| Model inference / OCR | Nebius |
| Orchestration | Kalibr-style retry + fallback |
| Tests | Vitest |
| Deployment | Render (render.yaml blueprint) |
Workspace packages ship raw TypeScript (no build step) — Next transpiles them just-in-time and Vitest inlines them.
Recourse/
├── apps/
│ ├── api/ # :3001 — the agent pipeline + all routes
│ ├── patient-intake/ # :3002 — upload a denial letter
│ ├── dashboard/ # :3003 — review the catch + appeal, Approve & File
│ └── case-audit/ # :3004 — status timeline + tamper-evident log
├── packages/
│ ├── types/ # Shared data model + API contracts (single source of truth)
│ ├── agents/ # The 13 agent run()s (one file each) + the eval set
│ ├── sponsors/ # Provider clients — real-or-mock by env key
│ ├── orchestrator/ # runDraftPipeline / runFilePipeline + withRetry
│ ├── seed/ # Maria's record, canned policy, demo tokens, in-memory db
│ ├── ui/ # Shared design system + typed API client + live trace
│ └── config/ # Tailwind preset, ESLint preset, tsconfig base
├── render.yaml # Render Blueprint: API + 3 UIs as services
└── .env.example # every integration key, blank
A render.yaml blueprint deploys the API + three UIs as Render web services. Each builds the whole workspace and serves on Render's $PORT; all integration keys are declared blank (sync: false) so the deploy runs on mocks out of the box. Fill them in the Render dashboard to go live, then paste the deployed API URL into each UI's NEXT_PUBLIC_API_URL.
One engine, five markets. The same 13-agent spine serves any domain where a high-stakes "no" can be overturned — swap only the evidence source (agent 4) and the filing target (agent 11):
| Vertical | Evidence source | Filing target |
|---|---|---|
| Health insurance (demo) | Payer medical policy + clinical guideline | Payer appeal portal |
| Eviction defense | Local housing statute + notice rules | Court e-filing portal |
| Government benefits | Program eligibility rules | Benefits re-application portal |
| Bank charges / subscriptions | Cardholder agreement / dispute rules | Bank dispute form |
| Auto / home claims | Policy language | Insurer claim portal |
MIT — © 2026 Shanay Gaitonde, Sahiel Bose
{ "success": true, "case": { "id": "case_maria", "status": "needs_human_approval", "vertical": "health", "memberName": "Maria Mitchell", "service": "Trastuzumab (Herceptin)", "payer": "BlueCross Demo Health", "contradiction": { "type": "self_contradiction", "citedPolicySection": "4.2", "metCriteria": ["HER2-positive (IHC 3+)…", "Stage II–IV…", "ECOG 0–2"], "deadlineDaysLeft": 166 // computed live from the notice date }, "draftAppeal": { "id": "draft_case_maria", "citations": ["…Policy §4.2…"] }, "computed": { "deadlineDaysLeft": 166, "eligibility": "1" }, "guardrail": { "passed": true, "checks": [ /* deadline, citations, PHI, format */ ] } }, "pipeline": { "agentsExecuted": 9, "status": "needs_human_approval" } }