GEORGE ANDRADE-MUÑOZSAN FRANCISCO --:--:-- · clear · 58°

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

asleepon the ops board →report filed 06 sept · 06:45

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

estimated 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 — is budgeted at fifteen to forty cents — a planning estimate, with every model call writing a priced row to a ledger so it can eventually be replaced by a measurement.

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 deterministic authority design — 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 — and it half held. The predictor has been measurable by default ever since; the prompt side turned out to be measurable only on paper, which is §03's newest entry.

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, newest first. The first entry has no fix yet — what broke was the instrument meant to catch the next one. The three June fixes under it are all load-bearing architecture now.

SEV-1

The nightly that graded nothing

What happened

No product code has moved since June 8th; what moved is what I know about it. A fresh-context critic read the repo against my eval standard and found the eval layer "well-designed on paper and has never actually run — the GitHub nightly has reported success 47/47 times while executing zero cases." The runner exits early and cleanly when there's no API key, which is right in pull-request CI where paid model calls have no business running. The scheduled job reuses that same entry point, and the repository secret was never set — so forty-seven nights printed a skip line, exited zero, and painted a green check beside twenty-eight labeled cases no model ever saw. Three findings outlive the missing secret: a blocked judgement degrades to a score of 0.0, which isn't neutral but maximal failure; every case runs once at the API's default temperature, so a single sampled mean is compared against a fixed 0.8 bar with no measured noise floor under it; and seller-written Amazon listing titles reach the model that proposes a household's pantry with nothing grading that seam.

The fix

None yet — the audit is the deliverable, and the repair is ten reviewed steps starting with the sensor: make a skipped run visibly not-a-pass, persist results so a baseline exists, then earn the threshold back with repeats. What the audit says to keep is the part built right on purpose: the judge suite sits alongside the deterministic groundedness checks instead of replacing them, and it never gates.

The lesson

A skip that exits zero is worse than a red build, because a red build gets fixed. Measure the measuring instrument first — everything downstream of a green check inherits whatever that check was actually doing.

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-01Repair the eval sensor first: make a skipped run visibly not-a-pass, persist results so a baseline exists, and earn the 0.8 threshold back with repeats instead of a single sample.
  2. NX-02Cases against the seam where seller-written product titles reach the model that proposes a pantry — the approval gate bounds the damage, but nothing grades the input.
  3. NX-03Budget envelopes — the monthly_budget_usd column exists; wire it so carts warn before they spend it.
  4. NX-04Learn consumption rates from confirmed purchases, not just declared habits — the order cadence is sitting in the events table.
  5. NX-05Risk-tiered auto-buy behind a deterministic policy gate. Cheap known staples self-approve; novel or expensive stays human.
  6. NX-06Gmail delivery tracking, so arrivals land on the real date instead of an estimate — and the 'did you order it?' tap disappears.