---
type: plan
status: delivered
created: 2026-08-21
---

# DFP Proof of Concept — plan

**Goal:** working PoC of the dutyfreeprofessor pricing/directory app, sent to Adam
**Thu Aug 27** (internal freeze Wed Aug 26; promised Fri Aug 28). $1,500 fixed.
Approved by Adam 2026-08-21: "I approve the US $1500 POC project to go forward and
primarily working with you... no Hammish for now... please start." Note: Adam chose
rian ALONE, dropping the run-both hedge rian offered — there is no parallel safety
net. Delivery is everything.

## Success criteria (what Adam must be able to do from Europe)
1. Open a link (id-auth shared-password gate) and browse/search real duty-free products.
2. Open a product and see prices for it across ≥2 airports, **collected by our
   pipeline from live retailer sites** (not hand-entered), with collection timestamps.
3. See at least one product carrying a real competition medal badge (genuine data from
   the bw-winners system).
4. Category browse (spirits first), navy/gold look echoing the wireframe direction he
   has already seen.
5. Watch a 3–5 minute video walkthrough. No call required.

## Explicitly OUT of the PoC
Alerts, the interactive map, editorial/CMS, user accounts, anything DFA, the
hub/multi-vertical platform, 200 locations, scheduled refresh automation (collection
runs manually for the demo; cadence is a review-meeting decision).

## Architecture decisions (2026-08-21, from rian's questions)

**D1. The fetch layer is a swappable interface — this is the anti-lock-in decision.**
One function: `fetch(url, opts) -> Page`. Retailer adapters NEVER call a vendor SDK
directly. If a supplier raises prices, degrades or dies, we change one config line.
Vendor risk becomes a line-item, not an architecture. Implementations, in order:

1. **`curl_cffi`** (MIT, v0.16.1 released 2026-08-21) — free, and the correct tier-1
   attempt. **NOT plain `httpx`/`requests`:** a Chrome User-Agent paired with a Python
   TLS fingerprint is flagged as a bot by every major vendor, so naive HTTP fails on
   the TLS handshake before headers are even read. `curl_cffi` impersonates real
   browser TLS/HTTP2 fingerprints, which is the actual bar.
2. **Headed Playwright** for sites that render prices in JavaScript but don't block.
   Run **headed, not headless** — measured soft-block rates are 15.2% headless vs
   7.2% headed, and 75% of headless-only blocks come from header-level giveaways
   (`sec-ch-ua`, UA leaking `HeadlessChrome`) that are nearly free to fix.
3. **A paid unblocker: only if a site tolerates collection but is merely awkward —
   never to defeat an active block** (F3-rev). If it's ever warranted, **Zyte API**
   is the best structural fit (charges only for successful responses, auto-selects
   the cheapest technique per target, ~$0.13–1.27 per 1,000 HTTP requests) and
   **Oxylabs** is the best value in the premium tier (~3–5× cheaper than Bright
   Data). **Firecrawl is a fine tool but the wrong default here** — see D1c.

**D1c. Firecrawl specifics, if we do use it.** v2 API, `POST /v2/scrape`. Monthly-billed
(not the annual rates most articles quote): Free $0/1k credits · Hobby $19/5k ·
Standard $99/100k · Growth $399/500k · Scale $749/1M. Three things to know:
(a) use the **`product` format at 1 credit**, never `json` LLM-extraction at **5
credits** — `product` is deterministic and e-commerce-specific, so `json` mode is a 5×
markup for worse determinism; (b) **credits do not roll over** below the Scale tier;
(c) **the account is team-scoped, so decide ownership before signing up** — an
agency-owned key means Adam can't take the product elsewhere without a migration; a
client-owned key means rian debugs billing he can't see. **Recommend: Adam's account,
from the start.** Also note **self-hosting is a trap here** — the AGPL core excludes
both the `product` format and the Fire-engine anti-bot layer, i.e. exactly the two
reasons to pick Firecrawl. Self-hosted Firecrawl is Playwright with a nicer API.

**D1d. Use Crawlee as the orchestration layer (Apache-2.0, free, runs anywhere).**
This is the piece that otherwise gets re-implemented badly: request queues, dedup,
retries with backoff, session/proxy rotation, concurrency limits, dataset storage,
with `HttpCrawler`/`ParselCrawler`/`PlaywrightCrawler` behind one API. Critically it
ships an **adaptive crawler that decides per request whether JS rendering is actually
needed** — which is our tier-1↔tier-2 problem solved in-library rather than
hand-maintained. It does **not** require the Apify platform.

**D1a. Licence hygiene — Adam owns this code, so AGPL is disqualifying.** `nodriver`
and `zendriver` are **AGPL-3.0** and must not enter the codebase: AGPL's network clause
would attach to a hosted app Adam owns and could be asked to distribute. Safe:
`curl_cffi` (MIT), SeleniumBase (MIT), Patchright (Apache-2.0), Camoufox (MPL-2.0).
Check the licence of anything added to the collector stack before importing it.

**D1b. Detection landscape context (2026), for expectation-setting.** Blocking tracks
the CDN, not the retailer's size — Cloudflare ~37% of sites, Akamai ~26%, AWS ~16%.
Defences have moved from IP reputation (being actively de-weighted by the defenders
themselves, because of collateral damage to shared/CGNAT addresses) toward
**fingerprint coherence** and **session-level behavioural scoring** (Cloudflare's
Precursor, live at ~206M evaluations/day). Two practical consequences: randomising
fingerprints harder makes things *worse* — cross-property consistency is what's
checked, so a few coherent identities beat many random ones; and residential egress is
necessary but no longer sufficient. This is why the honest framing to Adam is "ongoing
upkeep," not "solved once."

**D2. Adapters are per retail PLATFORM, not per airport — and the abstraction must be
proven with n>1 in the PoC.** Building only Avolta would produce an Avolta-shaped app.
So Heinemann ships in the PoC specifically as the second, differently-built adapter that
forces the interface to be honest. Interface: `discover()`, `list_page()`,
`parse_product()`. Adding an airport already on a supported platform is config, not code.

**D3. The canonical data model is retailer-agnostic.** No Avolta assumptions in the
schema. Identity resolution (deciding Athens' bottle == Heathrow's bottle) is OUR layer:
normalize brand + product + volume + ABV into a fingerprint, with manual override in the
admin. This is the same machinery that matches medals to products, so it's built once
and used twice.

**D4. Auth is app-native, NOT the BW/id-auth system.** Rian promised Adam "the code is
yours, movable to DigitalOcean" — id-auth Pattern B would silently break that promise by
coupling the app to rian's server. So: app-native accounts (email + password + sessions,
roles admin/editor/viewer) in the app's own database. The demoing.info id-auth gate stays
in front during development, but that's transport-level and creates no code dependency.
Auth is NOT built in the PoC (no accounts needed to demo); it's week 2+.

**D5. Medals come from the existing IBC competitions, not DFA.** DFA has run no
competitions and will have no winners at launch. The `awards` table is generic
(competition, year, medal, product_id) and is populated from: (a) the bw-winners system
on the apnetwork WordPress network via `srv-gw db-query` — real NY/Berlin/Melbourne/Asia
winners, which is the PoC's test data; (b) third-party awards already published on the
retailer product pages (IWSC, SFWSC etc.), captured free during scraping; (c) DFA later,
automatically, when it exists. Note (b) is a genuine bonus: it yields a record of which
award-winning products are actually stocked in duty free, which is a competition-entry
sales list for Adam.

## Verified landscape findings (research 2026-08-21) — these change the build

**F1. One Avolta adapter = 168 airports.** Verified: `london-heathrow.worlddutyfree.com`
and `athens.shopdutyfree.com` are literally the same Magento 2 app (identical
`Dufry_*` custom modules, differing only by theme + store id). The platform's own
embedded `locationconfig.locations` registry at `www.shopdutyfree.com/en/` lists
**171 storefronts = 168 airports + 2 seaports + 1 land border, 45 countries**
(Europe 84, LatAm 43, N.America 22, Africa 11, Asia 10, Oceania 1). That registry is
the source of truth — pull it periodically. **Do NOT probe DNS to find storefronts:**
dead legacy subdomains still resolve (`melbourne`, `dusseldorf` 301 to the hub;
`lisbon`, `nice`, `faro` return 400). Avolta's platform is sharded into ~7 regional
instances (div1 Spain/global, div3 Asia, div4 MX/CAM, div5 N.America, div6 UK, div8
Brazil, div9 Greece) — shard the crawler the same way; rate limits differ per division.

**F2. Coverage — SUPERSEDED AND CORRECTED 2026-08-21 (see F2-rev).** An earlier
research pass concluded Dubai had no e-commerce and that only 2 of the top 10 busiest
airports were reachable. **That was wrong**, and it was briefly relayed to rian as
fact. Corrected below. Kept visible rather than deleted so the error isn't
re-introduced by a future session reading old notes.

**F2-rev. Coverage, verified first-hand 2026-08-21 (curl, live).**

*The headline correction:* **Dubai Duty Free is the single best data source in the
industry, not an impossibility.** `www.dubaidutyfree.com` runs Oracle Commerce Cloud
with an **open, unauthenticated REST API**: `/ccstore/v1/products?limit=N&offset=N`
returns **33,871 products** with `displayName`, `brand` and `listPrice` in AED.
Verified live (Grant's Triple Wood 1L AED 87.00; Cutty Sark 1L AED 95.00). Product
pages also carry server-side JSON-LD `Product`+`Offer`, there's a product sitemap of
~13k URLs refreshed daily, and **robots.txt is permissive** — only `/cart`,
`/checkout`, `/profile`, `/searchresults`, `/wishlist` are disallowed. No login, no
flight details, no geo-block. DXB is the world's #1 international airport and Dubai
Duty Free is the largest single-airport retailer on earth (US$2.378bn, 2025). **Make
this adapter #2 — it is higher value and lower effort than anything else available.**

*Top-10 busiest international airports, revised:*
| # | Airport | Status |
|---|---|---|
| 1 | Dubai DXB | ✅ **open API, 33,871 products** |
| 2 | London LHR | ✅ Avolta **plus** Heathrow Boutique (see below) |
| 3 | Seoul ICN | ⚠️ Lotte publishes USD prices but on a *national*, not per-airport, storefront; the two research passes disagree on accessibility — verify before relying |
| 4 | Singapore SIN | ⚠️ Shilla; grid client-rendered; national not per-airport |
| 5 | Amsterdam AMS | ❌ Lagardère — no public storefront |
| 6 | Istanbul IST | ❌ Heinemann's API exposes catalogue *size* for IST but withholds products |
| 7 | Paris CDG | ⚠️ Extime — flight-gated |
| 8 | Hong Kong HKG | ✅ Avolta |
| 9 | Frankfurt FRA | 🚫 Heinemann — reachable but robots-restricted (see F5) |
| 10 | Doha DOH | ❌ **verified none** — Qatar DF's checkout sells raffle tickets only |

*So the honest line is:* three of the top ten are cleanly reachable including the
single biggest, several more are partial, and a handful genuinely publish nothing.
Not "nobody can build this" — but real, uneven coverage that must be shown honestly
in the UI.

**F2a. Heathrow Boutique is a high-value target and it publishes RRP.**
`boutique.heathrow.com` is run by Heathrow Airport Limited itself as a *multi-retailer
marketplace* — SKUs are namespaced by retailer (`world-duty-free_7279207`), so Avolta's
World Duty Free is the duty-free price source inside Heathrow's own storefront, and
Boots/Harrods/Kurt Geiger sit alongside. Salesforce Commerce Cloud, server-rendered,
full JSON-LD `Product`+`Offer`, **prices visible with no login and no flight details**
(flight details gate *availability* only). **Critically it publishes a strike-through
RRP** — "You save £19.50 compared to RRP." That directly answers "is duty free actually
cheaper?", which is one of the article titles already on Adam's placeholder site. Its
robots.txt (verified) disallows search/faceting/`Product-Show` but permits canonical
category and product paths; its sitemap 500s, so enumerate via the A–Z brand index.

**F2b. Trivially open Shopify catalogues — verified live.** Both expose
`/products.json?limit=250&page=N` with full variant, SKU and price data, no blocking:
- **`montrealdutyfree.ca`** (ARI, Montreal) — CAD. Verified: Tom Ford Bois Pacifique
  EDP CAD 215.00, sku 936136.
- **`pa.attenza.net`** (Motta/Attenza, Panama) — USD, with sibling storefronts for
  Colombia (`co.`), Ecuador (`ec.`) and El Salvador (`es.`). Verified: Philips
  OneBlade USD 34.00.
These are near-zero-effort adapters and add Americas coverage cheaply.

**F2c. Online price ≠ in-store price, systematically.** Heathrow Boutique shows "10%
OFF ONLINE"; Japanese pre-order shops give ~5% off the duty-free price. The online
discount is structural, not promotional. **Model online and in-store as separate price
types** and label which one is shown, or the app will confidently misinform someone
standing in the shop.

**F3. SUPERSEDED by F3-rev.** This previously read "Firecrawl is probably REQUIRED."
That framing was wrong and was relayed to rian as advice. See F3-rev.

**F3-rev. Do not escalate against sites that block. Build on the open doors and ASK
for the closed ones.** Three findings converge on this:

1. *Technical:* Avolta's blocking is **path-dependent**, not site-wide. Its homepage
   serves 500KB+ to plain curl; the **catalog/search paths return HTTP 202 with a
   zero-byte body** — an Akamai soft-block that looks like success. (Always validate
   on content, never on status code. That rule stands regardless.)
2. *Legal:* defeating a bot wall is the centre of the current litigation wave
   (§L3/L4), and in the **EU/UK the analogue is criminal, not civil** — CAPTCHA
   defeat, rate-limit evasion and identity rotation to bypass blocking edge toward
   the Computer Misuse Act 1990 and Directive 2013/40/EU. The stealth toolchain and
   the premium "unblocker" vendors exist precisely to cross that line.
3. *Commercial, and decisive:* **Adam's business is relationships with these very
   operators.** He is going to Cannes to meet them; his competitions solicit entries
   from their suppliers. An operator discovering that Professor Media Group evades
   their bot protection damages the competitions, not just the app. **The
   reputational exposure dwarfs the legal exposure**, and no engineering upside
   justifies it.

**So the rule is: if a site serves us politely, collect it. If it blocks, that is the
site saying no — log it as a partnership target and move on.** There is more than
enough open data to build a compelling product (F2-rev, F2a, F2b, F9). Avolta's 168
airports become the *prize* Adam negotiates for at Cannes rather than something we
take. That is also a far better story for him to tell in a meeting.

**F9. Heinemann's GLOBAL catalogue is open, robots-permitted, and solves the hardest
problem in the product — verified first-hand.** `GET /en/global/search/results?q=:relevance&page=N`
returns JSON: **10,336 products**, 40/page, no bot challenge, plain HTTP. The record is
extraordinarily rich — verified fields include **`gtin` and `ean` (the EAN barcode)**,
`name`, `brand`, `price`, `strikethroughPrice`, `alcoholByVolume`, `contentUnit` /
`numberContentUnits` (size), `stock`, `categoryPath`, `travelRetailExclusive`, `images`.
Live sample: *Brothers Bond Regenera Whiskey 46% 0.7L, €59.99, gtin 1240000024036.*

**Why this matters more than any price source:** deciding that Athens' bottle and
Dubai's bottle are the same bottle is the hardest unsolved problem in a comparison
product, and fuzzy-matching "Clarins Beauty Flash Face Care Set 50ml" across five
retailers is miserable. **A GTIN turns matching into a database join.** Use Heinemann's
global catalogue as the **product-identity spine** the other sources attach to.

**Scope limit, and it is a deliberate ethical line, not a technical one.** robots.txt
permits `/en/global/` but disallows every per-airport view (`/*/fra/`, `/*/vie/`…), and
**explicitly blocks `ChangeDetection` — a price-monitoring bot — by name**, alongside
`CCBot`. Their intent about price monitoring is unambiguous. So: **take the global
catalogue for product identity; do NOT mine per-airport prices from it**, even though
the JSON exposes `airports` and `allPosPrices` fields. Per-airport Heinemann pricing is
a partnership ask.

**F10. The 40× decision that dominates cost: harvest LISTING pages, not product pages.**
At ~30 retailers × ~8,000 SKUs = 240,000 products, weekly refresh:
| Strategy | Requests/refresh | Per month |
|---|---|---|
| Listing pages (40 products/request) | ~6,000 | **~26,000** |
| One request per product | 240,000 | **~1,040,000** |
Same data, **40× the volume**. At listing-only volumes this is a ~$0–99/month problem
on any vendor, or free on plain HTTP; at per-product volumes it is $500–$5,500/month.
**Vendor choice moves cost ~3×; this decision moves it 40×.** Heinemann proves it's
achievable — its listing JSON already carries name, brand, GTIN, price,
strikethrough, stock, size and images with no product-page visit at all. **Design rule:
get every field from listing/search endpoints wherever possible; treat product-page
fetches as the exception.** Worth noting this decision does not appear anywhere in the
competing proposal, whose cost model is built on per-page credits.

**F4. "The price" is becoming a distribution, not a fact.** Three compounding effects:
(a) Heathrow's store config sets `force_customer_destination_selection: 1` — on
per-airport storefronts, assortment AND price depend on declared destination, terminal
and flight date, so the price key is `(airport, terminal, destination-region, date)`;
(b) Avolta's CEO confirmed a **dynamic pricing rollout** varying price by customer
profile, nationality, market, origin/destination, seasonality and flight day; (c)
**Club Avolta member prices (16m members, 7% of group revenue) are invisible to
anonymous collection.** Design consequence: store timestamped *observations with
context*, never a canonical price, and be careful what the UI promises. Language like
"public list price observed on <date>" is honest; "the price at Heathrow" is not.

**F5. Heinemann stays OFF the crawl list — conflict resolved by direct check.** The two
research passes disagreed about Heinemann's robots.txt. **I fetched it: the restrictive
reading is correct.** It explicitly disallows `/*/fra/`, `/*/vie/`, `/*/ber/`,
`/*/ham/`, `/*/bud/` and every other per-airport view, both bare and with trailing
slash. Heinemann is technically the best-engineered source in the sector — full
`Product`/`Offer` JSON-LD, an open SAP Hybris OCC API, and the **only** operator
publishing `gtin13`, the universal cross-retailer join key — but the per-airport data
we'd want is exactly what they've asked crawlers not to take. **Treat it as the
strongest argument for a data partnership, not as a target.** That conversation is a
Cannes agenda item.

**Revised adapter order for the PoC:**
1. **Avolta** — breadth (168 airports), needs a paid fetcher to clear AWS WAF (F3).
2. **Dubai Duty Free** — open API, 33k products, permissive robots, the world's biggest
   single duty-free store (F2-rev). Highest value-to-effort ratio available.
3. **A Shopify one** (`montrealdutyfree.ca` or `pa.attenza.net`) — an afternoon's work,
   proves the adapter interface across a third stack, adds Americas coverage (F2b).

ARI Dublin/Cork and Heathrow Boutique are strong follow-ons (both Salesforce Commerce
Cloud, both JSON-LD, Heathrow adds RRP) — take them if the week allows, otherwise
they're the first post-review additions.

**F6. Compliance is the real constraint, not engineering.** Lotte: `Disallow: /` for
all generic crawlers. Shilla: explicitly bans ClaudeBot/GPTBot/CCBot and sets
`ai-train=no`. Heinemann: see F5. Avolta: `Crawl-delay: 60`, disallows `/catalog/` and
all query strings, but NOT category/product paths. **Rule for this build: honour
robots.txt, identify our bot honestly, rate-limit to the declared crawl-delay, cache
aggressively.** Where robots forbids what we need, the answer is a conversation with
the retailer — which is exactly what Adam is going to Cannes to have.

**F6a. The direction of travel favours the partnership play.** Robots.txt is becoming
mechanically enforced rather than advisory (Cloudflare auto-translates it into firewall
rules; ignoring `crawl-delay` gets a bot removed from verified-bot allowlists), the EU
AI Act's GPAI Code of Practice makes RFC 9309 compliance a named commitment for its
signatories, and the major CDNs are standing up **sanctioned, authenticated, paid**
agent channels (Agentic Commerce Protocol from Stripe/OpenAI; Akamai partnering with
Skyfire and TollBit) while the anonymous HTML path keeps hardening. Read across to this
project: scraping public prices politely stays viable, but the durable version of this
business is a **data relationship with retailers** — which is precisely the
conversation Cannes exists for. Frame it to Adam as where the industry is heading, not
as a workaround.

**F7. Industry facts worth having (for the app AND Adam's pitch).**
Market 2025: **US$75.95bn, +2.4%** (Generation Research) — still ~88% of the 2019 peak
nominally, ~70% in real terms, *while passenger numbers are 6.5% ABOVE 2019*. So spend
per passenger is down ~35% in real terms: more travellers buying less. Category split
H1 2025: fragrance/cosmetics ~32%, **wines & spirits ~18% and the laggard**, fashion
~14% (now #2 globally, having overtaken spirits), **tobacco +13.2% and the fastest
growing**. Two stats that argue FOR this product: **77% of travellers want a single
unified digital platform and 30% say it would make them spend more, yet only 30% have
any digital relationship with an airport** (Airport Dimensions AX26, 11,000+
travellers, April 2026). One that argues against, and must be handled honestly:
m1nd-set finds **price advantage fell from 30% (2019) to 13% (2024)** as a stated
reason to buy. The credible rebuttal — to test, not assume — is that price faded as a
*stated* driver precisely because duty-free pricing is opaque and travellers stopped
trying to compare, which is the gap this fills.

**F8. Cannes confirmed:** TFWA World Exhibition, **27 Sep – 1 Oct 2026**, Palais des
Festivals. 2025 edition: 7,999 visitors, 482 exhibitors — the industry's main annual
deal-making event. Also note **TR Consumer Forum, 1–3 July 2026, Geneva** — the most
consumer-app-relevant event on the calendar, for next year's planning.

## Build scope
- **Collectors:** **Avolta adapter** (one adapter, 168 airports — see F1; start with
  Heathrow + Athens, whose shared product IDs make cross-airport matching automatic)
  + **ARI / The Loop adapter** (Dublin + Cork — the second, differently-stacked
  adapter that proves the abstraction; replaces Heinemann per F5). Seed ~30–60 spirits
  SKUs including JW Blue 1L (Adam's own example). **Fetching: assume Firecrawl is
  needed for Avolta (F3) and test it first — this is the day-one critical path.** ARI
  should work on plain fetch. Korea (Lotte/Shilla/SSG) is out of scope permanently
  until there's a data agreement (F6), not merely deferred.
- **Data:** Postgres. products / variants (sizes are explicit — the Heathrow 1L vs
  20cl case) / locations / listings (product×location) / price_observations (history
  from day one) / medals. Every table carries a `vertical` field (liquor now) so the
  schema serves future verticals without a rebuild — but NO hub app gets built (see
  standing rule).
- **Medals:** pull one or two real winners via `srv-gw db-query --project apnetwork`
  (bw-winners tables) so the badge demo uses genuine IBC medal data.
- **Per-source kill switch** (from L11): every collector individually disableable from
  the admin without a deploy, plus per-source politeness delay and concurrency caps in
  config. Small to build now, essential the day a retailer emails.
- **App:** house-standard stack per `/srv/projects/standards/` (expected FastAPI +
  React + Postgres; confirm against the standards doc at build start). No login
  needed. Pages: home/browse (category pills, search), product detail (per-airport
  price table, currency conversions, medal badge, availability), a small
  about-the-data note (source + timestamp).
- **Deploy:** this workspace — replace the scaffold nginx compose, `srv-gw deploy
  --project professormediagroup --build`, served at professormediagroup.demoing.info
  behind the default gate. A prettier subdomain is optional later, not blocking.

## Day plan (today = Fri Aug 21)
- **Fri:** confirm stack vs standards; fetch experiments against Avolta/Heinemann;
  lock schema.
- **Sat–Sun:** collectors working end to end; DB seeded with real data.
- **Mon:** app UI — browse, product page, compare view, currency handling.
- **Tue:** medal integration, design polish, copy.
- **Wed Aug 26:** freeze, deploy, record video — internal DONE.
- **Thu Aug 27:** buffer; send link + video to Adam (a day early).
- **Fri Aug 28:** promised date (already delivered).

## Legal guardrails

Moved to `main/docs/COLLECTORS.md` (the collectors' home; researched 2026-08-21, moved
2026-09-04). Rules L1–L11 are unchanged there.

## The real deadline: Cannes (confirmed 2026-08-21)

**TFWA World Exhibition & Conference: 27 September – 1 October 2026, Palais des
Festivals, Cannes.** Verified on tfwa.com (~480 exhibitors, ~8,000 trade
professionals). This is the "show in Cannes" Adam named. Working backwards:

| Date | Milestone |
|---|---|
| Thu Aug 27 | PoC delivered (link + video), Adam in Europe |
| Sun Sep 6 (approx) | Adam back Sept 7 — review meeting on his return |
| ~Fri Sep 18 | "Operational" target = Adam's stated 4 weeks |
| Sep 18–26 | His outreach window to book Cannes meetings |
| Sep 27 – Oct 1 | Cannes |

**Key reframe — the mid-September audience is TRADE, not travellers.** Adam's stated
purpose is "outreach before the show to schedule the meetings at the show." So the
September milestone is not a consumer launch. It is a credible demonstration that makes
duty-free operators, brands and airport retailers want a meeting. That means the
priorities are **breadth of locations (his 10+, ideally 50) and a demo that survives
scrutiny by industry people** — NOT price alerts, consumer accounts, the map, or
editorial volume. Those are all post-Cannes. This materially de-risks the deadline and
should be confirmed with Adam at the review meeting before it is treated as settled.

## Risks
- Bot protection blocks direct fetch → Firecrawl fallback, ~$16, decision Sat latest.
- Variant/currency confusion in the demo → curated seed list; sizes modeled explicitly.
- Scope creep → anything outside success criteria goes on the review-meeting list.

## Review-meeting agenda (async or when Adam is back Sept 7)
Launch airport list · refresh cadence (drives collection cost per the Firecrawl
numbers) · category order (spirits → fragrance → …) · content approach (headless WP
dashboard vs native editor; the bar is Kristine + Matt fully independent) · quote to
operational (mid-Sept, 10+ locations).

## Standing rule (rian, 2026-08-21)
Keep circling back to Adam's WRITTEN instructions; do not trust rian's direction
blindly — his framing may be misguided. Known divergences, tracked deliberately:

| Adam's instruction | Rian's idea | Resolution |
|---|---|---|
| DFP is a one-off design | Hub-and-spoke: professormediagroup central data + vertical spokes | Schema stays vertical-agnostic (free); NO hub built until Adam asks |
| Blog content still matters | Headless WP dashboard; later a restricted-block page builder | Parked; not in PoC; content approach = review-meeting decision |
| "Halt the winners award pages campaign?" | — | Recommend NO: comp sites survive as-is; any future move redirects URLs |

## Requirements captured from Adam (2026-08-21 email)
- Kristine + Matt must be trainable to full independence on posting
  content/results in any new environment (training is part of any migration).
- Wants to revisit "the global database offering" — maps to the stalled
  bw-winners-global-site v2 concept; the new platform is its natural home.
- Same-look across the competition family remains the goal; DFA is already spun up
  in the WP environment, population is on Adam's team.
