{
  "name": "FetchFox Facebook comments CSV starter v1",
  "nodes": [
    {
      "id": "manual-run",
      "name": "Manual run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        240
      ],
      "parameters": {}
    },
    {
      "id": "configure-input",
      "name": "Configure input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        240
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "postUrl",
              "name": "postUrl",
              "type": "string",
              "value": "https://www.facebook.com/watch?v=REPLACE_VIDEO_ID"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "validate-input",
      "name": "Validate input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        240
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const value = $('Configure input').first().json.postUrl;\nif (typeof value !== 'string' || value.length > 2000 || value.includes('REPLACE_') || !new RegExp(\"^https:\\\\/\\\\/(?:www\\\\.|m\\\\.|web\\\\.)?facebook\\\\.com\\\\/(?:watch\\\\?v=\\\\d+(?:&[^\\\\s]*)?|reel\\\\/\\\\d+\\\\/?|[A-Za-z0-9_.-]+\\\\/(?:posts|videos)\\\\/[A-Za-z0-9]+\\\\/?)(?:[?#][^\\\\s]*)?$\").test(value)) throw new Error('Set postUrl to a direct public HTTPS Facebook post, reel or video URL.');\nreturn [{ json: { postUrl: value } }];"
      }
    },
    {
      "id": "fetch-comments",
      "name": "Fetch comments",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        240
      ],
      "parameters": {
        "method": "GET",
        "url": "https://api.fetchfox.net/facebook/comments",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $('Configure input').first().json.postUrl }}"
            },
            {
              "name": "limit",
              "value": "20"
            }
          ]
        },
        "options": {
          "timeout": 180000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": false,
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false
    },
    {
      "id": "csv-file",
      "name": "CSV file",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        240
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const response = $input.first().json;\nif (response.statusCode !== 200 || response.body?.success !== true || !response.body.data) throw new Error('FetchFox did not return a successful result. Inspect the HTTP node; correct input/authentication, wait for Retry-After on 429, or check your credits before rerunning.');\n\nconst data = response.body.data;\nif (!Array.isArray(data.comments) || data.comments.length > 20 || typeof data.hasMore !== 'boolean') throw new Error('Expected a bounded comment collection.');\nif (!data.comments.length && (data.hasMore || data.collectionStatus !== 'exhausted')) throw new Error('Empty collection was not confirmed.');\nconst fields = ['post_url','id','text','author','author_name','likes','replies','timestamp'];\nconst seen = new Set();\nfunction cell(value, identifier = false) {\n if (value === null || value === undefined) return '\"\"';\n if (!['string','number','boolean'].includes(typeof value)) return '\"\"';\n let text = String(value);\n if ((identifier && /^\\d+$/.test(text)) || (typeof value === 'string' && (/^[\\s\\u0000-\\u001f]*[=+\\-@]/u.test(text) || /^[\\u0000-\\u001f]/u.test(text)))) text = \"'\" + text;\n return '\"' + text.replaceAll('\"','\"\"') + '\"';\n}\nconst lines = [fields.map(value => cell(value)).join(',')];\nfor (const row of data.comments) {\n if (!row || typeof row.id !== 'string' || !row.id.trim() || typeof row.text !== 'string' || seen.has(row.id)) throw new Error('Malformed or duplicate comment.');\n seen.add(row.id);\n for (const field of ['likes','replyCount','timestamp']) if (row[field] != null && (!Number.isSafeInteger(row[field]) || row[field] < 0)) throw new Error('Invalid comment metric.');\n const values = [$('Configure input').first().json.postUrl,row.id,row.text,row.name,row.name,row.likes,row.replyCount,row.timestamp];\n lines.push(values.map((value, index) => cell(value, index === 1)).join(','));\n}\nconst content = '\\ufeff' + lines.join('\\r\\n') + '\\r\\n';\nreturn [{ json: { returnedCount: data.comments.length, collectionStatus: data.collectionStatus || 'unknown', hasMore: data.hasMore, retrievedAt: new Date().toISOString() }, binary: { data: { data: Buffer.from(content,'utf8').toString('base64'), mimeType: 'text/csv', fileName: 'fetchfox-facebook-comments.csv' } } }];"
      }
    },
    {
      "id": "setup-and-credit-cost",
      "name": "Setup and credit cost",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        240,
        240
      ],
      "parameters": {
        "content": "## FetchFox starter v1\n\n1. Edit Configure input.\n2. Create an n8n Header Auth credential: name x-access-key, value your FetchFox key. Select it in every Fetch node.\n3. Execute manually.\n\nOne batch of up to 20 comments. A successful request costs 1 credit, including a verified empty response or cached success. Failed processing uses no credits. No pagination. Download the CSV binary from CSV file. Configuration/code tested with fixtures; verify import and execution in your n8n installation.\n\nNo schedules or automatic retries. No credentials are included. Keep keys in n8n credentials. Source data can be partial or cached.",
        "width": 540,
        "height": 280
      }
    }
  ],
  "connections": {
    "Manual run": {
      "main": [
        [
          {
            "node": "Configure input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure input": {
      "main": [
        [
          {
            "node": "Validate input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate input": {
      "main": [
        [
          {
            "node": "Fetch comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch comments": {
      "main": [
        [
          {
            "node": "CSV file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 600,
    "timezone": "UTC",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveManualExecutions": false
  },
  "pinData": {},
  "tags": []
}
