{
  "id": "COew5sp75oHL2OGK",
  "name": "Crypto/Stocks News Watcher",
  "tags": [
    {
      "id": "BdxHwMUMr2WXfcxw",
      "name": "twutter",
      "createdAt": "2025-11-21T10:35:33.738Z",
      "updatedAt": "2025-11-21T10:35:33.738Z"
    },
    {
      "id": "DhyDfEdgkMYu0hEj",
      "name": "news",
      "createdAt": "2025-11-21T10:35:27.836Z",
      "updatedAt": "2025-11-21T10:35:27.836Z"
    },
    {
      "id": "PXrNe4uaUViL0EiW",
      "name": "crypto",
      "createdAt": "2025-11-21T10:35:19.757Z",
      "updatedAt": "2025-11-21T10:35:19.757Z"
    },
    {
      "id": "ho8Enj73YkWHSqa0",
      "name": "feed",
      "createdAt": "2025-11-21T10:35:35.200Z",
      "updatedAt": "2025-11-21T10:35:35.200Z"
    },
    {
      "id": "lRkv4ieCJFkbaiCU",
      "name": "x",
      "createdAt": "2025-11-21T10:35:31.050Z",
      "updatedAt": "2025-11-21T10:35:31.050Z"
    },
    {
      "id": "ovWAcv5DtL414Mhj",
      "name": "stocks",
      "createdAt": "2025-11-21T10:35:24.929Z",
      "updatedAt": "2025-11-21T10:35:24.929Z"
    },
    {
      "id": "yb7rVVJyQN19GgKt",
      "name": "rss",
      "createdAt": "2025-11-21T10:35:29.640Z",
      "updatedAt": "2025-11-21T10:35:29.640Z"
    }
  ],
  "nodes": [
    {
      "id": "3605441c-09a7-408d-ae58-952fca2fbddf",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1712,
        -32
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "95f9c5dd-bed8-4ae7-afd0-f52735d9a721",
      "name": "RSS Read - Coindesk",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -416,
        -256
      ],
      "parameters": {
        "url": "https://www.coindesk.com/arc/outboundfeeds/rss/",
        "options": {}
      },
      "executeOnce": true,
      "typeVersion": 1.2
    },
    {
      "id": "94b0094a-4e1d-4e02-bac6-d01b35fa3a89",
      "name": "RSS Read - Google news",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -416,
        -80
      ],
      "parameters": {
        "url": "={{$json.url}}",
        "options": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2
    },
    {
      "id": "56b70dc1-cef6-4dea-9ea3-e974a33bd5f1",
      "name": "RSS Read - X Posts",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -32,
        64
      ],
      "parameters": {
        "url": "={{ $json.rssUrl }}",
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "1733a9d9-08ac-46bb-ab9b-9b7987320d51",
      "name": "Source set - Coindesk",
      "type": "n8n-nodes-base.set",
      "position": [
        1024,
        -256
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "04f3aa10-f74d-48e6-bd8c-e63c017c5571",
              "name": "source",
              "type": "string",
              "value": "CoinDesk"
            },
            {
              "id": "39bd311b-dfa3-478f-beca-9e49012d9ffa",
              "name": "kind",
              "type": "string",
              "value": "Article / News"
            },
            {
              "id": "c22914b9-46fd-4de6-9f71-4b59a4fe93bc",
              "name": "topic",
              "type": "string",
              "value": "={{$json.topic}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d535536a-a590-405e-b806-e14617d158bf",
      "name": "Source set - Google news",
      "type": "n8n-nodes-base.set",
      "position": [
        1024,
        -80
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "04f3aa10-f74d-48e6-bd8c-e63c017c5571",
              "name": "source",
              "type": "string",
              "value": "Google News"
            },
            {
              "id": "39bd311b-dfa3-478f-beca-9e49012d9ffa",
              "name": "kind",
              "type": "string",
              "value": "News"
            },
            {
              "id": "55446164-5ddd-442c-be8d-5296569b2388",
              "name": "topic",
              "type": "string",
              "value": "={{$json.topic}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "0dcafdf0-9e39-4409-9c5d-54cae63010c1",
      "name": "Source set - Cointelegraph",
      "type": "n8n-nodes-base.set",
      "position": [
        1024,
        432
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "04f3aa10-f74d-48e6-bd8c-e63c017c5571",
              "name": "source",
              "type": "string",
              "value": "CoinTelegraph"
            },
            {
              "id": "39bd311b-dfa3-478f-beca-9e49012d9ffa",
              "name": "kind",
              "type": "string",
              "value": "Article / News"
            },
            {
              "id": "e6e7b215-75a3-4e01-9f27-59b8c833a881",
              "name": "topic",
              "type": "string",
              "value": "={{$json.topic}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "e113b336-a53f-4054-94e7-6a1f86ea8e44",
      "name": "Source set - X Posts",
      "type": "n8n-nodes-base.set",
      "position": [
        1024,
        240
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "04f3aa10-f74d-48e6-bd8c-e63c017c5571",
              "name": "source",
              "type": "string",
              "value": "X"
            },
            {
              "id": "39bd311b-dfa3-478f-beca-9e49012d9ffa",
              "name": "kind",
              "type": "string",
              "value": "Tweet"
            },
            {
              "id": "b3208846-d56e-4b3b-9f92-a0389d4fab77",
              "name": "topic",
              "type": "string",
              "value": "={{$json.topic}}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "8d5a498d-3c2a-40e8-ba90-456f400b382e",
      "name": "Merge - Coindesk + Google news",
      "type": "n8n-nodes-base.merge",
      "position": [
        1280,
        -176
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "f711a07b-0bbe-4102-81f8-d11736ed796f",
      "name": "Merge - X posts + CoinTelegraph",
      "type": "n8n-nodes-base.merge",
      "position": [
        1280,
        336
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "47e8fd90-a623-42a9-9637-55d9f262eceb",
      "name": "Merge - Merge previous two merges",
      "type": "n8n-nodes-base.merge",
      "position": [
        1568,
        64
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "9a05a06c-e0e3-4c48-adc7-ff6c8f869cd1",
      "name": "Code - Keywords Filter",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        64
      ],
      "parameters": {
        "jsCode": "// ---------------- Config (topic-aware) ----------------\n\n// Tje Keywords filtering - Replace keywords as you want.\nconst KEEP_CRYPTO = [\n  'crypto','cryptocurrency','bitcoin','btc','ethereum','eth','market','markets','signals',\n  'selloff','sell-off','dump','dumped','liquidation','flash crash','$Grass','$grass','grass token',\n  'bullish','bearish','breakout','btcusd','etf','spot etf','hack','hacked','hacking','exploit','rug pull','rug',\n];\n\nconst KEEP_STOCKS = [\n  'stock','stocks','equities','market','markets','earnings','guidance','revenue','profit','loss',\n  'ipo','dividend','split','buyback','upgrade','downgrade','rating','price target','outlook','fomc',\n  'dow','nasdaq','s&p','spy','qqq',\n  // common tickers / names\n  'nvda','nvidia','aapl','apple','tsla','tesla','msft','microsoft','amzn','amazon','meta','facebook','goog','googl','alphabet',\n];\n\nconst DROP = ['giveaway','airdrop','referral','signal group','win $','win$'];\nconst MAX_IMAGES = 4;\n\n// set true only while debugging to clear dedupe memory for this run\nconst CLEAR_MEMORY_THIS_RUN = false;\n\n// ---------------- Helpers ----------------\nconst wf = $getWorkflowStaticData('global');\nif (CLEAR_MEMORY_THIS_RUN) wf.seen = [];\nconst seen = new Set(wf.seen || []);\n\nconst esc = (s) => s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst buildKeepRE = (arr) =>\n  arr.map((k) => {\n    const pat = esc(k).replace(/\\\\\\s+/g, '\\\\s+');     // \"spot etf\" -> spot\\s+etf\n    return [k, new RegExp(`(^|[^\\\\w])${pat}([^\\\\w]|$)`, 'i')];\n  });\n\nconst KEEP_RE_CRYPTO = buildKeepRE(KEEP_CRYPTO);\nconst KEEP_RE_STOCKS = buildKeepRE(KEEP_STOCKS);\nconst DROP_RE = DROP.map((k) => {\n  const pat = esc(k).replace(/\\\\\\s+/g, '\\\\s+');\n  return new RegExp(`(^|[^\\\\w])${pat}([^\\\\w]|$)`, 'i');\n});\n\nconst normSource = (j, url) => {\n  if (j.source) return j.source;\n  const u = (url || '').toLowerCase();\n  if (u.includes('coindesk')) return 'CoinDesk';\n  if (u.includes('cointelegraph')) return 'CoinTelegraph';\n  if (u.includes('news.google')) return 'Google News';\n  if (u.includes('xcancel') || u.includes('/status/')) return 'X posts';\n  return 'rss';\n};\n\nconst kindOf = (j, url) => {\n  const src = (j.source || '').toLowerCase();\n  const u = (url || '').toLowerCase();\n  if (src.includes('x') || src.includes('tweet') || u.includes('/status/')) return 'tweet';\n  return 'article';\n};\n\nconst extractImages = (html = '', enclosure) => {\n  const out = [];\n  if (enclosure?.url) out.push(enclosure.url);\n  const re = /<img[^>]+src=[\"']([^\"']+)[\"']/ig;\n  let m;\n  while ((m = re.exec(html)) && out.length < MAX_IMAGES) out.push(m[1]);\n  return [...new Set(out)];\n};\n\nconst canonicalFromGoogleNews = (item) => {\n  const m = /<a[^>]+href=[\"']([^\"']+)[\"']/i.exec(item.content || '');\n  return m ? m[1] : (item.link || item.guid || '');\n};\n\nconst canonicalUrl = (j) => {\n  let u = j.link || j.guid || j.url || '';\n  if ((u || '').includes('news.google.com')) u = canonicalFromGoogleNews(j);\n  return u.replace(/^https?:\\/\\/(www\\.)?/i, '').replace(/#.*$/,'').trim();\n};\n\nconst textOf = (j) => {\n  const raw = [j.title, j.contentSnippet, j.content].filter(Boolean).join(' ');\n  return raw.replace(/[\\s\\u00A0]+/g, ' ').replace(/[#$]/g, ' ').trim();\n};\n\nconst matchedKeywords = (txt, keepList) => {\n  const hits = [];\n  for (const [label, re] of keepList) if (re.test(txt)) hits.push(label);\n  return [...new Set(hits)];\n};\n\nconst isDropped = (txt) => DROP_RE.some((re) => re.test(txt));\n\n// --------- NEW: derive run-level topic & repair each item ----------\nconst fromInit = $item(0)?.$node?.[\"Init RunConfig\"]?.json?.topic;\nconst RUN_TOPIC = (() => {\n  const raw = String($json.topic ?? fromInit ?? 'crypto').toLowerCase().trim();\n  if (raw === 'stocks' || raw === 'stock') return 'stocks';\n  return 'crypto';\n})();\n\n// make sure every incoming item has a good topic (fixes \"=\" or missing)\nconst input = $input.all().map(i => {\n  const t = String(i.json.topic ?? '').toLowerCase().trim();\n  const fixedTopic = t && t !== '=' ? t : RUN_TOPIC;\n  return { ...i.json, topic: fixedTopic };\n});\n\n// ---------------- Main ----------------\nconst counts = { in: input.length, kept: 0, dropped: 0, perSource: {} };\nconst out = [];\n\nfor (const j of input) {\n  const topic = String(j.topic || RUN_TOPIC).toLowerCase();\n  const KEEP_RE = topic === 'stocks' ? KEEP_RE_STOCKS : KEEP_RE_CRYPTO;\n\n  const urlRaw = j.link || j.guid || j.url || '';\n  const url = (urlRaw || '').trim();\n  const title = (j.title || '').trim();\n  if (!url || !title) { counts.dropped++; continue; }\n\n  const text = textOf(j);\n  if (!text) { counts.dropped++; continue; }\n\n  if (isDropped(text)) { counts.dropped++; continue; }\n\n  const hits = matchedKeywords(text, KEEP_RE);\n  if (!hits.length) { counts.dropped++; continue; }\n\n  const keyOnly = canonicalUrl(j);\n  if (!keyOnly) { counts.dropped++; continue; }\n\n  // Deduplicate across runs, per topic\n  const key = `${topic}:${keyOnly}`;\n  if (seen.has(key)) { counts.dropped++; continue; }\n  seen.add(key);\n\n  const when = new Date(j.isoDate || j.pubDate || j.publishedAt || Date.now());\n\n  const src = normSource(j, url);\n  counts.perSource[src] = (counts.perSource[src] || 0) + 1;\n\n  out.push({\n    json: {\n      id: url,\n      source: src,\n      kind: kindOf(j, url),\n      title,\n      url,\n      publishedAt: when.toISOString(),\n      matchedKeywords: hits.join(','),    // same field as before\n      summary: (j.contentSnippet || '').replace(/\\s+/g, ' ').trim(),\n      html: j.content || '',\n      media: extractImages(j.content, j.enclosure),\n      topic,                               // ensured & sanitized\n    },\n  });\n}\n\ncounts.kept = out.length;\nconsole.log({ stats: counts });\n\n// keep some memory (cap length)\nwf.seen = Array.from(seen).slice(-500);\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "54cf0cbf-2d00-47d7-bc97-c98daef776b7",
      "name": "Code - Array bind",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        64
      ],
      "parameters": {
        "jsCode": "const items = $input.all().map(i => i.json);\nconst topic = String(\n  items[0]?.topic ?? $item(0).$node[\"Init RunConfig\"].json.topic ?? 'crypto'\n).toLowerCase();\n\nreturn [{ json: { topic, items } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a44d59a2-eedf-4d90-a6f2-a15a209d443f",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1712,
        144
      ],
      "parameters": {
        "path": "run-workflow",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2.1
    },
    {
      "id": "9382dfc1-2dfb-4244-83bd-2b9cc9a3dd8c",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        -1504,
        48
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "60c11872-6e80-4023-9d18-e17a7da4f230",
      "name": "Init RunConfig",
      "type": "n8n-nodes-base.code",
      "position": [
        -1328,
        48
      ],
      "parameters": {
        "jsCode": "// Accept both schedule and webhook paths\nconst body = ($json.body ?? $json) || {}\n\nconst topic = body.topic ?? $json.topic ?? 'crypto'\nlet platforms = body.platforms\n\nif (!Array.isArray(platforms) || !platforms.length) {\n  platforms = ['coindesk', 'google', 'cointelegraph', 'x']\n}\n\nplatforms = platforms.map((s) => String(s).toLowerCase().trim())\n\n// simple query presets per topic\n//this is where you set your queries, replace to whatever your query is\n//XCancel does not provide a very long RSS Feed, so make sure not to include long set of queries OR Find a better alternative RSS provider that allows this.\nconst queries = topic === 'crypto' \n  ? {\n      google: '(bitcoin OR ethereum OR crypto OR blockchain)',\n      x: '(bitcoin OR crypto OR BTC OR ETH OR solana OR SOL)'\n    }\n  : {\n      google: '(stock OR stocks OR equities OR earnings OR market)',\n      x: '(stocks OR stock OR SPY OR QQQ OR NVDA OR AAPL OR TSLA OR MSFT)'\n    }\n\nreturn [{ json: { topic, platforms, queries } }]\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6713c172-8117-43be-8bdb-66e5ae755a0c",
      "name": "IF Gate - Coindesk",
      "type": "n8n-nodes-base.if",
      "position": [
        -848,
        -240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7e97ad2e-60ef-4798-851c-e8e082905444",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Array.isArray($json.platforms) && $json.topic === 'crypto' && $json.platforms.includes('coindesk') }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "2d94beb0-fc90-4f12-8ffa-8b1ec7565429",
      "name": "IF Gate - Google news",
      "type": "n8n-nodes-base.if",
      "position": [
        -848,
        -64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7e97ad2e-60ef-4798-851c-e8e082905444",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Array.isArray($json.platforms) && $json.platforms.includes('google') }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "a2378244-cd10-4848-a0c4-b7526ecd5a31",
      "name": "IF Gate - CoinTelegraph",
      "type": "n8n-nodes-base.if",
      "position": [
        -848,
        448
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7e97ad2e-60ef-4798-851c-e8e082905444",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Array.isArray($json.platforms) && $json.topic === 'crypto' && $json.platforms.includes('cointelegraph') }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "de6fe9fb-f1a0-4119-b0bf-189d45e3ff98",
      "name": "IF Gate - X",
      "type": "n8n-nodes-base.if",
      "position": [
        -848,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7e97ad2e-60ef-4798-851c-e8e082905444",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Array.isArray($json.platforms) && $json.platforms.includes('x') }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "1615f08c-e635-47f1-893e-10c7ff7fe3b7",
      "name": "X Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -320,
        144
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "e0000def-80a6-448e-b416-afe90ba8eb55",
      "name": "IF - More X batches?",
      "type": "n8n-nodes-base.if",
      "position": [
        512,
        224
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8adddbbc-6b29-4cd6-a1c5-12b37e171b43",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "=={{$json.hasMore}}",
              "rightValue": "=={{$json.batchCount - 1}}"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "eae176c5-2d96-4a78-86eb-9868cbc47488",
      "name": "Code - Finalize X batches (emit combined)",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        240
      ],
      "parameters": {
        "jsCode": "// Finalize X batches (emit combined)\nconst mem = $getWorkflowStaticData('global')\nconst all = Array.isArray(mem.x_items) ? mem.x_items : []\n// Emit as normal n8n items:\nreturn all.map(j => ({ json: j }))\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f835fc48-9583-4ba8-9d92-0a7e91574586",
      "name": "Code - Accumulate X items",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        64
      ],
      "parameters": {
        "jsCode": "const mem = $getWorkflowStaticData('global');\n\n// all RSS items produced for this batch\nconst batch = $input.all().map(i => i.json);\n\n// append to the accumulator\nmem.x_items = (mem.x_items ?? []).concat(batch);\n\n// read the metadata written by \u201cSet batch metadata\u201d\nconst batchIndex  = Number(mem.batchIndex ?? 0);\nconst batchCount  = Number(mem.batchCount ?? 0);\nconst hasMore     = batchIndex < (batchCount - 1);\n\n// emit a single control item that drives the IF node\nreturn [{\n  json: {\n    added: batch.length,\n    total: mem.x_items.length,\n    batchIndex,\n    batchCount,\n    hasMore,\n  },\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "48e24f9b-7f97-42f9-88ff-2eaafaead490",
      "name": "Code - Reset X accumulator",
      "type": "n8n-nodes-base.code",
      "position": [
        -496,
        144
      ],
      "parameters": {
        "jsCode": "// Start a fresh buffer for this execution.\nconst mem = $getWorkflowStaticData('global')  // \u2705 valid contexts: 'global' | 'node'\nmem.x_items = []          // where we'll collect all RSS items from each batch\nmem.x_topic = $json.topic // optional: remember topic ('crypto' | 'stocks') from builder\n\nreturn $input.all()       // pass through\n"
      },
      "retryOnFail": true,
      "typeVersion": 2
    },
    {
      "id": "b306e15c-b6d8-4116-ae30-a5d3140d64b7",
      "name": "Code - URL Build - XCancel",
      "type": "n8n-nodes-base.code",
      "position": [
        -640,
        144
      ],
      "parameters": {
        "jsCode": "// Build Xcancel URLs (batched, short queries)\n// topic comes from Init RunConfig (crypto | stocks)\n// optional: $json.tickers = [\"NVDA\",\"AAPL\",...] when topic === 'stocks'\n\nconst topic = String($json.topic ?? 'crypto').toLowerCase();\n\n// ----- Tunables you can tweak -----\nconst MAX_TICKERS_PER = 4;      // <= keep small\nconst MAX_NEWS_PER    = 3;      // <= keep small\nconst MAX_PER_BATCH_CRYPTO = 6; // crypto keywords per query\nconst MIN_FAVES = 20;\nconst MIN_RTS   = 5;\n// Optional length guard for extra safety (after quoting, before encoding)\nconst MAX_QUERY_CHARS = 420;\n// ----------------------------------\n\nconst filters =\n  `lang:en -is:retweet -is:reply -is:quote filter:links min_faves:${MIN_FAVES} min_retweets:${MIN_RTS}`;\n\nconst toOR = (terms) =>\n  '(' +\n  terms\n    .filter(Boolean)\n    // quote if it has a space, a $ (cashtag), or any non-word char (e.g. M&A, S&P)\n    .map((t) => (/[^\\w]/.test(t) ? `\"${t}\"` : t))\n    .join(' OR ') +\n  ')';\n\nconst toUrl = (q) =>\n  'https://xcancel.com/search/rss?f=tweets&q=' +\n  encodeURIComponent(q.replace(/\\s+/g, ' ').trim());\n\nconst chunk = (arr, size) => {\n  const out = [];\n  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));\n  return out;\n};\n\nlet batches = [];\n\nif (topic === 'stocks') {\n  // ----- STOCKS -----\n  const tickers = Array.isArray($json.tickers) && $json.tickers.length\n    ? $json.tickers\n    : [\n        'AAPL','MSFT','NVDA','AMZN','GOOGL','META','TSLA','AMD','AVGO',\n        'NFLX','TSM','JPM','BAC','WMT','ORCL',\n      ];\n  const cashtags = tickers.map((t) => `$${String(t).toUpperCase()}`);\n\n  const news = [\n    'earnings','EPS','guidance','revenue','outlook','forecast',\n    'upgrade','downgrade','price target','PT',\n    'dividend','buyback','merger','acquisition','M&A','IPO',\n    'halt','after hours','pre-market',\n  ];\n\n  const cashtagGroups = chunk(cashtags, MAX_TICKERS_PER);\n  const newsGroups    = chunk(news,    MAX_NEWS_PER);\n\n  // Pair small cashtag groups with small news groups: (tickers) AND (news)\n  // This keeps each query short and highly relevant.\n  for (const tg of cashtagGroups) {\n    for (const ng of newsGroups) {\n      let q = `${toOR(tg)} ${toOR(ng)} ${filters}`;\n      // Safety: if still long, split news group to singles\n      if (q.length > MAX_QUERY_CHARS) {\n        for (const single of ng) {\n          const q2 = `${toOR(tg)} ${toOR([single])} ${filters}`;\n          batches.push({ rssUrl: toUrl(q2), queryReadable: q2 });\n        }\n        continue;\n      }\n      batches.push({ rssUrl: toUrl(q), queryReadable: q });\n    }\n  }\n\n  // (Optional) add a very small set of pure-market queries (no cashtag) if you like:\n  // for (const ng of newsGroups) {\n  //   const q = `${toOR(ng)} (${['stocks','equities','market'].join(' OR ')}) ${filters}`;\n  //   batches.push({ rssUrl: toUrl(q), queryReadable: q });\n  // }\n} else {\n  // ----- CRYPTO -----\n  const cryptoTerms = [\n    'bitcoin','btc','spot ETF','flash crash','liquidation','btcusd','breakout',\n    'ethereum','eth','etf','rug pull','hack','hacked','exploit','bullish','bearish',\n    'Grass token','$Grass',\n  ];\n\n  for (const group of chunk(cryptoTerms, MAX_PER_BATCH_CRYPTO)) {\n    const q = `${toOR(group)} ${filters}`;\n    batches.push({ rssUrl: toUrl(q), queryReadable: q });\n  }\n}\n\n// annotate with batch indexes\nbatches = batches.map((b, i) => ({\n  ...b,\n  batchIndex: i,\n  batchCount: batches.length,\n  topic,\n}));\n\nreturn batches.map((b) => ({ json: b }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "055c48be-7df5-4338-b847-57af53707580",
      "name": "Code - Tag topic - Google news",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        -80
      ],
      "parameters": {
        "jsCode": "// Code - Tag topic (place right after RSS Read)\nconst topic = String(\n  $json.topic                                   // if present\n  ?? $item(0).$node[\"Init RunConfig\"].json.topic // from the config node\n  ?? 'crypto'\n).toLowerCase();\n\nreturn $input.all().map(i => ({ json: { ...i.json, topic } }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0ef11d59-ad03-4194-9d51-bb94212ccd2f",
      "name": "Code - Tag topic - Coin desk",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        -256
      ],
      "parameters": {
        "jsCode": "// Code - Tag topic (place right after RSS Read)\nconst topic = String(\n  $json.topic                                   // if present\n  ?? $item(0).$node[\"Init RunConfig\"].json.topic // from the config node\n  ?? 'crypto'\n).toLowerCase();\n\nreturn $input.all().map(i => ({ json: { ...i.json, topic } }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7c3880d2-e46d-47ff-bf60-fd56496aa3e6",
      "name": "Code - Tag topic - X",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        64
      ],
      "parameters": {
        "jsCode": "// Code - Tag topic (place right after RSS Read)\nconst topic = String(\n  $json.topic                                   // if present\n  ?? $item(0).$node[\"Init RunConfig\"].json.topic // from the config node\n  ?? 'crypto'\n).toLowerCase();\n\nreturn $input.all().map(i => ({ json: { ...i.json, topic } }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f67bece3-b9a2-4337-b5dd-41b922822d4d",
      "name": "Code - URL build - Google news",
      "type": "n8n-nodes-base.code",
      "position": [
        -640,
        -80
      ],
      "parameters": {
        "jsCode": "const q = $json.queries?.google ?? '(bitcoin OR crypto)'\nconst url = `https://news.google.com/rss/search?q=${encodeURIComponent(q)}&hl=en-US&gl=US&ceid=US:en`\nreturn [{ json: { ...$json, url } }]\n"
      },
      "typeVersion": 2
    },
    {
      "id": "628c02c2-de72-48a9-8510-8f4a2efee6c8",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        720
      ],
      "parameters": {
        "color": 6,
        "width": 848,
        "height": 928,
        "content": "## README\n\nREADME \u2013 Crypto/Stocks News \u2192 UI workflow\n\n### What this does\n- Collects crypto or stock-market news from CoinDesk, CoinTelegraph, Google News, and X (via xcancel.com RSS).\n- Tags each item with topic (crypto or stocks).\n- Filters & deduplicates items in \u201cCode - Keywords Filter\u201d.\n- Bundles everything in \u201cCode - Array bind\u201d as:\n```\n{ topic, items: [ { id, source, kind, title, url, publishedAt, matchedKeywords, summary, html, media[], topic } ] }\n```\n- Sends the items to your backend via \u201cHTTP Request - Send to localhost\u201d.\n\n### How to use\n- Use either \u201cSchedule Trigger\u201d (interval runs) or \u201cWebhook\u201d (run-workflow) as entry.\n- Open \u201cInit RunConfig\u201d to set default topic (crypto | stocks), platforms (coindesk / google / cointelegraph / x) and optional tickers.\n- Update \u201cHTTP Request - Send to localhost\u201d:\n- Change the URL from http://localhost:3000/api/hooks/news to your own API endpoint.\n- Either set a real x-webhook-secret header (and verify it in your backend) or remove that header completely.\n\nIf you call the Webhook node, also fix header auth: create an HTTP Header Auth credential for x-webhook-secret, or switch the node\u2019s auth to \u201cNone\u201d for local tests.\n\n- To change what gets through, edit the keyword & spam lists inside \u201cCode - Keywords Filter\u201d.\n\n### Original purpose\n\nThis workflow was built to transfer curated news data into a custom UI.\n\nYou can instead connect anything after \u201cCode - Array bind\u201d (DB, Slack/Telegram, email, etc.) and reuse the { topic, items } payload for your own use case."
      },
      "typeVersion": 1
    },
    {
      "id": "3757d187-20b8-4d1b-b05e-89275643edc1",
      "name": "HTTP Request - Send to your backend",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2288,
        64
      ],
      "parameters": {
        "url": "https://your-backend.example.com/api/hooks/news",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { items: $json.items } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "x-webhook-secret",
              "value": "Your Secret Here"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "bedf7a10-89f0-4189-a814-9f84b82fd27a",
      "name": "Loop back \u2013 X Batches",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -32,
        208
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "0824dd31-e98f-4979-9139-208b8cfff5a4",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -880,
        720
      ],
      "parameters": {
        "width": 416,
        "height": 256,
        "content": "## CONFIG\n\nEdit these places only for 90% of use cases:\n```\n1. Init RunConfig\n2.URL build nodes \n3. Keywords Filter lists\n4. HTTP Request URL/headers.\n``` "
      },
      "typeVersion": 1
    },
    {
      "id": "97602b31-2d25-488b-b813-2ee4e0f2a0fe",
      "name": "Code - Tag topic - Coin telegraph",
      "type": "n8n-nodes-base.code",
      "position": [
        128,
        432
      ],
      "parameters": {
        "jsCode": "// Code - Tag topic (place right after RSS Read)\nconst topic = String(\n  $json.topic                                   // if present\n  ?? $item(0).$node[\"Init RunConfig\"].json.topic // from the config node\n  ?? 'crypto'\n).toLowerCase();\n\nreturn $input.all().map(i => ({ json: { ...i.json, topic } }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "03e932ee-bb95-48ca-b92f-ce4837fcce42",
      "name": "RSS Read - CoinTelegraph",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -400,
        432
      ],
      "parameters": {
        "url": "https://cointelegraph.com/rss",
        "options": {}
      },
      "retryOnFail": true,
      "typeVersion": 1.2
    },
    {
      "id": "9fe664e3-9624-4c15-a667-b8acacdd5f2e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1776,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 496,
        "content": "## Section 1 \u2013 Triggers & Run Config\nThis section controls how the workflow starts and what it should fetch."
      },
      "typeVersion": 1
    },
    {
      "id": "d42831f4-64bd-4f56-a447-a8c0949f0831",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        -336
      ],
      "parameters": {
        "color": 7,
        "width": 1936,
        "height": 1008,
        "content": "## Section 2 \u2013 Fetch & Tag News from Sources\nThis section decides which sources to use and pulls news from each of them."
      },
      "typeVersion": 1
    },
    {
      "id": "8993380b-a412-467c-8b68-b3f930e3891c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        -336
      ],
      "parameters": {
        "color": 7,
        "width": 784,
        "height": 1008,
        "content": "## Section 3 - Merge and normalize All Items\nThis section normalizes metadata and merges all sources into one unified stream."
      },
      "typeVersion": 1
    },
    {
      "id": "17142132-b6c7-4e08-a17f-3739e1eb355e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1744,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 784,
        "height": 400,
        "content": "## Section 4 \u2013 Filter, Deduplicate & Build Payload \u2192 Send to Backend\nThis section prepares the final dataset and sends it to your app or UI."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "12b49360-c555-4ae4-be0c-5305dfa19ee3",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Init RunConfig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "X Batches": {
      "main": [
        [
          {
            "node": "RSS Read - X Posts",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop back \u2013 X Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Gate - X": {
      "main": [
        [
          {
            "node": "Code - URL Build - XCancel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Init RunConfig": {
      "main": [
        [
          {
            "node": "IF Gate - Coindesk",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF Gate - Google news",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF Gate - CoinTelegraph",
            "type": "main",
            "index": 0
          },
          {
            "node": "IF Gate - X",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Array bind": {
      "main": [
        [
          {
            "node": "HTTP Request - Send to your backend",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Gate - Coindesk": {
      "main": [
        [
          {
            "node": "RSS Read - Coindesk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read - X Posts": {
      "main": [
        [
          {
            "node": "Code - Tag topic - X",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read - Coindesk": {
      "main": [
        [
          {
            "node": "Code - Tag topic - Coin desk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Tag topic - X": {
      "main": [
        [
          {
            "node": "Code - Accumulate X items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - More X batches?": {
      "main": [
        [
          {
            "node": "X Batches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code - Finalize X batches (emit combined)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source set - X Posts": {
      "main": [
        [
          {
            "node": "Merge - X posts + CoinTelegraph",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Gate - Google news": {
      "main": [
        [
          {
            "node": "Code - URL build - Google news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source set - Coindesk": {
      "main": [
        [
          {
            "node": "Merge - Coindesk + Google news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Keywords Filter": {
      "main": [
        [
          {
            "node": "Code - Array bind",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read - Google news": {
      "main": [
        [
          {
            "node": "Code - Tag topic - Google news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Gate - CoinTelegraph": {
      "main": [
        [
          {
            "node": "RSS Read - CoinTelegraph",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop back \u2013 X Batches": {
      "main": [
        [
          {
            "node": "X Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read - CoinTelegraph": {
      "main": [
        [
          {
            "node": "Code - Tag topic - Coin telegraph",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source set - Google news": {
      "main": [
        [
          {
            "node": "Merge - Coindesk + Google news",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code - Accumulate X items": {
      "main": [
        [
          {
            "node": "IF - More X batches?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Reset X accumulator": {
      "main": [
        [
          {
            "node": "X Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - URL Build - XCancel": {
      "main": [
        [
          {
            "node": "Code - Reset X accumulator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source set - Cointelegraph": {
      "main": [
        [
          {
            "node": "Merge - X posts + CoinTelegraph",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code - Tag topic - Coin desk": {
      "main": [
        [
          {
            "node": "Source set - Coindesk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Tag topic - Google news": {
      "main": [
        [
          {
            "node": "Source set - Google news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - URL build - Google news": {
      "main": [
        [
          {
            "node": "RSS Read - Google news",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Coindesk + Google news": {
      "main": [
        [
          {
            "node": "Merge - Merge previous two merges",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - X posts + CoinTelegraph": {
      "main": [
        [
          {
            "node": "Merge - Merge previous two merges",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code - Tag topic - Coin telegraph": {
      "main": [
        [
          {
            "node": "Source set - Cointelegraph",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Merge previous two merges": {
      "main": [
        [
          {
            "node": "Code - Keywords Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Finalize X batches (emit combined)": {
      "main": [
        [
          {
            "node": "Source set - X Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}