← 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
A data/campaigns.dw.jsonA data/signals.dw.jsonA scripts/scan-ad-signals.mjs
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 →