{
  "nodes": [
    {
      "id": "9418fcb2-faa2-4b09-a586-0e2407f29bc1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2576,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 280,
        "height": 148,
        "content": "## Write to sheets\n\nAppends available domains to sheet \"available\", taken ones to sheet \"closed\"."
      },
      "typeVersion": 1
    },
    {
      "id": "76f9496b-5028-4289-ad46-638e8eee772d",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        368,
        336
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "12966170-54f2-4adb-8986-e21a98ecadc2",
      "name": "Read sheet available",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        528,
        336
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "available"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5,
      "continueOnFail": true
    },
    {
      "id": "0a434fcb-3328-40da-af7a-6c5fb3f6c2e8",
      "name": "Trigger closed read",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        336
      ],
      "parameters": {
        "jsCode": "return [{ json: {} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "939ea07b-1f6a-4d31-928d-2a9cfa70e0ee",
      "name": "Read sheet closed",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        848,
        336
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "closed"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5,
      "continueOnFail": true
    },
    {
      "id": "1e5e0aca-0d40-4f79-9cc1-78e77055910e",
      "name": "Build initial state",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        336
      ],
      "parameters": {
        "jsCode": "let availableRows = [];\nlet closedRows = [];\ntry { availableRows = $('Read sheet available').all().map(i => i.json || i) || []; } catch (e) { }\ntry { closedRows = $('Read sheet closed').all().map(i => i.json || i) || []; } catch (e) { }\nconst totalFound = Array.isArray(availableRows) ? availableRows.length : 0;\nconst dom = (r) => (r && (r.domain || r.name || r.Domain || r.Name));\nconst fromAv = (Array.isArray(availableRows) ? availableRows : []).map(dom).filter(Boolean);\nconst fromCl = (Array.isArray(closedRows) ? closedRows : []).map(dom).filter(Boolean);\nconst alreadyChecked = [...new Set([...fromAv, ...fromCl])];\nreturn { json: { totalFound, alreadyChecked, iteration: 0 } };"
      },
      "typeVersion": 2
    },
    {
      "id": "1c6e05de-3a65-4778-99fa-2bc1be51d184",
      "name": "Prepare prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        1232,
        336
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst totalFound = input.totalFound ?? 0;\nconst alreadyChecked = Array.isArray(input.alreadyChecked) ? input.alreadyChecked : [];\nconst iteration = input.iteration ?? 0;\nconst excludeText = alreadyChecked.length > 0\n  ? ` Do NOT suggest these names or domains (already checked, taken or already used): ${alreadyChecked.join(', ')}.`\n  : '';\nconst systemPrompt = `You are a naming expert. Generate short, distinctive, memorable brand names in English for a product or app. Use only letters, no numbers or hyphens. Each name must be under 14 characters. Return a valid JSON object with a single key \"names\" containing an array of exactly 20 strings.${excludeText}`;\nconst userPrompt = `Generate 20 new, unique names for a product or app. Letters only, no numbers or hyphens, under 14 characters per name.${excludeText} Return ONLY valid JSON: { \"names\": [\"Name1\", \"Name2\", ...] }`;\nreturn { json: { systemPrompt, userPrompt, totalFound, alreadyChecked, iteration } };"
      },
      "typeVersion": 2
    },
    {
      "id": "e04b598d-b421-466f-8371-91c406e2c896",
      "name": "Generate names (OpenAI)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1392,
        336
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o', temperature: 0.7, max_tokens: 2000, messages: [{ role: 'system', content: $json.systemPrompt }, { role: 'user', content: $json.userPrompt }], response_format: { type: 'json_object' } }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4
    },
    {
      "id": "114d5465-a2cb-40f0-8166-9bb144f4a7e9",
      "name": "Normalize to domains",
      "type": "n8n-nodes-base.code",
      "position": [
        1552,
        336
      ],
      "parameters": {
        "jsCode": "const openaiResponse = $json;\nif (!openaiResponse.choices || !openaiResponse.choices[0] || !openaiResponse.choices[0].message || !openaiResponse.choices[0].message.content) {\n  throw new Error('OpenAI returned empty or invalid response');\n}\nconst parsed = JSON.parse(openaiResponse.choices[0].message.content);\nconst names = Array.isArray(parsed.names) ? parsed.names : [];\nif (names.length === 0) throw new Error('AI returned no names');\nconst items = names.map(name => {\n  const clean = String(name).trim().replace(/\\s+/g, '').replace(/[^a-zA-Z]/g, '');\n  if (!clean) return null;\n  const domain = clean.toLowerCase() + '.com';\n  return { json: { name: clean, domain } };\n}).filter(Boolean);\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "493049ef-0815-4d22-893f-b45a9d56d61b",
      "name": "Check domain (API Ninjas)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        336
      ],
      "parameters": {
        "url": "https://api.api-ninjas.com/v1/domain",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "domain",
              "value": "={{ $json.domain }}"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "0a48d7ba-d5a0-4d2c-92bb-7f198ec317bf",
      "name": "Merge name and API result",
      "type": "n8n-nodes-base.code",
      "position": [
        1984,
        336
      ],
      "parameters": {
        "jsCode": "const apiItems = $input.all();\nconst normNode = $('Normalize to domains');\nconst normItems = normNode.all();\nconst state = $('Prepare prompt').last().json;\nconst roundState = { totalFound: state.totalFound ?? 0, alreadyChecked: Array.isArray(state.alreadyChecked) ? state.alreadyChecked : [], iteration: state.iteration ?? 0 };\nconst result = [];\nfor (let i = 0; i < apiItems.length; i++) {\n  const item = apiItems[i];\n  const apiJson = item.json || {};\n  if (typeof apiJson.available === 'boolean') {\n    result.push({ json: { ...normItems[i].json, ...apiJson, _roundState: roundState } });\n  }\n}\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "edc50197-55e6-49c3-b123-5763435d5ea8",
      "name": "Closed only",
      "type": "n8n-nodes-base.code",
      "position": [
        1984,
        720
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn items.filter(i => i.json && i.json.available === false).map(i => ({ json: { domain: i.json.domain, name: i.json.name } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "ce7811b1-cf8d-45e2-955f-fa248a4d6ec1",
      "name": "State for next run",
      "type": "n8n-nodes-base.code",
      "position": [
        1984,
        544
      ],
      "parameters": {
        "jsCode": "const mergeItems = $input.all();\nconst first = mergeItems[0] && mergeItems[0].json;\nconst prev = first && first._roundState ? first._roundState : { totalFound: 0, alreadyChecked: [], iteration: 0 };\nconst totalFound = prev.totalFound ?? 0;\nconst alreadyChecked = prev.alreadyChecked ?? [];\nconst iteration = prev.iteration ?? 0;\nconst availableCount = mergeItems.filter(i => i.json && i.json.available === true).length;\nconst domainsThisRound = mergeItems.map(i => i.json && i.json.domain).filter(Boolean);\nconst newAlreadyChecked = [...new Set([...alreadyChecked, ...domainsThisRound])];\nconst newTotalFound = totalFound + availableCount;\nconst newIteration = iteration + 1;\nreturn { json: { totalFound: newTotalFound, alreadyChecked: newAlreadyChecked, iteration: newIteration } };"
      },
      "typeVersion": 2
    },
    {
      "id": "035d99e3-e056-4909-80a9-f5d81258cc3d",
      "name": "Exit or loop",
      "type": "n8n-nodes-base.if",
      "position": [
        2208,
        544
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "cond-exit",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.totalFound >= 15 }}",
              "rightValue": true
            },
            {
              "id": "cond-iter",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.iteration > 10 }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "8ea1d3ca-12f4-42d3-bd4c-ccbb79d0aae9",
      "name": "Filter available only",
      "type": "n8n-nodes-base.filter",
      "position": [
        2208,
        336
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-available",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.available }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "fc2c9eca-d842-4b28-afbb-8f456f42ffed",
      "name": "Append to sheet available",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2640,
        336
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "domain",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "domain",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "available"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "319bf54c-7c74-4276-85e1-48fc779bd217",
      "name": "Append to sheet closed",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2208,
        720
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "domain",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "domain",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "closed"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "98e14d9d-acdb-4ded-bd01-a9c7dc1cbde9",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -208
      ],
      "parameters": {
        "width": 464,
        "height": 448,
        "content": "## How it works\n\nManual Trigger starts a run. The workflow reads your Google Sheet (tabs \"available\" and \"closed\") to get domains already checked. Build initial state builds the alreadyChecked list and totalFound count.\n\nPrepare prompt builds the prompt with your app context and passes alreadyChecked so the AI does not suggest the same names again. Generate names (OpenAI) returns 20 new names; Normalize to domains turns them into .com domains.\n\nCheck domain (API Ninjas) checks each name. Merge name and API result keeps only items with a valid API response. Filter available only and Closed only split results into available vs taken. Available domains go to Append to sheet available, taken ones to Append to sheet closed. State for next run updates the counters and alreadyChecked list. Exit or loop stops when you have at least 15 available domains or after 10 iterations (20 names per iteration), otherwise it loops back to Prepare prompt.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3537e384-9162-44f4-ac92-513c1fab213b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        288
      ],
      "parameters": {
        "width": 464,
        "height": 288,
        "content": "## Setup steps\n\n1. Credentials: Add OpenAI API key to the Generate names (OpenAI) node. Add API Ninjas key (header X-Api-Key from api-ninjas.com) to Check domain (API Ninjas). Add Google Sheets OAuth2 to all four Google Sheets nodes.\n2. In one Google spreadsheet create two sheets named \"available\" and \"closed\", each with header row: domain, name. In all four Google Sheets nodes set Document to your sheet ID (replace YOUR_GOOGLE_SHEET_ID in the node settings).\n3. Optional: Edit Prepare prompt to describe your product so the AI suggests fitting names (e.g. app category, style, reference brands)."
      },
      "typeVersion": 1
    },
    {
      "id": "ff9b747e-9b86-4997-9284-115fce0105ba",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        144
      ],
      "parameters": {
        "color": 7,
        "content": "## Start & load state\n\nReads sheets \"available\" and \"closed\", builds alreadyChecked and totalFound for the loop."
      },
      "typeVersion": 1
    },
    {
      "id": "fb7e0fae-f61b-40eb-a991-655691c480cd",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1392,
        144
      ],
      "parameters": {
        "color": 7,
        "content": "## Prompt & names\n\nEdit Prepare prompt for your product; OpenAI returns 20 names; Normalize to domains makes them .com."
      },
      "typeVersion": 1
    },
    {
      "id": "9fe661d2-b771-4e3f-a674-add5a79c3d1e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        144
      ],
      "parameters": {
        "color": 7,
        "content": "## Check & route\n\nAPI Ninjas checks each domain; results split into available/closed; state updated; loop or exit."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Closed only": {
      "main": [
        [
          {
            "node": "Append to sheet closed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Exit or loop": {
      "main": [
        [],
        [
          {
            "node": "Prepare prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Read sheet available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare prompt": {
      "main": [
        [
          {
            "node": "Generate names (OpenAI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read sheet closed": {
      "main": [
        [
          {
            "node": "Build initial state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "State for next run": {
      "main": [
        [
          {
            "node": "Exit or loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build initial state": {
      "main": [
        [
          {
            "node": "Prepare prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Trigger closed read": {
      "main": [
        [
          {
            "node": "Read sheet closed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize to domains": {
      "main": [
        [
          {
            "node": "Check domain (API Ninjas)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read sheet available": {
      "main": [
        [
          {
            "node": "Trigger closed read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter available only": {
      "main": [
        [
          {
            "node": "Append to sheet available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate names (OpenAI)": {
      "main": [
        [
          {
            "node": "Normalize to domains",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check domain (API Ninjas)": {
      "main": [
        [
          {
            "node": "Merge name and API result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge name and API result": {
      "main": [
        [
          {
            "node": "Filter available only",
            "type": "main",
            "index": 0
          },
          {
            "node": "State for next run",
            "type": "main",
            "index": 0
          },
          {
            "node": "Closed only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}