Send meal suggestions into a user's feed and read back their preferences, so
an assistant can learn what they actually like to cook.
The user swipes through your suggestions per meal category and the ones they like become a
day plan in the app. Every decision is readable through the history endpoint, which is what
makes the loop useful: suggest, observe, suggest better.
The app keeps the last plan it loaded. Once suggestions have arrived they
stay readable without a connection — the phone falls back to the stored plan only when the
server is unreachable, so an empty day stays empty rather than resurrecting yesterday.
The daily loop
Nothing about the user's diet is stored on the server — no profile endpoint, no settings
screen. What they eat, avoid and cook for lives in your prompt file on
their own machine. Once a day the assistant runs three moves:
1. GET /recipe/history?date=<today> are today's suggestions already there?
2. GET /recipe/history?state=liked what did they cook, what did they reject
3. POST /recipe/suggest today's dishes, per meal category
Step 1 is the guard. summary.total > 0 for today means the run stops before
it writes anything — important here, because suggestappends:
a second run would double every meal in the feed instead of replacing it.
Not interested in the plumbing? Skip to the lazy
path and let an assistant build all of it for you.
What lives where
Two places, and the split is deliberate:
On the server
In your prompt file
Every dish you were ever sent, its meal category and date, and whether you liked it, rejected it or left it open.
The distilled version: household, diet, allergies, hard no-gos — and the learned: lines the assistant writes after reading your decisions.
Durable. Readable any time with your key. Survives a reinstall, a new phone and a new laptop.
Yours. A plain text file you can read, correct or delete — open it in any editor.
The rule the assistant follows: raw events on the server, conclusions in the
file. It never copies the history into the file — it re-reads the history and
rewrites the conclusions, so a wrong assumption is one deleted line away from being gone.
The lazy path: hand the whole setup to your AI
Copy this into Claude Code or Codex CLI, replace the key,
and answer the questions. It reads the docs itself and comes back when the first dishes are
in your feed and tomorrow's run is scheduled.
Set up my daily inTouch meal suggestions, end to end.
Read these two pages first — they are plain text, written for you, and contain
everything you need:
https://docs.in-touch.cc/recipes.txt
https://docs.in-touch.cc/llms.txt
On the recipes page, follow the section "self-setup" — it is the job description.
My inTouch API key: itk_PASTE_YOUR_KEY_HERE
Ask me how I eat, then do the rest yourself: write the prompt file that holds my
diet, allergies and household, push a first day of suggestions, and install the
daily automation for my operating system. Show me what you are going to create
before you create it, and never print my key.
What it does with that. It asks how many people it cooks for, your diet,
allergies, hard no-gos and which meals you want each day, writes them into
~/intouch/recipe-prompt.md, pushes a first day into the app, adds a guard
script and registers a scheduler — launchd, systemd, Task Scheduler or a GitHub Actions
cron. Then it runs the script once so you can watch the guard refuse a second push.
The prompt stays short on purpose: the instructions live in
recipes.txt under self-setup, so they improve without
you ever copying a new prompt. What you paste is a pointer, not a program.
What it needs — and where it stops. An assistant with web access
and a terminal: Claude Code and Codex CLI both qualify, and Codex additionally
needs network access allowed in its sandbox. A browser chat can push a first day of dishes,
but it cannot write a prompt file or install a scheduler — it will say so and stop there.
The manual path: set it up yourself
1 · Get your key
In the app: Settings → App & Media → inTouch API, then
Generate key. The same key already works for
Vocabulary — you never need a second one.
2 · Pick your AI
Identical to the vocabulary setup: npm install -g @anthropic-ai/claude-code
or npm install -g @openai/codex, sign in once, then run the daily prompt
headless with claude -p "$(cat ~/intouch/recipe-prompt.md)" --allowedTools
"Bash(curl:*)" or codex exec --full-auto "$(cat
~/intouch/recipe-prompt.md)". Codex needs network access allowed in its sandbox —
see the vocabulary page for the details.
3 · Save the daily prompt
Put this in ~/intouch/recipe-prompt.md. The header block is the only
place your preferences exist — edit those lines whenever your week changes.
You keep my inTouch meal suggestions up to date. Run this once a day and do not ask
me anything — decide on your own and report in one line at the end.
HOW I EAT (edit these lines, they live here and nowhere else)
cooking for: 2 adults
diet: vegetarian, fish is fine
allergies: hazelnuts — never suggest them, not even as a topping
hard no: coriander, blue cheese
meals per day: breakfast, lunch, dinner
options per meal: 2
effort: weekdays max 30 min, weekends anything
learned: (you append one line here when you notice a pattern)
last read: (cursor from the history; you overwrite it every run)
API base: https://api.in-touch.cc
Auth: header "X-API-Key" with the value of the INTOUCH_API_KEY environment variable
Reference: https://docs.in-touch.cc/recipes.txt
STEP 1 - IS TODAY ALREADY DONE?
GET /api/addons/recipe/history?date=<today, UTC>
If summary.total is greater than 0, stop. suggest APPENDS - a second run would
double every meal in the feed. Never push twice for the same day.
STEP 2 - READ WHAT WORKED (only what is new)
GET /api/addons/recipe/history?since=<last read>&limit=200
- no "last read" yet? Leave since out for the first run.
- keep calling with the returned cursor while has_more is true; this brings
new dishes AND hearts tapped on older ones.
- write the final cursor into "last read:" at the top of this file.
Every few weeks, or when the picture feels thin, read the whole thing again:
GET /api/addons/recipe/history?state=liked
GET /api/addons/recipe/history?state=disliked
Look for patterns, not single dishes: cuisines, main ingredients, effort,
which meal categories they never decide on at all.
STEP 3 - BUILD TODAY'S DISHES
- One entry per meal category from the header block, "options per meal" each.
- Respect diet, allergies and hard no-gos absolutely. An allergy is not a
preference: never send it, in any form, in any dish.
- Lean towards what was liked, avoid what was rejected, and do not repeat a
dish they already saw this week.
- Fill in the recipe object properly: ingredients, steps, duration, servings
matching "cooking for", plus tags and nutrition when you know them.
STEP 4 - PUSH IT
POST /api/addons/recipe/suggest with {"plan_date": "<today>", "recipes": [ ... ]}
Each recipe needs meal_category and title; everything else is optional but the
card looks poor without image, duration and steps.
The response reports inserted, rejected and errors[] per index. Fix rejected
items and push ONLY those again - suggest appends, so never resend the batch.
STEP 5 - REMEMBER
If step 2 showed a clear pattern, append ONE line to "learned:" at the top of
this file. That is how this file gets better without me having to tell you.
Finish with one line: date, how many dishes, and what you changed since yesterday.
4 · Wrap it in a guard script
Same shape as the vocabulary guard, different question: has today already been filled?
#!/usr/bin/env bash
# ~/intouch/recipes.sh
set -euo pipefail
export INTOUCH_API_KEY="itk_YOUR_KEY"
BASE="https://api.in-touch.cc"
TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT
today="$(date -u +%F)" # the API works in UTC
# No network yet (the machine just woke up) is not an error. A rejected key is:
# it has to fail loudly, or the schedule quietly does nothing for weeks.
code="$(curl -sS -o "$TMP" -w '%{http_code}' \
-H "X-API-Key: $INTOUCH_API_KEY" \
"$BASE/api/addons/recipe/history?date=$today")" || {
echo "inTouch: API not reachable, leaving it for the next run"; exit 0
}
case "$code" in
200) ;;
401|403) echo "inTouch: key rejected ($code) - fix it in this script"; exit 1 ;;
*) echo "inTouch: API returned $code, trying again next run"; exit 0 ;;
esac
if [ "$(jq -r '.summary.total // 0' "$TMP")" -gt 0 ]; then
echo "inTouch: suggestions for $today already exist"
exit 0
fi
claude -p "$(cat "$HOME/intouch/recipe-prompt.md")" --allowedTools "Bash(curl:*)"
# Codex: codex exec --full-auto "$(cat "$HOME/intouch/recipe-prompt.md")"
Run it every day
The schedulers are exactly the four from the vocabulary page — launchd on macOS, a systemd
timer on Linux and a VPS, Task Scheduler on Windows, GitHub Actions when no machine of yours
is awake. All of them are written to catch up after a missed run, which is
what makes a laptop schedule work.
Take the snippets from there and change two things: the
script path (recipes.sh) and the job name (com.intouch.recipes,
intouch-recipes.timer, inTouch Recipes). Running both capabilities
side by side is two independent jobs — and a good reason to give them distinct names.
The full snippets are also in recipes.txt
under self-setup: working snippets, already carrying the recipe names — that is the
copy an assistant reads.
Submit suggestions
POST/api/addons/recipe/suggestX-API-Key
Body
Field
Type
Description
recipes[]
array
One or more recipes, max 50 per request
plan_date
string
Optional YYYY-MM-DD, defaults to today. Applies to recipes without their own plan_date.
A bare array, or a single recipe object, is accepted too — the
recipes wrapper is the clearest form.
Recipe object
Field
Type
Description
meal_category
string
One of the categories below
title
string
Dish name
image
string
Image URL
description
string
Short description
duration
string
e.g. "25 min"
difficulty
string
e.g. "Easy"
servings
number
Portions
ingredients
string[]
Ingredient list
steps
string[]
Preparation steps
tags
string[]
e.g. ["vegan","gluten-free"]
nutrition
object
{ calories, protein, carbs, fat }
Unknown fields are stored and handed back verbatim by the history
endpoint, so you can attach your own metadata.
Every dish ever sent to this user, with what they did about it. This is the durable
memory of the capability: it survives new pushes, reinstalls and a new laptop, and it is
what lets an assistant rebuild someone's taste from scratch.
Query
Description
state
liked · disliked · open · all (default)
date
Optional YYYY-MM-DD filter
limit
1–500, default 100
since
Cursor from a previous response — returns only what was added or decided since then, oldest first
Reading only what changed. Store the cursor from the response
and pass it back as since next time. You then get new suggestions
and decisions on older dishes — a heart tapped on last month's dinner shows up,
because the cursor tracks when something was decided, not when it was created. Keep calling
while has_more is true, each time with the newest cursor. Nothing is ever
deleted; you are simply not re-reading it.
The cursor is opaque — hand back exactly the string you were given.
Without since the response is the current picture, newest first.
summary always describes the whole picture (the whole
date scope, every state), never just the page you asked for — otherwise
?state=liked would report disliked: 0 and an assistant would read
the taste as one-sided. returned is how many dishes came back in this response.
State
Meaning
liked
The user tapped the heart — "I'll cook this today"
disliked
Explicitly rejected
open
Delivered but not decided yet
Errors
Status
Meaning
401
Missing X-API-Key header
403
Invalid API key
400
No valid recipe — missing title or invalid meal_category
500
Server error
# intouch api
base https://api.in-touch.cc
auth X-API-Key: itk_<64 hex> one key per user, no expiry, no scopes
docs https://docs.in-touch.cc
format every response is JSON and carries "success"
# recipes status: stable
Send meal suggestions into a user's feed, read back what they liked.
The user swipes through the suggestions per meal category; the ones they like
become a day plan. Loop: suggest, observe, suggest better.
# the daily loop
1. GET /api/addons/recipe/history?date=<today> are today's dishes already there?
2. GET /api/addons/recipe/history?state=liked what worked, what was rejected
3. POST /api/addons/recipe/suggest today's dishes per meal category
IDEMPOTENCY GUARD - this one matters more than in most APIs:
suggest APPENDS, it does not replace. If history for today already returns
summary.total > 0, STOP. A second run doubles every meal in the user's feed.
THERE IS NO SERVER-SIDE PROFILE. Diet, allergies, household size and the meals
the user wants live in the agent's own prompt file on the user's machine. The
server stores only the suggestions themselves and the like/dislike decisions.
# endpoints
POST /api/addons/recipe/suggest X-API-Key push suggestions
GET /api/addons/recipe/history X-API-Key read decisions
# POST /api/addons/recipe/suggest
body:
recipes[] array REQUIRED one or more recipes, max 50 per request
plan_date string YYYY-MM-DD, defaults to today; applies to
recipes without their own plan_date
A bare array or a single recipe object is accepted too; the "recipes" wrapper
is the clearest form.
# recipe object
meal_category string REQUIRED one of the categories below
title string REQUIRED dish name
image string image URL
description string short description
duration string "25 min"
difficulty string "Easy"
servings number portions
ingredients string[] ingredient list
steps string[] preparation steps
tags string[] ["vegan","gluten-free"]
nutrition object { calories, protein, carbs, fat }
Unknown fields are stored and handed back verbatim by history - use them for
your own metadata.
# meal_category (exact values)
breakfast brunch lunch dinner snack drink dessert sidedish
Anything else rejects that ITEM; the rest of the batch still goes through.
# example
curl -X POST https://api.in-touch.cc/api/addons/recipe/suggest \
-H "X-API-Key: itk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"plan_date": "2026-07-25",
"recipes": [
{ "meal_category": "breakfast",
"title": "Overnight Oats with Berries",
"image": "https://example.com/oats.jpg",
"description": "Creamy oats soaked overnight.",
"duration": "5 min + overnight",
"difficulty": "Easy",
"servings": 1,
"ingredients": ["50g oats","150ml milk","berries"],
"steps": ["Mix","Chill overnight","Top with berries"],
"tags": ["vegetarian","meal-prep"],
"nutrition": { "calories": 320, "protein": "12g",
"carbs": "48g", "fat": "8g" } }
]
}'
200 { "success": true, "inserted": 1, "rejected": 0, "plan_date": "2026-07-25" }
# GET /api/addons/recipe/history
The durable memory of this capability: every dish ever sent, with what the user
did about it. Survives new pushes, reinstalls and a new machine.
query:
state liked | disliked | open | all (default all)
date optional YYYY-MM-DD filter
limit 1-500, default 100
since cursor from a previous response: only what was ADDED or DECIDED after
it, oldest first. Opaque - hand back exactly the string you got.
curl "https://api.in-touch.cc/api/addons/recipe/history?state=liked&limit=50" -H "X-API-Key: itk_YOUR_KEY"
200 {
"success": true,
"state": "liked",
"summary": { "total": 42, "liked": 9, "disliked": 12, "open": 21,
"returned": 9 },
"cursor": "2026-07-26 18:04:11",
"has_more": false,
"dishes": [
{ "id": 123, "meal_category": "dinner", "plan_date": "2026-07-25",
"state": "liked", "recipe": { "title": "Oven Salmon", "...": "..." } }
]
}
# reading only what changed
Store "cursor" from the response, pass it back as ?since= next time, and keep
calling with the newest cursor while has_more is true. You get new suggestions
AND decisions on older dishes - a heart tapped on last month's dinner appears,
because the cursor follows when something was DECIDED, not when it was created.
Nothing is ever deleted; you are simply not re-reading it. Without since you get
the current picture, newest first.
summary always covers the WHOLE date scope and every state, never just the page
you asked for - otherwise ?state=liked would report disliked: 0 and you would
read the taste as one-sided. "returned" is how many dishes are in this response.
state meanings:
liked the user tapped the heart - "I'll cook this today"
disliked explicitly rejected
open delivered, not decided yet
# what lives where
server (durable, readable with the key, survives everything):
every suggestion, its meal category, its date, and liked/disliked/open
local prompt file ~/intouch/recipe-prompt.md (the user owns and can edit it):
the DISTILLED preferences - diet, allergies, household, the "learned:" lines
you write after reading the history, and "last read:" (your history cursor)
Rule: raw events belong on the server, conclusions belong in the file. Never
copy the whole history into the file; re-read it and rewrite the conclusions.
# offline behaviour
The app keeps the last plan it successfully loaded and shows it without a
connection. It falls back to that copy ONLY when the server is unreachable, so a
day you deliberately left empty stays empty instead of resurrecting yesterday.
# self-setup - read this when a user asks you to "set up my meal suggestions"
They pasted a short prompt and their key; the whole job is below. Work through it
in order and do not invent extra steps, files, services or repositories.
0. THE KEY IS A CREDENTIAL. Never print it, never echo it into a log, never put it
in the prompt file. It belongs in the guard script or an environment variable.
1. ASK - one message, with defaults so short answers are enough:
how many people you cook for, diet (omnivore / vegetarian / vegan / other),
allergies, hard no-gos, which meals per day (default breakfast, lunch, dinner),
how many options per meal (default 2), and how much effort on a weekday.
If the user already answered in their prompt, skip the questions.
2. WRITE THE PROMPT FILE - ~/intouch/recipe-prompt.md
This file IS the profile; the server stores none. Put the answers in a header
block ("cooking for", "diet", "allergies", "hard no", "meals per day",
"options per meal", "effort", plus empty "learned:" and "last read:" lines)
and below them the daily loop, the recipe object and the meal categories from
this page, so a future run needs nothing but this file. Every later run reads
only what is new (?since=<last read>), refreshes "learned:" and overwrites the
cursor. Show it to the user before writing it.
3. PUSH A FIRST DAY
Skip if history for today already has summary.total > 0.
One entry per meal category, "options per meal" each, respecting diet and
allergies absolutely. Read the response: fix rejected items and push ONLY
those again - suggest appends, so never resend the whole batch.
4. INSTALL THE DAILY AUTOMATION
Show the user the file paths and the schedule BEFORE creating anything, then:
a) the prompt file from step 2 is already in place.
b) ~/intouch/recipes.sh (chmod +x) - the guard:
- read GET /api/addons/recipe/history?date=<today, date -u +%F>
- API unreachable -> exit 0 (a machine that just woke has no network yet)
- 401/403 -> exit 1 loudly, the key is wrong
- summary.total > 0 -> exit 0, today is done
- otherwise run your own CLI headless with the prompt file
c) the scheduler that fits the user's OS, from "working snippets" below.
If the user also runs the vocabulary capability, these are two independent
jobs: keep the distinct names below so neither replaces the other.
5. VERIFY - run the guard script once by hand. It must report that today's
suggestions already exist, because step 3 created them.
6. HAND OVER - name the two files, the scheduler entry and the next run time, and
tell the user that changes ("more vegetarian", "no lunch", "I moved in with
someone") are a few lines at the top of ~/intouch/recipe-prompt.md - by talking
to you again, or in any text editor.
If you cannot run shell commands (browser chat, no terminal): do step 1, push a
first day (step 3), then tell the user plainly that the prompt file and the daily
schedule need Claude Code, Codex CLI or a cloud runner.
# self-setup: working snippets
Adapt paths and the CLI line; the shape is what matters.
--- ~/intouch/recipes.sh -----------------------------------------------------
#!/usr/bin/env bash
set -euo pipefail
export INTOUCH_API_KEY="itk_THE_USERS_KEY"
BASE="https://api.in-touch.cc"
TMP="$(mktemp)"; trap 'rm -f "$TMP"' EXIT
today="$(date -u +%F)"
# No network yet (the machine just woke up) is not an error. A rejected key is:
# it has to fail loudly, or the schedule quietly does nothing for weeks.
code="$(curl -sS -o "$TMP" -w '%{http_code}' \
-H "X-API-Key: $INTOUCH_API_KEY" \
"$BASE/api/addons/recipe/history?date=$today")" || {
echo "inTouch: API not reachable, leaving it for the next run"; exit 0
}
case "$code" in
200) ;;
401|403) echo "inTouch: key rejected ($code) - fix it in this script"; exit 1 ;;
*) echo "inTouch: API returned $code, trying again next run"; exit 0 ;;
esac
if [ "$(jq -r '.summary.total // 0' "$TMP")" -gt 0 ]; then
echo "inTouch: suggestions for $today already exist"
exit 0
fi
# your own CLI, headless, allowed to reach the network. For example:
# claude -p "$(cat "$HOME/intouch/recipe-prompt.md")" --allowedTools "Bash(curl:*)"
# codex exec --full-auto "$(cat "$HOME/intouch/recipe-prompt.md")"
------------------------------------------------------------------------------
--- macOS: ~/Library/LaunchAgents/com.intouch.recipes.plist --------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>com.intouch.recipes</string>
<key>ProgramArguments</key> <array>
<string>/bin/bash</string>
<string>/Users/YOU/intouch/recipes.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>7</integer><key>Minute</key><integer>0</integer></dict>
<key>RunAtLoad</key> <true/>
<key>StandardOutPath</key> <string>/tmp/intouch-recipes.log</string>
<key>StandardErrorPath</key> <string>/tmp/intouch-recipes.log</string>
</dict>
</plist>
launchctl load -w ~/Library/LaunchAgents/com.intouch.recipes.plist
StartCalendarInterval catches up after sleep; RunAtLoad adds a run at login.
------------------------------------------------------------------------------
--- Linux / VPS: systemd user timer ------------------------------------------
# ~/.config/systemd/user/intouch-recipes.service
[Unit]
Description=inTouch vocabulary set
[Service]
Type=oneshot
ExecStart=%h/intouch/recipes.sh
# ~/.config/systemd/user/intouch-recipes.timer
[Unit]
Description=Daily inTouch vocabulary set
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now intouch-recipes.timer
# VPS without a login session: loginctl enable-linger $USER
Persistent=true is the catch-up after a missed run.
------------------------------------------------------------------------------
--- Windows: Task Scheduler --------------------------------------------------
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-File C:\intouch\recipes.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
$set = New-ScheduledTaskSettingsSet -StartWhenAvailable -WakeToRun
Register-ScheduledTask -TaskName "inTouch Recipes" -Action $action `
-Trigger $trigger -Settings $set
-StartWhenAvailable is the catch-up. If the CLI lives in WSL, keep the bash
script and let the task call: wsl -e ~/intouch/recipes.sh
------------------------------------------------------------------------------
--- No local machine: GitHub Actions -----------------------------------------
# .github/workflows/recipes.yml (private repo, secrets for both keys)
name: inTouch Recipes
on:
schedule:
- cron: "0 5 * * *"
workflow_dispatch:
jobs:
push-set:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install -g @anthropic-ai/claude-code
- run: claude -p "$(cat recipes-prompt.md)" --allowedTools "Bash(curl:*)"
env:
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
INTOUCH_API_KEY: \${{ secrets.INTOUCH_API_KEY }}
Scheduled runs can start a few minutes late, and GitHub disables the schedule
after 60 days without repository activity.
------------------------------------------------------------------------------
# errors
401 missing X-API-Key header
403 invalid API key
400 no valid recipe: missing title or invalid meal_category
500 server error
# pages
/ overview, capability list
/quickstart get a key, first request
/authentication key format, rotation, failure modes
/conventions envelope, status codes, limits, dates
/recipes recipes capability
/vocabulary vocabulary capability
/roadmap capability anatomy, what is planned
every page is also plain text: append .txt (e.g. https://docs.in-touch.cc/vocabulary.txt)
all pages at once: /llms.txt (index) and /llms-full.txt (everything)
# for agents
- Use only the endpoints listed here; do not invent paths, fields or values.
- Check "success" in the body, not just the HTTP status.
- Error strings are German and developer-facing; branch on the status code.
- Batches are accepted partially: read "rejected" and "errors[].index".
- Dates are UTC. Calendar days are YYYY-MM-DD, timestamps are ISO 8601.
- The API key is a credential: never log it, never pass it to a third party.
- Ask the user before writing content into their app for the first time.