{
  "id": "wesSFaik8lD7g9lq",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Monitor Competitor Prices with Firecrawl, GPT-4.1 & Send Alerts to Gmail",
  "tags": [
    {
      "id": "7zsdOA50QGm7RNqx",
      "name": "Monitoring",
      "createdAt": "2025-10-23T16:41:17.031Z",
      "updatedAt": "2025-10-23T16:41:17.031Z"
    },
    {
      "id": "BL8TsHYj5FkNYzfi",
      "name": "E-commerce",
      "createdAt": "2025-10-23T16:41:16.985Z",
      "updatedAt": "2025-10-23T16:41:16.985Z"
    },
    {
      "id": "dlf9zFSN3j6s2jgO",
      "name": "Business Intelligence",
      "createdAt": "2025-10-23T16:41:17.008Z",
      "updatedAt": "2025-10-23T16:41:17.008Z"
    },
    {
      "id": "lpozR2Ct8reF9bCk",
      "name": "AI",
      "createdAt": "2025-10-23T16:41:17.062Z",
      "updatedAt": "2025-10-23T16:41:17.062Z"
    }
  ],
  "nodes": [
    {
      "id": "1a7684b3-eb26-414f-ad30-e613306b50b1",
      "name": "\ud83d\udcca Read Historical Data",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Loads previous scan data for comparison",
      "position": [
        -1472,
        176
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "mode": "name",
          "value": "Historical Data"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.GOOGLE_SHEET_ID }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "9239f63d-2717-4248-918a-b886016c9a98",
      "name": "\ud83d\udd00 Merge Current with Historical",
      "type": "n8n-nodes-base.merge",
      "notes": "Combines current scrape with historical data for comparison",
      "position": [
        -1296,
        0
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "fieldsToMatchString": "rawResponse.message.content"
      },
      "typeVersion": 3
    },
    {
      "id": "3b453791-3aca-4563-97d7-34d1bc751824",
      "name": "\ud83d\udd0d Detect Price & Stock Changes",
      "type": "n8n-nodes-base.code",
      "notes": "Intelligent change detection with alert level classification",
      "position": [
        -1120,
        0
      ],
      "parameters": {
        "jsCode": "// Compare current prices with historical and detect changes\nconst results = [];\n\nfor (const item of $input.all()) {\n  const current = item.json;\n  \n  // Skip error items\n  if (current.error) {\n    results.push({ json: current });\n    continue;\n  }\n  \n  // Find historical data for this competitor\n  const historical = $('\ud83d\udcca Read Historical Data').all()\n    .find(h => h.json.competitorName === current.competitorName);\n  \n  let alertLevel = 'none';\n  let changes = [];\n  \n  if (historical && historical.json.currentPrice) {\n    const oldPrice = parseFloat(historical.json.currentPrice);\n    const newPrice = parseFloat(current.currentPrice);\n    const priceChange = newPrice - oldPrice;\n    const priceChangePercent = ((priceChange / oldPrice) * 100).toFixed(2);\n    \n    current.priceChange = priceChange;\n    current.priceChangePercent = parseFloat(priceChangePercent);\n    current.previousPrice = oldPrice;\n    \n    // Determine alert level based on price changes\n    if (Math.abs(priceChangePercent) >= 20) {\n      alertLevel = 'critical';\n      changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n    } else if (Math.abs(priceChangePercent) >= 10) {\n      alertLevel = 'warning';\n      changes.push(`Price ${priceChange > 0 ? 'increased' : 'decreased'} by ${Math.abs(priceChangePercent)}%`);\n    } else if (Math.abs(priceChangePercent) >= 5) {\n      alertLevel = 'info';\n      changes.push(`Minor price change: ${priceChangePercent}%`);\n    }\n    \n    // Check if it's a new low price\n    const historicalLow = parseFloat(historical.json.lowestPrice || oldPrice);\n    if (newPrice < historicalLow) {\n      current.isNewLow = true;\n      alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n      changes.push('\ud83c\udfaf NEW LOWEST PRICE!');\n    }\n    current.lowestPrice = Math.min(newPrice, historicalLow);\n    \n    // Stock level changes\n    if (historical.json.stockLevel !== current.stockLevel) {\n      if (current.stockLevel === 'Out of Stock') {\n        alertLevel = 'critical';\n        changes.push('\ud83d\udce6 Product went OUT OF STOCK');\n      } else if (current.stockLevel === 'Low Stock') {\n        alertLevel = alertLevel === 'none' ? 'warning' : alertLevel;\n        changes.push('\u26a0\ufe0f Stock level is LOW');\n      } else if (historical.json.stockLevel === 'Out of Stock' && current.inStock) {\n        alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n        changes.push('\u2705 Back in stock!');\n      }\n    }\n    \n    // Rating changes\n    const oldRating = parseFloat(historical.json.rating || 0);\n    const newRating = parseFloat(current.rating || 0);\n    const ratingChange = newRating - oldRating;\n    \n    if (Math.abs(ratingChange) >= 0.5) {\n      alertLevel = alertLevel === 'none' ? 'info' : alertLevel;\n      changes.push(`\u2b50 Rating ${ratingChange > 0 ? 'improved' : 'dropped'} by ${Math.abs(ratingChange).toFixed(1)} stars`);\n    }\n    \n    // Review count changes\n    const oldReviews = parseInt(historical.json.reviewCount || 0);\n    const newReviews = parseInt(current.reviewCount || 0);\n    const reviewDiff = newReviews - oldReviews;\n    \n    if (reviewDiff > 0) {\n      changes.push(`\ud83d\udcac ${reviewDiff} new review${reviewDiff > 1 ? 's' : ''}`);\n    }\n  } else {\n    // First time seeing this competitor\n    alertLevel = 'info';\n    changes.push('\ud83c\udd95 First time tracking this competitor');\n    current.lowestPrice = current.currentPrice;\n  }\n  \n  current.alertLevel = alertLevel;\n  current.changesSummary = changes.join(' | ');\n  current.hasChanges = alertLevel !== 'none';\n  \n  results.push({ json: current });\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "c83f80f9-8954-406e-a43a-20299f94d4ef",
      "name": "\ud83d\udcbe Update Historical Data",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Saves current data to historical tracking sheet",
      "position": [
        -944,
        80
      ],
      "parameters": {
        "columns": {
          "value": {
            "rating": "={{ $json.rating }}",
            "inStock": "={{ $json.inStock }}",
            "currency": "={{ $json.currency }}",
            "scrapedAt": "={{ $json.scrapedAt }}",
            "productUrl": "={{ $json.productUrl }}",
            "stockLevel": "={{ $json.stockLevel }}",
            "lowestPrice": "={{ $json.lowestPrice }}",
            "productName": "={{ $json.productName }}",
            "reviewCount": "={{ $json.reviewCount }}",
            "currentPrice": "={{ $json.currentPrice }}",
            "originalPrice": "={{ $json.originalPrice }}",
            "competitorName": "={{ $json.competitorName }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "mode": "name",
          "value": "Historical Data"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "= {{ $env.GOOGLE_SHEET_ID }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c1ba6419-781d-4c2d-bfa6-dfe202751e19",
      "name": "\ud83d\udcdd Log Alert Details",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Logs all alerts to separate tracking sheet",
      "position": [
        -944,
        -80
      ],
      "parameters": {
        "columns": {
          "value": {
            "rating": "={{ $json.rating }}",
            "timestamp": "={{ $json.scrapedAt }}",
            "alertLevel": "={{ $json.alertLevel }}",
            "productUrl": "={{ $json.productUrl }}",
            "stockLevel": "={{ $json.stockLevel }}",
            "priceChange": "={{ $json.priceChange || 0 }}",
            "productName": "={{ $json.productName }}",
            "currentPrice": "={{ $json.currentPrice }}",
            "previousPrice": "={{ $json.previousPrice || 'N/A' }}",
            "changesSummary": "={{ $json.changesSummary }}",
            "competitorName": "={{ $json.competitorName }}",
            "priceChangePercent": "={{ $json.priceChangePercent || 0 }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "mode": "name",
          "value": "Alert Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.GOOGLE_SHEET_ID }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "233f07c7-c121-4e14-abf1-0a917b023a11",
      "name": "\ud83d\udcca Aggregate Daily Digest",
      "type": "n8n-nodes-base.code",
      "notes": "Combines all alerts into a comprehensive summary",
      "position": [
        -944,
        -240
      ],
      "parameters": {
        "jsCode": "// Aggregate all items for daily digest email\nconst allItems = $input.all();\n\nconst criticalAlerts = allItems.filter(item => item.json.alertLevel === 'critical');\nconst warningAlerts = allItems.filter(item => item.json.alertLevel === 'warning');\nconst infoAlerts = allItems.filter(item => item.json.alertLevel === 'info');\nconst noChanges = allItems.filter(item => item.json.alertLevel === 'none');\n\nconst summary = {\n  totalCompetitors: allItems.length,\n  criticalCount: criticalAlerts.length,\n  warningCount: warningAlerts.length,\n  infoCount: infoAlerts.length,\n  noChangeCount: noChanges.length,\n  timestamp: new Date().toISOString(),\n  criticalAlerts: criticalAlerts.map(i => ({\n    competitor: i.json.competitorName,\n    product: i.json.productName,\n    changes: i.json.changesSummary,\n    price: `${i.json.currency} ${i.json.currentPrice}`,\n    priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n    stock: i.json.stockLevel,\n    url: i.json.productUrl\n  })),\n  warningAlerts: warningAlerts.map(i => ({\n    competitor: i.json.competitorName,\n    product: i.json.productName,\n    changes: i.json.changesSummary,\n    price: `${i.json.currency} ${i.json.currentPrice}`,\n    priceChange: i.json.priceChangePercent ? `${i.json.priceChangePercent}%` : 'N/A',\n    stock: i.json.stockLevel\n  })),\n  infoAlerts: infoAlerts.map(i => ({\n    competitor: i.json.competitorName,\n    product: i.json.productName,\n    changes: i.json.changesSummary\n  }))\n};\n\nreturn [{ json: summary }];"
      },
      "typeVersion": 2
    },
    {
      "id": "bb79bf76-29bd-44d9-8064-48c14622e1f5",
      "name": "Scrape URL: nike.com",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "position": [
        -1904,
        -176
      ],
      "parameters": {
        "url": "https://www.nike.com/sg/w/mens-shoes-nik1zy7ok",
        "operation": "scrape",
        "scrapeOptions": {
          "options": {
            "formats": {
              "format": [
                {
                  "type": "json",
                  "prompt": "price of the shoe"
                }
              ]
            },
            "headers": {}
          }
        },
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "905a1a25-92aa-4459-8d4e-8392d6ca4e61",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -2096,
        32
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "fb7b71da-f70d-48d8-afef-ee42bdb48567",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -800,
        -240
      ],
      "parameters": {
        "sendTo": " info@example.com",
        "message": "The pricing of the competitors is attached",
        "options": {},
        "subject": "Shoes pricing"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "5c08b0c2-5bc1-4594-be36-938e67308a1f",
      "name": "Scrape URL: adidas.com",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "position": [
        -1904,
        -16
      ],
      "parameters": {
        "url": "=https://www.adidas.com/us/men-shoes",
        "operation": "scrape",
        "scrapeOptions": {
          "options": {
            "formats": {
              "format": [
                {
                  "type": "json",
                  "prompt": "price of the shoe"
                }
              ]
            },
            "headers": {}
          }
        },
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "57ac5f72-cc18-4919-bdea-4e16939b8080",
      "name": "Scrape URL: sneakerpricer.com",
      "type": "@mendable/n8n-nodes-firecrawl.firecrawl",
      "position": [
        -1904,
        144
      ],
      "parameters": {
        "url": "=https://www.sneakerpricer.com/us-EN",
        "operation": "scrape",
        "scrapeOptions": {
          "options": {
            "formats": {
              "format": [
                {
                  "type": "json",
                  "prompt": "price of the shoe"
                }
              ]
            },
            "headers": {}
          }
        },
        "requestOptions": {}
      },
      "credentials": {
        "firecrawlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "5eb1ffd8-d98d-4682-b200-92ab2432fd81",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2656,
        -400
      ],
      "parameters": {
        "width": 2032,
        "height": 880,
        "content": "## Introduction\nAutomate price monitoring for e-commerce competitors\u2014ideal for retailers, analysts, and pricing teams.\n\n**\u26a0\ufe0f Self-Hosted Only:** Requires self-hosted n8n instance.\n## How It Works\nScrapes competitor URLs, extracts data via AI, detects price/stock changes, logs to Google Sheets with email alerts.\n## Workflow Template\nTrigger \u2192 Scrape \u2192 AI Extract \u2192 Parse \u2192 Compare \u2192 Detect Changes \u2192 Update Sheets + Alert\n## Workflow Steps\n1. **Scraping:** Firecrawl fetches Nike, Adidas, Sneaker data\n2. **AI Extraction:** Processes product details\n3. **Parsing:** Structures response\n4. **Historical Check:** Reads Sheets data\n5. **Change Detection:** Identifies price/stock updates\n6. **Dual Output:** Updates Sheets + sends alerts\n## Setup Instructions\n1. **Firecrawl API**\nGet key from dashboard \u2192 Add to n8n\n2. **OpenAI API**\nGet key from platform \u2192 Add to n8n\n3. **Google Sheets OAuth2**\nCreate OAuth2 in Google Cloud Console \u2192 Authorize in n8n \u2192 Enable API\n4. **Gmail OAuth2**\nUse same project \u2192 Authorize in n8n \u2192 Enable API\n5. **Spreadsheet Setup**\nCreate Sheet with required columns \u2192 Copy ID from URL \u2192 Paste in workflow\n## Prerequisites\nSelf-hosted n8n, Firecrawl account, OpenAI key, Google account (Sheets + Gmail OAuth2)\n## Customization\nAdd URLs, adjust thresholds, integrate Slack\n## Benefits\nSaves 2+ hours daily, real-time tracking, automated alerts-time competitor tracking, automated alerts, historical data analysis."
      },
      "typeVersion": 1
    },
    {
      "id": "738fd9c2-91ff-4f15-b7a3-1aa0e4c1f8a1",
      "name": "Converts unstructured AI text into organized, usable data fields",
      "type": "n8n-nodes-base.code",
      "notes": "Parses and validates the AI extracted data",
      "position": [
        -1504,
        -16
      ],
      "parameters": {
        "jsCode": "// Parse AI response and clean data\nconst items = [];\n\nfor (const item of $input.all()) {\n  try {\n    // Parse the AI response\n    let parsed;\n    const response = item.json.choices?.[0]?.message?.content || item.json.message || '';\n    \n    // Remove markdown code blocks if present\n    const cleaned = response.replace(/```json\\n?|```\\n?/g, '').trim();\n    \n    try {\n      parsed = JSON.parse(cleaned);\n    } catch (e) {\n      // Try to extract JSON from the response\n      const jsonMatch = cleaned.match(/\\{[\\s\\S]*\\}/);\n      if (jsonMatch) {\n        parsed = JSON.parse(jsonMatch[0]);\n      } else {\n        throw new Error('Could not parse JSON from AI response');\n      }\n    }\n    \n    // Enrich with metadata\n    items.push({\n      json: {\n        ...parsed,\n        scrapedAt: new Date().toISOString(),\n        priceChange: 0, // Will be calculated in comparison\n        priceChangePercent: 0,\n        isNewLow: false,\n        alertLevel: 'none'\n      }\n    });\n  } catch (error) {\n    console.error('Failed to parse item:', error.message);\n    // Add error item for debugging\n    items.push({\n      json: {\n        error: error.message,\n        rawResponse: item.json,\n        competitorName: 'Parse Error',\n        scrapedAt: new Date().toISOString()\n      }\n    });\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "2a1d727b-8147-42e7-bfcd-c3ed37af7bc7",
      "name": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
      "type": "n8n-nodes-base.openAi",
      "notes": "Uses OpenAI to intelligently extract structured data from HTML",
      "position": [
        -1696,
        -16
      ],
      "parameters": {
        "prompt": {
          "messages": [
            {
              "role": "system",
              "content": "You are a precise e-commerce data extraction expert. Extract shoes information from HTML and return ONLY valid JSON with no markdown formatting.\n\nExtract these fields:\n- productName: string\n- currentPrice: number (numeric value only, no currency symbols)\n- originalPrice: number (if discounted, otherwise same as currentPrice)\n- currency: string (USD, EUR, etc.)\n- inStock: boolean\n- stockLevel: string (\"In Stock\", \"Low Stock\", \"Out of Stock\", \"Limited\", etc.)\n- rating: number (0-5 scale)\n- reviewCount: number\n- lastUpdated: string (current ISO timestamp)\n- productUrl: string (from context)\n- competitorName: string (from context)\n\nReturn ONLY the JSON object, no explanations."
            },
            {
              "content": "HTML Content:\n{{ $json.body }}\n\nProduct URL: {{ $json.url || 'unknown' }}\nCompetitor: {{ $json.competitor || 'unknown' }}\n\nExtract the product data as JSON:"
            }
          ]
        },
        "options": {
          "temperature": 0.1
        },
        "resource": "chat",
        "chatModel": "gpt-4.1-mini",
        "requestOptions": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "652a2128-6e01-4d6e-8bcb-b3f844cec2a8",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1584,
        -352
      ],
      "parameters": {
        "color": 6,
        "width": 352,
        "height": 240,
        "content": "## Google Sheets Structure\n**Required Columns:**\n- **Product Name** (Column A)\n- **Current Price** (Column B)\n- **Previous Price** (Column C)\n- **Stock Status** (Column D)\n- **Last Updated** (Column E)\n- **URL** (Column F)\n- **Change Detected** (Column G)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "703c4545-13fd-46b1-9a69-7e8c6ec4c656",
  "connections": {
    "Send a message": {
      "main": [
        []
      ]
    },
    "Scrape URL: nike.com": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape URL: adidas.com": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Read Historical Data": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Merge Current with Historical",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "\ud83d\udcca Aggregate Daily Digest": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape URL: sneakerpricer.com": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd0d Detect Price & Stock Changes": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Aggregate Daily Digest",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udcbe Update Historical Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "\ud83d\udcdd Log Alert Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 Merge Current with Historical": {
      "main": [
        [
          {
            "node": "\ud83d\udd0d Detect Price & Stock Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Scrape URL: nike.com",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape URL: adidas.com",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape URL: sneakerpricer.com",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 AI Extract Product Data using GPT-4.1-mini": {
      "main": [
        [
          {
            "node": "Converts unstructured AI text into organized, usable data fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Converts unstructured AI text into organized, usable data fields": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Merge Current with Historical",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}