ANDRADE-MUÑOZSAN FRANCISCO --:--:-- · overcast · 13°

/projects/grocery-buddy — n°004HOME OPS · COMMERCELIVE

asleep1 worker session · 299 edits building · 61 checkson the ops board →report filed 07 jul · 06:52

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

00

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

schedule "0 13 * * *" utc → GroceryRunWorkflow · trigger=schedule · cooldown 3 h

pantry — days_left = qty ÷ rate

eggs3 eggs ÷ 1/d = 3.0 d
whole milk0.5 gal ÷ 0.25/d = 2.0 d
coffee0.15 lb ÷ 0.06/d = 2.5 d
rice8 cups ÷ 0.4/d = 20.0 d
paper towels6 rolls ÷ 0.5/d = 12.0 d

| = the 3-day line — lead 2 d + buffer 1 dwatching · next tick 13:00 utc

cart — 3 must-buy + 1 filler$25.66 ≥ $25

granola (filler — rides along for free shipping) $5.49

telegram

Looking at your pantry, I'd grab:
eggs
whole milk
coffee
Cart total: $25.66
✅ Looks good❌ Skip

— durable wait · up to 24 h —

🛒 Your cart's ready — $25.66
Nothing's been bought yet — tap Open my Amazon cart and finish checkout.
🧾 Open my Amazon cart✅ I placed the order
ordered
📦 in-transit → eta set → pantry topped up on arrival

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.

01

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.

grocery buddy — at a glance
Telegramthe entire UIWebhookfastapi · decision treeTemporal3 workflows · durable timersPredictorrule-based · no llmActivities ×20all I/O lives hereAmazonplaywright sessionPostgres18 tables · supabaseApproval gatehuman · every cartCheckout linkhuman places the ordermessagestart · signalrunpredictstage cartread · writebriefingapprove

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.

02

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.

GroceryRunWorkflow — friday 05:00, trigger: schedulesession
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
  1. Jun 02v0.1

    Scaffold, schema, evals wiring — same day

    Langfuse, the cost ledger, and prediction snapshots existed before the first cart did.

  2. Jun 03–04

    Amazon auth edge cases + import, phase 2

    The state-machine sign-in and the Telegram 2FA relay earn their keep.

  3. Jun 07v0.6

    In-transit replenishment lifecycle

    Confirm → on-the-way → restock on arrival. The pantry stops double-suggesting.

  4. Jun 08v1.0

    Resilience layer, evals rebuilt, live on Fly.io

    Selector self-repair, synthetic health probe, cost telemetry. Operating since.

03

What broke

#

Filed candidly — every fix below is now load-bearing architecture.

SEV-2

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.

SEV-1

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.

SEV-1

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.

04

Decisions & tradeoffs

#
Decision01

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.
Decision02

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.
Decision03

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.
Decision04

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.
05

What's next

#
  1. NX-01Budget envelopes — the monthly_budget_usd column exists; wire it so carts warn before they spend it.
  2. NX-02Learn consumption rates from confirmed purchases, not just declared habits — the order cadence is sitting in the events table.
  3. 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.
  4. NX-04Gmail delivery tracking, so arrivals land on the real date instead of an estimate — and the 'did you order it?' tap disappears.
SHIPProcurement Agentthe convergence decision: shared design DNA, deliberately zero shared code