Nutritional Values

How it works

Four steps run between pasting a recipe and seeing a number. Each one can be wrong in a way you can see and correct, which is the point — nothing here is a black box.

1. Reading the recipe

Two parsers run, in that order.

A rule-based parser runs first, in your browser. It splits the text into lines and tests each against a set of rules — no network, no cost, under a millisecond. It handles fractions (1 1/2, ), ranges (2-3 cloves, taken as the midpoint), package sizes (1 (400g) tin), size words (1 large onion) and bullet characters, and skips anything that reads like a method step. Your ingredient list appears immediately from this pass.

Most of what it does is throw things away. A recipe pasted off a blog brings the whole card with it — a star rating, the author, four kinds of time, a course, a cuisine, a row of Print/Pin buttons and a nutrition panel that is itself a list of numbers. Every one of those parses as plausibly as an ingredient does, so they are matched and dropped by name. Tick boxes and bullets are stripped as a run, because “* ▢1 lb. pasta” has two of them and removing only the first leaves the quantity glued to a symbol.

If the text has no title, the ingredients usually say what the dish is: pasta with a cheese sauce is macaroni cheese, and eggs with cream and sugar and no flour is a custard. A name is suggested rather than assumed — it is labelled as a guess, and one click opens an editor with alternatives to pick from.

Then a language model reads the same text, and usually replaces that first guess. It handles the lines rules miss — unusual phrasings, ingredients written mid-sentence, sub-recipes — but its real job is the next step’s problem: translating what you wrote into the vocabulary the nutrition database uses.

The model runs on the server, once per distinct recipe, and the result is cached by a hash of the text. Analysing a popular recipe costs nothing after the first person does it. If the model is unavailable for any reason, the rule-based parse is what you keep — the site never breaks, it just matches slightly less well.

2. Matching each ingredient to a food

USDA descriptions are not written the way recipes are. The database calls plain flour “Flour, wheat, all-purpose, enriched, bleached” and an onion “Onions, raw”. Searching it with the words off a recipe card misses constantly.

So each ingredient gets a second, machine-readable name in that idiom:

You wrote Searched as
2 cups plain flour, sifted flour, wheat, all-purpose
2 x 400g tins chopped tomatoes tomatoes, canned
1 large free-range egg egg, whole, raw
500g lean beef mince beef, ground, lean, raw
a splash of good olive oil oil, olive

The rule is that qualifiers which change the nutrition are kept — canned, frozen, dried, raw, whole wheat, lean, skinless — and qualifiers which don’t are dropped: brand names, “organic”, “free-range”, and preparation words like chopped or grated. The list you see keeps your wording; only the search uses the translated form.

Anything that still can’t be matched is flagged Not matched in amber rather than silently guessed at, and you can pick the right food from a list — or photograph the packet. Scanning a label is offered right on the row, because that is the moment it earns its keep: a generic database entry is an average, and for anything branded the packet in your hand can be a long way off it. Sodium is the worst offender — two brands of the same product can be threefold apart. A scanned label becomes that ingredient’s numbers outright, with no name lookup in between.

3. Converting to grams

USDA nutrient data is per 100 grams, so everything has to become a weight. Three different routes get there:

  • Weights convert exactly. 8 oz is always 226.8 g.
  • Volumes convert via the food’s own density. This matters more than people expect: a cup of flour is 125 g, a cup of sugar is 200 g, a cup of honey is 339 g. Using a single density for everything would be wrong by a factor of nearly three at the extremes.
  • Counts convert via a typical item weight — one clove of garlic is 3 g, one medium egg is 50 g, one medium onion is 110 g.

Where none of those routes works — an unrecognised unit, or a food with no per-item weight on record — the weight is estimated from the kind of food it is. That matters more than it sounds. An earlier version assumed 100 g per item for everything, which turned “2 heaping tsp. dry mustard” into 200 grams of mustard: a plausible-looking number twenty times too large, sitting next to a pound of cheese that was right to the gram. A spice arriving as a bare count is almost always a spoon whose unit failed to parse, so the assumption is spoon-sized. Being too small is the safer error.

Any row whose weight was estimated rather than converted is tagged est. weight, so you know which numbers to trust least.

Volume measures are US customary, which is what US recipes and FoodData Central both assume. A US cup is 236.6 ml and a US tablespoon is 14.8 ml — if your recipe is Australian, its tablespoon is 20 ml and you should say 20 ml rather than 1 tbsp.

4. Totalling and dividing

Every ingredient’s per-100 g nutrient vector is scaled by its weight and summed. The total is then divided by the serving count you set.

The macro split percentages are calculated from the macro grams using Atwater factors (4 kcal/g protein, 4 kcal/g carbohydrate, 9 kcal/g fat) so the three slices always add to 100%. This can differ by a few percent from the stated calorie figure, because USDA calories come from measured food energy rather than from multiplying the macros out. That gap is real, not a rounding bug.

All of this is ordinary arithmetic, deliberately. The model reads text; it never computes a nutrition figure. Every number on the page comes from multiplying a database value by a weight, which means it is reproducible, testable, and cannot be hallucinated.

5. Working out what would improve it

The results page offers a goal — cut saturated fat, cut added sugar, cut salt, raise protein, raise fibre — only when the recipe actually has that problem. A wall of buttons that are irrelevant to what you pasted teaches people to ignore all of them.

Within a goal, the suggestions are ranked by which ingredient is responsible. “Reduce fat” is useless advice; “the 250 g of butter is 78% of the saturated fat, and oil in its place would cut 14 g per serving” is something you can act on. So each option names the row it changes and is sorted by how much that row contributes.

The number attached to every option is the computed result of making that change — the whole recipe re-totalled with the substitution in place, using the same code as the main figures. Nothing is estimated, and the prediction cannot disagree with what you see afterwards. Options that would move the number by less than 5% of where you already are, or by less than a fixed floor, are not shown at all: a change smaller than the error in the estimate is filler, not advice.

Where the honest answer is not a substitution, it says so. Cutting a quarter of the sugar in a bake usually beats every sweetener swap available, and the panel offers that as its own option rather than pretending a swap is always the answer. None of this involves a model call, so it works when Claude does not.

Which suggestions appear at all depends on the dish. A substitution table knows that parmesan and cream cheese are both cheese; it does not know that the 30 g of parmesan in a bolognese is seasoning four portions of sauce, and that a soft fresh cheese cannot do that job. It does not know that the cream in a crème brûlée has to set with the egg, and that Greek yogurt will curdle instead. Both are perfectly good swaps somewhere else, which is exactly why the table offers them.

So the dish is guessed — from the title first, then from the composition — along with what each ingredient is doing in it: holding the thing together, bulking it out, enriching it, or seasoning it. Suggestions the pair rules out are removed and the reason is shown, because “a soft fresh cheese would disappear into this” is more useful than a shorter list. An unrecognised dish blocks nothing: a wrong block costs a good idea, which is worse than an extra mediocre one.

Hovering any substitution shows what it does to the dish rather than to the numbers. That advice is a function of both foods, not just the one you are switching to: sugar → honey means giving up creaming, holding back other liquid and dropping the oven, while maple syrup → honey means almost nothing at all. Half of it is derived from the two nutrient vectors — a moisture difference is arithmetic — and half from a small table of things no nutrient vector records, like whether a fat is solid at room temperature or a flour has gluten.

6. Rescaling

The Recipe Rescaler shares the parser and the food table and asks a different question. Any line can be the pivot, because the constraint in a real kitchen is usually an ingredient rather than a serving count, and the factor is derived in grams so the old and new amounts need not even share a unit.

Converting between cups and grams is per-ingredient by necessity: a cup of flour is 125 g and a cup of sugar is 200 g, so there is no single factor to apply. The per-cup weight of each matched food comes from the same USDA data as everything else, and an ingredient that could not be matched simply keeps the unit the recipe gave it rather than being converted through a guess.

Every amount is then rounded to something the kitchen can actually produce. There is no 3/8 cup measure, so cup fractions snap to the ladder that exists — eighth, quarter, third, half, two thirds, three quarters. Metric never grows a fraction, because a fraction of a gram is a rounding error with a slash in it. And small spoon measures survive every conversion untouched, because metric recipes say “1 tsp vanilla” too, and nobody can pour 3 ml of anything.

Where the data comes from

Nutrient values come from USDA FoodData Central, the US government’s food composition database — the same source most nutrition labelling is built on.

Rather than call it on every lookup, this site keeps its own mirror. Each food is fetched once and stored; after that, lookups are answered locally in a millisecond with no rate limit. Stored records are revalidated against USDA every 90 days, and a stale record is refreshed behind your response rather than in front of it, so you never wait for it. You can see the state of the mirror — how many foods, how many are stale, what share of lookups avoided a USDA call — at /api/stats.

The site also ships a built-in table of about 140 common ingredients so it works with no configuration at all. Those are rounded reference figures derived from USDA Standard Reference — fine for estimates, labelled as such, and superseded by live data whenever the database is available.

Configuring it yourself

Three secrets, all optional and all independently degradable:

Secret Without it
FDC_API_KEY Falls back to the built-in table of ~140 ingredients
ANTHROPIC_API_KEY Falls back to the rule-based parser
ADMIN_KEY Only the manual refresh sweep is disabled
  1. Get a free FoodData Central key at fdc.nal.usda.gov/api-key-signup.html — instant, email only.
  2. Get an Anthropic API key at console.anthropic.com.
  3. Set them on the deployment: npx wrangler pages secret put FDC_API_KEY, and the same for the others.

Keys live on the server and are never sent to a browser.

What it costs to run

Parsing uses Claude Haiku 4.5 at $1 per million input tokens and $5 per million output. A typical recipe is about 1,200 input and 700 output tokens — roughly half a cent — and because results are cached by recipe hash, the amortised cost falls toward zero as the same recipes recur. USDA lookups are free and, after the mirror warms, mostly don’t happen at all.