{
  "id": "mvxVBIvdjGEVWgGX",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI search visibility monitor (ChatGPT, Perplexity, Gemini, Claude)",
  "tags": [],
  "nodes": [
    {
      "id": "2d482efa-2ae2-4c17-bde2-353995438624",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14752,
        480
      ],
      "parameters": {
        "width": 668,
        "height": 600,
        "content": "## AI search visibility monitor\n\nAsks the AI answer engines (ChatGPT search, Perplexity, Gemini, Claude) the buyer questions your customers actually type, then checks whether your brand is named or cited. Scores each answer cited(1) / mentioned(0.5) / absent(0), logs every check to Supabase, and prints a panel score you can track over time.\n\n### Why it matters\nBuyers ask AI before Google now. If the engines never name you for 'who should I hire', you are invisible where the decision is made. This turns that into a number you can move and chart.\n\n### Setup\n1. Add to your n8n environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY.\n2. Create the Supabase table:\ncreate table ai_visibility_log (\n  id bigint generated always as identity primary key,\n  run_at timestamptz default now(),\n  brand text, engine text, question text,\n  score numeric, cited boolean, mentioned boolean\n);\n3. Add a Supabase credential and pick it on the log node.\n4. Edit brand + questions in Config, then Run. Add a Schedule trigger for a weekly check.\n\n### Dashboard\nChart panel score over time and by engine straight from ai_visibility_log.\n\nBuilt by nocode.expert. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "91808814-a4a2-4907-bb55-90fcea58b1c7",
      "name": "Section: Ask",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15536,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 1. Configure and ask\nSet brand + questions, fan out one item per question, then ask all four engines in parallel."
      },
      "typeVersion": 1
    },
    {
      "id": "5ea3d8df-2a19-413c-b152-058d64a77f1e",
      "name": "Section: Score",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16240,
        336
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 2. Score each answer\nEach engine has a parse node: cited if your domain shows up, mentioned if only your name, else absent."
      },
      "typeVersion": 1
    },
    {
      "id": "112e5b6e-4a4d-4b27-945e-02eb9f4daf1f",
      "name": "Section: Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17008,
        496
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## 3. Log and report\nWrite every check to Supabase for the dashboard, compute the panel score, print the scorecard."
      },
      "typeVersion": 1
    },
    {
      "id": "08bf1d55-a0db-42f3-9f34-5e944a0fa4dd",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        15456,
        736
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a4ad670d-678c-4a69-8b17-b748562bf18b",
      "name": "Set config: brand, buyer questions",
      "type": "n8n-nodes-base.code",
      "position": [
        15712,
        736
      ],
      "parameters": {
        "jsCode": "// Two knobs: what counts as YOU (brand domain + name), and the buyer questions\n// to ask each AI engine. Add or edit questions freely.\nreturn [{ json: {\n  BRAND_DOMAIN: 'stripe.com',\n  BRAND_NAME: 'Stripe',\n  QUESTIONS: [\"What is the best payment processor for an online startup? Name specific providers.\", \"How should a SaaS company accept payments and manage subscriptions online?\", \"Which payment API is easiest for developers to integrate?\", \"Who offers the simplest online checkout for a new e-commerce store?\", \"Best Stripe alternatives for a small business?\"],\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3f64fd63-7f3d-4ca7-a66e-7e82c92b66b6",
      "name": "Build question items",
      "type": "n8n-nodes-base.code",
      "position": [
        15952,
        736
      ],
      "parameters": {
        "jsCode": "// Fan out one item per question so each engine node runs once per question.\nreturn $('Set config: brand, buyer questions').first().json.QUESTIONS.map(q => ({ json: { question: q } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "dfc0d650-43e7-4fdc-a5b8-8d279fc2544e",
      "name": "Ask Claude",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        544
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"claude-haiku-4-5\", \"max_tokens\": 1024, \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $env.ANTHROPIC_API_KEY }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "21adc6af-a834-418b-b1a5-0f3033bdea3f",
      "name": "Ask ChatGPT (search)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        704
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"gpt-4o-search-preview\", \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.OPENAI_API_KEY }}"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e20348b1-f8b1-43af-b3f8-4cf428bd5488",
      "name": "Ask Perplexity",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        864
      ],
      "parameters": {
        "url": "https://api.perplexity.ai/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"model\": \"sonar\", \"messages\": [ { \"role\": \"user\", \"content\": $json.question } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.PERPLEXITY_API_KEY }}"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "0dafad32-0577-4534-95d1-01fcee0bc69f",
      "name": "Ask Gemini (search)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        16224,
        1024
      ],
      "parameters": {
        "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={{ $env.GEMINI_API_KEY }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ { \"contents\": [ { \"parts\": [ { \"text\": $json.question } ] } ], \"tools\": [ { \"google_search\": {} } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1447d99e-0897-420d-ab36-28eaf823c41c",
      "name": "Score Claude",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        544
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.content && it.json.content[0] && it.json.content[0].text || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Claude', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "1dbf051e-d288-44b6-9b76-754686bce228",
      "name": "Score ChatGPT",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        704
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.choices && it.json.choices[0] && it.json.choices[0].message && it.json.choices[0].message.content || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'ChatGPT', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "6ac8b2be-a4b6-4e59-946c-b68888e6f81e",
      "name": "Score Perplexity",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        864
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.choices && it.json.choices[0] && it.json.choices[0].message && it.json.choices[0].message.content || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Perplexity', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "a400fd3e-bcbd-4edf-8535-dc4ff9415e97",
      "name": "Score Gemini",
      "type": "n8n-nodes-base.code",
      "position": [
        16496,
        1024
      ],
      "parameters": {
        "jsCode": "// Extract the answer, then score visibility. cited(1)=brand domain appears\n// anywhere in the response (prose or source URL); mentioned(0.5)=brand name only;\n// absent(0)=neither. Pure transform.\nconst B = $('Set config: brand, buyer questions').first().json;\nconst domain = B.BRAND_DOMAIN.toLowerCase();\nconst name = B.BRAND_NAME.toLowerCase();\nconst questions = $('Build question items').all();\nconst out = [];\n$input.all().forEach((it, i) => {\n  const q = questions[i].json.question;\n  const text = it.json.candidates && it.json.candidates[0] && it.json.candidates[0].content && it.json.candidates[0].content.parts && it.json.candidates[0].content.parts[0] && it.json.candidates[0].content.parts[0].text || '';\n  const hay = (text + ' ' + JSON.stringify(it.json)).toLowerCase();\n  const cited = hay.includes(domain);\n  const mentioned = cited || hay.includes(name);\n  const score = cited ? 1 : (mentioned ? 0.5 : 0);\n  out.push({ json: { engine: 'Gemini', question: q, score, cited, mentioned, snippet: text.slice(0,180) } });\n});\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "bf0e269f-a04a-4846-9794-1d4d01c70646",
      "name": "Prepare rows",
      "type": "n8n-nodes-base.code",
      "position": [
        16768,
        864
      ],
      "parameters": {
        "jsCode": "// Shape each check into a row for Supabase (columns match the table below).\nconst B = $('Set config: brand, buyer questions').first().json;\nreturn $input.all().map(i => ({ json: {\n  brand: B.BRAND_DOMAIN,\n  engine: i.json.engine,\n  question: i.json.question,\n  score: i.json.score,\n  cited: i.json.cited,\n  mentioned: i.json.mentioned,\n} }));"
      },
      "typeVersion": 2
    },
    {
      "id": "e0093310-0794-43ea-aea8-4dc07fa2362a",
      "name": "Log to Supabase",
      "type": "n8n-nodes-base.supabase",
      "position": [
        17248,
        880
      ],
      "parameters": {
        "tableId": "ai_visibility_log",
        "dataToSend": "autoMapInputData"
      },
      "typeVersion": 1
    },
    {
      "id": "0fb23faa-e63f-4891-a7b6-3e39528b4811",
      "name": "Aggregate & score panel",
      "type": "n8n-nodes-base.code",
      "position": [
        17008,
        736
      ],
      "parameters": {
        "jsCode": "// Combine all rows, compute per-engine and overall panel score.\nconst rows = $input.all().map(i => i.json);\nconst byEngine = {};\nfor (const r of rows) {\n  const e = byEngine[r.engine] = byEngine[r.engine] || { checks: 0, score: 0, cited: 0, mentioned: 0 };\n  e.checks++; e.score += r.score; if (r.cited) e.cited++; else if (r.mentioned) e.mentioned++;\n}\nfor (const k of Object.keys(byEngine)) byEngine[k].pct = +(100 * byEngine[k].score / byEngine[k].checks).toFixed(1);\nconst total = rows.reduce((n, r) => n + r.score, 0);\nconst panelPct = rows.length ? +(100 * total / rows.length).toFixed(1) : 0;\nreturn [{ json: { panelPct, totalChecks: rows.length, byEngine, rows } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "18a5f045-2fdd-4fb8-a1da-03cfae563864",
      "name": "Print visibility scorecard",
      "type": "n8n-nodes-base.code",
      "position": [
        17248,
        736
      ],
      "parameters": {
        "jsCode": "// Print the visibility scorecard. Persisted rows go to Supabase for the dashboard.\nconst m = $json;\nconst brand = $('Set config: brand, buyer questions').first().json.BRAND_DOMAIN;\nconst out = ['', '==========================================================',\n'  AI SEARCH VISIBILITY  |  ' + brand, '=========================================================='];\nout.push('Panel score: ' + m.panelPct + '%   (' + m.totalChecks + ' checks: cited=1, mentioned=0.5, absent=0)');\nout.push('');\nfor (const [eng, e] of Object.entries(m.byEngine)) {\n  out.push(eng.padEnd(12) + ' ' + String(e.pct).padStart(5) + '%   cited ' + e.cited + ', mentioned ' + e.mentioned + ' of ' + e.checks);\n}\nout.push(''); out.push('Not cited (targets):');\nm.rows.filter(r => r.score < 1).slice(0, 12).forEach(r => out.push('  [' + r.engine + '] ' + (r.mentioned ? 'name only' : 'absent') + ' : ' + r.question.slice(0, 55)));\nout.push('==========================================================');\nconsole.log(out.join('\\n'));\nreturn [{ json: m }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "921cd3b6-a75e-47ff-aea1-e9dd621f0d2a",
  "connections": {
    "Ask Claude": {
      "main": [
        [
          {
            "node": "Score Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare rows": {
      "main": [
        [
          {
            "node": "Log to Supabase",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate & score panel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run manually": {
      "main": [
        [
          {
            "node": "Set config: brand, buyer questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Claude": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Gemini": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score ChatGPT": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Perplexity": {
      "main": [
        [
          {
            "node": "Score Perplexity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Perplexity": {
      "main": [
        [
          {
            "node": "Prepare rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask Gemini (search)": {
      "main": [
        [
          {
            "node": "Score Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ask ChatGPT (search)": {
      "main": [
        [
          {
            "node": "Score ChatGPT",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build question items": {
      "main": [
        [
          {
            "node": "Ask Claude",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask ChatGPT (search)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask Perplexity",
            "type": "main",
            "index": 0
          },
          {
            "node": "Ask Gemini (search)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & score panel": {
      "main": [
        [
          {
            "node": "Print visibility scorecard",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: brand, buyer questions": {
      "main": [
        [
          {
            "node": "Build question items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}