Costco receipt capture
Pulls Costco AU account data into plain markdown plus PDFs. The site is specific, but the shape is not: the same fire-and-poll capture, print-to-PDF and financial-year mirror works for any retailer whose receipts live behind a login.
export COSTCO_AREA=~/costco # where the markdown and PDFs land
export TAX_DIR=~/costco/tax # where the FY mirror lands
export COSTCO_WORK=~/costco-work # scratch dir for the screenshot pass
Two surfaces:
| Surface |
Coverage |
Speed |
Use |
Website (costco.com.au) |
rolling ~12 months of warehouse receipts plus membership, rewards, orders |
fast (DOM extraction plus print-to-PDF) |
default |
| iOS app (phone mirroring) |
full history, back to the first digital receipt |
slow (manual scroll and screenshot) |
only when receipts older than 12 months are needed |
Prerequisites
- Browser: a browser-automation MCP attached to a real Chrome with the user's session.
- Login: the site must already be signed in. The account has MFA, so unattended login is not possible. If
My Account redirects to /login, stop and ask the user to sign in. Do not enter credentials.
- Chrome binary for PDF rendering:
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome.
Step 1: membership, rewards, orders
Navigate and read the page text of each, writing into $COSTCO_AREA:
/my-account/update-profile → membership.md (name, member number, type, expiry, auto-renew, contact details)
/my-account/address-book → default address, into membership.md
/my-account/rewards → rewards.md (percentage estimate, accrual-since date, redeemable-on date, available today)
/my-account/orders → order-history.md (online and delivery orders; open each for line items)
Step 2: warehouse receipts, last 12 months
- Navigate to
/my-account/receipts. Confirm you are logged in: a month dropdown and receipt cards appear.
- Install helpers by running
scripts/extract.js through the browser's JavaScript tool.
- Get the month list:
window.__wx.months(), newest first.
- For each month, in order:
await window.__wx.goMonth('<Month YYYY>') switches the native <select> and returns a receipt count.
window.__wx.capMonth('<Month YYYY>'); 'fired' — fire without awaiting. See the gotcha below.
- Poll
window.__wx.status('<Month YYYY>') until have === total before switching months.
- One call per month is achievable: wait for the previous month's status to complete, then
goMonth(next) and fire capMonth(next).
- When every month reports
have === total, window.__wx.download() writes ~/Downloads/costco_webcap.json. Clear any old copy first.
Step 3: PDFs and detail markdown
python3 scripts/gen_pdfs.py [~/Downloads/costco_webcap.json] [$COSTCO_AREA]
One PDF per receipt in receipts-pdf/, named YYYY-MM-DD_HHMMam/pm_<total>[_RETURN].pdf, plus warehouse-receipts-detail.md carrying the full receipt text and PDF links.
Step 3b: screenshot ingestion, for receipts past the 12-month window
The website keeps about 12 months. For older receipts, the user screenshots each receipt on their phone (long receipts span two to four overlapping scrolls) into a folder. Then:
- Transcribe. Fan out vision sub-agents, each given about 10 image paths, writing one verbatim
<IMG>.txt per screenshot into $COSTCO_WORK/transcripts/, with header lines RECEIPT:, IMG:, HEADER:, TOTALS: then --- then the exact receipt text. Instruct them to transcribe exactly and never summarise.
- Stitch and reconcile.
python3 scripts/stitch_screenshots.py [$COSTCO_WORK] [$COSTCO_AREA] groups transcripts by receipt number, merges overlapping screenshots (longest normalised suffix equals prefix), and reconciles each receipt's line-item sum against its printed total. Writes stitched.json plus a report of which receipts do not reconcile.
- Fix exceptions. For receipts whose sum does not match the total, re-read just those images and write
clean/<receipt>.clean.txt reconciled to the known total. Watch for VOID lines that net to zero, discount and reward negatives, quantities above one, and genuine gaps where not every item was photographed. Gaps are flagged, never invented.
- Generate markdown.
python3 scripts/gen_screenshot_md.py [$COSTCO_WORK] [$COSTCO_AREA] writes the older-receipts detail file in the same format as the website one, preferring cleaned versions over the raw stitch.
Step 4: full history (app only, optional)
For the complete history, drive the retailer's app through phone mirroring: purchases, warehouse receipts, all available receipts, scrolling the lazy-loading list and recording each receipt's date, total and number into a master index. Slow; only when explicitly asked.
Step 5: financial-year mirror
python3 scripts/gen_tax.py [~/Downloads/costco_webcap.json] [$TAX_DIR] [2025]
Writes costco-fyNN-transactions.csv and .md for the Australian financial year (1 July to 30 June), filtered to that year.
Deductibility is flagged, never decided. The script defaults to N for private groceries and ? for fuel and any detected office or computer item. A retail club membership fee is not a professional membership. Leave the actual position to the user; a tool that decides tax outcomes is a tool that gets someone audited.
Gotchas (all learned the hard way)
- The 45 second eval limit. A single browser eval that awaits a whole month of modal opens and closes exceeds Chrome DevTools'
Runtime.evaluate limit and errors, but the page-side async loop keeps running. So fire capMonth and poll status; never await a multi-receipt loop. This is the single most important line in this file.
- Tool output truncation. Long return strings are truncated, so you cannot read big receipt HTML back through the tool. Download a blob to disk and process the file with Python.
- Headless Chrome for PDFs. Use classic
--headless, not --headless=new, which hangs. Add --virtual-time-budget, and use a fresh --user-data-dir per file, because a reused profile makes Chrome hang on exit after the PDF is already written. The script polls for the output file and then kills Chrome rather than waiting for a clean exit.
- Receipt modal: selector
.cdk-dialog-container, removed from the DOM on close. End-of-render marker: the text contains Items Sold or TOTAL NUMBER OF ITEMS.
- Month picker is a native
<select>: set selectedIndex then dispatch a change event.
- Fuel receipts: the "Items Sold" number is litres, not line items. Do not mistake a low-dollar, high-"item" row for a big grocery shop.
- No in-app PDF export for warehouse receipts; the PDFs here are rendered from the website receipt text.
- Third-party same-day delivery orders are not hosted by the retailer and will not appear.
1---2name: costco-receipt-capture3description: Capture Costco (Australia) membership, rewards, order history and warehouse receipts into a markdown area, with one print-to-PDF per receipt and a full index, and mirror the financial-year transactions into a tax folder for review. Use when the user wants to refresh Costco purchase or receipt data, rebuild the Costco area, or pull Costco spending for tax. Drives costco.com.au in a logged-in browser session.4---56# Costco receipt capture78Pulls Costco AU account data into plain markdown plus PDFs. The site is specific, but the shape is not: the same fire-and-poll capture, print-to-PDF and financial-year mirror works for any retailer whose receipts live behind a login.910```11export COSTCO_AREA=~/costco # where the markdown and PDFs land12export TAX_DIR=~/costco/tax # where the FY mirror lands13export COSTCO_WORK=~/costco-work # scratch dir for the screenshot pass14```1516Two surfaces:1718| Surface | Coverage | Speed | Use |19|---|---|---|---|20| **Website** (`costco.com.au`) | rolling **~12 months** of warehouse receipts plus membership, rewards, orders | **fast** (DOM extraction plus print-to-PDF) | **default** |21| **iOS app** (phone mirroring) | **full history**, back to the first digital receipt | slow (manual scroll and screenshot) | only when receipts older than 12 months are needed |2223## Prerequisites2425- **Browser:** a browser-automation MCP attached to a real Chrome with the user's session.26- **Login:** the site must already be signed in. The account has MFA, so unattended login is not possible. If `My Account` redirects to `/login`, **stop and ask the user to sign in**. Do not enter credentials.27- **Chrome binary** for PDF rendering: `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`.2829## Step 1: membership, rewards, orders3031Navigate and read the page text of each, writing into `$COSTCO_AREA`:3233- `/my-account/update-profile` → `membership.md` (name, member number, type, expiry, auto-renew, contact details)34- `/my-account/address-book` → default address, into `membership.md`35- `/my-account/rewards` → `rewards.md` (percentage estimate, accrual-since date, redeemable-on date, available today)36- `/my-account/orders` → `order-history.md` (online and delivery orders; open each for line items)3738## Step 2: warehouse receipts, last 12 months39401. Navigate to `/my-account/receipts`. Confirm you are logged in: a month dropdown and receipt cards appear.412. Install helpers by running `scripts/extract.js` through the browser's JavaScript tool.423. Get the month list: `window.__wx.months()`, newest first.434. For **each** month, in order:44 - `await window.__wx.goMonth('<Month YYYY>')` switches the native `<select>` and returns a receipt count.45 - `window.__wx.capMonth('<Month YYYY>'); 'fired'` — **fire without awaiting**. See the gotcha below.46 - Poll `window.__wx.status('<Month YYYY>')` until `have === total` before switching months.47 - One call per month is achievable: wait for the previous month's status to complete, then `goMonth(next)` and fire `capMonth(next)`.485. When every month reports `have === total`, `window.__wx.download()` writes `~/Downloads/costco_webcap.json`. Clear any old copy first.4950## Step 3: PDFs and detail markdown5152```53python3 scripts/gen_pdfs.py [~/Downloads/costco_webcap.json] [$COSTCO_AREA]54```5556One PDF per receipt in `receipts-pdf/`, named `YYYY-MM-DD_HHMMam/pm_<total>[_RETURN].pdf`, plus `warehouse-receipts-detail.md` carrying the full receipt text and PDF links.5758## Step 3b: screenshot ingestion, for receipts past the 12-month window5960The website keeps about 12 months. For older receipts, the user screenshots each receipt on their phone (long receipts span two to four overlapping scrolls) into a folder. Then:61621. **Transcribe.** Fan out vision sub-agents, each given about 10 image paths, writing one verbatim `<IMG>.txt` per screenshot into `$COSTCO_WORK/transcripts/`, with header lines `RECEIPT:`, `IMG:`, `HEADER:`, `TOTALS:` then `---` then the exact receipt text. Instruct them to transcribe exactly and never summarise.632. **Stitch and reconcile.** `python3 scripts/stitch_screenshots.py [$COSTCO_WORK] [$COSTCO_AREA]` groups transcripts by receipt number, merges overlapping screenshots (longest normalised suffix equals prefix), and reconciles each receipt's line-item sum against its printed total. Writes `stitched.json` plus a report of which receipts do not reconcile.643. **Fix exceptions.** For receipts whose sum does not match the total, re-read just those images and write `clean/<receipt>.clean.txt` reconciled to the known total. Watch for VOID lines that net to zero, discount and reward negatives, quantities above one, and genuine gaps where not every item was photographed. Gaps are flagged, never invented.654. **Generate markdown.** `python3 scripts/gen_screenshot_md.py [$COSTCO_WORK] [$COSTCO_AREA]` writes the older-receipts detail file in the same format as the website one, preferring cleaned versions over the raw stitch.6667## Step 4: full history (app only, optional)6869For the complete history, drive the retailer's app through phone mirroring: purchases, warehouse receipts, all available receipts, scrolling the lazy-loading list and recording each receipt's date, total and number into a master index. Slow; only when explicitly asked.7071## Step 5: financial-year mirror7273```74python3 scripts/gen_tax.py [~/Downloads/costco_webcap.json] [$TAX_DIR] [2025]75```7677Writes `costco-fyNN-transactions.csv` and `.md` for the Australian financial year (1 July to 30 June), filtered to that year.7879**Deductibility is flagged, never decided.** The script defaults to `N` for private groceries and `?` for fuel and any detected office or computer item. A retail club membership fee is not a professional membership. Leave the actual position to the user; a tool that decides tax outcomes is a tool that gets someone audited.8081## Gotchas (all learned the hard way)8283- **The 45 second eval limit.** A single browser eval that awaits a whole month of modal opens and closes exceeds Chrome DevTools' `Runtime.evaluate` limit and errors, **but the page-side async loop keeps running**. So fire `capMonth` and poll `status`; never await a multi-receipt loop. This is the single most important line in this file.84- **Tool output truncation.** Long return strings are truncated, so you cannot read big receipt HTML back through the tool. Download a blob to disk and process the file with Python.85- **Headless Chrome for PDFs.** Use classic `--headless`, not `--headless=new`, which hangs. Add `--virtual-time-budget`, and use a **fresh `--user-data-dir` per file**, because a reused profile makes Chrome hang on *exit* after the PDF is already written. The script polls for the output file and then kills Chrome rather than waiting for a clean exit.86- **Receipt modal:** selector `.cdk-dialog-container`, removed from the DOM on close. End-of-render marker: the text contains `Items Sold` or `TOTAL NUMBER OF ITEMS`.87- **Month picker** is a native `<select>`: set `selectedIndex` then dispatch a `change` event.88- **Fuel receipts:** the "Items Sold" number is **litres**, not line items. Do not mistake a low-dollar, high-"item" row for a big grocery shop.89- **No in-app PDF export** for warehouse receipts; the PDFs here are rendered from the website receipt text.90- Third-party same-day delivery orders are not hosted by the retailer and will not appear.