{
  "id": "RLgV19NH2JlyGlwF",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "The Mismatch Engine: AI Lead Discovery for Local Businesses",
  "tags": [],
  "nodes": [
    {
      "id": "5f6d08a4-cf94-42aa-9ac5-bf5b50e50566",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2800,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 2244,
        "height": 642,
        "content": "## Discovery & Targeting\n1. Set the Location & Category\n2. AI determines seasonal patterns & localized search terms\n3. Firecrawl hunts down relevant directory profiles\n4. AI parses exactly 3 high-potential leads from the noise"
      },
      "typeVersion": 1
    },
    {
      "id": "483c7a5e-4da8-4852-8ae5-03de0612ac90",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -528,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 418,
        "height": 636,
        "content": "## Deduplication\nChecks the existing database so we never process or pay for the same lead twice."
      },
      "typeVersion": 1
    },
    {
      "id": "f36154e3-2033-4fc1-a861-e7dbbdacf356",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 2898,
        "height": 638,
        "content": "## The Deep Scrape Loop\nIterates through the new leads with built-in rate limiting.\n1. Scrapes their actual website\n2. Finds and scrapes their 3 closest competitors\n3. Pulls recent customer review snippets for analysis"
      },
      "typeVersion": 1
    },
    {
      "id": "7ddedaa6-3534-41f7-9623-ae957bd8f79a",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2864,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 578,
        "height": 626,
        "content": "## Mismatch Engine\nCalculates reputation vs. digital capability, finds revenue leaks, and writes the sales pitch."
      },
      "typeVersion": 1
    },
    {
      "id": "2810ca9d-3c27-4423-8091-58dc35a89228",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        688
      ],
      "parameters": {
        "color": 7,
        "width": 1494,
        "height": 646,
        "content": "## Output & Reporting\nRanks the finished leads by opportunity score, saves them to the n8n database, and compiles a clean HTML report."
      },
      "typeVersion": 1
    },
    {
      "id": "67b4088a-feb8-46f6-a612-878dd86e598d",
      "name": "Weekly Monday 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2736,
        224
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * 1"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e041e89a-a3bd-4b23-adcb-f598c0e644a7",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        -2512,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "location",
              "type": "string",
              "value": "Helsinki, Finland"
            },
            {
              "id": "s2",
              "name": "category",
              "type": "string",
              "value": "Restaurants"
            },
            {
              "id": "s3",
              "name": "current_month",
              "type": "number",
              "value": "={{ $now.month }}"
            },
            {
              "id": "s4",
              "name": "run_date",
              "type": "string",
              "value": "={{ $now.toFormat('yyyy-MM-dd') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "36459253-a414-4d2b-8424-a00f8f4ea234",
      "name": "Generate Category Signals",
      "type": "n8n-nodes-base.code",
      "position": [
        -2288,
        224
      ],
      "parameters": {
        "jsCode": "const cfg = $input.first().json;\n\nconst locationLower = cfg.location.toLowerCase();\nconst isGerman = locationLower.includes('germany') || locationLower.includes('berlin') || locationLower.includes('munich');\nconst isFinnish = locationLower.includes('finland') || locationLower.includes('helsinki');\nconst isSpanish = locationLower.includes('spain') || locationLower.includes('madrid');\n\nconst languageHint = isGerman ? ' Include German terms.' : \n                     isFinnish ? ' Include Finnish terms.' : \n                     isSpanish ? ' Include Spanish terms.' : \n                     ' Use English terms.';\n\nconst prompt = `Analyze this business category for seasonal patterns and conversion signals.\\n\\nCategory: ${cfg.category}\\nLocation: ${cfg.location}\\n\\nTask 1 - Peak Season Analysis:\\nIdentify the 2-4 months (1-12) when this business type has highest demand in this location.\\nConsider: local climate, cultural events, holidays, consumer behavior patterns.\\n\\nTask 2 - Conversion Signals:\\nList 5-8 keywords/phrases that indicate a website has online booking or conversion capability.\\n${languageHint}\\n\\nReturn ONLY this JSON structure with NO additional text:\\n{\\n  \"peak_months\": [1, 2, 9],\\n  \"peak_label\": \"Brief explanation of why these months (max 60 chars)\",\\n  \"conversion_signals\": [\"keyword1\", \"keyword2\", \"keyword3\", \"keyword4\", \"keyword5\"]\\n}`;\n\nreturn [{\n  json: {\n    ...cfg,\n    signal_prompt: prompt\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "25c135ed-eb72-4c42-b703-e2279aa5376e",
      "name": "Basic LLM Chain2",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -2064,
        224
      ],
      "parameters": {
        "text": "={{ $json.signal_prompt }}",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "5d733b19-790a-421e-8d41-c9c049dd46d6",
      "name": "Groq Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        -1992,
        448
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0f45da6e-6e79-411c-b42b-e194b7a6df33",
      "name": "Parse Category Signals ",
      "type": "n8n-nodes-base.code",
      "position": [
        -1712,
        224
      ],
      "parameters": {
        "jsCode": "const cfg = $('Configuration').first().json;\nconst rawText = $input.first().json.text || $input.first().json.output || \"\";\n\nlet parsedSignals = {\n  peak_months: [1, 6, 12],\n  peak_label: \"Peak demand periods\",\n  conversion_signals: ['book', 'appointment', 'schedule', 'reserve', 'contact']\n};\n\ntry {\n  const jsonMatch = rawText.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    const cleanJson = jsonMatch[0];\n    const parsed = JSON.parse(cleanJson);\n    if (parsed.peak_months && Array.isArray(parsed.peak_months) && parsed.peak_months.length > 0) {\n      parsedSignals.peak_months = parsed.peak_months;\n    }\n    if (parsed.peak_label && typeof parsed.peak_label === 'string') {\n      parsedSignals.peak_label = parsed.peak_label;\n    }\n    if (parsed.conversion_signals && Array.isArray(parsed.conversion_signals) && parsed.conversion_signals.length > 0) {\n      parsedSignals.conversion_signals = parsed.conversion_signals;\n    }\n  }\n} catch (error) {\n  console.error('Failed to parse LLM signals, using defaults:', error.message);\n}\n\nreturn [{\n  json: {\n    category: cfg.category,\n    location: cfg.location,\n    current_month: cfg.current_month,\n    run_date: cfg.run_date,\n    season_matrix: { [cfg.category]: { peak_months: parsedSignals.peak_months, label: parsedSignals.peak_label } },\n    conversion_signals: { [cfg.category]: parsedSignals.conversion_signals }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "dd452e7e-4bba-4e69-8184-0328b9977547",
      "name": "Discovery Search",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        -1488,
        224
      ],
      "parameters": {
        "query": "={{ $json.category + ' ' + $json.location + ' (site:yelp.com OR site:tripadvisor.com OR site:google.com/maps)' }}",
        "resource": "MapSearch",
        "operation": "search",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "11feb953-9703-4411-b680-8225bdb107e6",
      "name": "Format Search Context",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        224
      ],
      "parameters": {
        "jsCode": "const inputItems = $input.all();\nconst cfg = $('Parse Category Signals ').first().json;\n\nconst normalizeFirecrawlResponse = (data) => {\n  if (!data) return [];\n  if (Array.isArray(data)) return data;\n  if (data.data && Array.isArray(data.data)) return data.data;\n  if (data.web && Array.isArray(data.web)) return data.web;\n  if (data.results && Array.isArray(data.results)) return data.results;\n  if (data.data && data.data.web && Array.isArray(data.data.web)) return data.data.web;\n  if (data.data && data.data.data && Array.isArray(data.data.data)) return data.data.data;\n  return [];\n};\n\nreturn inputItems.map((itemWrapper) => {\n  const item = itemWrapper.json;\n  const originalData = { ...cfg };\n  let results = normalizeFirecrawlResponse(item);\n  let ctx = 'No results returned';\n  \n  if (results.length > 0) {\n    ctx = results.slice(0, 8).map((r, i) => {\n      const title = r.title || r.metadata?.title || 'No Title';\n      const url = r.url || 'No URL';\n      const desc = (r.description || r.markdown || '').substring(0, 300);\n      return `[${i+1}] ${title}\\nURL: ${url}\\nSnippet: ${desc}`;\n    }).join('\\n\\n');\n  }\n\n  return { json: { ...originalData, search_context: ctx } };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "3d68a84e-dc1a-40ba-83a3-3f5ca01feb74",
      "name": "Basic LLM Chain",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -1040,
        224
      ],
      "parameters": {
        "text": "=Extract exactly 3 local businesses from the search results that match this profile:\n- Independent businesses (NOT chains)\n- High ratings or strong reputation signals\n- Likely need digital improvement\n\nCategory: {{ $('Configuration').first().json.category }}\nLocation: {{ $('Configuration').first().json.location }}\n\nCRITICAL: ONLY extract businesses that are EXACTLY in the \"{{ $('Configuration').first().json.category }}\" category.\nDo NOT include businesses from other categories even if they appear in results.\n\nSearch Results:\n{{ $json.search_context }}\n\nSELECTION CRITERIA (in priority order):\n1. Has reviews/ratings mentioned \u2192 signals reputation\n2. Independent local business \u2192 NOT franchise/chain\n3. Weak digital signals \u2192 old site, no online booking, basic presence\n4. Category match \u2192 must be {{ $('Configuration').first().json.category }}\n\nCRITICAL - URL EXTRACTION:\n- Extract the SPECIFIC business profile URL from ANY directory (Yelp, Google Maps, Foursquare, TripAdvisor, etc.)\n- Must be the BUSINESS PROFILE page\n- NOT search results pages\n- NOT directory homepages\n- Each business MUST have a unique profile URL from a directory site\n\nIf you find fewer than 3 businesses, return only what you found.\nIf search results are empty or contain no {{ $('Configuration').first().json.category }} businesses, return: {\"businesses\": []}\n\nReturn ONLY this JSON structure with NO additional text:\n{\n  \"businesses\": [\n    {\n      \"business_name\": \"Exact name from listing\",\n      \"category\": \"{{ $('Configuration').first().json.category }}\",\n      \"website_url\": \"Full directory profile URL (any accepted source)\"\n    }\n  ]\n}",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "9138da5b-f713-4ec1-9fe1-4cec025c1822",
      "name": "Groq Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        -968,
        448
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6b0298ac-24f1-4db3-9ee4-fa563e595dae",
      "name": "Parse Business List",
      "type": "n8n-nodes-base.code",
      "position": [
        -688,
        224
      ],
      "parameters": {
        "jsCode": "const rawText = $input.first().json.text || \"\";\ntry {\n  const jsonMatch = rawText.match(/\\{[\\s\\S]*\\}/);\n  if (!jsonMatch) {\n    console.log('No JSON found in response:', rawText.substring(0, 200));\n    return [];\n  }\n  const data = JSON.parse(jsonMatch[0]);\n  if (!data.businesses || !Array.isArray(data.businesses)) {\n    console.error('Invalid schema from LLM: missing businesses array');\n    return [];\n  }\n  const validBusinesses = data.businesses.filter(b => \n    b.business_name && typeof b.business_name === 'string' &&\n    b.website_url && typeof b.website_url === 'string'\n  );\n  if (validBusinesses.length > 0) {\n    return validBusinesses.slice(0, 3).map(b => ({ \n      json: {\n        ...b,\n        category: b.category ? (b.category.endsWith('s') ? b.category : b.category + 's') : 'Unknown'\n      }\n    }));\n  } else {\n    return [];\n  }\n} catch (error) {\n  console.error('JSON Parsing Failed:', error.message);\n  return [{ json: { error: \"JSON Parsing Failed\", message: error.message } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "5613c64b-f7b4-47df-ba59-0f12e1e4aab7",
      "name": "Fetch Existing Leads",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -464,
        224
      ],
      "parameters": {
        "operation": "get",
        "returnAll": true,
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "2pGhdxpAThxGwmss",
          "cachedResultUrl": "/projects/hDHhdcMr4jVn06kt/datatables/2pGhdxpAThxGwmss",
          "cachedResultName": "Lead Database"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true,
      "alwaysOutputData": true
    },
    {
      "id": "30d925c4-afb0-4407-b5b4-2638038181e6",
      "name": "Filter New Leads",
      "type": "n8n-nodes-base.code",
      "position": [
        -240,
        224
      ],
      "parameters": {
        "jsCode": "const parsedLeads = $('Parse Business List').all().map(i => i.json);\nconst existingLeads = $input.all().map(i => i.json);\n\nconst existingNames = new Set(existingLeads.map(l => (l.business_name || '').toLowerCase().trim()));\n\nconst newLeads = parsedLeads.filter(l => !existingNames.has((l.business_name || '').toLowerCase().trim()) && !l.error);\n\nif (newLeads.length === 0) {\n  return []; // Gracefully stops if there are no new leads to process\n}\n\nreturn newLeads.map(l => ({ json: l }));"
      },
      "typeVersion": 2
    },
    {
      "id": "08cf87d8-0240-4354-a72a-fccd9190009f",
      "name": "Process Each Lead",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -16,
        224
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3,
      "continueOnFail": true
    },
    {
      "id": "891d88e6-56f8-4418-823d-5450fddde130",
      "name": "Rate Limit Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        208,
        224
      ],
      "parameters": {
        "amount": 2
      },
      "typeVersion": 1.1
    },
    {
      "id": "41ab7688-feea-409e-b8bb-4cb509a43222",
      "name": "Scrape Business Profile",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        432,
        224
      ],
      "parameters": {
        "url": "={{ $json.website_url }}",
        "parsers": "=",
        "operation": "scrape",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "a7209fa1-c3fd-43b9-aea7-b8f5629cd5b3",
      "name": "Has Website?",
      "type": "n8n-nodes-base.if",
      "position": [
        656,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.website }}",
              "rightValue": "none"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "177faa98-6891-4621-8055-4b1b2c29ffb4",
      "name": "Scrape Website",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        880,
        128
      ],
      "parameters": {
        "url": "={{ $json.website }}",
        "operation": "scrape",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "61c4ca7a-f4b7-49a7-a0fa-d61af8d64416",
      "name": "Merge Website",
      "type": "n8n-nodes-base.code",
      "position": [
        1104,
        128
      ],
      "parameters": {
        "jsCode": "try {\n  const lead = $('Process Each Lead').item.json;\n  if (lead._error) return [{ json: lead }];\n\n  const scrape = $input.first().json || {};\n  if (scrape.error) throw new Error(scrape.error.message || 'Website Scrape API Error');\n\n  const scrapeData = scrape.data || scrape;\n  return [{\n    json: {\n      ...lead,\n      website_markdown: scrapeData.markdown || '',\n      website_screenshot: scrapeData.screenshot || null,\n      website_exists: true\n    }\n  }];\n} catch (error) {\n  const lead = $('Process Each Lead').item?.json || {};\n  return [{ json: { ...lead, _error: error.message, _failed_step: 'website_scrape', website_exists: false } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "cc62249c-4e26-4769-9142-572895eb3f9f",
      "name": "No Website Path",
      "type": "n8n-nodes-base.code",
      "position": [
        1104,
        320
      ],
      "parameters": {
        "jsCode": "const lead = $('Process Each Lead').item.json;\nif (lead._error) return [{ json: lead }];\nreturn [{\n  json: {\n    ...lead,\n    website_markdown: '',\n    website_screenshot: null,\n    website_exists: false\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9d95a0da-0f37-47a8-b67f-d102333674f0",
      "name": "Find Competitors",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        1328,
        224
      ],
      "parameters": {
        "query": "={{ $json.category + ' near ' + ($json.address || $json.location) + ' -' + $json.business_name }}",
        "resource": "MapSearch",
        "operation": "search",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "083ee6cc-9099-45dc-a180-8e3da908a4d7",
      "name": "Filter Competitors",
      "type": "n8n-nodes-base.code",
      "position": [
        1552,
        224
      ],
      "parameters": {
        "jsCode": "try {\n  const lead = $input.first().json;\n  if (lead._error) return [{ json: lead }];\n\n  const normalizeFirecrawlResponse = (data) => {\n    if (!data) return [];\n    if (Array.isArray(data)) return data;\n    if (data.data && Array.isArray(data.data)) return data.data;\n    if (data.web && Array.isArray(data.web)) return data.web;\n    if (data.results && Array.isArray(data.results)) return data.results;\n    if (data.data && data.data.web && Array.isArray(data.data.web)) return data.data.web;\n    if (data.data && data.data.data && Array.isArray(data.data.data)) return data.data.data;\n    return [];\n  };\n\n  const results = normalizeFirecrawlResponse(lead);\n  \n  let leadDomain = '';\n  if (lead.website_url) {\n    try { leadDomain = new URL(lead.website_url).hostname.replace('www.', ''); } catch(e) {}\n  }\n\n  const leadNameLower = (lead.business_name || '').toLowerCase();\n  const skipDomains = ['yelp.com', 'facebook.com', 'instagram.com', 'tripadvisor.com', 'google.com', 'googleusercontent.com'];\n\n  const competitors = results\n    .map(r => ({ url: r.url || '', title: r.title || r.metadata?.title || '' }))\n    .filter(c => {\n      if (!c.url || !c.url.startsWith('http')) return false;\n      if (leadDomain && c.url.includes(leadDomain)) return false;\n      if (leadNameLower.length > 4 && c.title.toLowerCase().includes(leadNameLower)) return false;\n      try {\n        const hostname = new URL(c.url).hostname.replace('www.', '');\n        if (skipDomains.some(d => hostname.includes(d))) return false;\n      } catch(e) { return false; }\n      return true;\n    })\n    .slice(0, 3);\n\n  return [{ json: { ...lead, data: undefined, results: undefined, competitor_urls: competitors.map(c => c.url) } }];\n} catch (error) {\n  const lead = $input.first().json || {};\n  return [{ json: { ...lead, _error: error.message, _failed_step: 'filter_competitors' } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "723c14a4-9058-445f-82d8-a0193853f108",
      "name": "Split Competitors",
      "type": "n8n-nodes-base.code",
      "position": [
        1776,
        224
      ],
      "parameters": {
        "jsCode": "try {\n  const lead = $input.first().json;\n  if (lead._error) return [{ json: lead }];\n\n  const urls = lead.competitor_urls || [];\n  if (urls.length === 0) return [{ json: { ...lead, _skip_comp: true, _comp_idx: 0, _total: 0 } }];\n\n  return urls.map((url, idx) => ({\n    json: { _parent_lead: lead, _comp_url: url, _comp_idx: idx, _total: urls.length }\n  }));\n} catch (error) {\n  const lead = $input.first().json || {};\n  return [{ json: { ...lead, _error: error.message, _failed_step: 'split_competitors' } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "28f37a4c-e554-4f31-8870-141ef4db0e93",
      "name": "Scrape Competitor",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        2000,
        224
      ],
      "parameters": {
        "url": "={{ $json._comp_url }}",
        "operation": "scrape",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "d637d4ab-cac1-4225-bd35-d091219cdb54",
      "name": "Merge Competitors",
      "type": "n8n-nodes-base.code",
      "position": [
        2224,
        224
      ],
      "parameters": {
        "jsCode": "try {\n  const items = $input.all();\n  if (!items.length) return [];\n  if (items[0].json._error) return [{ json: items[0].json }];\n\n  if (items[0].json._skip_comp) {\n    const { _skip_comp, _comp_idx, _total, ...cleanLead } = items[0].json;\n    return [{ json: { ...cleanLead, competitor_markdowns: [] } }];\n  }\n\n  const parentLead = items[0].json._parent_lead;\n  const validItems = items.filter(i => typeof i.json._comp_idx === 'number');\n  \n  if (validItems.length !== items.length) {\n    console.error(`Missing competitor indexes: expected ${items.length}, got ${validItems.length}`);\n  }\n\n  const sorted = validItems.sort((a, b) => a.json._comp_idx - b.json._comp_idx);\n  const markdowns = sorted.map(i => (i.json.data || i.json).markdown || '');\n\n  return [{ json: { ...parentLead, competitor_markdowns: markdowns } }];\n} catch (error) {\n  const parentLead = $input.all()[0]?.json?._parent_lead || {};\n  return [{ json: { ...parentLead, _error: error.message, _failed_step: 'merge_competitors' } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "fbf76368-8703-4fcf-86d6-f8bedfb1226a",
      "name": "Search Reviews",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "maxTries": 3,
      "position": [
        2448,
        224
      ],
      "parameters": {
        "query": "={{ $json.business_name + ' ' + ($json.address || '') + ' reviews bewertungen' }}",
        "resource": "MapSearch",
        "operation": "search",
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "continueOnFail": true,
      "waitBetweenTries": 3000
    },
    {
      "id": "ab9bf777-f316-4488-b498-4a70e00453c4",
      "name": "Extract Review Snippets",
      "type": "n8n-nodes-base.code",
      "position": [
        2672,
        224
      ],
      "parameters": {
        "jsCode": "try {\n  const lead = $input.first().json;\n  if (lead._error) return [{ json: lead }];\n\n  const normalizeFirecrawlResponse = (data) => {\n    if (!data) return [];\n    if (Array.isArray(data)) return data;\n    if (data.data && Array.isArray(data.data)) return data.data;\n    if (data.web && Array.isArray(data.web)) return data.web;\n    if (data.results && Array.isArray(data.results)) return data.results;\n    if (data.data && data.data.web && Array.isArray(data.data.web)) return data.data.web;\n    if (data.data && data.data.data && Array.isArray(data.data.data)) return data.data.data;\n    return [];\n  };\n\n  const results = normalizeFirecrawlResponse(lead);\n  const snippets = results\n    .slice(0, 5)\n    .map(r => (r.description || r.markdown || '').substring(0, 500))\n    .filter(t => t.length > 30);\n\n  return [{ json: { ...lead, data: undefined, results: undefined, review_snippets: snippets } }];\n} catch (error) {\n  const lead = $input.first().json || {};\n  return [{ json: { ...lead, _error: error.message, _failed_step: 'extract_reviews' } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "235a4e95-1bff-4671-a8e3-4f809d7e3cf2",
      "name": "Basic LLM Chain1",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2976,
        224
      ],
      "parameters": {
        "text": "=Extract specific services that customers are praising from these reviews.\n\nBusiness: {{ $json.business_name }}\nCategory: {{ $json.category }}\n\nReviews:\n{{ ($json.review_snippets || []).join('\\n---\\n').substring(0, 2500) }}\n\nRULES:\n1. Only extract CONCRETE SERVICES that customers explicitly praise or describe positively\n2. Use the exact terminology from reviews when possible (e.g., \"balayage\" not \"hair coloring\")\n3. Ignore: staff names, atmosphere comments, parking, prices, general praise (\"great service\")\n4. Extract service names only - no descriptions or qualifiers\n5. Return empty array if no specific services are mentioned\n\nReturn ONLY valid JSON in this exact format:\n{\n  \"services\": [\"service1\", \"service2\"]\n}",
        "batching": {},
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "6871732b-628e-4ead-9432-1e49f5c0447f",
      "name": "Groq Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        2976,
        448
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f8e88a98-6ac3-4532-8317-e0069576d3aa",
      "name": "The Mismatch Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        3264,
        400
      ],
      "parameters": {
        "jsCode": "try {\n  const lead = $('Extract Review Snippets').item?.json || $input.first().json || {};\n  if (lead._error) return [{ json: lead }];\n\n  const openaiResp = $input.first().json || {};\n  let praisedServices = [];\n  try {\n    const content = openaiResp?.choices?.[0]?.message?.content || openaiResp?.text || '{}';\n    const parsed = JSON.parse(content);\n    praisedServices = Array.isArray(parsed.services) ? parsed.services : [];\n  } catch(e) { praisedServices = []; }\n\n  const currentMonth = parseInt(lead.current_month) || (new Date().getMonth() + 1);\n  const category = lead.category || 'Unknown';\n  const seasonMatrix = lead.season_matrix || {};\n  const conversionSignals = lead.conversion_signals || {};\n\n  const mkdwn = (lead.website_markdown || '').toLowerCase();\n  const websiteExists = !!lead.website_exists;\n  const hasHttps = lead.website_url ? lead.website_url.startsWith('https') : false;\n  const hasMobileViewport = mkdwn.includes('viewport');\n  const copyrightMatch = mkdwn.match(/copyright[^\\d]*(20\\d{2})|\u00a9\\s*(20\\d{2})/);\n  const copyrightYear = copyrightMatch ? parseInt(copyrightMatch[1] || copyrightMatch[2]) : null;\n  const hasOldCopyright = copyrightYear ? copyrightYear < 2020 : false;\n\n  const convSignals = conversionSignals[category] || [];\n  const hasConversionPath = convSignals.some(s => mkdwn.includes(s.toLowerCase()));\n\n  const cmsMap = { WordPress: ['wp-content','wp-includes'], Wix: ['wix.com','wixsite'], Squarespace: ['squarespace'], Jimdo: ['jimdo'], Joomla: ['joomla'] };\n  let detectedCms = null;\n  for (const [cms, sigs] of Object.entries(cmsMap)) {\n    if (sigs.some(s => mkdwn.includes(s))) { detectedCms = cms; break; }\n  }\n\n  const revenueLeak = praisedServices.filter(svc =>\n    svc && typeof svc === 'string' && !mkdwn.includes(svc.toLowerCase())\n  );\n\n  const compMarkdowns = lead.competitor_markdowns || [];\n  let competitorsWithConversion = 0;\n  for (const md of compMarkdowns) {\n    if (convSignals.some(s => (md || '').toLowerCase().includes(s.toLowerCase()))) {\n      competitorsWithConversion++;\n    }\n  }\n\n  const seasonData = seasonMatrix[category];\n  let seasonalUrgency = null;\n  let weeksToPeak = null;\n  let peakLabel = null;\n  if (seasonData && Array.isArray(seasonData.peak_months)) {\n    peakLabel = seasonData.label;\n    const diffs = seasonData.peak_months.map(pm => {\n      let d = pm - currentMonth;\n      if (d <= 0) d += 12;\n      return d;\n    });\n    const minMonths = Math.min(...diffs);\n    weeksToPeak = Math.round(minMonths * 4.33);\n    seasonalUrgency = minMonths <= 2 ? 'CRITICAL' : minMonths <= 4 ? 'HIGH' : 'MEDIUM';\n  }\n\n  const reviewCount = parseInt(lead.directory_review_count) || 0;\n  const rating = parseFloat(lead.directory_rating) || 0;\n\n  const reputationScore = Math.min(100, (Math.min(reviewCount, 200) / 200 * 60) + (rating / 5 * 40));\n\n  let digitalScore = 0;\n  if (websiteExists) {\n    if (hasHttps) digitalScore += 20;\n    if (hasMobileViewport) digitalScore += 20;\n    if (!hasOldCopyright) digitalScore += 15;\n    if (hasConversionPath) digitalScore += 30;\n    if (!detectedCms || !['Wix','Jimdo','Joomla'].includes(detectedCms)) digitalScore += 15;\n  }\n\n  const mismatchRaw = reputationScore - (digitalScore * 0.6);\n  const mismatchScore = Math.max(0, Math.min(100, Math.round(mismatchRaw) + 40));\n\n  let opportunityScore = mismatchScore;\n  if (revenueLeak.length > 0) opportunityScore += 15;\n  if (competitorsWithConversion > 0) opportunityScore += 10;\n  if (seasonalUrgency === 'CRITICAL') opportunityScore += 10;\n  else if (seasonalUrgency === 'HIGH') opportunityScore += 5;\n  opportunityScore = Math.min(100, Math.round(opportunityScore));\n\n  const opportunityTier = opportunityScore >= 75 ? 'High' : opportunityScore >= 50 ? 'Medium' : 'Low';\n\n  const objectiveIssues = [];\n  const heuristicIssues = [];\n  if (!websiteExists) objectiveIssues.push('No website found in public listings');\n  else {\n    if (!hasHttps) objectiveIssues.push('No HTTPS');\n    if (!hasMobileViewport) objectiveIssues.push('No mobile viewport meta tag');\n    if (hasOldCopyright) objectiveIssues.push(`Copyright year ${copyrightYear}`);\n    if (!hasConversionPath) objectiveIssues.push(`No ${category} conversion path`);\n  }\n  if (detectedCms && ['Wix','Jimdo','Joomla'].includes(detectedCms)) heuristicIssues.push(`Built with ${detectedCms} \u2014 may indicate template-based site`);\n  if (hasOldCopyright) heuristicIssues.push('Site freshness appears low');\n\n  const websiteIssues = [\n    ...objectiveIssues.map(i => `[Objective] ${i}`),\n    ...heuristicIssues.map(i => `[Heuristic] ${i}`)\n  ].join('; ') || 'None detected';\n\n  const observedFacts = [];\n  const heuristicAssessment = [];\n  if (!websiteExists) observedFacts.push('No website found in public directory listings');\n  else {\n    observedFacts.push(`Website: ${lead.website_url}`);\n    observedFacts.push(hasHttps ? 'HTTPS present' : 'HTTPS absent');\n    observedFacts.push(hasMobileViewport ? 'Mobile viewport tag present' : 'Mobile viewport tag absent');\n    if (copyrightYear) observedFacts.push(`Footer copyright: ${copyrightYear}`);\n    observedFacts.push(hasConversionPath ? `${category} conversion path detected` : `No ${category} conversion path`);\n    if (detectedCms) observedFacts.push(`CMS: ${detectedCms}`);\n  }\n  if (reviewCount > 0) observedFacts.push(`${reviewCount} public reviews at ${rating} stars average`);\n  if (hasOldCopyright) heuristicAssessment.push(`Site freshness low (~${copyrightYear})`);\n  if (detectedCms && ['Wix','Jimdo','Joomla'].includes(detectedCms)) heuristicAssessment.push(`${detectedCms} template suggests DIY build`);\n  if (competitorsWithConversion > 0) heuristicAssessment.push(`${competitorsWithConversion} nearby competitors appear more digitally capable`);\n  if (reputationScore > 70 && digitalScore < 40) heuristicAssessment.push('Strong reputation paired with weak digital execution');\n\n  let pitchHook, pitchAngle;\n  if (revenueLeak.length > 0) {\n    pitchAngle = 'Revenue Leak';\n    pitchHook = `Your customers are telling Google you do exceptional ${revenueLeak[0]}. Your website does not know that word exists.`;\n  } else if (!websiteExists) {\n    pitchAngle = 'Missing Presence';\n    pitchHook = `You have ${reviewCount} reviews and ${rating} stars. Every person who finds you online and wants to book has nowhere to go.`;\n  } else if (!hasConversionPath) {\n    pitchAngle = 'Missing Conversion';\n    pitchHook = `You have ${reviewCount} strong reviews and no way for a new customer to take action after reading them.`;\n  } else if (competitorsWithConversion > 0) {\n    pitchAngle = 'Competitive Pressure';\n    pitchHook = `${competitorsWithConversion} of your nearest competitors have online booking. You are the only one in this cluster without it.`;\n  } else if (seasonalUrgency === 'CRITICAL') {\n    pitchAngle = 'Seasonal Timing';\n    pitchHook = `${peakLabel} is ${weeksToPeak} weeks away. Now is the window where a website upgrade pays for itself fastest.`;\n  } else {\n    pitchAngle = 'General Mismatch';\n    pitchHook = `Strong reputation with ${reviewCount} reviews \u2014 your digital presence is not converting that trust into new bookings.`;\n  }\n\n  const whyPoints = [\n    !websiteExists ? 'No website in public listings' : null,\n    revenueLeak.length > 0 ? `Revenue leak: praised services absent from website (${revenueLeak.slice(0,2).join(', ')})` : null,\n    !hasConversionPath && websiteExists ? `No ${category} conversion path` : null,\n    competitorsWithConversion > 0 ? `${competitorsWithConversion} nearby competitors stronger digitally` : null,\n    seasonalUrgency === 'CRITICAL' ? `Peak season in ~${weeksToPeak} weeks` : null\n  ].filter(Boolean);\n\n  return [{ json: {\n    run_date: lead.run_date,\n    business_name: lead.business_name,\n    category,\n    address: lead.address || 'not listed',\n    phone: lead.phone || 'not listed',\n    website: lead.website_url || 'none',\n    website_issues: websiteIssues,\n    services_listed: Array.isArray(lead.services_listed) ? lead.services_listed.join(', ') : 'not listed',\n    opportunity_score: opportunityTier,\n    opportunity_score_numeric: opportunityScore,\n    why_a_lead: whyPoints.join('. '),\n    source_links: lead.directory_source_url || 'not listed',\n    public_rating: rating,\n    review_count: reviewCount,\n    tech_signals: detectedCms || 'not detected',\n    freshness_signal: copyrightYear ? `Footer copyright ${copyrightYear}` : 'not detected',\n    competitor_urls: (lead.competitor_urls || []).join(', '),\n    competitors_with_booking: competitorsWithConversion,\n    website_screenshot: lead.website_screenshot || null,\n    praised_services: praisedServices.join(', ') || 'none extracted',\n    revenue_leak_services: revenueLeak.join(', ') || 'none',\n    revenue_leak_detected: revenueLeak.length > 0,\n    seasonal_urgency: seasonalUrgency || 'not applicable',\n    weeks_to_peak_season: weeksToPeak || null,\n    peak_season_context: peakLabel || null,\n    reputation_score: parseFloat(reputationScore.toFixed(1)),\n    digital_score: parseFloat(digitalScore.toFixed(1)),\n    mismatch_score: mismatchScore,\n    pitch_angle: pitchAngle,\n    pitch_hook: pitchHook,\n    observed_facts: observedFacts.join('. '),\n    heuristic_assessment: heuristicAssessment.join('. ') || 'No heuristic flags'\n  }}];\n} catch (error) {\n  const lead = $('Extract Review Snippets').item?.json || $input.first().json || {};\n  return [{ json: { ...lead, _error: error.message, _failed_step: 'mismatch_engine' } }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "eacd61a7-1f9c-4913-857e-9d8d7fe5eb6c",
      "name": "Aggregate All Leads",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        688,
        896
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "a37a5b0e-5e88-4b45-af25-5ab7fd05f72c",
      "name": "Rank and Deduplicate",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        896
      ],
      "parameters": {
        "jsCode": "const aggregated = $('Aggregate All Leads').first().json.data || $('Aggregate All Leads').first().json || [];\nconst leadsArray = Array.isArray(aggregated) ? aggregated : [aggregated];\n\nconst newLeads = leadsArray\n  .filter(l => l && l.business_name && !l._error) \n  .sort((a, b) => (b.opportunity_score_numeric || 0) - (a.opportunity_score_numeric || 0))\n  .slice(0, 10);\n\nif (newLeads.length === 0) return [{ json: { _no_new_leads: true } }];\nreturn newLeads.map(l => ({ json: l }));"
      },
      "typeVersion": 2
    },
    {
      "id": "553c7d96-7b05-441d-a1fa-4522e03b833d",
      "name": "Filter Valid Leads",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        896
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst validLeads = items.filter(item => !item.json._no_new_leads && !item.json._error);\nif (validLeads.length === 0) return [];\nreturn validLeads;"
      },
      "typeVersion": 2
    },
    {
      "id": "f397728b-ddc0-4e92-80e8-084a9780af59",
      "name": "Save to n8n Table",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        1360,
        800
      ],
      "parameters": {
        "columns": {
          "value": {
            "website": "={{ $json.website }}",
            "category": "={{ $json.category }}",
            "pitch_hook": "={{ $json.pitch_hook }}",
            "business_name": "={{ $json.business_name }}",
            "opportunity_score": "={{ $json.opportunity_score }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "2pGhdxpAThxGwmss",
          "cachedResultUrl": "/projects/hDHhdcMr4jVn06kt/datatables/2pGhdxpAThxGwmss",
          "cachedResultName": "Lead Database"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "6af50b02-95e0-421d-b774-6f6d3ed27b12",
      "name": "Aggregate for Report",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1360,
        992
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "842b3184-8e0e-483f-9221-74ff791c7dda",
      "name": "Build HTML Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        896
      ],
      "parameters": {
        "jsCode": "const allLeads = $input.first().json.data || [];\nconst leads = allLeads.filter(l => !l._no_new_leads);\nconst top5 = leads.slice(0, 5);\nconst runDate = new Date().toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });\n\nif (leads.length === 0) {\n return [{ json: { html_report: '<html><body><h1>No new leads this week</h1></body></html>' } }];\n}\n\nconst tierColor = t => ({ High: '#10b981', Medium: '#f59e0b', Low: '#6b7280' }[t] || '#6b7280');\nconst ue = u => ({ CRITICAL: '\ud83d\udd34', HIGH: '\ud83d\udfe0', MEDIUM: '\ud83d\udfe1' }[u] || '');\n\nconst renderLead = (lead, rank) => {\n const hasShot = lead.website_screenshot && String(lead.website_screenshot).length > 100;\n const urgEmoji = lead.seasonal_urgency && lead.seasonal_urgency !== 'not applicable' ? ue(lead.seasonal_urgency) : '';\n return `\\n<div style=\"background:#1a1a1a;border:1px solid #333;border-radius:12px;padding:28px;margin-bottom:20px;\">\\n <div style=\"display:flex;align-items:center;gap:16px;margin-bottom:20px;\">\\n <div style=\"font-size:26px;font-weight:800;color:#444;min-width:50px;\">#${rank+1}</div>\\n <div style=\"flex:1;\">\\n <h2 style=\"color:#fff;margin:0 0 4px;font-size:20px;\">${lead.business_name}</h2>\\n <div style=\"color:#aaa;font-size:13px;\">${lead.category} \u00b7 ${lead.address}</div>\\n </div>\\n <div style=\"border:2px solid ${tierColor(lead.opportunity_score)};border-radius:10px;padding:10px 16px;text-align:center;\">\\n <div style=\"font-size:24px;font-weight:800;color:#fff;\">${lead.opportunity_score_numeric}</div>\\n <div style=\"font-size:11px;font-weight:700;color:${tierColor(lead.opportunity_score)};text-transform:uppercase;\">${lead.opportunity_score}</div>\\n </div>\\n </div>\\n <div style=\"background:#111;border-radius:8px;padding:14px;margin-bottom:16px;display:flex;flex-wrap:wrap;gap:14px;font-size:13px;color:#ccc;\">\\n <span>\u2b50 ${lead.public_rating} stars \u00b7 ${lead.review_count} reviews</span>\\n <span>\ud83d\udcde ${lead.phone}</span>\\n <span>\ud83c\udf10 ${lead.website !== 'none' ? `<a href=\"${lead.website}\" style=\"color:#3b82f6;\">${lead.website}</a>` : '<em>No website</em>'}</span>\\n ${urgEmoji ? `<span style=\"color:#f59e0b;font-weight:600;\">${urgEmoji} Peak in ~${lead.weeks_to_peak_season} weeks</span>` : ''}\\n </div>\\n ${hasShot ? `<div style=\"margin-bottom:16px;border-radius:8px;overflow:hidden;border:1px solid #333;max-height:280px;\"><img src=\"${lead.website_screenshot}\" style=\"width:100%;display:block;\" /></div>` : ''}\\n <div style=\"border-left:3px solid #10b981;background:linear-gradient(135deg,#064e3b15,#1e3a8a15);padding:16px;border-radius:8px;margin-bottom:16px;\">\\n <div style=\"font-size:11px;font-weight:700;color:#10b981;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;\">\ud83d\udcac ${lead.pitch_angle} Pitch</div>\\n <div style=\"font-size:16px;font-style:italic;color:#f5f5f5;line-height:1.5;\">${lead.pitch_hook}</div>\\n </div>\\n <div style=\"margin-bottom:16px;\">\\n <div style=\"display:grid;grid-template-columns:130px 1fr 36px;gap:10px;align-items:center;font-size:12px;margin-bottom:6px;\">\\n <span style=\"color:#aaa;\">Reputation</span><div style=\"background:#0a0a0a;border-radius:3px;height:7px;overflow:hidden;\"><div style=\"background:#10b981;width:${lead.reputation_score}%;height:100%;\"></div></div><span style=\"color:#ccc;text-align:right;\">${lead.reputation_score}</span>\\n </div>\\n <div style=\"display:grid;grid-template-columns:130px 1fr 36px;gap:10px;align-items:center;font-size:12px;margin-bottom:6px;\">\\n <span style=\"color:#aaa;\">Digital Presence</span><div style=\"background:#0a0a0a;border-radius:3px;height:7px;overflow:hidden;\"><div style=\"background:#3b82f6;width:${lead.digital_score}%;height:100%;\"></div></div><span style=\"color:#ccc;text-align:right;\">${lead.digital_score}</span>\\n </div>\\n <div style=\"display:grid;grid-template-columns:130px 1fr 36px;gap:10px;align-items:center;font-size:12px;\">\\n <span style=\"color:#fbbf24;font-weight:600;\">Mismatch</span><div style=\"background:#0a0a0a;border-radius:3px;height:7px;overflow:hidden;\"><div style=\"background:linear-gradient(90deg,#f59e0b,#dc2626);width:${lead.mismatch_score}%;height:100%;\"></div></div><span style=\"color:#fbbf24;text-align:right;font-weight:600;\">${lead.mismatch_score}</span>\\n </div>\\n </div>\\n ${lead.revenue_leak_detected ? `<div style=\"background:#7f1d1d20;border-left:3px solid #ef4444;padding:12px 16px;border-radius:6px;margin-bottom:10px;font-size:13px;\"><strong style=\"color:#f87171;\">\ud83d\udcb8 Revenue Leak:</strong> Customers praise <em>${lead.revenue_leak_services}</em> but absent from website.</div>` : ''}\\n ${lead.competitors_with_booking > 0 ? `<div style=\"background:#7c2d1220;border-left:3px solid #f97316;padding:12px 16px;border-radius:6px;margin-bottom:10px;font-size:13px;\"><strong style=\"color:#fb923c;\">\u2694\ufe0f Competitor Pressure:</strong> ${lead.competitors_with_booking} nearby competitors have conversion paths this lead lacks.</div>` : ''}\\n <div style=\"display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:16px;padding-top:16px;border-top:1px solid #333;font-size:12px;\">\\n <div><div style=\"color:#666;text-transform:uppercase;font-size:10px;letter-spacing:0.5px;margin-bottom:4px;\">Observed Facts</div><div style=\"color:#ccc;\">${lead.observed_facts}</div></div>\\n <div><div style=\"color:#666;text-transform:uppercase;font-size:10px;letter-spacing:0.5px;margin-bottom:4px;\">Heuristic Assessment</div><div style=\"color:#999;font-style:italic;\">${lead.heuristic_assessment}</div></div>\\n </div>\\n <div style=\"margin-top:14px;padding-top:12px;border-top:1px solid #333;display:flex;justify-content:space-between;font-size:11px;color:#666;\">\\n <span>${lead.tech_signals !== 'not detected' ? 'Built on ' + lead.tech_signals : ''}</span>\\n <a href=\"${lead.source_links}\" style=\"color:#3b82f6;text-decoration:none;\">Directory source \u2192</a>\\n </div>\\n</div>`;\n};\n\nconst html = `<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Mismatch Engine Report \u2014 ${runDate}</title></head>\\n<body style=\"background:#0a0a0a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Inter,sans-serif;padding:40px 20px;line-height:1.5;\">\\n<div style=\"max-width:860px;margin:0 auto;\">\\n <div style=\"text-align:center;padding:40px 0 50px;border-bottom:1px solid #222;margin-bottom:36px;\">\\n <h1 style=\"font-size:38px;font-weight:800;margin:0 0 8px;background:linear-gradient(135deg,#10b981,#3b82f6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;\">Mismatch Engine Report</h1>\\n <div style=\"color:#aaa;font-size:15px;\">${runDate} \u00b7 Top ${top5.length} leads by Mismatch Score</div>\\n </div>\\n <div style=\"display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:36px;\">\\n <div style=\"background:#171717;border:1px solid #333;border-radius:10px;padding:18px;text-align:center;\"><div style=\"font-size:28px;font-weight:700;color:#10b981;\">${leads.length}</div><div style=\"color:#666;font-size:12px;margin-top:3px;\">New Leads</div></div>\\n <div style=\"background:#171717;border:1px solid #333;border-radius:10px;padding:18px;text-align:center;\"><div style=\"font-size:28px;font-weight:700;color:#ef4444;\">${leads.filter(l=>l.revenue_leak_detected).length}</div><div style=\"color:#666;font-size:12px;margin-top:3px;\">Revenue Leaks</div></div>\\n <div style=\"background:#171717;border:1px solid #333;border-radius:10px;padding:18px;text-align:center;\"><div style=\"font-size:28px;font-weight:700;color:#f59e0b;\">${leads.filter(l=>l.website==='none').length}</div><div style=\"color:#666;font-size:12px;margin-top:3px;\">No Website</div></div>\\n <div style=\"background:#171717;border:1px solid #333;border-radius:10px;padding:18px;text-align:center;\"><div style=\"font-size:28px;font-weight:700;color:#3b82f6;\">${leads.filter(l=>l.seasonal_urgency==='CRITICAL').length}</div><div style=\"color:#666;font-size:12px;margin-top:3px;\">Critical Timing</div></div>\\n </div>\\n ${top5.map((l,i) => renderLead(l,i)).join('')}\\n <div style=\"text-align:center;padding:30px 0;color:#444;font-size:11px;\">Generated by The Mismatch Engine \u00b7 n8n \u00d7 Firecrawl</div>\\n</div></body></html>`;\n\nreturn [{ json: { html_report: html, lead_count: leads.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "2dc123c4-0d04-4475-b94f-20a3adc5f0d3",
      "name": "Build Slack Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        1088
      ],
      "parameters": {
        "jsCode": "const aggregated = $('Aggregate for Report').first().json.data || [];\nif (aggregated.length === 0) return [{ json: { _no_new_leads: true } }];\nreturn aggregated.map(l => ({ json: { ...l } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "9bd29ec0-5738-445b-aa8c-68f2f8667aaf",
      "name": "Send Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        1808,
        1088
      ],
      "parameters": {
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.2
    },
    {
      "id": "85c2658a-2464-466b-9275-b5c78e3b992e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3344,
        0
      ],
      "parameters": {
        "width": 486,
        "height": 640,
        "content": "\n\n\n\n# The Mismatch Engine: AI Lead Discovery for Local Businesses\n## How it works\n1. **Trigger**: Runs automatically every week (or manually).\n2. **Discover**: AI & Firecrawl hunt down relevant local businesses.\n3. **Scrape**: Analyzes websites, top competitors, and reviews in depth.\n4. **Evaluate**: Calculates a Mismatch Score (reputation vs. digital presence).\n5. **Report**: Generates an HTML report and saves leads to the database.\n\n## Setup steps\n- [ ] Connect Firecrawl API Credentials\n- [ ] Connect Groq API Credentials (LLM)\n- [ ] Update `location` and `category` in Configuration node\n- [ ] Set up 'Lead Database' n8n Table\n- [ ] (Optional) Connect Slack account for notifications"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "de31735c-cdee-4d66-ab16-3cda8368c8c0",
  "connections": {
    "Has Website?": {
      "main": [
        [
          {
            "node": "Scrape Website",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Website Path",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Generate Category Signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Website": {
      "main": [
        [
          {
            "node": "Find Competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Website": {
      "main": [
        [
          {
            "node": "Merge Website",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Reviews": {
      "main": [
        [
          {
            "node": "Extract Review Snippets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain": {
      "main": [
        [
          {
            "node": "Parse Business List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "No Website Path": {
      "main": [
        [
          {
            "node": "Find Competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain1": {
      "main": [
        [
          {
            "node": "The Mismatch Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain2": {
      "main": [
        [
          {
            "node": "Parse Category Signals ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Discovery Search": {
      "main": [
        [
          {
            "node": "Format Search Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter New Leads": {
      "main": [
        [
          {
            "node": "Process Each Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Competitors": {
      "main": [
        [
          {
            "node": "Filter Competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Groq Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain2",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Delay": {
      "main": [
        [
          {
            "node": "Scrape Business Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Competitors": {
      "main": [
        [
          {
            "node": "Search Reviews",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Each Lead": {
      "main": [
        [
          {
            "node": "Rate Limit Delay",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Aggregate All Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Competitor": {
      "main": [
        [
          {
            "node": "Merge Competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Competitors": {
      "main": [
        [
          {
            "node": "Scrape Competitor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Monday 9AM": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Report": {
      "main": [
        [
          {
            "node": "Send Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Competitors": {
      "main": [
        [
          {
            "node": "Split Competitors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Valid Leads": {
      "main": [
        [
          {
            "node": "Save to n8n Table",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate for Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate All Leads": {
      "main": [
        [
          {
            "node": "Rank and Deduplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Business List": {
      "main": [
        [
          {
            "node": "Fetch Existing Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "The Mismatch Engine": {
      "main": [
        [
          {
            "node": "Process Each Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate for Report": {
      "main": [
        [
          {
            "node": "Build HTML Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Slack Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Existing Leads": {
      "main": [
        [
          {
            "node": "Filter New Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank and Deduplicate": {
      "main": [
        [
          {
            "node": "Filter Valid Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Search Context": {
      "main": [
        [
          {
            "node": "Basic LLM Chain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Review Snippets": {
      "main": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Category Signals ": {
      "main": [
        [
          {
            "node": "Discovery Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Business Profile": {
      "main": [
        [
          {
            "node": "Has Website?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Category Signals": {
      "main": [
        [
          {
            "node": "Basic LLM Chain2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}