# Nutrition and health score — the app contract `GET /live/api/v1/mob/products/getproductbyvariant?tenantid=&productid=&variantid=` Each product in `details[]` may now carry two extra keys. Both come from the catalogue-intelligence service (`mcp.nearle.ai.in`) — the same records behind the health score card in the console — fetched server-side, so the app needs no second host, no second failure mode, and no copy of the rules below. --- ## nutrition ```json "nutrition": { "per": "100g", "servingsize": "1 mini (11 g)", "items": [ { "name": "Energy", "value": 545, "unit": "kcal" }, { "name": "Protein", "value": 7.5, "unit": "g" } ] } ``` - **Absent when unknown.** Not `null`, not `{}`. A missing key means "we do not know", never "this food has no nutrition". - `items` is never empty when `nutrition` is present. - `per` is `"100g"` for everything the service returns today. `servingsize` is often absent — show the basis only when it is there. - `value` may be a decimal. `unit` is free text and may be absent. - Rows appear only when the service stated them. A null field is omitted; a stated zero is kept, because "no fibre" is a fact and a dash is not. ## healthscore ```json "healthscore": { "score": 65, "band": "good", "label": "Healthy", "positives": ["Good source of protein (7.5 g per 100 g)."], "cautions": ["High in saturated fat (14.4 g per 100 g)."], "diettags": ["High Fiber", "Vegetarian"], "allergens": [], "allergensunconfirmed": true, "caveat": "Matched to a reference product with 61% confidence — treat these figures as a guide.", "source": { "label": "openfoodfacts", "url": "https://..." } } ``` - **Absent when there is nothing safe to show** — unscored, not food, or no record at all. All three read as "not rated yet". - `band` is one of `excellent` | `good` | `fair` | `poor`, for styling. `label` is what a shopper reads. Use the label; do not re-derive it. - `score` is 0–100, already rounded. - `positives`, `cautions` and `diettags` are sentences the service wrote for a person. Render as given. ### Two rules the app MUST honour **`caveat`, when present, has to be on screen.** It means the underlying source match was weak — most are; the service matches down to 0.32 confidence. A nutrition table presented as fact on a 61% match is a claim the data does not support. **`allergensunconfirmed: true` means an empty `allergens` list must NOT be rendered as "contains none".** Say "not confirmed — check the packet". Silence standing in for "none" is the one failure here that can put somebody in hospital. A declared allergen is always sent and must always be shown. --- ## Why the judgement is server-side The service returns a raw number and, from this endpoint, no band. Deciding which band, whether the match is strong enough to state plainly, and whether the product is even food is a set of rules that already exists in the console. Two implementations would drift and disagree about the same product on two screens. The edibility guard is the sharpest of them. The upstream per-product endpoint is **not** gated for it: on 4 Sep 2026 it rated Godrej Hit insecticide 80/100 with `data_status: "verified"`, and soap and shampoo both scored 37.5. Those records now read "unavailable", and the guard stays — this tenant sells soap and toothpaste beside its biscuits. ## Coverage today Measured 29 Sep 2026 against tenant 1147: **6 of 15 catalogue-linked products have nutrition**, and fewer have a score. The Patanjali ghee this work started from has neither. Test with **product 7101, Balaji Wafers Simply Salted** — a full panel and a 65/100 score. ```sh go run ./scratch/nutritionproof # three real products go run ./scratch/nutritionproof # any product ``` ## Known bad data upstream Balaji Wafers reports `sodium_mg: 0.967` — under 1 mg per 100 g, for salted crisps, where 500–900 mg is normal. The `Salt` figure of 0.002 g is wrong the same way. It looks like a grams/milligrams mix-up at the source. Fiesta passes the value through as given rather than scaling it: silently "correcting" a food label is how wrong data becomes invisible. It will look wrong in the app until the agent team fixes the unit. ## Configuration `NUTRITION_BASE=https://mcp.nearle.ai.in/api`, in `.env`. Unset means neither key is ever sent and nothing else changes. Lookups are cached six hours, capped at three seconds, and every failure costs that product its panel rather than the response.