QidQuotes
Kid quotes in, watercolor storybook art out
The problem
Kids say incredible things, and they say them exactly once. You tell yourself you'll remember, and you don't. QidQuotes is a Telegram bot that lives in our family group chat: anyone types the quote as it happens, and a minute later the bot posts back an AI-illustrated watercolor quote card — with the child's actual face, their age down to the month, and the quote as a caption. Every quote and every image is archived automatically.
The whole backend is one Cloudflare Worker
There's no server, no database, and no monthly bill. A single Cloudflare Worker (free tier) handles everything, and Google Sheets is the store of record. The stack:
- Cloudflare Workers (TypeScript + Wrangler) — the entire backend
- Telegram Bot API — the user interface; the family group chat is the app
- Replicate → OpenAI
gpt-image-2(low quality) — image generation ($0.01 per image) - Google Sheets — the database (Sheets API v4, service account)
- Google Photos — permanent image archive (OAuth2 refresh token)
- Imgur — hosting for reference photos of each kid
- Google Apps Script — a "Rerun" menu inside the spreadsheet itself
The worker exposes three endpoints: the Telegram webhook (all messages and button
presses), a Replicate callback that fires when an image finishes generating, and
an authenticated /rerun endpoint the spreadsheet can call to bulk-regenerate
images for selected rows.
Capturing a quote
There are two ways in, depending on how much of a power user you are:
- Pre-tagged:
Ezra||I want to marry the moon— name, two pipes, quote. Add a date (Ezra|03/14/2026|…) or a scene hint in parentheses ((at the beach)) and the bot uses them. - Plain message: just type the quote. The bot replies "Who said this?" with one button per family member, then walks you through date (Today / Yesterday / custom) and one more question — was this an eggcorn, a kid-mangled word worth flagging? — before logging it.
Attach a photo and the bot does more: it uploads the photo to Imgur, saves it as a reference image for that child, and uses it for this generation. Attach a photo of someone the bot doesn't know and a "✏️ Custom" button appears — reply with a name and birthday, and a whole new person is added to the system, from inside the chat.
Making the picture
Once a quote is logged, the worker fires an async generation request to Replicate and returns immediately — Workers have CPU-time limits, so nothing blocks. The prompt builder does the interesting work:
- Reference faces: it picks a reference photo of the speaker (an uploaded photo wins; otherwise a random one from their stored set), and if the quote or scene hint mentions another family member by name, their photos get added too — so "Ezra said something about Aviya" renders both kids.
- Age math: if the speaker is a child with a known birthday, the card is attributed with their exact age — "4 years and 2 months old" — computed against the quote date in Chicago time. Adults (or unknown birthdays) get the date instead.
- Style: everything is rendered as a watercolor storybook illustration, with the quote and attribution composed into the image.
When Replicate finishes, it calls the worker back. The worker downloads the image, posts it to the Telegram group, uploads it to a Google Photos album for permanent keeping, and writes the final URL into the spreadsheet row.
Don't like it? Regenerate.
Every image the bot posts carries a 🔄 Regenerate button. Pressing it sends a preview strip of the child's reference photos with a picker: choose a specific photo, upload a brand-new one, or let it pick automatically. Each regeneration lands in the next empty column of that quote's spreadsheet row, so nothing is ever overwritten — the sheet accumulates every version.
And because Google Sheets is the database, the spreadsheet itself is also a control
surface: a small Apps Script adds a QidQuotes → Rerun selected row(s) menu,
which calls the worker's /rerun endpoint to regenerate any rows you
highlight — useful after improving the prompt.
The data model is two spreadsheet tabs
- Quotes: date, time, name, quote, eggcorn flag, scene hint, then image URLs marching rightward — one column per generation.
- Names: one row per person — name, birthday, their default scene hint, then reference photo URLs, also appended over time.
Scene hints are sticky: give one once ((in the bath)) and it's saved as
that person's default until replaced. All secrets — Telegram token, Google service
account key, Replicate token, Photos OAuth tokens — live as Wrangler secrets, never
in the repo.
Why I like this architecture
It's a full product — chat UI, async job queue, image pipeline, photo archive, admin panel — and every piece is something my family already uses or something free. Telegram is the frontend, Sheets is the database and the admin UI, webhooks replace the job queue, and the one piece of real code is a single TypeScript worker small enough to read in a sitting. Total infrastructure cost: the pennies Replicate charges per image.