惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
F
Fortinet All Blogs
B
Blog
GbyAI
GbyAI
P
Proofpoint News Feed
量子位
The Register - Security
The Register - Security
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
B
Blog RSS Feed
WordPress大学
WordPress大学
Recorded Future
Recorded Future
Recent Announcements
Recent Announcements
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Secure Thoughts
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Webroot Blog
Webroot Blog
AWS News Blog
AWS News Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The GitHub Blog
The GitHub Blog
爱范儿
爱范儿
O
OpenAI News
月光博客
月光博客
H
Hacker News: Front Page
S
Security Affairs
W
WeLiveSecurity
The Hacker News
The Hacker News
aimingoo的专栏
aimingoo的专栏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Help Net Security
Help Net Security
MongoDB | Blog
MongoDB | Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
D
Docker
T
The Blog of Author Tim Ferriss
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
J
Java Code Geeks
S
Securelist
Microsoft Azure Blog
Microsoft Azure Blog
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
A
About on SuperTechFans

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How we built a medicine-substitution engine that refuses to be clever
Aman Sachan · 2026-06-18 · via DEV Community

How we built a medicine-substitution engine that refuses to be clever

There is a category of bugs where the code looks perfectly correct in code review, the unit tests pass, the demo on stage goes beautifully, and a real person dies.

Medicine substitution is one of them.

We built Agada — point a phone camera at a medicine strip in India, and the app tells you whether the drug is registered with the regulator, what it does, and whether a chemically identical version is available at the government pharmacy for a fraction of the price. The point is real: Indians spend about ₹65,000 crore a year out of pocket on branded medicines when the same molecule sits in a Jan Aushadhi Kendra at a tenth of the cost. The Dolo-650 story (₹32 vs ₹4.90 for the same paracetamol) is the most famous example, not the only one.

The hard part isn't the camera, the OCR, or the price lookup. The hard part is the substitution engine — the piece of code that decides "is this salt the same as that salt." Get that wrong in the wrong direction, and the app cheerfully tells a user that their 500mg anti-epileptic can be replaced with a 200mg one, because the strings look similar.

So we built a matcher that refuses to be clever. This post is about the parts we deleted.

The starting problem

A user scans "Crocin 500mg Tablet IP". A Jan Aushadhi record says "Paracetamol 500 mg". A naive matcher says: both contain "500", both contain "Paracetamol" (or "Acetaminophen", which is the same thing in a different country), ship it. The user buys the generic. Savings: ₹20. Everyone wins.

Now consider:

  • Crocin 650 Advance vs Dolo 650 — same molecule, same dose, different brands. Fine.
  • Augmentin 625 vs Augmentin 375 — same two salts, different ratio. The combo tolerance might let it through, but the clinical implication is real.
  • Levofloxacin 500 vs Ofloxacin 400 — both fluoroquinolones, both "similar," but levofloxacin is the levo-isomer of ofloxacin and is dosed at roughly half. A "fuzzy" match here could halve someone's antibiotic dose mid-infection.
  • Levothyroxine 50 vs Thyroxine 50 — different drug. The prefix levo- matters. Pharmacologically, they are not interchangeable.
  • Metformin 500 + Glimepiride 2 (a combo) vs Metformin 500 alone — the user has a combo product, the generic is a single salt. Recommending the single-salt replacement to a patient on a combo is medically meaningless at best.

A generic-substitution engine that "just works" in code review is one CSV import away from a clinic visit. We needed blocking rules, not scoring rules.

What we deleted before we shipped

The first version of dbService.js was a similarity engine. We gave every candidate product a score, ranked them, and returned the top three. It looked beautiful. It would have killed someone within a month of launch.

Specifically, what we deleted:

  1. A pure Levenshtein / token-overlap score across the full product name. The string "Augmentin DDS" looks close to "Augmentin" by edit distance. They are different drugs.
  2. A "if both contain the same keyword" fallback. Both products contain "500" and "mg". So does a vitamin tablet.
  3. A confidence label that meant "how similar these strings are" — which we then surfaced to the user as if it meant "how safe this substitution is."

In their place we built a hard-block-first, score-second architecture. Hard blocks are non-negotiable. Soft scoring only ranks among things that have already passed every block.

The five hard blocks

Every candidate passes through these in order. Any failure means the candidate is silently dropped — never shown to the user, never even considered for ranking. The code (lightly compressed) is in src/services/dbService.js:

// BLOCKING RULES (hard, not soft):
//  1. Form bucket mismatch — solid / liquid / injection / topical must match exactly
//  2. Drug-prefix mismatch — levo-/s-/dextro-/nor-/des- prefix = different drug
//  3. Extra salt in product — combo product never shown for simpler query
//  4. Combipack — always blocked (multiple drugs in one pack)
//  5. Every salt's dose — ALL salts checked, combo ±5%, single ±10%

Let me walk through each, because the implementation details are where the safety lives.

Block 1: Form bucket must match exactly

We bucket every product into one of four forms: solid, liquid, injection, topical. Syrups, suspensions, drops and oral solutions all go in liquid (per 5ml, etc.). Gels, creams, ointments, lotions, shampoos and soaps all go in topical.

A user scanning a tablet cannot be shown a syrup as a substitute, even if the active ingredient and dose are identical. The dispensing format matters for compliance, dose measurement, and child safety. A 5ml spoonful is not the same as a tablet to a parent with a sick toddler.

function formBucket(text) {
  const t = (text || '').toLowerCase()
  if (/\bgel\b|\bcream\b|\bointment\b|\blotion\b|\bshampoo\b|\bsoap\b|\btopical\b/.test(t)) return 'topical'
  if (/\binjection\b|\binfusion\b|\biv\b/.test(t))                                          return 'injection'
  if (/\bsuspension\b|\bsyrup\b|\bdrops?\b|\bsolution\b|\boral\s+liquid\b|\bper\s+\d+\s*ml\b/.test(t)) return 'liquid'
  return 'solid'
}

The bucket is computed from the raw text, not from a curated column, because the Jan Aushadhi CSV and the user's scanned text both come in as messy strings. We cannot trust the data.

Block 2: Drug-modifying prefix

This is the rule that catches levofloxacin vs ofloxacin, levo-thyroxine vs thyroxine, s-amlodipine vs amlodipine. If one salt name starts with a pharmacological modifier and the other does not, they are different drugs and the match is blocked.

The exception — and this took us a week to get right — is when both names share the same prefix. Levofloxacin and levofloxacin are still the same drug, even though levo- is a modifier. The check is asymmetric, not absolute:

const DRUG_PREFIX = /^(levo|dextro|nor|des|fos|s\s*[-\s]|r\s*[-\s]|methyl|ethyl|iso|neo)\s*/i

function saltNameMatch(a, b) {
  const na = normName(a), nb = normName(b)
  if (na === nb) return true
  const aHasPrefix = DRUG_PREFIX.test(na)
  const bHasPrefix = DRUG_PREFIX.test(nb)
  if (aHasPrefix !== bHasPrefix) return false  // ← the safety-critical line
  // ... word-boundary substring match only if both share (or both lack) the prefix
}

That single line is, in practice, the difference between a useful app and a malpractice suit.

Block 3: No extra salts in either direction

If the user has Metformin + Glimepiride, the generic must contain both Metformin and Glimepiride. If the user has plain Metformin, the candidate generic must not have extra active ingredients. The check is bidirectional:

for (const qs of qSalts) {
  if (!pSalts.some(ps => saltNameMatch(qs.name, ps.name))) return 'blocked'
}
for (const ps of pSalts) {
  if (!qSalts.some(qs => saltNameMatch(qs.name, ps.name))) return 'blocked'
}

The bug this prevents is real and well-documented: combo products in India sometimes have five or six active ingredients, and a "cheaper alternative" that drops two of them is not an alternative — it is a different prescription.

Block 4: Combipacks are always blocked

A combipack is two separate drugs in two separate strips in the same box, often with different doses for each. We refuse to substitute them. The check is one line:

if (/\bcombipack\b/i.test(text)) return []

This was a product decision, not a technical one. We could have built a multi-drug matcher. We chose not to, because the cost of getting it wrong is the same as the cost of getting a single-drug matcher wrong, and the upside is a tiny fraction of users.

Block 5: Every salt's dose, within tolerance

Once all four prior blocks have passed, we check the dose of every salt, not just one. The tolerance is tighter for combos (±5%) than for single drugs (±10%) because combo products often come in fixed-dose ratios that should not be approximated:

const isCombo = qSalts.length > 1
const tol = isCombo ? 0.05 : 0.10

for (const qs of qSalts) {
  const ps = pSalts.find(p => saltNameMatch(qs.name, p.name))
  if (!ps?.dose) continue
  const ratio = ps.dose / qs.dose
  if (ratio < (1 - tol) || ratio > (1 + tol)) hasMismatch = true
}

Mismatched-dose candidates are not deleted — they are demoted into a separate field on the result object (doseMismatchAlt). The UI shows them as a clearly-labeled weaker recommendation, with the original exact-dose match always taking precedence. This is how we preserve usefulness (a patient on a 480mg dose benefits from seeing that 500mg exists, even if it's not an exact match) without ever quietly demoting a "close enough" result into the primary slot.

Soft ranking, only after the blocks pass

Once we have a list of exact-dose candidates that survived all five blocks, we rank them. Two soft rules:

  1. Prefer immediate-release. A query for plain paracetamol should not lead with an SR/ER/prolonged-release generic. We apply a +5 penalty to modified-release candidates unless the query itself mentions SR/ER/XR:
   function srPenalty(productName, queryRaw) {
     const wantsSR = /\bsr\b|\ber\b|\bxr\b|\bprolonged\b|\bsustained\b|\bextended\b/.test(q)
     if (wantsSR) return 0
     return /\bprolonged\b|\bsustained\b|\bextended\b|\bmodified\b/.test(productName.toLowerCase()) ? 1 : 0
   }

  1. Then cheapest per-tablet price. Same release profile → cheaper wins.

The full sort is exact.sort((a, b) => a.srPenalty - b.srPenalty || a.mrp - b.mrp). We never sort by a learned score. We never sort by a "match quality" that doesn't correspond to a hard block. The ranking is two keys, both of which a pharmacist would agree with on sight.

What we surface to the user

Every recommendation card in the UI carries one of three explicit source labels:

  • CDSCO Verified — the salt is in the government drug registry.
  • BPPI Jan Aushadhi — the price and availability are from the government's own generic-medicine programme.
  • AI Estimated — the recommendation is a best-guess from the language model. A visible note tells the user to verify with a pharmacist.

If the CDSCO database returns no match, we say so clearly. We do not silently fall back to a fuzzy match and call it verified. We do not show a partial result as definitive. We'd rather show an honest "not found" than give a patient false confidence about a counterfeit strip.

This was a deliberate choice early on, and it remains the right one. The cost of false confidence in medicine is measured in hospital visits, not refunds.

A note on what we don't try to do

We don't give dosage advice. We don't recommend brands. We don't tell the user what to take, when, or whether. The app tells you what a medicine is and what it treats, not how much to take. That boundary is enforced at the prompt level in geminiService.js and again at the response-shape level when we render results.

We don't store scan history. There's no user account, no record of what medicines a person has photographed. A person scanning their cancer medication or psychiatric prescription is sharing sensitive health information, and we had no reason to keep it, so we built the app to process and discard it.

We don't have a barcode scanner, because most Indian medicine strips don't have machine-readable barcodes that map to the CDSCO registry. The text on the label is more reliably useful than the barcode.

What we'd love feedback on

The matcher is open source and lives in the agada repo. The team is Aman Sachan, Siddharth Lalwani, Chetna Kalra and Syed Akbar. The app is live at agadahealth.vercel.app and free, no account, no download.

Specifically, the things we are not sure we got right:

  • The form-bucket regex is conservative by design. It probably over-buckets products into liquid and under-buckets into solid. We'd rather over-block than under-block, but if you have a counterexample we'd like to know.
  • The drug-prefix list is hard-coded. There are probably more pharmacological modifiers we missed. The list is in the constant DRUG_PREFIX near the top of dbService.js.
  • The dose tolerance (5% for combos, 10% for singles) is a guess. We have no clinical authority. If you have a citation for a tighter or looser value, please open an issue.
  • The synonym map (amoxicillin ↔ amoxycillin, frusemide ↔ furosemide, etc.) is hand-maintained. We know we are missing entries.

If you are a pharmacist, a clinician, or someone who has actually watched a patient get the wrong generic, we would genuinely value your review of the blocking rules. The code is small — the file is under 300 lines. The hard parts are the parts we deleted, and we want to make sure we deleted the right ones.


Agada is built by Team Agada (Aman Sachan, Siddharth Lalwani, Chetna Kalra, Syed Akbar) for Open Innovation 2026. The data we use — CDSCO, Jan Aushadhi, NPPA — is public domain and we don't claim ownership. Agada is not affiliated with CDSCO, BPPI, NPPA, or the Ministry of Health. It is not a substitute for medical advice.

opensource #india #healthtech #react #javascript