{
  "name": "SpicyAPI \u00b7 private character image bot",
  "nodes": [
    {
      "id": "ce36d2b6-fa05-4603-9dc2-cc9c8ad2c64e",
      "name": "Setup and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Private Telegram character image bot\n\nSelf-hosted n8n 2.42 + SpicyAPI node 0.1.1.\n\n1. Select the same Telegram API credential in Telegram Input and Reply with Image. Select a SpicyAPI credential in Generate Character.\n2. Settings: configure your own numeric allowedChatId and a public HTTPS referenceImageUrl. Keep enabled=false until you are ready, review maxCostUsd, then enabled=true.\n3. Test privately, then activate deliberately. Telegram needs a reachable HTTPS n8n webhook. Send /image followed by a scene description (up to 2000 characters).\n\nOne message creates at most one Qwen Image 2.1 edit task (1k, 3:4). Other chats and non-command messages stop before a quote. A task is quoted with a $0.10 cap and downloaded into binary.data before Send Photo. For an ongoing bot, configure a spending cap on its dedicated API key in the console.\n\nIf generation or the 300-second wait fails, no photo is sent. The accepted task may still finish: recover its task ID instead of resubmitting. Sending the same command again starts a new execution; there is no cross-execution deduplication or public multi-user queue in this template. This download is inactive and sends nothing by itself.\n\nSelect the same SpicyAPI credential in Get Character Result. Generate submits once and stores its task ID; Get Character Result retrieves that task and waits/downloads without generating again. Any result that is not ready becomes needs_review. Accepted task IDs remain in the Generate node and result output even if fetching fails. If no ID was returned, inspect the saved execution and your console before creating any new task.",
        "width": 780,
        "height": 520
      },
      "position": [
        0,
        -420
      ]
    },
    {
      "id": "fea90329-93ba-48cd-abe6-c6184d6178f1",
      "name": "Telegram Input",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.3,
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "download": false
        }
      },
      "position": [
        0,
        0
      ],
      "webhookId": "1b86b6ae-2468-443b-af1d-de05c769c6d3",
      "credentials": {
        "telegramApi": {
          "id": "",
          "name": "Select your Telegram bot credential"
        }
      }
    },
    {
      "id": "08cf8b03-1ac6-48c9-97fd-d232f92c71b8",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a69e0eb7-ff27-46e9-89ee-a13dad312bcf",
              "name": "enabled",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "2d175cd6-c0aa-4105-8aa4-586aca105f2a",
              "name": "allowedChatId",
              "value": "",
              "type": "string"
            },
            {
              "id": "8266e8f8-795e-45de-a250-ba80fbfed209",
              "name": "referenceImageUrl",
              "value": "",
              "type": "string"
            },
            {
              "id": "8e0e8b51-97a6-4c80-90f9-094f886bd117",
              "name": "maxCostUsd",
              "value": 0.1,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "position": [
        220,
        0
      ]
    },
    {
      "id": "07bec055-226a-4794-ad40-b81e1bef4c95",
      "name": "Prepare Command",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function publicImage(value) {\n  const url = String(value || '').trim();\n  const match = url.match(/^https:\\/\\/([^/?#]+)(?:[/?#]|$)/i);\n  const host = match ? match[1].toLowerCase() : '';\n  const hostname = host.split(':')[0];\n  if (!host || host.includes('@') || host.startsWith('[') || !/^[a-z0-9.-]+(?::443)?$/.test(host) || /^(localhost|127\\.|0\\.|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)/i.test(hostname)) throw new Error('Set a public HTTPS referenceImageUrl in Settings, without URL credentials.');\n  return url;\n}\nfunction costLimit(value) {\n  const limit = Number(value);\n  if (!Number.isFinite(limit) || limit <= 0 || limit > 0.10) throw new Error('maxCostUsd must be above 0 and at most 0.10 per image.');\n  return limit;\n}\n\nconst c = $('Settings').first().json;\nif (c.enabled !== true) return [];\nconst referenceImageUrl = publicImage(c.referenceImageUrl);\ncostLimit(c.maxCostUsd);\nconst allowedChatId = String(c.allowedChatId || '').trim();\nif (!/^-?[0-9]+$/.test(allowedChatId)) throw new Error('Set one numeric allowedChatId before enabling the bot.');\nconst message = $('Telegram Input').first().json.message;\nif (!message || String(message.chat?.id) !== allowedChatId) return [];\nconst match = String(message.text || '').trim().match(/^\\/image(?:@[A-Za-z0-9_]+)?\\s+([\\s\\S]+)$/);\nif (!match) return [];\nconst scene = match[1].trim();\nif (!scene || scene.length > 2000) throw new Error('Use /image followed by a scene of 1 to 2000 characters.');\nreturn [{json: {chatId: allowedChatId, messageId: message.message_id, referenceImageUrl, prompt: 'Use the same adult fictional character as the reference. Keep the face and hairstyle recognisable while changing the scene. ' + scene + ' Fashion photography, complete clothing, natural anatomy.'}, pairedItem: {item: 0}}];"
      },
      "position": [
        440,
        0
      ]
    },
    {
      "id": "56129cd4-e4f9-4b47-ac43-5ca69c683396",
      "name": "Generate Character",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "alibaba/qwen-image-2.1/edit"
        },
        "prompt": "={{ $json.prompt }}",
        "mediaInputs": {
          "input": [
            {
              "field": "image_urls",
              "source": "url",
              "url": "={{ $json.referenceImageUrl }}"
            }
          ]
        },
        "modelParameters": {
          "mappingMode": "defineBelow",
          "value": {
            "aspect_ratio": "3:4",
            "output_format": "png",
            "resolution": "1k",
            "enable_prompt_expansion": false,
            "background": "opaque"
          },
          "schema": []
        },
        "additionalParameters": "{}",
        "maxCost": "={{ $('Settings').first().json.maxCostUsd }}",
        "generateOptions": {
          "waitForCompletion": false,
          "downloadOutput": false
        }
      },
      "position": [
        670,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "7d4dab40-92e1-4748-a4f2-ee4d7fa76e90",
      "name": "Get Character Result",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "task",
        "operation": "get",
        "taskId": "={{ $json.taskId || '' }}",
        "taskOptions": {
          "waitForCompletion": true,
          "maxWaitTime": 300,
          "downloadOutput": true,
          "binaryPropertyName": "data"
        }
      },
      "position": [
        900,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "36cb57bc-664b-41cb-945a-922c4fb3e0b1",
      "name": "Ready to Reply",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $input.first();\nconst accepted = $('Generate Character').first().json;\nconst taskId = item.json.taskId || accepted.taskId || '';\nconst ready = item.json.state === 'succeeded' && !!item.binary?.data;\nreturn [{json: {...item.json, taskId, status: ready ? 'done' : 'needs_review', error: ready ? '' : String(item.json.error || item.json.errorMessage || accepted.error || 'Retrieve the accepted task before creating another image.') + (taskId ? '' : ' Inspect the saved execution and console before creating another task.'), chatId: $('Prepare Command').first().json.chatId}, ...(ready ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      },
      "position": [
        1130,
        0
      ]
    },
    {
      "id": "ebe75ed6-cf6f-466a-8d2b-34be9e72c0c4",
      "name": "Reply with Image",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "parameters": {
        "resource": "message",
        "operation": "sendPhoto",
        "chatId": "={{ $json.chatId }}",
        "binaryData": true,
        "binaryPropertyName": "data",
        "additionalFields": {
          "caption": "Your character image",
          "appendAttribution": false
        }
      },
      "position": [
        1590,
        0
      ],
      "credentials": {
        "telegramApi": {
          "id": "",
          "name": "Select your Telegram bot credential"
        }
      }
    },
    {
      "id": "d38237fe-908e-4ca9-a853-31374e79e7d8",
      "name": "Image Ready?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "23fc82d7-e450-422c-8dc9-2accdac5c1da",
              "leftValue": "={{ $json.status === 'done' }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "position": [
        1360,
        0
      ]
    },
    {
      "id": "1f948da9-00a5-4311-885c-e628ca58e535",
      "name": "Needs Review",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return $input.all();"
      },
      "position": [
        1590,
        220
      ]
    }
  ],
  "connections": {
    "Telegram Input": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Prepare Command",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Command": {
      "main": [
        [
          {
            "node": "Generate Character",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Character": {
      "main": [
        [
          {
            "node": "Get Character Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ready to Reply": {
      "main": [
        [
          {
            "node": "Image Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Character Result": {
      "main": [
        [
          {
            "node": "Ready to Reply",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Ready?": {
      "main": [
        [
          {
            "node": "Reply with Image",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Needs Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 600
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
