/projects/grocery-buddy — n°004 — HOME OPS · COMMERCE — LIVE
Grocery Buddy
A pantry that runs itself over Telegram — predicts what's low, builds the Amazon cart, stages checkout, and hands a human the final tap. Scaffold to live on Fly.io in seven days.
Role
Solo — design, engineering, evals, ops
Stack
Claude Haiku 4.5 + Sonnet 4.6 · Temporal · FastAPI · Postgres · Playwright · Telegram
Timeline
Jun 2026 — operating
Status
LIVE
7
days, scaffold → live
$0.15–0.40
cost per run
0
orders placed by the agent
Brief
#Groceries are a standing tax on attention: notice what's low, remember the brands, rebuild the same cart, every single week. Grocery Buddy collects that tax instead. It lives entirely in Telegram — no app, no dashboard — watches the pantry, predicts what's running out, prices a cart against live Amazon, and parks everything behind one approval message.
Two invariants hold everywhere. The agent never places an order — it stages the cart and hands back a checkout link; the human taps "Place order." And nothing touches the pantry or a cart without the user's say-so — every purchase passes an approval gate, and imports stage a proposal before a single row is written.
It went from empty repo to live on Fly.io in seven days, and it has been running my household since. A full run — prediction, pricing, briefing — costs between fifteen and forty cents.
Here's one of those runs, end to end — the automation made visible:
One run, while you sleep
pantry — days_left = qty ÷ rate
| = the 3-day line — lead 2 d + buffer 1 dwatching · next tick 13:00 utc
granola (filler — rides along for free shipping) $5.49
telegram
• eggs
• whole milk
• coffee
Cart total: $25.66
— durable wait · up to 24 h —
Nothing's been bought yet — tap Open my Amazon cart and finish checkout.
staging — the real cart, item by item
the cart fills itself · checkout is your phone, your tap, your card
where this goes nextv1.0 — live
today — predicts, prices, stages — you tap Place order. running my household since jun 2026
budget envelopes — monthly_budget_usd is already in the schema; carts warn before they spend it
learned rates — consumption learned from confirmed orders — the cadence sits in the events table
delivery tracking — gmail arrival dates land restocks on the real day; the “did you order it?” tap disappears
tiered auto-buy — procurement agent's authority design, reimplemented locally — cheap staples self-approve, novel or expensive stays human
every rung still ends at a human tap until the five-condition money-live gate — precision ≥ 0.70, cost ceiling, scraper green — says otherwise. autonomy is earned with evals, not vibes.
green = operating · outline = the model has no say — code or a human decides
The arithmetic is real and running: days_left = qty ÷ rate with the repo's 2-day lead + 1-day buffer executes on this pantry in your browser. The cron spec, activity sequence, durable waits, message templates and the hardcoded checkout_verified=False hard stop are quoted from the code; the roadmap is the repo's own — the monthly_budget_usd column already exists. The pantry contents, prices and the staging scene are representative.
The system
#Two kinds of brains, deliberately separated. Claude owns language: reading intent, composing briefings, judging which Amazon listing matches "the usual milk." Temporal owns time and money: schedules, durable 24-hour approval timers, retries, and exactly-once movement at every edge that matters. When language is unsure it asks the user; when orchestration is unsure it replays deterministically.
outlined = where the model has no say — code or a human decides
Telegram is the whole interface. Claude reads and writes language; Temporal owns every consequence.
The pantry is one picture of on-hand plus on-the-way: confirm an order and its items count as covered stock — the agent won't re-suggest eggs that are already in a van. Prediction is deliberately boring math, days_left = (qty + incoming) / daily_rate, debuggable in one line; the models never guess at arithmetic.
How I built it
#The week was ordered by risk, not by feature list. Day one was scaffold, schema, Temporal, and — before any feature worked — the evals wiring: Langfuse tracing, a cost ledger that prices every model call, and a prediction_snapshots table so the predictor's decisions could be graded later. Instrumentation first is the cheapest decision in the whole build; everything after it was measurable by default.
The middle of the week went to the two hard surfaces. Onboarding imports two years of Amazon order history: a Playwright scrape, then pure-Python aggregation, then the system's single Sonnet call turns the pile into a pantry proposal — habits learned from every order, on-hand estimated honestly (nobody has three-month-old milk; a stale order teaches the habit while contributing zero stock). The user edits the proposal conversationally and nothing is written until they confirm.
The other surface is staying signed in to a website that doesn't want robots. When the session expires, the agent re-authenticates itself: a bounded state machine drives the sign-in form, every field write is read back and verified, and if Amazon asks for a one-time code, the worker opens a challenge row and asks me, over Telegram — then consumes the reply and finishes the login. The browser holds the page; the human holds the secret.
05:00:00guardrails: no pending cart · cooldown clear → run05:00:01reconcile_arrivals_activity → 1 order landed, pantry topped up05:00:01apply_estimated_depletion_activity → on-hand estimates advanced05:00:02select_run_candidates_activity → 4 must-buy + 2 fillers to clear free shipping05:00:09[brand-select · haiku] "2% milk" → preferred brand, 64 oz · conf high05:00:14lookup_amazon_prices → 6 items priced against the live store05:00:15assemble_run_cart_activity → $43.87 · fillers trimmed at threshold05:00:16[briefing · haiku] composed — grounded on exact names and prices05:00:16cart parked: pending_approval · timer 24h · nothing ordered07:42:03approve ← telegram button07:42:19prepare_checkout_activity → cart cleared, staged by ASIN, link sent07:42:19awaiting "I placed the order" — checkout belongs to the human
- Jun 02v0.1
Scaffold, schema, evals wiring — same day
Langfuse, the cost ledger, and prediction snapshots existed before the first cart did.
- Jun 03–04
Amazon auth edge cases + import, phase 2
The state-machine sign-in and the Telegram 2FA relay earn their keep.
- Jun 07v0.6
In-transit replenishment lifecycle
Confirm → on-the-way → restock on arrival. The pantry stops double-suggesting.
- Jun 08v1.0
Resilience layer, evals rebuilt, live on Fly.io
Selector self-repair, synthetic health probe, cost telemetry. Operating since.
What broke
#Filed candidly — every fix below is now load-bearing architecture.
The import that returned nothing
What happened
Two-plus years of order history hit the synthesis call as ~150 raw rows. The forced tool call overflowed max_tokens and returned unparseable, empty proposals — silently.
The fix
A pure-Python pre-aggregation pass collapses orders to one record per product (times ordered, units, recency) before Sonnet sees anything. stop_reason and item counts are logged so truncation can never be quiet again.
The lesson
Don't make a model read what code can compress. Aggregate first; spend the model on judgment, not parsing.
Silent selector rot
What happened
Amazon churns its UI. A renamed CSS class returns an empty list — which a scraper swallows at logger.warning — so pricing and import quietly under-deliver while looking healthy.
The fix
Layered element resolution (CSS chains plus role/ARIA fallbacks), LLM-assisted repair that runs only on a total zero-match and caches what it rediscovers, and a synthetic health probe that searches known staples and pages me on the first regression.
The lesson
To a scraper, an empty list isn't an error. Make silence page you.
Two ways to buy twice
What happened
Stale items could survive in the Amazon cart across runs and ride into the next checkout; separately, two concurrent runs could both pass the pending-cart guardrail on stale reads.
The fix
Clear-first staging — the cart is emptied before items are staged by ASIN, idempotent on a per-run key — and guardrails that check-and-write atomically with a per-user cooldown.
The lesson
Anything that touches money is exactly-once or it doesn't ship. There is no mostly-once.
Decisions & tradeoffs
#Temporal, not a job queue
- Chose
- Durable workflows for every consequence: schedules, 24-hour approval timers, retries, exactly-once activities.
- Over
- Cron plus a queue, or stretching the agent framework into an orchestrator.
- Cost
- Real infrastructure to run and learn, for a one-household product.
- Payoff
- An approval can sit for a day, the worker can die mid-run, and nothing double-fires. The money path replays deterministically instead of guessing.
The agent never places the order
- Chose
- Staged cart + checkout link; the human owns the final tap, indefinitely.
- Over
- Auto-purchase — which every demo of this product category pretends is fine.
- Cost
- One tap of friction per week, forever. The demo is less magic.
- Payoff
- The blast radius of any bug is an unwanted cart, never an unwanted charge. Autonomy waits behind a gate with five conditions, not a vibe.
Haiku everywhere, Sonnet exactly once
- Chose
- Haiku for intent, replies, briefings, brand selection — six call-sites. Sonnet only where two years of messy orders become a structured pantry.
- Over
- The big model everywhere.
- Cost
- Occasional escalations and a quality ceiling on chat nuance.
- Payoff
- Full runs cost $0.15–0.40, which is the difference between a product and a party trick.
Playwright over waiting for an API
- Chose
- A persistent authenticated browser profile, request interception over HTML scraping, and a self-healing login.
- Over
- Pretending Amazon's consumer API exists. It doesn't.
- Cost
- A genuinely hostile surface that demanded its own resilience layer.
- Payoff
- The product exists. And the resilience layer — probe, repair, health gate — turned out to be half the engineering story worth telling.
What's next
#- NX-01Budget envelopes — the monthly_budget_usd column exists; wire it so carts warn before they spend it.
- NX-02Learn consumption rates from confirmed purchases, not just declared habits — the order cadence is sitting in the events table.
- NX-03Risk-tiered auto-buy behind a deterministic policy gate — Procurement Agent's authority design, reimplemented locally. Cheap known staples self-approve; novel or expensive stays human.
- NX-04Gmail delivery tracking, so arrivals land on the real date instead of an estimate — and the 'did you order it?' tap disappears.