Vibecode Cal AI
track this build4 phases, 12 steps, beginner friendly0%The core trick, send a food photo to a multimodal model and ask for calories and macros as JSON, is a one-evening build and works surprisingly well. Where it stops being easy is everything around it: a native app that opens fast, a camera flow you actually use three times a day, barcode lookups against a real food database, HealthKit or Google Fit sync, and streaks that keep you logging past day four. Accuracy is also less about your prompt and more about calibration, portion-size guessing is where these apps live or die and you have no correction data. A local PWA is a genuinely useful personal replacement if you are the kind of person who will tolerate a browser bookmark instead of an app icon. You are also renting the vision model, so this is not fully self-contained.
You are building a lean indie version of Cal AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Cal AI · indie build A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A small web application installed to the phone home screen, served on your own network | the camera flow has to be one tap and a native app is not worth building for one user | | Storage | SQLite beside the app, with photographs on disk, nothing in anyone else's cloud | a picture of everything you ate is a diary, and it should live where your diary lives | | Estimation | One call per photograph to a multimodal model, with structured output and validation | the model is rented and is the only part that leaves the machine | | Presentation | Ranges everywhere, never a single number, with the model and date attached | the published error is tens of percent and the interface has to say so | | Correction | An edit step that cannot be skipped, storing what you changed | supplying portion and ingredient context is the one thing shown to halve the error | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application on the machine that serves your phone. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a multimodal model that accepts images** · pay per image; a few meals a day is small change, and the ceiling in this build is what stops it becoming otherwise - Why: The estimate is one call per photograph, and this is the only part of the build that leaves your machine or costs money. - Get it: Create a key with the provider you prefer and set a spend limit in their console. - Verify: One photograph returns valid structured output - [ ] **A phone on the same network, with the app installed to the home screen** · free - Why: The whole design stands or falls on the capture being two taps, and that is only testable on the device you will actually use at dinner. - Get it: Open the app on the phone and add it to the home screen. - Verify: Two taps from the icon produce a stored photograph - [ ] **A certificate for the local address** · free - Why: The camera will not open on an insecure origin from another device, which is the single most common reason this kind of build stalls on day one. - Get it: A local certificate authority for your network, or a hostname with a real certificate pointed at the local address. - Verify: The phone opens the app without a certificate warning and the camera works - [ ] **What the interface is allowed to display** · free - Why: The literature is clear that the error is large and directional, and deciding presentation rules afterwards means deciding them from whatever looked nicest. - Get it: Write down that ranges are mandatory, that the middle value is never shown alone, and what the minimum correction count for calibration is. - Verify: All three rules appear in the README and are asserted in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. - A native app with widgets, notifications and instant cold start - Barcode scanning against a maintained packaged-food database - HealthKit / Google Fit / Apple Watch sync - Streaks, coaching copy and the habit scaffolding that makes tracking stick - Whatever portion-size calibration they have learned from millions of corrected logs If one of those is essential to you, that is the reason to keep paying for Cal AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cal AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private food log that estimates a meal from a photograph and shows the estimate as a range you are expected to correct to replace Cal AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This app makes a measurement claim, and the published evidence says the measurement is poor. That decides the whole design. Estimating energy from one photograph is not accurate. A 2025 study in Current Developments in Nutrition put standardised meal photographs through frontier multimodal models and measured a mean absolute percentage error for energy of about 36 percent for the two best and about 64 percent for the third, with the same order of error on weight and worse on fat and protein. The bias is not random: underestimation grew with portion size, and a separate 2025 study in Nutrients found meal weight underestimated in 76 percent of photographs, with agreement acceptable for small meals and breaking down for medium and large ones. Hidden oil, sauce and filling are the main reason. So: never display a bare number as if it were measured. Every estimate is shown as a range with the model named and the date. The correction step is not optional and cannot be skipped, because the same literature shows that supplying ingredient and portion context roughly halves the error, in one study from about 31 percent to about 14 percent. Do not add insulin dosing, medical advice or anything that treats these numbers as clinical: a 2026 study found carbohydrate estimates within ten grams for only 47 percent of composite meals. Regulators treat a calorie counter as not a medical device, which means nobody is checking your numbers except the person relying on them. And do not build streak pressure or shaming copy into a food tracking app pointed at a phone camera. The daily numbers can be hidden, the targets are optional, and there are no notifications. ### Stack (fixed, do not substitute) - Runtime: A small web application installed to the phone home screen, served on your own network · the camera flow has to be one tap and a native app is not worth building for one user - Storage: SQLite beside the app, with photographs on disk, nothing in anyone else's cloud · a picture of everything you ate is a diary, and it should live where your diary lives - Estimation: One call per photograph to a multimodal model, with structured output and validation · the model is rented and is the only part that leaves the machine - Presentation: Ranges everywhere, never a single number, with the model and date attached · the published error is tens of percent and the interface has to say so - Correction: An edit step that cannot be skipped, storing what you changed · supplying portion and ingredient context is the one thing shown to halve the error ### Data model (create this before Phase 1) - meals(id, at, photo_path, note, model_id, prompt_version, raw_response_json, api_cost_minor, state) - items(id, meal_id, name, grams_low, grams_mid, grams_high, kcal_low, kcal_mid, kcal_high, protein_g, carbs_g, fat_g, confidence, source) · source is model, edited or manual - corrections(id, item_id, field, model_value, user_value, at) · the record of every time the estimate was wrong, which is the only calibration data that exists - calibration(period_start, period_end, n_items, median_ratio, iqr_low, iqr_high) · computed from corrections, shown with its sample size or not shown - targets(id, kind, value, set_at, hidden) · optional, and hideable, because a target is not a requirement - settings(photo_retention_days, monthly_cost_ceiling_minor, numbers_hidden, model_id) - Every displayed figure carries a low and a high. There is no column anywhere in this schema holding a single authoritative calorie value. ### Phase 1 · A camera that opens, and a meal that stays yours Build: A home screen listing today's meals, and one large capture control using the phone camera directly. Resize on the device before upload. Store the photograph on disk and the row in SQLite. Serve over your own network with a certificate, since the camera needs a secure origin. Trap: A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. Done when: A meal is photographed and stored in two taps from the home screen, timed on a phone. Images are resized on the device before upload and the stored file is under a stated size, checked. The only outbound request in the whole flow is the model call, verified by watching the network. The application refuses to start over plain http on anything but the local machine, with a message explaining why the camera needs a secure origin. Do not build yet: No estimation, editing, history or export yet. ### Phase 2 · An estimate that arrives as a range Build: Ask for structured output: one entry per distinct food with a name, a low, middle and high gram estimate, macros for each, and a confidence. Validate against a schema. On a parse failure retry once, then show the manual entry form. Store the raw response and the prompt version with the meal so an estimate can be explained months later. Trap: Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. Done when: Every stored item has low, middle and high values and the interface has no path that displays the middle alone, checked by review. A malformed model response never produces a stored estimate, tested with a fake returning prose. The model identifier, prompt version and raw response are stored with every meal. A meal with no successful estimate is still loggable by hand and appears in the totals. Do not build yet: No correction step, calibration, history or export yet. ### Phase 3 · The correction step is the product Build: After an estimate, show every detected item with editable grams and an ingredient note, and rescale that item's macros as the grams change. Nothing is saved until the screen is confirmed. Prompt specifically for the things photographs hide, which the literature identifies as cooking oil, sauce and filling. Record every change as a correction row. Trap: Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. Done when: No meal reaches the log without passing through the confirmation screen, asserted in the suite. Changing an item's grams rescales its macros proportionally and the change is stored as a correction row with both values. The confirmation screen asks about added oil, sauce or filling for every meal. An item can be deleted or added by hand on the confirmation screen and the totals follow. Do not build yet: No calibration, history or export yet. ### Phase 4 · Calibration from your own corrections Build: Compute the ratio of corrected value to model value per item over a rolling window, and report the median with its spread and the number of corrections behind it. Show it as information, not as a silent multiplier. Below a stated number of corrections, show nothing at all. Trap: Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. Done when: Calibration is computed from correction rows only and is absent until a configured minimum of corrections exists, tested at the boundary. The figure is shown with its spread and its count wherever it appears. Applying calibration changes the displayed range and never rewrites a stored item, verified in the database after toggling it. Turning calibration off restores the original ranges exactly. Do not build yet: No history, export or retention work yet. ### Phase 5 · Weeks, not days, and a way out Build: A history view whose headline is a rolling seven-day average with a band, with single days available but never the front page. Export everything as CSV including corrections. Delete photographs after a configured retention while keeping the rows. Track the monthly model spend and stop estimating when it reaches the ceiling, falling back to manual entry. Trap: A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. Done when: The history front page shows a rolling average with a band and no single-day figure without its range, checked by review. Photographs older than the retention period are deleted and their rows survive with the estimate intact, tested. Export produces every meal, item and correction as CSV and reimports into an empty database, tested. Reaching the monthly cost ceiling disables estimation with a clear message and leaves manual entry working. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything clinical, including carbohydrate counting for insulin dosing, since a 2026 study found photograph-based carbohydrate estimates within ten grams for under half of composite meals. - Barcode scanning against a packaged food database, which is a licensing question and a maintained dataset rather than a weekend of code. - Health platform synchronisation, which needs a native application and a review process for one number a day. - Streaks, notifications and coaching copy, which are the habit machinery the subscription sells and are the part most likely to do harm pointed at a camera. ### README must contain - The published error figures for photograph-based energy estimation, with the studies and the dates, stated plainly before any instructions - That the estimate is biased downwards and that the bias grows with portion size and with hidden oil, sauce and filling - That the correction step exists because supplying portion and ingredient context roughly halves the error in published comparisons - That this is not a medical tool and must not be used for insulin dosing or any clinical decision - Where the photographs live, how long they are kept, and how to delete everything ===== AGENTS.md ===== # Agent instructions · Cal AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A small web application installed to the phone home screen, served on your own network, SQLite beside the app, with photographs on disk, nothing in anyone else's cloud, One call per photograph to a multimodal model, with structured output and validation, Ranges everywhere, never a single number, with the model and date attached, An edit step that cannot be skipped, storing what you changed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ===== BUILD_PLAN.md ===== # Build plan · Cal AI A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A camera that opens, and a meal that stays yours Photographing a meal takes one tap and nothing leaves the machine except the model call. ### Steps 1. Make capture two taps and prove it This number decides whether the app survives the first week. Files: `src/app/page.tsx`, `src/components/Capture.tsx` 2. Resize on the device before upload A phone photograph is several megabytes of detail the model does not use. Files: `src/components/Capture.tsx` 3. Serve over a secure origin on your network The camera will not open otherwise, and this trips everyone once. Files: `docs/lan-setup.md` ### Done when - [ ] Two taps from home screen to stored photograph - [ ] Images are resized on the device under a stated size - [ ] The model call is the only outbound request - [ ] An insecure origin is refused with an explanation ### Watch out - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. ## Phase 2 · An estimate that arrives as a range The model returns items with low, middle and high values, validated, and never invented on failure. ### Steps 1. Ask for a range per item, not a number The range is the honest output of this measurement. Files: `src/lib/estimate.ts`, `src/lib/schema.ts` 2. Validate, retry once, then hand over to manual entry Never fabricate a number when the response did not parse. Files: `src/lib/estimate.ts`, `tests/estimate.test.ts` 3. Store the raw response and the prompt version So a strange estimate can be explained rather than argued about. Files: `src/lib/meals.ts` ### Done when - [ ] Every item carries low, middle and high - [ ] No interface path shows a single number alone - [ ] A malformed response stores nothing - [ ] Raw response and prompt version are retained ### Watch out - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. ## Phase 3 · The correction step is the product Confirming a meal means editing what the model got wrong, and the edit is recorded. ### Steps 1. Put the edit between the photograph and the log An edit step people can skip is an edit step people skip. Files: `src/app/confirm/page.tsx` 2. Ask about oil, sauce and filling by name That is where the energy a photograph cannot see is hiding. Files: `src/app/confirm/page.tsx` 3. Record every correction with both values It is the only data you will ever have about your own errors. Files: `src/lib/corrections.ts`, `tests/corrections.test.ts` ### Done when - [ ] No meal is logged without confirmation - [ ] Editing grams rescales macros and stores a correction - [ ] Every meal is asked about oil, sauce and filling - [ ] Items can be added or removed by hand ### Watch out - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. ## Phase 4 · Calibration from your own corrections The app learns how wrong it usually is for you, and says so with the sample size. ### Steps 1. Compute from corrections over a rolling window Old corrections describe an older diet and an older model. Files: `src/lib/calibration.ts`, `tests/calibration.test.ts` 2. Hide it entirely below a minimum count A ratio from eight corrections is a rumour. Files: `src/lib/calibration.ts` 3. Never write a calibrated value into storage Stored numbers are what the model said and what you said. Files: `src/lib/meals.ts` ### Done when - [ ] No calibration appears below the minimum count - [ ] The figure always carries its spread and count - [ ] Applying calibration never alters stored rows - [ ] Turning it off restores the original ranges ### Watch out - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ## Not in this build - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. ## After v1, if you want it - A reference object in the photograph, which the literature suggests helps less than supplying ingredients does - Recipes you cook often, stored once and logged by name, which removes the model from the meals you eat most - A monthly report of how far the model was from your corrections, which is the only accuracy number that means anything here ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. MODEL_API_KEY=sk-...redacted # Required. Stored with every meal, because a different model gives different numbers. MODEL_ID=a multimodal model identifier # Required. Meals, items, corrections and calibration. Back this up; the photographs matter less. DATABASE_PATH=./data/food.db # Required. Meal photographs. Deleted after the retention period, rows retained. PHOTO_DIR=./data/photos # Required. How long pictures of your meals are kept. Shorter is a reasonable default. PHOTO_RETENTION_DAYS=30 # Required. Estimation stops at this figure for the month and manual entry continues. MONTHLY_COST_CEILING=5.00 # Required. Below this, no calibration figure is shown at all. CALIBRATION_MIN_CORRECTIONS=40
You are building a lean indie version of Cal AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Cal AI · indie build A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A small web application installed to the phone home screen, served on your own network | the camera flow has to be one tap and a native app is not worth building for one user | | Storage | SQLite beside the app, with photographs on disk, nothing in anyone else's cloud | a picture of everything you ate is a diary, and it should live where your diary lives | | Estimation | One call per photograph to a multimodal model, with structured output and validation | the model is rented and is the only part that leaves the machine | | Presentation | Ranges everywhere, never a single number, with the model and date attached | the published error is tens of percent and the interface has to say so | | Correction | An edit step that cannot be skipped, storing what you changed | supplying portion and ingredient context is the one thing shown to halve the error | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application on the machine that serves your phone. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a multimodal model that accepts images** · pay per image; a few meals a day is small change, and the ceiling in this build is what stops it becoming otherwise - Why: The estimate is one call per photograph, and this is the only part of the build that leaves your machine or costs money. - Get it: Create a key with the provider you prefer and set a spend limit in their console. - Verify: One photograph returns valid structured output - [ ] **A phone on the same network, with the app installed to the home screen** · free - Why: The whole design stands or falls on the capture being two taps, and that is only testable on the device you will actually use at dinner. - Get it: Open the app on the phone and add it to the home screen. - Verify: Two taps from the icon produce a stored photograph - [ ] **A certificate for the local address** · free - Why: The camera will not open on an insecure origin from another device, which is the single most common reason this kind of build stalls on day one. - Get it: A local certificate authority for your network, or a hostname with a real certificate pointed at the local address. - Verify: The phone opens the app without a certificate warning and the camera works - [ ] **What the interface is allowed to display** · free - Why: The literature is clear that the error is large and directional, and deciding presentation rules afterwards means deciding them from whatever looked nicest. - Get it: Write down that ranges are mandatory, that the middle value is never shown alone, and what the minimum correction count for calibration is. - Verify: All three rules appear in the README and are asserted in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. - A native app with widgets, notifications and instant cold start - Barcode scanning against a maintained packaged-food database - HealthKit / Google Fit / Apple Watch sync - Streaks, coaching copy and the habit scaffolding that makes tracking stick - Whatever portion-size calibration they have learned from millions of corrected logs If one of those is essential to you, that is the reason to keep paying for Cal AI, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Cal AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private food log that estimates a meal from a photograph and shows the estimate as a range you are expected to correct to replace Cal AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This app makes a measurement claim, and the published evidence says the measurement is poor. That decides the whole design. Estimating energy from one photograph is not accurate. A 2025 study in Current Developments in Nutrition put standardised meal photographs through frontier multimodal models and measured a mean absolute percentage error for energy of about 36 percent for the two best and about 64 percent for the third, with the same order of error on weight and worse on fat and protein. The bias is not random: underestimation grew with portion size, and a separate 2025 study in Nutrients found meal weight underestimated in 76 percent of photographs, with agreement acceptable for small meals and breaking down for medium and large ones. Hidden oil, sauce and filling are the main reason. So: never display a bare number as if it were measured. Every estimate is shown as a range with the model named and the date. The correction step is not optional and cannot be skipped, because the same literature shows that supplying ingredient and portion context roughly halves the error, in one study from about 31 percent to about 14 percent. Do not add insulin dosing, medical advice or anything that treats these numbers as clinical: a 2026 study found carbohydrate estimates within ten grams for only 47 percent of composite meals. Regulators treat a calorie counter as not a medical device, which means nobody is checking your numbers except the person relying on them. And do not build streak pressure or shaming copy into a food tracking app pointed at a phone camera. The daily numbers can be hidden, the targets are optional, and there are no notifications. ### Stack (fixed, do not substitute) - Runtime: A small web application installed to the phone home screen, served on your own network · the camera flow has to be one tap and a native app is not worth building for one user - Storage: SQLite beside the app, with photographs on disk, nothing in anyone else's cloud · a picture of everything you ate is a diary, and it should live where your diary lives - Estimation: One call per photograph to a multimodal model, with structured output and validation · the model is rented and is the only part that leaves the machine - Presentation: Ranges everywhere, never a single number, with the model and date attached · the published error is tens of percent and the interface has to say so - Correction: An edit step that cannot be skipped, storing what you changed · supplying portion and ingredient context is the one thing shown to halve the error ### Data model (create this before Phase 1) - meals(id, at, photo_path, note, model_id, prompt_version, raw_response_json, api_cost_minor, state) - items(id, meal_id, name, grams_low, grams_mid, grams_high, kcal_low, kcal_mid, kcal_high, protein_g, carbs_g, fat_g, confidence, source) · source is model, edited or manual - corrections(id, item_id, field, model_value, user_value, at) · the record of every time the estimate was wrong, which is the only calibration data that exists - calibration(period_start, period_end, n_items, median_ratio, iqr_low, iqr_high) · computed from corrections, shown with its sample size or not shown - targets(id, kind, value, set_at, hidden) · optional, and hideable, because a target is not a requirement - settings(photo_retention_days, monthly_cost_ceiling_minor, numbers_hidden, model_id) - Every displayed figure carries a low and a high. There is no column anywhere in this schema holding a single authoritative calorie value. ### Phase 1 · A camera that opens, and a meal that stays yours Build: A home screen listing today's meals, and one large capture control using the phone camera directly. Resize on the device before upload. Store the photograph on disk and the row in SQLite. Serve over your own network with a certificate, since the camera needs a secure origin. Trap: A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. Done when: A meal is photographed and stored in two taps from the home screen, timed on a phone. Images are resized on the device before upload and the stored file is under a stated size, checked. The only outbound request in the whole flow is the model call, verified by watching the network. The application refuses to start over plain http on anything but the local machine, with a message explaining why the camera needs a secure origin. Do not build yet: No estimation, editing, history or export yet. ### Phase 2 · An estimate that arrives as a range Build: Ask for structured output: one entry per distinct food with a name, a low, middle and high gram estimate, macros for each, and a confidence. Validate against a schema. On a parse failure retry once, then show the manual entry form. Store the raw response and the prompt version with the meal so an estimate can be explained months later. Trap: Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. Done when: Every stored item has low, middle and high values and the interface has no path that displays the middle alone, checked by review. A malformed model response never produces a stored estimate, tested with a fake returning prose. The model identifier, prompt version and raw response are stored with every meal. A meal with no successful estimate is still loggable by hand and appears in the totals. Do not build yet: No correction step, calibration, history or export yet. ### Phase 3 · The correction step is the product Build: After an estimate, show every detected item with editable grams and an ingredient note, and rescale that item's macros as the grams change. Nothing is saved until the screen is confirmed. Prompt specifically for the things photographs hide, which the literature identifies as cooking oil, sauce and filling. Record every change as a correction row. Trap: Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. Done when: No meal reaches the log without passing through the confirmation screen, asserted in the suite. Changing an item's grams rescales its macros proportionally and the change is stored as a correction row with both values. The confirmation screen asks about added oil, sauce or filling for every meal. An item can be deleted or added by hand on the confirmation screen and the totals follow. Do not build yet: No calibration, history or export yet. ### Phase 4 · Calibration from your own corrections Build: Compute the ratio of corrected value to model value per item over a rolling window, and report the median with its spread and the number of corrections behind it. Show it as information, not as a silent multiplier. Below a stated number of corrections, show nothing at all. Trap: Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. Done when: Calibration is computed from correction rows only and is absent until a configured minimum of corrections exists, tested at the boundary. The figure is shown with its spread and its count wherever it appears. Applying calibration changes the displayed range and never rewrites a stored item, verified in the database after toggling it. Turning calibration off restores the original ranges exactly. Do not build yet: No history, export or retention work yet. ### Phase 5 · Weeks, not days, and a way out Build: A history view whose headline is a rolling seven-day average with a band, with single days available but never the front page. Export everything as CSV including corrections. Delete photographs after a configured retention while keeping the rows. Track the monthly model spend and stop estimating when it reaches the ceiling, falling back to manual entry. Trap: A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. Done when: The history front page shows a rolling average with a band and no single-day figure without its range, checked by review. Photographs older than the retention period are deleted and their rows survive with the estimate intact, tested. Export produces every meal, item and correction as CSV and reimports into an empty database, tested. Reaching the monthly cost ceiling disables estimation with a clear message and leaves manual entry working. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything clinical, including carbohydrate counting for insulin dosing, since a 2026 study found photograph-based carbohydrate estimates within ten grams for under half of composite meals. - Barcode scanning against a packaged food database, which is a licensing question and a maintained dataset rather than a weekend of code. - Health platform synchronisation, which needs a native application and a review process for one number a day. - Streaks, notifications and coaching copy, which are the habit machinery the subscription sells and are the part most likely to do harm pointed at a camera. ### README must contain - The published error figures for photograph-based energy estimation, with the studies and the dates, stated plainly before any instructions - That the estimate is biased downwards and that the bias grows with portion size and with hidden oil, sauce and filling - That the correction step exists because supplying portion and ingredient context roughly halves the error in published comparisons - That this is not a medical tool and must not be used for insulin dosing or any clinical decision - Where the photographs live, how long they are kept, and how to delete everything ===== AGENTS.md ===== # Agent instructions · Cal AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A small web application installed to the phone home screen, served on your own network, SQLite beside the app, with photographs on disk, nothing in anyone else's cloud, One call per photograph to a multimodal model, with structured output and validation, Ranges everywhere, never a single number, with the model and date attached, An edit step that cannot be skipped, storing what you changed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ===== BUILD_PLAN.md ===== # Build plan · Cal AI A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A camera that opens, and a meal that stays yours Photographing a meal takes one tap and nothing leaves the machine except the model call. ### Steps 1. Make capture two taps and prove it This number decides whether the app survives the first week. Files: `src/app/page.tsx`, `src/components/Capture.tsx` 2. Resize on the device before upload A phone photograph is several megabytes of detail the model does not use. Files: `src/components/Capture.tsx` 3. Serve over a secure origin on your network The camera will not open otherwise, and this trips everyone once. Files: `docs/lan-setup.md` ### Done when - [ ] Two taps from home screen to stored photograph - [ ] Images are resized on the device under a stated size - [ ] The model call is the only outbound request - [ ] An insecure origin is refused with an explanation ### Watch out - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. ## Phase 2 · An estimate that arrives as a range The model returns items with low, middle and high values, validated, and never invented on failure. ### Steps 1. Ask for a range per item, not a number The range is the honest output of this measurement. Files: `src/lib/estimate.ts`, `src/lib/schema.ts` 2. Validate, retry once, then hand over to manual entry Never fabricate a number when the response did not parse. Files: `src/lib/estimate.ts`, `tests/estimate.test.ts` 3. Store the raw response and the prompt version So a strange estimate can be explained rather than argued about. Files: `src/lib/meals.ts` ### Done when - [ ] Every item carries low, middle and high - [ ] No interface path shows a single number alone - [ ] A malformed response stores nothing - [ ] Raw response and prompt version are retained ### Watch out - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. ## Phase 3 · The correction step is the product Confirming a meal means editing what the model got wrong, and the edit is recorded. ### Steps 1. Put the edit between the photograph and the log An edit step people can skip is an edit step people skip. Files: `src/app/confirm/page.tsx` 2. Ask about oil, sauce and filling by name That is where the energy a photograph cannot see is hiding. Files: `src/app/confirm/page.tsx` 3. Record every correction with both values It is the only data you will ever have about your own errors. Files: `src/lib/corrections.ts`, `tests/corrections.test.ts` ### Done when - [ ] No meal is logged without confirmation - [ ] Editing grams rescales macros and stores a correction - [ ] Every meal is asked about oil, sauce and filling - [ ] Items can be added or removed by hand ### Watch out - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. ## Phase 4 · Calibration from your own corrections The app learns how wrong it usually is for you, and says so with the sample size. ### Steps 1. Compute from corrections over a rolling window Old corrections describe an older diet and an older model. Files: `src/lib/calibration.ts`, `tests/calibration.test.ts` 2. Hide it entirely below a minimum count A ratio from eight corrections is a rumour. Files: `src/lib/calibration.ts` 3. Never write a calibrated value into storage Stored numbers are what the model said and what you said. Files: `src/lib/meals.ts` ### Done when - [ ] No calibration appears below the minimum count - [ ] The figure always carries its spread and count - [ ] Applying calibration never alters stored rows - [ ] Turning it off restores the original ranges ### Watch out - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ## Not in this build - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. ## After v1, if you want it - A reference object in the photograph, which the literature suggests helps less than supplying ingredients does - Recipes you cook often, stored once and logged by name, which removes the model from the meals you eat most - A monthly report of how far the model was from your corrections, which is the only accuracy number that means anything here ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. MODEL_API_KEY=sk-...redacted # Required. Stored with every meal, because a different model gives different numbers. MODEL_ID=a multimodal model identifier # Required. Meals, items, corrections and calibration. Back this up; the photographs matter less. DATABASE_PATH=./data/food.db # Required. Meal photographs. Deleted after the retention period, rows retained. PHOTO_DIR=./data/photos # Required. How long pictures of your meals are kept. Shorter is a reasonable default. PHOTO_RETENTION_DAYS=30 # Required. Estimation stops at this figure for the month and manual entry continues. MONTHLY_COST_CEILING=5.00 # Required. Below this, no calibration figure is shown at all. CALIBRATION_MIN_CORRECTIONS=40
You are building a production product version of Cal AI. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Cal AI · product brief ## Problem The core trick, send a food photo to a multimodal model and ask for calories and macros as JSON, is a one-evening build and works surprisingly well. Where it stops being easy is everything around it: a native app that opens fast, a camera flow you actually use three times a day, barcode lookups against a real food database, HealthKit or Google Fit sync, and streaks that keep you logging past day four. Accuracy is also less about your prompt and more about calibration, portion-size guessing is where these apps live or die and you have no correction data. A local PWA is a genuinely useful personal replacement if you are the kind of person who will tolerate a browser bookmark instead of an app icon. You are also renting the vision model, so this is not fully self-contained. ## Product outcome A food log that is honest about a measurement the published evidence says is rough: every estimate is a range, the correction step is the interface rather than a dialogue, the history leads with a rolling average, and the photographs expire. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - An API key for a multimodal model that accepts images - Node 20 and a machine or cheap VPS to run it - A phone browser, added to home screen, for the camera flow - Acceptance that portion estimates will be wrong by 20 percent sometimes ## Explicit non-goals for v1 - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. - A native app with widgets, notifications and instant cold start - Barcode scanning against a maintained packaged-food database - HealthKit / Google Fit / Apple Watch sync - Streaks, coaching copy and the habit scaffolding that makes tracking stick - Whatever portion-size calibration they have learned from millions of corrected logs ## Success criteria - No interface path is confirmed to show a single calorie figure alone - A malformed model response is confirmed to store nothing - No meal is confirmed to reach the log without the correction screen - Calibration is confirmed absent below the minimum correction count - The cost ceiling is confirmed to stop estimation and leave manual entry working ===== BRIEF.md ===== # Build brief · Cal AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private food log that estimates a meal from a photograph and shows the estimate as a range you are expected to correct to replace Cal AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This app makes a measurement claim, and the published evidence says the measurement is poor. That decides the whole design. Estimating energy from one photograph is not accurate. A 2025 study in Current Developments in Nutrition put standardised meal photographs through frontier multimodal models and measured a mean absolute percentage error for energy of about 36 percent for the two best and about 64 percent for the third, with the same order of error on weight and worse on fat and protein. The bias is not random: underestimation grew with portion size, and a separate 2025 study in Nutrients found meal weight underestimated in 76 percent of photographs, with agreement acceptable for small meals and breaking down for medium and large ones. Hidden oil, sauce and filling are the main reason. So: never display a bare number as if it were measured. Every estimate is shown as a range with the model named and the date. The correction step is not optional and cannot be skipped, because the same literature shows that supplying ingredient and portion context roughly halves the error, in one study from about 31 percent to about 14 percent. Do not add insulin dosing, medical advice or anything that treats these numbers as clinical: a 2026 study found carbohydrate estimates within ten grams for only 47 percent of composite meals. Regulators treat a calorie counter as not a medical device, which means nobody is checking your numbers except the person relying on them. And do not build streak pressure or shaming copy into a food tracking app pointed at a phone camera. The daily numbers can be hidden, the targets are optional, and there are no notifications. ### Stack (fixed, do not substitute) - Runtime: A small web application installed to the phone home screen, served on your own network · the camera flow has to be one tap and a native app is not worth building for one user - Storage: SQLite beside the app, with photographs on disk, nothing in anyone else's cloud · a picture of everything you ate is a diary, and it should live where your diary lives - Estimation: One call per photograph to a multimodal model, with structured output and validation · the model is rented and is the only part that leaves the machine - Presentation: Ranges everywhere, never a single number, with the model and date attached · the published error is tens of percent and the interface has to say so - Correction: An edit step that cannot be skipped, storing what you changed · supplying portion and ingredient context is the one thing shown to halve the error ### Data model (create this before Phase 1) - meals(id, at, photo_path, note, model_id, prompt_version, raw_response_json, api_cost_minor, state) - items(id, meal_id, name, grams_low, grams_mid, grams_high, kcal_low, kcal_mid, kcal_high, protein_g, carbs_g, fat_g, confidence, source) · source is model, edited or manual - corrections(id, item_id, field, model_value, user_value, at) · the record of every time the estimate was wrong, which is the only calibration data that exists - calibration(period_start, period_end, n_items, median_ratio, iqr_low, iqr_high) · computed from corrections, shown with its sample size or not shown - targets(id, kind, value, set_at, hidden) · optional, and hideable, because a target is not a requirement - settings(photo_retention_days, monthly_cost_ceiling_minor, numbers_hidden, model_id) - Every displayed figure carries a low and a high. There is no column anywhere in this schema holding a single authoritative calorie value. ### Phase 1 · A camera that opens, and a meal that stays yours Build: A home screen listing today's meals, and one large capture control using the phone camera directly. Resize on the device before upload. Store the photograph on disk and the row in SQLite. Serve over your own network with a certificate, since the camera needs a secure origin. Trap: A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. Done when: A meal is photographed and stored in two taps from the home screen, timed on a phone. Images are resized on the device before upload and the stored file is under a stated size, checked. The only outbound request in the whole flow is the model call, verified by watching the network. The application refuses to start over plain http on anything but the local machine, with a message explaining why the camera needs a secure origin. Do not build yet: No estimation, editing, history or export yet. ### Phase 2 · An estimate that arrives as a range Build: Ask for structured output: one entry per distinct food with a name, a low, middle and high gram estimate, macros for each, and a confidence. Validate against a schema. On a parse failure retry once, then show the manual entry form. Store the raw response and the prompt version with the meal so an estimate can be explained months later. Trap: Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. Done when: Every stored item has low, middle and high values and the interface has no path that displays the middle alone, checked by review. A malformed model response never produces a stored estimate, tested with a fake returning prose. The model identifier, prompt version and raw response are stored with every meal. A meal with no successful estimate is still loggable by hand and appears in the totals. Do not build yet: No correction step, calibration, history or export yet. ### Phase 3 · The correction step is the product Build: After an estimate, show every detected item with editable grams and an ingredient note, and rescale that item's macros as the grams change. Nothing is saved until the screen is confirmed. Prompt specifically for the things photographs hide, which the literature identifies as cooking oil, sauce and filling. Record every change as a correction row. Trap: Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. Done when: No meal reaches the log without passing through the confirmation screen, asserted in the suite. Changing an item's grams rescales its macros proportionally and the change is stored as a correction row with both values. The confirmation screen asks about added oil, sauce or filling for every meal. An item can be deleted or added by hand on the confirmation screen and the totals follow. Do not build yet: No calibration, history or export yet. ### Phase 4 · Calibration from your own corrections Build: Compute the ratio of corrected value to model value per item over a rolling window, and report the median with its spread and the number of corrections behind it. Show it as information, not as a silent multiplier. Below a stated number of corrections, show nothing at all. Trap: Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. Done when: Calibration is computed from correction rows only and is absent until a configured minimum of corrections exists, tested at the boundary. The figure is shown with its spread and its count wherever it appears. Applying calibration changes the displayed range and never rewrites a stored item, verified in the database after toggling it. Turning calibration off restores the original ranges exactly. Do not build yet: No history, export or retention work yet. ### Phase 5 · Weeks, not days, and a way out Build: A history view whose headline is a rolling seven-day average with a band, with single days available but never the front page. Export everything as CSV including corrections. Delete photographs after a configured retention while keeping the rows. Track the monthly model spend and stop estimating when it reaches the ceiling, falling back to manual entry. Trap: A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. Done when: The history front page shows a rolling average with a band and no single-day figure without its range, checked by review. Photographs older than the retention period are deleted and their rows survive with the estimate intact, tested. Export produces every meal, item and correction as CSV and reimports into an empty database, tested. Reaching the monthly cost ceiling disables estimation with a clear message and leaves manual entry working. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything clinical, including carbohydrate counting for insulin dosing, since a 2026 study found photograph-based carbohydrate estimates within ten grams for under half of composite meals. - Barcode scanning against a packaged food database, which is a licensing question and a maintained dataset rather than a weekend of code. - Health platform synchronisation, which needs a native application and a review process for one number a day. - Streaks, notifications and coaching copy, which are the habit machinery the subscription sells and are the part most likely to do harm pointed at a camera. ### README must contain - The published error figures for photograph-based energy estimation, with the studies and the dates, stated plainly before any instructions - That the estimate is biased downwards and that the bias grows with portion size and with hidden oil, sauce and filling - That the correction step exists because supplying portion and ingredient context roughly halves the error in published comparisons - That this is not a medical tool and must not be used for insulin dosing or any clinical decision - Where the photographs live, how long they are kept, and how to delete everything ===== ARCHITECTURE.md ===== # Architecture · Cal AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A small web application installed to the phone home screen, served on your own network | the camera flow has to be one tap and a native app is not worth building for one user | | Storage | SQLite beside the app, with photographs on disk, nothing in anyone else's cloud | a picture of everything you ate is a diary, and it should live where your diary lives | | Estimation | One call per photograph to a multimodal model, with structured output and validation | the model is rented and is the only part that leaves the machine | | Presentation | Ranges everywhere, never a single number, with the model and date attached | the published error is tens of percent and the interface has to say so | | Correction | An edit step that cannot be skipped, storing what you changed | supplying portion and ingredient context is the one thing shown to halve the error | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Camera, resize and local storage. | Any front end, provided capture stays at two taps. | | Estimation | One model call, structured output, validation. | Any provider, provided the output is a range and a failure is never a number. | | Correction | The confirmation screen and correction rows. | Not swappable for an optional edit, which is the same as no edit. | | Calibration | Ratios from corrections with sample size. | Any statistic, provided it is displayed rather than applied silently. | | History | Rolling averages, retention and export. | Any presentation, provided a day figure never appears without its range. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. - `MODEL_ID` · required · Stored with every meal, because a different model gives different numbers. - `DATABASE_PATH` · required · Meals, items, corrections and calibration. Back this up; the photographs matter less. - `PHOTO_DIR` · required · Meal photographs. Deleted after the retention period, rows retained. - `PHOTO_RETENTION_DAYS` · required · How long pictures of your meals are kept. Shorter is a reasonable default. - `MONTHLY_COST_CEILING` · required · Estimation stops at this figure for the month and manual entry continues. - `CALIBRATION_MIN_CORRECTIONS` · required · Below this, no calibration figure is shown at all. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Cal AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A small web application installed to the phone home screen, served on your own network, SQLite beside the app, with photographs on disk, nothing in anyone else's cloud, One call per photograph to a multimodal model, with structured output and validation, Ranges everywhere, never a single number, with the model and date attached, An edit step that cannot be skipped, storing what you changed. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. - A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. ===== MILESTONES.md ===== # Delivery milestones · Cal AI Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A camera that opens, and a meal that stays yours Photographing a meal takes one tap and nothing leaves the machine except the model call. ### Steps 1. Make capture two taps and prove it This number decides whether the app survives the first week. Files: `src/app/page.tsx`, `src/components/Capture.tsx` 2. Resize on the device before upload A phone photograph is several megabytes of detail the model does not use. Files: `src/components/Capture.tsx` 3. Serve over a secure origin on your network The camera will not open otherwise, and this trips everyone once. Files: `docs/lan-setup.md` ### Done when - [ ] Two taps from home screen to stored photograph - [ ] Images are resized on the device under a stated size - [ ] The model call is the only outbound request - [ ] An insecure origin is refused with an explanation ### Watch out - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. ## M2 · An estimate that arrives as a range The model returns items with low, middle and high values, validated, and never invented on failure. ### Steps 1. Ask for a range per item, not a number The range is the honest output of this measurement. Files: `src/lib/estimate.ts`, `src/lib/schema.ts` 2. Validate, retry once, then hand over to manual entry Never fabricate a number when the response did not parse. Files: `src/lib/estimate.ts`, `tests/estimate.test.ts` 3. Store the raw response and the prompt version So a strange estimate can be explained rather than argued about. Files: `src/lib/meals.ts` ### Done when - [ ] Every item carries low, middle and high - [ ] No interface path shows a single number alone - [ ] A malformed response stores nothing - [ ] Raw response and prompt version are retained ### Watch out - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. ## M3 · The correction step is the product Confirming a meal means editing what the model got wrong, and the edit is recorded. ### Steps 1. Put the edit between the photograph and the log An edit step people can skip is an edit step people skip. Files: `src/app/confirm/page.tsx` 2. Ask about oil, sauce and filling by name That is where the energy a photograph cannot see is hiding. Files: `src/app/confirm/page.tsx` 3. Record every correction with both values It is the only data you will ever have about your own errors. Files: `src/lib/corrections.ts`, `tests/corrections.test.ts` ### Done when - [ ] No meal is logged without confirmation - [ ] Editing grams rescales macros and stores a correction - [ ] Every meal is asked about oil, sauce and filling - [ ] Items can be added or removed by hand ### Watch out - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. ## M4 · Calibration from your own corrections The app learns how wrong it usually is for you, and says so with the sample size. ### Steps 1. Compute from corrections over a rolling window Old corrections describe an older diet and an older model. Files: `src/lib/calibration.ts`, `tests/calibration.test.ts` 2. Hide it entirely below a minimum count A ratio from eight corrections is a rumour. Files: `src/lib/calibration.ts` 3. Never write a calibrated value into storage Stored numbers are what the model said and what you said. Files: `src/lib/meals.ts` ### Done when - [ ] No calibration appears below the minimum count - [ ] The figure always carries its spread and count - [ ] Applying calibration never alters stored rows - [ ] Turning it off restores the original ranges ### Watch out - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ## M5 · Weeks, not days, and a way out (production only) History is presented at the timescale the error allows, the photographs expire, and the spend has a ceiling. ### Steps 1. Lead with the rolling average and its band Day to day movement is mostly measurement error. Files: `src/app/history/page.tsx` 2. Expire photographs, keep the rows The picture stops being useful long before the log does. Files: `src/lib/retention.ts` 3. Cap the monthly spend and degrade to manual A rented model with no ceiling is a surprise bill. Files: `src/lib/cost.ts`, `tests/cost.test.ts` ### Done when - [ ] The front page leads with a rolling average and band - [ ] Photographs expire and their rows survive - [ ] Export and reimport round-trips every table - [ ] The cost ceiling disables estimation, not the app ### Watch out - A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. ===== OPERATIONS.md ===== # Operations · Cal AI ## Backup The database first, the photographs second. Corrections are the only irreplaceable data here, since the estimates can be regenerated and the pictures cannot be eaten again. ## Restore Restore the database, confirm the meal count and that calibration recomputes to the same figure, then restore photographs if you kept them. A restore that loses corrections loses the calibration. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly model spend against the ceiling, the rate of parse failures, and the proportion of meals whose estimates were corrected. A falling correction rate usually means the confirmation screen got easier to click through, not that the model improved. ## Incident checklist The failure that matters is not an outage, it is a number that somebody trusted. The stored raw responses and correction rows are what let you go back and see what was shown on a given day and how far off it was. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No interface path is confirmed to show a single calorie figure alone - [ ] A malformed model response is confirmed to store nothing - [ ] No meal is confirmed to reach the log without the correction screen - [ ] Calibration is confirmed absent below the minimum correction count - [ ] The cost ceiling is confirmed to stop estimation and leave manual entry working ## Launch constraint Do not market omitted Cal AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. MODEL_API_KEY=sk-...redacted # Required. Stored with every meal, because a different model gives different numbers. MODEL_ID=a multimodal model identifier # Required. Meals, items, corrections and calibration. Back this up; the photographs matter less. DATABASE_PATH=./data/food.db # Required. Meal photographs. Deleted after the retention period, rows retained. PHOTO_DIR=./data/photos # Required. How long pictures of your meals are kept. Shorter is a reasonable default. PHOTO_RETENTION_DAYS=30 # Required. Estimation stops at this figure for the month and manual entry continues. MONTHLY_COST_CEILING=5.00 # Required. Below this, no calibration figure is shown at all. CALIBRATION_MIN_CORRECTIONS=40
# Cal AI · indie build A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A small web application installed to the phone home screen, served on your own network | the camera flow has to be one tap and a native app is not worth building for one user | | Storage | SQLite beside the app, with photographs on disk, nothing in anyone else's cloud | a picture of everything you ate is a diary, and it should live where your diary lives | | Estimation | One call per photograph to a multimodal model, with structured output and validation | the model is rented and is the only part that leaves the machine | | Presentation | Ranges everywhere, never a single number, with the model and date attached | the published error is tens of percent and the interface has to say so | | Correction | An edit step that cannot be skipped, storing what you changed | supplying portion and ingredient context is the one thing shown to halve the error | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application on the machine that serves your phone. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An API key for a multimodal model that accepts images** · pay per image; a few meals a day is small change, and the ceiling in this build is what stops it becoming otherwise - Why: The estimate is one call per photograph, and this is the only part of the build that leaves your machine or costs money. - Get it: Create a key with the provider you prefer and set a spend limit in their console. - Verify: One photograph returns valid structured output - [ ] **A phone on the same network, with the app installed to the home screen** · free - Why: The whole design stands or falls on the capture being two taps, and that is only testable on the device you will actually use at dinner. - Get it: Open the app on the phone and add it to the home screen. - Verify: Two taps from the icon produce a stored photograph - [ ] **A certificate for the local address** · free - Why: The camera will not open on an insecure origin from another device, which is the single most common reason this kind of build stalls on day one. - Get it: A local certificate authority for your network, or a hostname with a real certificate pointed at the local address. - Verify: The phone opens the app without a certificate warning and the camera works - [ ] **What the interface is allowed to display** · free - Why: The literature is clear that the error is large and directional, and deciding presentation rules afterwards means deciding them from whatever looked nicest. - Get it: Write down that ranges are mandatory, that the middle value is never shown alone, and what the minimum correction count for calibration is. - Verify: All three rules appear in the README and are asserted in tests ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. - A native app with widgets, notifications and instant cold start - Barcode scanning against a maintained packaged-food database - HealthKit / Google Fit / Apple Watch sync - Streaks, coaching copy and the habit scaffolding that makes tracking stick - Whatever portion-size calibration they have learned from millions of corrected logs If one of those is essential to you, that is the reason to keep paying for Cal AI, and the README should say so rather than pretend.
# Build brief · Cal AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private food log that estimates a meal from a photograph and shows the estimate as a range you are expected to correct to replace Cal AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This app makes a measurement claim, and the published evidence says the measurement is poor. That decides the whole design. Estimating energy from one photograph is not accurate. A 2025 study in Current Developments in Nutrition put standardised meal photographs through frontier multimodal models and measured a mean absolute percentage error for energy of about 36 percent for the two best and about 64 percent for the third, with the same order of error on weight and worse on fat and protein. The bias is not random: underestimation grew with portion size, and a separate 2025 study in Nutrients found meal weight underestimated in 76 percent of photographs, with agreement acceptable for small meals and breaking down for medium and large ones. Hidden oil, sauce and filling are the main reason. So: never display a bare number as if it were measured. Every estimate is shown as a range with the model named and the date. The correction step is not optional and cannot be skipped, because the same literature shows that supplying ingredient and portion context roughly halves the error, in one study from about 31 percent to about 14 percent. Do not add insulin dosing, medical advice or anything that treats these numbers as clinical: a 2026 study found carbohydrate estimates within ten grams for only 47 percent of composite meals. Regulators treat a calorie counter as not a medical device, which means nobody is checking your numbers except the person relying on them. And do not build streak pressure or shaming copy into a food tracking app pointed at a phone camera. The daily numbers can be hidden, the targets are optional, and there are no notifications. ### Stack (fixed, do not substitute) - Runtime: A small web application installed to the phone home screen, served on your own network · the camera flow has to be one tap and a native app is not worth building for one user - Storage: SQLite beside the app, with photographs on disk, nothing in anyone else's cloud · a picture of everything you ate is a diary, and it should live where your diary lives - Estimation: One call per photograph to a multimodal model, with structured output and validation · the model is rented and is the only part that leaves the machine - Presentation: Ranges everywhere, never a single number, with the model and date attached · the published error is tens of percent and the interface has to say so - Correction: An edit step that cannot be skipped, storing what you changed · supplying portion and ingredient context is the one thing shown to halve the error ### Data model (create this before Phase 1) - meals(id, at, photo_path, note, model_id, prompt_version, raw_response_json, api_cost_minor, state) - items(id, meal_id, name, grams_low, grams_mid, grams_high, kcal_low, kcal_mid, kcal_high, protein_g, carbs_g, fat_g, confidence, source) · source is model, edited or manual - corrections(id, item_id, field, model_value, user_value, at) · the record of every time the estimate was wrong, which is the only calibration data that exists - calibration(period_start, period_end, n_items, median_ratio, iqr_low, iqr_high) · computed from corrections, shown with its sample size or not shown - targets(id, kind, value, set_at, hidden) · optional, and hideable, because a target is not a requirement - settings(photo_retention_days, monthly_cost_ceiling_minor, numbers_hidden, model_id) - Every displayed figure carries a low and a high. There is no column anywhere in this schema holding a single authoritative calorie value. ### Phase 1 · A camera that opens, and a meal that stays yours Build: A home screen listing today's meals, and one large capture control using the phone camera directly. Resize on the device before upload. Store the photograph on disk and the row in SQLite. Serve over your own network with a certificate, since the camera needs a secure origin. Trap: A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. Done when: A meal is photographed and stored in two taps from the home screen, timed on a phone. Images are resized on the device before upload and the stored file is under a stated size, checked. The only outbound request in the whole flow is the model call, verified by watching the network. The application refuses to start over plain http on anything but the local machine, with a message explaining why the camera needs a secure origin. Do not build yet: No estimation, editing, history or export yet. ### Phase 2 · An estimate that arrives as a range Build: Ask for structured output: one entry per distinct food with a name, a low, middle and high gram estimate, macros for each, and a confidence. Validate against a schema. On a parse failure retry once, then show the manual entry form. Store the raw response and the prompt version with the meal so an estimate can be explained months later. Trap: Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. Done when: Every stored item has low, middle and high values and the interface has no path that displays the middle alone, checked by review. A malformed model response never produces a stored estimate, tested with a fake returning prose. The model identifier, prompt version and raw response are stored with every meal. A meal with no successful estimate is still loggable by hand and appears in the totals. Do not build yet: No correction step, calibration, history or export yet. ### Phase 3 · The correction step is the product Build: After an estimate, show every detected item with editable grams and an ingredient note, and rescale that item's macros as the grams change. Nothing is saved until the screen is confirmed. Prompt specifically for the things photographs hide, which the literature identifies as cooking oil, sauce and filling. Record every change as a correction row. Trap: Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. Done when: No meal reaches the log without passing through the confirmation screen, asserted in the suite. Changing an item's grams rescales its macros proportionally and the change is stored as a correction row with both values. The confirmation screen asks about added oil, sauce or filling for every meal. An item can be deleted or added by hand on the confirmation screen and the totals follow. Do not build yet: No calibration, history or export yet. ### Phase 4 · Calibration from your own corrections Build: Compute the ratio of corrected value to model value per item over a rolling window, and report the median with its spread and the number of corrections behind it. Show it as information, not as a silent multiplier. Below a stated number of corrections, show nothing at all. Trap: Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. Done when: Calibration is computed from correction rows only and is absent until a configured minimum of corrections exists, tested at the boundary. The figure is shown with its spread and its count wherever it appears. Applying calibration changes the displayed range and never rewrites a stored item, verified in the database after toggling it. Turning calibration off restores the original ranges exactly. Do not build yet: No history, export or retention work yet. ### Phase 5 · Weeks, not days, and a way out Build: A history view whose headline is a rolling seven-day average with a band, with single days available but never the front page. Export everything as CSV including corrections. Delete photographs after a configured retention while keeping the rows. Track the monthly model spend and stop estimating when it reaches the ceiling, falling back to manual entry. Trap: A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. Done when: The history front page shows a rolling average with a band and no single-day figure without its range, checked by review. Photographs older than the retention period are deleted and their rows survive with the estimate intact, tested. Export produces every meal, item and correction as CSV and reimports into an empty database, tested. Reaching the monthly cost ceiling disables estimation with a clear message and leaves manual entry working. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything clinical, including carbohydrate counting for insulin dosing, since a 2026 study found photograph-based carbohydrate estimates within ten grams for under half of composite meals. - Barcode scanning against a packaged food database, which is a licensing question and a maintained dataset rather than a weekend of code. - Health platform synchronisation, which needs a native application and a review process for one number a day. - Streaks, notifications and coaching copy, which are the habit machinery the subscription sells and are the part most likely to do harm pointed at a camera. ### README must contain - The published error figures for photograph-based energy estimation, with the studies and the dates, stated plainly before any instructions - That the estimate is biased downwards and that the bias grows with portion size and with hidden oil, sauce and filling - That the correction step exists because supplying portion and ingredient context roughly halves the error in published comparisons - That this is not a medical tool and must not be used for insulin dosing or any clinical decision - Where the photographs live, how long they are kept, and how to delete everything
# Agent instructions · Cal AI indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A small web application installed to the phone home screen, served on your own network, SQLite beside the app, with photographs on disk, nothing in anyone else's cloud, One call per photograph to a multimodal model, with structured output and validation, Ranges everywhere, never a single number, with the model and date attached, An edit step that cannot be skipped, storing what you changed. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value.
# Build plan · Cal AI A private photo food log whose hard problems are presenting an estimate honestly when the published error is tens of percent, making the correction step the centre of the interface rather than a dialogue nobody opens, and learning a personal calibration from corrections without pretending it is precision. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A camera that opens, and a meal that stays yours Photographing a meal takes one tap and nothing leaves the machine except the model call. ### Steps 1. Make capture two taps and prove it This number decides whether the app survives the first week. Files: `src/app/page.tsx`, `src/components/Capture.tsx` 2. Resize on the device before upload A phone photograph is several megabytes of detail the model does not use. Files: `src/components/Capture.tsx` 3. Serve over a secure origin on your network The camera will not open otherwise, and this trips everyone once. Files: `docs/lan-setup.md` ### Done when - [ ] Two taps from home screen to stored photograph - [ ] Images are resized on the device under a stated size - [ ] The model call is the only outbound request - [ ] An insecure origin is refused with an explanation ### Watch out - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. ## Phase 2 · An estimate that arrives as a range The model returns items with low, middle and high values, validated, and never invented on failure. ### Steps 1. Ask for a range per item, not a number The range is the honest output of this measurement. Files: `src/lib/estimate.ts`, `src/lib/schema.ts` 2. Validate, retry once, then hand over to manual entry Never fabricate a number when the response did not parse. Files: `src/lib/estimate.ts`, `tests/estimate.test.ts` 3. Store the raw response and the prompt version So a strange estimate can be explained rather than argued about. Files: `src/lib/meals.ts` ### Done when - [ ] Every item carries low, middle and high - [ ] No interface path shows a single number alone - [ ] A malformed response stores nothing - [ ] Raw response and prompt version are retained ### Watch out - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. ## Phase 3 · The correction step is the product Confirming a meal means editing what the model got wrong, and the edit is recorded. ### Steps 1. Put the edit between the photograph and the log An edit step people can skip is an edit step people skip. Files: `src/app/confirm/page.tsx` 2. Ask about oil, sauce and filling by name That is where the energy a photograph cannot see is hiding. Files: `src/app/confirm/page.tsx` 3. Record every correction with both values It is the only data you will ever have about your own errors. Files: `src/lib/corrections.ts`, `tests/corrections.test.ts` ### Done when - [ ] No meal is logged without confirmation - [ ] Editing grams rescales macros and stores a correction - [ ] Every meal is asked about oil, sauce and filling - [ ] Items can be added or removed by hand ### Watch out - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. ## Phase 4 · Calibration from your own corrections The app learns how wrong it usually is for you, and says so with the sample size. ### Steps 1. Compute from corrections over a rolling window Old corrections describe an older diet and an older model. Files: `src/lib/calibration.ts`, `tests/calibration.test.ts` 2. Hide it entirely below a minimum count A ratio from eight corrections is a rumour. Files: `src/lib/calibration.ts` 3. Never write a calibrated value into storage Stored numbers are what the model said and what you said. Files: `src/lib/meals.ts` ### Done when - [ ] No calibration appears below the minimum count - [ ] The figure always carries its spread and count - [ ] Applying calibration never alters stored rows - [ ] Turning it off restores the original ranges ### Watch out - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ## Not in this build - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. ## After v1, if you want it - A reference object in the photograph, which the literature suggests helps less than supplying ingredients does - Recipes you cook often, stored once and logged by name, which removes the model from the meals you eat most - A monthly report of how far the model was from your corrections, which is the only accuracy number that means anything here
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. MODEL_API_KEY=sk-...redacted # Required. Stored with every meal, because a different model gives different numbers. MODEL_ID=a multimodal model identifier # Required. Meals, items, corrections and calibration. Back this up; the photographs matter less. DATABASE_PATH=./data/food.db # Required. Meal photographs. Deleted after the retention period, rows retained. PHOTO_DIR=./data/photos # Required. How long pictures of your meals are kept. Shorter is a reasonable default. PHOTO_RETENTION_DAYS=30 # Required. Estimation stops at this figure for the month and manual entry continues. MONTHLY_COST_CEILING=5.00 # Required. Below this, no calibration figure is shown at all. CALIBRATION_MIN_CORRECTIONS=40
# Cal AI · product brief ## Problem The core trick, send a food photo to a multimodal model and ask for calories and macros as JSON, is a one-evening build and works surprisingly well. Where it stops being easy is everything around it: a native app that opens fast, a camera flow you actually use three times a day, barcode lookups against a real food database, HealthKit or Google Fit sync, and streaks that keep you logging past day four. Accuracy is also less about your prompt and more about calibration, portion-size guessing is where these apps live or die and you have no correction data. A local PWA is a genuinely useful personal replacement if you are the kind of person who will tolerate a browser bookmark instead of an app icon. You are also renting the vision model, so this is not fully self-contained. ## Product outcome A food log that is honest about a measurement the published evidence says is rough: every estimate is a range, the correction step is the interface rather than a dialogue, the history leads with a rolling average, and the photographs expire. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - An API key for a multimodal model that accepts images - Node 20 and a machine or cheap VPS to run it - A phone browser, added to home screen, for the camera flow - Acceptance that portion estimates will be wrong by 20 percent sometimes ## Explicit non-goals for v1 - Anything clinical, including carbohydrate counting for dosing. - Barcode scanning and a packaged food database. - Health platform synchronisation. - Streaks, notifications and coaching copy. - A native app with widgets, notifications and instant cold start - Barcode scanning against a maintained packaged-food database - HealthKit / Google Fit / Apple Watch sync - Streaks, coaching copy and the habit scaffolding that makes tracking stick - Whatever portion-size calibration they have learned from millions of corrected logs ## Success criteria - No interface path is confirmed to show a single calorie figure alone - A malformed model response is confirmed to store nothing - No meal is confirmed to reach the log without the correction screen - Calibration is confirmed absent below the minimum correction count - The cost ceiling is confirmed to stop estimation and leave manual entry working
# Build brief · Cal AI The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private food log that estimates a meal from a photograph and shows the estimate as a range you are expected to correct to replace Cal AI. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero This app makes a measurement claim, and the published evidence says the measurement is poor. That decides the whole design. Estimating energy from one photograph is not accurate. A 2025 study in Current Developments in Nutrition put standardised meal photographs through frontier multimodal models and measured a mean absolute percentage error for energy of about 36 percent for the two best and about 64 percent for the third, with the same order of error on weight and worse on fat and protein. The bias is not random: underestimation grew with portion size, and a separate 2025 study in Nutrients found meal weight underestimated in 76 percent of photographs, with agreement acceptable for small meals and breaking down for medium and large ones. Hidden oil, sauce and filling are the main reason. So: never display a bare number as if it were measured. Every estimate is shown as a range with the model named and the date. The correction step is not optional and cannot be skipped, because the same literature shows that supplying ingredient and portion context roughly halves the error, in one study from about 31 percent to about 14 percent. Do not add insulin dosing, medical advice or anything that treats these numbers as clinical: a 2026 study found carbohydrate estimates within ten grams for only 47 percent of composite meals. Regulators treat a calorie counter as not a medical device, which means nobody is checking your numbers except the person relying on them. And do not build streak pressure or shaming copy into a food tracking app pointed at a phone camera. The daily numbers can be hidden, the targets are optional, and there are no notifications. ### Stack (fixed, do not substitute) - Runtime: A small web application installed to the phone home screen, served on your own network · the camera flow has to be one tap and a native app is not worth building for one user - Storage: SQLite beside the app, with photographs on disk, nothing in anyone else's cloud · a picture of everything you ate is a diary, and it should live where your diary lives - Estimation: One call per photograph to a multimodal model, with structured output and validation · the model is rented and is the only part that leaves the machine - Presentation: Ranges everywhere, never a single number, with the model and date attached · the published error is tens of percent and the interface has to say so - Correction: An edit step that cannot be skipped, storing what you changed · supplying portion and ingredient context is the one thing shown to halve the error ### Data model (create this before Phase 1) - meals(id, at, photo_path, note, model_id, prompt_version, raw_response_json, api_cost_minor, state) - items(id, meal_id, name, grams_low, grams_mid, grams_high, kcal_low, kcal_mid, kcal_high, protein_g, carbs_g, fat_g, confidence, source) · source is model, edited or manual - corrections(id, item_id, field, model_value, user_value, at) · the record of every time the estimate was wrong, which is the only calibration data that exists - calibration(period_start, period_end, n_items, median_ratio, iqr_low, iqr_high) · computed from corrections, shown with its sample size or not shown - targets(id, kind, value, set_at, hidden) · optional, and hideable, because a target is not a requirement - settings(photo_retention_days, monthly_cost_ceiling_minor, numbers_hidden, model_id) - Every displayed figure carries a low and a high. There is no column anywhere in this schema holding a single authoritative calorie value. ### Phase 1 · A camera that opens, and a meal that stays yours Build: A home screen listing today's meals, and one large capture control using the phone camera directly. Resize on the device before upload. Store the photograph on disk and the row in SQLite. Serve over your own network with a certificate, since the camera needs a secure origin. Trap: A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. Done when: A meal is photographed and stored in two taps from the home screen, timed on a phone. Images are resized on the device before upload and the stored file is under a stated size, checked. The only outbound request in the whole flow is the model call, verified by watching the network. The application refuses to start over plain http on anything but the local machine, with a message explaining why the camera needs a secure origin. Do not build yet: No estimation, editing, history or export yet. ### Phase 2 · An estimate that arrives as a range Build: Ask for structured output: one entry per distinct food with a name, a low, middle and high gram estimate, macros for each, and a confidence. Validate against a schema. On a parse failure retry once, then show the manual entry form. Store the raw response and the prompt version with the meal so an estimate can be explained months later. Trap: Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. Done when: Every stored item has low, middle and high values and the interface has no path that displays the middle alone, checked by review. A malformed model response never produces a stored estimate, tested with a fake returning prose. The model identifier, prompt version and raw response are stored with every meal. A meal with no successful estimate is still loggable by hand and appears in the totals. Do not build yet: No correction step, calibration, history or export yet. ### Phase 3 · The correction step is the product Build: After an estimate, show every detected item with editable grams and an ingredient note, and rescale that item's macros as the grams change. Nothing is saved until the screen is confirmed. Prompt specifically for the things photographs hide, which the literature identifies as cooking oil, sauce and filling. Record every change as a correction row. Trap: Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. Done when: No meal reaches the log without passing through the confirmation screen, asserted in the suite. Changing an item's grams rescales its macros proportionally and the change is stored as a correction row with both values. The confirmation screen asks about added oil, sauce or filling for every meal. An item can be deleted or added by hand on the confirmation screen and the totals follow. Do not build yet: No calibration, history or export yet. ### Phase 4 · Calibration from your own corrections Build: Compute the ratio of corrected value to model value per item over a rolling window, and report the median with its spread and the number of corrections behind it. Show it as information, not as a silent multiplier. Below a stated number of corrections, show nothing at all. Trap: Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. Done when: Calibration is computed from correction rows only and is absent until a configured minimum of corrections exists, tested at the boundary. The figure is shown with its spread and its count wherever it appears. Applying calibration changes the displayed range and never rewrites a stored item, verified in the database after toggling it. Turning calibration off restores the original ranges exactly. Do not build yet: No history, export or retention work yet. ### Phase 5 · Weeks, not days, and a way out Build: A history view whose headline is a rolling seven-day average with a band, with single days available but never the front page. Export everything as CSV including corrections. Delete photographs after a configured retention while keeping the rows. Track the monthly model spend and stop estimating when it reaches the ceiling, falling back to manual entry. Trap: A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached. Done when: The history front page shows a rolling average with a band and no single-day figure without its range, checked by review. Photographs older than the retention period are deleted and their rows survive with the estimate intact, tested. Export produces every meal, item and correction as CSV and reimports into an empty database, tested. Reaching the monthly cost ceiling disables estimation with a clear message and leaves manual entry working. Do not build yet: Nothing after this. ### Out of scope (and why) - Anything clinical, including carbohydrate counting for insulin dosing, since a 2026 study found photograph-based carbohydrate estimates within ten grams for under half of composite meals. - Barcode scanning against a packaged food database, which is a licensing question and a maintained dataset rather than a weekend of code. - Health platform synchronisation, which needs a native application and a review process for one number a day. - Streaks, notifications and coaching copy, which are the habit machinery the subscription sells and are the part most likely to do harm pointed at a camera. ### README must contain - The published error figures for photograph-based energy estimation, with the studies and the dates, stated plainly before any instructions - That the estimate is biased downwards and that the bias grows with portion size and with hidden oil, sauce and filling - That the correction step exists because supplying portion and ingredient context roughly halves the error in published comparisons - That this is not a medical tool and must not be used for insulin dosing or any clinical decision - Where the photographs live, how long they are kept, and how to delete everything
# Architecture · Cal AI ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | A small web application installed to the phone home screen, served on your own network | the camera flow has to be one tap and a native app is not worth building for one user | | Storage | SQLite beside the app, with photographs on disk, nothing in anyone else's cloud | a picture of everything you ate is a diary, and it should live where your diary lives | | Estimation | One call per photograph to a multimodal model, with structured output and validation | the model is rented and is the only part that leaves the machine | | Presentation | Ranges everywhere, never a single number, with the model and date attached | the published error is tens of percent and the interface has to say so | | Correction | An edit step that cannot be skipped, storing what you changed | supplying portion and ingredient context is the one thing shown to halve the error | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Camera, resize and local storage. | Any front end, provided capture stays at two taps. | | Estimation | One model call, structured output, validation. | Any provider, provided the output is a range and a failure is never a number. | | Correction | The confirmation screen and correction rows. | Not swappable for an optional edit, which is the same as no edit. | | Calibration | Ratios from corrections with sample size. | Any statistic, provided it is displayed rather than applied silently. | | History | Rolling averages, retention and export. | Any presentation, provided a day figure never appears without its range. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `MODEL_API_KEY` · required, secret · The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. - `MODEL_ID` · required · Stored with every meal, because a different model gives different numbers. - `DATABASE_PATH` · required · Meals, items, corrections and calibration. Back this up; the photographs matter less. - `PHOTO_DIR` · required · Meal photographs. Deleted after the retention period, rows retained. - `PHOTO_RETENTION_DAYS` · required · How long pictures of your meals are kept. Shorter is a reasonable default. - `MONTHLY_COST_CEILING` · required · Estimation stops at this figure for the month and manual entry continues. - `CALIBRATION_MIN_CORRECTIONS` · required · Below this, no calibration figure is shown at all. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Cal AI product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A small web application installed to the phone home screen, served on your own network, SQLite beside the app, with photographs on disk, nothing in anyone else's cloud, One call per photograph to a multimodal model, with structured output and validation, Ranges everywhere, never a single number, with the model and date attached, An edit step that cannot be skipped, storing what you changed. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. - A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached.
# Delivery milestones · Cal AI Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A camera that opens, and a meal that stays yours Photographing a meal takes one tap and nothing leaves the machine except the model call. ### Steps 1. Make capture two taps and prove it This number decides whether the app survives the first week. Files: `src/app/page.tsx`, `src/components/Capture.tsx` 2. Resize on the device before upload A phone photograph is several megabytes of detail the model does not use. Files: `src/components/Capture.tsx` 3. Serve over a secure origin on your network The camera will not open otherwise, and this trips everyone once. Files: `docs/lan-setup.md` ### Done when - [ ] Two taps from home screen to stored photograph - [ ] Images are resized on the device under a stated size - [ ] The model call is the only outbound request - [ ] An insecure origin is refused with an explanation ### Watch out - A capture flow that takes three taps and two seconds is abandoned by the fourth day, which is the actual reason paid trackers win, so any friction you add here costs more than any accuracy you gain elsewhere. Measure the taps from opening the app to a stored photograph and keep it at two. ## M2 · An estimate that arrives as a range The model returns items with low, middle and high values, validated, and never invented on failure. ### Steps 1. Ask for a range per item, not a number The range is the honest output of this measurement. Files: `src/lib/estimate.ts`, `src/lib/schema.ts` 2. Validate, retry once, then hand over to manual entry Never fabricate a number when the response did not parse. Files: `src/lib/estimate.ts`, `tests/estimate.test.ts` 3. Store the raw response and the prompt version So a strange estimate can be explained rather than argued about. Files: `src/lib/meals.ts` ### Done when - [ ] Every item carries low, middle and high - [ ] No interface path shows a single number alone - [ ] A malformed response stores nothing - [ ] Raw response and prompt version are retained ### Watch out - Asking for one number per food and displaying it produces an interface that looks like a scale reading, and people then plan around a figure whose published error on mixed meals runs into the tens of percent. Ask for a range, store all three values, and let the interface have no way of showing the middle one on its own. ## M3 · The correction step is the product Confirming a meal means editing what the model got wrong, and the edit is recorded. ### Steps 1. Put the edit between the photograph and the log An edit step people can skip is an edit step people skip. Files: `src/app/confirm/page.tsx` 2. Ask about oil, sauce and filling by name That is where the energy a photograph cannot see is hiding. Files: `src/app/confirm/page.tsx` 3. Record every correction with both values It is the only data you will ever have about your own errors. Files: `src/lib/corrections.ts`, `tests/corrections.test.ts` ### Done when - [ ] No meal is logged without confirmation - [ ] Editing grams rescales macros and stores a correction - [ ] Every meal is asked about oil, sauce and filling - [ ] Items can be added or removed by hand ### Watch out - Saving the estimate straight to the log with an edit button somewhere afterwards means nobody ever edits, and the log fills with numbers that the published evidence says are biased downwards and get worse as the plate gets bigger. Put the edit between the photograph and the log, and ask about oil and sauce by name because that is where the missing energy is. ## M4 · Calibration from your own corrections The app learns how wrong it usually is for you, and says so with the sample size. ### Steps 1. Compute from corrections over a rolling window Old corrections describe an older diet and an older model. Files: `src/lib/calibration.ts`, `tests/calibration.test.ts` 2. Hide it entirely below a minimum count A ratio from eight corrections is a rumour. Files: `src/lib/calibration.ts` 3. Never write a calibrated value into storage Stored numbers are what the model said and what you said. Files: `src/lib/meals.ts` ### Done when - [ ] No calibration appears below the minimum count - [ ] The figure always carries its spread and count - [ ] Applying calibration never alters stored rows - [ ] Turning it off restores the original ranges ### Watch out - Quietly multiplying future estimates by a factor learned from a dozen corrections produces numbers that look better and are not, because the factor was fitted on the meals you happened to correct and those are the ones that looked wrong. Display the calibration with its sample size, let the user decide whether to apply it, and never fold it into a stored value. ## M5 · Weeks, not days, and a way out (production only) History is presented at the timescale the error allows, the photographs expire, and the spend has a ceiling. ### Steps 1. Lead with the rolling average and its band Day to day movement is mostly measurement error. Files: `src/app/history/page.tsx` 2. Expire photographs, keep the rows The picture stops being useful long before the log does. Files: `src/lib/retention.ts` 3. Cap the monthly spend and degrade to manual A rented model with no ceiling is a surprise bill. Files: `src/lib/cost.ts`, `tests/cost.test.ts` ### Done when - [ ] The front page leads with a rolling average and band - [ ] Photographs expire and their rows survive - [ ] Export and reimport round-trips every table - [ ] The cost ceiling disables estimation, not the app ### Watch out - A daily total displayed to the calorie with a target line invites someone to act on a difference of a couple of hundred between two days, when the published per-meal error is far larger than that difference, so the interface is reporting noise as a result. Lead with a rolling average and a band, and keep day figures behind a tap with their uncertainty attached.
# Operations · Cal AI ## Backup The database first, the photographs second. Corrections are the only irreplaceable data here, since the estimates can be regenerated and the pictures cannot be eaten again. ## Restore Restore the database, confirm the meal count and that calibration recomputes to the same figure, then restore photographs if you kept them. A restore that loses corrections loses the calibration. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Monthly model spend against the ceiling, the rate of parse failures, and the proportion of meals whose estimates were corrected. A falling correction rate usually means the confirmation screen got easier to click through, not that the model improved. ## Incident checklist The failure that matters is not an outage, it is a number that somebody trusted. The stored raw responses and correction rows are what let you go back and see what was shown on a given day and how far off it was. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] No interface path is confirmed to show a single calorie figure alone - [ ] A malformed model response is confirmed to store nothing - [ ] No meal is confirmed to reach the log without the correction screen - [ ] Calibration is confirmed absent below the minimum correction count - [ ] The cost ceiling is confirmed to stop estimation and leave manual entry working ## Launch constraint Do not market omitted Cal AI capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. The multimodal model. Give it a provider-side spend limit as well as the ceiling in this app. MODEL_API_KEY=sk-...redacted # Required. Stored with every meal, because a different model gives different numbers. MODEL_ID=a multimodal model identifier # Required. Meals, items, corrections and calibration. Back this up; the photographs matter less. DATABASE_PATH=./data/food.db # Required. Meal photographs. Deleted after the retention period, rows retained. PHOTO_DIR=./data/photos # Required. How long pictures of your meals are kept. Shorter is a reasonable default. PHOTO_RETENTION_DAYS=30 # Required. Estimation stops at this figure for the month and manual entry continues. MONTHLY_COST_CEILING=5.00 # Required. Below this, no calibration figure is shown at all. CALIBRATION_MIN_CORRECTIONS=40
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because calorie tracking only works if the friction is near zero, and a subscription buys an app icon, a camera that opens in half a second, a food database, and a nag notification at 8pm. A self-hosted web version costs you nothing per month but adds three seconds and a mental hurdle to every meal, which is exactly the amount of friction that ends a tracking habit. People are not paying for the vision call, they are paying for the thing that makes them do it on day thirty.
xA native app with widgets, notifications and instant cold start
xBarcode scanning against a maintained packaged-food database
xHealthKit / Google Fit / Apple Watch sync
xStreaks, coaching copy and the habit scaffolding that makes tracking stick
xWhatever portion-size calibration they have learned from millions of corrected logs
Nothing worth pointing at. That's why the prompt exists.
Vibecode Cal AI
Kinda. The core of Cal AI is buildable in a weekend with the prompt on this page, but there are real gaps: A native app with widgets, notifications and instant cold start, Barcode scanning against a maintained packaged-food database. Read the honest list above before committing.
How much does Cal AI cost?
Cal AI costs about $9.99/month (Cal AI Unlimited, checked 2026-08-18), which is $119.88 per year.
What do I lose by replacing Cal AI?
Honestly: A native app with widgets, notifications and instant cold start; Barcode scanning against a maintained packaged-food database; HealthKit / Google Fit / Apple Watch sync; Streaks, coaching copy and the habit scaffolding that makes tracking stick; Whatever portion-size calibration they have learned from millions of corrected logs. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Cal AI?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.