{
  "name": "Track AI agent costs and decisions with webhooks and WhatsApp alerts",
  "nodes": [
    {
      "id": "4ae734ca-88b9-4f35-9d15-869892b92f81",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2160,
        -256
      ],
      "parameters": {
        "width": 1344,
        "height": 2248,
        "content": "## AI Agent Execution Ledger\n\nRecords every tool call, LLM call, decision and error from any agent (or Supervisor workflow). Each event is priced, stored with a sequence number, and rolled into per-run cost / token / error summaries. Budget overruns trigger a one-time WhatsApp alert. A report endpoint + daily digest give cost & activity by agent, tool, model and decision.\n\n### Pipeline\n```\nPOST /agent-ledger-event  →  Validate & Price  →  Append to Ledger  →  (optional) Budget Alert\nGET  /agent-ledger-report →  Aggregate report  →  JSON or WhatsApp digest\n```\n\n### Setup checklist\n1. Add WhatsApp Business credentials to both WhatsApp nodes.\n2. Edit **Ledger Config**: run budget, pricing-per-1M-tokens, retention limits, WhatsApp IDs.\n3. Edit **Report Config**: digest window, recent-entry limit, WhatsApp IDs.\n4. Activate the workflow (static data only persists on active / production runs).\n5. No Anthropic credential is required – this workflow only records data.\n\n### Send events\n`POST /webhook/agent-ledger-event`\n```json\n{\n  \"events\": [\n    {\n      \"runId\": \"run-1\",\n      \"agentName\": \"researcher\",\n      \"eventType\": \"llm_call\",\n      \"model\": \"claude-sonnet-4-6\",\n      \"inputTokens\": 1800,\n      \"outputTokens\": 420,\n      \"latencyMs\": 2300\n    },\n    {\n      \"runId\": \"run-1\",\n      \"agentName\": \"researcher\",\n      \"eventType\": \"tool_call\",\n      \"toolName\": \"web_search\",\n      \"toolCostUsd\": 0.01,\n      \"status\": \"ok\"\n    },\n    {\n      \"runId\": \"run-1\",\n      \"agentName\": \"supervisor\",\n      \"eventType\": \"decision\",\n      \"decision\": \"route:writer\",\n      \"reasoning\": \"research done\",\n      \"confidence\": 0.9\n    }\n  ]\n}\n```\nA single event object (without the `events` array) also works.\n\n### Read the ledger\n`GET /webhook/agent-ledger-report`  \nOptional query params: `?runId=`, `?agent=`, `?since=<ISO date>`, `?limit=`.\n\n### Hook into a Supervisor workflow\nAfter each specialist output / Claude call, add an HTTP Request node that POSTs the agent name, usage tokens and decision to the event webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "aee99576-5914-497c-ab5e-583b77109096",
      "name": "Sticky Note - Record Events",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 1608,
        "height": 880,
        "content": "## 1. Record Events\n\n**Purpose**  \nValidate incoming events, calculate cost (tokens × model rate + tool cost, or explicit `costUsd`), append them to the persistent ledger, update per-run summaries, and fire a one-time WhatsApp alert if a run exceeds its budget.\n\n**Key nodes**\n- Receive Ledger Event (POST webhook)\n- Ledger Config\n- Validate and Price Events\n- If Events Are Valid\n- Append to Ledger\n- Respond with Recorded Summary / Validation Error\n- If Budget Alert Needed → Send Budget Alert via WhatsApp"
      },
      "typeVersion": 1
    },
    {
      "id": "7fe0d987-c5aa-4b8b-b611-902ff5cbd6db",
      "name": "Sticky Note - Report & Digest",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3584,
        816
      ],
      "parameters": {
        "color": 7,
        "width": 1560,
        "height": 992,
        "content": "## 2. Report & Daily Digest\n\n**Purpose**  \nAggregate the ledger by agent, tool, model and decision. Return a full JSON report via webhook or send a WhatsApp digest for the last 24 hours (or configured window).\n\n**Key nodes**\n- Receive Report Request (GET webhook)\n- Daily Digest Schedule (cron 09:00)\n- Report Config\n- Build Ledger Report\n- If Request Came From Webhook\n- Respond with Ledger Report\n- Send Daily Digest via WhatsApp"
      },
      "typeVersion": 1
    },
    {
      "id": "197868f8-6530-4342-b271-eb198179e850",
      "name": "Receive Ledger Event",
      "type": "n8n-nodes-base.webhook",
      "position": [
        3648,
        192
      ],
      "parameters": {
        "path": "agent-ledger-event",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "c2daab3f-67c6-4205-8200-2117fc9243be",
      "name": "Ledger Config",
      "type": "n8n-nodes-base.set",
      "position": [
        3872,
        192
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ff4ff9f4-1b75-4784-8f78-9a679caab17a",
              "name": "runBudgetUsd",
              "type": "number",
              "value": 0.5
            },
            {
              "id": "cb0543ac-86fd-489a-be51-41e4943c0cf5",
              "name": "maxEntries",
              "type": "number",
              "value": 5000
            },
            {
              "id": "7c4f8b74-d86b-4206-a0de-9923104cfcb4",
              "name": "maxRuns",
              "type": "number",
              "value": 200
            },
            {
              "id": "598589a9-d4b3-4d6c-95c8-10c4f6a07862",
              "name": "pricingPerMillionUsd",
              "type": "string",
              "value": "{\"default\": {\"input\": 3, \"output\": 15}, \"claude-haiku\": {\"input\": 1, \"output\": 5}, \"claude-sonnet\": {\"input\": 3, \"output\": 15}, \"claude-opus\": {\"input\": 5, \"output\": 25}}"
            },
            {
              "id": "f416ef34-696d-46ba-bb0a-14db98c7f3c0",
              "name": "whatsappBusinessPhoneId",
              "type": "string",
              "value": "your-whatsapp-phone-number-id"
            },
            {
              "id": "65370fc3-e35b-4876-8ecd-909c13e46ba7",
              "name": "whatsappRecipientPhone",
              "type": "string",
              "value": "+1234567890"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d6ea9a51-28c5-4fd1-91c4-4e17dc057a6b",
      "name": "Validate and Price Events",
      "type": "n8n-nodes-base.code",
      "position": [
        4096,
        192
      ],
      "parameters": {
        "jsCode": "const cfg = $input.first().json;\nconst body = cfg.body || {};\n\nlet pricing;\ntry { pricing = JSON.parse(cfg.pricingPerMillionUsd); } catch (e) { pricing = { default: { input: 3, output: 15 } }; }\n\nconst TYPES = ['tool_call', 'decision', 'llm_call', 'error'];\nconst raw = Array.isArray(body.events) ? body.events : (body.eventType ? [body] : []);\nconst errors = [];\nconst events = [];\nconst num = v => (Number(v) > 0 ? Number(v) : 0);\nconst preview = v => v === undefined || v === null ? null : (typeof v === 'string' ? v : JSON.stringify(v)).slice(0, 500);\n\nfunction rateFor(model) {\n  const keys = Object.keys(pricing)\n    .filter(k => k !== 'default' && String(model || '').startsWith(k))\n    .sort((a, b) => b.length - a.length);\n  return pricing[keys[0]] || pricing.default || { input: 0, output: 0 };\n}\n\nif (raw.length === 0) errors.push('Body must contain an events array or a single event with eventType');\n\nraw.forEach((e, i) => {\n  const p = 'events[' + i + ']';\n  if (!e || typeof e !== 'object') { errors.push(p + ' must be an object'); return; }\n  if (!e.runId) errors.push(p + ' is missing runId');\n  if (!TYPES.includes(e.eventType)) errors.push(p + ' eventType must be one of ' + TYPES.join(', '));\n  if (e.eventType === 'tool_call' && !e.toolName) errors.push(p + ' tool_call requires toolName');\n  if (e.eventType === 'decision' && !e.decision) errors.push(p + ' decision requires decision');\n\n  const inTok = num(e.inputTokens), outTok = num(e.outputTokens), cacheTok = num(e.cacheReadTokens);\n  const r = rateFor(e.model);\n  const cacheRate = r.cacheRead !== undefined ? r.cacheRead : (r.input || 0) * 0.1;\n  const tokenCost = inTok / 1e6 * (r.input || 0) + outTok / 1e6 * (r.output || 0) + cacheTok / 1e6 * cacheRate;\n  const explicit = e.costUsd !== undefined && Number(e.costUsd) >= 0 ? Number(e.costUsd) : null;\n  const costUsd = explicit !== null ? explicit : tokenCost + num(e.toolCostUsd);\n\n  events.push({\n    runId: e.runId,\n    taskId: e.taskId || null,\n    agentName: e.agentName || 'unknown',\n    eventType: e.eventType,\n    eventTimestamp: e.timestamp || new Date().toISOString(),\n    toolName: e.toolName || null,\n    toolInput: preview(e.toolInput),\n    toolOutput: preview(e.toolOutput),\n    model: e.model || null,\n    decision: e.decision || null,\n    reasoning: e.reasoning ? String(e.reasoning).slice(0, 1000) : null,\n    confidence: typeof e.confidence === 'number' ? e.confidence : null,\n    status: e.status || (e.eventType === 'error' ? 'failed' : 'ok'),\n    errorMessage: e.errorMessage ? String(e.errorMessage).slice(0, 500) : null,\n    inputTokens: inTok,\n    outputTokens: outTok,\n    cacheReadTokens: cacheTok,\n    latencyMs: num(e.latencyMs),\n    costUsd: Math.round(costUsd * 1e6) / 1e6,\n    metadata: e.metadata && typeof e.metadata === 'object' ? e.metadata : {}\n  });\n});\n\nreturn [{\n  json: {\n    valid: errors.length === 0,\n    validationError: errors.join('; '),\n    events,\n    runBudgetUsd: Number(cfg.runBudgetUsd) || 0,\n    maxEntries: Number(cfg.maxEntries) || 5000,\n    maxRuns: Number(cfg.maxRuns) || 200,\n    whatsappBusinessPhoneId: cfg.whatsappBusinessPhoneId,\n    whatsappRecipientPhone: cfg.whatsappRecipientPhone\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1d65953f-adb7-44d7-9662-074559fcdccc",
      "name": "If Events Are Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        4320,
        192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.valid }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ee093af8-e4e8-47bd-9d14-5e0ee4bedd49",
      "name": "Respond with Validation Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "onError": "continueRegularOutput",
      "position": [
        4544,
        288
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ error: $json.validationError }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "066d7234-ad82-4c02-9a24-c951e06c8620",
      "name": "Append to Ledger",
      "type": "n8n-nodes-base.code",
      "position": [
        4544,
        96
      ],
      "parameters": {
        "jsCode": "const s = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\nif (!sd.ledger) sd.ledger = { seq: 0, entries: [], runs: {} };\nconst L = sd.ledger;\nconst now = new Date().toISOString();\nconst touched = [];\nconst alerts = [];\nlet seqFrom = null;\n\nfor (const ev of s.events) {\n  L.seq += 1;\n  if (seqFrom === null) seqFrom = L.seq;\n  L.entries.push({ seq: L.seq, id: 'led-' + L.seq, recordedAt: now, ...ev });\n\n  const r = L.runs[ev.runId] || (L.runs[ev.runId] = {\n    runId: ev.runId, taskId: ev.taskId, agents: {}, totalCostUsd: 0,\n    inputTokens: 0, outputTokens: 0, llmCalls: 0, toolCalls: 0, decisions: 0, errors: 0,\n    firstEventAt: ev.eventTimestamp, lastEventAt: ev.eventTimestamp, budgetAlerted: false\n  });\n  r.agents[ev.agentName] = (r.agents[ev.agentName] || 0) + 1;\n  r.totalCostUsd = Math.round((r.totalCostUsd + ev.costUsd) * 1e6) / 1e6;\n  r.inputTokens += ev.inputTokens;\n  r.outputTokens += ev.outputTokens;\n  if (ev.eventType === 'llm_call') r.llmCalls += 1;\n  if (ev.eventType === 'tool_call') r.toolCalls += 1;\n  if (ev.eventType === 'decision') r.decisions += 1;\n  if (ev.eventType === 'error' || ev.status !== 'ok') r.errors += 1;\n  r.lastEventAt = ev.eventTimestamp;\n  if (!touched.includes(ev.runId)) touched.push(ev.runId);\n\n  if (s.runBudgetUsd > 0 && r.totalCostUsd > s.runBudgetUsd && !r.budgetAlerted) {\n    r.budgetAlerted = true;\n    alerts.push('Run ' + ev.runId + ' spent $' + r.totalCostUsd.toFixed(4) + ' (budget $' + s.runBudgetUsd.toFixed(2) + ') after ' + (r.llmCalls + r.toolCalls) + ' calls. Last agent: ' + ev.agentName);\n  }\n}\n\n// Retention\nif (L.entries.length > s.maxEntries) L.entries = L.entries.slice(-s.maxEntries);\nconst ids = Object.keys(L.runs);\nif (ids.length > s.maxRuns) {\n  ids.sort((a, b) => String(L.runs[a].lastEventAt).localeCompare(String(L.runs[b].lastEventAt)))\n     .slice(0, ids.length - s.maxRuns)\n     .forEach(id => { delete L.runs[id]; });\n}\n\nconst runs = touched.filter(id => L.runs[id]).map(id => {\n  const r = L.runs[id];\n  return {\n    runId: r.runId, totalCostUsd: r.totalCostUsd, inputTokens: r.inputTokens, outputTokens: r.outputTokens,\n    llmCalls: r.llmCalls, toolCalls: r.toolCalls, decisions: r.decisions, errors: r.errors,\n    budgetUsd: s.runBudgetUsd || null, budgetExceeded: s.runBudgetUsd > 0 && r.totalCostUsd > s.runBudgetUsd\n  };\n});\n\nreturn [{\n  json: {\n    budgetAlert: alerts.length > 0,\n    whatsappText: '\\uD83D\\uDEA8 Agent Ledger budget alert\\n\\n' + alerts.join('\\n'),\n    whatsappBusinessPhoneId: s.whatsappBusinessPhoneId,\n    whatsappRecipientPhone: s.whatsappRecipientPhone,\n    webhookResponse: { recorded: s.events.length, seqFrom, seqTo: L.seq, ledgerSize: L.entries.length, runs }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "363e1461-25e0-49f6-99aa-62401d3f67b0",
      "name": "Respond with Recorded Summary",
      "type": "n8n-nodes-base.respondToWebhook",
      "onError": "continueRegularOutput",
      "position": [
        4768,
        0
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json.webhookResponse) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "ff4f4028-0f2c-45b2-9f02-5b25a2e4c5e5",
      "name": "If Budget Alert Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        4768,
        192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.budgetAlert }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "48805d59-90bd-4667-900d-a3ffd7ae48fd",
      "name": "Send Budget Alert via WhatsApp",
      "type": "n8n-nodes-base.whatsApp",
      "onError": "continueRegularOutput",
      "position": [
        4992,
        192
      ],
      "parameters": {
        "textBody": "={{ $json.whatsappText }}",
        "operation": "send",
        "phoneNumberId": "={{ $json.whatsappBusinessPhoneId }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $json.whatsappRecipientPhone }}"
      },
      "typeVersion": 1
    },
    {
      "id": "21c83852-1930-4003-99e2-8b970959dc53",
      "name": "Receive Report Request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        3664,
        1328
      ],
      "parameters": {
        "path": "agent-ledger-report",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "5c451aae-10a6-4594-8af6-879dc532c1d2",
      "name": "Daily Digest Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3648,
        1568
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "619d293c-2a73-4737-ba8e-3b937cd85813",
      "name": "Report Config",
      "type": "n8n-nodes-base.set",
      "position": [
        3968,
        1424
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "13000ea5-75dd-4ce1-aa9e-996334eb2ffe",
              "name": "digestWindowHours",
              "type": "number",
              "value": 24
            },
            {
              "id": "1d1135c7-fb9d-4946-9b57-7da2a60664e7",
              "name": "recentLimit",
              "type": "number",
              "value": 20
            },
            {
              "id": "f61b4bcd-3354-435e-aae4-d0bd0a3aec4a",
              "name": "whatsappBusinessPhoneId",
              "type": "string",
              "value": "your-whatsapp-phone-number-id"
            },
            {
              "id": "40749873-67d7-4af5-9298-393f62c12236",
              "name": "whatsappRecipientPhone",
              "type": "string",
              "value": "+1234567890"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "0e08c406-9b3d-475b-ba4c-ed59c7f27552",
      "name": "Build Ledger Report",
      "type": "n8n-nodes-base.code",
      "position": [
        4272,
        1424
      ],
      "parameters": {
        "jsCode": "const c = $input.first().json;\nconst isWebhook = !!c.headers;\nconst q = c.query || {};\nconst sd = $getWorkflowStaticData('global');\nconst L = sd.ledger || { seq: 0, entries: [], runs: {} };\nconst round = n => Math.round(n * 1e6) / 1e6;\n\nlet entries = L.entries;\nlet windowLabel = 'all recorded history';\nif (!isWebhook) {\n  const hours = Number(c.digestWindowHours) || 24;\n  const cutoff = Date.now() - hours * 3600000;\n  entries = entries.filter(e => new Date(e.recordedAt).getTime() >= cutoff);\n  windowLabel = 'last ' + hours + 'h';\n}\nif (q.runId) entries = entries.filter(e => e.runId === q.runId);\nif (q.agent) entries = entries.filter(e => e.agentName === q.agent);\nif (q.since) entries = entries.filter(e => e.recordedAt >= q.since);\n\nconst group = keyFn => {\n  const m = {};\n  for (const e of entries) {\n    const k = keyFn(e);\n    if (k === null || k === undefined) continue;\n    const g = m[k] || (m[k] = { name: k, events: 0, costUsd: 0, inputTokens: 0, outputTokens: 0, errors: 0, totalLatencyMs: 0 });\n    g.events += 1;\n    g.costUsd += e.costUsd;\n    g.inputTokens += e.inputTokens;\n    g.outputTokens += e.outputTokens;\n    g.totalLatencyMs += e.latencyMs;\n    if (e.eventType === 'error' || e.status !== 'ok') g.errors += 1;\n  }\n  return Object.values(m)\n    .map(({ totalLatencyMs, ...g }) => ({ ...g, costUsd: round(g.costUsd), avgLatencyMs: g.events ? Math.round(totalLatencyMs / g.events) : 0 }))\n    .sort((a, b) => b.costUsd - a.costUsd);\n};\n\nconst byAgent = group(e => e.agentName);\nconst byTool = group(e => e.eventType === 'tool_call' ? e.toolName : null);\nconst byModel = group(e => e.model);\nconst topRuns = group(e => e.runId).slice(0, 5);\n\nconst dm = {};\nfor (const e of entries.filter(x => x.eventType === 'decision')) {\n  const d = dm[e.decision] || (dm[e.decision] = { decision: e.decision, count: 0, confidenceSum: 0, confidenceN: 0 });\n  d.count += 1;\n  if (typeof e.confidence === 'number') { d.confidenceSum += e.confidence; d.confidenceN += 1; }\n}\nconst byDecision = Object.values(dm)\n  .map(d => ({ decision: d.decision, count: d.count, avgConfidence: d.confidenceN ? Math.round(d.confidenceSum / d.confidenceN * 100) / 100 : null }))\n  .sort((a, b) => b.count - a.count);\n\nconst totals = {\n  events: entries.length,\n  costUsd: round(entries.reduce((a, e) => a + e.costUsd, 0)),\n  inputTokens: entries.reduce((a, e) => a + e.inputTokens, 0),\n  outputTokens: entries.reduce((a, e) => a + e.outputTokens, 0),\n  llmCalls: entries.filter(e => e.eventType === 'llm_call').length,\n  toolCalls: entries.filter(e => e.eventType === 'tool_call').length,\n  decisions: entries.filter(e => e.eventType === 'decision').length,\n  errors: entries.filter(e => e.eventType === 'error' || e.status !== 'ok').length\n};\ntotals.errorRate = totals.events ? Math.round(totals.errors / totals.events * 1000) / 1000 : 0;\n\nconst limit = Number(q.limit) || Number(c.recentLimit) || 20;\nconst recentEntries = entries.slice(-limit).reverse();\n\nconst report = {\n  generatedAt: new Date().toISOString(),\n  window: windowLabel,\n  filters: { runId: q.runId || null, agent: q.agent || null, since: q.since || null },\n  totals, byAgent, byTool, byModel, byDecision, topRuns, recentEntries,\n  runSummaries: q.runId && L.runs[q.runId] ? L.runs[q.runId] : undefined\n};\n\nconst top = arr => arr.length ? arr[0].name + ' ($' + arr[0].costUsd.toFixed(4) + ')' : 'n/a';\nconst whatsappText = '\\uD83D\\uDCD2 Agent Ledger digest (' + windowLabel + ')\\n'\n  + 'Events: ' + totals.events + ' | LLM calls: ' + totals.llmCalls + ' | Tool calls: ' + totals.toolCalls + ' | Decisions: ' + totals.decisions + '\\n'\n  + 'Total cost: $' + totals.costUsd.toFixed(4) + ' | Errors: ' + totals.errors + '\\n'\n  + 'Top agent: ' + top(byAgent) + '\\n'\n  + 'Top tool: ' + top(byTool) + '\\n'\n  + 'Top model: ' + top(byModel) + '\\n'\n  + 'Costliest run: ' + top(topRuns);\n\nreturn [{ json: { isWebhook, report, whatsappText, whatsappBusinessPhoneId: c.whatsappBusinessPhoneId, whatsappRecipientPhone: c.whatsappRecipientPhone } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a78102bd-6fb5-4593-ab14-9cc17cbcfd17",
      "name": "If Request Came From Webhook",
      "type": "n8n-nodes-base.if",
      "position": [
        4672,
        1456
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.isWebhook }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a7ff4733-95f3-4fa1-b720-8fd7b786f780",
      "name": "Respond with Ledger Report",
      "type": "n8n-nodes-base.respondToWebhook",
      "onError": "continueRegularOutput",
      "position": [
        4880,
        1200
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json.report) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "60afaae4-5d64-4109-aa87-baca62bff044",
      "name": "Send Daily Digest via WhatsApp",
      "type": "n8n-nodes-base.whatsApp",
      "onError": "continueRegularOutput",
      "position": [
        4896,
        1616
      ],
      "parameters": {
        "textBody": "={{ $json.whatsappText }}",
        "operation": "send",
        "phoneNumberId": "={{ $json.whatsappBusinessPhoneId }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $json.whatsappRecipientPhone }}"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Ledger Config": {
      "main": [
        [
          {
            "node": "Validate and Price Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report Config": {
      "main": [
        [
          {
            "node": "Build Ledger Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Ledger": {
      "main": [
        [
          {
            "node": "Respond with Recorded Summary",
            "type": "main",
            "index": 0
          },
          {
            "node": "If Budget Alert Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Ledger Report": {
      "main": [
        [
          {
            "node": "If Request Came From Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Events Are Valid": {
      "main": [
        [
          {
            "node": "Append to Ledger",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond with Validation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Ledger Event": {
      "main": [
        [
          {
            "node": "Ledger Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Digest Schedule": {
      "main": [
        [
          {
            "node": "Report Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Budget Alert Needed": {
      "main": [
        [
          {
            "node": "Send Budget Alert via WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive Report Request": {
      "main": [
        [
          {
            "node": "Report Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate and Price Events": {
      "main": [
        [
          {
            "node": "If Events Are Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Request Came From Webhook": {
      "main": [
        [
          {
            "node": "Respond with Ledger Report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Daily Digest via WhatsApp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}
