← 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
A .gitignoreA SKILL.mdA config.example.jsonA references/platform-notes.mdA scripts/enter-code.jsA scripts/launch.jsA scripts/lib.jsA scripts/restart-login.jsA scripts/state.js
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 →