{
  "name": "Telegram AI Expense Tracker",
  "nodes": [
    {
      "parameters": {
        "content": "## Telegram AI Expense Tracker: Track Expenses via Bot to Google Sheets\n\nTrack expenses by sending simple messages to a Telegram bot (like 'Lunch $25'), have AI extract the details, and save everything to a Google Sheet for analysis.\n\n**Setup time:** 15 minutes  ·  **Level:** Intermediate\n\n**You'll need accounts/credentials for:** Telegram, OpenAI, Google Sheets\n\n**Replace these before running:**\n- `REPLACE_WITH_BOT_TOKEN`\n- `REPLACE_WITH_SPREADSHEET`\n\n**Setup steps:**\n1. Connect Telegram, OpenAI, and Google Sheets in their respective nodes.\n2. Replace REPLACE_WITH_BOT_TOKEN with your Telegram bot token.\n3. Replace REPLACE_WITH_SPREADSHEET with your Google Sheet ID.\n4. Create a Google Sheet with columns: Date, Amount, Category, Merchant, Note.\n5. Set up your Telegram bot webhook to point to this workflow's Telegram Trigger URL.\n\nFree templates and updates: navkhan103.github.io",
        "height": 300,
        "width": 400,
        "color": 4
      },
      "id": "sticky-setup",
      "name": "Setup — read me first",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        -120
      ]
    },
    {
      "parameters": {
        "content": "### How it runs\n\nStarts from **Telegram Trigger**.\n\n1. A Telegram Trigger listens for messages sent to your bot.\n2. An OpenAI node extracts amount, category, and merchant from the message text.\n3. A Code node validates the extracted data and formats it for Sheets.\n4. A Google Sheets node appends the expense to your expense tracking spreadsheet.\n5. The bot sends a confirmation message back to the user.",
        "height": 300,
        "width": 400,
        "color": 7
      },
      "id": "sticky-how",
      "name": "How it runs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        220
      ]
    },
    {
      "parameters": {
        "content": "### If something breaks\n\nThe read and AI nodes here **retry 3× automatically** (5s apart), which absorbs rate limits and brief timeouts.\n\nNodes that **send or write deliberately do not retry** — a send that reached the far end but lost its response would otherwise fire twice, so you'd get duplicate messages or duplicate rows.\n\nTo be told when one of those fails, build a workflow starting with an **Error Trigger** node that posts to Slack or email, then set it under **three-dot menu > Settings > Error Workflow**. Do it once and point every workflow at it.",
        "height": 380,
        "width": 400,
        "color": 3
      },
      "id": "sticky-fail",
      "name": "If something breaks",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -440,
        560
      ]
    },
    {
      "parameters": {
        "botToken": "REPLACE_WITH_BOT_TOKEN",
        "additionalFields": {}
      },
      "id": "f9a0b1c2-0001-4000-8000-000000000001",
      "name": "Telegram Trigger",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.6-luna"
        },
        "messages": {
          "values": [
            {
              "content": "=Extract expense details from this message. Return only valid JSON with keys: amount (number), category (string), merchant (string), note (string). Example: {\"amount\": 25.5, \"category\": \"Food\", \"merchant\": \"Starbucks\", \"note\": \"Morning coffee\"}\n\nMessage: {{ $json.message.text }}",
              "role": "user"
            }
          ]
        }
      },
      "id": "f9a0b1c2-0002-4000-8000-000000000002",
      "name": "Extract Expense",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        220,
        0
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "parameters": {
        "jsCode": "// Parse the AI response and validate data\ntry {\n  const aiResponse = $input.first().json.message.content;\n  const expense = JSON.parse(aiResponse);\n  \n  // Validate required fields\n  if (typeof expense.amount !== 'number' || isNaN(expense.amount)) {\n    throw new Error('Invalid amount');\n  }\n  \n  // Set default values if missing\n  expense.category = expense.category || 'Uncategorized';\n  expense.merchant = expense.merchant || 'Unknown';\n  expense.note = expense.note || $('Telegram Trigger').first().json.message.text;\n  \n  // Format for Google Sheets\n  const now = new Date();\n  return [{ json: {\n    date: now.toISOString().split('T')[0],\n    amount: expense.amount,\n    category: expense.category,\n    merchant: expense.merchant,\n    note: expense.note,\n    raw_message: $('Telegram Trigger').first().json.message.text\n  } }];\n} catch (error) {\n  // If parsing fails, log the original message with error flag\n  return [{ json: {\n    date: new Date().toISOString().split('T')[0],\n    amount: 0,\n    category: 'ERROR',\n    merchant: 'PARSE_FAILED',\n    note: `Error: ${error.message}`,\n    raw_message: $('Telegram Trigger').first().json.message.text\n  } }];\n}"
      },
      "id": "f9a0b1c2-0003-4000-8000-000000000003",
      "name": "Validate Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "REPLACE_WITH_SPREADSHEET"
        },
        "sheetName": "Expenses",
        "columns": "date,amount,category,merchant,note,raw_message"
      },
      "id": "f9a0b1c2-0004-4000-8000-000000000004",
      "name": "Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "operation": "sendMessage",
        "chatId": "={{ $('Telegram Trigger').first().json.message.chat.id }}",
        "text": "={{ $json.amount ? `✅ Expense recorded: $${$json.amount} for ${$json.merchant} (${ $json.category })` : `❌ Could not parse expense. Please try format like: 'Lunch $25 at Restaurant'`}}",
        "additionalFields": {}
      },
      "id": "f9a0b1c2-0005-4000-8000-000000000005",
      "name": "Confirm to User",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        880,
        0
      ]
    }
  ],
  "connections": {
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Extract Expense",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Expense": {
      "main": [
        [
          {
            "node": "Validate Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Data": {
      "main": [
        [
          {
            "node": "Log to Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Confirm to User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}