{
  "name": "Unstop \u2014 Hackathons (direct upsert)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 12
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000001",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "jsCode": "// === Unstop Public Search API ===\n// India-centric platform for hackathons, case competitions, internships.\n// The public search endpoint powers their listings UI; returns rich JSON.\n//\n// We hit the hackathons surface here. To add competitions/internships,\n// clone this workflow and change `opportunity=` query param + category mapping.\n\nlet data;\ntry {\n  data = await this.helpers.httpRequest({\n    method: 'GET',\n    url: 'https://unstop.com/api/public/opportunity/search-result?opportunity=hackathons&per_page=20&oppstatus=open',\n    json: true,\n    headers: { Accept: 'application/json' },\n  });\n} catch (e) {\n  throw new Error('Unstop API fetch failed: ' + ((e && e.message) || e));\n}\n\n// Response shape: { data: { current_page, data: [...] } }\nconst items = (data && data.data && Array.isArray(data.data.data)) ? data.data.data : [];\nif (items.length === 0) {\n  throw new Error('Unstop returned 0 hackathons');\n}\n\nfunction extractTags(h) {\n  const tags = new Set();\n  if (Array.isArray(h.required_skills)) {\n    for (const s of h.required_skills) {\n      const name = (s && (s.name || s.title)) || (typeof s === 'string' ? s : null);\n      if (name) tags.add(String(name).toLowerCase());\n    }\n  }\n  if (Array.isArray(h.filters)) {\n    for (const f of h.filters) {\n      const name = (f && (f.name || f.title)) || (typeof f === 'string' ? f : null);\n      if (name) tags.add(String(name).toLowerCase());\n    }\n  }\n  return Array.from(tags).slice(0, 6);\n}\n\nfunction extractLocation(h) {\n  // Unstop nests location info in address_with_country_logo or similar\n  if (h.address_with_country_logo) {\n    const a = h.address_with_country_logo;\n    return [a.city, a.state, a.country].filter(Boolean).join(', ') || 'India';\n  }\n  if (typeof h.location === 'string') return h.location;\n  return 'India';\n}\n\nfunction extractCompensation(h) {\n  // Prize info lives in different fields depending on the listing.\n  // Always strip HTML \u2014 Unstop sometimes returns currency-formatted markup.\n  if (h.prize_money) return stripHtml(String(h.prize_money));\n  if (Array.isArray(h.prizes) && h.prizes.length) {\n    const first = h.prizes[0];\n    if (first && first.amount) return stripHtml(String(first.amount));\n  }\n  // Sometimes embedded in description as \u20b9X,XX,XXX \u2014 let admin fix manually.\n  return null;\n}\n\nfunction extractEligibility(h) {\n  if (h.regnRequirements) {\n    const r = h.regnRequirements;\n    const parts = [];\n    if (r.team_size_min || r.team_size_max) {\n      parts.push(`Team size: ${r.team_size_min || 1}-${r.team_size_max || 'any'}`);\n    }\n    if (r.eligibility) parts.push(String(r.eligibility));\n    return parts.length ? parts.join('. ') : null;\n  }\n  return null;\n}\n\nfunction stripHtml(s) {\n  if (!s) return null;\n  return String(s)\n    .replace(/<[^>]*>/g, ' ')\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&amp;/g, '&')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nconst out = [];\nfor (const h of items) {\n  const orgName = (h.organisation && h.organisation.name) || 'Unstop';\n  const cleanDesc = stripHtml(h.description);\n  const summary = cleanDesc\n    ? (cleanDesc.length > 280 ? cleanDesc.slice(0, 277) + '...' : cleanDesc)\n    : `Hackathon by ${orgName}`;\n\n  const opportunity = {\n    title: h.title,\n    organization: orgName,\n    category: 'hackathon',\n    description: cleanDesc,\n    summary: summary,\n    tags: extractTags(h),\n    deadline: h.end_date || null,  // already ISO 8601\n    eligibility: extractEligibility(h),\n    location: extractLocation(h),\n    compensation: extractCompensation(h),\n    is_remote: false,  // Unstop hackathons are mostly offline at colleges\n    apply_url: h.seo_url,\n    difficulty: null,\n    estimated_value_score: null,\n    extraction_confidence: 0.9,\n  };\n\n  out.push({ json: { opportunity, source_url: h.seo_url } });\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        208,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000002",
      "name": "Fetch Unstop Hackathons"
    },
    {
      "parameters": {
        "maxItems": 15
      },
      "type": "n8n-nodes-base.limit",
      "typeVersion": 1,
      "position": [
        416,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000003",
      "name": "Limit"
    },
    {
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        624,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000004",
      "name": "Loop Over Items"
    },
    {
      "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.source_url }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        832,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000005",
      "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": [
        1040,
        0
      ],
      "id": "50ust001-0000-4000-8000-000000000006",
      "name": "If New"
    },
    {
      "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": "={{ $('Loop Over Items').item.json.opportunity }}"
            },
            {
              "name": "source_url",
              "value": "={{ $('Loop Over Items').item.json.source_url }}"
            },
            {
              "name": "source_name",
              "value": "Unstop"
            }
          ]
        },
        "options": {
          "timeout": 15000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1280,
        -64
      ],
      "id": "50ust001-0000-4000-8000-000000000007",
      "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_duplicate"
            },
            {
              "name": "source_url",
              "value": "={{ $('Loop Over Items').item.json.source_url }}"
            },
            {
              "name": "source_name",
              "value": "Unstop"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1280,
        272
      ],
      "id": "50ust001-0000-4000-8000-000000000008",
      "name": "Log Duplicate"
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Unstop Hackathons",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Unstop Hackathons": {
      "main": [
        [
          {
            "node": "Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Check Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Exists": {
      "main": [
        [
          {
            "node": "If New",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If New": {
      "main": [
        [
          {
            "node": "Upsert Opportunity",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Opportunity": {
      "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": []
}