Nutrition and workout tracker
A Telegram agent that logs my meals and workouts
Overview
I had been working out consistently for almost a year, but I wasn't seeing much change. Because I wasn't tracking my meals or workouts, I had no idea whether I was hitting my calorie and protein goals or improving my workouts over time.
I built this Telegram fitness tracker to log my meals and workouts and tell me where I stand against my targets.
What it does
It logs meals and workouts, and answers questions about nutrition or training, like whether a snack fits into today's remaining calories or how to adjust my next leg session after skipping the last one. It also sends me nudges to stay consistent.
The key features I built it for:
Meal logging
I can log meals in natural language, and the tracker records their calories and macros against my daily targets.
Workout logging
I can describe a workout in my own words, and the tracker turns it into structured exercises, sets, reps, and weights.
Nudges
I receive nudges like weekly summaries and reminders that help me stay consistent.
How it works
Every message passes through four components before the tracker responds.
- Claude: Understands the request and decides which tool to use.
- Tools: Perform specific tasks such as logging meals or retrieving workout history.
- Application: The code that executes the requested tool, validates the result, and prepares the response.
- SQL Database: Stores every meal and workout and acts as the source of truth.
Nutrition
Each dish begins with a lookup against previously logged dishes. If a match exists, the saved calories and macros are retrieved. Otherwise, Claude estimates the calories and macros, which I can adjust before confirming. Once confirmed, the dish is logged, daily totals are updated, and the response is returned to Telegram.
Workout
I describe my workout in shorthand. Claude interprets it as a workout logging request, structures the exercises and sets, and asks me to confirm them. After I confirm it, the workout is written to the database. Recent history for each exercise is then retrieved and compared against previous sessions. If there's enough evidence of a consistent trend, Claude adds a coaching note to the response.
Key challenges and decisions
Designing for ambiguity
When logging a meal, I simply type whatever comes naturally. Sometimes I describe the lunch in full, like "200 g black chana and 100 g rice". Other times I just type "oats" for breakfast, expecting the tracker to figure out whether I meant overnight oats or oatmeal. Some messages are straightforward, while others could be interpreted in more than one way. I wanted the tracker to understand those variations without asking unnecessary questions, while still asking for clarification when an assumption could affect the log. The table below shows how I handled those decisions.
| Situation | What it does | Why |
|---|---|---|
| The dish name matches more than one saved item ("Had oats" when you've saved both "oatmeal" and "overnight oats") | Shows both options with their macros for me to choose. | Guessing wrong would log a different food entirely. |
| A never-before-seen phrase could be one dish or two items ("Had rajma chawal," nothing similar saved) | Asks directly, before estimating anything, whether it's one dish or two. | The answer determines how the dish gets stored. |
| A saved dish is re-described with different numbers than what's already logged (oats saved at 685 kcal, new ingredients add up to something else) | Asks if it's the same dish, a correction, or a new variant. | A wrong choice overwrites good data or creates duplicates. |
| A brand-new dish with no breakdown ("chicken sandwich") | Asks whether to estimate or requests a breakdown, unless it's a single item like "eggs", which gets estimated directly | Recipes vary too much to default safely. |
Calculating daily totals deterministically
Early on, I let Claude calculate the day's calorie and protein totals from the meals it had logged. One day Claude reported 2,101 kcal for a day that actually totaled 2,021 kcal. The calories and macros for each logged meal were correct, but the day's total wasn't. After seeing it happen a few more times, I stopped relying on Claude to calculate the totals and let the code handle it instead.
A better model would probably make this mistake less often today, but this calculation is still better handled in code. I realized the calorie and macro values for every logged meal were already known, so I shouldn't have been asking Claude to recalculate the totals. It's a deterministic operation that doesn't need Claude's reasoning.
Replacing classification with tool use
The first version of the tracker ran on Ollama, a local model I chose because it was free to run and kept my nutrition data on my own machine. Every request went through a classify-then-extract pattern: one call to figure out what I was trying to do, a second to pull out the details.
Both calls depended on the model getting that format exactly right, every time, and Ollama wasn't always reliable about it. Sometimes it added a little extra formatting around its answer, like wrapping it in ```, and that alone was enough to confuse the code, even though the answer itself was correct. When that happened, the tracker replied, "Sorry, I couldn't parse that meal. Could you rephrase?"
I switched to Claude for more reliable outputs, but the architecture stayed the same. One day I typed "log my lunch and show my remaining protein," and it logged the meal but said nothing about the protein. The architecture could only handle one request per message.
I realized I'd built the wrong architecture. I moved to Claude's native tool use, which let Claude call a function directly instead of describing its intent in text for the code to parse. It gave Claude direct access to the application's functions as tools: match_food, log_meal, get_daily_summary, and the rest. Claude could call more than one tool in the same exchange, so a message with two requests could finally be handled as a single conversation. The code stopped routing requests. It executed whatever tool Claude chose to call.
Closing thoughts
This project changed quite a bit as I built it. It still isn't the cleanest system, but it does the job I built it for.
It made me much more aware of my eating habits. I still have days where I overshoot my nutrition targets. The difference is now I know when it happens and what caused it.
Building this also deepened my understanding of AI agents. While I was building it, we were working on an agentic pipeline at work, and a colleague shared how he was approaching it. He described an agent as having a brain and a set of tools, which made me rethink the project's architecture, and so I moved from classification to Claude's native tool use. Later, I came across Anthropic describing the same idea more formally in their writing on building effective agents.
None of this would have been worth building a few years ago. I'd have needed a team, or paid for one of the dozen nutrition apps already on the market. The cost of building software has dropped enough that being curious is reason enough to build something. That's made me question what a SaaS product's moat even is anymore.