Skip to main content

amazon-reports

Amazon platform only. MUST load BEFORE taking any action when the task involves Amazon Seller Central report pages (Tax Document Library, Business Reports, Fulfillment, Payments CSV, Advertising Reports, etc.). Contains URLs, hover navigation, CSV structures, and wait times.

Jump to install

Source facts

Repository
zpoint/vibe-seller
Last source activity
August 4, 2026 at 08:02
Detected SKILL.md language
English
Stars
68
Forks
14

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
amazon-reports
description
Amazon platform only. MUST load BEFORE taking any action when the task involves Amazon Seller Central report pages (Tax Document Library, Business Reports, Fulfillment, Payments CSV, Advertising Reports, etc.). Contains URLs, hover navigation, CSV structures, and wait times.
requires
["amazon-shared"]
review
{"criteria":"- The requested report(s) were ACTUALLY exported: the file exists,\n is non-empty, and has rows > 0 โ€” OR the page explicitly shows \"No\n Data Available\" (a valid empty result, not a missing/failed pull).\n- Scope matches the ask: every requested country / the requested date\n window is covered, not a partial pull.\n","verify_by":"Open the newest downloaded file in ~/.vibe-seller/downloads/<slug>/\nand confirm the header columns + a non-zero row count; open the\nreport-history page and confirm the \"Date Range Covered\" and scope\nmatch the request. Do not accept a claimed export without the file.\n"}
# Amazon Seller Central โ€” Reports Export Guide > **PREREQUISITE:** Read `../amazon-shared/SKILL.md` for marketplace > TLD map, the hamburger-menu hover pattern (referenced below), > sign-in / Ziniao / OTP handling, and the capture rule. This skill documents how to navigate to and export every report type available in Amazon Seller Central and Amazon Advertising, including CSV/PDF structures for building analysis scripts. ## Critical: Hamburger Menu Navigation Many reports are accessed via the **hamburger menu** (top-left corner). This menu uses a **hover-to-reveal** pattern โ€” you must dispatch a `mouseover`, not a `click`, on a category to reveal its submenu items. (See `../amazon-shared/SKILL.md ยง 4` for the canonical version.) ```bash browser-use <<'PY' # 1. Open the hamburger menu (opens the full sidebar overlay). # The button is a role=button inside the navigation-hamburger-menu # shadow DOM โ€” reach it through its shadow host. js("document.querySelector('navigation-hamburger-menu').shadowRoot.querySelector('[role=button]').click()") wait_for_load() # 2. Reveal the submenu by dispatching mouseover on the "Reports" # category (aria-expanded flips false -> true; flyout appears). js(""" var cat = [...document.querySelectorAll('[aria-expanded=false]')] .find(e => e.textContent.trim() === 'Reports'); cat.dispatchEvent(new MouseEvent('mouseover', {bubbles: true})); """) print(page_info()) # 3. Click the submenu item once it renders (e.g. "Tax Document Library"). js(""" [...document.querySelectorAll('a, [role=button]')] .find(e => e.textContent.trim() === 'Tax Document Library').click(); """) PY ``` **Do NOT click the category name** โ€” clicking navigates away instead of revealing the submenu. Always dispatch `mouseover` first, then `print(page_info())` to see the submenu items, then click the target. If a stable selector isn't obvious, fall back to `capture_screenshot()` then `click_at_xy(x, y)`. ## General Rule: Download Existing Reports First **Before generating any report, always check if a matching report already exists.** All report pages maintain a download history table showing previously generated reports with their date ranges and Download buttons. This applies to ALL report types: - **Fulfillment reports** (`/reportcentral/`): Click the report in the left sidebar โ†’ "Download" tab โ†’ history table with columns: Report Type | Date Range Covered | Date Requested | File Format | Report Status (Download button). If a row matches the requested date range, click Download directly. - **Advertising reports** (Unified Reporting, `advertising.amazon.{tld}/reporting`): the report list shows saved/generated reports with a **ๆŠฅๅ‘Š็Šถๆ€** column; click into a **ๅทฒๅฎŒๆˆ** (Completed) row for the Download link. If a completed run matches the date range, download it. (The old `/reports` console is retiring 2026-12-31 โ€” see ยง6.) - **Payments reports** (`/payments/reports-repository`): Check the report list for existing reports covering the target period. Only request/generate a new report if no existing one covers the needed date range. ## An empty export is a SUCCESS; "N/A" must be proven on the page Two rules that together stop the biggest false-failure pattern (a store was marked FAILED for reports it "doesn't have" โ€” decided from metadata, never checked on the page): **1. Store metadata does NOT tell you which reports apply.** The store's `platforms` / `countries` (e.g. `{"amazon": ["SA"]}`) only say which marketplaces it sells on. They encode **nothing** about FBA enrollment or whether an Advertising account exists. **Never** conclude "no FBA โ†’ skip storage/returns" or "no Ads โ†’ skip the ad report" from metadata. You must open the actual page and let *it* tell you. **2. A report that exports with zero data rows is DOWNLOADED, not missing.** Request each report on its page: - **Ads report:** warm the ad console first (via Campaign Manager in the Seller Central menu, **or** the `choose-account?destination=/reporting` flow in ยง6.1) โ€” a **cold** direct `advertising.amazon.{tld}` URL gives a "Sign in / Register" marketing page. Once warm, build the report in Unified Reporting (ยง6) โ€” Amazon **generates it even with zero campaigns** (headers, 0 rows) โ†’ download that empty file, it's a completed deliverable. The ad report is genuinely N/A **only** if, *after* warming up, the store shows an advertiser **onboarding / registration** flow (no advertiser account). A marketing landing reached by direct URL proves nothing โ€” it just means you weren't SSO'd; it is NOT evidence of "no Ads". - **FBA storage / returns:** open the report page and request the month. An empty result / "No Data Available" for a *valid* request still means you asked correctly โ€” download whatever file is produced. Only if the page **explicitly says the store is not enrolled in FBA** is it N/A. **When to use each task outcome:** - **downloaded** (incl. empty 0-row exports) โ†’ deliverable met. - **N/A** โ€” only when the *page* proved the capability is absent (advertiser-registration landing; explicit not-enrolled-in-FBA). Record in `vibe_seller_set_task_result`; do **NOT** `vibe_seller_set_task_error`. - **pending-Amazon-latency** (e.g. Monthly Storage Fees not yet published, see ยง2) โ†’ `vibe_seller_set_task_result`, not `vibe_seller_set_task_error`. - **failed** โ†’ `vibe_seller_set_task_error` **only** for a report the page shows you *should* be able to get but couldn't (dead button, 0-byte download, error page). If the only gaps are proven-N/A or latency-pending, the task **COMPLETES**. State per report which of {downloaded, N/A-proven-on-page, pending-latency, failed} it is, so the outcome is unambiguous โ€” and never downgrade "I didn't check" into "N/A". ## Clicking a Download button (Amazon `kat-*` shadow DOM) Report tables wrap the Download control as `kat-table-row โ†’ kat-table-cell โ†’ kat-button`, and the real clickable `<button>` lives **inside `kat-button.shadowRoot`**. A plain `document.querySelector('kat-table-cell button')` (or any `[data-testid=...]` / `.download-btn` guess) returns `null` โ€” CSS selectors do **not** cross shadow boundaries, and `[...document.querySelectorAll('button')]` never sees a button that lives inside a shadow root. So those selectors silently no-op and you waste turns. Reach the inner button through the shadow host and `.click()` it. (This `.click()` works for table Download buttons; the left-sidebar `kat-button`s are the exception โ€” those need a coordinate click.) **Use this one snippet on every report page.** Match the target row by a substring of its date range, then click its Download control: ```bash browser-use <<'PY' print(js(r""" var rowMatch = '01/06/26'; // substring of the TARGET row's date range var rows = document.querySelectorAll('kat-table-row, tr'); for (var r of rows) { if (!(r.textContent || '').includes(rowMatch)) continue; for (var h of r.querySelectorAll('kat-button, kat-link, a, button')) { var real = h.shadowRoot ? h.shadowRoot.querySelector('button, a') : h; var label = (h.textContent||'') + ' ' + ((h.getAttribute && h.getAttribute('label')) || ''); if (real && (h.tagName === 'KAT-BUTTON' || /download|\.csv/i.test(label))) { real.click(); return 'clicked download for row ~ ' + rowMatch; } } } return 'NO download control for ~' + rowMatch + ' (check the date substring / row exists)'; """)) import time; time.sleep(3) # let the download start PY ``` On "NO download control", fall back to `print(page_info())` (it often lists a plain `Download` link, clickable by index) or `capture_screenshot()` + coordinate click โ€” but try the snippet first; it one-shots the common `kat-button` shadow case. The same host-then-inner-button walk applies to any `kat-button` (e.g. "Request Report", "Download CSV") โ€” match on its `label`/text instead of the date substring. ## Setting a custom date range (works across every Seller Central variant) Date controls differ **per marketplace and per report page** โ€” and Amazon changes them over time. You will meet native `<select>` presets, `kat-dropdown` presets, plain `<input>` date fields, `kat-date-picker` / `kat-date-range-picker` shadow widgets, and month/year dropdowns โ€” the same report can render differently on `.sa` vs `.ae` vs `.com`. **Do not assume a widget.** Follow the same three-step loop everywhere: **observe โ†’ interact the human way โ†’ verify the outcome.** The outcome check is DOM-independent and is what makes this robust. **Step 1 โ€” Observe.** `capture_screenshot()` and glance at the DOM (`document.querySelector('kat-date-range-picker')`? a native `<select>`? plain `input[placeholder]`?). Identify the preset control and, once a custom range is chosen, the two date fields. **Step 2 โ€” Interact the way a human would for THAT widget.** Prefer the interaction that fires the component's real handlers: - **Preset โ†’ "Exact dates":** a **native `<select>`** takes a value-set (`el.value='โ€ฆ'` + `change`); a **`kat-dropdown`** does **not** โ€” `click_at_xy` to expand, then `click_at_xy` the "Exact dates" row. - **The two date fields:** the reliable path on **every** variant is the **calendar popup with real clicks** โ€” click the field's calendar icon, `click_at_xy` the **โ€น / โ€บ** month arrows to the target month, then `click_at_xy` the day cell. (Locate cells from the screenshot โ€” they aren't reliable via `page_info()`/JS rects.) Plain `<input>` fields on some marketplaces also accept `fill_input(selector, "<date>")` โ€” try it if they truly are light-DOM inputs, but **only trust it after Step 3**. > โš ๏ธ **On `kat-*` date widgets, programmatic value-setting is > display-only and silently ignored on submit** โ€” live-verified on AE FBA > Customer Returns (2026-07-07): `setAttribute('start-value')`, the > `startValue`/`endValue` property, the nested shadow `<input>.value` + > `input`/`change` events, and CDP `Input.insertText` all updated the > visible field (and even `rp.startValue`) yet the report generated for > **today**. `dropdown.selectOption('-1')` likewise sets `.value` without > emitting the reveal event. The component's submit reads an internal > model only real user input updates โ€” so calendar clicks, not scripts. **Step 3 โ€” Verify the outcome (the universal acceptance test).** This is the check that survives every DOM difference: 1. A quick sanity read after a calendar click โ€” `js("var rp=document.querySelector('kat-date-range-picker'); return rp?rp.startValue+' -> '+rp.endValue:'n/a'")` โ€” is fine as a *first* look, but it is **not authoritative**: a programmatic set can leave `startValue`/`endValue` showing your target without the value actually committing (see the warning above), so a match here does **not** guarantee the report will use it. 2. The authoritative check: request the report, then **CONFIRM the new history row's "Date Range Covered" is your target month, not today.** A today-dated (or wrong) row means the date never committed โ€” go back to Step 2 and use real clicks; do not download the wrong-range file. Because the calendar fills the field itself, **date-format variance (`DD/MM/YYYY` / `MM/DD/YYYY` / `YYYY/M/D`) is irrelevant** โ€” never hand- type the format. Month/year dropdowns (Payments Reports Repository, Monthly Storage Fees) are `kat-dropdown`s โ†’ select by real click, never `selectOption()`; month index has been seen **0-indexed** (Jan=0) on some pages โ€” confirm against the visible label, don't hardcode it. **Report-status polling โ€” keep each sleep short.** After requesting a report, poll for the Download button; do **not** `time.sleep(120)` in one call โ€” a single long sleep exceeds the browser-use tool timeout and the turn is killed mid-wait. Loop with short sleeps and re-check instead: ```bash browser-use <<'PY' import time for _ in range(6): # ~6 ร— 30s โ‰ˆ 3 min, each well under the tool timeout time.sleep(30) info = page_info() if 'Download' in info or 'Ready' in info: break print(info) PY ``` ## Critical: One Account, Two Marketplaces โ€” Never Reuse a Download A seller account can span several marketplaces (a MENA account serves both SA and AE). Seller Central then **ignores the URL subdomain and serves the session's last active marketplace**, and the reports come down under names that carry no marketplace at all: 2026JulMonthlyTransaction.csv <- same name for SA and for AE storage.csv <- same name for SA and for AE Both land in the one store downloads dir, so the second marketplace's download **silently overwrites the first**. And if that second download then fails, is slow, or is never actually triggered, `ls -lt` still returns a file under the expected name โ€” the *first* marketplace's โ€” and the agent copies one marketplace's money into both marketplaces' folders. This is not hypothetical. It reached production: a month's report set had one marketplace carrying a second copy of its sibling's revenue, because the same CSV was copied into both target directories. ### Rule 1 โ€” rename before switching marketplace Download โ†’ **immediately** move the file to its target directory under its final name โ†’ only then switch marketplace and download the next. Never download both marketplaces and sort it out afterwards. ```bash DL=~/.vibe-seller/downloads/<slug> # marketplace 1: download, then move it out at once mv "$DL/2026JulMonthlyTransaction.csv" reports_07_<cc1>_<slug>/2026JulMonthlyTransaction.csv # only now switch the marketplace picker and download the second mv "$DL/2026JulMonthlyTransaction.csv" reports_07_<cc2>_<slug>/2026JulMonthlyTransaction.csv ``` `mv`, not `cp`: it leaves nothing behind in the downloads dir for the next step to pick up by mistake. ### Rules 2 and 3 โ€” one check, run it on every file Both remaining rules read the same two things off a file, so one command covers them. It takes the paths as arguments, finds the header row by shape (the first row with more than five fields โ€” these exports carry preamble lines), then reads the marketplace column **by name**, and streams both the hash and the rows so a large export costs no memory: ```bash python3 - reports_07_<cc1>_<slug>/2026JulMonthlyTransaction.csv \ reports_07_<cc2>_<slug>/2026JulMonthlyTransaction.csv <<'PY' import csv, hashlib, sys for path in sys.argv[1:]: digest = hashlib.md5() with open(path, 'rb') as fh: for chunk in iter(lambda: fh.read(1 << 20), b''): digest.update(chunk) with open(path, newline='', encoding='utf-8-sig') as fh: rows = csv.reader(fh) header = next(r for r in rows if len(r) > 5) cols = [c.strip().lower() for c in header] name = 'marketplace' if 'marketplace' in cols else 'country_code' i = cols.index(name) seen = {r[i].strip().lower() for r in rows if len(r) > i and r[i].strip()}
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub