{
  "nodes": [
    {
      "parameters": {
        "content": "### Step 3: Save to Google Sheet\n⚙️ Add **Google Sheets OAuth2** credential and select your spreadsheet.\n⚙️ Add Sheet ID and Sheet name (Transactions)\n",
        "height": 800,
        "width": 816,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14576,
        1584
      ],
      "typeVersion": 1,
      "id": "ad44b778-5e23-41e6-85dc-949ed2c965aa",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "### Step 2: AI Categorization\n⚙️ Add **Google Gemini API** credential.",
        "height": 800,
        "width": 816,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        13664,
        1584
      ],
      "typeVersion": 1,
      "id": "79fab9e1-5b55-4229-889e-5196fa135e7f",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "### Step 1: Receive & Parse\nNo configuration needed.",
        "height": 800,
        "width": 464,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        13104,
        1584
      ],
      "typeVersion": 1,
      "id": "ab6d348c-0ab7-452f-8d70-4dab1adeebd4",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## 📱 Auto Expense Tracker with FlowTrigger\n\nAutomatically tracks expenses by capturing banking push notifications from your phone using the [FlowTrigger](https://flowtrigger.app) app, categorizing them with AI (Google Gemini), and logging everything to a Google Sheet.\n\n---\n\n### Setup\n\n\n**1. Add credentials to `Google Gemini Chat Model:**\n Add your Google Gemini API key ([Get one here](https://aistudio.google.com/apikey))\n\n\n**2. Connect your spreadsheet**\n- Copy the Google Sheets Template -> [Copy](https://docs.google.com/spreadsheets/d/1SINQnhu4slpNsVZ-CF8LqhTDKpqPqS1gukQcFlRWJXs/copy)\n- Open `Google Sheets` node :\n   - Connect Google Sheets OAuth2 account\n   - Select the copied spreadsheet → select the **Transactions** sheet\n\n\n**3. Get the Webhook URL**\nActivate this workflow → open the `Webhook` node → copy the **Production URL**\n\n\n**4. Set up FlowTrigger**\n- Install [FlowTrigger](https://play.google.com/store/apps/details?id=app.flowtrigger) on your Android phone\n- Create a **Notification Trigger**\n- Paste the Webhook URL\n- Select your banking app (e.g. Google Wallet, Revolut, your bank) as the notification source\n- Done! Expenses are now tracked automatically 🎉\n\n---\n\n### How it works\n1. Your banking app sends a payment notification → FlowTrigger captures it and sends the data to this workflow\n2. The workflow extracts the amount and date, then AI categorizes the merchant and expense type\n3. The categorized expense is saved to your Google Sheet\n\nFor more details visit [flowtrigger.app](https://flowtrigger.app)\"\n",
        "height": 1168,
        "width": 512
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        12464,
        1456
      ],
      "typeVersion": 1,
      "id": "bd6d4772-5d3e-4344-b543-3a7dd2abb42a",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "expenses",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "58a0cd06-63bc-4527-8021-43c817f25c97",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        13168,
        1840
      ],
      "webhookId": "template-webhook-id"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: \"error\", source: \"google_sheets\", message: ($json.error ? ($json.error.message || JSON.stringify($json.error)) : \"Failed to write transaction to Google Sheets.\") }) }}",
        "options": {
          "responseCode": 422
        }
      },
      "id": "b4a7508e-bbff-408c-a599-37d08bfa48c3",
      "name": "Respond Error Sheets",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        14944,
        1984
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: \"success\", data: { merchant: $('Categorize Expense').item.json.merchant_cleaned, category: $('Categorize Expense').item.json.category, amount: $('Extract Amount').item.json.extractedAmount, currency: $('Categorize Expense').item.json.currency, date: $('Extract Amount').item.json.formattedDate } }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "4a7a96db-2ece-4a03-962b-a687bba3e209",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        14944,
        1792
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "<__PLACEHOLDER_VALUE__Google Sheets Document ID__>",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "<__PLACEHOLDER_VALUE__Sheet Tab Name__>",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Date time": "={{ DateTime.fromISO($('Webhook').item.json.body.notificationPostTime).toFormat('yyyy-MM-dd HH:mm:ss') }}",
            "Merchant": "={{ $('Categorize Expense').item.json.merchant_cleaned }}",
            "Amount": "={{ $('Extract Amount').item.json.extractedAmount }}",
            "Currency": "={{ $('Categorize Expense').item.json.currency }}",
            "Category": "={{ $('Categorize Expense').item.json.category }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Date time",
              "displayName": "Date time",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Merchant",
              "displayName": "Merchant",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Amount",
              "displayName": "Amount",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Currency",
              "displayName": "Currency",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Category",
              "displayName": "Category",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "cellFormat": "USER_ENTERED"
        }
      },
      "id": "188598f3-5816-4313-aac8-8f24b9320642",
      "name": "Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        14640,
        1808
      ],
      "retryOnFail": true,
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "hn3KdzNsDtLTRZVh",
          "name": "Google Sheets account"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: \"error\", source: \"categorization\", message: ($json.error ? ($json.error.message || JSON.stringify($json.error)) : \"AI categorization failed. The LLM did not return a valid response.\") }) }}",
        "options": {
          "responseCode": 422
        }
      },
      "id": "b8026dae-525c-4cba-8c9f-6d7bddbff182",
      "name": "Respond Error LLM",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        14320,
        1984
      ]
    },
    {
      "parameters": {
        "jsonSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"merchant_cleaned\": {\n      \"type\": \"string\",\n      \"description\": \"The clean name of the brand or store.\"\n    },\n    \"reasoning\": {\n      \"type\": \"string\",\n      \"description\": \"Short explanation of the business and why the category was chosen.\"\n    },\n    \"category\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"Groceries\",\n        \"Health\",\n        \"Transport\",\n        \"Dining\",\n        \"Utilities\",\n        \"Shopping\",\n        \"Entertainment\",\n        \"Other\"\n      ]\n    },\n    \"currency\": {\n      \"type\": \"string\",\n      \"description\": \"The standard 3-letter ISO currency code (e.g., PLN, EUR, USD) found in the text or inferred from the country.\"\n    }\n  },\n  \"required\": [\"merchant_cleaned\", \"reasoning\", \"category\", \"currency\"]\n}"
      },
      "id": "ffe9eff5-db4e-47ff-b8df-2df03570cb51",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1,
      "position": [
        14112,
        2016
      ]
    },
    {
      "parameters": {
        "modelName": "models/gemini-flash-lite-latest",
        "options": {}
      },
      "id": "355092b8-36ca-406a-bace-32f9463f48b4",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "typeVersion": 1,
      "position": [
        13936,
        2016
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=You are a precise financial data extraction bot.\n\nMerchant Text: {{ $json.body.notificationTitle }}\nContext Text: {{ $json.body.notificationBody }}\n\nSTEP 1 - ANALYZE PAYLOAD:\n- Read full notification body and title content. You should only look for expense notifications. Sometimes banking apps send informational notifications to users. Sometimes notification is about money return or other income - look for clues in the content. If there is any sign that the notification might not be an expense ignore it. You should only analyze payment related notifications, ignore the rest. Expense notifications often have just merchant in title and price + card details in body.\n\nSTEP 2 — IDENTIFY THE MERCHANT:\n- Strip store numbers, location suffixes, and branch codes from the Merchant Text to extract the clean brand or business name.\n- Determine what kind of business this merchant is. If it is a well-known retail chain, supermarket, restaurant, pharmacy, gas station, etc., use your knowledge of that business to choose the category.\n\nSTEP 3 — DETERMINE REGION:\n- Infer the country from the language and currency in Context Text.\n\nSTEP 4 — CATEGORIZE:\n- Map to EXACTLY ONE category:🛒 Groceries,🏥 Health,🚗 Transport,🍽️ Dining,⚡ Utilities,🛍️ Shopping,👠 Entertainment,📦 Other.\n- Supermarkets and discount grocery stores are \"Groceries\", not \"Dining\" — even if they also sell prepared food.\n- If you do not recognize the merchant, reason carefully from the name and regional context. Do NOT guess based on partial word translations.\n\nSTEP 5 — CURRENCY:\n- Output the 3-letter ISO currency code.",
        "hasOutputParser": true
      },
      "id": "5a0b3b0e-1366-4cb2-9655-7a2d7d8e1c7c",
      "name": "Categorize Expense",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.4,
      "position": [
        13952,
        1824
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: \"skipped\", message: \"No price detected. Non-expense notification ignored.\" }) }}",
        "options": {
          "responseCode": 200
        }
      },
      "id": "cc4d51d8-033c-4b1d-95bf-1a72f68182de",
      "name": "Respond Skipped",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        13936,
        2224
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 1
          },
          "conditions": [
            {
              "id": "condition_amount_exists",
              "leftValue": "={{ $json.extractedAmount }}",
              "rightValue": "",
              "operator": {
                "type": "number",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "99adf765-8e60-4d04-b7c2-7e4c01ae25b0",
      "name": "Is Expense?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        13696,
        1840
      ]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfor (const item of items) {\n  const payload = item.json.body || item.json;\n  const bodyText = payload.notificationBody || \"\";\n\n  const amountMatch = bodyText.match(/[\\d]+[.,][\\d]{2}/);\n  item.json.extractedAmount = amountMatch ? parseFloat(amountMatch[0].replace(',', '.')) : null;\n}\n\nreturn items;"
      },
      "id": "c8bbc8aa-e9bf-4821-a747-0b30adfbeb4e",
      "name": "Extract Amount",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        13408,
        1840
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Extract Amount",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Sheets": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Error Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Categorize Expense",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Categorize Expense",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Categorize Expense": {
      "main": [
        [
          {
            "node": "Google Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Error LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Expense?": {
      "main": [
        [
          {
            "node": "Categorize Expense",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Amount": {
      "main": [
        [
          {
            "node": "Is Expense?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "9ff1816e41534aeef40950b5f08aa9012da174683ffab0ec046294b4752686a4"
  }
}