{
  "id": "f5x0V0gia7wOWWUL",
  "name": "Vinted New-Listing Alerts to Telegram",
  "tags": [],
  "nodes": [
    {
      "id": "50000000-0000-0000-4000-8000-000000000001",
      "name": "Manual Start Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1840,
        224
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000002",
      "name": "Hourly Alert Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1840,
        16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000003",
      "name": "Create Delivery Ledger",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        -1568,
        112
      ],
      "parameters": {
        "columns": {
          "column": [
            {
              "name": "workflowSlug"
            },
            {
              "name": "itemKey"
            },
            {
              "name": "destination"
            },
            {
              "name": "deliveredAt",
              "type": "date"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        },
        "resource": "table",
        "operation": "create",
        "tableName": "FetchCat Delivery Ledger"
      },
      "typeVersion": 1.1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000005",
      "name": "Set Vinted Search Parameters",
      "type": "n8n-nodes-base.set",
      "position": [
        -1328,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "vinted-search",
              "name": "searchText",
              "type": "string",
              "value": "cycling jersey"
            },
            {
              "id": "vinted-audience",
              "name": "audience",
              "type": "string",
              "value": "Women"
            },
            {
              "id": "vinted-domain",
              "name": "domain",
              "type": "string",
              "value": "www.vinted.fr"
            },
            {
              "id": "vinted-min-price",
              "name": "minimumPrice",
              "type": "number",
              "value": 0
            },
            {
              "id": "vinted-max-price",
              "name": "maximumPrice",
              "type": "number",
              "value": 150
            },
            {
              "id": "vinted-brands",
              "name": "allowedBrands",
              "type": "string",
              "value": "MAAP"
            },
            {
              "id": "vinted-sizes",
              "name": "allowedSizes",
              "type": "string",
              "value": "S, XS"
            },
            {
              "id": "vinted-colors",
              "name": "allowedColors",
              "type": "string",
              "value": "blue, bleu, black, noir, white, blanc, multi, multicolor, multicolour, multicolore, red, rouge, yellow, jaune"
            },
            {
              "id": "vinted-brand-ids",
              "name": "brandIds",
              "type": "string",
              "value": ""
            },
            {
              "id": "vinted-catalog-ids",
              "name": "catalogIds",
              "type": "string",
              "value": ""
            },
            {
              "id": "vinted-results",
              "name": "maxResults",
              "type": "number",
              "value": 10
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000006",
      "name": "Validate Search Settings",
      "type": "n8n-nodes-base.code",
      "position": [
        -848,
        112
      ],
      "parameters": {
        "jsCode": "const input = $input.first()?.json || {};\nconst searchText = String(input.searchText || '').trim();\nconst domain = String(input.domain || '').trim().toLowerCase().replace(/^https?:\\/\\//, '').replace(/\\/$/, '');\nconst minimumPrice = Number(input.minimumPrice ?? 0);\nconst maximumPrice = Number(input.maximumPrice ?? 0);\nconst maxResults = Number(input.maxResults ?? 10);\nconst normalize = (value) => String(value || '').normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\nconst parseList = (value) => String(value || '').split(',').map(normalize).filter(Boolean);\nconst parseIds = (value, label) => {\n  const ids = String(value || '').split(',').map((entry) => entry.trim()).filter(Boolean).map(Number);\n  if (ids.some((id) => !Number.isInteger(id) || id <= 0)) throw new Error(label + ' must contain comma-separated positive numeric Vinted IDs.');\n  return [...new Set(ids)];\n};\nconst audienceMap = { any: 'Any', women: 'Women', men: 'Men', girls: 'Girls', boys: 'Boys' };\nconst audience = audienceMap[normalize(input.audience || 'Any')];\nconst allowedBrands = parseList(input.allowedBrands);\nconst allowedSizes = parseList(input.allowedSizes);\nconst allowedColors = parseList(input.allowedColors);\nconst brandIds = parseIds(input.brandIds, 'Brand IDs');\nconst catalogIds = parseIds(input.catalogIds, 'Catalog IDs');\nif (searchText.length < 2 || searchText.length > 200) throw new Error('Search text must be 2 to 200 characters.');\nif (!audience) throw new Error('Audience must be Any, Women, Men, Girls, or Boys.');\nif (!/^www\\.vinted\\.[a-z.]{2,10}$/.test(domain)) throw new Error('Use a public Vinted domain such as www.vinted.fr, www.vinted.de, or www.vinted.co.uk.');\nif (!Number.isFinite(minimumPrice) || minimumPrice < 0) throw new Error('Minimum price must be zero or greater.');\nif (!Number.isFinite(maximumPrice) || maximumPrice <= 0 || maximumPrice < minimumPrice) throw new Error('Maximum price must be greater than or equal to minimum price.');\nif (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 50) throw new Error('Maximum results must be an integer from 1 to 50.');\nconst audienceTerm = audience === 'Any' ? '' : audience.toLowerCase();\nconst searchTerms = new Set(normalize(searchText).split(' ').filter(Boolean));\nconst actorSearchText = audienceTerm && !searchTerms.has(audienceTerm) ? searchText + ' ' + audienceTerm : searchText;\nconst monitorKey = [domain, normalize(searchText), audience, minimumPrice, maximumPrice, [...allowedBrands].sort().join(','), [...allowedSizes].sort().join(','), [...allowedColors].sort().join(','), [...brandIds].sort((a, b) => a - b).join(','), [...catalogIds].sort((a, b) => a - b).join(',')].join('|');\nconst focusedBrands = brandIds.length ? [] : allowedBrands;\nconst searchCount = Math.max(1, focusedBrands.length);\nconst itemsPerSearch = Math.ceil(maxResults / searchCount);\nconst makeActorInput = (focusedBrand = '') => ({\n  searchText: focusedBrand ? focusedBrand + ' ' + actorSearchText : actorSearchText,\n  domain,\n  priceMin: minimumPrice,\n  priceMax: maximumPrice,\n  maxItems: itemsPerSearch,\n  order: 'newest_first',\n  includeSeller: true,\n  ...(brandIds.length ? { brandIds } : {}),\n  ...(catalogIds.length ? { catalogIds } : {})\n});\nconst actorInputs = focusedBrands.length ? focusedBrands.map(makeActorInput) : [makeActorInput()];\nreturn [{ json: {\n  searchText, actorSearchText, audience, domain, minimumPrice, maximumPrice, allowedBrands, allowedSizes, allowedColors, brandIds, catalogIds,\n  maxResults, monitorKey, searchCount, itemsPerSearch, actorInputs\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000032",
      "name": "Build Brand Search Queries",
      "type": "n8n-nodes-base.code",
      "position": [
        -608,
        16
      ],
      "parameters": {
        "jsCode": "const config = $('Validate Search Settings').first().json;\nreturn config.actorInputs.map((actorInput, index) => ({ json: {\n  actorInput,\n  searchNumber: index + 1,\n  searchCount: config.actorInputs.length\n} }));"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000008",
      "name": "Start FetchCat Vinted Search",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -160,
        16
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/F1GAwbqJ9xc9h7P87/runs",
        "method": "POST",
        "options": {
          "timeout": 310000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ $json.actorInput }}",
        "sendBody": true,
        "sendQuery": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "300"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept-Encoding",
              "value": "identity"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000009",
      "name": "Download Vinted Search Results",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        80,
        16
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.data.defaultDatasetId }}/items",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "clean",
              "value": "true"
            },
            {
              "name": "limit",
              "value": "={{ $(\"Validate Search Settings\").first().json.maxResults }}"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000010",
      "name": "Normalize and Filter Listings",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        208
      ],
      "parameters": {
        "jsCode": "const config = $('Validate Search Settings').first().json;\nconst rawListings = $input.all().flatMap((item) => {\n  let payload = item.json?.data ?? item.json;\n  if (typeof payload === 'string') {\n    try { payload = JSON.parse(payload); } catch { throw new Error('Apify returned invalid JSON.'); }\n  }\n  return Array.isArray(payload) ? payload : [payload];\n}).slice(0, config.maxResults);\nconst normalized = [];\nconst seenListingIds = new Set();\nconst filterProgress = { validListings: 0, withinPrice: 0, matchingBrand: 0, matchingSize: 0 };\nconst normalize = (value) => String(value || '').normalize('NFKD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();\nconst containsPhrase = (text, phrase) => (' ' + text + ' ').includes(' ' + phrase + ' ');\nfor (const listing of rawListings) {\n  const id = String(listing.id || '').trim();\n  const title = String(listing.title || '').trim();\n  const url = String(listing.url || '').trim();\n  const priceAmount = Number(listing.priceAmount);\n  const brand = String(listing.brandTitle || '').trim();\n  const size = String(listing.sizeTitle || '').trim();\n  const normalizedTitle = normalize(title);\n  const normalizedBrand = normalize(brand);\n  const normalizedSize = normalize(size);\n  const sizeParts = new Set([normalizedSize, ...String(size || '').split(/[\\/,;|()[\\]]+/).map(normalize).filter(Boolean)]);\n  const matchedColors = config.allowedColors.filter((color) => containsPhrase(normalizedTitle, color));\n  if (!id || !title || !/^https:\\/\\//.test(url) || !Number.isFinite(priceAmount)) continue;\n  if (seenListingIds.has(id)) continue;\n  seenListingIds.add(id);\n  filterProgress.validListings += 1;\n  if (priceAmount < config.minimumPrice || priceAmount > config.maximumPrice) continue;\n  filterProgress.withinPrice += 1;\n  if (config.allowedBrands.length && !config.allowedBrands.includes(normalizedBrand)) continue;\n  filterProgress.matchingBrand += 1;\n  if (config.allowedSizes.length && !config.allowedSizes.some((allowedSize) => sizeParts.has(allowedSize))) continue;\n  filterProgress.matchingSize += 1;\n  normalized.push({ json: {\n    listingId: id,\n    itemKey: config.monitorKey + '|' + id,\n    monitorKey: config.monitorKey,\n    title,\n    priceAmount,\n    currency: String(listing.currency || ''),\n    brand: brand || 'Brand not specified',\n    size: size || 'Size not specified',\n    matchedColors,\n    audience: config.audience,\n    searchText: config.searchText,\n    condition: String(listing.status || 'Condition not specified'),\n    seller: String(listing.sellerLogin || 'Seller not specified'),\n    favoriteCount: Number.isFinite(Number(listing.favoriteCount)) ? Number(listing.favoriteCount) : null,\n    viewCount: Number.isFinite(Number(listing.viewCount)) && Number(listing.viewCount) > 0 ? Number(listing.viewCount) : null,\n    photoUrl: Array.isArray(listing.photoUrls) && listing.photoUrls[0] ? String(listing.photoUrls[0]) : '',\n    url,\n    scrapedAt: String(listing.scrapedAt || '')\n  } });\n}\nif (normalized.length) return normalized;\nconst stages = [\n  ['valid listing data', filterProgress.validListings],\n  ['price', filterProgress.withinPrice],\n  ['brand', filterProgress.matchingBrand],\n  ['size', filterProgress.matchingSize]\n];\nconst blockedAt = rawListings.length === 0 ? 'search results' : (stages.find(([, count]) => count === 0)?.[0] || 'configured filters');\nreturn [{ json: {\n  noMatches: true,\n  status: 'No listings matched all configured filters.',\n  returnedCount: rawListings.length,\n  blockedAt,\n  filterProgress,\n  returnedBrands: [...new Set(rawListings.map((listing) => String(listing.brandTitle || 'Brand not specified').trim()))].sort(),\n  returnedSizes: [...new Set(rawListings.map((listing) => String(listing.sizeTitle || 'Size not specified').trim()))].sort(),\n  suggestion: rawListings.length === 0\n    ? 'Broaden searchText or confirm the selected Vinted domain.'\n    : 'Clear or broaden the blocking filter, use exact Vinted brand/catalog IDs when available, or increase maxResults for wider coverage.'\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000029",
      "name": "Check for Matching Listings",
      "type": "n8n-nodes-base.if",
      "position": [
        544,
        112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "vinted-matches-condition",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.noMatches !== true }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000011",
      "name": "Keep Undelivered Listings",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        784,
        16
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "workflowSlug",
              "keyValue": "vinted-new-listing-alerts"
            },
            {
              "keyName": "itemKey",
              "keyValue": "={{ $json.itemKey }}"
            }
          ]
        },
        "matchType": "allConditions",
        "operation": "rowNotExists",
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "FetchCat Delivery Ledger"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000030",
      "name": "Show No-Match Details",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        208
      ],
      "parameters": {
        "jsCode": "return $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000012",
      "name": "Build Telegram Alert Batch",
      "type": "n8n-nodes-base.code",
      "position": [
        1008,
        16
      ],
      "parameters": {
        "jsCode": "const listings = $input.all().map((item) => item.json);\nif (listings.length === 0) return [];\nreturn [{ json: { listings } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000017",
      "name": "Format Telegram Messages",
      "type": "n8n-nodes-base.code",
      "position": [
        1248,
        368
      ],
      "parameters": {
        "jsCode": "const escapeHtml = (value) => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst all = $json.listings;\nconst config = $('Validate Search Settings').first().json;\nconst messages = [];\nfor (let offset = 0; offset < all.length; offset += 5) {\n  const chunk = all.slice(offset, offset + 5);\n  const lines = chunk.map((listing, index) => {\n    const position = offset + index + 1;\n    const price = listing.priceAmount.toLocaleString('en-US', { maximumFractionDigits: 2 }) + (listing.currency ? ' ' + listing.currency : '');\n    const engagement = [listing.viewCount === null ? null : listing.viewCount + ' views', listing.favoriteCount === null ? null : listing.favoriteCount + ' favorites'].filter(Boolean).join(' | ');\n    return position + '. <b>' + escapeHtml(listing.title.slice(0, 110)) + '</b>\\n' +\n      '<b>Price:</b> ' + escapeHtml(price) + '\\n' +\n      '<b>Brand:</b> ' + escapeHtml(listing.brand) + ' | <b>Size:</b> ' + escapeHtml(listing.size) +\n      (listing.matchedColors.length ? ' | <b>Color:</b> ' + escapeHtml(listing.matchedColors.join(', ')) : '') + '\\n' +\n      '<b>Condition:</b> ' + escapeHtml(listing.condition) + ' | <b>Seller:</b> ' + escapeHtml(listing.seller) +\n      (engagement ? '\\n' + escapeHtml(engagement) : '') + '\\n' +\n      '<a href=\"' + escapeHtml(listing.url) + '\">Open Vinted listing</a>';\n  });\n  const heading = offset === 0\n    ? '<b>' + all.length + ' new Vinted ' + (all.length === 1 ? 'match' : 'matches') + '</b>'\n    : '<b>Vinted matches continued</b>';\n  const criteria = offset === 0 ? '\\n<b>Search:</b> ' + escapeHtml(config.searchText) + ' | <b>Audience:</b> ' + escapeHtml(config.audience) : '';\n  messages.push({ json: { telegramMessage: heading + criteria + '\\n\\n' + lines.join('\\n\\n') } });\n}\nreturn messages;"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000018",
      "name": "Send New Listings to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1488,
        368
      ],
      "parameters": {
        "text": "={{ $json.telegramMessage }}",
        "chatId": "123456789",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false,
          "disable_notification": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000019",
      "name": "Prepare Delivery Records",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        416
      ],
      "parameters": {
        "jsCode": "return $('Build Telegram Alert Batch').first().json.listings.map((listing) => ({ json: { workflowSlug: 'vinted-new-listing-alerts', itemKey: listing.itemKey } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000020",
      "name": "Record Delivered Listings",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2000,
        416
      ],
      "parameters": {
        "columns": {
          "value": {
            "itemKey": "={{ $json.itemKey }}",
            "deliveredAt": "={{ $now.toISO() }}",
            "destination": "Telegram",
            "workflowSlug": "={{ $json.workflowSlug }}"
          },
          "schema": [
            {
              "id": "workflowSlug",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "workflowSlug",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "itemKey",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "itemKey",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "destination",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "destination",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "deliveredAt",
              "type": "dateTime",
              "display": true,
              "required": false,
              "displayName": "deliveredAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "name",
          "value": "FetchCat Delivery Ledger"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000021",
      "name": "Workflow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2448,
        -256
      ],
      "parameters": {
        "width": 480,
        "height": 960,
        "content": "## Vinted New-Listing Alerts to Telegram\n\nMonitor a focused public Vinted search and receive Telegram alerts for matching listings that have not been delivered before. This workflow runs `fetch_cat/vinted-search-scraper` through Apify, works on n8n Cloud or self-hosted n8n, and does not require OpenAI.\n\n### How it works\n\n1. Starts manually or on the editable hourly schedule.\n2. Validates the marketplace, query, audience, price, brand, size, color labels, and result limit.\n3. Runs focused FetchCat searches and downloads the newest Vinted listings.\n4. Filters results and removes listing IDs already present in the delivery ledger.\n5. Sends readable Telegram alerts and records IDs only after delivery succeeds.\n\n### Setup\n\n- [ ] Edit `Set Vinted Search Parameters` for your marketplace and saved-search criteria.\n- [ ] Connect one Apify HTTP Header Auth credential to both FetchCat HTTP Request nodes.\n- [ ] Connect a Telegram Bot credential and choose the destination chat.\n- [ ] Adjust `Hourly Alert Trigger` when a different interval is worth the extra executions.\n- [ ] Run manually, confirm the current matches arrive, then publish the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000022",
      "name": "Trigger Workflow",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1888,
        -256
      ],
      "parameters": {
        "color": 7,
        "height": 624,
        "content": "## Trigger the monitor\n\nStarts manually for testing or on the editable hourly schedule."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000023",
      "name": "Initialize and Configure Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 1152,
        "height": 480,
        "content": "## Initialize and configure search\n\nCreates the delivery ledger, reads the saved-search parameters, validates every value, and builds one focused Actor input per brand name. A size such as `M`, `38`, or `10` matches Vinted's combined value `M / 38 / 10`."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000024",
      "name": "Execute Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 416,
        "content": "## Execute the FetchCat search\n\nStarts `fetch_cat/vinted-search-scraper` through Apify and downloads the completed dataset."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000025",
      "name": "Process and Filter Results",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 608,
        "content": "## Process and filter results\n\nNormalizes listings, applies price, brand, and size filters, explains empty results, and keeps only IDs absent from the delivery ledger."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000031",
      "name": "Prepare and Send Alerts",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 752,
        "content": "## Prepare and send alerts\n\nBatches unseen listings, formats readable Telegram messages, and sends current matches immediately, including on the first run."
      },
      "typeVersion": 1
    },
    {
      "id": "50000000-0000-0000-4000-8000-000000000027",
      "name": "Log and Finalize",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 416,
        "content": "## Record successful delivery\n\nWrites listing IDs only after Telegram succeeds so interrupted deliveries remain retryable."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "Europe/Lisbon",
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "versionId": "b6dbfc9a-b16d-44cb-a519-d5f453d25cd2",
  "nodeGroups": [],
  "connections": {
    "Hourly Alert Trigger": {
      "main": [
        [
          {
            "node": "Create Delivery Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Start Trigger": {
      "main": [
        [
          {
            "node": "Create Delivery Ledger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Delivery Ledger": {
      "main": [
        [
          {
            "node": "Set Vinted Search Parameters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Telegram Messages": {
      "main": [
        [
          {
            "node": "Send New Listings to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Delivery Records": {
      "main": [
        [
          {
            "node": "Record Delivered Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Search Settings": {
      "main": [
        [
          {
            "node": "Build Brand Search Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Undelivered Listings": {
      "main": [
        [
          {
            "node": "Build Telegram Alert Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Brand Search Queries": {
      "main": [
        [
          {
            "node": "Start FetchCat Vinted Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Telegram Alert Batch": {
      "main": [
        [
          {
            "node": "Format Telegram Messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Matching Listings": {
      "main": [
        [
          {
            "node": "Keep Undelivered Listings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Show No-Match Details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Vinted Search Parameters": {
      "main": [
        [
          {
            "node": "Validate Search Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start FetchCat Vinted Search": {
      "main": [
        [
          {
            "node": "Download Vinted Search Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize and Filter Listings": {
      "main": [
        [
          {
            "node": "Check for Matching Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send New Listings to Telegram": {
      "main": [
        [
          {
            "node": "Prepare Delivery Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Vinted Search Results": {
      "main": [
        [
          {
            "node": "Normalize and Filter Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}