{
  "id": "4mIHotRjUv96W7uM",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Personal Financial Digital Twin",
  "tags": [],
  "nodes": [
    {
      "id": "03b935a1-dfcc-4a2d-8c00-715077021f52",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        704,
        0
      ],
      "parameters": {
        "width": 1020,
        "height": 1012,
        "content": "## Personal Financial Digital Twin\n\nThis workflow builds a Monte Carlo \"digital twin\" of the user's real financial life, then stress-tests it against a chosen life event (buy a house, lose a job, or both) using their actual linked account balances and real spending patterns - not hypothetical numbers.\n\n### Who's it for\n- Anyone who wants to know \"can I actually afford this\" before signing a mortgage or before a layoff wave hits their industry\n- Fintech apps or advisors who want a real-accounts-based stress test instead of a generic calculator\n- Personal finance tools that want to move from static budgeting to probabilistic planning\n\n### How it works\n1. A form captures the life event to test (Buy a House / Lose a Job / Both) plus its parameters (home price, down payment %, job-loss duration, simulation horizon)\n2. Two APIs are called: the user's linked account balances & recent transactions (modeled on a Plaid-style aggregator), and historical market return statistics (mean/volatility) to drive realistic random returns\n3. JS - Build Financial Baseline & Simulation Inputs turns raw account/transaction data into a clean baseline: net worth, monthly income, monthly expenses, and emergency-fund runway\n4. JS - Run Monte Carlo Simulation runs 1,000 randomized month-by-month simulations over the chosen horizon, applying the life event's cash-flow shocks and random market returns each month, tracking ending net worth and whether the balance ever goes negative\n5. AI turns the raw percentiles and shortfall probability into a plain-English narrative, a risk level, and concrete recommendations\n6. The full digital-twin report is emailed to the user - before they make the decision, not after\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to the Anthropic model node\n3. Add your account-aggregator API credentials (e.g. Plaid) to Fetch Linked Account Balances & Transactions and replace YOUR_PLAID_CLIENT_ID / YOUR_PLAID_SECRET / YOUR_PLAID_ACCESS_TOKEN\n4. Add your market-data API key to Fetch Historical Market Return Statistics and replace YOUR_MARKET_DATA_API_KEY\n5. Add SMTP credentials to the Send Email node and replace the from/to addresses\n6. Activate the workflow and open the form URL to run a simulation\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- An account-aggregator API (Plaid, Teller, MX, or similar) for real balances and transactions\n- A market-data API for historical return mean/volatility\n- SMTP credentials for report delivery\n\n### How to customize\n- Adjust ITERATIONS or the mortgage-rate assumption in JS - Run Monte Carlo Simulation\n- Add more life-event types (have a baby, start a business, retire early) as additional cash-flow shock branches\n- Log every simulation run to a Google Sheet or database for a historical view of the user's risk trend over time\n- Add a chart image (via a charting API) embedded in the email showing the P10/P50/P90 net worth fan chart"
      },
      "typeVersion": 1
    },
    {
      "id": "6de7e4ca-a4da-4c44-a8ec-18dca9a5fba4",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1872,
        64
      ],
      "parameters": {
        "color": 3,
        "width": 1300,
        "height": 760,
        "content": "## Stage 1: Real Account Data & Baseline\n\nForm - Request Life Event Simulation captures which event to test and its parameters.\n\nFetch Linked Account Balances & Transactions (API 1) and Fetch Historical Market Return Statistics (API 2) run in parallel off the form submission.\n\nMerge - Combine Account & Market Data joins both API responses into a single item.\n\nJS - Build Financial Baseline & Simulation Inputs (Code Node 1) computes net worth, monthly income/expenses from real transactions, emergency-fund runway, and packages the chosen life event plus market statistics into clean simulation inputs."
      },
      "typeVersion": 1
    },
    {
      "id": "897d87fa-e9db-44ad-9e0f-9b7d8310eb15",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3264,
        64
      ],
      "parameters": {
        "color": 4,
        "width": 900,
        "height": 760,
        "content": "## Stage 2: Monte Carlo Simulation Engine\n\nJS - Run Monte Carlo Simulation (Code Node 2) runs 1,000 randomized month-by-month paths over the simulation horizon. Each month applies the life event's cash-flow shock (job-loss income drop, or house down payment + new mortgage payment) plus a randomly sampled market return drawn from the real historical mean/volatility.\n\nIt tracks each path's ending net worth and whether the balance ever dipped below zero, then summarizes the results as P10/P50/P90 ending net worth and a shortfall probability.\n\nWait - Simulation Buffer (Wait Node 1) pauses briefly before the AI call to respect API rate limits."
      },
      "typeVersion": 1
    },
    {
      "id": "dfd1e510-4561-46b2-9471-f3b2c9df87ad",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4368,
        64
      ],
      "parameters": {
        "color": 5,
        "width": 892,
        "height": 760,
        "content": "## Stage 3: AI Narrative & Delivery\n\nThe narrative AI (claude-sonnet-4, temperature 0.3) turns the raw simulation percentiles and shortfall probability into a plain-English summary, a risk level (Low/Moderate/High/Severe), and three concrete recommendations.\n\nJS - Parse AI Output & Format Digital Twin Report (Code Node 3) extracts the structured JSON safely, with a fallback that builds a rule-based summary from the shortfall probability if AI parsing fails, and assembles the full report body.\n\nWait - Rate Limit (Wait Node 2) pauses briefly before delivery, then Send Digital Twin Report Email delivers the full report - baseline, simulated outcomes, risk level, and recommendations - to the user."
      },
      "typeVersion": 1
    },
    {
      "id": "30fc6478-d072-4f10-94c0-2286900baa52",
      "name": "Form - Request Life Event Simulation",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        1968,
        528
      ],
      "parameters": {
        "options": {},
        "formTitle": "=",
        "formFields": {
          "values": [
            {
              "fieldType": "dropdown",
              "fieldLabel": "Event Type",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Buy a House"
                  },
                  {
                    "option": "Lose a Job"
                  },
                  {
                    "option": "Both"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "number",
              "fieldLabel": "Home Price"
            },
            {
              "fieldType": "number",
              "fieldLabel": "Down Payment %"
            },
            {
              "fieldType": "number",
              "fieldLabel": "Job Loss Duration Months"
            },
            {
              "fieldType": "number",
              "fieldLabel": "Simulation Horizon Months",
              "requiredField": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "db7c94ee-8577-4242-89d3-9c53acbc1c11",
      "name": "Fetch Linked Account Balances & Transactions",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2288,
        384
      ],
      "parameters": {
        "url": "https://production.plaid.com/accounts/balance/get",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"client_id\": \"YOUR_PLAID_CLIENT_ID\",\n  \"secret\": \"YOUR_PLAID_SECRET\",\n  \"access_token\": \"YOUR_PLAID_ACCESS_TOKEN\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "8315c1fc-c808-4384-a889-747f6ed26d1e",
      "name": "Fetch Historical Market Return Statistics",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2288,
        656
      ],
      "parameters": {
        "url": "https://api.marketdata-provider.example.com/v1/equities/sp500/return-stats?period=10y",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "484f723f-dcd2-4f1f-9544-a9c767f6c9df",
      "name": "Merge - Combine Account & Market Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        2624,
        528
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "b575984a-9720-420b-8788-3ac4e91cbc79",
      "name": "JS - Build Financial Baseline & Simulation Inputs",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst formData = $('Form - Request Life Event Simulation').first().json;\n\nconst accounts = item.accounts || [];\nconst transactions = item.recentTransactions || [];\n\nconst totalAssets = accounts\n  .filter(a => ['checking', 'savings', 'investment'].includes((a.type || '').toLowerCase()))\n  .reduce((sum, a) => sum + (parseFloat(a.balance) || 0), 0);\n\nconst totalLiabilities = accounts\n  .filter(a => ['credit', 'loan'].includes((a.type || '').toLowerCase()))\n  .reduce((sum, a) => sum + Math.abs(parseFloat(a.balance) || 0), 0);\n\nconst netWorth = Math.round((totalAssets - totalLiabilities) * 100) / 100;\n\n// Recent transactions sampled over roughly 3 months; positive amount = income, negative = expense\nconst monthlyIncome = Math.round(\n  (transactions.filter(t => (parseFloat(t.amount) || 0) > 0)\n    .reduce((sum, t) => sum + (parseFloat(t.amount) || 0), 0) / 3) * 100\n) / 100;\n\nconst monthlyExpenses = Math.round(\n  (Math.abs(transactions.filter(t => (parseFloat(t.amount) || 0) < 0)\n    .reduce((sum, t) => sum + (parseFloat(t.amount) || 0), 0)) / 3) * 100\n) / 100;\n\nconst emergencyFundMonths = monthlyExpenses > 0 ? Math.round((totalAssets / monthlyExpenses) * 10) / 10 : null;\n\nconst marketMeanMonthlyReturn = item.meanMonthlyReturn ?? 0.006;\nconst marketStdDevMonthlyReturn = item.stdDevMonthlyReturn ?? 0.04;\n\nconst lifeEvent = {\n  type: formData['Event Type'] || 'Custom',\n  homePrice: parseFloat(formData['Home Price']) || 0,\n  downPaymentPct: parseFloat(formData['Down Payment %']) || 20,\n  jobLossDurationMonths: parseInt(formData['Job Loss Duration Months'], 10) || 0\n};\n\nconst horizonMonths = parseInt(formData['Simulation Horizon Months'], 10) || 60;\n\nreturn {\n  json: {\n    netWorth, totalAssets, totalLiabilities,\n    monthlyIncome, monthlyExpenses, emergencyFundMonths,\n    marketMeanMonthlyReturn, marketStdDevMonthlyReturn,\n    lifeEvent, horizonMonths\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "662638d6-22e8-4092-a37d-38a6ace6b0ff",
      "name": "JS - Run Monte Carlo Simulation",
      "type": "n8n-nodes-base.code",
      "position": [
        3344,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst {\n  netWorth, monthlyIncome, monthlyExpenses,\n  marketMeanMonthlyReturn, marketStdDevMonthlyReturn,\n  lifeEvent, horizonMonths\n} = item;\n\nconst ITERATIONS = 1000;\n\nfunction randomNormal(mean, stdDev) {\n  let u = 0, v = 0;\n  while (u === 0) u = Math.random();\n  while (v === 0) v = Math.random();\n  const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);\n  return mean + z * stdDev;\n}\n\nconst endingNetWorths = [];\nlet shortfallCount = 0;\n\nfor (let i = 0; i < ITERATIONS; i++) {\n  let balance = netWorth;\n  let ranOutOfMoney = false;\n\n  for (let month = 1; month <= horizonMonths; month++) {\n    let income = monthlyIncome;\n    let expenses = monthlyExpenses;\n\n    if ((lifeEvent.type === 'Lose a Job' || lifeEvent.type === 'Both') && month <= (lifeEvent.jobLossDurationMonths || 0)) {\n      income = 0;\n    }\n\n    if (lifeEvent.type === 'Buy a House' || lifeEvent.type === 'Both') {\n      if (month === 1) {\n        const downPayment = (lifeEvent.homePrice || 0) * ((lifeEvent.downPaymentPct || 20) / 100);\n        balance -= downPayment;\n      }\n      const loanAmount = (lifeEvent.homePrice || 0) * (1 - (lifeEvent.downPaymentPct || 20) / 100);\n      const monthlyMortgage = loanAmount > 0 ? (loanAmount * 0.005) : 0; // approx principal+interest at ~6%/yr\n      expenses += monthlyMortgage;\n    }\n\n    const marketReturn = randomNormal(marketMeanMonthlyReturn, marketStdDevMonthlyReturn);\n    balance = balance * (1 + marketReturn) + (income - expenses);\n\n    if (balance < 0) ranOutOfMoney = true;\n  }\n\n  endingNetWorths.push(Math.round(balance * 100) / 100);\n  if (ranOutOfMoney) shortfallCount++;\n}\n\nendingNetWorths.sort((a, b) => a - b);\nconst percentile = (p) => endingNetWorths[Math.floor((p / 100) * (endingNetWorths.length - 1))];\n\nconst simulation = {\n  iterations: ITERATIONS,\n  horizonMonths,\n  p10EndingNetWorth: percentile(10),\n  p50EndingNetWorth: percentile(50),\n  p90EndingNetWorth: percentile(90),\n  shortfallProbabilityPct: Math.round((shortfallCount / ITERATIONS) * 1000) / 10\n};\n\nreturn { json: { ...item, simulation } };"
      },
      "typeVersion": 2
    },
    {
      "id": "69a9b074-2981-4e30-868b-ff8ad170a5f7",
      "name": "AI - Generate Risk Narrative & Recommendations",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3888,
        528
      ],
      "parameters": {
        "text": "=You are a personal financial planning assistant reviewing a Monte Carlo stress test run against a user's real linked accounts.\n\nCurrent Baseline:\nNet Worth: ${{ $json.netWorth }}\nMonthly Income: ${{ $json.monthlyIncome }}\nMonthly Expenses: ${{ $json.monthlyExpenses }}\nEmergency Fund Coverage: {{ $json.emergencyFundMonths }} months\n\nLife Event Tested: {{ $json.lifeEvent.type }}\nHome Price: {{ $json.lifeEvent.homePrice }} | Down Payment: {{ $json.lifeEvent.downPaymentPct }}% | Job Loss Duration: {{ $json.lifeEvent.jobLossDurationMonths }} months\nSimulation Horizon: {{ $json.horizonMonths }} months across {{ $json.simulation.iterations }} Monte Carlo runs\n\nSimulated Outcomes:\nP10 (pessimistic) ending net worth: ${{ $json.simulation.p10EndingNetWorth }}\nP50 (median) ending net worth: ${{ $json.simulation.p50EndingNetWorth }}\nP90 (optimistic) ending net worth: ${{ $json.simulation.p90EndingNetWorth }}\nProbability of running out of money at some point: {{ $json.simulation.shortfallProbabilityPct }}%\n\nWrite a plain-English narrative a non-expert could understand, assign one risk level, and give three short, concrete, actionable recommendations.\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"narrativeSummary\": \"3-4 sentences, plain English, specific to these numbers\",\n  \"riskLevel\": \"One of: Low, Moderate, High, Severe\",\n  \"topRecommendations\": [\"short actionable recommendation 1\", \"short actionable recommendation 2\", \"short actionable recommendation 3\"]\n}\n\nReturn ONLY the JSON. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "27ae6f59-2fb6-474f-83f8-06628d7dcc87",
      "name": "Anthropic - Narrative Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        3760,
        672
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.3
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ec635561-b330-4200-9bee-790032cf72b2",
      "name": "JS - Parse AI Output & Format Digital Twin Report",
      "type": "n8n-nodes-base.code",
      "position": [
        4448,
        528
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  const pct = item.simulation.shortfallProbabilityPct;\n  const risk = pct >= 25 ? 'Severe' : pct >= 10 ? 'High' : pct >= 3 ? 'Moderate' : 'Low';\n  parsed = {\n    narrativeSummary: `Simulating ${item.lifeEvent.type} over ${item.horizonMonths} months against your real accounts shows a ${pct}% chance your balance goes negative at some point. Median ending net worth is $${item.simulation.p50EndingNetWorth}.`,\n    riskLevel: risk,\n    topRecommendations: [\n      'Build a larger cash buffer before taking on this life event.',\n      'Reassess monthly expenses to create more breathing room.',\n      'Re-run this simulation after adjusting the event parameters.'\n    ]\n  };\n}\n\nconst reportGeneratedAt = new Date().toISOString();\nconst recommendationsText = (parsed.topRecommendations || [])\n  .map((r, i) => `${i + 1}. ${r}`)\n  .join('\\n');\n\nconst reportBody = `Personal Financial Digital Twin Report\\nGenerated: ${reportGeneratedAt}\\n\\nLife Event Tested: ${item.lifeEvent.type}\\nSimulation Horizon: ${item.horizonMonths} months (${item.simulation.iterations} Monte Carlo runs)\\n\\nCurrent Net Worth: $${item.netWorth}\\nMonthly Income: $${item.monthlyIncome} | Monthly Expenses: $${item.monthlyExpenses}\\nEmergency Fund Coverage: ${item.emergencyFundMonths} months\\n\\nSimulated Outcomes:\\n- Pessimistic (P10) Ending Net Worth: $${item.simulation.p10EndingNetWorth}\\n- Median (P50) Ending Net Worth: $${item.simulation.p50EndingNetWorth}\\n- Optimistic (P90) Ending Net Worth: $${item.simulation.p90EndingNetWorth}\\n- Probability of Running Out of Money: ${item.simulation.shortfallProbabilityPct}%\\n\\nRisk Level: ${parsed.riskLevel}\\n\\n${parsed.narrativeSummary}\\n\\nRecommendations:\\n${recommendationsText}`;\n\nreturn {\n  json: {\n    ...item,\n    ...parsed,\n    reportGeneratedAt,\n    reportBody\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "cf520f3c-0eb8-4cc2-9a43-c2039e5d3e0c",
      "name": "Send Digital Twin Report Email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        5056,
        528
      ],
      "parameters": {
        "options": {},
        "subject": "=Your Financial Digital Twin Report - Risk Level: {{ $json.riskLevel }}",
        "toEmail": "user@example.com",
        "fromEmail": "user@example.com"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "87264ae1-1b29-4c52-b56f-cca2f1f8db66",
      "name": "Wait For  Rate Limit",
      "type": "n8n-nodes-base.wait",
      "position": [
        4768,
        528
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "1d192753-871d-4c1a-8d3c-345542157c98",
      "name": "Wait For Simulation Buffer",
      "type": "n8n-nodes-base.wait",
      "position": [
        3600,
        528
      ],
      "parameters": {},
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "dbd5b762-9342-44c6-ad96-2ca7ea441bb7",
  "connections": {
    "Wait For  Rate Limit": {
      "main": [
        [
          {
            "node": "Send Digital Twin Report Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Simulation Buffer": {
      "main": [
        [
          {
            "node": "AI - Generate Risk Narrative & Recommendations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anthropic - Narrative Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Risk Narrative & Recommendations",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "JS - Run Monte Carlo Simulation": {
      "main": [
        [
          {
            "node": "Wait For Simulation Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Form - Request Life Event Simulation": {
      "main": [
        [
          {
            "node": "Fetch Linked Account Balances & Transactions",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Historical Market Return Statistics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Combine Account & Market Data": {
      "main": [
        [
          {
            "node": "JS - Build Financial Baseline & Simulation Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Historical Market Return Statistics": {
      "main": [
        [
          {
            "node": "Merge - Combine Account & Market Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch Linked Account Balances & Transactions": {
      "main": [
        [
          {
            "node": "Merge - Combine Account & Market Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Risk Narrative & Recommendations": {
      "main": [
        [
          {
            "node": "JS - Parse AI Output & Format Digital Twin Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Build Financial Baseline & Simulation Inputs": {
      "main": [
        [
          {
            "node": "JS - Run Monte Carlo Simulation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse AI Output & Format Digital Twin Report": {
      "main": [
        [
          {
            "node": "Wait For  Rate Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}