[object Object]

← back to Delivery Address Skill

delivery-address skill: multi-user UberEats/DoorDash/Instacart address fixer generalized from uber-address-fix (TK-10144)

24069c2a07232ecff3925342949e4b7eaaffeb40 · 2026-08-02 15:16:33 -0700 · Steve

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 24069c2a07232ecff3925342949e4b7eaaffeb40
Author: Steve <steve@designerwallcoverings.com>
Date:   Sun Aug 2 15:16:33 2026 -0700

    delivery-address skill: multi-user UberEats/DoorDash/Instacart address fixer generalized from uber-address-fix (TK-10144)
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 .gitignore                   | 13 +++++++
 SKILL.md                     | 81 +++++++++++++++++++++++++++++++++++++++++++
 config.example.json          |  8 +++++
 references/platform-notes.md | 57 ++++++++++++++++++++++++++++++
 scripts/enter-code.js        | 34 ++++++++++++++++++
 scripts/launch.js            | 29 ++++++++++++++++
 scripts/lib.js               | 82 ++++++++++++++++++++++++++++++++++++++++++++
 scripts/restart-login.js     | 27 +++++++++++++++
 scripts/state.js             | 13 +++++++
 9 files changed, 344 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a58eac
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+node_modules/
+.env*
+*.bak*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+config.json
+profiles/
+sessions/
+*.png
diff --git a/SKILL.md b/SKILL.md
new file mode 100644
index 0000000..20867a2
--- /dev/null
+++ b/SKILL.md
@@ -0,0 +1,81 @@
+---
+name: delivery-address
+description: Fix delivery info (addresses, account details) on UberEats, Instacart, or DoorDash by driving a REAL headed Chrome browser through the platform's own UI — the platforms hard-block API/mid-order edits, so the browser is the only path. Multi-user and multi-platform by config (no hardcoded accounts). Handles the 2FA relay loop (email code / SMS code / phone-call code / card-digit verification) with the user in the loop over email or chat. Use when a user says "my UberEats/DoorDash/Instacart order is going to the wrong address", "change my delivery address", "fix my delivery info", "/delivery-address", or any delivery-app account/address fix that needs a live login.
+---
+
+# delivery-address — multi-platform delivery-app address/info fixer
+
+Generalized from the 2026-08-02 UberEats live session (`~/uber-address-fix/`, TK-10140).
+Works for ANY user and three platforms: **ubereats**, **doordash**, **instacart**.
+
+## Hard rules (non-negotiable, learned the hard way)
+
+1. **VERBATIM addresses only.** Save EXACTLY the address the user provides, word for word.
+   NEVER infer an address from context (a map pin, a charger location, a prior order).
+   The Alameda incident: an inferred address was flat wrong and the user had to revoke it.
+   If you only have an inference, ASK — never save.
+2. **Never place, modify, or cancel an order** without the user's explicit go for that
+   specific action. Saving an address the user dictated is fine; anything touching a live
+   order needs its own confirmation.
+3. **Select the tab by URL, never `pages()[0]`.** Other automations share the browser.
+   Every script here takes a `--match <url-substring>` and refuses to guess.
+4. **Newest code wins.** Any resend / call-me / flow-restart INVALIDATES all prior 2FA
+   codes. Every time a new code fires, tell the user explicitly: "only the newest one works."
+5. **Auth sessions die in well under 30 minutes.** If a code comes back "session timed
+   out", don't retry it — restart the flow (`restart-login.js`), which usually auto-fires
+   a fresh code, and tell the user a new code is coming.
+6. **Screenshot every state change** into the session dir and report what you saw, not
+   what you expected.
+7. **Never echo full card numbers or 2FA codes into email/logs.** Last-4 style references
+   only.
+8. Per Steve's standing rule: never refuse because "the app blocks it" — drive the real
+   browser and change it in the site UI like a human would.
+
+## Per-user setup (the "plugin" step)
+
+Copy `config.example.json` → `config.json` (gitignored) and fill in the user's own
+values. Nothing about any specific user is hardcoded in the scripts — email, port,
+session dir, and code-reading channel all come from config or CLI flags.
+
+Profiles persist per user+platform under `profiles/<user>-<platform>/` so a completed
+login survives restarts and future fixes skip most of the 2FA dance.
+
+## Platform entry points
+
+| platform  | login URL | quirks |
+|-----------|-----------|--------|
+| ubereats  | `https://auth.uber.com/v2/?next_url=https%3A%2F%2Fwww.ubereats.com%2Flogin-redirect` | email → email code → SMS/call code → sometimes a card missing-digits gate. Email field id `#PHONE_NUMBER_or_EMAIL_ADDRESS`, submit `#forward-button`. Addresses: Account → Addresses. |
+| doordash  | `https://identity.doordash.com/auth` | email → emailed code or password; magic-link possible (user must click it — ask them to paste the landing URL if so). Addresses: Account → Addresses. |
+| instacart | `https://www.instacart.com/login` | email → emailed code. Address lives per-order AND in Account settings; a live order's address change is time-limited (before shopping starts). |
+
+Full flow notes and failure modes: `references/platform-notes.md`.
+
+## Workflow
+
+1. **Launch**: `node scripts/launch.js --platform ubereats --user <email> [--port 9223]`
+   — headed real Chrome (Playwright persistent context, `channel: 'chrome'`), CDP on the
+   configured port, navigates to the platform. Keep it running; all other scripts attach
+   over CDP.
+2. **Login relay loop** (repeat until logged in):
+   - `node scripts/state.js --port 9223 --match uber.com` — screenshot + page text +
+     visible inputs. Diagnose which gate you're at before acting.
+   - Email code and config has `george_account`? Read it yourself via George
+     (`http://127.0.0.1:9850/api/messages`, auth `GEORGE_AUTH` in the secrets master).
+     ⚠️ Strip `<style>` blocks before grepping — numbers in CSS or street addresses
+     masquerade as codes.
+   - SMS/call code or card digits? Message the user, WAIT, then
+     `node scripts/enter-code.js --port 9223 --match uber.com --code 1234`.
+   - "Session timed out" → `node scripts/restart-login.js --platform ubereats --user <email> --port 9223`.
+   - Rate-limited SMS ("Bad request / use another method") → restart-login; the platform
+     usually rotates to phone-call or email code by itself.
+3. **Once logged in**:
+   - Check for a LIVE order first. Wrong address on an active order → order Help →
+     change delivery address if the platform offers it; otherwise tell the user honestly.
+   - Then Account → Addresses: add/fix the address the user dictated (rule 1).
+   - Surface any suspicious saved addresses to the user BEFORE deleting anything.
+4. **Report**: screenshots + a plain-language summary of exactly what was saved/changed.
+
+## Cost
+
+$0 (local) — real local Chrome, no metered APIs. If a platform hard-blocks local
+automation, escalate to the `browserbase` skill (metered — show cost first).
diff --git a/config.example.json b/config.example.json
new file mode 100644
index 0000000..e1debd6
--- /dev/null
+++ b/config.example.json
@@ -0,0 +1,8 @@
+{
+  "user_email": "user@example.com",
+  "platform": "ubereats",
+  "cdp_port": 9223,
+  "reply_channel": "email",
+  "george_account": "",
+  "session_dir": ""
+}
diff --git a/references/platform-notes.md b/references/platform-notes.md
new file mode 100644
index 0000000..591143d
--- /dev/null
+++ b/references/platform-notes.md
@@ -0,0 +1,57 @@
+# Platform flow notes & failure modes
+
+Everything below UberEats was proven live on 2026-08-02 (TK-10140); DoorDash and
+Instacart entries are the expected shapes — verify with `state.js` at every gate
+and update this file as sessions teach us more.
+
+## UberEats (proven live)
+
+Login chain observed: email → email code → SMS 4-digit code → (sometimes) card
+missing-digits gate → logged in.
+
+- Login URL: `https://auth.uber.com/v2/?next_url=https%3A%2F%2Fwww.ubereats.com%2Flogin-redirect`
+  Email field `#PHONE_NUMBER_or_EMAIL_ADDRESS`, submit `#forward-button`.
+- **Session expiry is brutal** — well under 30 min. A code entered after expiry
+  returns "Bad request — Current session has timed out." Restart the flow; Uber
+  often skips already-passed steps and goes straight to the next gate, auto-firing
+  a fresh code (cooldown timer visible = code fired).
+- **SMS rate-limit**: "Error sending code / Bad request / Please use another
+  method" — restart the flow; Uber rotates to phone-call ("Call me with code")
+  or email code by itself. Email code is the best outcome (self-serve via George).
+- **Card missing-digits gate**: after 2FA, Uber may ask for N digits of a saved
+  card (e.g. "____-5001" = the 4 digits immediately before the last 4; on a
+  15-digit Amex that's digits 8–11). STATIC data, no expiry race. Exactly one
+  text input + Next; there is no alternative path — you need the user.
+- **Email codes via George**: strip `<style>` blocks before searching the body —
+  Uber's marketing CSS and its street address (e.g. "1725") look like codes.
+- Addresses: Account → Addresses → add; a pin editor may appear (drag only where
+  the USER said — never infer). Mid-order address edits are hard-blocked; the
+  only live-order path is order Help → change delivery address, when offered.
+
+## DoorDash (expected shape — verify live)
+
+- Login: `https://identity.doordash.com/auth` — email first; then either
+  password, a 6-digit emailed code, or a magic link. A magic link must be
+  clicked in OUR browser session: ask the user to paste the link/landing URL,
+  then `page.goto()` it in the matched tab.
+- Addresses: Account → Addresses. Live order: order page → Help → may allow
+  address change before pickup.
+- Watch for PerimeterX-style bot walls; we run REAL Chrome headed, which
+  usually passes. If hard-blocked, escalate to `browserbase` (metered).
+
+## Instacart (expected shape — verify live)
+
+- Login: `https://www.instacart.com/login` — email → 6-digit emailed code.
+- Address lives in two places: Account settings AND per-order. A live order's
+  address is editable only until shopping starts ("Your orders" → order →
+  delivery details). Fix BOTH the account default and the live order when asked.
+
+## Universal failure modes
+
+| symptom | meaning | action |
+|---|---|---|
+| code rejected, "session timed out" | auth session expired (<30 min) | restart-login.js; tell user fresh code coming |
+| "Error sending code / use another method" | channel rate-limited | restart-login.js; platform rotates channel |
+| page reset to email/phone entry | parked state expired | restart-login.js |
+| tab index changed / wrong page | another automation opened tabs | ALWAYS select by `--match`, never index |
+| number in email looks like a code | CSS/address noise | strip `<style>`, prefer the code near "code" text |
diff --git a/scripts/enter-code.js b/scripts/enter-code.js
new file mode 100644
index 0000000..a367d00
--- /dev/null
+++ b/scripts/enter-code.js
@@ -0,0 +1,34 @@
+// Enter a 2FA / verification code into whatever code UI the page shows:
+// N separate one-digit boxes, or a single text input. Then click Next/Submit.
+// Usage: node enter-code.js --port 9223 --match uber.com --code 1234
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const code = args.code;
+  if (!code || !/^\d{3,8}$/.test(code)) { console.error('Need --code <3-8 digits>'); process.exit(2); }
+  const match = args.match || (args.platform && platform(args.platform).match);
+  if (!match) { console.error('Need --match <url-substring> or --platform'); process.exit(2); }
+
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, match);
+  const inputs = await page.$$('input:visible');
+  console.log('visible inputs:', inputs.length);
+  if (inputs.length >= code.length) {
+    for (let i = 0; i < code.length; i++) await inputs[i].fill(code[i]);
+  } else if (inputs.length >= 1) {
+    await inputs[0].click();
+    await page.keyboard.type(code, { delay: 120 });
+  } else {
+    console.error('No visible input to enter the code into — run state.js first.');
+    process.exit(2);
+  }
+  await page.waitForTimeout(1500);
+  for (const label of ['Next', 'Submit', 'Verify', 'Continue', 'Sign in']) {
+    const btn = await page.$(`button:has-text("${label}")`);
+    if (btn) { await btn.click().catch(() => {}); break; }
+  }
+  await page.waitForTimeout(6000);
+  await dumpState(page, sessionDir(cfg, args), 'after-code');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
diff --git a/scripts/launch.js b/scripts/launch.js
new file mode 100644
index 0000000..0ccec9b
--- /dev/null
+++ b/scripts/launch.js
@@ -0,0 +1,29 @@
+// Launch a long-lived headed real-Chrome session with CDP for the relay scripts.
+// Usage: node launch.js --platform ubereats --user me@example.com [--port 9223]
+const path = require('path');
+const { chromium } = require('playwright');
+const { parseArgs, loadConfig, platform } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const plat = platform(args.platform || cfg.platform);
+  const user = args.user || cfg.user_email;
+  if (!user) { console.error('Need --user <email> (or user_email in config.json)'); process.exit(2); }
+  const port = args.port || cfg.cdp_port || 9223;
+  const profile = path.join(__dirname, '..', 'profiles',
+    `${user.replace(/[@.]/g, '_')}-${args.platform || cfg.platform}`);
+
+  const ctx = await chromium.launchPersistentContext(profile, {
+    channel: 'chrome',
+    headless: false,
+    viewport: { width: 1360, height: 900 },
+    args: [`--remote-debugging-port=${port}`],
+  });
+  const page = ctx.pages()[0] || (await ctx.newPage());
+  await page.goto(plat.home, { waitUntil: 'domcontentloaded', timeout: 60000 })
+    .catch(e => console.log('nav err', e.message));
+  console.log(`READY cdp=http://127.0.0.1:${port} profile=${profile}`);
+  console.log('URL:', page.url());
+  await new Promise(() => {}); // keep alive
+})().catch(e => { console.error('LAUNCH FAIL', e.message); process.exit(1); });
diff --git a/scripts/lib.js b/scripts/lib.js
new file mode 100644
index 0000000..ce04202
--- /dev/null
+++ b/scripts/lib.js
@@ -0,0 +1,82 @@
+// Shared helpers for delivery-address scripts. Zero deps beyond playwright.
+const fs = require('fs');
+const path = require('path');
+
+const PLATFORMS = {
+  ubereats: {
+    home: 'https://www.ubereats.com/',
+    login: 'https://auth.uber.com/v2/?next_url=https%3A%2F%2Fwww.ubereats.com%2Flogin-redirect',
+    match: 'uber.com',
+    emailField: '#PHONE_NUMBER_or_EMAIL_ADDRESS',
+    emailSubmit: '#forward-button',
+  },
+  doordash: {
+    home: 'https://www.doordash.com/',
+    login: 'https://identity.doordash.com/auth',
+    match: 'doordash.com',
+    emailField: 'input[type="email"]',
+    emailSubmit: 'button[type="submit"]',
+  },
+  instacart: {
+    home: 'https://www.instacart.com/',
+    login: 'https://www.instacart.com/login',
+    match: 'instacart.com',
+    emailField: 'input[type="email"]',
+    emailSubmit: 'button[type="submit"]',
+  },
+};
+
+function parseArgs(argv) {
+  const args = {};
+  for (let i = 2; i < argv.length; i++) {
+    const m = argv[i].match(/^--([a-z-]+)$/);
+    if (m) { args[m[1]] = argv[i + 1]; i++; }
+  }
+  return args;
+}
+
+function loadConfig() {
+  const p = path.join(__dirname, '..', 'config.json');
+  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : {};
+}
+
+function platform(name) {
+  const p = PLATFORMS[name];
+  if (!p) { console.error(`Unknown platform "${name}". One of: ${Object.keys(PLATFORMS).join(', ')}`); process.exit(2); }
+  return p;
+}
+
+function sessionDir(cfg, args) {
+  const dir = args['session-dir'] || cfg.session_dir ||
+    path.join(__dirname, '..', 'sessions', `${(args.user || cfg.user_email || 'user').replace(/[@.]/g, '_')}-${args.platform || 'session'}`);
+  fs.mkdirSync(dir, { recursive: true });
+  return dir;
+}
+
+// HARD RULE: select the tab by URL substring, never pages()[0] — other
+// automations share the browser (Tesla tab hijacked slot 0 on 2026-08-02).
+async function findTab(port, match) {
+  const { chromium } = require('playwright');
+  const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
+  const pages = browser.contexts()[0].pages();
+  const page = pages.find(p => p.url().includes(match));
+  if (!page) {
+    console.error(`NO TAB matching "${match}". Open tabs: ${pages.map(p => p.url()).join(' | ')}`);
+    process.exit(2);
+  }
+  return { browser, page };
+}
+
+async function dumpState(page, dir, label) {
+  const shot = path.join(dir, `${label}-${Date.now()}.png`);
+  await page.screenshot({ path: shot }).catch(() => {});
+  console.log('URL:', page.url());
+  console.log('SHOT:', shot);
+  console.log(await page.evaluate(() => document.body.innerText.slice(0, 1000)));
+  const inputs = await page.evaluate(() =>
+    Array.from(document.querySelectorAll('input')).map(i =>
+      ({ t: i.type, ph: i.placeholder, val: i.value ? '<set>' : '', vis: !!i.offsetParent })));
+  console.log('INPUTS:', JSON.stringify(inputs.filter(i => i.vis)));
+}
+
+module.exports = { PLATFORMS, parseArgs, loadConfig, platform, sessionDir, findTab, dumpState };
diff --git a/scripts/restart-login.js b/scripts/restart-login.js
new file mode 100644
index 0000000..36dc331
--- /dev/null
+++ b/scripts/restart-login.js
@@ -0,0 +1,27 @@
+// Restart an expired auth flow: reload the platform's login URL and resubmit
+// the user's email. Usually auto-fires a FRESH code (which invalidates all
+// prior codes — TELL THE USER "newest code wins").
+// Usage: node restart-login.js --platform ubereats --user me@example.com --port 9223
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const plat = platform(args.platform || cfg.platform);
+  const user = args.user || cfg.user_email;
+  if (!user) { console.error('Need --user <email> (or user_email in config.json)'); process.exit(2); }
+
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, args.match || plat.match);
+  await page.goto(plat.login, { waitUntil: 'domcontentloaded' });
+  await page.waitForTimeout(4000);
+  const emailInput = await page.$(plat.emailField);
+  if (emailInput) {
+    await page.fill(plat.emailField, user);
+    await page.click(plat.emailSubmit);
+    await page.waitForTimeout(7000);
+  } else {
+    console.log('No email input — flow may have skipped straight to a code gate:');
+  }
+  await dumpState(page, sessionDir(cfg, args), 'restart');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
diff --git a/scripts/state.js b/scripts/state.js
new file mode 100644
index 0000000..2780f0d
--- /dev/null
+++ b/scripts/state.js
@@ -0,0 +1,13 @@
+// Diagnose the current gate: screenshot + page text + visible inputs.
+// Usage: node state.js --port 9223 --match uber.com [--platform ubereats]
+const { parseArgs, loadConfig, platform, sessionDir, findTab, dumpState } = require('./lib');
+
+(async () => {
+  const args = parseArgs(process.argv);
+  const cfg = loadConfig();
+  const match = args.match || (args.platform && platform(args.platform).match);
+  if (!match) { console.error('Need --match <url-substring> or --platform'); process.exit(2); }
+  const { browser, page } = await findTab(args.port || cfg.cdp_port || 9223, match);
+  await dumpState(page, sessionDir(cfg, args), 'state');
+  await browser.close();
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });

(oldest)  ·  back to Delivery Address Skill  ·  deprecate: merged into delivery-address-fix (canonical), see 30d4b0d →