DEV Community

Thiago Longo Moraes
Thiago Longo Moraes

Posted on

I used the ICD-10 tree as a scoring function

Most medical quiz apps score you as right or wrong. That throws away the part that matters. In clinical reasoning, mistaking cholecystitis for appendicitis and mistaking it for a myocardial infarction are not the same mistake. One is a near miss inside the acute abdomen. The other is a different organ system.

So when we built NEXO, a daily diagnosis game for physicians and medical students, we made the scoring reflect that.

The idea

ICD-10 is a tree. A code like I21.0 sits inside category I21, inside group I20-I25, inside the chapter for diseases of the circulatory system. That hierarchy is already a distance metric. We just had to use it.

When a player submits a guess, we compare the guessed code against the answer and return the deepest node they share:

exact code         I21.0  vs  I21.0
same category      I21.9  vs  I21.0
same group         I20    vs  I21.0
same chapter       I50    vs  I21.0
different chapter  J18    vs  I21.0
Enter fullscreen mode Exit fullscreen mode

Five levels instead of two. A player who lands on "same chapter" learns they were reasoning in the right system and missed the specific entity. A player who lands on "different chapter" learns something else entirely. Both are wrong answers, and they are not worth the same.

Keeping the answer secret

The daily case is the same for every player worldwide, and the answer has to stay hidden until the day rolls over. The comparison runs server side against the stored code. The client never receives the answer, only the level. That kept the API small: send a code, receive an enum.

The harder problem: cases that hold up

Generating a plausible clinical case with an LLM takes seconds. Generating one a physician will not tear apart is a different problem. A clue that sounds right but does not belong to the diagnosis is worse than no clue at all.

We built NEXO Core for that. Every disease, syndrome, exam, clinical finding and ICD-10 code is a node. Edges connect what clinical practice treats as neighbors: pneumonia connects to its etiological agent, its radiological pattern, its differentials. The graph currently holds around 11,500 concepts and 26,000 edges, drawn from 27 reference textbooks.

Every generated case crosses that graph before approval. Each clue has to appear among the neighboring concepts of the proposed diagnosis. A clue that does not, fails. It is a structural check, not a human reading it over and nodding.

Stack

  • React Native with Expo for the app
  • Supabase for Postgres, auth, storage and edge functions
  • Next.js on Vercel for the marketing site and the case management backoffice
  • RevenueCat and StoreKit 2 for subscriptions
  • Sentry for crash reporting

649 cases across 25 specialties, in six languages.

What I would do differently

Start with the graph, not the cases. We wrote a few hundred cases before the graph existed and had to re-audit all of them against it afterwards. That audit found problems a read-through had missed.

NEXO is live on iOS and the daily case is free forever, with no ads. The site is at nexo.wiki.br and the app is on the App Store.

Happy to answer questions about the graph or the scoring.

Top comments (0)