DoorDash ordering that actually respects your macros.
Tell MacroMe your calorie and protein goals, meal times, budget, and food prefs once. When a meal is due, it opens a real browser, hunts nearby restaurants, builds a cart that fits the slot, and waits for you to approve before anything is charged. Leave it running and it’ll kick off meals on schedule.
2nd place in the WebAgent track at Battle of the Schools (UTMIST × Wat.ai, UofT vs Waterloo). View Devpost
Built with a Node web app, Steel cloud browser, Playwright, OpenRouter for meal picking, a local USDA nutrition DB, and plain JSON files on disk.
- Setup wizard for macros, meals, schedule, budget, addresses, and preferences
- Dashboard with upcoming orders, Run now, live browser view, and cart approval
- Scheduler that fires meals at each order time from the same
npm run devprocess - Live Today macro/spend summary, plus Final end-of-day digests (dashboard and optional email)
- Missed meals recorded when the laptop was asleep — never ordered late
- Optional MCP tools so Cursor, Claude Code, or Codex can read today’s summary or email a digest
Nothing is charged until you approve the verified checkout; the agent then presses DoorDash’s Place Order button. Explicit demo mode does not charge.
Browser UI (wizard / dashboard)
│
▼
server.js ── cron every minute ──▶ order queue, agent run, day log, digests
│
▼
macrome-config.json ──▶ plan (macros, times, budget, prefs)
│
▼
doordash-macro-agent ──▶ Steel session → scrape menus → pick meal → cart → approval
macrome-config.json is your plan. Queue state, day log, and digests live in separate files so a restart still knows what already happened.
This is the supported path today. MacroMe wants a long-lived Node process on your machine.
npm install
npm install --prefix doordash-macro-agent
npm run join-nutrition-db --prefix doordash-macro-agent # builds local nutrition.db
cp doordash-macro-agent/.env.example doordash-macro-agent/.envThe server loads both:
- repo-root
.env doordash-macro-agent/.env(wins on duplicate keys)
Don’t commit .env files.
Required for live DoorDash ordering
| Variable | Purpose |
|---|---|
STEEL_API_KEY |
Cloud browser (steel.dev) |
OPENROUTER_API_KEY |
Meal picking + checkout recovery (openrouter.ai) |
STEEL_PROFILE_ID |
Saved DoorDash login from npm run setup-profile |
Without these three you can still edit the plan, but Run now / scheduled live orders will fail.
Optional
| Variable | Purpose |
|---|---|
RESEND_API_KEY + DIGEST_EMAIL |
Email Final digests (resend.com) |
MACROME_EMAIL_FROM |
Custom From (needs a verified Resend domain for non-account inboxes) |
MACROME_MODEL |
OpenRouter model id (default: free Nemotron). Example: anthropic/claude-sonnet-4 |
MACROME_DEMO_PLACE |
Set 1 for an explicit demo without charging; default 0 uses real checkout after approval |
MACROME_MAX_STORES |
Menus to compare (default 3; use 1 for faster demos) |
MACROME_SEARCH_BUDGET_SECONDS |
Discovery time budget (default 240; try 120 for demos) |
MACROME_MAX_MENU_ITEMS |
Items sent to the picker (default 20) |
MACROME_SESSION_TIMEOUT_MINUTES |
Steel session length (Launch is usually capped at 15) |
MACROME_DIGEST_TIME |
Force EOD as HH:MM in the plan timezone |
MACROME_DRY_RUN |
1 = mock menu, no Steel / no real order |
MACROME_NO_CRON |
1 = this process does not own the minute clock |
PORT |
Web port (default 3000) |
npm run setup-profileLog in in the live browser, then paste the printed STEEL_PROFILE_ID into .env. Re-run when DoorDash logs you out.
npm run devOpen http://localhost:3000.
- Leave that process running if you want scheduled meals to fire (don’t sleep the machine).
- Finish the wizard, or go straight to the dashboard if a plan already exists.
- Use Run now or wait for a scheduled slot; approve in the dashboard.
- Node deps installed (root + agent)
-
nutrition.dbjoined - Steel + OpenRouter keys set
-
setup-profiledone andSTEEL_PROFILE_IDsaved -
npm run devleft running - For real charges: a valid DoorDash payment method and
MACROME_DEMO_PLACE=0(the default) - For email digests: Resend key +
DIGEST_EMAIL
| State | What you see |
|---|---|
| No plan yet | Onboarding wizard (macros, meals, budget, days, addresses, prefs) |
| Plan saved | Dashboard: plan, next orders, Run now, live browser, Today / digests |
Open /setup anytime to edit the plan.
Run now starts the next upcoming meal. Scheduled meals use the same approval flow.
You’ll see cart lines, prices, DoorDash’s checkout total, and the Agent pick. Place order confirms the whole cart. Don’t order cancels without charging.
Real checkout is the default. After a verified cart and total are shown, approving presses DoorDash’s Place Order button. Set MACROME_DEMO_PLACE=1 only for a demo; it records a verified cart as placed without charging DoorDash. A recommendation without a verified cart cannot be approved or recorded as placed.
Today is a live summary until a Final is saved. A Final is written when:
- end-of-day time hits (default: ~90 minutes after your last meal), or
- every planned meal for that day is placed (for example 3/3)
Empty days with no meal outcomes don’t get a Final. Earlier days stay under Earlier days after restart.
If email is configured, a new Final is emailed automatically (and can be sent early via MCP or the API). In demo place mode, the same day can be emailed again after you reset local state.
Meals whose order time already passed are marked missed. They show up in the day summary; they are not ordered late.
Personal state stays on disk and is gitignored:
| File | Contents |
|---|---|
macrome-config.json |
Plan: macros, meals, schedule, budget, addresses, prefs |
macrome-queue-state.json |
Completed / missed / attempted slots + last cron tick |
macrome-day-log.json |
Per-meal outcomes |
macrome-digests.json |
Saved Final summaries |
macrome-scheduler.lock |
Which process owns the minute clock |
Upcoming orders are computed from the plan each minute. Only lifecycle state is stored, so restarts are cheap.
Reset a demo day (keep the plan): delete the day log, digests, and queue state files (or clear their contents), and set queue lastSeenAt to now so past breakfast slots aren’t all marked missed again.
| Command | What it does |
|---|---|
npm run dev |
Website + schedule clock → http://localhost:3000 |
npm run setup-profile |
One-time DoorDash login for Steel |
npm run agent |
One order from the terminal |
npm run schedule |
Clock alone (only if you don’t want it inside dev) |
npm run mcp |
MCP server on stdio (optional) |
npm run seed-demo -- --digest |
Fake day data for digest UI demos |
MACROME_DRY_RUN=1 npm run dev |
Practice mode, no live browser |
npm test |
Queue, digests, scheduler, server tests |
npm run mcp| Tool | What it does |
|---|---|
get_today_summary |
Live or Final summary for a day (read-only) |
list_digests |
Saved Finals, newest first |
send_eod_digest |
Save Final if needed, then email it |
Configs in-repo: .cursor/mcp.json, .mcp.json, .codex/config.toml. Point another project at MacroMe with absolute paths and cwd set to this repo. MCP does not keep the scheduler alive; npm run dev still must be running for orders.
The Codex MCP configuration uses this checkout's absolute launcher and working directory. If you move the checkout, update both paths in .codex/config.toml. The launcher resolves nvm's Node with a minimal PATH and keeps startup diagnostics on stderr.
On Ubuntu, run bash scripts/setup-sandbox.sh in a terminal with sudo access to install bubblewrap and verify user-namespace startup. On Ubuntu 24.04 the script installs the additional AppArmor profile only if the initial check fails. It follows the official sandbox prerequisites and does not disable AppArmor globally.
Required food options are selected within the item dialog using the meal goals, dietary restrictions, avoided ingredients, and food budget. Documented dairy-free choices take priority; unknown ingredients need whole-meal validation, and optional extras stay unchanged. Unresolved choices stop the add and appear in the HTML/JSON decision report. Nutrition remains labeled as an estimate unless a matching whole dish is verified; a condiment's USDA serving cannot verify a restaurant burrito.
Cart confirmation requires matching item lines, quantities, and selected modifiers. A timed-out or uncertain add closes its browser tab and reconciles through a fresh cart inspection before another mutation. An unreadable cart or failed cleanup stops further additions.
Run npm test at the root for application and MCP checks, and npm test --prefix doordash-macro-agent for browser, checkout, recovery, modifier, picker, and session checks. Browser tests require an installed Playwright-compatible Chromium (CHROMIUM_PATH can specify its executable) and its shared libraries.
npm run trials:cart --prefix doordash-macro-agent requests 20 cart-only attempts across five restaurants and saves a structured report. It never invokes Place Order. From the agent directory, npm run trials:cart -- --from=reports/cart-trials-<timestamp>.json reuses observed candidate IDs; menus are still checked live. Reconcile any uncertain prior cart before restarting trials: from the agent directory, run node --import tsx scripts/reconcile-trial.ts reports/cart-trials-<timestamp>.json and require cleared: true. Cart verification and checkout readiness are reported separately; fewer than 20 completed attempts do not establish the acceptance target.
Ordering needs:
- a process that stays up for the minute cron and multi-minute Steel sessions
- secrets and a DoorDash login profile
- a large local nutrition SQLite DB
- JSON persistence across restarts
Short-lived serverless functions are a poor fit. For the hackathon and personal use, one always-on Node process on your laptop (or a small VPS) is the right shape.
Today (local / single user)
- One
npm run devprocess = website + jobs - JSON files as the database
- Steel Launch sessions (~15 minutes)
- Human approval before charge (or demo place for pitches)
Next (always-on for a small team)
- Deploy the same Node process to a host that stays up (Railway, Fly, Render, or a VPS), not a serverless web-only host
- Keep one process owning site + cron (same as local)
- Move secrets to the host’s env; keep Steel profile + DoorDash login healthy
- Keep real checkout enabled (
MACROME_DEMO_PLACE=0) and require a valid DoorDash payment method - Optional: separate worker only if you outgrow a single box
Later (multi-user product)
- Replace JSON files with a real database (users, plans, orders, digests, queue)
- Auth and per-user Steel profiles / DoorDash identities
- Horizontal scale behind a queue so two runs never fight one cart
- Keep MCP (or a public API) as the agent-facing surface; the always-on backend still owns browsers and cron
- Stronger models via OpenRouter where free ones time out
What we are not aiming for first
- Pure serverless “upload and forget” hosting for the agent itself
- Fully unsupervised ordering with no human approval
| Piece | Role |
|---|---|
server.js |
HTTP UI, APIs, spawns agent, runs minute cron |
ui/ |
Wizard + dashboard |
doordash-macro-agent/ |
Steel + Playwright scrape, pick, cart, recovery |
order-queue.cjs / day-log.cjs / digest.cjs |
Schedule lifecycle, meal outcomes, Finals |
digest-email.cjs |
Resend email for Finals |
mcp-server.cjs |
IDE tools over the same files |
Default meal model: nvidia/nemotron-3-super-120b-a12b:free via OpenRouter. Override with MACROME_MODEL.
| Variable | Required? | Purpose |
|---|---|---|
STEEL_API_KEY |
live orders | Cloud browser |
OPENROUTER_API_KEY |
live orders | Meal picking / recovery |
STEEL_PROFILE_ID |
live orders | Saved DoorDash login |
MACROME_MODEL |
no | OpenRouter model id |
RESEND_API_KEY / DIGEST_EMAIL |
Final digest mail | |
MACROME_EMAIL_FROM |
no | Sender address |
MACROME_DEMO_PLACE |
no | Real checkout by default; 1 = explicit demo |
MACROME_MAX_STORES |
no | Restaurants to compare |
MACROME_SEARCH_BUDGET_SECONDS |
no | Discovery budget |
MACROME_MAX_MENU_ITEMS |
no | Picker menu size |
MACROME_SESSION_TIMEOUT_MINUTES |
no | Browser lifetime |
MACROME_DIGEST_TIME |
no | EOD HH:MM |
MACROME_DRY_RUN |
no | Mock run |
MACROME_NO_CRON |
no | Disable clock in this process |
PORT |
no | Web port (default 3000) |
