{
  "id": "rgpHmMpjYVZFi020",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Price Monitor (Template)",
  "tags": [],
  "nodes": [
    {
      "id": "f9298f65-d6ca-49b5-8aa9-8949e3d300e6",
      "name": "Main Sticky",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2608,
        1088
      ],
      "parameters": {
        "width": 450,
        "height": 612,
        "content": "### \ud83d\udecd\ufe0f AI Price Monitor & Aggregator\n\n#### How it works\nThis workflow takes a product name from Telegram, searches Google for purchase links (Amazon EG & Jumia), scrapes the prices, and uses AI to summarize the best deal.\n1. **Trigger:** Receive product name via Telegram.\n2. **Search:** Google Search finds relevant URLs.\n3. **Scrape:** Parallel processing scrapes Amazon and Jumia/Other sites separately.\n4. **AI Analysis:** GPT-4o extracts the product details and prices from the raw HTML.\n5. **Response:** Sends a clean summary back to Telegram.\n\n#### Setup steps\n1. **Credentials:** configure your `Telegram API`, `OpenAI`, and `Decodo` credentials.\n2. **Decodo:** Ensure your Decodo account allows Google Search operations.\n3. **Webhook:** Verify the Telegram webhook is active.\n\n#### Customization tips\n- **Sites:** Edit the `Build Queries` node to add more stores (e.g., noon.com).\n- **Filters:** Adjust the `Router` node if you want to add specific scraping logic for other domains."
      },
      "typeVersion": 1
    },
    {
      "id": "51a537b5-3a24-4055-a3b4-4ad53ef52a42",
      "name": "Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2144,
        1232
      ],
      "parameters": {
        "color": 7,
        "width": 900,
        "height": 260,
        "content": "## 1. Get Links & Route\nReceives the user request, searches Google, and separates Amazon links from others for specialized handling."
      },
      "typeVersion": 1
    },
    {
      "id": "4b054f9d-c25c-4166-bb02-ac3257c1a0d3",
      "name": "Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1296,
        896
      ],
      "parameters": {
        "color": 7,
        "width": 750,
        "height": 300,
        "content": "## 2. Amazon Scraping\nSpecialized HTML cleaning and scoring logic optimized for Amazon product pages."
      },
      "typeVersion": 1
    },
    {
      "id": "4feef981-8b47-4b59-8066-028c6d6c5589",
      "name": "Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1328,
        1632
      ],
      "parameters": {
        "color": 7,
        "width": 750,
        "height": 300,
        "content": "## 3. General Scraping\nGeneric scraping logic for Jumia and other supported e-commerce sites."
      },
      "typeVersion": 1
    },
    {
      "id": "be4e928a-c8a9-45cf-9a32-7d95d27b6d03",
      "name": "Section 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        1232
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 380,
        "content": "## 4. AI Extraction & Reply\nMerges results and uses an AI Agent to extract structured price data and reply to the user."
      },
      "typeVersion": 1
    },
    {
      "id": "6d1b1c0b-fdc6-4d34-b364-4bffca5ec4c3",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        -2112,
        1344
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "b345f524-39b0-494d-aab3-cda1f8a886b4",
      "name": "Build Queries",
      "type": "n8n-nodes-base.code",
      "position": [
        -1904,
        1344
      ],
      "parameters": {
        "jsCode": "const product = $json.message.text?.trim();\nif (!product) return [];\n\nconst stores = [\n  { store: \"amazon\", site: \"amazon.eg\" },\n  { store: \"jumia\", site: \"jumia.com.eg\" },\n];\n\nreturn stores.map(s => ({\n  json: {\n    store: s.store,\n    product,\n    query: `${product} site:${s.site}`\n  }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e899ff7e-84cf-43e8-93dc-03f035134f1c",
      "name": "AI Extraction Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -640,
        1344
      ],
      "parameters": {
        "text": "=User search intent:\n{{ $('Telegram Trigger').first().json.message.text }}\n\nScraped data (multiple sources):\n{{ $json.text }}\n",
        "options": {
          "systemMessage": "You are a product comparison extraction agent.\n\nYou will receive scraped and cleaned HTML text from multiple e-commerce pages\n(Amazon, Jumia, or others), along with their URLs and relevance scores.\n\nYour job is to:\n\n1. Identify the SAME product across different platforms.\n2. For EACH platform extract:\n   - Product name (clean, human readable)\n   - Price (number + currency if present)\n   - Platform name (Amazon, Jumia, etc.)\n   - URL\n3. Compare the platforms.\n4. Prepare a final user-friendly message suitable for Telegram.\n\nSTRICT RULES:\n- Do NOT invent prices or product names.\n- If price is missing, write \"Price not found\".\n- If multiple prices exist on the same page, choose the most prominent product price.\n- Ignore ads, bundles, delivery fees, unrelated products.\n- Use ONLY the provided text.\n- Be concise and clear.\n- Use emojis moderately (Telegram friendly).\n\nOUTPUT FORMAT:\nReturn ONLY a single formatted message string.\nNo JSON.\nNo explanations.\nNo markdown blocks.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "91e84561-d41a-464c-90eb-13174e459871",
      "name": "Google Search",
      "type": "@decodo/n8n-nodes-decodo.decodo",
      "position": [
        -1728,
        1344
      ],
      "parameters": {
        "query": "={{ $json.query }}",
        "headless": false,
        "operation": "google_search",
        "results_limit": 1
      },
      "typeVersion": 1
    },
    {
      "id": "57f34ec5-1b04-4c7d-aa33-9d3c398f2bec",
      "name": "Extract & Filter URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        1344
      ],
      "parameters": {
        "jsCode": "// Collect URLs from Decodo Google Search output\nconst allItems = $input.all();\n\nconst urls = [];\n\nfunction collectUrls(obj) {\n  if (!obj || typeof obj !== 'object') return;\n\n  // Collect URL fields\n  if (typeof obj.url === 'string' && obj.url.startsWith('http')) {\n    urls.push(obj.url);\n  }\n\n  // Recurse into arrays\n  if (Array.isArray(obj)) {\n    for (const item of obj) {\n      collectUrls(item);\n    }\n  }\n  // Recurse into objects\n  else {\n    for (const key of Object.keys(obj)) {\n      collectUrls(obj[key]);\n    }\n  }\n}\n\n// Walk through every Decodo item\nfor (const item of allItems) {\n  collectUrls(item.json);\n}\n\n// Deduplicate + remove Google search URLs\nconst uniqueUrls = [...new Set(urls)]\n  .filter(url => !url.includes('google.com/search'))\n  .slice(0, 20); // \u2705 LIMIT TO 20 ONLY\n\n// Return one item per URL\nreturn uniqueUrls.map(url => ({\n  json: { url }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "98f99c7f-c4a1-4599-8d55-43ff58dff4f0",
      "name": "Router (Amazon vs. Other)",
      "type": "n8n-nodes-base.if",
      "position": [
        -1344,
        1344
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c783147f-8282-43f6-9b12-8e2526f35be9",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.url }}",
              "rightValue": "amazon"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "b88b3738-780f-4d91-8071-06739ede596f",
      "name": "Scrape Amazon",
      "type": "@decodo/n8n-nodes-decodo.decodo",
      "position": [
        -1072,
        1008
      ],
      "parameters": {
        "url": "={{ $('Extract & Filter URLs').item.json.url }}",
        "parse": false,
        "operation": "amazon"
      },
      "typeVersion": 1
    },
    {
      "id": "719695c2-8b79-49bb-9637-43abece23440",
      "name": "Loop (Amazon)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1232,
        992
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "886ec5de-c344-490f-a8aa-70909b6b60d4",
      "name": "Clean HTML (Amazon)",
      "type": "n8n-nodes-base.code",
      "position": [
        -912,
        1008
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\n\nlet html = \"\";\n\ntry {\n  html = item.results[0].content || \"\";\n} catch (e) {\n  html = \"\";\n}\n\nlet cleaned = html\n  .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n  .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, \"\")\n  .replace(/<noscript[^>]*>[\\s\\S]*?<\\/noscript>/gi, \"\")\n  .replace(/<\\/?[^>]+>/g, \" \")\n  .replace(/&nbsp;/gi, \" \")\n  .replace(/&amp;/gi, \"&\")\n  .replace(/&quot;/gi, '\"')\n  .replace(/&#39;/gi, \"'\")\n  .replace(/&lt;/gi, \"<\")\n  .replace(/&gt;/gi, \">\")\n  .replace(/\\s+/g, \" \")\n  .trim();\n\nreturn {\n  json: {\n    text_clean: cleaned,\n    url: $('Loop (Amazon)').first().json.url\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "28e8d953-d8f5-4946-b2d5-a09a156cc63e",
      "name": "Scrape Other",
      "type": "@decodo/n8n-nodes-decodo.decodo",
      "position": [
        -1072,
        1728
      ],
      "parameters": {
        "url": "={{ $json.url }}"
      },
      "typeVersion": 1
    },
    {
      "id": "ca82c8e1-bd5e-4432-84d9-8bf30b45b098",
      "name": "Loop (Other)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1232,
        1712
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "08182aff-c250-4133-8dea-45dba46d1c68",
      "name": "Clean HTML (Other)",
      "type": "n8n-nodes-base.code",
      "position": [
        -896,
        1728
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\n\nlet html = \"\";\n\ntry {\n  html = item.results[0].content || \"\";\n} catch (e) {\n  html = \"\";\n}\n\nlet cleaned = html\n  .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, \"\")\n  .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, \"\")\n  .replace(/<noscript[^>]*>[\\s\\S]*?<\\/noscript>/gi, \"\")\n  .replace(/<\\/?[^>]+>/g, \" \")\n  .replace(/&nbsp;/gi, \" \")\n  .replace(/&amp;/gi, \"&\")\n  .replace(/&quot;/gi, '\"')\n  .replace(/&#39;/gi, \"'\")\n  .replace(/&lt;/gi, \"<\")\n  .replace(/&gt;/gi, \">\")\n  .replace(/\\s+/g, \" \")\n  .trim();\n\nreturn {\n  json: {\n    text_clean: cleaned,\n    url: $('Loop (Other)').first().json.url\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "95becc17-a85a-42b3-8c7f-41707045543d",
      "name": "Score Match (Other)",
      "type": "n8n-nodes-base.code",
      "position": [
        -736,
        1728
      ],
      "parameters": {
        "jsCode": "// 1\ufe0f\u20e3 User message (dynamic, from Telegram)\nconst userMessage =\n  $('Telegram Trigger').first().json.message.text\n    .toLowerCase()\n    .replace(/[^\\w\\s\\u0600-\\u06FF]/g, ' ');\n\n// 2\ufe0f\u20e3 Tokenize user message dynamically\nconst userTokens = userMessage\n  .split(/\\s+/)\n  .filter(w => w.length >= 3);\n\n// 3\ufe0f\u20e3 Cleaned page text\nconst pageText = ($json.text_clean || '')\n  .toLowerCase()\n  .replace(/[^\\w\\s\\u0600-\\u06FF]/g, ' ');\n\n// 4\ufe0f\u20e3 URL (pass-through from earlier nodes)\nconst url = $json.url || null;\n\n// Guards\nif (!pageText || userTokens.length === 0) {\n  return [{\n    json: {\n      match: false,\n      score: 0,\n      url\n    }\n  }];\n}\n\n// 5\ufe0f\u20e3 Dynamic scoring\nlet score = 0;\nlet matchedTokens = [];\n\nfor (const token of userTokens) {\n  const occurrences = pageText.split(token).length - 1;\n  if (occurrences > 0) {\n    score += Math.min(occurrences * 3, 15); // density-aware\n    matchedTokens.push(token);\n  }\n}\n\n// 6\ufe0f\u20e3 Structural bonuses (language-agnostic)\nif (pageText.length > 1500) score += 5;   // real product page\nif (pageText.length < 400) score -= 10;   // weak page\nif (\n  pageText.includes('add to cart') ||\n  pageText.includes('\u0627\u0636\u0641') ||\n  pageText.includes('\u0623\u0636\u0641')\n) score += 5;\n\n// 7\ufe0f\u20e3 Dynamic threshold (based on user input length)\nconst threshold = Math.max(12, userTokens.length * 4);\nconst isMatch = score >= threshold;\n\n// 8\ufe0f\u20e3 Output EVERYTHING needed downstream\nreturn [{\n  json: {\n    match: isMatch,\n    score,\n    threshold,\n    url,\n    matched_tokens: matchedTokens,\n    text_clean: pageText\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "502a928b-2091-4b63-9716-651337efa735",
      "name": "Score Match (Amazon)",
      "type": "n8n-nodes-base.code",
      "position": [
        -736,
        1008
      ],
      "parameters": {
        "jsCode": "// 1\ufe0f\u20e3 User message (dynamic, from Telegram)\nconst userMessage =\n  $('Telegram Trigger').first().json.message.text\n    .toLowerCase()\n    .replace(/[^\\w\\s\\u0600-\\u06FF]/g, ' ');\n\n// 2\ufe0f\u20e3 Tokenize user message dynamically\nconst userTokens = userMessage\n  .split(/\\s+/)\n  .filter(w => w.length >= 3);\n\n// 3\ufe0f\u20e3 Cleaned page text\nconst pageText = ($json.text_clean || '')\n  .toLowerCase()\n  .replace(/[^\\w\\s\\u0600-\\u06FF]/g, ' ');\n\n// 4\ufe0f\u20e3 URL (pass-through from earlier nodes)\nconst url = $json.url || null;\n\n// Guards\nif (!pageText || userTokens.length === 0) {\n  return [{\n    json: {\n      match: false,\n      score: 0,\n      url\n    }\n  }];\n}\n\n// 5\ufe0f\u20e3 Dynamic scoring\nlet score = 0;\nlet matchedTokens = [];\n\nfor (const token of userTokens) {\n  const occurrences = pageText.split(token).length - 1;\n  if (occurrences > 0) {\n    score += Math.min(occurrences * 3, 15); // density-aware\n    matchedTokens.push(token);\n  }\n}\n\n// 6\ufe0f\u20e3 Structural bonuses (language-agnostic)\nif (pageText.length > 1500) score += 5;   // real product page\nif (pageText.length < 400) score -= 10;   // weak page\nif (\n  pageText.includes('add to cart') ||\n  pageText.includes('\u0627\u0636\u0641') ||\n  pageText.includes('\u0623\u0636\u0641')\n) score += 5;\n\n// 7\ufe0f\u20e3 Dynamic threshold (based on user input length)\nconst threshold = Math.max(12, userTokens.length * 4);\nconst isMatch = score >= threshold;\n\n// 8\ufe0f\u20e3 Output EVERYTHING needed downstream\nreturn [{\n  json: {\n    match: isMatch,\n    score,\n    threshold,\n    url,\n    matched_tokens: matchedTokens,\n    text_clean: pageText\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d373cd04-5d7b-42fb-bb8e-bff1e13bf9e8",
      "name": "Pick Best (Amazon)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1120,
        1280
      ],
      "parameters": {
        "jsCode": "const items = $input.all().map(i => i.json);\n\n// Keep only valid matches\nconst valid = items.filter(i => i.match === true);\n\n// If none matched, return nothing\nif (!valid.length) return [];\n\n// Pick highest score\nvalid.sort((a, b) => b.score - a.score);\n\nreturn [{\n  json: valid[0]\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dd39e9a7-5491-487a-a4d2-9bfb636f645b",
      "name": "Pick Best (Other)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1120,
        1424
      ],
      "parameters": {
        "jsCode": "const items = $input.all().map(i => i.json);\n\n// Keep only valid matches\nconst valid = items.filter(i => i.match === true);\n\n// If none matched, return nothing\nif (!valid.length) return [];\n\n// Pick highest score\nvalid.sort((a, b) => b.score - a.score);\n\nreturn [{\n  json: valid[0]\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d8c9cb70-d67b-401c-8191-474efc31eaac",
      "name": "Merge Results",
      "type": "n8n-nodes-base.merge",
      "position": [
        -944,
        1344
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "c863240d-717b-4557-9aa9-cdbe7944c45c",
      "name": "GPT Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -640,
        1504
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "a3b4c774-7d06-4188-9503-8735319cbae8",
      "name": "Format Context",
      "type": "n8n-nodes-base.code",
      "position": [
        -800,
        1344
      ],
      "parameters": {
        "jsCode": "const items = $input.all().map(i => i.json);\n\nconst combinedText = items.map(item => `\nSOURCE URL:\n${item.url}\n\nRELEVANCE SCORE:\n${item.score}\n\nCONTENT:\n${item.text_clean.slice(0, 5000)}\n`).join('\\n\\n====================\\n\\n');\n\nreturn [{\n  json: {\n    text: combinedText   // \ud83d\udd34 THIS NAME IS CRITICAL\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fb221ed8-fa1c-4483-b9d2-38289e5a21df",
      "name": "Send Reply",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -336,
        1344
      ],
      "parameters": {
        "text": "={{ $json.output }}",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "9d7a4e7f-c857-4178-a66d-490c22015ce6",
  "connections": {
    "GPT Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Extraction Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Loop (Other)": {
      "main": [
        [
          {
            "node": "Pick Best (Other)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape Other",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Other": {
      "main": [
        [
          {
            "node": "Clean HTML (Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Queries": {
      "main": [
        [
          {
            "node": "Google Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Search": {
      "main": [
        [
          {
            "node": "Extract & Filter URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop (Amazon)": {
      "main": [
        [
          {
            "node": "Pick Best (Amazon)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape Amazon",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Results": {
      "main": [
        [
          {
            "node": "Format Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Amazon": {
      "main": [
        [
          {
            "node": "Clean HTML (Amazon)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Context": {
      "main": [
        [
          {
            "node": "AI Extraction Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Build Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Best (Other)": {
      "main": [
        [
          {
            "node": "Merge Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Clean HTML (Other)": {
      "main": [
        [
          {
            "node": "Score Match (Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Best (Amazon)": {
      "main": [
        [
          {
            "node": "Merge Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Extraction Agent": {
      "main": [
        [
          {
            "node": "Send Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean HTML (Amazon)": {
      "main": [
        [
          {
            "node": "Score Match (Amazon)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Match (Other)": {
      "main": [
        [
          {
            "node": "Loop (Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Match (Amazon)": {
      "main": [
        [
          {
            "node": "Loop (Amazon)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract & Filter URLs": {
      "main": [
        [
          {
            "node": "Router (Amazon vs. Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Router (Amazon vs. Other)": {
      "main": [
        [
          {
            "node": "Loop (Amazon)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop (Other)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}