Inspiration

Here are two numbers from the same dataset, and the gap between them is the whole project.

In 2024, Medicare Advantage insurers denied 4.1 million prior authorization requests. Only 11.5% of those denials were appealed.

Of the appeals that were filed, 80.7% were overturned. KFF, who publish this from the CMS reporting files, note that across every year they examined, more than eight in ten appeals reversed the original denial.

So the appeals win. Four out of five of them. And nine out of ten denials never get one.

That is roughly 3.6 million denials a year that nobody appealed, in a system where the appeals that do get filed succeed most of the time. I want to be careful with that inference. The denials people choose to appeal are probably the stronger cases, so you cannot assume all 3.6 million would have been overturned. But the number is 3.6 million, and the overturn rate is eight in ten. Even a fraction of that is a lot of care that was ordered by a doctor, refused by an insurer, and then simply not argued.

Why does nobody appeal? The AMA asked. Their 2025 survey of a thousand physicians found practices spend 13 hours a week on prior authorization, handling about 40 requests per physician, and four in ten employ somebody whose entire job is prior auth and nothing else. Only 21% always appeal a denial. 59% say they expect to lose. 52% say there is no staff time.

That first reason is the part that gets me. Fifty-nine percent expect to lose an argument that succeeds eight times out of ten.

The work itself is not hard. Read the denial code. Pull the payer's medical policy. Check the chart against the criteria. Write a letter citing the right section. It is just relentless, and it arrives forty times a week, and the person doing it has already been doing it for thirteen hours.

What it does

The survey gives two reasons appeals do not get filed: no staff time, and an expectation of losing. Mpatapo is built against both.

Six agents behind one gateway, and a denial passes through all of them in order.

Intake normalises the denial notice into CARC and RARC reason codes. Policy retrieves the payer's medical policy for the denied service. Clinical maps what is documented in the chart against the criteria in that policy. Strategy decides whether to appeal, request a peer-to-peer review, or abandon, using what this payer has done with similar arguments before. Drafting writes the appeal letter. Compliance redacts PHI and enforces guardrails on every boundary crossing.

Two of those handoffs matter more than the rest. Policy never sees the chart, and clinical never sees the policy source. Each agent gets only what its job needs.

Then the part the whole thing turns on. Before a letter goes anywhere, every citation in it is checked against the text that was actually retrieved. If the letter cites a section that is not in the retrieved policy, it does not go out. It goes back to drafting with the correction.

That is not hypothetical. Open the console and the first claim you see was stopped twice. The model cited a section §8.8 that does not exist in the policy. Rejected. It redrafted and cited §1.9, also fabricated. Rejected again. The claim is now sitting escalated, waiting on a person, because the fleet would not send a letter it could not stand behind.

An appeal letter with an invented citation is worse than no letter. It gets the appeal denied, and it costs the practice credibility with that payer on every future claim.

So, against the two reasons. The staff time problem is what the six agents are for: the reading, the retrieving, the matching and the drafting all happen without a person. The expectation of losing is what the strategy agent is for — it looks at what this payer has actually done with this argument before, so the decision to appeal is made on evidence rather than on a feeling formed after thirteen hours of paperwork.

How we built it

Python on Cloud Run. Pub/Sub carries denial events. Firestore holds every claim, the agent registry, and the audit trail — which is written step by step as the run happens, not assembled afterwards, so the console can replay exactly what each agent saw and decided. Gemini 3.5 Flash on Vertex AI through the Google GenAI SDK does the reasoning. Cloud Trace collects spans through OpenTelemetry.

Underneath is a substrate I wrote first and vendored into each of my three projects — config, safety guards, the Firestore wrapper, the Gemini client, telemetry, test fakes. Copied in rather than published as a package, so anybody can clone one repo and run it without hunting for a dependency.

The console is plain HTML, CSS and JavaScript. No framework. It scans clean against axe-core.

Everything on screen is synthetic. Every claim, patient, payer and chart is fabricated for the demo, and the console says so on every page it serves.

Challenges we ran into

The Gemini Enterprise Agent Platform registry cannot hold your own agents. This one cost real time. The Agent Registry is genuinely live and creatable, but base_agent is a required, immutable field, and it currently accepts exactly one value: Google's own Antigravity coding-agent product. There is no field in the schema for an arbitrary agent card. It is a first-party resource, not the generic catalogue the documentation reads like. So the registry is Firestore-backed instead, in app/registry.py, and it is live in the deployed service.

Locations are not interchangeable, and calling the wrong one looks exactly like the feature not existing. Agent Registry lives at global. Agent Runtime and Model Armor live at us-central1. Gemini model access routes through a Vertex location that has to be global, because gemini-3.5-flash is served nowhere else and every regional endpoint 404s. Cloud Run, Firestore and Pub/Sub are us-central1. Get one wrong and you get a 404 or an INVALID_ARGUMENT that reads like the thing was never built.

Memory Bank was the wrong shape for what I needed. It round-tripped fine end to end. But retrieve is embedding similarity over free-text facts, and what the strategy agent actually needs is an exact (wins, attempts) counter with no race. That is the primitive behind "this payer has overturned 4 of 6 similar denials". Firestore transactions already solve that. Memory Bank does not offer an equivalent for structured counters.

And a dependency broke production on a routine redeploy. google-api-core 2.35.0 percent-encodes path segments, so Firestore's (default) database becomes %28default%29 and every query fails server-side. The container installs from pyproject.toml with no lock file, so a rebuild picked it up silently. It is pinned now, with a test.

Accomplishments that we're proud of

It stops itself. The claim on the front page was rejected twice for citing policy sections that do not exist, and it escalated to a human instead of sending. In a field where a confident wrong citation costs you the appeal and your credibility with that payer, an agent that knows when to stop is worth more than one that always produces an answer.

The registry is genuinely cross-department. It started as six agents all owned by revenue-cycle, which is one department wearing a catalogue. It now also carries an accessibility agent owned by engineering and an eviction-response helper owned by legal aid — both running on this same substrate, both separately deployed, both listed with the address a caller would actually use. A catalogue entry nobody can reach is a claim. One that resolves is a fact.

The audit trail is written as it happens, not reconstructed at the end. If a run dies halfway, what it did up to that point is already durable.

The console passes its own accessibility scan, zero violations in light and dark, which felt like the least I could do given what the sibling project does for a living.

Data sources

Everything is synthetic and generated for this project: the denial notices, the payer medical policies, the chart findings, the claim identifiers, the payer names. No real patient, provider or payer record is used anywhere, and no PHI exists in this system to leak.

The real-world inputs are two published studies. KFF's January 2026 analysis of CMS Part C reporting files, which is where the 4.1 million denials, the 11.5% appeal rate and the 80.7% overturn rate come from. And the AMA's 2025 Prior Authorization Physician Survey, which is where the 13 hours a week and the reasons people give for not appealing come from.

The CARC and RARC reason codes are the genuine industry code set, because the whole point of the intake agent is speaking the language a payer actually uses.

What we learned

Design the handoffs, not just the agents. The interesting decisions in this project were about what each agent is not allowed to see. Policy never sees the chart. Clinical never sees the policy source text. That constraint is what makes the citation check meaningful, because the validator is comparing against something the drafting agent could not have simply copied.

Verification has to compare against what was retrieved, not against what sounds right. A citation that looks plausible is exactly the failure you cannot catch by reading the output.

A green test suite can sit over a broken product. The api-core failure took the read API down while every test passed, because the tests use fakes and never build a real client. That is a gap in the shape of the test strategy, not in any one test.

And confidently written notes are not evidence. I found claims in my own documentation that the code did not support. Now I try to break an invariant before writing it down.

What's next for Mpatapo

Real payer policies. The clinical agent's reasoning is only as good as the policy text it is mapping against, and real medical policies are longer, worse structured, and far more contradictory than anything I generated.

A wider strategy signal. "This payer overturned 4 of 6 similar denials" is the right idea on a sample size that means nothing. It wants real appeal outcomes across many practices before it should influence a decision.

Peer-to-peer scheduling. Strategy can already recommend a peer-to-peer review, but a human still has to go and book it.

And the honest one: this has never touched a real claim. Everything here is synthetic by design, because building it any other way would mean handling PHI I have no business handling. The next step is not more agents. It is a pilot with one practice, one payer, and somebody qualified reading every letter before it goes out.

Share this project:

Updates

Submission history