# Revenue Leak Audit

**An audit that reads 12 months of a dental practice's ledger, schedule and claims data and returns
one dollar figure, itemized, with the patient names attached.**

Product of RIG (Mike Rodgers). Buyer is an owner-dentist or medspa owner doing $900K–$2.5M.
Slug: `revenue-leak-audit`.

---

## Run it

No build step, no dependencies, no package manager.

```bash
cd ~/Developer/rig-offerings/revenue-leak-audit
python3 -m http.server 8781
```

Then open:

| | |
|---|---|
| Live app | <http://127.0.0.1:8781/app/index.html> |
| Landing page | <http://127.0.0.1:8781/landing.html> |

`landing.html` also opens fine as a `file://` URL. **The app should be served over HTTP**, because
`app/index.html` references the icon through an SVG `<use href="../icon.svg#mark">`, which most
browsers block on `file://`. Everything else in the app works either way.

---

## What is in here

```
revenue-leak-audit/
├── app/
│   ├── index.html      4-step app shell
│   ├── styles.css      dark UI + white print-ready report
│   ├── engine.js       the audit engine. all math lives here
│   └── app.js          controller, rendering, CSV, fleet calls
├── icon.svg            512x512 hand-authored
├── icon-256.png        raster export
├── landing.html        conversion landing page
├── GTM.md              ICP, triggers, 5 emails, 3 posts, objections, 10 prospect types
├── PRICING.md          tiers, replacement costs, ROI, margin math, guarantee
└── README.md
```

---

## How the app works

Four steps across the top.

**1. Practice** — a benchmark model. Enter collections, active patients and five operating
percentages; the six-category leak recomputes live on every keystroke and slider drag. Four
presets (solo GP, 3-location group, medspa, well-run practice).

**2. Ledger** — the real scan. Either generate a deterministic sample ledger (seeded, so the same
seed always produces the same 420 patients and therefore the same number twice), or drop in a CSV
export. Column matching is loose: `Last Visit`, `lastvisit` and `LastVisitDate` all resolve.

**3. Findings** — the output that matters. Every finding is a row: patient name, chart number,
category, what was found, gross dollars, expected recovery, and the next action. Filterable by
category, paginated, exportable to CSV.

**4. Report** — a white, print-ready client deliverable. `Print / save as PDF` produces the actual
artifact a client receives. The executive summary is generated by an LLM on the local GPU fleet.

### The six leak categories

| # | Category | What it detects |
|---|---|---|
| 1 | Unscheduled diagnosed treatment | Accepted treatment plans with no appointment attached |
| 2 | Lapsed recall / overdue hygiene | Past-due patients with nothing on the books |
| 3 | No-shows and same-day cancellations | Broken appointments net of same-day backfill |
| 4 | Undercoded and unbilled procedures | D1110 billed where D4910 was performed, unbilled D0330 panos, missing D1206 |
| 5 | Aged and denied insurance claims | Claims over 90 days or denied and never re-worked |
| 6 | Expiring unused insurance benefits | Unused annual maximums that reset December 31 |

### Two things that make the number defensible

**Overlap netting.** The same patient usually appears in several categories at once: a lapsed
patient also carries unscheduled treatment and an unused annual maximum. Naive leak calculators sum
the categories and produce a number larger than the practice. The gross is netted down 28%
(`K.overlapFactor`) before anything is claimed, and the report states this explicitly.

**Monotonicity.** A better-run practice must show less leak. This is enforced and tested: with
collections held at $1.2M, the model returns 30.5% of collections for a sloppy practice, 26.1% for
average, and 18.8% for a tight one. An earlier version failed this (higher case acceptance inflated
the backlog) and was fixed by adding the same-day-scheduling driver.

Every constant is named and commented at the top of `engine.js`. There are no magic numbers buried
in the calculations.

---

## Verification performed

Driven with Playwright against Chromium at 1440x1000, not just eyeballed.

| Check | Result |
|---|---|
| App loads, 6 categories render | pass |
| Sliders recompute the headline (recall 68% → 40% moved $366,002 → $479,128) | pass |
| All 4 presets recompute | pass |
| Sample ledger scan: 420 patients → 709 findings across 344 patients | pass |
| Category filters, pagination (25 → 50 rows) | pass |
| Report populates: 10 top actions, 7 category rows | pass |
| AI narrative generated live on blackwell | pass |
| CSV round trip: 420 rows out, 420 back, re-audit succeeds | pass |
| Determinism: same seed produces byte-identical records | pass |
| Monotonicity: better practice shows less leak | pass |
| Console errors, app and landing | **0** |
| Landing page: 5 sections, 7 FAQ, 3 tiers, 6 leak cards | pass |
| Mobile 390 / 360 / 320px: no horizontal overflow, no collision | pass |
| Nav link landing → app resolves | pass |

Two real bugs were found by this process and fixed: the report table's numeric columns were
rendering near-white on the white report card (dark-theme `td.num` bleeding through), and the
landing page brand text collided with the nav button below 400px.

---

## Fleet usage

Heavy generation ran on the local GPU fleet, not a cloud API.

| Node | Endpoint | Role | Measured latency |
|---|---|---|---|
| blackwell | `100.67.126.117:8000` (vLLM, 3x RTX PRO 6000) | narrative, pricing draft | **1.1s** |
| rig-36gb | `100.89.143.27:11434` | objections draft | 5.3s |
| rig-96gb | `100.102.142.84:11434` | LinkedIn + prospect drafts | 103.7s |
| rig-28gb | `100.76.209.22:11434` | reserve | 273.6s |
| rig-256gb | `100.91.39.12:11434` (laguna-262k) | pricing draft | degenerated, discarded |

The app fails over down this list fastest-first with per-node timeouts. The original ordering was
worst-first, which caused the 120s abort to fire and silently fall back; that is fixed.

**Honest note on the generated copy.** The fleet produced usable structure but weak prose and
arithmetic errors: `rig-256gb` collapsed into a repetition loop mid-document, and the pricing draft
returned a transposed table plus a claimed "100 clients/month ceiling" for a solo operator. Fleet
output was used as raw material; the shipped `GTM.md` and `PRICING.md` were rewritten, and all their
dollar figures are generated directly from `engine.js` so the docs and the app can never disagree.

---

## What is real vs stubbed

### Real and working

- The audit engine. All six categories, both modes, every formula. Runs in the browser and under Node.
- The row-level ledger scan producing named, ranked, per-patient findings.
- The deterministic sample ledger generator (seeded PRNG, realistic bimodal distributions).
- The CSV parser, written from scratch, handling quoted fields and embedded commas.
- CSV import, CSV export of both the ledger and the work list.
- Live recompute, filtering, pagination, print-to-PDF report.
- AI executive summary generated on the local fleet, with a deterministic written fallback that is
  never a placeholder. Verified generating live on blackwell.
- The landing page, fully responsive.
- All dollar figures in `GTM.md` and `PRICING.md`, computed from the engine.

### Stubbed, missing, or blocked

- **No practice management system integration.** Nothing connects to Dentrix, Eaglesoft, Open Dental
  or Curve. Delivery today is a manual CSV export. The landing page and GTM copy are careful to say
  "you export, I scan" rather than implying a connector exists.
- **Sample data is synthetic.** Realistic distributions, not real patients. No real practice ledger
  has been audited with this yet, so the "typical finding" ranges are modeled, not observed.
- **Benchmark constants are researched estimates, not proprietary data.** `dxRatio` 1.55,
  `undercodePct` 3.4%, `agedClaimPct` 6.2% and the recovery rates are defensible industry ranges.
  They should be recalibrated against the first five real audits.
- **The case study in GTM email 3 is modeled, not a real client.** It is derived from the engine's
  solo-practice profile. Do not send it as a named reference until a real one exists.
- **Apollo API is blocked.** The key in `com.rig.env.APOLLO_API_KEY` authenticates
  (`/auth/health` → `is_logged_in: true`) but the account is on the **Free plan with zero credits**.
  `mixed_people/search`, `mixed_companies/search` and `people/match` all return 403
  `API_INACCESSIBLE`. GTM section 7 is therefore written for the Apollo web UI. Programmatic
  prospecting requires a paid plan.
- **LinkedIn `li_at` cookie is present but unused.** No LinkedIn automation is built here, and none
  should be added without a Gate-D approval.
- **The fleet is not reachable from a client's browser.** The endpoints are Tailscale addresses on
  Mike's network. On a client machine the app silently uses the deterministic written summary, which
  is complete and reads well. For a real client deliverable the narrative should be generated on
  Mike's machine before the PDF is sent.
- **No BAA template.** Required before the first paying client touches real data.
- **No backend, no auth, no persistence.** Everything is client side. Nothing is stored, which is
  convenient for HIPAA posture but means there is no multi-client history yet.
- **Medspa category mapping is described but not implemented.** The six categories in `engine.js`
  are dental-specific.

---

## Next three things

1. Run the audit free on two real practices in the network. Recalibrate the benchmark constants
   against observed results and replace the modeled case study with a real one.
2. Build the Open Dental connector first. It has a documented database schema and the friendliest
   licensing of the four major systems.
3. Get Apollo onto a paid plan, or drop it and drive prospecting from state dental board rosters,
   which are public, free, and better suited to the trigger events in `GTM.md`.
