[object Object]

← back to Ads Dashboard

populate ad-ops feeds: $0 pixel-scan scanner → real competitor signals (11) + DW campaign presence (1); spend stays null (no ad-account API) — TK-10544

4385f86985716f5708c7aa0f790444b61fe0b6a8 · 2026-08-13 14:18:10 -0700 · Steve Abrams

Files touched

Diff

commit 4385f86985716f5708c7aa0f790444b61fe0b6a8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 14:18:10 2026 -0700

    populate ad-ops feeds: $0 pixel-scan scanner → real competitor signals (11) + DW campaign presence (1); spend stays null (no ad-account API) — TK-10544
---
 data/campaigns.dw.json      |  17 +++++
 data/signals.dw.json        |  91 +++++++++++++++++++++++++++
 scripts/scan-ad-signals.mjs | 147 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 255 insertions(+)

diff --git a/data/campaigns.dw.json b/data/campaigns.dw.json
new file mode 100644
index 0000000..23624ed
--- /dev/null
+++ b/data/campaigns.dw.json
@@ -0,0 +1,17 @@
+{
+  "source": "pixel-scan (DW-owned domains)",
+  "present": true,
+  "generated_at": "2026-08-13T21:17:38.080Z",
+  "note": "Active ad-platform presence detected via tracking pixels. Spend is null until a Google Ads / Meta Ads account is connected — not fabricated.",
+  "campaigns": [
+    {
+      "name": "Meta (Facebook) — apartmentwallpaper",
+      "domain": "apartmentwallpaper.com",
+      "platform": "Meta (Facebook)",
+      "status": "active",
+      "spend": null,
+      "created_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/data/signals.dw.json b/data/signals.dw.json
new file mode 100644
index 0000000..56ef68c
--- /dev/null
+++ b/data/signals.dw.json
@@ -0,0 +1,91 @@
+{
+  "source": "advertising-signals (pixel-scan, competitor domains)",
+  "present": true,
+  "generated_at": "2026-08-13T21:17:38.080Z",
+  "scanned": [
+    "fschumacher.com",
+    "thibautdesign.com",
+    "yorkwallcoverings.com",
+    "phillipjeffries.com",
+    "cole-and-son.com"
+  ],
+  "signals": [
+    {
+      "advertiser": "yorkwallcoverings.com",
+      "platform": "Google Ads",
+      "url": "https://yorkwallcoverings.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "yorkwallcoverings.com",
+      "platform": "Meta (Facebook)",
+      "url": "https://yorkwallcoverings.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "yorkwallcoverings.com",
+      "platform": "Pinterest",
+      "url": "https://yorkwallcoverings.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "yorkwallcoverings.com",
+      "platform": "Google Tag Manager",
+      "url": "https://yorkwallcoverings.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "Google Ads",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "Meta (Facebook)",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "TikTok",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "Pinterest",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "LinkedIn",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "Microsoft (Bing)",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    },
+    {
+      "advertiser": "phillipjeffries.com",
+      "platform": "Google Tag Manager",
+      "url": "https://phillipjeffries.com/",
+      "seen_at": "2026-08-13T21:17:38.080Z",
+      "detected_by": "pixel-scan"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/scripts/scan-ad-signals.mjs b/scripts/scan-ad-signals.mjs
new file mode 100644
index 0000000..d9c615f
--- /dev/null
+++ b/scripts/scan-ad-signals.mjs
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+/**
+ * scan-ad-signals.mjs — $0 local ad-platform pixel scanner for ads-dashboard.
+ *
+ * Fetches each domain's homepage HTML and fingerprints which paid-ad platforms
+ * it is running tracking pixels for. NO fabrication: a platform only appears if
+ * its pixel is actually present in the served HTML.
+ *
+ *   DW-owned domains  -> data/campaigns.dw.json  (our own active ad channels; spend=null, needs ad-account API)
+ *   competitor domains -> data/signals.dw.json   (competitor ad signals)
+ *
+ * Zero dependencies (global fetch). Run: node scripts/scan-ad-signals.mjs
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const DATA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'data');
+
+// Real DW properties (→ campaigns: our own detected ad channels)
+const OWN = ['designerwallcoverings.com', 'apartmentwallpaper.com', 'philipperomano.com'];
+// Real wallcovering competitors (→ signals: competitor ad presence)
+const COMPETITORS = [
+  'fschumacher.com', 'thibautdesign.com', 'yorkwallcoverings.com',
+  'phillipjeffries.com', 'cole-and-son.com',
+];
+
+// Pixel fingerprints: platform -> [regexes that prove the ad pixel is present]
+const FINGERPRINTS = {
+  'Google Ads':      [/googleadservices\.com\/pagead\/conversion/i, /gtag\(['"]config['"],\s*['"]AW-/i, /google_conversion_id/i, /googleads\.g\.doubleclick\.net/i],
+  'Meta (Facebook)': [/connect\.facebook\.net\/[a-z_]+\/fbevents\.js/i, /\bfbq\(/i],
+  'TikTok':          [/analytics\.tiktok\.com/i, /ttq\.load\(/i],
+  'Pinterest':       [/s\.pinimg\.com\/ct\//i, /\bpintrk\(/i],
+  'LinkedIn':        [/snap\.licdn\.com/i, /_linkedin_partner_id/i],
+  'X (Twitter)':     [/static\.ads-twitter\.com/i, /\btwq\(/i],
+  'Microsoft (Bing)':[/bat\.bing\.com/i, /\buetq\b/i],
+  'Snapchat':        [/sc-static\.net\/scevent/i, /\bsnaptr\(/i],
+  'Reddit':          [/redditstatic\.com\/ads/i, /\brdt\(/i],
+};
+
+const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
+
+async function fetchHTML(domain) {
+  const url = 'https://' + domain + '/';
+  try {
+    const ctrl = new AbortController();
+    const t = setTimeout(() => ctrl.abort(), 15000);
+    const r = await fetch(url, { headers: { 'User-Agent': UA, 'Accept': 'text/html' }, redirect: 'follow', signal: ctrl.signal });
+    clearTimeout(t);
+    if (!r.ok) return { ok: false, reason: 'http ' + r.status };
+    return { ok: true, html: await r.text() };
+  } catch (e) { return { ok: false, reason: e.name === 'AbortError' ? 'timeout' : e.message }; }
+}
+
+function detect(html) {
+  const hit = [];
+  for (const [platform, regexes] of Object.entries(FINGERPRINTS)) {
+    if (regexes.some(re => re.test(html))) hit.push(platform);
+  }
+  return hit;
+}
+
+// Fetch a raw JS/text URL (GTM container) and return its body.
+async function fetchText(url) {
+  try {
+    const ctrl = new AbortController();
+    const t = setTimeout(() => ctrl.abort(), 15000);
+    const r = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow', signal: ctrl.signal });
+    clearTimeout(t);
+    return r.ok ? await r.text() : '';
+  } catch { return ''; }
+}
+
+// Pull GTM container ids (GTM-XXXX) referenced in the page.
+function gtmIds(html) {
+  const ids = new Set();
+  const re = /GTM-[A-Z0-9]+/g; let m;
+  while ((m = re.exec(html))) ids.add(m[0]);
+  return [...ids];
+}
+
+async function scan(domain) {
+  const res = await fetchHTML(domain);
+  if (!res.ok) return { domain, ok: false, reason: res.reason, platforms: [] };
+  const found = new Set(detect(res.html));
+  // Second hop: many ad pixels are injected by Google Tag Manager, not inline.
+  // Fetch each GTM container and fingerprint the tags configured inside it.
+  const ids = gtmIds(res.html);
+  for (const id of ids) {
+    const container = await fetchText('https://www.googletagmanager.com/gtm.js?id=' + id);
+    if (container) detect(container).forEach(p => found.add(p));
+    // Google Ads conversion linker inside a container:
+    if (/AW-\d{6,}/.test(container)) found.add('Google Ads');
+  }
+  if (ids.length) found.add('Google Tag Manager');
+  return { domain, ok: true, platforms: [...found], gtm: ids };
+}
+
+(async () => {
+  const nowISO = new Date().toISOString();
+  console.log('[scan] fetching', OWN.length + COMPETITORS.length, 'domains…');
+
+  const ownResults = await Promise.all(OWN.map(scan));
+  const compResults = await Promise.all(COMPETITORS.map(scan));
+
+  // DW-owned detected platforms -> campaigns (our active ad channels; spend unknown w/o ad-account API)
+  const campaigns = [];
+  for (const r of ownResults) {
+    for (const p of r.platforms) {
+      campaigns.push({
+        name: p + ' — ' + r.domain.replace(/\.com$/, ''),
+        domain: r.domain,
+        platform: p,
+        status: 'active',       // pixel is live on the site
+        spend: null,            // honest: real $ needs Google Ads / Meta API (renders "—")
+        created_at: nowISO,
+        detected_by: 'pixel-scan',
+      });
+    }
+    console.log('  own', r.domain, r.ok ? r.platforms.join(', ') || '(no ad pixels)' : 'FAIL: ' + r.reason);
+  }
+
+  // Competitor detected platforms -> signals
+  const signals = [];
+  for (const r of compResults) {
+    for (const p of r.platforms) {
+      signals.push({
+        advertiser: r.domain,
+        platform: p,
+        url: 'https://' + r.domain + '/',
+        seen_at: nowISO,
+        detected_by: 'pixel-scan',
+      });
+    }
+    console.log('  comp', r.domain, r.ok ? r.platforms.join(', ') || '(no ad pixels)' : 'FAIL: ' + r.reason);
+  }
+
+  const campaignsOut = { source: 'pixel-scan (DW-owned domains)', present: true, generated_at: nowISO,
+    note: 'Active ad-platform presence detected via tracking pixels. Spend is null until a Google Ads / Meta Ads account is connected — not fabricated.',
+    campaigns };
+  const signalsOut = { source: 'advertising-signals (pixel-scan, competitor domains)', present: true, generated_at: nowISO,
+    scanned: COMPETITORS, signals };
+
+  fs.writeFileSync(path.join(DATA_DIR, 'campaigns.dw.json'), JSON.stringify(campaignsOut, null, 2));
+  fs.writeFileSync(path.join(DATA_DIR, 'signals.dw.json'), JSON.stringify(signalsOut, null, 2));
+  console.log(`[scan] wrote ${campaigns.length} campaign rows, ${signals.length} signal rows. $0 (local).`);
+})();

← c3e36a8 nav-agent: universal grid-controls drop-in on internal dashb  ·  back to Ads Dashboard  ·  fix: duplicate 'camps' const collision blanked ALL dashboard 335d884 →