{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "77d347e1-1f86-48d8-9475-d1c8557fb424",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 520,
        "content": "## Henry Hub Futures Tracker\n\nScrapes real-time Henry Hub data, extracts spot price and the next 12 monthly futures contracts, and delivers a clean tabular update to Telegram.\n\n### How it works\n1. Triggers multiple times during market hours\n2. Scrapes the Henry Hub futures page\n3. Extracts current spot price and 12 forward contracts\n4. Formats data into a structured HTML table\n5. Sends the update to your Telegram channel/chat\n\n### Setup\n- [ ] Add your Telegram Bot credentials\n- [ ] Replace `YOUR_TELEGRAM_CHAT_ID` in the final node\n- [ ] Adjust Schedule Trigger for your specific time zone\n\n### How to Use\nActivate the workflow and receive automated market updates, or press 'Test Workflow' for an immediate pull."
      },
      "typeVersion": 1
    },
    {
      "id": "679dd1f7-e910-489e-ae4c-339b1a581bbd",
      "name": "Sec1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        304,
        96
      ],
      "parameters": {
        "width": 460,
        "height": 276,
        "content": "## 1. Fetch\nTriggers and pulls HTML."
      },
      "typeVersion": 1
    },
    {
      "id": "92eee4f6-2997-4185-bf9f-4616c71c0e69",
      "name": "Sec3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        96
      ],
      "parameters": {
        "width": 282,
        "height": 276,
        "content": "## 3. Send\nDelivers to Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "4c5000ed-1a4f-4d84-b110-8ef575768b8b",
      "name": "Fetch Natural Gas Page1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        624,
        208
      ],
      "parameters": {
        "url": "https://oilprice.com/futures/natural-gas/",
        "options": {}
      },
      "typeVersion": 4.3
    },
    {
      "id": "3254ebf7-a419-4a02-b77e-f71d8d24d597",
      "name": "Market Hours Trigger1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        400,
        208
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 10
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 12
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 14
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 16
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 18,
              "triggerAtMinute": 19
            },
            {
              "field": "weeks",
              "triggerAtDay": [
                1,
                2,
                3,
                4,
                5
              ],
              "triggerAtHour": 19,
              "triggerAtMinute": 20
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "b4c864af-128c-4434-bf0c-a4dcd76f5794",
      "name": "Extract Spot & Futures1",
      "type": "n8n-nodes-base.code",
      "position": [
        848,
        208
      ],
      "parameters": {
        "jsCode": "// n8n Code Node to Extract Henry Hub Natural Gas Futures Prices + CURRENT SPOT PRICE\n// This function extracts all available contract data AND the current real-time price\n\nconst items = $input.all();\nconst extractedData = [];\n\nfor (const item of items) {\n  const htmlContent = item.json.data;\n  \n  // ========================================\n  // EXTRACT CURRENT SPOT PRICE (Real-time)\n  // ========================================\n  let currentSpotPrice = null;\n  let currentChange = null;\n  let currentChangePercent = null;\n  let currentChangeDirection = null;\n  \n  // Look for the current Natural Gas spot price in the header section\n  const spotPricePattern = /<tr class=\"[^\"]*\" data-hash=\"Natural-Gas\"[^>]*>(.*?)<\\/tr>/s;\n  const spotMatch = htmlContent.match(spotPricePattern);\n  \n  if (spotMatch) {\n    const spotRow = spotMatch[1];\n    \n    // Extract current price\n    const priceMatch = spotRow.match(/<td class=\"value\">\\s*([\\d.]+)\\s*<i/);\n    if (priceMatch) {\n      currentSpotPrice = parseFloat(priceMatch[1]);\n    }\n    \n    // Extract change value\n    const changeMatch = spotRow.match(/<td class=\"change_amount\">\\s*([+\\-][\\d.]+)/);\n    if (changeMatch) {\n      currentChange = parseFloat(changeMatch[1]);\n    }\n    \n    // Extract change percentage\n    const percentMatch = spotRow.match(/<td class=\"change_percent\">\\s*([+\\-][\\d.]+)%/);\n    if (percentMatch) {\n      currentChangePercent = parseFloat(percentMatch[1]);\n    }\n    \n    // Extract direction (up/down)\n    const directionMatch = spotRow.match(/class=\"(change_up|change_down)/);\n    if (directionMatch) {\n      currentChangeDirection = directionMatch[1].replace('change_', '');\n    }\n  }\n  \n  // ========================================\n  // EXTRACT FUTURES CONTRACTS DATA\n  // ========================================\n  const rowPattern = /<div class=\"info_table_row\" data-symbol=\"([^\"]+)\" data-days_5='([^']*)' data-days_30='([^']*)' data-days_90='([^']*)' data-year='([^']*)' data-ytd='([^']*)'[^>]*>((?:(?!<div class=\"info_table_row\").)*?)<\\/div>/gs;\n  \n  const contracts_list = [];\n  let match;\n  \n  while ((match = rowPattern.exec(htmlContent)) !== null) {\n    const symbol = match[1];\n    const days5Price = match[2] ? parseFloat(match[2]) : null;\n    const days30Price = match[3] ? parseFloat(match[3]) : null;\n    const days90Price = match[4] ? parseFloat(match[4]) : null;\n    const yearPrice = match[5] ? parseFloat(match[5]) : null;\n    const ytdPrice = match[6] ? parseFloat(match[6]) : null;\n    const innerHtml = match[7];\n    \n    // Extract contract details\n    const nameMatch = innerHtml.match(/data-name=\"([^\"]+)\"/);\n    const lastPriceMatch = innerHtml.match(/<div class=\"info_table_cell last_price\">\\s*([\\d.]+)/);\n    const changeMatch = innerHtml.match(/<div class=\"info_table_cell (up|down) change\">\\s*([+\\-][\\d.]+)/);\n    const openMatch = innerHtml.match(/<div class=\"info_table_cell open\">\\s*([\\d.]+)/);\n    const highMatch = innerHtml.match(/<div class=\"info_table_cell high\">\\s*([\\d.]+)/);\n    const lowMatch = innerHtml.match(/<div class=\"info_table_cell low\">\\s*([\\d.]+)/);\n    const dateMatch = innerHtml.match(/<div class=\"info_table_cell date\">\\s*([^<\\n]+)/);\n    \n    const lastPrice = lastPriceMatch ? parseFloat(lastPriceMatch[1]) : null;\n    const changeDirection = changeMatch ? changeMatch[1] : null;\n    const changeValue = changeMatch ? parseFloat(changeMatch[2]) : null;\n    \n    const contractData = {\n      symbol: symbol,\n      contract_name: nameMatch ? nameMatch[1] : '',\n      last_price: lastPrice,\n      change_direction: changeDirection,\n      change_value: changeValue,\n      change_percent: lastPrice && changeValue ? parseFloat(((changeValue / lastPrice) * 100).toFixed(2)) : null,\n      open: openMatch ? parseFloat(openMatch[1]) : null,\n      high: highMatch ? parseFloat(highMatch[1]) : null,\n      low: lowMatch ? parseFloat(lowMatch[1]) : null,\n      date_time: dateMatch ? dateMatch[1].trim() : '',\n      price_5_days_ago: days5Price,\n      price_30_days_ago: days30Price,\n      price_90_days_ago: days90Price,\n      price_1_year_ago: yearPrice,\n      price_ytd: ytdPrice\n    };\n    \n    // Calculate percentage changes\n    if (lastPrice) {\n      if (days5Price) contractData.change_5_days_percent = parseFloat((((lastPrice - days5Price) / days5Price) * 100).toFixed(2));\n      if (days30Price) contractData.change_30_days_percent = parseFloat((((lastPrice - days30Price) / days30Price) * 100).toFixed(2));\n      if (days90Price) contractData.change_90_days_percent = parseFloat((((lastPrice - days90Price) / days90Price) * 100).toFixed(2));\n      if (yearPrice) contractData.change_1_year_percent = parseFloat((((lastPrice - yearPrice) / yearPrice) * 100).toFixed(2));\n      if (ytdPrice) contractData.change_ytd_percent = parseFloat((((lastPrice - ytdPrice) / ytdPrice) * 100).toFixed(2));\n    }\n    \n    contractData.scraped_at = new Date().toISOString();\n    contracts_list.push(contractData);\n  }\n  \n  // ========================================\n  // COMBINE SPOT PRICE + CONTRACTS\n  // ========================================\n  // Add spot price data as a special entry at the beginning\n  if (currentSpotPrice) {\n    extractedData.push({\n      type: 'spot_price',\n      spot_price: currentSpotPrice,\n      spot_change: currentChange,\n      spot_change_percent: currentChangePercent,\n      spot_direction: currentChangeDirection,\n      timestamp: new Date().toISOString(),\n      timestamp_ist: new Date().toLocaleString('en-IN', { \n        timeZone: 'Asia/Kolkata',\n        year: 'numeric',\n        month: 'short',\n        day: 'numeric',\n        hour: '2-digit',\n        minute: '2-digit',\n        second: '2-digit',\n        hour12: true\n      })\n    });\n  }\n  \n  // Add all contract data\n  contracts_list.forEach(contract => {\n    extractedData.push({\n      type: 'futures_contract',\n      ...contract\n    });\n  });\n}\n\nreturn extractedData.map(data => ({ json: data }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9f9ea823-ff65-4038-b2f2-210c56dbe458",
      "name": "Build HTML Table1",
      "type": "n8n-nodes-base.code",
      "position": [
        1072,
        208
      ],
      "parameters": {
        "jsCode": "// Get all input data\nconst items = $input.all();\n\n// Separate spot price and futures contracts\nlet spotData = null;\nconst futuresContracts = [];\n\nitems.forEach(item => {\n  if (item.json.type === 'spot_price') {\n    spotData = item.json;\n  } else if (item.json.type === 'futures_contract') {\n    futuresContracts.push(item.json);\n  }\n});\n\n// Build the message\nlet message = `\ud83d\udcca <b>HENRY HUB NATURAL GAS FUTURES</b>\\n`;\nmessage += `\ud83d\udcc5 ${spotData.timestamp_ist}\\n`;\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\n\n// Spot Price Section - TABLE FORMAT (same as futures)\nconst isPositive = spotData.spot_change > 0;\nconst indicator = isPositive ? '\ud83d\udfe2' : '\ud83d\udd34';\n\nmessage += `\ud83d\udcb0 <b>SPOT PRICE</b>\\n\\n`;\nmessage += `<pre>`;\nmessage += `Contract  Period      Price    Change\\n`;\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\n\n// Format spot price row\nconst spotSymbol = `${indicator} SPOT  `.padEnd(10);\nconst today = new Date();\nconst todayStr = today.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: '2-digit' }).replace(/ /g, ' ');\nconst spotPeriod = `Today`.padEnd(10);\nconst spotPrice = `$${spotData.spot_price.toFixed(2)}`.padStart(7);\nconst spotChange = `${isPositive ? '$+' : '$'}${Math.abs(spotData.spot_change).toFixed(2)}`.padStart(8);\n\nmessage += `${spotSymbol}  ${spotPeriod}  ${spotPrice}  ${spotChange}\\n`;\nmessage += `</pre>\\n\\n`;\n\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\nmessage += `\ud83d\udcc8 <b>NEXT 12 MONTHS</b>\\n\\n`;\n\n// Futures contracts table\nmessage += `<pre>`;\nmessage += `Contract  Period      Price    Change\\n`;\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\n\nconst monthlyContracts = futuresContracts.slice(0, 12);\n\nmonthlyContracts.forEach(contract => {\n  // Extract month and year from contract name\n  const nameParts = contract.contract_name.replace('Natural Gas ', '').split(' ');\n  const month = nameParts[0].substring(0, 3); // First 3 letters of month\n  const year = `'${nameParts[1].substring(2)}`; // Last 2 digits of year\n  \n  // Use 30-day price as current (since last_price is null)\n  const currentPrice = contract.price_30_days_ago;\n  \n  // Calculate change from 1 year ago\n  const changeValue = contract.price_1_year_ago ? currentPrice - contract.price_1_year_ago : 0;\n  \n  // Format the row with proper spacing\n  const symbol = `\ud83d\udd34 ${contract.symbol.padEnd(6)}`;\n  const period = `${month} ${year}`.padEnd(10);\n  const price = `$${currentPrice.toFixed(2)}`.padStart(7);\n  const change = `${changeValue > 0 ? '$+' : '$'}${changeValue.toFixed(2)}`.padStart(8);\n  \n  message += `${symbol}  ${period}  ${price}  ${change}\\n`;\n});\n\nmessage += `</pre>\\n\\n`;\n\n// Summary stats\nconst monthlyPrices = monthlyContracts.map(c => c.price_30_days_ago);\nconst lowestPrice = Math.min(...monthlyPrices);\nconst highestPrice = Math.max(...monthlyPrices);\nconst avgPrice = monthlyPrices.reduce((a, b) => a + b, 0) / monthlyPrices.length;\n\nmessage += `\ud83d\udcca Range: $${lowestPrice.toFixed(2)} - $${highestPrice.toFixed(2)}\\n`;\nmessage += `\ud83d\udcc8 Avg: $${avgPrice.toFixed(2)}\\n\\n`;\n\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\nmessage += `<i>\ud83d\udd14 Source: Henry Hub Futures Data</i>`;\n\n// Return the formatted message\nreturn {\n  json: {\n    telegram_message: message\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "3341bc66-95c5-4b39-99f5-d570f57ed9ae",
      "name": "Send Telegram Update1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1312,
        208
      ],
      "parameters": {
        "text": "={{ $json.telegram_message }}",
        "chatId": "YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "c66e004b-ef3c-49c4-8e64-7888ff5a89f1",
      "name": "Sec2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        784,
        96
      ],
      "parameters": {
        "width": 436,
        "height": 276,
        "content": "## 2. Process\nParses prices and formats table."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Build HTML Table1": {
      "main": [
        [
          {
            "node": "Send Telegram Update1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Market Hours Trigger1": {
      "main": [
        [
          {
            "node": "Fetch Natural Gas Page1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Spot & Futures1": {
      "main": [
        [
          {
            "node": "Build HTML Table1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Natural Gas Page1": {
      "main": [
        [
          {
            "node": "Extract Spot & Futures1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}