{
  "id": "wYBTkt01w6ZJpwim",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Sync & Play - Anti-Spoiler Alerts",
  "tags": [],
  "nodes": [
    {
      "id": "e75024c2-0966-4f93-8776-ae438412d9ed",
      "name": "README: Setup & Fixes",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        80
      ],
      "parameters": {
        "color": 2,
        "width": 560,
        "height": 632,
        "content": "# Sync & Play - Anti-Spoiler Alerts\n## How it works\n1. **Schedule**: Polls every 10 minutes during match window (\u00b12 hours of kickoff) to stay under API limits.\n2. **Validate**: Checks match timing and active status before making API calls.\n3. **Monitor**: Fetches live match data from API-Football and detects new goals for your team.\n4. **Sync**: Waits for your exact streaming delay (30-90 seconds) before sending notification.\n5. **Alert**: Delivers goal notification via email with scorer, minute, and score.\n6. **Reset**: Auto-disables tracking when match ends to prevent unnecessary polling.\n\n## Setup steps\n- [ ] Create Google Sheet with 8 required columns: User Email, Team ID, Active, Delay Seconds, Streaming Service, Match Datetime, Last Notified Goals, Last Notification\n- [ ] Get free API-Football key (100 req/day) and find your team ID\n- [ ] Connect Google Sheets OAuth2 credential in 3 nodes (Load, Update, Disable)\n- [ ] Connect Gmail OAuth2 credential in Send Alert node\n- [ ] Replace `YOUR_SPREADSHEET_ID` with your Google Sheet ID\n- [ ] Add API key to HTTP Request node header parameters\n- [ ] Set Match Datetime in ISO format (YYYY-MM-DDTHH:MM:SSZ) and Active=\"Yes\"\n- [ ] Test with manual execution during match window"
      },
      "typeVersion": 1
    },
    {
      "id": "b270eedf-2400-4223-b856-10425e858c3d",
      "name": "Sticky Note - Section 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 1336,
        "height": 340,
        "content": "### Polling & Timing Intelligence\nTriggers every 10 minutes. The Setup Validation node runs pre-flight checks. The Match Timing Window code node calculates hours until kickoff and only proceeds if within \u00b12 hours, saving 80-90% of API calls on non-match days."
      },
      "typeVersion": 1
    },
    {
      "id": "d574ef01-475b-4f4d-8eef-1e330cde97df",
      "name": "Sticky Note - Section ",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2112,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 648,
        "height": 340,
        "content": "### Data Loading & Filtering\nReads user preferences from Google Sheets. The IF node filters for Active=\"Yes\" status to ensure only active users are processed during match tracking."
      },
      "typeVersion": 1
    },
    {
      "id": "2eccd2f5-f2c9-458e-a8e8-5bb690871dcb",
      "name": "Sticky Note - Section 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2784,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 664,
        "height": 340,
        "content": "### State Management & Delivery\nLogs goal IDs to Google Sheet using User Email as row lookup. Waits for the user's exact streaming delay, then sends Gmail notification with scorer, minute, and score. Perfectly timed to your screen\u2014no spoilers."
      },
      "typeVersion": 1
    },
    {
      "id": "555a9cd5-db86-4dc1-97eb-2f753a8a885d",
      "name": "Sticky Note - Section 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3472,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 664,
        "height": 340,
        "content": "### Auto-Reset\nAfter email delivery, checks if match status is finished (FT, AET, PEN, etc). If yes, updates Google Sheet to set Active=\"No\" so polling stops automatically until the next match."
      },
      "typeVersion": 1
    },
    {
      "id": "bc10b713-18c2-40b2-bda0-3ee89c215849",
      "name": "Schedule: Every 10 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "FIXED: Changed from 2min to 10min to stay under API-Football free tier (100 req/day)",
      "position": [
        816,
        384
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 10
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "14b19593-36e7-4dfc-aa18-ab20a954691a",
      "name": "Setup Validation",
      "type": "n8n-nodes-base.code",
      "notes": "NEW: Pre-flight checks before processing",
      "position": [
        1040,
        384
      ],
      "parameters": {
        "jsCode": "try {\n  if (!$input.all().length) {\n    return [{\n      json: {\n        valid: true,\n        message: \"Initial trigger - proceeding to load preferences\"\n      }\n    }];\n  }\n\n  const items = $input.all();\n  if (!items.length) {\n    return [{\n      json: {\n        valid: false,\n        error: \"No data received from trigger\"\n      }\n    }];\n  }\n\n  return [{\n    json: {\n      valid: true,\n      message: \"Validation passed\"\n    }\n  }];\n\n} catch (error) {\n  return [{\n    json: {\n      valid: false,\n      error: `Setup validation failed: ${error.message}`\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "197f0e00-ca12-44f5-a50c-6a6ab66d605f",
      "name": "Load User Preferences",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1264,
        384
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SPREADSHEET_ID",
          "cachedResultName": "User Preferences"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "62786d3a-3592-4216-ac54-817ad07e9493",
      "name": "Filter Active Users Only",
      "type": "n8n-nodes-base.if",
      "position": [
        1472,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "active-check",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.Active }}",
              "rightValue": "Yes"
            }
          ]
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "fa58c543-163c-4802-9c83-7f7b9831b468",
      "name": "Check Match Timing Window",
      "type": "n8n-nodes-base.code",
      "notes": "NEW: Only poll API during match window (saves 80% of API calls)",
      "position": [
        1712,
        384
      ],
      "parameters": {
        "jsCode": "try {\n  const input = $input.first().json;\n  \n  // Parse match datetime\n  const matchDateStr = input['Match Datetime'];\n  if (!matchDateStr) {\n    return [{\n      json: {\n        shouldPoll: false,\n        reason: \"No Match Datetime set. Please add match time to Google Sheet.\"\n      }\n    }];\n  }\n\n  const matchTime = new Date(matchDateStr);\n  const now = new Date();\n  \n  // Calculate hours difference\n  const hoursDiff = (matchTime - now) / (1000 * 60 * 60);\n  \n\n  const shouldPoll = hoursDiff >= -2 && hoursDiff <= 2;\n  \n  if (!shouldPoll) {\n    return [{\n      json: {\n        shouldPoll: false,\n        reason: `Match window not active. Match is ${Math.abs(hoursDiff).toFixed(1)}h ${hoursDiff > 0 ? 'in future' : 'in past'}`,\n        nextCheck: matchTime.toISOString()\n      }\n    }];\n  }\n  \n  return [{\n    json: {\n      shouldPoll: true,\n      ...input\n    }\n  }];\n\n} catch (error) {\n  return [{\n    json: {\n      shouldPoll: false,\n      error: `Match timing check failed: ${error.message}`\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "443231da-081b-44e9-8570-6eae9a3c0fb6",
      "name": "Should Poll API?",
      "type": "n8n-nodes-base.if",
      "position": [
        1936,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.shouldPoll }}",
              "value2": true
            }
          ],
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "bfd68c50-c8fc-4bff-87a1-a4c385ab9696",
      "name": "API-Football: Get Live Match",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2160,
        384
      ],
      "parameters": {
        "url": "=https://v3.football.api-sports.io/fixtures?live=all&team={{ $json['Team ID'] }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-rapidapi-key",
              "value": "YOUR_API_FOOTBALL_KEY"
            },
            {
              "name": "x-rapidapi-host",
              "value": "v3.football.api-sports.io"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "faa5d7d7-3890-4634-bf23-4a9c89726e87",
      "name": "Detect New Goals",
      "type": "n8n-nodes-base.code",
      "notes": "FIXED: Error handling, type safety, null checks",
      "position": [
        2384,
        384
      ],
      "parameters": {
        "jsCode": "try {\n  const apiResponse = $input.item.json;\n  const response = apiResponse.response;\n  \n  // Validate API response\n  if (!response || !Array.isArray(response) || response.length === 0) {\n    return [];\n  }\n\n  const match = response[0];\n  const events = match?.events || [];\n  \n  const userPrefsNode = $('Filter Active Users Only');\n  if (!userPrefsNode || !userPrefsNode.first()) {\n    throw new Error('Cannot find user preferences node');\n  }\n  \n  const userPrefs = userPrefsNode.first().json;\n  \n  const userTeamId = parseInt(userPrefs['Team ID']);\n  if (isNaN(userTeamId)) {\n    throw new Error(`Invalid Team ID: ${userPrefs['Team ID']}`);\n  }\n\n  const goals = events.filter(event => {\n    return event?.type === 'Goal' && \n           event?.team?.id === userTeamId &&\n           event?.detail !== 'Missed Penalty';\n  });\n\n  const notifiedGoals = userPrefs['Last Notified Goals'] || '';\n  const notifiedArray = notifiedGoals.split(',').filter(Boolean);\n\n  const newGoals = goals.filter(goal => {\n    const goalId = `${goal.time.elapsed}_${goal.player.id}`;\n    return !notifiedArray.includes(goalId);\n  });\n\n  if (newGoals.length === 0) {\n    return [];\n  }\n\n  const allNewGoalIds = newGoals.map(g => `${g.time.elapsed}_${g.player.id}`);\n  const updatedNotifiedGoals = [...notifiedArray, ...allNewGoalIds].join(',');\n\n  const opponent = match?.teams?.home?.id === userTeamId \n    ? match?.teams?.away?.name || 'Unknown' \n    : match?.teams?.home?.name || 'Unknown';\n\n  return newGoals.map(goal => ({\n    json: {\n      goalId: `${goal.time.elapsed}_${goal.player.id}`,\n      playerName: goal?.player?.name || 'Unknown Player',\n      minute: goal?.time?.elapsed || '0',\n      teamName: goal?.team?.name || userPrefs['Team ID'],\n      matchScore: `${match?.goals?.home || 0}-${match?.goals?.away || 0}`,\n      opponent: opponent,\n      userEmail: userPrefs['User Email'],\n      delaySeconds: parseInt(userPrefs['Delay Seconds']) || 45,\n      streamingService: userPrefs['Streaming Service'] || 'Streaming',\n      updatedNotifiedGoals: updatedNotifiedGoals,\n      matchStatus: match?.fixture?.status?.short || 'UNKNOWN'\n    }\n  }));\n\n} catch (error) {\n  return [{\n    json: {\n      error: true,\n      errorMessage: `Goal detection failed: ${error.message}`,\n      timestamp: new Date().toISOString()\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "f35419b2-f4db-4d29-b6bf-308959ed7443",
      "name": "Filter Error Results",
      "type": "n8n-nodes-base.if",
      "notes": "NEW: Prevent errors from propagating",
      "position": [
        2592,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.error !== true }}",
              "value2": true
            }
          ],
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "67f41c5b-0606-4ed0-9d2c-2d0d5d401d7a",
      "name": "Update Notified Goals Log",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "FIXED: Added User Email as lookup column for proper row targeting",
      "position": [
        2816,
        384
      ],
      "parameters": {
        "columns": {
          "value": {
            "User Email": "={{ $json.userEmail }}",
            "Last Notification": "={{ $now.toISO() }}",
            "Last Notified Goals": "={{ $json.updatedNotifiedGoals }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "User Email"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "e2625636-7ad3-4f2c-9628-cdc38eb7012b",
      "name": "Wait: User Streaming Delay",
      "type": "n8n-nodes-base.wait",
      "notes": "\u26a0\ufe0f State lost on restart - document this limitation",
      "position": [
        3040,
        384
      ],
      "parameters": {
        "amount": "={{ $json.delaySeconds }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "7e19841a-7040-4739-8810-eaf6b9d89810",
      "name": "Gmail: Send Goal Alert",
      "type": "n8n-nodes-base.gmail",
      "notes": "FIXED: Proper markdown link for upsell",
      "position": [
        3264,
        384
      ],
      "parameters": {
        "sendTo": "={{ $json.userEmail }}",
        "message": "=\ud83c\udf89 **GOAL FOR {{ $json.teamName.toUpperCase() }}!**\n\n**Scorer:** {{ $json.playerName }}\n**Minute:** {{ $json.minute }}'\n**Score:** {{ $json.matchScore }}\n**Opponent:** {{ $json.opponent }}\n\n---\n\n\u2705 **Synced to your {{ $json.streamingService }} delay**\nNo spoilers. Just pure football joy.\n\n_Sync & Play - Anti-Spoiler Alerts_",
        "options": {},
        "subject": "=\u26bd GOAL! {{ $json.playerName }} {{ $json.minute }}' - {{ $json.teamName }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "cb05e2d1-e467-47ec-a586-304ace0aed9e",
      "name": "Check Match Status",
      "type": "n8n-nodes-base.code",
      "notes": "FIXED: Runs in parallel with goal detection to prevent Empty Array Trap",
      "position": [
        3504,
        384
      ],
      "parameters": {
        "jsCode": "// ===== CHECK MATCH STATUS =====\n// Runs in parallel with goal detection - prevents Empty Array Trap\n\ntry {\n  const apiResponse = $input.item.json;\n  const response = apiResponse.response;\n  \n  // Validate API response\n  if (!response || !Array.isArray(response) || response.length === 0) {\n    return [];  // No match data, skip gracefully\n  }\n\n  const match = response[0];\n  const matchStatus = match?.fixture?.status?.short || 'UNKNOWN';\n  \n  // Get user email from the active user node\n  const userPrefsNode = $('Filter Active Users Only');\n  if (!userPrefsNode || !userPrefsNode.first()) {\n    throw new Error('Cannot find user preferences node');\n  }\n  \n  const userPrefs = userPrefsNode.first().json;\n  const userEmail = userPrefs['User Email'];\n  \n  // Check if match is finished\n  const finishedStatuses = ['FT', 'AET', 'PEN', 'CANC', 'ABD', 'AWD', 'PST', 'SUSP'];\n  const isFinished = finishedStatuses.includes(matchStatus);\n  \n  return [{\n    json: {\n      isFinished,\n      matchStatus,\n      userEmail,\n      timestamp: new Date().toISOString()\n    }\n  }];\n\n} catch (error) {\n  return [{\n    json: {\n      error: true,\n      errorMessage: `Match status check failed: ${error.message}`\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "c4542076-69ac-4d8e-ae53-3f8fc715317c",
      "name": "Is Match Finished?",
      "type": "n8n-nodes-base.if",
      "position": [
        3696,
        384
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.isFinished }}",
              "value2": true
            }
          ],
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "c3f982cc-b783-4451-87be-eed95b502940",
      "name": "Disable Active Status",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "FIXED: Added User Email as lookup column",
      "position": [
        3920,
        384
      ],
      "parameters": {
        "columns": {
          "value": {
            "Active": "No",
            "User Email": "={{ $json.userEmail }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "User Email"
          ]
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "typeVersion": 4.5
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "70fd6b37-11c2-433f-ac3c-af5841408fd2",
  "connections": {
    "Detect New Goals": {
      "main": [
        [
          {
            "node": "Filter Error Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup Validation": {
      "main": [
        [
          {
            "node": "Load User Preferences",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Poll API?": {
      "main": [
        [
          {
            "node": "API-Football: Get Live Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Match Status": {
      "main": [
        [
          {
            "node": "Is Match Finished?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Match Finished?": {
      "main": [
        [
          {
            "node": "Disable Active Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Error Results": {
      "main": [
        [
          {
            "node": "Update Notified Goals Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load User Preferences": {
      "main": [
        [
          {
            "node": "Filter Active Users Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Active Users Only": {
      "main": [
        [
          {
            "node": "Check Match Timing Window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Match Timing Window": {
      "main": [
        [
          {
            "node": "Should Poll API?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Notified Goals Log": {
      "main": [
        [
          {
            "node": "Wait: User Streaming Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule: Every 10 Minutes": {
      "main": [
        [
          {
            "node": "Setup Validation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait: User Streaming Delay": {
      "main": [
        [
          {
            "node": "Gmail: Send Goal Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API-Football: Get Live Match": {
      "main": [
        [
          {
            "node": "Detect New Goals",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check Match Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}