# Utopia Pricing Agent โ€” Architecture (engineering detail) The technical reference: data sources, decision cascade, formulas, file map, backtests. For the plain-English overview see **[ARCHITECTURE.md](ARCHITECTURE.md)**. A one-page Streamlit dashboard (Utopia/CRAI design system) that turns **live COSMOS data** into price recommendations a human can Approve / Modify / Reject. Read-only: nothing is written back to COSMOS or Amazon. --- ## 1. High-level view ```mermaid flowchart LR U["๐Ÿง‘ User
(same-network browser)"] --> APP subgraph APP["app.py โ€” presentation (Streamlit, CRAI theme)"] SIDE["Sidebar
Single product / Product line
+ filters, kill switch"] QUEUE["Recommendation queue
tiles ยท pills ยท rows"] SECT["Per-SKU sections
price ยท inventory ยท scenarios
competitors ยท PPC ยท costs ยท AI"] end subgraph DASH["dashboard/ package"] THEME["theme.py
CRAI tokens + plotly template"] LIVE["live_data.py
adapter + decision engine
+ scenario economics"] end subgraph CORE["src/pricing_agent โ€” analysis core"] AN["analyze.py"] MARGIN["margin_engine.py"] ELAST["elasticity.py"] PERF["performance.py"] SVC["cosmos/service.py"] CLIENT["cosmos/client.py"] end COSMOS[("COSMOS API")] APIFY[("Apify โ€” optional")] APP --> THEME APP --> LIVE LIVE --> AN AN --> MARGIN & ELAST & PERF AN --> SVC --> CLIENT --> COSMOS LIVE --> SVC AN -.optional.-> APIFY ``` --- ## 2. Layers | Layer | Files | Responsibility | |---|---|---| | **Presentation** | `app.py` | All rendering, zero pricing logic. Session state (approve/modify/reject, filters, per-SKU section + window), staged progress loader, session-state cache. | | **Design system** | `dashboard/theme.py`, `.streamlit/config.toml` | CRAI palette (cream `#f4f0e8`, teal `#0c8276`, coral `#df4f33`, navy `#22304e`), Inter font, plotly template. | | **Adapter + engine** | `dashboard/live_data.py` | Builds the per-SKU dict; **decides** the action; computes **scenario economics** (elasticity projection โ†’ bulk reconciliation โ†’ calibration); exposes `scenarios_for_window()`. | | **Competitive state** | `src/pricing_agent/competitive_state.py` | The one competitive fact the cascade may read. Adapts either scraper into a typed `WON`/`LOST_PRICE`/`LOST_ELIGIBILITY`/`SUPPRESSED` state with a source and a timestamp, gates it on age, and logs disagreement between sources. | | **Analysis core** | `src/pricing_agent/analyze.py` | Orchestrates one SKU: fees โ†’ trend โ†’ bulk โ†’ ad cost โ†’ elasticity โ†’ actual-profit evidence. | | **Money math** | `tools/margin_engine.py` | Pure: break-even, MAP, contribution margin, suggested price. | | **Statistics** | `elasticity.py`, `performance.py` | Log-log elasticity fit, profit-optimal sweep, actual-profit aggregation. | | **Data access** | `cosmos/{client,service,models}.py` | Auth + retry client; endpoint calls + response flattening; typed pydantic models. | --- ## 3. Data sources โ€” what each COSMOS endpoint feeds ```mermaid flowchart TB subgraph COSMOS["COSMOS API"] TH["/sales-insight/takehome-calculator
nested fees.breakdown ยท cost.breakdown"] INVP["/invp-insight
trend + inventory + dateMap PROJECTIONS"] BULK["/sales-insight/bulk-calculator
storage + total take-home"] SI["/sales-insight (daily, 6-month)
price ยท units ยท revenue ยท profit ยท ad spend"] PROD["/products
brand ยท marketplace"] CAMP["/api/campaigns ยท /adsApi
budget ยท ACoS ยท ad sales (not yet wired)"] end TH -->|"_flatten_takehome()"| FEES["Fee model
referral% ยท FBA ยท landed ยท returns"] INVP --> TREND["Velocity + cover days"] INVP --> INVPROJ["Inventory Outlook tab
real weekly units/value/cover/arrivals"] BULK --> STORAGE["Storage + take-home (scenarios)"] SI --> HIST["180-day daily series
(window filter + calibration)"] SI --> ADS["Ad spend / TACoS (PPC tab)"] PROD --> META["Brand / marketplace"] ``` **Three response quirks handled:** - **Fees come nested** (`fees.breakdown["Referral Fee"]`, `"$ 9.28"` strings). `service._flatten_takehome()` normalises them โ€” without it every fee parsed to 0 (the old "$0.99 / break-even $0" bug). - **INVP `dateMap`** holds COSMOS's own **forward inventory projection** (weekly units, value, cover days, warehouse arrivals). The Inventory tab renders this directly โ€” not a locally-invented forecast. - **SKU lookups are CONTAINS/relevance searches, not equality.** See ยง3.1 โ€” this one silently bound the wrong product's data to a SKU. ### 3.1 Exact-SKU joins (`service._exact_row`) Neither `/api/products` nor `/api/invp-insight` has an equality filter, and both used to fall back to `data[0]` "if COSMOS returned a fuzzy match set". That bound one product's ASIN, cost, brand and inventory to a **different product's SKU**. Observed live: `get_product("UBCFKFITTEDSHEETWHITECALKING")` โ€” a SKU COSMOS does not carry at all โ€” returned `UBMICROFIBERGUSSETPILLOWWHITEQUEEN` / `B08DTH86Q2`. Two independent problems, and each guard is necessary: 1. **Wrong parameter.** `get_product` queried `q=` (relevance across the whole catalogue). For `UBMICROFIBERDUVETTWINWHITE` the correct row sat on **page 2 of 100-row pages**, behind 100 unrelated products โ€” a 20-row lookup never saw it. `sku=` returns it first. 2. **`sku=` is still a CONTAINS filter.** `sku=UBCFKMATTRESSPROTECTORTWIN88` returns three rows: the real one (`B00MRH9NCK`), the `...BOX` variant (`B09K7HXJ4M`), and a `WAL...` Walmart row whose "ASIN" (`8946709597`) is not an ASIN. **All three are `marketplace: AMAZON_USA`**, so the marketplace check alone does not separate them โ€” only the exact SKU test does. `_exact_row()` requires an exact SKU match **and** the right marketplace, and returns `None` otherwise, logging what it rejected. Blast radius of the old behaviour: `analyze_price` assigns `asin = product.asin` whenever INVP has none, so the competitive scrape would have run against an unrelated listing; and `get_invp`'s `skuPrefix` matches every colour variant, so a sibling's inventory and cover days fed `LOW_STOCK` / `EXCESS_STOCK` directly โ€” a sibling reading 12 units / 3 days would fire a stockout raise on a SKU holding 4,000 units. Genuinely **not** in this COSMOS integration โ†’ shown as "โ€”", never faked: ad-attributed sales / ACoS / campaign budget (live in `/api/campaigns` + `/adsApi`, not yet wired), and historical competitor prices (Apify gives a current snapshot only). **Competitor data is no longer display-only.** COSMOS has no Buy Box, no rival price and no third-party offer anywhere in it โ€” that gap is filled by a scrape, and two of its facts (our Buy Box being suppressed, and a rival materially undercutting us) reach the verdict. They are the only two, they are bounded by the guardrails, and their absence changes nothing. See ยง6. --- ## 4. Per-SKU pipeline (one "Analyze") ```mermaid sequenceDiagram participant U as User participant A as app.py participant L as live_data.build_live_sku participant AN as analyze.py participant S as CosmosService U->>A: Single product / Product line A->>L: get_live_data(skus, progress_cb) Note over A: staged progress bar (3โ†’10โ†’45โ†’82โ†’94โ†’100%) L->>S: get_current_price L->>S: get_sales_history (180d, parallel windows) L->>AN: analyze_price (fees, trend, bulk, elasticity, evidence) L->>S: get_invp (real inventory projection) L->>S: bulk_quote (storage) L->>L: decide action + scenario economics + 30d/6mo calibration L-->>A: {summary, details, errors} (session-cached) A-->>U: queue + expandable per-SKU analysis ``` --- ## 5. Scenario economics (the heart of the Scenarios tab) For each candidate price, one consistent chain: ```mermaid flowchart LR W["Window filter
7/14/30/90d ยท 6mo"] --> BASE["Baseline velocity
= avg units/day in window"] BASE --> DEMAND["Units(p) = units ร— (p/cur)^elasticity"] DEMAND --> REV["Revenue = units ร— p ร— 30"] REV --> AD["Ad spend = TACoS ร— revenue"] DEMAND --> TH["Take-home (bulk calculator fee model)"] TH --> GROSS["Gross = take-home โˆ’ storage โˆ’ ad"] GROSS --> CAL["ร— realization factor
(actual booked รท modeled at current)"] CAL --> NET["Net profit / 30d"] ``` Key rules: - **Current row = FACT**, not a projection: real units, real revenue, real ad spend, real booked profit. Its price is the **average price sold** (revenue รท units) so `price ร— units ร— 30 = revenue` reconciles โ€” this is *below* list when promos ran, and changes with the window because the avg selling price differed period to period. The **list price is fixed**. - **Calibration:** raw bulk-calculator profit over-states reality (prices at list, ignores real returns/promos). A **realization factor** = actual booked profit รท modeled profit at the current price scales every projected row. - **The elasticity used for projections is gated** (`projection_elasticity()`). It honours the same `actionable` test the decision does: a slope whose 95% CI spans zero โ€” or a **positive** slope, which `estimate_elasticity` can return since `actionable` requires `e < 0` but the value is not clamped โ€” falls back to `FALLBACK_ELASTICITY`. Ungated, a positive slope projected that *raising* price sells *more*, and that number drives the 30-day impact tile, the portfolio opportunity total and the queue sort. `elasticity_is_fitted` records which was used. - **โญ** marks the highest-net-profit price in the current view. --- ## 6. Decision engine (deterministic, first match wins) ```mermaid flowchart TD S([signals]) --> R0{cost data = 0?} R0 -- yes --> INV["๐Ÿ” INVESTIGATE ยท NO_COST_DATA"] R0 -- no --> RB{our Buy Box suppressed?} RB -- yes --> INVB["๐Ÿ” INVESTIGATE ยท BUYBOX_SUPPRESSED"] RB -- no --> R1{price < break-even?} R1 -- yes --> UP1["โ†‘ raise to safe floor ยท BELOW_BREAK_EVEN"] R1 -- no --> R2{losing money after ads?} R2 -- yes --> UP2["โ†‘ raise ยท LOSING_MONEY"] R2 -- no --> R3{cover โ‰ค 35d?} R3 -- yes --> UP3["โ†‘ +5% ยท LOW_STOCK"] R3 -- no --> R4{โˆ’30% sales, no cause?} R4 -- yes --> INV2["๐Ÿ” INVESTIGATE ยท UNEXPLAINED_DROP"] R4 -- no --> R5{cover โ‰ฅ 90d?} R5 -- yes --> DN["โ†“ โˆ’5% ยท EXCESS_STOCK"] R5 -- no --> RC{rival โ‰ฅ3% below AND corroborated?} RC -- yes --> DNC["โ†“ toward rival ยท COMPETITOR_UNDERCUT"] RC -- no --> R6{profit-optimal โ‰  current?} R6 -- yes --> MOVE["โ†‘/โ†“ toward optimal ยท PROFIT_OPTIMAL"] R6 -- no --> R7{a price we ran booked more?} R7 -- yes --> BO["โ†‘/โ†“ toward it ยท BEST_OBSERVED"] R7 -- no --> HOLD["โ†’ MAINTAIN ยท NO_SIGNALS"] ``` Guardrails: floor = highest of four break-evens ร— 1.02, ceiling = current ร— 1.25; the recommended move is capped at ยฑ5% per step; the kill switch pauses all approvals. ### AD_SPIRAL โ€” applied after the cascade When ad cost per unit climbs almost as fast as price, each extra $1 of price buys only cents of contribution and **no** price reaches break-even. That verdict (`AD_SPIRAL`, Investigate-and-hold) is applied *after* the cascade and overrides whatever fired โ€” so what it must **not** override is named explicitly: ```python AD_SPIRAL_YIELDS_TO = frozenset({"NO_COST_DATA", "BUYBOX_SUPPRESSED", "LOW_STOCK"}) ``` | Yields to | Why | |---|---| | `NO_COST_DATA` | With `costPerUnit`/`fbaFee` at 0, `fixed` is understated, which makes the `contribution <= 0` test **easier** to hit. A missing-COGS SKU would be sent to the ad console when the fix is a data-entry field. | | `BUYBOX_SUPPRESSED` | A listing nobody can buy from has no meaningful ad economics. | | `LOW_STOCK` | The only one that changes a **price**, not just a label. A shelf about to empty gets +5% whatever the ad slope does โ€” those units sell regardless, so the only question is what we get for them. Holding sells the last of the stock cheap. | Deliberately narrow: `EXCESS_STOCK` does **not** outrank it, because cutting price to clear stock is exactly the move that cannot work when ads eat the contribution. ### Competitor rules โ€” the two that can move a price, and what bounds them Both branches read a single `CompetitiveState` ([competitive_state.py](src/pricing_agent/competitive_state.py)), never a raw scrape: | Rule | Fires when | Effect | |---|---|---| | `BUYBOX_SUPPRESSED` | Amazon is not showing our offer | **Investigate, hold.** Placed directly under `NO_COST_DATA`: the only two states where the answer is "go and fix something" rather than "set a price". | | `COMPETITOR_UNDERCUT` | cheapest rival โ‰ฅ `competitor_undercut_material_pct` below us, **and the basis qualifies** (below) | **Decrease toward the rival**, floored and step-capped like every other branch. | **Basis decides what "qualifies" means**, because `competitor_min` can be a price of two different things: - `same-asin-buybox` (Apify) โ€” another seller's offer on **our own listing**. Only `LOST_PRICE` fires; a rival holding the Buy Box *above* us is `LOST_ELIGIBILITY`, where cutting donates margin. Losing the Buy Box on price **is** the corroboration. - `like-for-like-sheet` โ€” a **rival brand's** equivalent variant, matched on size + colour. This additionally requires **corroboration**: either demand has materially dropped, or we are not actually winning the Buy Box. *Why:* on its own a sheet row says only "a different brand is cheaper". True, reportable, but not evidence the gap is costing us anything โ€” we can sit 3% dearer, hold our own Buy Box and sell fine on brand, reviews or the Prime badge. And because this branch sits **above** `PROFIT_OPTIMAL`, an uncorroborated cut could overrule an elasticity fit that wanted a *raise*. Switch: `competitor_sheet_requires_corroboration` (default `true`). The documented `UNEXPLAINED_DROP` fall-through is unaffected โ€” `UBMICROFIBERDUVETKINGPURPLE` and `UBMICROFIBERBS4PCFULLGREY` arrive here *with* a velocity drop, which is the first form of corroboration. An uncorroborated material undercut is still **reported** in the root cause ("Competitor undercut not acted on"), so it never looks like missing data. A third signal, a **competitor premium** while we hold the Buy Box, is a narrative note only. It never sets a price and never changes an action. Ordering is deliberate: **inventory risk outranks competitor position, which outranks profit-optimal.** Chasing a rival down while the shelf is emptying pays margin to sell out faster. Three properties make this safe to ship: 1. **Fail-safe.** Absent, failed, stale (> `competitor_state_max_age_hours`) and "ownership unknown" all collapse to one flag, and the cascade then computes exactly the verdict it computed before competitor data existed. Competitor data can only ever *add* a verdict. 2. **Never below break-even.** The rival price is a *candidate* (`comp_match`), not a decision. 3. **One named reason per verdict.** No blended scores โ€” every fired rule is traceable to a single reason code, and `logger.info` names the SKU, the rule, the state and the source. Thresholds live in [config/pricing_rules.yaml](config/pricing_rules.yaml) (`competitor_undercut_material_pct: 0.03`, `competitor_premium_material_pct: 0.10`, `competitor_state_max_age_hours: 6.0`, `competitor_sheet_requires_corroboration: true`), not in code. The 3% floor sits above the ~2% band our own realized price already swings through as coupons toggle. ### Inventory cover matches COSMOS Inventory Planning `cover_days` **is COSMOS's own `coverDays`**, so the dashboard and the INVP grid never quote two different numbers for one SKU. COSMOS counts **inbound** stock against a **7-day** velocity, so it reads longer than what is on the shelf โ€” `UBMICROFIBERDUVETTWINWHITE` is 78 days on (3,999 on hand + 1,030 inbound) รท 64/day, against 63 on-hand-only. Both are reported: the tile leads with the matched figure and appends `63 d on hand, rest inbound`. The on-hand figure remains the **fallback**, because COSMOS returns `coverDays: 0` on some very low-velocity SKUs that hold months of stock (`UBMICROFIBERBS4PCFULLGREY`: 167 units, 334 real days, COSMOS said `0`). Zero satisfies neither inventory rule, so taken literally it silences both. The reconciled figure is computed **before** `_decide` and passed in, so the tile and the rule that fired read the same number by construction. **Trade-off, accepted deliberately:** stockout risk is now judged partly on stock that has not landed. Measured over the 56-SKU covered line, matching COSMOS moved 7 verdicts โ€” `LOW_STOCK` 7 โ†’ 4, `EXCESS_STOCK` 18 โ†’ 22. The one to watch is `UBMICROFIBERBS4PCKINGWHITE`: **12 days on the shelf, 84 with inbound**, so it no longer raises. If that shipment slips, nothing protects it. Display bands are COSMOS's Alpha/Beta scheme (`theme.COVER_BANDS`, Alpha 20/40/70/100). The pricing **triggers** are separate and live in `pricing_rules.yaml` (`low_cover_days: 35`, `high_cover_days: 90`) โ€” COSMOS's pink at 70 days is a *replenishment* warning, while crossing a trigger here spends margin on a 5% move. ### Coverage: the comparison sheet gates competitor data, one product line at a time The competitor workbook currently covers **one product line**, so the engine reads it as the first competitor source and **gates on coverage**: | SKU | Competitor state | |---|---| | In the sheet | Priced from the sheet โ€” real like-for-like rival prices, `basis=like-for-like-sheet` | | Not in the sheet | **`N/A`**, naming what the sheet *does* cover. No rule fires; the verdict is byte-identical to the competitor-blind one | **Coverage is the exact SKU set in the sheet, not a line prefix.** Measured against the real workbook, a prefix gate would be wrong in both directions: the `UBMICROFIBERDUVET` run contains 49 `UBMICROFIBERDUVET*` SKUs **and 7 `UBMICROFIBERBS4PC*`** ones, while the line has 139 SKUs in COSMOS of which only 56 reached a comparison row. So the sheet's own SKU list is the authority, and "not in the sheet" is a **coverage hole**, never a claim that the SKU has no competitors. `competitor_sheet_only: true` (default while one line is under test) means an uncovered SKU gets N/A rather than falling through to a per-ASIN Apify scrape. Config: `competitor_sheet_path` (blank = auto-discover the newest `Competitor_Price_Comparison_*.xlsx`), `competitor_sheet_dirs`, `competitor_sheet_max_age_hours: 168` (the sheet is a 25โ€“35 min batch run, not a live feed). Two sheet-driven refinements, both from real rows: - **A rival whose own Buy Box is suppressed is excluded from the band.** Their price is not buyable, so undercutting it donates margin for nothing. - **A material undercut can explain a velocity drop** (subject to the corroboration rule above), converting an `UNEXPLAINED_DROP` Investigate into an actionable verdict โ€” exactly as the existing stockout branch already did. ### Two scrapers, one state | Source | Authoritative for | Why | |---|---|---| | **Apify** (`tools/amazon/apify.py`) | **Buy Box state read by the engine** | The only source carrying a seller id, so the only one that can tell `WON` from `LOST_PRICE` from `LOST_ELIGIBILITY`. Those lead to opposite actions. | | **Playwright** (`../scraper/`) | The workbook: like-for-like size/colour matching, BSR, demand buckets, SKU gaps | Apify cannot produce any of it. Its Buy Box field knows only whether a price *rendered*, not whose it was. | The split is by **question**, not preference. `reconcile()` cross-checks the authoritative state against the Playwright run's own cache (`scraper/.scrape_cache.json`, keyed `ASIN@ZIP`) and **logs any disagreement**. A disagreement never changes the verdict. --- ## 6.1 Verdict backtest `scripts/backtest_competitor_rules.py` re-runs the **captured real cascade inputs** (real `AnalysisResult`, real 180-day history, real fee stack, real scenario grid) through the same `_decide`, varying only the arm. Nothing is reconstructed by hand. Competitor rules blind vs live (7 requested SKUs, 6 analysed โ€” see ยง9): | Arm | Verdicts changed | |---|---| | Real competitor state | **0 / 6** โ€” the fail-safe working | | Counterfactual 8% undercut | 4 / 6 โ€” the others blocked by `LOW_STOCK` / `LOSING_MONEY` | | Counterfactual suppression | 6 / 6 โ†’ Investigate | Policy delta, old vs new (the two cascade changes), same SKUs: | Case | Changed | Notes | |---|---|---| | `SHEET_UNDERCUT_WE_WIN` | 2 / 6 | Both were being **cut while holding the Buy Box with flat demand**: `UBMICROFIBERDUVETTWINWHITE` Decrease $17.06 โ†’ **Increase $17.94** (`BEST_OBSERVED`), `UBMICROFIBERGUSSETPILLOWWHITEQUEEN` Decrease $25.01 โ†’ **Increase $27.37**. The two corroborated SKUs were unchanged. | | `AD_SPIRAL_LOW_STOCK` | 4 / 6 | `Investigate/AD_SPIRAL` โ†’ `Increase/LOW_STOCK` | | `AD_SPIRAL_NO_COST` | 6 / 6 | `Investigate/AD_SPIRAL` โ†’ `Investigate/NO_COST_DATA` | | **Targets below break-even, any arm** | **0** | | **The invariant is measured on the TARGET, not the step-capped first move.** A SKU already selling under its own ad-inclusive floor cannot be lifted over it in one 5% step, and reporting that deliberate multi-cycle climb as a breach buries any real one. Two SKUs are below floor and stepping up by design; both are reported separately. --- ## 7. Key formulas | Quantity | Formula | |---|---| | Take-home / unit | `pยท(1 โˆ’ referral% โˆ’ returns%) โˆ’ landed โˆ’ FBA โˆ’ other` | | Break-even | `(landed + FBA + returns + other) / (1 โˆ’ referral%)` | | Elasticity | OLS on `ln(units/day) = a + eยทln(price)` over 6 months | | Scenario demand | `units ร— (p / pโ‚€)^e` | | Realization factor | `actual booked profit (window) รท modeled net at current price` | | TACoS | `ad spend รท total revenue` (window) | | Avg sold price | `revenue รท units` (window) โ€” reconciles the Current row | | Best observed price | `avg_price` of the best-earning band โ€” **the price actually charged**, never the $0.50-rounded `price_band` key | --- ## 8. Repository map ``` pricing_agent/ โ”œโ”€โ”€ app.py # dashboard (presentation only) โ”œโ”€โ”€ legacy_app.py # previous analyst UI (still runnable) โ”œโ”€โ”€ dashboard/ โ”‚ โ”œโ”€โ”€ theme.py # CRAI design tokens + plotly template โ”‚ โ””โ”€โ”€ live_data.py # COSMOS adapter, decision + scenario engine โ”œโ”€โ”€ src/pricing_agent/ โ”‚ โ”œโ”€โ”€ analyze.py # per-SKU orchestration โ†’ AnalysisResult โ”‚ โ”œโ”€โ”€ competitive_state.py # canonical Buy Box state + two-scraper reconciliation โ”‚ โ”œโ”€โ”€ elasticity.py # demand model + profit optimizer โ”‚ โ”œโ”€โ”€ performance.py # actual-profit evidence โ”‚ โ”œโ”€โ”€ tools/margin_engine.py # pure fee/break-even math (golden-tested) โ”‚ โ””โ”€โ”€ cosmos/ โ”‚ โ”œโ”€โ”€ client.py # auth + retry HTTP โ”‚ โ”œโ”€โ”€ service.py # endpoints, _exact_row, _flatten_takehome, INVP projections โ”‚ โ””โ”€โ”€ models.py # typed COSMOS responses (pydantic) โ”œโ”€โ”€ config/ # settings + pricing_rules.yaml (incl. competitor thresholds) โ”œโ”€โ”€ scripts/ โ”‚ โ””โ”€โ”€ backtest_competitor_rules.py # verdict delta + old-vs-new policy delta โ”œโ”€โ”€ .streamlit/config.toml # CRAI theme โ””โ”€โ”€ tests/ # margin-engine golden values, test_competitor_rules.py # (fail-safe + ordering invariants), test_pricing_safety.py, # test_cosmos.py (exact-SKU joins) ``` --- ## 9. Known data gaps - **Ad-attributed sales / ACoS / campaign budget** โ€” live in `/api/campaigns` + `/adsApi`, not yet wired. Shown as "โ€”". - **Historical competitor prices** โ€” Apify returns a current snapshot only. - **`UBCFKFITTEDSHEETWHITECALKING` is not in COSMOS at all.** `sku=` returns zero rows on any marketplace and the fee endpoint replies "Product not found". Likely delisted or renamed. It now fails loudly (`get_product` โ†’ `None`) rather than adopting another product's identity. A catalogue question, not a code one. --- ## 10. Principles 1. **Deterministic core, narrative shell** โ€” every number is a formula over COSMOS data; language models only phrase explanations. 2. **Read-only** โ€” the agent proposes; a human approves; nothing writes back. `submit_price_approval` remains a stub with no callers. 3. **Honest gaps** โ€” missing upstream data shows "โ€”" or an explicit investigation, never a fabricated number. A wrong number is worse than a blank one. 4. **Facts vs projections are labeled** โ€” the Current row is real booked history; other prices are clearly modeled. 5. **Everything reconciles** โ€” one averaging window drives units, revenue, ads and profit so `price ร— units = revenue` always holds.