Files
backend_fiesta/docs/NUTRITION_API.md
2026-09-29 22:40:36 +05:30

4.5 KiB
Raw Blame History

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

"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

"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.

go run ./scratch/nutritionproof                    # three real products
go run ./scratch/nutritionproof <brand> <image_id> # 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.