{
  "id": "ahonmOot7AiUYfQ9",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Filter Blacklist phone numers",
  "tags": [],
  "nodes": [
    {
      "id": "2c9eb45c-ed81-4017-8c34-60792c02158d",
      "name": "Manual Trigger \u2014 Test Execution",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -960,
        256
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "cd3046e0-d1fb-4756-9271-37422f97c1c4",
      "name": "Set \u2014 Mock WhatsApp Input",
      "type": "n8n-nodes-base.set",
      "position": [
        -720,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4a32b9fe-7334-4364-a370-dfecf2c40d94",
              "name": "phoneNumber",
              "type": "string",
              "value": "+1234567890"
            },
            {
              "id": "5fe13ec8-e7d7-4702-9931-0ec57a950ebf",
              "name": "message",
              "type": "string",
              "value": "Hi, how are you?"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "c9b27906-c3bf-4fb2-8a9f-cc1519218ff8",
      "name": "Code \u2014 Sanitize Phone Number",
      "type": "n8n-nodes-base.code",
      "position": [
        -496,
        128
      ],
      "parameters": {
        "jsCode": "// Get incoming data from the previous mock node\nconst inputData = $input.first().json;\nconst rawPhone = inputData.phoneNumber || \"\";\n\n// Strip away any non-numeric characters using a clean regex string\nconst cleanInputPhone = rawPhone.replace(/\\D/g, '');\n\nreturn [{\n  json: {\n    phoneNumber: rawPhone,\n    cleanPhone: cleanInputPhone,\n    message: inputData.message || \"\"\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7a14ab3f-aaa0-46ec-9ec9-b36fe0b5ea16",
      "name": "Google Sheets \u2014 Fetch Blacklist",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -720,
        384
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/156U8ozH0yvaz4pIDfLWvPN5b9NcHGYQTTU7frXiC47o/edit#gid=0",
          "cachedResultName": "Blacklist"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/156U8ozH0yvaz4pIDfLWvPN5b9NcHGYQTTU7frXiC47o/edit?usp=sharing"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.7,
      "waitBetweenTries": 2000
    },
    {
      "id": "be0b3b8a-6d25-421a-9ee3-8792c95aca6c",
      "name": "Merge \u2014 Match Blacklist",
      "type": "n8n-nodes-base.merge",
      "position": [
        -176,
        256
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "720002de-8fb8-492f-9c5d-6d559187adc3",
      "name": "Code \u2014 Sanitize Blacklist Rows",
      "type": "n8n-nodes-base.code",
      "position": [
        -496,
        384
      ],
      "parameters": {
        "jsCode": "// Grab all rows coming from the Google Sheets node\nconst items = $input.all();\n\n// Map and sanitize each row into a clean array structure\nconst blacklistArray = items.map(item => {\n  const rawPhone = item.json.phone || \"\";\n  const cleanPhone = rawPhone.replace(/\\D/g, '');\n  \n  return {\n    row_number: item.json.row_number,\n    originalPhone: rawPhone,\n    cleanPhone: cleanPhone,\n    reason: item.json.reason || \"Blocked\"\n  };\n});\n\n// Output as a single n8n item with the flat array inside\nreturn [{\n  json: {\n    blacklistData: blacklistArray\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d02e8a51-55b0-4a24-8c68-48b7c5557f57",
      "name": "Code \u2014 Evaluate Blacklist Status",
      "type": "n8n-nodes-base.code",
      "position": [
        32,
        256
      ],
      "parameters": {
        "jsCode": "// Grab all incoming items from the Merge node\nconst allItems = $input.all();\n\n// Extract the user data (Item 0) and the blacklist array (Item 1)\nconst userData = allItems[0]?.json || {};\nconst blacklistContainer = allItems[1]?.json || {};\nconst blacklistArray = blacklistContainer.blacklistData || [];\n\nconst userCleanPhone = userData.cleanPhone;\n\n// Check if the user's phone exists inside the blacklist array\nconst matchedBlock = blacklistArray.find(b => b.cleanPhone === userCleanPhone);\n\n// Prepare the output structure\nlet isBlocked = false;\nlet blockReason = \"N/A\";\n\nif (matchedBlock) {\n  isBlocked = true;\n  blockReason = matchedBlock.reason || \"Blocked by Administrator\";\n}\n\n// Return a SINGLE clean item for the rest of the workflow to use\nreturn [{\n  json: {\n    phoneNumber: userData.phoneNumber,\n    cleanPhone: userCleanPhone,\n    message: userData.message,\n    isBlocked: isBlocked,\n    blockReason: blockReason\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2504e499-65b6-4bc1-9707-60e12da2a017",
      "name": "If \u2014 is Blocked?",
      "type": "n8n-nodes-base.if",
      "position": [
        256,
        256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "63ab5a8b-2759-4162-bd1c-dd2fc98130ba",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.isBlocked }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "8c157d6f-5c71-43b4-b662-00439fbe29fe",
      "name": "Code \u2014 Mock Review Flood (DELETE IN PRODUCTION)",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        640
      ],
      "parameters": {
        "jsCode": "// ACCESS N8N FLOW MEMORY\nconst staticData = $getWorkflowStaticData('global');\nconst _msgLimit = $input.first().json.limitThreshold\n\n// Initialize if it doesn't exist\nif (!staticData.rateLimitLogs) {\n  staticData.rateLimitLogs = [];\n}\n\nconst currentTime = Date.now();\n\n// SIMULATION: If the memory is fresh, we inject 31 interactions \n// spanning across the last 45 seconds to force a rate-limit breach.\nif (staticData.rateLimitLogs.length === 0) {\n  for (let i = 0; i < _msgLimit+1; i++) {\n    staticData.rateLimitLogs.push(currentTime - (i * 1000));\n  }\n}\n\n// Pass the incoming user payload straight to the next node untouched\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "a1289843-04c5-422d-adfd-1d3580e32f56",
      "name": "Code \u2014 Rate Limiter Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        640
      ],
      "parameters": {
        "jsCode": "// ACCESS N8N FLOW MEMORY\nconst staticData = $getWorkflowStaticData('global');\n\nif (!staticData.rateLimitLogs) {\n  staticData.rateLimitLogs = [];\n}\n\nconst currentTime = Date.now();\nconst oneMinuteAgo = currentTime - 60000;\n\n// 1. ADD CURRENT TRANSACTION\nstaticData.rateLimitLogs.push(currentTime);\n\n// 2. AUTOMATIC CLEANUP (Garbage Collector for obsolete entries)\nstaticData.rateLimitLogs = staticData.rateLimitLogs.filter(timestamp => timestamp > oneMinuteAgo);\n\n// 3. EVALUATE ACTIVE METRICS\nconst activeRequestCount = staticData.rateLimitLogs.length;\nconst rateLimitExceeded = activeRequestCount > $input.first().json.limitThreshold;\n\n// Output clean structural payload\nreturn [{\n  json: {\n    phoneNumber: $input.first().json.phoneNumber,\n    cleanPhone: $input.first().json.cleanPhone,\n    message: $input.first().json.message,\n    rateLimit: {\n      currentCount: activeRequestCount,\n      limitThreshold: $input.first().json.limitThreshold,\n      timeWindow: \"60s\",\n      isExceeded: rateLimitExceeded\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8ebeb8a1-aa05-4ad2-9975-f2cf398efe7d",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1456,
        -32
      ],
      "parameters": {
        "width": 496,
        "height": 720,
        "content": "## WhatsApp Blacklist & RateLimit Filter \u2014 Block Numbers Before AI Processing\n\nBlocks incoming WhatsApp messages from blacklisted phone numbers and prevents spam through rate limiting before they reach AI processing. Keeps your AI assistant safe from known malicious actors and excessive traffic.\n\n### How it works\n1. Receives WhatsApp message via manual trigger (for testing)\n2. Sanitizes and cleans the phone number\n3. Fetches blacklist from Google Sheets and cross-references the number\n4. If not blocked, evaluates rate limit (30 msgs/min threshold)\n5. Blocked numbers stop execution; valid messages continue to AI processing\n\n### How to use\n1. Connect your WhatsApp trigger to replace the manual trigger\n2. Set up Google Sheets with a **Blacklist** sheet (columns: `phone`, `reason`)\n3. Configure your WhatsApp credentials\n4. Adjust `limitThreshold` in **Set \u2014 Max messages per minute** if needed\n5. Route the \"continue\" path to your AI assistant\n\n### Customization\n- Replace Google Sheets with any database (Airtable, PostgreSQL, etc.)\n- Add auto-reply nodes for blocked and rate-limited users\n- Connect Grafana or logging DB for security monitoring\n- Adjust rate limit window by changing `60000` (ms) in **Code \u2014 Rate Limiter Engine**\n\n**Author:** Luis Rendon"
      },
      "typeVersion": 1
    },
    {
      "id": "732ea4fc-4481-4ed9-a423-1b061cc0c514",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        576
      ],
      "parameters": {
        "color": 3,
        "height": 240,
        "content": "## Delete me\nSimulate flood messages"
      },
      "typeVersion": 1
    },
    {
      "id": "b33d94b1-3c46-4bc8-863a-d890c54a6e3d",
      "name": "If \u2014 Rate Limit Exceeded?",
      "type": "n8n-nodes-base.if",
      "position": [
        880,
        640
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "120a1160-c866-4627-95d0-7d60e9043562",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.rateLimit.isExceeded }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "e2ae630e-b511-4e9d-8ee6-a99cb4045c4f",
      "name": "Set \u2014 Production Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        1088,
        880
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ef2bb899-40c9-4c8e-ac0b-e1f364ad4b37",
              "name": "phoneNumber",
              "type": "string",
              "value": "={{ $json.phoneNumber }}"
            },
            {
              "id": "457ce000-81f8-4cd1-bb6c-9e23b0aa8842",
              "name": "cleanPhone",
              "type": "string",
              "value": "={{ $json.cleanPhone }}"
            },
            {
              "id": "56a5b887-fe88-47a4-b1ac-a15c71b1548a",
              "name": "message",
              "type": "string",
              "value": "={{ $json.message }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6fc8d3c9-5168-4d2c-80be-3f9d3767ee03",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        -128
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 464,
        "content": "### \u26d4 Blacklist Path\nThis node stops execution because the number matches the Google Sheets blocklist.\n\n**Production Upgrades:**\n* **Auto-Response:** Replace with a WhatsApp/Telegram node (e.g., *\"Access Denied\"*).\n* **Audit Logs:** Route to a `blocked_attempts` database to track malicious users.\n* **Admin Alert:** Connect a Slack/Email node to notify your team instantly."
      },
      "typeVersion": 1
    },
    {
      "id": "cc67347e-427f-4138-b13a-1ea7be2a25e7",
      "name": "NoOp \u2014 Blocked by Blacklist",
      "type": "n8n-nodes-base.noOp",
      "position": [
        464,
        160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "72add07e-8966-40f8-aada-bc6bbe96e198",
      "name": "NoOp \u2014 Blocked by Rate Limit",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1104,
        544
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c69913ab-769c-4232-8386-246b25e4fceb",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1040,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 336,
        "height": 512,
        "content": "### \u23f1\ufe0f Rate Limit Path\nThis node stops execution because the sender exceeded the 30 messages/min threshold.\n\n**Production Upgrades:**\n* **Cooldown Alert:** Replace with a messaging node (e.g., *\"Too many messages, try again in 1 minute\"*).\n* **System Metrics:** Connect to Grafana or a DB to monitor API stress or scraping attempts.\n* **Security Lock:** Route persistent abusers to automatically add them to the permanent Google Sheets blocklist."
      },
      "typeVersion": 1
    },
    {
      "id": "78a0eb58-d350-4e59-9357-0448f4058a41",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -768,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 256,
        "content": "### Fetch blacklist items and sanitize"
      },
      "typeVersion": 1
    },
    {
      "id": "a44aa95d-18c0-4160-b7df-c78ae592af04",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -768,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 256,
        "content": "### Get user message and sanitize phone number"
      },
      "typeVersion": 1
    },
    {
      "id": "ddf3f2f7-94d7-41d1-93e2-c8b22a79a904",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        368
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 464,
        "content": "### Rate limit guardian"
      },
      "typeVersion": 1
    },
    {
      "id": "52f5de23-a425-44de-bc5e-2d4cc4689fba",
      "name": "Set \u2014 Max messages per minute",
      "type": "n8n-nodes-base.set",
      "position": [
        464,
        416
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4a2662ea-9ce6-47fe-8215-d1f534dc95e7",
              "name": "limitThreshold",
              "type": "number",
              "value": 30
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "d5ba4fac-befc-41c7-8e40-2a2f0a996e26",
  "connections": {
    "If \u2014 is Blocked?": {
      "main": [
        [
          {
            "node": "NoOp \u2014 Blocked by Blacklist",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set \u2014 Max messages per minute",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge \u2014 Match Blacklist": {
      "main": [
        [
          {
            "node": "Code \u2014 Evaluate Blacklist Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If \u2014 Rate Limit Exceeded?": {
      "main": [
        [
          {
            "node": "NoOp \u2014 Blocked by Rate Limit",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set \u2014 Production Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set \u2014 Mock WhatsApp Input": {
      "main": [
        [
          {
            "node": "Code \u2014 Sanitize Phone Number",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Rate Limiter Engine": {
      "main": [
        [
          {
            "node": "If \u2014 Rate Limit Exceeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Sanitize Phone Number": {
      "main": [
        [
          {
            "node": "Merge \u2014 Match Blacklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set \u2014 Max messages per minute": {
      "main": [
        [
          {
            "node": "Code \u2014 Mock Review Flood (DELETE IN PRODUCTION)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Sanitize Blacklist Rows": {
      "main": [
        [
          {
            "node": "Merge \u2014 Match Blacklist",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Google Sheets \u2014 Fetch Blacklist": {
      "main": [
        [
          {
            "node": "Code \u2014 Sanitize Blacklist Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger \u2014 Test Execution": {
      "main": [
        [
          {
            "node": "Set \u2014 Mock WhatsApp Input",
            "type": "main",
            "index": 0
          },
          {
            "node": "Google Sheets \u2014 Fetch Blacklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Evaluate Blacklist Status": {
      "main": [
        [
          {
            "node": "If \u2014 is Blocked?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code \u2014 Mock Review Flood (DELETE IN PRODUCTION)": {
      "main": [
        [
          {
            "node": "Code \u2014 Rate Limiter Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}