{
  "id": "mvxVBIvdjGEVWgGX",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Monitor Meta CAPI event match quality (EMQ) across every pixel",
  "tags": [],
  "nodes": [
    {
      "id": "cb460661-be5e-4bd7-8907-494c52e13fba",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        10304,
        784
      ],
      "parameters": {
        "width": 604,
        "height": 536,
        "content": "## Meta CAPI Event Match Quality (EMQ) Monitor\n\nWatches your Meta pixels for match-quality decay. Every CAPI template only SENDS events; this one MONITORS how well they match, so you catch a silent signal-quality drop before it wrecks optimization and costs.\n\n### How it works\n- Discovers the ad account's pixels and keeps the recently-active ones.\n- Pulls each pixel's server-event match stats (PII coverage per event) from the Graph API.\n- Scores match quality 0-100, weighting the conversion events you optimize on three times heavier.\n- Flags any pixel below your alert threshold and has Claude explain the gap and the exact fix (hashed email/phone, fbc/fbp, Advanced Matching).\n\n### Setup\n1. Add to your .env: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID, META_API_VERSION, ANTHROPIC_API_KEY.\n2. Run the manual trigger, or attach a Schedule trigger for daily monitoring.\n\n### Customization\nSet CONVERSION_EVENTS and ALERT_BELOW in Config. To track decay over time, add a node that appends the daily scores to Google Sheets or a database and diff against the prior run. Swap stdout for Slack or email to auto-alert.\n\nBuilt by **nocode.expert** - done-for-you automation & tracking. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "93a3baa8-0ace-4691-b605-07f49c8d53b9",
      "name": "Section: Read match stats",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        11168,
        1008
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 288,
        "content": "## 1. Read match stats\nDiscover pixels and pull per-event PII coverage from the Graph API."
      },
      "typeVersion": 1
    },
    {
      "id": "6872c879-91f5-472b-a2f3-0a12936a59f3",
      "name": "Section: Score & explain",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        11728,
        1008
      ],
      "parameters": {
        "color": 7,
        "width": 1152,
        "height": 288,
        "content": "## 2. Score & explain\nScore match quality, then Claude explains gaps and fixes."
      },
      "typeVersion": 1
    },
    {
      "id": "ae042b40-645f-4061-8a05-36ebea34283d",
      "name": "Section: Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        12944,
        992
      ],
      "parameters": {
        "color": 7,
        "width": 396,
        "height": 320,
        "content": "## 3. Report\nReport the EMQ scorecard and alerts (swap for Slack/email)."
      },
      "typeVersion": 1
    },
    {
      "id": "5bd641d3-f6f4-45b8-83a8-177206256cb6",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        10976,
        1136
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "f52344d6-8efa-4553-a353-5da655ea7e50",
      "name": "Set config: conversion events, alert threshold",
      "type": "n8n-nodes-base.code",
      "position": [
        11216,
        1136
      ],
      "parameters": {
        "jsCode": "// CONVERSION_EVENTS are weighted heavier because match quality matters most for\n// the events you optimize on. ALERT_BELOW is the score that triggers an alert.\n//\n// DATASET_IDS: leave empty [] to auto-discover and surface EVERY active pixel/\n// dataset on the account, or list specific IDs to monitor only those.\nreturn [{ json: {\n  DATASET_IDS: [],         // e.g. ['YOUR_DATASET_ID_1', 'YOUR_DATASET_ID_2'] to pick specific datasets\n  PIXEL_LIMIT: 50,         // max pixels to check when auto-discovering\n  ACTIVE_DAYS: 14,         // when auto-discovering, only score pixels fired within this many days\n  ALERT_BELOW: 70,         // score under this = alert\n  CONVERSION_EVENTS: ['SubmitApplication', 'Lead', 'Purchase', 'CompleteRegistration', 'Optin-Start', 'Subscribe', 'StartTrial', 'AddPaymentInfo'],\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "0b9b3686-fe55-4ae0-865a-1b1b10f7cd39",
      "name": "Build AI match-quality summary prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        12304,
        1136
      ],
      "parameters": {
        "jsCode": "// Ask Claude to explain the biggest match-quality gaps and how to fix them.\nconst { pixels, cfg } = $json;\nconst worst = pixels.filter((p) => p.score !== null && p.score < cfg.ALERT_BELOW);\nconst system = 'You are a server-side tracking specialist. You are given Meta pixel event match-quality data (PII coverage per event; conversion events matter most). For each low-scoring pixel, explain the risk in one line and give one concrete fix (e.g. add hashed email/phone to CAPI payloads, pass fbc/fbp, enable Advanced Matching). Be specific and terse. Return ONLY valid JSON.';\nconst user = 'Alert threshold: ' + cfg.ALERT_BELOW + '. Pixels:\\n' + JSON.stringify(pixels) + '\\n\\nReturn JSON exactly: {\"summary\":\"one sentence\",\"pixels\":[{\"name\":\"\",\"score\":0,\"risk\":\"\",\"fix\":\"\"}]}';\nconst body = { model: 'claude-haiku-4-5', max_tokens: 1200, system, messages: [{ role: 'user', content: user }] };\nreturn [{ json: { body, _ctx: $json } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a6cc4bec-19b9-4a3b-a832-416639998676",
      "name": "Summarize match-quality gaps with Claude AI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        12624,
        1136
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.body }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d0a4974b-eb52-41c3-9edb-bb65eb1d7ba2",
      "name": "Print EMQ scorecard & alerts",
      "type": "n8n-nodes-base.code",
      "position": [
        13024,
        1136
      ],
      "parameters": {
        "jsCode": "// Print the EMQ scorecard and AI recommendations to stdout.\nconst ctx = $node['Score event match quality per pixel'].json;\nconst raw = ($json.content && $json.content[0] && $json.content[0].text) || '{}';\nconst s = raw.indexOf('{'), e = raw.lastIndexOf('}');\nlet parsed = { summary: '', pixels: [] };\ntry { parsed = JSON.parse(raw.slice(s, e + 1)); } catch (err) {}\nconst fixOf = {}; (parsed.pixels || []).forEach((p) => { fixOf[p.name] = p; });\n\nconst out = [];\nout.push('');\nout.push('==========================================================');\nout.push('  META CAPI EVENT MATCH QUALITY MONITOR  |  nocode.expert');\nout.push('==========================================================');\nout.push(parsed.summary || '');\nout.push('Pixels checked: ' + ctx.checked + '   |   Alert below: ' + ctx.cfg.ALERT_BELOW);\nout.push('');\nfor (const p of ctx.pixels) {\n  const flag = p.score !== null && p.score < ctx.cfg.ALERT_BELOW ? '  <-- ALERT' : '';\n  out.push('[' + (p.score === null ? ' n/a' : String(p.score).padStart(3)) + '/100] ' + p.name + flag);\n  const conv = p.events.filter((x) => x.is_conversion);\n  for (const c of conv.slice(0, 3)) out.push('        ' + c.name + ': ' + c.pii_pct + '% PII on ' + c.events + ' events');\n  const f = fixOf[p.name];\n  if (f && f.fix) out.push('        FIX: ' + f.fix);\n}\nout.push('==========================================================');\nconsole.log(out.join('\\n'));\nreturn [{ json: { scorecard: ctx.pixels, summary: parsed.summary } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "110b68f4-fb80-473c-9fe0-093a55c19280",
      "name": "List active pixels (Meta)",
      "type": "n8n-nodes-base.facebookGraphApi",
      "position": [
        11392,
        1136
      ],
      "parameters": {
        "edge": "adspixels",
        "node": "={{ ($env.META_AD_ACCOUNT_ID || '').trim() }}",
        "options": {
          "queryParameters": {
            "parameter": [
              {
                "name": "fields",
                "value": "id,name,last_fired_time"
              },
              {
                "name": "limit",
                "value": "100"
              }
            ]
          }
        },
        "graphApiVersion": "v21.0"
      },
      "typeVersion": 1
    },
    {
      "id": "00bb6a32-57ca-4ca4-a581-040c986b7ece",
      "name": "Select active pixels",
      "type": "n8n-nodes-base.code",
      "position": [
        11584,
        1136
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: conversion events, alert threshold').first().json;\nconst now = Date.now();\nconst list = $json.data || [];\nlet active;\nif (Array.isArray(cfg.DATASET_IDS) && cfg.DATASET_IDS.length) {\n  const want = new Set(cfg.DATASET_IDS.map(String));\n  active = list.filter(p => want.has(String(p.id)));\n} else {\n  active = list.filter(p => p.last_fired_time && (now - new Date(p.last_fired_time).getTime()) < cfg.ACTIVE_DAYS*864e5).slice(0, cfg.PIXEL_LIMIT);\n}\nreturn active.map(p => ({ json: { id: p.id, name: p.name } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "32c6e534-9391-4c33-87fb-e1a899a0fcb5",
      "name": "Pixel PII stats (Meta)",
      "type": "n8n-nodes-base.facebookGraphApi",
      "position": [
        11792,
        1136
      ],
      "parameters": {
        "edge": "stats",
        "node": "={{ $json.id }}",
        "options": {
          "queryParameters": {
            "parameter": [
              {
                "name": "aggregation",
                "value": "had_pii"
              }
            ]
          }
        },
        "graphApiVersion": "v21.0"
      },
      "typeVersion": 1
    },
    {
      "id": "c1242e48-5947-46f2-a885-5383774405f0",
      "name": "Score event match quality per pixel",
      "type": "n8n-nodes-base.code",
      "position": [
        12016,
        1136
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: conversion events, alert threshold').first().json;\nconst pixelsMeta = $('Select active pixels').all();\nconst CONV = new Set(cfg.CONVERSION_EVENTS);\nconst responses = $input.all();\nconst pixels = [];\nresponses.forEach((item, i) => {\n  const p = pixelsMeta[i].json;\n  const rows = (((item.json.data||[{}])[0]||{}).data) || [];\n  const ev = {};\n  for (const r of rows) { const e = (ev[r.event] = ev[r.event] || { has:0, total:0 }); const c = Number(r.count)||0; e.total += c; if (r.value==='has_pii') e.has += c; }\n  const events = Object.entries(ev).map(([name,c]) => ({ name, events:c.total, pii_pct: c.total? Math.round(100*c.has/c.total):0, is_conversion: CONV.has(name) })).sort((a,b)=>b.events-a.events);\n  let wSum=0,wPii=0;\n  for (const e of events){ const w=(e.is_conversion?3:1)*e.events; wSum+=w; wPii+=w*e.pii_pct; }\n  const score = wSum? Math.round(wPii/wSum): null;\n  if (events.length) pixels.push({ id:p.id, name:p.name, score, events });\n});\npixels.sort((a,b)=>(a.score??101)-(b.score??101));\nreturn [{ json: { pixels, checked: pixelsMeta.length, cfg } }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "ab3dc5fa-9c1e-4798-a959-cdf9cdf2bbaa",
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Set config: conversion events, alert threshold",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select active pixels": {
      "main": [
        [
          {
            "node": "Pixel PII stats (Meta)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pixel PII stats (Meta)": {
      "main": [
        [
          {
            "node": "Score event match quality per pixel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List active pixels (Meta)": {
      "main": [
        [
          {
            "node": "Select active pixels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score event match quality per pixel": {
      "main": [
        [
          {
            "node": "Build AI match-quality summary prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI match-quality summary prompt": {
      "main": [
        [
          {
            "node": "Summarize match-quality gaps with Claude AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize match-quality gaps with Claude AI": {
      "main": [
        [
          {
            "node": "Print EMQ scorecard & alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: conversion events, alert threshold": {
      "main": [
        [
          {
            "node": "List active pixels (Meta)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}