{
  "name": "Reddit \u2014 India + student career subs (RSS aggregator)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 12
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000001",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "jsCode": "// === Reddit subreddit aggregator ===\n// Fetches Atom feed (.rss endpoint returns Atom XML) for each subreddit\n// and flattens posts into a single item list. Reddit gates by User-Agent;\n// without a real-looking UA it returns 429s.\n//\n// Heavy keyword filter is applied later via the IF node; community posts\n// are noisy.\n\nconst SUBREDDITS = [\n  { slug: 'developersIndia',         displayName: 'r/developersIndia' },\n  { slug: 'cscareerquestionsIndia',  displayName: 'r/cscareerquestionsIndia' },\n  { slug: 'csMajors',                displayName: 'r/csMajors' }\n];\n\n// Per-subreddit cap so one busy sub doesn't dominate the run.\nconst PER_SUB_LIMIT = 10;\n\nfunction unescapeEntities(s) {\n  if (!s) return '';\n  return String(s)\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/&amp;/g, '&')\n    .replace(/&quot;/g, '\"')\n    .replace(/&#39;/g, \"'\")\n    .replace(/&nbsp;/g, ' ');\n}\n\nfunction stripHtml(s) {\n  if (!s) return '';\n  return unescapeEntities(String(s))\n    .replace(/<[^>]*>/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\n// Parse Atom <entry> blocks via regex \u2014 Reddit's feeds are simple.\nfunction parseAtomEntries(xml) {\n  const entries = [];\n  const entryRe = /<entry\\b[\\s\\S]*?<\\/entry>/g;\n  let match;\n  while ((match = entryRe.exec(xml)) !== null) {\n    const block = match[0];\n    const title = (block.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/) || [])[1] || '';\n    const link = (block.match(/<link[^>]*href=\"([^\"]+)\"/) || [])[1] || '';\n    const content = (block.match(/<content[^>]*>([\\s\\S]*?)<\\/content>/) || [])[1] || '';\n    const updated = (block.match(/<updated>([^<]+)<\\/updated>/) || [])[1] || '';\n    entries.push({\n      title: unescapeEntities(title.trim()),\n      link,\n      content: unescapeEntities(content),\n      updated,\n    });\n  }\n  return entries;\n}\n\n// Negative-keyword pre-filter: skip obvious meta/discussion posts before we\n// burn AI tokens trying to extract a non-existent opportunity. Tuned from\n// failures observed in production (megathreads, weekly threads, rants).\nconst SKIP_PHRASES = [\n  'megathread',\n  'monthly thread',\n  'weekly thread',\n  'who\\'s looking',\n  'who is looking',\n  'who\\'s hiring? -',\n  'who is hiring? -',\n  'discussion thread',\n  'general discussion',\n  'rant',\n  'meme',\n  'shitpost',\n  'salary thread',\n  'compensation thread',\n  'interview experience',\n  'career advice',\n  'doubt',\n  'help needed',\n];\n\nfunction looksLikeMeta(title) {\n  const t = String(title || '').toLowerCase();\n  if (!t) return true;\n  return SKIP_PHRASES.some((p) => t.includes(p));\n}\n\nconst out = [];\nconst errors = [];\n\nfor (const sub of SUBREDDITS) {\n  try {\n    const xml = await this.helpers.httpRequest({\n      method: 'GET',\n      url: `https://www.reddit.com/r/${sub.slug}/.rss`,\n      headers: {\n        Accept: 'application/atom+xml, application/xml',\n        'User-Agent': 'opportunity-os/1.0 (RSS aggregator; https://opportunity-os-eight.vercel.app)',\n      },\n    });\n    const entries = parseAtomEntries(String(xml)).slice(0, PER_SUB_LIMIT);\n    for (const e of entries) {\n      if (!e.link) continue;\n      if (looksLikeMeta(e.title)) continue; // drop megathreads, rants, etc.\n      out.push({\n        json: {\n          title: e.title,\n          link: e.link,\n          contentSnippet: stripHtml(e.content).slice(0, 1000),\n          content: e.content,\n          subreddit: sub.displayName,\n          source_name: `Reddit: ${sub.displayName}`,\n          updated_at: e.updated,\n        }\n      });\n    }\n  } catch (e) {\n    errors.push(`${sub.slug}: ${(e && e.message) || e}`);\n  }\n}\n\nif (out.length === 0) {\n  throw new Error('Reddit aggregator returned 0 usable posts. Errors: ' + (errors.join(' | ') || 'none'));\n}\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        208,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000002",
      "name": "Fetch Reddit Subreddits"
    },
    {
      "parameters": {
        "maxItems": 15
      },
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        416,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000003",
      "name": "Limit"
    },
    {
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        624,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000004",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "k1",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "hiring",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k2",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "intern",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k3",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "fellowship",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k4",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "scholarship",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k5",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "hackathon",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k6",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "openings",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k7",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "freshers",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            },
            {
              "id": "k8",
              "leftValue": "={{ ($json.title + ' ' + ($json.contentSnippet || '')).toLowerCase() }}",
              "rightValue": "referral",
              "operator": {
                "type": "string",
                "operation": "contains"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        832,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000005",
      "name": "Filter Keywords"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:3000/api/ingest/check-exists",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "REPLACE_WITH_YOUR_INGEST_SHARED_SECRET"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "source_url",
              "value": "={{ $json.link }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1040,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000006",
      "name": "Check Exists"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "n1",
              "leftValue": "={{ $json.exists }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1248,
        0
      ],
      "id": "a0red005-0000-4000-8000-000000000007",
      "name": "If New"
    },
    {
      "parameters": {
        "amount": 7
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1456,
        -64
      ],
      "id": "a0red005-0000-4000-8000-000000000008",
      "name": "Wait"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:3000/api/ai/extract",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "REPLACE_WITH_YOUR_INGEST_SHARED_SECRET"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "={{ ($('Loop Over Items').item.json.title + '\\n\\n' + ($('Loop Over Items').item.json.contentSnippet || '')).slice(0, 4000) }}"
            },
            {
              "name": "source_url",
              "value": "={{ $('Loop Over Items').item.json.link }}"
            },
            {
              "name": "source_name",
              "value": "={{ $('Loop Over Items').item.json.source_name }}"
            },
            {
              "name": "hint",
              "value": "={{ 'Reddit post from ' + $('Loop Over Items').item.json.subreddit + ' \u2014 community-shared opportunity. May be a referral, internship, or hiring announcement; expect informal phrasing.' }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "timeout": 60000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1648,
        -64
      ],
      "id": "a0red005-0000-4000-8000-000000000009",
      "name": "AI Extract",
      "retryOnFail": true,
      "waitBetweenTries": 1000
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:3000/api/ingest/upsert",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "REPLACE_WITH_YOUR_INGEST_SHARED_SECRET"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "opportunity",
              "value": "={{ $json.opportunity }}"
            },
            {
              "name": "source_url",
              "value": "={{ $('Loop Over Items').item.json.link }}"
            },
            {
              "name": "source_name",
              "value": "={{ $('Loop Over Items').item.json.source_name }}"
            }
          ]
        },
        "options": {
          "timeout": 15000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1888,
        -64
      ],
      "id": "a0red005-0000-4000-8000-000000000010",
      "name": "Upsert Opportunity"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:3000/api/log",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "REPLACE_WITH_YOUR_INGEST_SHARED_SECRET"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "skipped_filtered"
            },
            {
              "name": "source_url",
              "value": "={{ $json.link }}"
            },
            {
              "name": "source_name",
              "value": "={{ $json.source_name }}"
            },
            {
              "name": "reason",
              "value": "no opportunity-related keyword in title/content"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1136,
        272
      ],
      "id": "a0red005-0000-4000-8000-000000000011",
      "name": "Log Filtered"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:3000/api/log",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "REPLACE_WITH_YOUR_INGEST_SHARED_SECRET"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "skipped_duplicate"
            },
            {
              "name": "source_url",
              "value": "={{ $('Loop Over Items').item.json.link }}"
            },
            {
              "name": "source_name",
              "value": "={{ $('Loop Over Items').item.json.source_name }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1520,
        272
      ],
      "id": "a0red005-0000-4000-8000-000000000012",
      "name": "Log Duplicate"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Reddit Subreddits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Reddit Subreddits": {
      "main": [
        [
          {
            "node": "Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Filter Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Keywords": {
      "main": [
        [
          {
            "node": "Check Exists",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Filtered",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Exists": {
      "main": [
        [
          {
            "node": "If New",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If New": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "AI Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Extract": {
      "main": [
        [
          {
            "node": "Upsert Opportunity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Opportunity": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Filtered": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Duplicate": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "tags": []
}