DoorDash (family ordering via dd-cli)
Order food for the Beck family: pick a restaurant, compose an order that covers everyone's
tastes + health + the kids' ages, build the cart with dd-cli, and place it only after
Idan approves the items, total, tip, card, and ETA.
Execution: dd-cli (installed + authed)
Binary at
~/.local/bin/dd-cli, signed in as idanbeck@gmail.com (v0.2.3). For command navigation see thedd-cli-usageskill; drill with--helpon demand.Always run
dd-cli --json-output <cmd>and read data from thestructuredContent.<field>object (e.g.structuredContent.addresses,.cards,.carts,.stores,.quote). Ignorewidget_type/assistant_instructions/ any "the widget above" text — terminal context, no UI.--intentis REQUIRED on every tool-backed command (v0.2.2 / CLI-401) —search,menu,cart *,order *,address list,payment-method list. Omit it and the command errors out. Use this exact value for a family dinner run (two lines, and reuse it verbatim across the whole pipeline so the session reads as one workflow):Summary: Help Idan order dinner delivery for his family at home user prompt/purpose: "<Idan's verbatim ask, e.g. order us food for the family tonight>"Shell-friendly:
INTENT=$'Summary: ...\nuser prompt/purpose: "..."'then--intent "$INTENT".🔒
--intentis sent to DoorDash and reviewed by them. NEVER put profile detail in it. DoorDash's own guidance says to exclude health/medical details and information about other people. That means no allergies, no Noa's iron requirement, no kids' names/ages, no "toddler"/"picky", no street address. Keep the food logic local to this skill and the helper — the intent line stays generic ("his family at home"). This is the one field that leaves the box.Auth (v0.2.3 — much better): desktop sessions now keep a refresh token in the macOS keychain and renew the access token automatically, so the old "re-login every time it expires" tax is gone. You should rarely see an auth error now. If one does appear ("missing credentials" / "Failed to authenticate"),
dd-cli loginis an interactive browser flow you cannot run — ask Idan to run! dd-cli loginhimself, then re-verify with a read-onlyaddress listbefore continuing. Don't burn turns retrying. (Headless alternative, not needed on this Mac:export-token→DD_CLI_ACCESS_TOKEN; refresh tokens are NOT stored in headless/cloud contexts.)Every subcommand — even
--help— requires credentials. Only the top-leveldd-cli --helpworks signed out. So a baredd-cli order status --helpfailing is an auth signal, not a missing-command signal.Location: the default delivery address is 2267 Shibley Ave, San Jose (Willow Glen), coords lat 37.285902 / lng -121.896708.
searchdoes not auto-use the saved default — resolve the default's lat/lng fromaddress listat order time (fall back to those coords) and pass--lat/--lngexplicitly.Payment: 3 cards on file.
order submitcharges the default card. The default is thestructuredContent.cards[]entry whosepayment_method_id== top-leveldefault_payment_method_id.
Hard rules (do not skip)
- Allergy gate. Read the profile. If the Allergies field is
TODO, stop and ask Idan to fill it (or setnone). The helper enforces this (checkexits 2). - Payment approval gate — mandatory. The pipeline is:
cart add-items→order preview(no charge, authoritative pricing + ETA) → show Idan the approval report (every line item + who it's for, subtotal, fees, tax, tip, TOTAL, ETA, and the named default card brand+last4) → get an explicit "approved" → only thenorder submit. Neverorder submitwithout that. order submitis DESTRUCTIVE + NON-IDEMPOTENT. It charges the default card immediately, and re-running with the samecart_uuidcreates a duplicate order. Run it exactly once. If you're unsure whether it went through, do not re-submit — recover theorder_uuidfromorder historyand checkorder status --order-uuid <uuid>. Capture theorder_uuidfrom the submit response immediately —order statusrequires--order-uuid; there is no "status of my last order" call.- Name the card + confirm the tip. Surface the default card (brand + last4) in the report.
--tip-centsdefaults to0(no tip) — always pass it explicitly; default is 15% (profile), in CENTS (--tip-cents 800= $8.00, not $0.08). If Idan wants a different card, the CLI can't swap it — route him toorder checkout-urlto finish in the browser. - Reconcile before submit. The report's numbers must come from
order preview, not guesses. If a fresh preview drifts materially from what was approved, re-approve. - Quote-matching flags.
--priorityand--no-apply-creditsmust be passed identically toorder previewandorder submit, or the amount charged won't match the amount approved. Default for family orders: pass neither (standard delivery, credits auto-applied). Same rule for--fulfillment— omit at submit to inherit the cart's mode, or pass the same value. - Never set
--include-work-benefitson a family order. Home delivery to Shibley is personal spend; work-benefits flags belong to Epoch/company orders only.
Data
- Profile (source of truth):
~/Library/Mobile Documents/iCloud~md~obsidian/Documents/idanbeck/Personal/Family Food Profile.md— tastes, allergies, per-person no-gos, health mode (balanced), budget (flexible, flag unusual), order-composition rules, restaurant list, and a machine-readable JSON block. - Helper (food-logic validator, no ordering/payment):
python3 ~/.claude/skills/doordash/doordash_skill.pyprofile— dump the parsed profile.schema— the order-JSON shape.check ORDER.json(orcheck -for stdin) — allergy gate + allergen scan + balanced/coverage/kid/iron checks + the approval report. Exit 0 = ready, 1 = blockers, 2 = refused (allergies unset / empty order).- ⛔ BLOCKERS (exit 1 — must fix, never order through them): allergen match · spicy assigned to a kid or Noa · no iron-rich item for Noa · a kid with no item of their own · a travels-badly item (soup dumplings/XLB) on a delivery order · no noodle/rice base. These are Idan's hard rules, not preferences.
- ⚠️ Notes (exit 0 — advisory): no veg, no protein, >1 fried, pricey item (>$30), high
total (>$150),
SPICE UNVERIFIED,AVOIDING(per-person soft avoid), and the standing Noa soft-food/choking reminder (can't be auto-verified — you must eyeball the actual dish). - 🌶️
SPICE UNVERIFIEDis the one to actually act on. A dish name does not tell you the spice level, and"table"dishes reach the kids. Added 2026-08-14 after Cashew Chicken and Mixed Vegetable with Thai Basil both arrived spicy with neutral names — the name-based spicy scan passed both. When you see it: request mild explicitly in the item options / special instructions, swap to a structurally-plain dish, or make it parents-only. - Per-person prefs are enforced from the profile JSON:
no_gos[]→ blocker,avoid_soft[]→ note, andprotein_preference[]is surfaced as the suggested swap order. Matching is substring-on-item-name, so only concrete food words work ("beef"✓,"adventurous"✗ — harmless no-op). - Fixed 2026-08-14: these hard rules were previously filed as soft notes, so
checkreturned "Ready"/exit 0 on an order that was spicy-for-Zev, iron-less, and left Sara nothing. Withallergies: noneon file, exit 1 was unreachable — the validator could not block at all.
- dd-cli commands used:
search·menu·cart add-items|show|remove-item|list|delete·order preview|submit|status|checkout-url|history·address list·payment-method list.
Flow
Set INTENT once at the top of the run and pass --intent "$INTENT" on every command below.
- Auth pre-flight:
dd-cli --json-output address list --intent "$INTENT". If it errors on auth, stop and ask Idan to run! dd-cli login. Everything downstream fails without this. 0b. 🔎 PULLdd-cli order historyBEFORE the vault's restaurant list — it is the real "historical".dd-cli --json-output order history --max 100 --days 365 --intent "$INTENT"→ countstore_namefrequency AND readorders[].items[]for the exact repeat dishes to reorder. The vault's Restaurants table is a hand-written summary; the order log is evidence, and on 2026-08-19 they disagreed badly — the KB pointed at Sushi Koya/Yuki while the actual #1 was Sushi Arashi (5 of 35 orders), with Akita and Harumi next and none of them in the KB. It also disproved a "not available locally" claim I'd written. Then fold in the profile for tastes/rules. ⚠️ Absence from DoorDash history ≠ not a favorite — Kazoo is a dine-in Japantown regular with zero delivery orders. If Idan names a place you don't see,searchfor it; don't doubt him. - Read the profile:
python3 doordash_skill.py profile. Confirm allergies are set. Note tastes/no-gos, health mode, budget, the kids (Sara 7, Zev 5 — picky/plain, Noa ~16mo — toddler, iron-priority), and the restaurant list. (Local only — never goes into--intent.) - Resolve location: from the step-0
address list→ theis_defaultentry's lat/lng (currently Shibley, 37.285902 / -121.896708). Scan the fulladdresses[]— the default can be anywhere in the list. - Pick the restaurant: the one Idan asked for, or propose 1–2 from the profile. Rotate cuisine vs the Hits log. Authentic only (no American-Chinese — "we don't like the fake stuff").
- Find the store:
dd-cli --json-output search -q "<cuisine>" --lat 37.285902 --lng -121.896708 --limit 8 --intent "$INTENT"→structuredContent.stores[].store_id. Pre-flight:dd-cli --json-output cart list --intent "$INTENT"— only one open cart per store is allowed, so if one already exists there, extend it (reuse itscart_uuid) orcart deleteit; don't silently stack a new one. 4b. ⏱️ IF TIME MATTERS, CHECK ASAP FIRST.searchreturns no ETA. A store that only does scheduled windows is indistinguishable from a 20-minute store until you preview a built cart. Build a minimal cart →order preview→ confirmquote.delivery_availability.asap_availableistrueand readasap_minutes_range_string. Then hone the items. (2026-08-14: composed a full IZAKA-YA order before discoveringasap_available: false, earliest window 8:10 PM on a 7:20 PM order — total rebuild under time pressure.) Abandoning a store?cart delete --cart-uuid. - Read the menu:
dd-cli --json-output menu --store-id <id> --intent "$INTENT"→structuredContent.menu_id+items[].item_id+ prices. Use real names / prices / ids — don't invent them. Ignoreis_popular/popularity_rank— DoorDash says that data isn't decision-grade right now. 5b. Assign proteins per person BEFORE composing. When a dish takes a protein choice, read the person'sprotein_preference[]from the profile. Stacey → seafood → tofu → chicken; beef last and generally not at all (and never low-grade Thai-restaurant beef — that's what made 2026-08-14 a miss). Don't let one protein dominate the whole order. 5c. 📏 SIZE FOR THE ADULTS. Idan and Stacey eat the majority of every order; the kids barely dent theirs. A long item list is NOT the same as enough food — the 2026-08-19 Kazoo order ran 15 items at a nominal +40% and both adults were still hungry with almost no leftovers. When adding the leftover buffer, add substantial adult mains (donburi, chirashi, entrées, combo boats), never more sides or small plates. 5d. 🔍 Judge the PORTION, not the name or the price. "Poki Salad" at $11.45 read like a full salad and arrived as an appetizer. Anything from an appetizer/small-plate section is a side, full stop. And never double up on one ingredient — tamago nigiri plus a tamago omelet meant two egg dishes and neither got eaten. - Compose the order per the profile's rules: protein + veg + starch; a noodle/rice base; a safe item for each kid (Zev = plain: fries/nuggets/plain noodles/cheese pizza); a soft, iron-rich, no-choking option Noa can share (soft beef / dark-meat chicken / tofu / lentils / spinach) ideally with a vitamin-C side; ≤1 fried; spicy = parents only; order +10–20% extra for leftovers; skip items that travel badly (soup dumplings) for delivery.
- Build the cart:
dd-cli --json-output cart add-items --store-id <id> --menu-id <menu_id> --items-json '[{"item_id":"...","item_name":"...","quantity":1}, ...]' --intent "$INTENT"→structuredContentreturns thecart_uuid. Keep passing thatcart_uuidon further calls. Adjust withcart show --cart-uuid <u>/cart remove-item --cart-uuid <u> --cart-item-id <n>(note:cart_item_idis the cart-lineid, notitem_id). Item customizations go innested_options[](each needsid+name+quantity). Don't pass--group-cart— that's a shareable multi-participant cart, not what a family dinner on one card needs.- Expect partial failures. Many restaurant mains have a required option group
(
Protein Choice) and error withitem_validation_error. The error payload returnsitem_errors[].required_options[]with every option'sid/name/price— read it and re-add those items withnested_options, appending via--cart-uuid. Don't re-fetch the menu. - Always check
item_errorsand theCart now has N itemscount —cart add-itemsreports partial success ("partially succeeded") and returns exit 0. Silently missing items is the failure mode. - Search dish names loosely/phonetically. Menus spell things their own way — Pad See Ew was
listed as
#132. Pad Se Ew. A strict regex on the spoken spelling finds nothing and you'll wrongly conclude the restaurant doesn't carry it. - ⚠️
menureturns a CAPPED ~150 items. Items you have genuinely ordered before can be absent from the response (Ikura and Chicken Katsu were missing for Arashi despite appearing in past orders). Never conclude "they don't carry it" frommenualone — cross-checkorder history, and search names loosely (nigiri is often listed as"<Fish> (2 Pcs)"). - Parse menus with Python
json, notgrep -oE— a regex sweep over a ~200KB menu JSON can backtrack catastrophically and hang for minutes. - Confirm a specifically-requested dish exists BEFORE committing to a store (Zev's satay wasn't on any of Thai Spice's 123 items). Check, then pick the restaurant.
- Expect partial failures. Many restaurant mains have a required option group
(
- Validate food logic: write the order JSON (names/prices from the menu; see shape below) and
run
python3 doordash_skill.py check order.json. Fix blockers (exit 1) / refusals (exit 2). - Preview (authoritative, NO charge):
dd-cli --json-output order preview --cart-uuid <u> --intent "$INTENT"→ read the real subtotal, fees, tax, and ETA fromstructuredContent.quote/delivery_availability. Compute the tip (15% default). Pass no--priority/--no-apply-credits(see rule 6). - Show Idan the approval report — items + who (from your composition) with the real preview numbers + tip + grand total + ETA + the named default card. Wait for an explicit "approved." (He may edit — adjust the cart and re-preview.)
- Submit (charges default card, once):
dd-cli --json-output order submit --cart-uuid <u> --tip-cents <cents> -y --intent "$INTENT"Immediately recordorder_uuidfrom the response, then confirm withdd-cli --json-output order status --order-uuid <order_uuid> --intent "$INTENT". Never re-run submit. If the response was lost, find the order viaorder history --intent "$INTENT"and check its status — do not submit again.- 🔴 v0.2.3
order statusschema — VERIFIED 2026-08-19 against a real past order. The status field MOVED intoresultand the vocabulary CHANGED:
☠️ This is a silent-failure trap. v0.2.2 code readingstructuredContent.success true structuredContent.message "Order Complete" structuredContent.result.status "completed" ← was "successful" at TOP level structuredContent.result.action_required false structuredContent.result.merchant_name "Thai Spice" structuredContent.result.is_pickup false structuredContent.result.quoted_delivery_time / actual_delivery_time structuredContent.result.estimated_pickup_time / actual_pickup_time structuredContent.result.delivery_window_start / delivery_window_end structuredContent.result.eta_trend (running-late trend, null when on time) structuredContent.result.late_reason / cancellation_reason / status_message structuredContent.result.status_updated_atstructuredContent.statusnow getsNone, and"successful"no longer appears anywhere — so a naivestatus == "successful"check reads as "the order did NOT go through" on an order that went through perfectly. Combined with a non-idempotentorder submit, that could tempt a DUPLICATE CHARGE. Readresult.status, and treat an unrecognized value as UNKNOWN — never as failure, and never as grounds to re-submit. When in doubt,order history. Timestamps are UTC (Z) — convert before showing Idan a local time. - Because status is now richer,
order statusis worth re-polling to answer "where is it?" rather than only as a one-shot did-it-go-through check.
- 🔴 v0.2.3
- Log it: append a row to the Hits & Misses table in the profile (date, restaurant, what was ordered, later the verdict). Add good restaurants to the Restaurants table.
Order JSON shape (for the helper)
{
"restaurant": "Mian Sichuan Noodles & Dumplings",
"eta_minutes": 40,
"items": [
{"name": "Dan Dan Noodles (from menu)", "for": ["Idan", "Stacey"], "price": 13.95, "qty": 1, "tags": ["starch"]},
{"name": "Steamed Rice + shredded chicken", "for": ["Zev", "Noa"], "price": 4.00, "qty": 1, "tags": ["starch", "protein"]},
{"name": "Sautéed Spinach with garlic", "for": ["Noa", "table"], "price": 9.50, "qty": 1, "tags": ["veg", "protein"]}
],
"fees": {"delivery": 3.99, "service": 4.20, "tax": 3.10, "tip": 8.00}
}
for = who it's for (names, or "table" for shared). tags help the coverage check
(protein/veg/starch/fried); names are auto-tagged. Prices/ids come from dd-cli menu;
fees/tax/ETA come from dd-cli order preview — not guesses.
Notes
- JSON envelope: every
--json-outputresponse wraps data instructuredContent; read from there and ignorewidget_type/assistant_instructions. --intentis required everywhere (v0.2.2) and is transmitted to DoorDash — generic only, no allergies / medical / kids' details / address. See the Execution section for the exact wording.searchneeds explicit--lat/--lng— it does not auto-use the saved default address. Without them it falls back toDD_LAT/DD_LNGand finally a Cupertino default — wrong city, wrong stores. Always pass them.order submitis silent-charge + non-idempotent — approval gate, run once, captureorder_uuid, verify withorder status --order-uuid <uuid>. A different card requiresorder checkout-url(browser).order statusoutput shape changed in v0.2.3 — re-verify the field names on first live use.- New in v0.2.3 (verified):
address find --query "<address>"resolves free text to candidates,address add --place-id <id>saves it as the default (⚠️ does NOT dedupe — runaddress listfirst), andaddress setchanges the default among saved ones. Alsoorder history --include-group-order(plus--max1-100,--daysdefault 90 / cap 365). --tip-centsdefaults to 0 — silently no tip if you forget it. Always pass it.- Version note: now on v0.2.3 (2026-08-19, checksum-verified). Backups:
~/.local/bin/dd-cli.v0.2.2.bak,dd-cli.v0.2.0.bak.- ⚠️ v0.2.3 changed PACKAGING to a PyInstaller onedir bundle. The binary alone is NOT
runnable — it needs a sibling
~/.local/bin/_internal/directory (~19MB). Install by running the archive'sbash install.sh, never by copying just the binary (that fails withFailed to load Python shared library ... _internal/libpython3.13.dylib). Same applies to any future upgrade. - 🔴 v0.2.3
order statusresponse shape changed and is explicitly NOT backward-compatible. - Also new in 0.2.3:
address find/address add,order history --include-group-order, and automatic keychain token refresh. - v0.2.2 brought mandatory
--intent, headlessDD_CLI_ACCESS_TOKEN, group carts,--priority,--no-apply-credits.
- ⚠️ v0.2.3 changed PACKAGING to a PyInstaller onedir bundle. The binary alone is NOT
runnable — it needs a sibling
- Toddler safety for Noa is non-negotiable: no whole grapes/nuts/hard chunks/popcorn; nothing spicy; soft small pieces she can gum.
- Budget is flexible — don't optimize for cost, but flag anything unusually pricey (the helper flags items > $30 and totals > $150).
- Groceries/retail (not restaurants): use
find-nearby-stores/find-items/build-grocery-list, notsearch. - Keep the profile current: when Idan reacts to a meal, update the per-person Loves/No-gos and the Hits & Misses log so the skill learns.
- Cold-start stall: the profile lives in iCloud Drive. If it's been evicted, the first read blocks for a minute-plus while iCloud materializes it — it looks like a hang but isn't. Warm it early (step 1) rather than mid-checkout, and don't kill it and retry.