{
  "id": "c1Kqe60KXsBCMRf5",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Crypto Price Alert Bot \u2014 Telegram, Sheets & Daily Digest",
  "tags": [],
  "nodes": [
    {
      "id": "375d1a99-0355-4f26-bb7a-f7e1e9cf4ad5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        400
      ],
      "parameters": {
        "width": 480,
        "height": 800,
        "content": "## Crypto Price Alert Bot \u2014 Telegram, Sheets & Daily Digest\n\n### How it works\n\nThis workflow monitors a configured crypto watchlist on a schedule, fetches CoinGecko market data, evaluates price-change, threshold, and RSI-style alert conditions, then sends Telegram alerts and logs them to Google Sheets. It also sends a separate scheduled daily Telegram digest with current prices and the Fear & Greed Index. A Telegram command listener lets users request an on-demand price lookup with `/price`, while unknown commands receive a help-style reply.\n\n### Setup steps\n\n- Configure the Telegram bot credentials for the alert, daily digest, and command reply Telegram nodes, and set the target chat ID where messages should be sent.\n- Connect Google Sheets credentials and point the alert logging node to the correct spreadsheet, sheet, and columns.\n- Review the Watchlist Config and Daily Digest Config code nodes to set CoinGecko asset IDs, symbols, thresholds, cooldowns, currencies, and schedule-specific options.\n- Confirm the schedule trigger timings for price monitoring and the daily digest match the desired timezone and frequency.\n- If using CoinGecko or other APIs with rate limits or paid keys, add the required authentication headers or query parameters to the HTTP Request nodes.\n\n### Customization\n\nAdjust alert thresholds, cooldown duration, tracked assets, RSI period, digest content, and supported Telegram command aliases in the relevant configuration and formatting code nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "e02e8e84-952f-47fd-ba41-b17ece510906",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        528
      ],
      "parameters": {
        "color": 5,
        "width": 640,
        "height": 304,
        "content": "## Start watchlist cycle\n\nScheduled monitoring begins here, builds the configured crypto watchlist, and feeds each asset into the batch loop for per-asset processing."
      },
      "typeVersion": 1
    },
    {
      "id": "92f9bc25-1e66-484e-9694-ed11178dba6b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1376,
        512
      ],
      "parameters": {
        "color": 5,
        "width": 640,
        "height": 320,
        "content": "## Fetch current market data\n\nRequests the latest market data for the active asset from CoinGecko, normalizes it with the watchlist settings, and skips back to the loop if no price data is available."
      },
      "typeVersion": 1
    },
    {
      "id": "caf7c3ca-bbac-4a04-9d76-3a6727037836",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2048,
        400
      ],
      "parameters": {
        "color": 5,
        "width": 624,
        "height": 416,
        "content": "## Load baseline state\n\nLoads stored per-asset state, checks whether the asset is being seen for the first time, and saves an initial baseline price when needed before returning to the loop."
      },
      "typeVersion": 1
    },
    {
      "id": "61ceeb99-d200-4c6b-9bc0-53b39f2d7921",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2448,
        800
      ],
      "parameters": {
        "color": 5,
        "width": 640,
        "height": 368,
        "content": "## Calculate alert indicators\n\nFor assets with existing state, this cluster fetches historical data, calculates RSI, and evaluates the configured alert rules against the current market conditions."
      },
      "typeVersion": 1
    },
    {
      "id": "d6d0e00c-6be1-4e35-8e00-41622b5dac51",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3120,
        608
      ],
      "parameters": {
        "color": 5,
        "width": 656,
        "height": 512,
        "content": "## Apply alert gating\n\nDecides whether an alert should be sent, enforces the cooldown window, and saves the latest price without sending an alert when conditions fail or cooldown has not passed."
      },
      "typeVersion": 1
    },
    {
      "id": "bbfd3a97-b289-4497-ab2d-8f97019a1ad2",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3808,
        640
      ],
      "parameters": {
        "color": 5,
        "width": 864,
        "height": 304,
        "content": "## Send and record alert\n\nFormats the Telegram alert, sends it, logs the event to Google Sheets, updates persistent alert state, and then returns control to the asset loop."
      },
      "typeVersion": 1
    },
    {
      "id": "ff5692c6-23c6-49e6-9a01-51f6e8678493",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        1728
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 320,
        "content": "## Start daily digest\n\nA separate scheduled lane starts the daily summary and loads configuration for the digest asset list and message settings."
      },
      "typeVersion": 1
    },
    {
      "id": "c46ff774-6c3b-496d-b690-f5b6f99906ef",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1088,
        1728
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 320,
        "content": "## Collect digest inputs\n\nFetches bulk current prices for the digest and enriches the summary with the latest Fear & Greed Index value."
      },
      "typeVersion": 1
    },
    {
      "id": "98e2d78c-f1ff-4b8b-ab9c-73e185feec52",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1536,
        1728
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 320,
        "content": "## Send daily summary\n\nCompiles the fetched market and sentiment data into a human-readable digest and sends it to Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "ddb69996-7a0f-4278-993d-a32d12030a52",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        2512
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 320,
        "content": "## Receive Telegram command\n\nListens for incoming Telegram messages and parses the command text, including resolving supported ticker aliases to CoinGecko IDs."
      },
      "typeVersion": 1
    },
    {
      "id": "93535468-f095-40e4-83a7-5aaf389dbbe3",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        2512
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 480,
        "content": "## Route command response\n\nChecks whether the parsed message is a supported `/price` command and sends an unknown-command reply for unsupported input."
      },
      "typeVersion": 1
    },
    {
      "id": "d047257f-6494-4cdd-9208-5fe68fda23c0",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        2544
      ],
      "parameters": {
        "color": 5,
        "width": 640,
        "height": 304,
        "content": "## Reply with price\n\nFetches the requested asset price, formats a Telegram-friendly response, and sends the on-demand price reply."
      },
      "typeVersion": 1
    },
    {
      "id": "140172b0-d787-4551-8a76-518df8bf57b4",
      "name": "Hourly Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        736,
        656
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9088db40-ed94-4c23-938e-9891624d0c5a",
      "name": "Configure Watchlist",
      "type": "n8n-nodes-base.code",
      "position": [
        960,
        656
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u2699\ufe0f  CRYPTO PRICE ALERT BOT \u2014 ALL CONFIGURATION HERE\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// This is the ONLY node you need to edit to get started.\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// \u2500\u2500 STEP 1: Telegram Chat ID \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Message @userinfobot on Telegram to get your Chat ID.\nconst TELEGRAM_CHAT_ID = 'YOUR_TELEGRAM_CHAT_ID_HERE';\n\n// \u2500\u2500 STEP 2: Google Sheets ID (optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Create a Google Sheet and paste its ID from the URL:\n// https://docs.google.com/spreadsheets/d/{SHEET_ID}/edit\n// Leave as placeholder to disable Sheets logging.\nconst GOOGLE_SHEET_ID = 'YOUR_GOOGLE_SHEET_ID_HERE';\n\n// \u2500\u2500 STEP 3: Your Crypto Watchlist \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Add or remove assets. Find CoinGecko IDs at:\n// https://api.coingecko.com/api/v3/coins/list\nconst watchlist = [\n  {\n    symbol: 'bitcoin',        // CoinGecko coin ID (must be lowercase)\n    display: 'BTC',           // Short label shown in Telegram alerts\n    threshold: 75000,         // USD price that triggers the alert\n    direction: 'below',       // 'above' or 'below'\n    cooldownMinutes: 60,      // Minimum minutes between repeated alerts\n    holdings: 0,              // Coins you own \u2014 set 0 to disable portfolio tracking\n    priceChangePct: 5,        // Alert if price moves \u00b15% since last check (0 = off)\n    useRSI: true,             // true = enable 14-period RSI confirmation filter\n    rsiOversold: 30,          // RSI below this is oversold (used as filter)\n    rsiOverbought: 70         // RSI above this is overbought (used as filter)\n  },\n  {\n    symbol: 'ethereum',\n    display: 'ETH',\n    threshold: 2000,\n    direction: 'above',\n    cooldownMinutes: 30,\n    holdings: 0,\n    priceChangePct: 5,\n    useRSI: true,\n    rsiOversold: 30,\n    rsiOverbought: 70\n  },\n  {\n    symbol: 'solana',\n    display: 'SOL',\n    threshold: 90,\n    direction: 'below',\n    cooldownMinutes: 60,\n    holdings: 0,\n    priceChangePct: 10,\n    useRSI: false,\n    rsiOversold: 30,\n    rsiOverbought: 70\n  }\n];\n\n// \u2500\u2500 Internal: pass config fields to each item \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nreturn watchlist.map(asset => ({\n  json: {\n    ...asset,\n    telegramChatId: TELEGRAM_CHAT_ID,\n    googleSheetId: GOOGLE_SHEET_ID\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "b1f4dc25-ca5d-40df-9aaf-3c7bb330f9c8",
      "name": "Loop Over Assets",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1184,
        656
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "c5cea446-0843-49f4-a3aa-627110bc3349",
      "name": "Fetch Crypto Prices",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 3,
      "position": [
        1424,
        672
      ],
      "parameters": {
        "url": "https://api.coingecko.com/api/v3/simple/price",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ids",
              "value": "={{ $json.symbol }}"
            },
            {
              "name": "vs_currencies",
              "value": "usd"
            },
            {
              "name": "include_24hr_change",
              "value": "true"
            },
            {
              "name": "include_24hr_vol",
              "value": "true"
            },
            {
              "name": "include_market_cap",
              "value": "true"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "6d2fe96d-4f67-4b6a-bf30-af576347092d",
      "name": "Merge Market Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1648,
        672
      ],
      "parameters": {
        "jsCode": "// Merge asset config (from Loop Assets) with the CoinGecko API response.\n// Handles API errors gracefully and computes portfolio value.\nconst assetConfig = $('Loop Over Assets').first().json;\nconst apiResponse = $input.first().json;\nconst symbol = assetConfig.symbol;\n\n// Guard: empty or failed API response\nif (!apiResponse || typeof apiResponse !== 'object' || Object.keys(apiResponse).length === 0) {\n  return [{ json: {\n    symbol,\n    display: assetConfig.display,\n    hasError: true,\n    errorMessage: `CoinGecko returned empty response for '${symbol}'. Check rate limits or coin ID.`\n  }}];\n}\n\nconst coinData = apiResponse[symbol];\n\n// Guard: symbol not found in response\nif (!coinData || typeof coinData.usd === 'undefined') {\n  return [{ json: {\n    symbol,\n    display: assetConfig.display,\n    hasError: true,\n    errorMessage: `Symbol '${symbol}' not found on CoinGecko. Verify the coin ID.`\n  }}];\n}\n\nconst currentPrice = Number(coinData.usd);\nconst holdings = Number(assetConfig.holdings || 0);\nconst portfolioValue = holdings > 0\n  ? parseFloat((currentPrice * holdings).toFixed(2))\n  : null;\n\nreturn [{ json: {\n  // Asset identity\n  symbol: assetConfig.symbol,\n  display: assetConfig.display,\n  chartUrl: `https://www.coingecko.com/en/coins/${assetConfig.symbol}`,\n  // Alert configuration\n  threshold: Number(assetConfig.threshold),\n  direction: assetConfig.direction,\n  cooldownMinutes: Number(assetConfig.cooldownMinutes),\n  priceChangePct: Number(assetConfig.priceChangePct || 0),\n  useRSI: Boolean(assetConfig.useRSI),\n  rsiOversold: Number(assetConfig.rsiOversold || 30),\n  rsiOverbought: Number(assetConfig.rsiOverbought || 70),\n  // Portfolio\n  holdings,\n  portfolioValue,\n  // Credentials / config pass-through\n  telegramChatId: assetConfig.telegramChatId,\n  googleSheetId: assetConfig.googleSheetId,\n  // Live market data\n  currentPrice,\n  change24h: parseFloat(Number(coinData.usd_24h_change || 0).toFixed(2)),\n  volume24h: Number(coinData.usd_24h_vol || 0),\n  marketCap: Number(coinData.usd_market_cap || 0),\n  // Metadata\n  currentTimestamp: Date.now(),\n  hasError: false\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "dc557b6d-9714-4b2e-81d2-a38101a8238a",
      "name": "If Price Data Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        1872,
        672
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-hasprice",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.hasError }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8d5ace0e-4405-48e3-9523-0c4488a46029",
      "name": "Load Asset State",
      "type": "n8n-nodes-base.code",
      "position": [
        2096,
        656
      ],
      "parameters": {
        "jsCode": "// Load per-asset persistent state from workflow static data.\n// Static data survives between executions when the workflow is activated.\nconst staticData = $getWorkflowStaticData('global');\nconst item = $input.first().json;\nconst { symbol } = item;\n\n// Initialise top-level store if first-ever execution\nif (!staticData.assets) staticData.assets = {};\n\n// Initialise per-asset state if this coin was never seen before\nif (!staticData.assets[symbol]) {\n  staticData.assets[symbol] = {\n    lastPrice: null,\n    lastAlertAt: null,\n    avgVolume: null,\n    volumeHistory: []\n  };\n}\n\nconst assetState = staticData.assets[symbol];\nconst nowTimestamp = Date.now();\n\nreturn [{ json: {\n  ...item,\n  lastPrice: assetState.lastPrice,\n  lastAlertAt: assetState.lastAlertAt,\n  avgVolume: assetState.avgVolume || null,\n  nowTimestamp,\n  isFirstRun: assetState.lastPrice === null\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "fdc83281-9251-446f-ae30-b1961026bff0",
      "name": "If First Time Running",
      "type": "n8n-nodes-base.if",
      "position": [
        2304,
        656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-firstrun",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isFirstRun }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "907e16ab-1442-4cc2-867c-e0f5360fe2a3",
      "name": "Store Initial Price",
      "type": "n8n-nodes-base.code",
      "position": [
        2528,
        560
      ],
      "parameters": {
        "jsCode": "// First run for this asset \u2014 store the baseline price so future cycles\n// can detect changes. No alert is sent on the first run.\nconst staticData = $getWorkflowStaticData('global');\nconst item = $input.first().json;\nif (!staticData.assets) staticData.assets = {};\nstaticData.assets[item.symbol] = {\n  lastPrice: item.currentPrice,\n  lastAlertAt: null,\n  avgVolume: null,\n  volumeHistory: []\n};\nreturn [{ json: item }];"
      },
      "typeVersion": 2
    },
    {
      "id": "acdbafc0-8709-41ea-b560-9c7376f63510",
      "name": "Fetch RSI Values",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 2,
      "position": [
        2496,
        1008
      ],
      "parameters": {
        "url": "={{ 'https://api.coingecko.com/api/v3/coins/' + $json.symbol + '/market_chart' }}",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "vs_currency",
              "value": "usd"
            },
            {
              "name": "days",
              "value": "15"
            },
            {
              "name": "interval",
              "value": "daily"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "d668d002-8de7-4c46-824a-03cd94c33493",
      "name": "Compute RSI",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        1008
      ],
      "parameters": {
        "jsCode": "// Calculate 14-period RSI using Wilder's Smoothing Method.\n// Uses CoinGecko daily close prices from the /market_chart endpoint.\n// Falls back gracefully if data is unavailable or useRSI is false.\nconst assetData = $('Load Asset State').first().json;\nconst marketChart = $input.first().json;\n\nfunction calculateRSI(prices, period) {\n  if (!prices || prices.length < period + 1) return null;\n  let gains = 0, losses = 0;\n  // Initial average over first `period` changes\n  for (let i = 1; i <= period; i++) {\n    const diff = prices[i] - prices[i - 1];\n    if (diff >= 0) gains += diff; else losses += Math.abs(diff);\n  }\n  let avgGain = gains / period;\n  let avgLoss = losses / period;\n  // Wilder's smoothing for remaining periods\n  for (let i = period + 1; i < prices.length; i++) {\n    const diff = prices[i] - prices[i - 1];\n    const g = diff >= 0 ? diff : 0;\n    const l = diff < 0 ? Math.abs(diff) : 0;\n    avgGain = (avgGain * (period - 1) + g) / period;\n    avgLoss = (avgLoss * (period - 1) + l) / period;\n  }\n  if (avgLoss === 0) return 100;\n  return parseFloat((100 - (100 / (1 + avgGain / avgLoss))).toFixed(2));\n}\n\nlet rsi = null;\nlet rsiLabel = 'N/A';\n\nif (assetData.useRSI && marketChart && Array.isArray(marketChart.prices) && marketChart.prices.length > 0) {\n  const closes = marketChart.prices.map(p => p[1]);\n  rsi = calculateRSI(closes, 14);\n  if (rsi !== null) {\n    rsiLabel = rsi > 70 ? '\ud83d\udd34 Overbought' : rsi < 30 ? '\ud83d\udfe2 Oversold' : '\u26aa Neutral';\n  }\n}\n\nreturn [{ json: { ...assetData, rsi, rsiLabel } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "2082b0a5-b461-4870-9120-fba233036964",
      "name": "Check Alert Conditions",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        1008
      ],
      "parameters": {
        "jsCode": "// Evaluate all three alert condition types and produce a decision.\n// Returns shouldAlert (bool) and triggerReasons (string array).\nconst item = $input.first().json;\nconst {\n  currentPrice, lastPrice, threshold, direction,\n  priceChangePct, rsi, rsiOversold, rsiOverbought,\n  useRSI, volume24h, avgVolume\n} = item;\n\nconst triggerReasons = [];\n\n// \u2500\u2500 Condition 1: Fixed Price Threshold \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst thresholdMet =\n  (direction === 'above' && currentPrice > threshold) ||\n  (direction === 'below' && currentPrice < threshold);\nif (thresholdMet) {\n  const dir = direction === 'above' ? 'Above \ud83d\udcc8' : 'Below \ud83d\udcc9';\n  triggerReasons.push(`Price ${dir} $${threshold.toLocaleString('en-US')}`);\n}\n\n// \u2500\u2500 Condition 2: Price % Change Since Last Check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet pctChangeMet = false;\nif (priceChangePct > 0 && lastPrice && lastPrice > 0) {\n  const actualPct = Math.abs((currentPrice - lastPrice) / lastPrice * 100);\n  if (actualPct >= priceChangePct) {\n    pctChangeMet = true;\n    const signedPct = ((currentPrice - lastPrice) / lastPrice * 100).toFixed(2);\n    const emoji = currentPrice > lastPrice ? '\ud83d\udcc8' : '\ud83d\udcc9';\n    triggerReasons.push(`${emoji} Price moved ${signedPct}% (trigger: \u00b1${priceChangePct}%)`);\n  }\n}\n\n// \u2500\u2500 Condition 3: Volume Spike (\u22652\u00d7 rolling 7-period average) \u2500\nlet volumeSpikeMet = false;\nif (avgVolume && avgVolume > 0 && volume24h > 0) {\n  const ratio = volume24h / avgVolume;\n  if (ratio >= 2) {\n    volumeSpikeMet = true;\n    triggerReasons.push(`\ud83d\udd0a Volume spike: ${ratio.toFixed(1)}\u00d7 average`);\n  }\n}\n\n// \u2500\u2500 RSI Filter: suppress alerts at extremes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// RSI is a FILTER, not a trigger. It prevents chasing extremes.\nlet rsiConfirms = true;\nif (useRSI && rsi !== null) {\n  if (direction === 'above' && rsi > rsiOverbought) rsiConfirms = false; // overbought, don't chase\n  if (direction === 'below' && rsi < rsiOversold)   rsiConfirms = false; // oversold, don't short\n}\n\nconst shouldAlert =\n  thresholdMet ||\n  volumeSpikeMet ||\n  (pctChangeMet && rsiConfirms);\n\nconst priceChangeFromLast = (lastPrice && lastPrice > 0)\n  ? parseFloat(((currentPrice - lastPrice) / lastPrice * 100).toFixed(2))\n  : null;\n\nreturn [{ json: {\n  ...item,\n  shouldAlert,\n  triggerReasons,\n  triggerReasonsStr: triggerReasons.join(' | '),\n  rsiConfirms,\n  priceChangeFromLast\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b6273cf2-9313-452a-8649-d2e2d5da64c9",
      "name": "If Alert Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        3168,
        768
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-shouldalert",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.shouldAlert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9f618697-25da-4612-92eb-5b1a75d3dd29",
      "name": "If Alert Cooldown Over",
      "type": "n8n-nodes-base.if",
      "position": [
        3392,
        768
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-cooldown",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.lastAlertAt === null || (($json.nowTimestamp - $json.lastAlertAt) > ($json.cooldownMinutes * 60 * 1000)) }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9caa8d74-ada9-4729-a572-950351eeefa6",
      "name": "Create Alert Message",
      "type": "n8n-nodes-base.code",
      "position": [
        3856,
        768
      ],
      "parameters": {
        "jsCode": "// Build a rich, human-readable Telegram alert message using Markdown.\n// Conditionally includes RSI, portfolio value, and % change sections.\nconst item = $input.first().json;\n\nconst fmt = (n) => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst changeSign = item.change24h >= 0 ? '+' : '';\nconst changeEmoji = item.change24h >= 0 ? '\ud83d\udcc8' : '\ud83d\udcc9';\nconst alertDate = new Date(item.nowTimestamp).toUTCString();\n\nconst lines = [\n  `\ud83d\udea8 *${item.display} Alert Triggered!*`,\n  ``,\n  `\ud83d\udcb0 *Current Price:* $${fmt(item.currentPrice)}`,\n  `${changeEmoji} *24h Change:* ${changeSign}${item.change24h}%`,\n];\n\n// Price change since last check (if available)\nif (item.priceChangeFromLast !== null) {\n  const s = item.priceChangeFromLast >= 0 ? '+' : '';\n  lines.push(`\ud83d\udcca *Change Since Last Check:* ${s}${item.priceChangeFromLast}%`);\n}\n\n// RSI reading (if RSI filter is enabled)\nif (item.useRSI && item.rsi !== null) {\n  lines.push(`\ud83d\udcd0 *RSI (14):* ${item.rsi} \u2014 ${item.rsiLabel}`);\n}\n\n// Portfolio value (if holdings are configured)\nif (item.portfolioValue !== null) {\n  lines.push(`\ud83d\udcbc *Portfolio Value:* $${fmt(item.portfolioValue)} (${item.holdings} ${item.display})`);\n}\n\n// Trigger reason breakdown\nlines.push(``, `\ud83c\udfaf *Triggered By:*`);\n(item.triggerReasons || []).forEach(r => lines.push(`  \u2022 ${r}`));\n\n// RSI override notice\nif (!item.rsiConfirms) {\n  lines.push(``, `\u26a0\ufe0f _RSI filter was overridden \u2014 multiple conditions met_`);\n}\n\nlines.push(``, `\ud83d\udcca [View Chart on CoinGecko](${item.chartUrl})`);\n\nreturn [{ json: { ...item, alertMessage: lines.join('\\n'), alertDate } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3ab4e413-3d06-4d8f-8dee-bb9c22e05bd3",
      "name": "Send Alert on Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        4080,
        768
      ],
      "parameters": {
        "text": "={{ $json.alertMessage }}",
        "chatId": "={{ $json.telegramChatId }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "dabb7d9e-caad-407e-b264-b1ffe00561d1",
      "name": "Append Alert to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        4304,
        768
      ],
      "parameters": {
        "columns": {
          "value": {
            "RSI": "={{ $json.rsi || '' }}",
            "Date": "={{ $json.alertDate }}",
            "Symbol": "={{ $json.symbol }}",
            "Display": "={{ $json.display }}",
            "Price ($)": "={{ $json.currentPrice }}",
            "Threshold ($)": "={{ $json.threshold }}",
            "24h Change (%)": "={{ $json.change24h }}",
            "Alert Triggers": "={{ $json.triggerReasonsStr }}",
            "Portfolio Value ($)": "={{ $json.portfolioValue || '' }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Symbol",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Symbol",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Display",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Display",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Price ($)",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Price ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "24h Change (%)",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "24h Change (%)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Alert Triggers",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Alert Triggers",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Threshold ($)",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Threshold ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Portfolio Value ($)",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Portfolio Value ($)",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "RSI",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "RSI",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "gid",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.googleSheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "continueOnFail": true
    },
    {
      "id": "59abcf6b-be86-44bd-a633-8b286c45b867",
      "name": "Save Alert Status",
      "type": "n8n-nodes-base.code",
      "position": [
        4528,
        768
      ],
      "parameters": {
        "jsCode": "// Save state after a successful alert.\n// Updates BOTH lastPrice AND lastAlertAt (starts the cooldown timer).\n// Also maintains a rolling 7-period volume history for spike detection.\nconst staticData = $getWorkflowStaticData('global');\nconst item = $input.first().json;\nif (!staticData.assets) staticData.assets = {};\n\nconst existing = staticData.assets[item.symbol] || {};\nconst volumeHistory = existing.volumeHistory ? [...existing.volumeHistory] : [];\nif (item.volume24h > 0) {\n  volumeHistory.push(item.volume24h);\n  if (volumeHistory.length > 7) volumeHistory.shift(); // keep last 7 only\n}\nconst avgVolume = volumeHistory.length > 0\n  ? volumeHistory.reduce((a, b) => a + b, 0) / volumeHistory.length\n  : null;\n\nstaticData.assets[item.symbol] = {\n  lastPrice: item.currentPrice,\n  lastAlertAt: item.nowTimestamp,  // \u2190 this starts the cooldown\n  avgVolume,\n  volumeHistory\n};\n\nreturn [{ json: item }];"
      },
      "typeVersion": 2
    },
    {
      "id": "8cd63fef-0954-4c22-9aea-475b126259b5",
      "name": "Log Price Without Alert",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        960
      ],
      "parameters": {
        "jsCode": "// Save the latest price WITHOUT resetting the cooldown timer.\n// Used when: no condition was met, or the cooldown hasn't expired yet.\n// Also updates the rolling volume history for spike detection.\nconst staticData = $getWorkflowStaticData('global');\nconst item = $input.first().json;\nif (!staticData.assets) staticData.assets = {};\n\nconst existing = staticData.assets[item.symbol] || {};\nconst volumeHistory = existing.volumeHistory ? [...existing.volumeHistory] : [];\nif (item.volume24h > 0) {\n  volumeHistory.push(item.volume24h);\n  if (volumeHistory.length > 7) volumeHistory.shift();\n}\nconst avgVolume = volumeHistory.length > 0\n  ? volumeHistory.reduce((a, b) => a + b, 0) / volumeHistory.length\n  : null;\n\nstaticData.assets[item.symbol] = {\n  lastPrice: item.currentPrice,\n  lastAlertAt: existing.lastAlertAt || null,  // \u2190 preserved, not reset\n  avgVolume,\n  volumeHistory\n};\n\nreturn [{ json: item }];"
      },
      "typeVersion": 2
    },
    {
      "id": "070b44ee-a13c-46db-ba45-860c1e53a9b7",
      "name": "Daily 8am Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        688,
        1888
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "224bffe1-9891-4d5b-a672-39d961f349de",
      "name": "Configure Daily Digest",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        1888
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500 Daily Digest Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Keep this watchlist in sync with the main Watchlist Config node.\n// Only symbol, display, and holdings are needed here.\n\nconst TELEGRAM_CHAT_ID = 'YOUR_TELEGRAM_CHAT_ID_HERE';\n\nconst watchlist = [\n  { symbol: 'bitcoin',  display: 'BTC', holdings: 0 },\n  { symbol: 'ethereum', display: 'ETH', holdings: 0 },\n  { symbol: 'solana',   display: 'SOL', holdings: 0 }\n];\n\nconst allSymbols = watchlist.map(a => a.symbol).join(',');\n\nreturn [{ json: { allSymbols, watchlist, telegramChatId: TELEGRAM_CHAT_ID } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "873627cb-0400-4151-b1e8-cc1f9561125d",
      "name": "Retrieve All Crypto Prices",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1136,
        1888
      ],
      "parameters": {
        "url": "https://api.coingecko.com/api/v3/simple/price",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ids",
              "value": "={{ $json.allSymbols }}"
            },
            {
              "name": "vs_currencies",
              "value": "usd"
            },
            {
              "name": "include_24hr_change",
              "value": "true"
            },
            {
              "name": "include_market_cap",
              "value": "true"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "32292f7f-6b38-4fee-b646-5b0e996edd45",
      "name": "Get Fear & Greed Index",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1360,
        1888
      ],
      "parameters": {
        "url": "https://api.alternative.me/fng/?limit=1",
        "options": {
          "timeout": 5000
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "f4f5b273-b435-44a9-9b75-fd208e456214",
      "name": "Build Daily Digest Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        1888
      ],
      "parameters": {
        "jsCode": "// Compile the daily digest message from bulk price data and Fear & Greed index.\n// Gracefully handles unavailable Fear & Greed data.\nconst digestConfig = $('Configure Daily Digest').first().json;\nconst pricesData = $('Retrieve All Crypto Prices').first().json;\nconst fngResponse = $input.first().json;\n\nconst fmt = (n) => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\nconst dateStr = new Date().toUTCString().replace(' GMT', ' UTC');\n\n// Fear & Greed block (optional \u2014 fails silently if API is down)\nlet fngLine = '';\ntry {\n  if (fngResponse && fngResponse.data && fngResponse.data[0]) {\n    const v = Number(fngResponse.data[0].value);\n    const label =\n      v < 25 ? '\ud83d\ude31 Extreme Fear' :\n      v < 45 ? '\ud83d\ude28 Fear' :\n      v < 55 ? '\ud83d\ude10 Neutral' :\n      v < 75 ? '\ud83d\ude04 Greed' :\n               '\ud83e\udd11 Extreme Greed';\n    fngLine = `\\n\ud83d\udcca *Fear & Greed:* ${v}/100 \u2014 ${label}`;\n  }\n} catch (_) {}\n\nconst lines = [\n  `\ud83d\udcc5 *Daily Crypto Summary*`,\n  `_${dateStr}_`,\n  fngLine,\n  ``,\n  `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`\n];\n\nlet totalPortfolio = 0;\n\nfor (const asset of digestConfig.watchlist) {\n  const coin = pricesData && pricesData[asset.symbol];\n  if (!coin) { lines.push(`\u2022 *${asset.display}* \u2014 data unavailable`); continue; }\n  const price = Number(coin.usd);\n  const chg = parseFloat(Number(coin.usd_24h_change || 0).toFixed(2));\n  const sign = chg >= 0 ? '+' : '';\n  const emoji = chg >= 0 ? '\ud83d\udcc8' : '\ud83d\udcc9';\n  const portfolioStr = asset.holdings > 0 ? ` | \ud83d\udcbc $${fmt(price * asset.holdings)}` : '';\n  if (asset.holdings > 0) totalPortfolio += price * asset.holdings;\n  lines.push(`${emoji} *${asset.display}:* $${fmt(price)} (${sign}${chg}%)${portfolioStr}`);\n}\n\nlines.push(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);\nif (totalPortfolio > 0) {\n  lines.push(`\ud83d\udcb0 *Total Portfolio:* $${fmt(totalPortfolio)}`, ``);\n}\nlines.push(`\ud83e\udd16 _Crypto Price Alert Bot_`);\n\nreturn [{ json: {\n  telegramChatId: digestConfig.telegramChatId,\n  digestMessage: lines.join('\\n')\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "24536483-b882-4541-b60c-85e0731b367f",
      "name": "Send Digest on Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1808,
        1888
      ],
      "parameters": {
        "text": "={{ $json.digestMessage }}",
        "chatId": "={{ $json.telegramChatId }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6dbae2a5-6449-4026-a4cb-83cb48695cd6",
      "name": "When Telegram Command Received",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        656,
        2672
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "c0052fb8-f282-44b5-88ea-54e5c8d5bdf0",
      "name": "Decode Telegram Message",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        2672
      ],
      "parameters": {
        "jsCode": "// Parse incoming Telegram message and resolve ticker aliases to CoinGecko IDs.\n// Handles commands in formats: /price, /price@botname, /price btc, /price bitcoin\nconst msg = $input.first().json.message;\n\nif (!msg || !msg.text) {\n  return [{ json: { commandType: 'unknown', symbol: '', chatId: String(msg?.chat?.id || ''), displayArg: '' } }];\n}\n\nconst raw = (msg.text || '').trim().toLowerCase();\nconst chatId = String(msg.chat.id);\nconst parts = raw.split(/\\s+/);\nconst cmdRaw = parts[0].replace(/^\\//, '').split('@')[0]; // strip / and @botname\nconst arg = (parts[1] || '').trim();\n\n// Ticker alias \u2192 CoinGecko coin ID mapping\nconst aliases = {\n  btc: 'bitcoin', eth: 'ethereum', sol: 'solana',\n  ada: 'cardano', dot: 'polkadot', matic: 'matic-network',\n  avax: 'avalanche-2', link: 'chainlink', doge: 'dogecoin',\n  xrp: 'ripple', bnb: 'binancecoin', uni: 'uniswap',\n  atom: 'cosmos', ltc: 'litecoin'\n};\n\nconst resolvedSymbol = aliases[arg] || arg;\n\nreturn [{ json: {\n  commandType: cmdRaw,\n  symbol: resolvedSymbol,\n  displayArg: arg.toUpperCase(),\n  chatId,\n  rawText: raw\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "748de380-2da4-402b-9157-3b2e2f3c1a70",
      "name": "If Command is /price",
      "type": "n8n-nodes-base.if",
      "position": [
        1104,
        2672
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-cmd-price",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.commandType }}",
              "rightValue": "price"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1bf6dd39-705d-4aa2-89f3-570cb0a83395",
      "name": "Fetch Specific Price",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1552,
        2672
      ],
      "parameters": {
        "url": "https://api.coingecko.com/api/v3/simple/price",
        "options": {
          "timeout": 8000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ids",
              "value": "={{ $json.symbol }}"
            },
            {
              "name": "vs_currencies",
              "value": "usd"
            },
            {
              "name": "include_24hr_change",
              "value": "true"
            },
            {
              "name": "include_market_cap",
              "value": "true"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "766e3773-e7e2-4412-945f-472c0365c110",
      "name": "Create Price Reply Message",
      "type": "n8n-nodes-base.code",
      "position": [
        1776,
        2672
      ],
      "parameters": {
        "jsCode": "// Format the price reply for a /price command.\n// Provides a helpful fallback message if the coin ID is not found.\nconst cmdData = $('Decode Telegram Message').first().json;\nconst priceData = $input.first().json;\nconst symbol = cmdData.symbol;\nconst coin = priceData && priceData[symbol];\n\nconst fmt = (n) => n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });\n\nif (!coin || !coin.usd) {\n  return [{ json: {\n    chatId: cmdData.chatId,\n    replyMessage: [\n      `\u2753 Could not find price for *${cmdData.displayArg || symbol}*.`,\n      ``,\n      `Try the full CoinGecko coin ID:`,\n      `\\`/price bitcoin\\``,\n      ``,\n      `Or browse IDs at: coingecko.com/en/coins`\n    ].join('\\n')\n  }}];\n}\n\nconst price = Number(coin.usd);\nconst chg = parseFloat(Number(coin.usd_24h_change || 0).toFixed(2));\nconst sign = chg >= 0 ? '+' : '';\nconst emoji = chg >= 0 ? '\ud83d\udcc8' : '\ud83d\udcc9';\nconst mcapLine = coin.usd_market_cap\n  ? `\\n\ud83c\udfe6 *Market Cap:* $${fmt(coin.usd_market_cap)}`\n  : '';\n\nconst msg = [\n  `\ud83d\udcb0 *${cmdData.displayArg || symbol.toUpperCase()} Live Price*`,\n  ``,\n  `*Price:* $${fmt(price)}`,\n  `${emoji} *24h Change:* ${sign}${chg}%`,\n  mcapLine,\n  ``,\n  `[View on CoinGecko](https://www.coingecko.com/en/coins/${symbol})`\n].join('\\n');\n\nreturn [{ json: { chatId: cmdData.chatId, replyMessage: msg } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "54c9737d-9e3c-446c-9cc3-82b5aba041bd",
      "name": "Send Price Reply on Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2000,
        2672
      ],
      "parameters": {
        "text": "={{ $json.replyMessage }}",
        "chatId": "={{ $json.chatId }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "426ecefc-4776-46a2-86aa-58d04f12c50f",
      "name": "Send Unknown Command Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1328,
        2832
      ],
      "parameters": {
        "text": "\u2753 *Unknown command.*\n\n*Available commands:*\n\u2022 `/price <coin>` \u2014 Get live price\n\n*Examples:*\n`/price bitcoin`\n`/price btc`\n`/price eth`\n\n*Supported tickers:*\nbtc, eth, sol, ada, dot, matic, avax, link, doge, xrp, bnb, uni, atom, ltc",
        "chatId": "={{ $json.chatId }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f6af5329-e258-4ea5-aece-26573230e6bb",
      "name": "\ud83d\udcf1 Telegram Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4016,
        112
      ],
      "parameters": {
        "color": 3,
        "width": 420,
        "height": 444,
        "content": "## \ud83d\udcf1 Telegram Setup\n\n**Create a bot:**\n1. Telegram \u2192 @BotFather \u2192 `/newbot`\n2. Follow prompts \u2192 copy the API token\n\n**Get your Chat ID:**\n1. Message @userinfobot on Telegram\n2. Copy the numeric Chat ID it returns\n3. For groups: add bot \u2192 use group Chat ID (negative number)\n\n**Connect in n8n:**\n\u2022 Open any Telegram node\n\u2022 Credential \u2192 Create New \u2192 paste bot token\n\u2022 Reuse the same credential on all 4 Telegram nodes:\n  - Send Telegram Alert\n  - Send Daily Digest\n  - Send Price Reply\n  - Send Unknown Command"
      },
      "typeVersion": 1
    },
    {
      "id": "8aa007dd-9a22-48d0-a8ac-25b2e738b6fd",
      "name": "\u2699\ufe0f Watchlist Guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        32
      ],
      "parameters": {
        "color": 3,
        "width": 352,
        "height": 448,
        "content": "## \u2699\ufe0f Watchlist Config\n\nThe **only node you need to edit** to get started.\n\n**Asset fields:**\n\u2022 `symbol`: CoinGecko ID (lowercase)\n  e.g. `bitcoin`, `ethereum`, `solana`\n  Full list \u2192 coingecko.com/en/coins\n\u2022 `display`: Label shown in alerts (`BTC`)\n\u2022 `threshold`: USD price trigger\n\u2022 `direction`: `'above'` or `'below'`\n\u2022 `cooldownMinutes`: Min wait between alerts\n\u2022 `holdings`: Coins owned for portfolio value\n  (Set `0` to disable)\n\u2022 `priceChangePct`: Alert on \u00b1N% move\n  (Set `0` to disable)\n\u2022 `useRSI`: RSI filter toggle (`true`/`false`)\n\u2022 `rsiOversold` / `rsiOverbought`: Default: 30/70\n\n\u26a0\ufe0f **CoinGecko Free Tier:** ~30 req/min\nDefault hourly schedule is safe for 10+ assets."
      },
      "typeVersion": 1
    },
    {
      "id": "017f4353-f3a9-47dc-95c4-57aedf14fe1c",
      "name": "\ud83e\udd16 Telegram Command Bot",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        2176
      ],
      "parameters": {
        "width": 1200,
        "height": 320,
        "content": "## \ud83e\udd16 Telegram Command Bot Branch\n\nYour bot responds to commands sent directly in Telegram chat.\n\n**Available commands:**\n\u2022 `/price bitcoin` or `/price btc` \u2192 instant live price + 24h change + market cap\n\n**Supported ticker aliases:**\nbtc, eth, sol, ada, dot, matic, avax, link, doge, xrp, bnb, uni, atom, ltc\n\n**How it works:** The Telegram Trigger node creates a webhook when the workflow is activated. n8n receives the message, parses the command, fetches a live price, and replies instantly.\n\n\u26a0\ufe0f One webhook per bot token. Activating this workflow sets the webhook URL automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "66eada17-657a-4ef4-be43-8b1b575db114",
      "name": "\ud83d\udcc5 Daily Digest Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        1408
      ],
      "parameters": {
        "width": 1200,
        "height": 256,
        "content": "## \ud83d\udcc5 Daily Digest Branch\n\nFires every day at **8:00 AM UTC**.\nSends one Telegram message summarising all watched assets: prices, 24h changes, portfolio values, total portfolio, and the **Crypto Fear & Greed Index**.\n\n**To configure:**\n1. Open **Daily Digest Config** node\n2. Set `TELEGRAM_CHAT_ID`\n3. Keep `watchlist` array in sync with **Watchlist Config**\n\n**Fear & Greed API:** `api.alternative.me/fng` (free, no key). If unreachable, digest sends without it."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "0b3f087a-167e-405a-9256-db780a7cee83",
  "connections": {
    "Compute RSI": {
      "main": [
        [
          {
            "node": "Check Alert Conditions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hourly Trigger": {
      "main": [
        [
          {
            "node": "Configure Watchlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alert Needed": {
      "main": [
        [
          {
            "node": "If Alert Cooldown Over",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Price Without Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch RSI Values": {
      "main": [
        [
          {
            "node": "Compute RSI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Asset State": {
      "main": [
        [
          {
            "node": "If First Time Running",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Assets": {
      "main": [
        [],
        [
          {
            "node": "Fetch Crypto Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily 8am Trigger": {
      "main": [
        [
          {
            "node": "Configure Daily Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Market Data": {
      "main": [
        [
          {
            "node": "If Price Data Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Alert Status": {
      "main": [
        [
          {
            "node": "Loop Over Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Watchlist": {
      "main": [
        [
          {
            "node": "Loop Over Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Crypto Prices": {
      "main": [
        [
          {
            "node": "Merge Market Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Initial Price": {
      "main": [
        [
          {
            "node": "Loop Over Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Alert Message": {
      "main": [
        [
          {
            "node": "Send Alert on Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Specific Price": {
      "main": [
        [
          {
            "node": "Create Price Reply Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Command is /price": {
      "main": [
        [
          {
            "node": "Fetch Specific Price",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Unknown Command Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Price Data Exists": {
      "main": [
        [
          {
            "node": "Load Asset State",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If First Time Running": {
      "main": [
        [
          {
            "node": "Store Initial Price",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch RSI Values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Alert to Sheets": {
      "main": [
        [
          {
            "node": "Save Alert Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Alert Conditions": {
      "main": [
        [
          {
            "node": "If Alert Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Daily Digest": {
      "main": [
        [
          {
            "node": "Retrieve All Crypto Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Fear & Greed Index": {
      "main": [
        [
          {
            "node": "Build Daily Digest Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alert Cooldown Over": {
      "main": [
        [
          {
            "node": "Create Alert Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Price Without Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Alert on Telegram": {
      "main": [
        [
          {
            "node": "Append Alert to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decode Telegram Message": {
      "main": [
        [
          {
            "node": "If Command is /price",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Price Without Alert": {
      "main": [
        [
          {
            "node": "Loop Over Assets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Daily Digest Message": {
      "main": [
        [
          {
            "node": "Send Digest on Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Price Reply Message": {
      "main": [
        [
          {
            "node": "Send Price Reply on Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve All Crypto Prices": {
      "main": [
        [
          {
            "node": "Get Fear & Greed Index",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Telegram Command Received": {
      "main": [
        [
          {
            "node": "Decode Telegram Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}