Claims and benefits

Does obfuscation break insurance claim adjudication?

No, not on its own. We protected a claim-adjudication module on five presets and every claim came out with the same member responsibility to the cent. Then we renamed the members it reads, and a family with one shared $6,000 out-of-pocket maximum was billed $11,800 without a single wrong number appearing anywhere.

What we actually measured

A cost-share estimator decides how many dollars a member owes. The module we built for this article has the shape those estimators really have: a deductible, a coinsurance rate, an out-of-pocket maximum, a network pricer the payer wired in because the negotiated rates are theirs and not the vendor's, and one more thing that gets far less attention than any of them - a setting that says what the running totals are counted over.

The engine that consumes all of that is a separate installed library. We copied it in unprotected, exactly as a real dependency arrives, and protected only the file the team owns. That split is the whole point: a rename moves the names in your code and cannot move the names inside a package you did not rebuild.

First the baseline. We protected the module on five presets covering both the ES5 and the modern targets, plus the string transforms, and ran every claim through each build. Twenty-five comparisons, twenty-five identical outputs. Protection alone changed nothing about what anybody owed. If you take one thing from this page, take that: the transform is not the hazard.

The setting nobody audits

This plan is a family plan. One $2,000 deductible and one $6,000 out-of-pocket maximum, shared by everybody on the policy. That is what the household bought, and it is expressed in a single option:

accumulatorScope: 'family'

It is not a threshold. It is not a flag, a list, a limit or a unit. Every dollar figure in the file is correct whatever happens to it. What it decides is which claims get added together before those correct figures are compared to anything.

That distinction turns out to matter more than any of the numbers around it, and it is invisible to the defence this series has recommended most often. We built the engine to refuse a member responsibility that is not a finite number, because that one line catches a whole family of failures. It cannot catch this one. A total taken over the wrong population is a perfectly good number.

Renaming the scope: the family plan becomes five individual plans

The vendor's own default is per-member, which is the only sensible default for a library that cannot know what you sold. When the option key moved, that default was substituted, and the household stopped sharing anything.

Nobody's figure is wrong. The deductible is still $2,000. The out-of-pocket maximum is still $6,000. The coinsurance is still 20%. Every claim is priced correctly against the correct fee schedule. But the second member of the household, who correctly owed $330 because the family deductible was already satisfied, now owes $1,650 - a whole second deductible. The third owed $220 and now owes $1,100. The fourth had their surgery trimmed to $3,230 by the shared ceiling and now owes the full $6,000, because they have a ceiling of their own. A claim that correctly cost nothing at all, because the household maximum had been met, now costs $830.

Add those up. The household is billed $11,800 against an out-of-pocket maximum of $6,000. Every individual line on every explanation of benefits is arithmetically correct, and the plan's own configuration dump still reads:

deductible=$2000 oop-max=$6000 coinsurance=20%

There is nothing on that screen to notice. The three numbers a benefits analyst would check are all right.

Renaming the grouping key: strangers start paying each other's deductibles

The option names a field on the claim record - familyId - and that field is the other half of the same idea. An option key cannot become unreadable, because the library substitutes its own default. A record field genuinely can.

When it went missing, every bucket key collapsed to the same string. Not an error, not a crash: the engine creates buckets on demand, so a claim carrying a key it has never seen simply starts a new bucket, or silently joins somebody else's.

buckets=fam:undefined=$6000.00(ded $2000.00)

One bucket, for the entire book of business. An unrelated household - different policy, different people, no connection to the first family at all - filed a $4,950 claim that should have cost them $2,590, starting from their own zero. It cost them nothing:

unrelated-family-must-start-from-zero PAID=true ... owes $0.00 USD ... CAPPED at the out-of-pocket maximum, OOP-MAX-MET

Four strangers' claims had already exhausted the shared maximum. The engine did exactly what it was asked to do.

Same concept, opposite directions, decided by which half moved

This is the finding worth carrying out of the article. Losing the option over-charges the household that owns the plan. Losing the record field under-charges everybody by pooling the whole book. Same idea, one name apart, opposite directions - and which direction you get is not a property of what the setting means. It is decided by whether the name that moved was one the library substitutes a default for, or one it accepts as data.

Neither failure raises anything. Both produce finite, positive, plausible dollar amounts. Both survive serialisation, reach dashboards, print on statements and get paid.

The number one field away failed loudly

On the same claim record, immediately beside the grouping key, sits billedUsd - an ordinary amount. We renamed it as a control.

Every claim in the file was refused, immediately and unmistakably:

PAID=false non-finite-member-responsibility(refused before payment)

Total loss of function. Nothing adjudicates, somebody is paged within the hour, and the deploy is rolled back before a single statement goes out. That is the good outcome.

So on one record, one field apart: losing the amount fails closed and screams, and losing the key that says what the amount is counted with fails open and says nothing. The mitigation this series has recommended most often - require the number to be finite before you compare it - is structurally unable to see the second one, because the numbers were never the problem.

What actually protects you

The first control is in how the data is shaped, and it is worth having regardless of obfuscation, because a serialisation hop that drops undefined keys, a hand-edited config and a library upgrade all produce the same missing name. Do not let a grouping key be optional. An accumulator lookup that cannot identify its bucket should refuse, not invent one. Creating a bucket on demand is a convenience that turns a missing identifier into a silent merge.

The second is reconciliation. A per-member total is checkable against the claims that fed it. If the sum of the claims in a bucket does not equal the bucket, or if a bucket contains members who are not on one policy, that is a report you can run nightly and it catches this failure in a day rather than at open enrolment.

The third is scope. Member renaming is off unless you turn it on, and when you turn it on it takes a pattern. Point that pattern at what you actually want obscured. Option keys a third-party library reads, field names that cross a serialisation boundary, and anything that identifies a record rather than describing it are all names you do not own alone, whatever file they are written in.

Frequently asked questions

Does protecting a claim adjudicator change what a member owes?

Not in what we measured. Five presets covering the ES5 and modern targets plus the string transforms, every claim through every build: twenty-five comparisons, twenty-five identical results. The default protection path did not change a single member responsibility.

Which setting actually caused the failure?

Member renaming, which is a separate option that is off unless you enable it and takes a pattern when you do. It rewrites property names in the file you protect. Any name that has to survive a boundary - an option another library reads, a field it treats as data - is at risk, and everything in this article follows from that.

How much does a lost accumulator scope actually change?

In our measurement, a household with one shared $6,000 out-of-pocket maximum was billed $11,800, because the plan silently became five individual plans. Every individual figure stayed correct: the deductible, the maximum, the coinsurance rate and each claim's allowed amount were all exactly as configured.

Why did losing the grouping key fail in the opposite direction?

Because a library substitutes its own default for an option key it cannot read, but treats a record field it cannot read as data. The option reverted to per-member and over-charged one household. The field collapsed every claim into one bucket, so unrelated households began satisfying each other's deductibles - an unrelated family's $2,590 claim cost them nothing.

Would a finite-number check have caught this?

No, and that is the point worth carrying. We built that check into the engine deliberately. It caught the renamed claim amount instantly and refused every claim loudly. It cannot see a total taken over the wrong population, because such a total is a perfectly ordinary finite number.

What is the cheapest thing to change in our own code?

Refuse to adjudicate when the grouping key is absent, rather than creating a bucket on demand. A missing identifier should be an error, not a new bucket. That single change converts the silent failure in this article into a loud one, and it protects you against dropped keys and config mistakes as well.

Can we still obfuscate a claims front end at all?

Yes. The baseline measurement is the answer to that: protection alone left every result identical on all five presets. Use member renaming deliberately with a pattern scoped to names you own on both sides, and keep identifiers, cross-boundary option keys and anything a downstream system matches by text out of it.

Related reading