- name
- noon-ads
- description
- Noon Ad Manager — campaigns (Auto / Manual / Brand), tuning audits, keyword research, negatives, Vantage analytics. Covers Targets / Customer Queries / Export Data flows. Load for any noon ads work — review, audit, tune, create. References under references/ for the tuning playbook (ads-tuning.md), creation guide (ads-creation.md), and keyword research (ads-keyword-research.md).
- requires
- ["noon-shared"]
- gates
- ["ad_completeness_review","ad_negation_allowlist","ad_execution_fidelity"]
# Noon — Ad Manager
> **PREREQUISITE:** Read `../noon-shared/SKILL.md` for login, page
> structure, and common patterns.
## 0. FIRST — declare what this task is for
**Before any browser action, call `vibe_seller_declare_ad_task`.** This is
a precondition, not a courtesy: a report with no declaration behind it is
refused.
Two things follow from the declaration and cannot be changed afterwards —
how much the completeness gate asks of you, and whether the user gets a
review console. Read them off what the person actually asked for, not off
what you expect to find once you look.
| They asked for | `kind` | `scope` |
|---|---|---|
| "审计一下广告" / "review our ads" | `audit` | omit `combos` — whole store |
| "复核 widget-006 在 amazon 的广告" | `audit` | that platform+country, those campaign ids |
| "把这三个词的出价降下来" | `audit` | those campaigns |
| "帮 widget-006 建关键词广告" | `create` | the market you are creating in |
| "执行刚才确认的调整" | `execute` | the campaigns being changed |
| "我们 SA 的 ACOS 大概多少" | `investigate` | the market asked about |
**Numbers or changes?** That is the line between `investigate` and
`audit`, and it matters more than the table above. If the answer the
person wants is a FIGURE — what did we spend, what is our ROAS, how did
last month go — that is `investigate`, however many markets it spans:
you are reading, not proposing. If they want to know what to CHANGE, or
asked "is this right?", that is `audit`, and it opens a review console.
Observed live: a plain "how much did we spend on ads this month, and
what came back?" was declared a whole-store `audit`, which put five
marketplaces of drill work behind a question that wanted four numbers. And the other way round, observed in
CI: "review the keyword bids on <campaign>" was declared `investigate`,
so the bid recommendations it produced reached the user with no console
to approve them on.
**If you got it wrong, correct it upward.** If you declared
`investigate` and the work turned out to produce decisions — a table
telling someone to raise, cut, pause or negate — call
`vibe_seller_declare_ad_task` again with `kind: "audit"` and the same
scope, or a narrower one. That is the ONLY kind change allowed inside a
turn, and it is allowed because it only ever makes you owe more: the
coverage obligation appears and the user gets the console. Going the
other way (`audit` → `investigate`), adding a marketplace, or widening
the campaign list are all still refused, and still need a new message
from the user. Do not instead delete your recommendations to fit the
declaration — the person asked what to change.
**There is no `edit` kind.** A request to change specific bids is an
`audit` whose scope names those campaigns — the user still reviews the
change before it is applied, and the scope is what makes it small.
**The scope trap:** omitting `combos` means EVERY marketplace this store
sells on, and you will be held to all of them. Omit it only when the
request really is store-wide. A store selling on five marketplaces has
been asked for all five because a one-product task left the field out.
**If the request names a product, not a campaign** ("widget-006 的广告"),
declare the MARKET now and the campaigns later. You cannot know campaign
ids before you have looked, and declaring a market with no campaign list
means **every campaign in it** — which is how a one-product request turns
into an audit owing a whole marketplace.
So: declare `{kind, combos, products}` first, enumerate the campaign list,
find the campaigns carrying that SKU family, then call the tool AGAIN with
the same kind and the same combos plus `campaigns: [...]`. That second
call is a **narrowing refinement** and is accepted within the same turn.
**Refining may only ever remove reach.** Same kind, same marketplaces, and
a campaign list going from "all of them" to a named subset. You cannot add
a marketplace, change the kind, or widen the campaign list — a wider scope
needs a new message from the user. If a gate asks for something outside
your scope, say so in your result and let the user redirect you; do not
try to re-declare around it.
**When the user sends a NEW message that changes what you are doing,
declare again.** That is a new phase, and it is the only way a
declaration changes. Two cases you will hit often:
- "现在把刚创建的广告复核一下" after a `create` phase → declare `audit`,
and its scope may name the campaigns you created earlier in this same
task.
- The review console submits the user's decisions back as a follow-up
message → declare `execute`, scoped to the campaigns that submission
actually names.
---
Mechanics for noon Ad Manager. The actual *thinking* — when to
create / tune / kill a campaign, how to research keywords — lives
in the three reference files (see § 11).
**URL**: `https://admanager.noon.partners/en-{cc}/home?mpCode=noon&project=PRJ{project_id}`
Left nav is an **icon rail** (not a text `ul[role=menu]`): **Campaigns**,
**Recommendations**, **Budget**, **Vantage**, **Billing**, **Settings**
— the labels render as `role=menuitem` spans, so match by text, not by
`ul[role=menu] a`.
> ⚠️ **The Ad Manager was redesigned (verified live 2026-07-21).** The
> `/home` landing is now an **Overview** dashboard (KPI panels +
> promos), NOT the campaign list. The "Ad Manager" card carries three
> tabs — **Overview | Performance | Campaigns** — and the campaign list
> lives under the **Campaigns** tab at `…/home?…&tab=campaigns`. A page
> that also shows a **Sale Event Optimizer** widget (event budget/bid
> boosts) and a **Recommendations** panel is the current build. See § 2
> for how to enumerate the list — the old paginator is gone.
## 1. Campaigns Overview Metrics
Overview-tab KPI cards:
| Metric | Formula / Notes |
|--------|-----------------|
| ROAS | Return on Ad Spend = Revenue / Spends |
| Revenue | Total ad-attributed revenue |
| Spends | Total ad spend |
| eCPC | Effective Cost Per Click |
| CTR | Click-Through Rate = Clicks / Views |
| Orders | Ad-attributed orders |
| Clicks | Total clicks on ads |
| Views | Total ad impressions |
| ATC | Add To Cart count |
| CvR | Conversion Rate (shown on campaign detail) |
A time-series performance chart below lets you toggle any metric.
## 2. Campaign List — Filters, True Totals, Full Enumeration
Open the **Campaigns** tab (`…/home?…&tab=campaigns`). Filters above
the list:
- **Search** box (by campaign name)
- **Date range** (default `Last 30 days`)
- **Ad Type**: `All types` / `Product` / `Brand` / `Display`
- **Targeting**: `All targeting` / `Auto` / `Manual`
- **Status count control** (segmented): `Live N` · `Paused N` · `All N`
+ a `More status filter` dropdown. The counts are the **true totals**
for the current filter — read them directly; there is no page math.
- **Export all campaigns** (top-right of the list) — a list-level bulk
export (distinct from the per-tab Export Data in § 7).
Columns (horizontally scrollable): Campaign, Status, Budget, Revenue,
ROAS, Ad Spend, eCPC, Orders, Views, Clicks, ATC, … Actions.
### Enumerate EVERY campaign — the paginator is gone
The Ant-Design pager (`li.ant-pagination-item-N`, "15 items per page")
**no longer exists**. The list is now a **lazy-loaded, inner-scroll
table**: only ~15–20 rows render on first paint, and **`window.scroll`
does nothing** — you must scroll the list's own container until every
row loads. Skipping this silently under-counts: an unscrolled read that
sees the first ~20 rows can miss more than half the Live campaigns when
the `Live N` chip is much larger, which then fails the completeness
gate.
> ⚠️ **Enumerate LIVE from the scrolled list — never from a pre-existing
> or downloaded export file.** A leftover `Campaign_*.csv` /
> `Export all campaigns` file in `~/.vibe-seller/downloads/` (from a
> prior run, or a first-paint export before you scrolled) captures only
> the rows that were loaded when it was written — typically the first
> ~20. Drilling that file makes the audit look done at `20/20` while the
> account has far more Live campaigns. (Live failure this fixes: an agent
> reused a 20-row export and reported noon SA `20/20` when the `Live`
> chip showed **45**.) **Any campaign set whose count is below the
> `Live N` chip is stale — re-enumerate by scrolling (below); and if you
> do use `Export all campaigns`, first scroll the list fully, then verify
> the file's row count equals the chip before trusting it.**
**What you owe is what you DECLARED.** `./AUDIT_TARGETS.json` lists every
country the store is configured for — the MENU, written by the server
from Settings. Your `vibe_seller_declare_ad_task` call turns part of it
into an obligation: the countries you declared each need their own
`AUDIT_SCOPE.json` combo entry (step 4) AND their own `## noon <CC>`
report section; declaring no combos at all (a whole-store audit) means
you owe every country in the file. Do NOT expand past your declaration
because the menu is longer — reporting on a market nobody asked about is
an out-of-scope gap. A country with
genuinely no Live campaigns is still written down — an entry with
`"active_ids": []` and `"total_active": 0`, plus a section saying so;
可以为空,但不能不写。Omitting a declared combo is a `[基线]` gap that
blocks submission.
Phase 1 (Discover) MUST, per country:
1. **Read the true total** from the status chips — the `Live N` / `All N`
numbers are your completeness target. The chip *label* (`Live` today)
is English on the noon partner console, but rely on the **number** and
the **`/campaign/details/` links** — both language-neutral — so this
works whatever the seller-market locale; don't key on the label word:
```bash
browser-use <<'PY'
print(js("return JSON.stringify([...document.querySelectorAll('*')].filter(e=>e.children.length<=2 && /^(Live|Paused|All)\\s*\\d+$/i.test(e.textContent.replace(/\\s+/g,' ').trim())).map(e=>e.textContent.replace(/\\s+/g,' ').trim()))"))
PY
```
2. **Scroll the list container to the bottom** (match its class by the
`CampaignListRevamp_` prefix — the hashed suffix changes per build;
fall back to any inner `overflow-y:auto` scroller taller than its
viewport). Repeat until the campaign-link count stops growing:
```bash
browser-use <<'PY'
for _ in range(12):
js("[...document.querySelectorAll('*')].filter(e=>{var s=getComputedStyle(e);return (s.overflowY==='auto'||s.overflowY==='scroll') && e.scrollHeight>e.clientHeight+50;}).forEach(e=>e.scrollTop=e.scrollHeight)")
wait(1)
print("links:", js('return document.querySelectorAll("a[href*=\\"/campaign/details/\\"]").length'))
PY
```
3. **Extract IDs** with the still-valid `a[href*="/campaign/details/"]`
read (§ 3) and de-dupe. Only when the distinct count matches the chip
total (e.g. `Live N`) is the manifest complete. Re-run this whole
loop **after every country switch** (`/en-{cc}/`).
4. **Persist the scope — required for audits.** Append this combo to
`./AUDIT_SCOPE.json` at the task root, with the de-duped ids **and**
the `Live N` chip number:
```json
{"combos": [
{"platform": "noon", "country": "AE",
"total_active": 45,
"total_active_source": "chip:Live 45",
"active_ids": ["C_DEMO0001", "C_DEMO0002"]}
]}
```
The server requires `total_active == len(active_ids)`, and rejects the
scope when they disagree. That is deliberate: the chip is rendered by
the server and does **not** depend on how far you scrolled, so a
half-scrolled list (20 ids, chip 45) is caught as stale instead of
being accepted as a complete `20/20` audit. If they disagree, keep
scrolling — don't "fix" it by editing the number down. Every id you
list must then get its own `### <id> | … ` drill block in the report.
Full field reference: `amazon-ads/references/audit-quickref.md` Step 1.
**`total_active_source` is required, and for noon it is the chip
reading** — `"chip:Live N"`, the number you read in step 1. Without
it the combo is a `[基线]` gap. Reason: `total_active ==
len(active_ids)` only proves the two numbers agree, not that either
was *observed* — trivially true when both come from the same parse
(observed live: a run declared a 12-campaign marketplace as `4/4`
because its script silently dropped files it couldn't read, and every
check passed). The chip form isn't verifiable from disk the way
Amazon's `"bulk:<file>.xlsx"` is, but writing the reading down turns
an invented total from an omission into a claim the reviewer can check
against the live page. Read the chip; don't back-fill it from the id
count.
## 3. Campaign Detail Page
**URL**: `admanager.noon.partners/en-{cc}/campaign/details/{campaign_id}?mpCode=noon&project=PRJ{project_id}`
Campaign ID format: `C_{alphanumeric}` (10 alphanumerics after the
underscore, e.g. `C_XXXXXXXXXX`).
**Campaign ID extraction** from the campaigns list (IDs are in
`<a href>` attributes, not visible text):
```bash
browser-use <<'PY'
print(js("""
var links = document.querySelectorAll('a[href*="/campaign/details/"]');
var data = [];
links.forEach(function(l) {
var m = l.href.match(/\\/campaign\\/details\\/([^?]+)/);
if (m) data.push({name: l.textContent.trim(), id: m[1]});
});
return JSON.stringify(data);
"""))
PY
```
> ⚠️ **The link extraction above only returns the rows currently
> rendered.** The list lazy-loads on inner scroll, so a raw read
> captures ~15–20 of what may be many more. **Enumerate the full set
> via the § 2 procedure** (read the `Live N` / `All N` chip totals, then
> scroll the list container until the distinct link count matches).
> Treat a single unscrolled read as a spot-check, never the full set —
> under-counting here fails the completeness gate.
**Campaign names can be misleading.** Verify actual products via
the **Products tab** — do not trust the campaign name. A campaign
named "mouse004 Auto" may target keyboard SKUs, not a mouse.
Header shows: campaign name, Status badge, Budget, **Top-of-Search
boost** (displayed as `Top Slot: N%` between Budget and Bidding
Strategy for manual campaigns with TOS configured), Bidding
Strategy, Running From date, Last Updated. **Top-right action icons:**
a date-range picker, **pause/resume**, **duplicate**, and a round
**blue pencil = Edit** (opens the campaign editor — see § 9 for
adding/removing keywords & negatives).
`target_filter` query param on the Targets tab switches the view:
`target_filter=all` (positive keywords, default) vs
`target_filter=negative` (negative keywords).
**Brand Ads have different CTR/ROAS norms.** Brand Video ads measure
view-through differently — never compare CTR directly to Product Ads.
A `brand video` CTR of 0.23% is not "weak" vs product ad peers at
1.7–6.7%. Compare Brand Ads only against other Brand Ads, or against
the brand ad's own historical ROAS.
**Brand Ad "Creative" row.** The Products tab may show a "Creative"
row (logo/video element) that accumulates clicks with zero attributed
orders — noon can't attribute conversions to the creative. If
Creative spend > 20% of campaign budget, flag it; calculate ROAS
both with and without the Creative row for true product performance.
KPI cards (same 10 metrics as overview, scoped to this campaign).
Performance chart with metric toggles: ROAS, Revenue, Spends, eCPC,
CTR, CvR, Orders, Clicks, Views, ATC.
**4 Sub-tabs on campaign detail:**
| Tab | Purpose |
|-----|---------|
| Products | SKUs in this campaign with per-SKU metrics |
| Placements | Ad placement performance |
| **Targets** | **Keywords** with match types and bids |
GitHubで見る