{
  "name": "6ixo - Crawl4AI Listings to CSV",
  "nodes": [
    {
      "parameters": {},
      "id": "manual-trigger",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        120,
        220
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Every 6 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        120,
        420
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "githubToken",
              "name": "githubToken",
              "value": "PASTE_GITHUB_TOKEN_HERE",
              "type": "string"
            },
            {
              "id": "githubOwner",
              "name": "githubOwner",
              "value": "bisco401",
              "type": "string"
            },
            {
              "id": "githubRepo",
              "name": "githubRepo",
              "value": "6ixo",
              "type": "string"
            },
            {
              "id": "githubBranch",
              "name": "githubBranch",
              "value": "main",
              "type": "string"
            },
            {
              "id": "csvPath",
              "name": "csvPath",
              "value": "data/scraped-listings.csv",
              "type": "string"
            },
            {
              "id": "defaultImportStatus",
              "name": "defaultImportStatus",
              "value": "published",
              "type": "string"
            },
            {
              "id": "crawl4aiUrl",
              "name": "crawl4aiUrl",
              "value": "http://10.0.0.164:11235/crawl",
              "type": "string"
            },
            {
              "id": "sourcesJson",
              "name": "sourcesJson",
              "value": "[\n  {\n    \"name\": \"JACars Cars\",\n    \"enabled\": true,\n    \"list_url\": \"https://www.jacars.net/cars/\",\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"app_subcategory\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": true,\n      \"recent_only\": true,\n      \"max_age_hours\": 48,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"name\": \"JACars Car Parts\",\n    \"enabled\": true,\n    \"list_url\": \"https://www.jacars.net/car-parts/car-parts/\",\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"app_subcategory\": \"auto_parts\",\n    \"rate_limit_seconds\": 3,\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"name\": \"JACars Car Accessories\",\n    \"enabled\": true,\n    \"list_url\": \"https://www.jacars.net/car-parts/car-accessories/\",\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"app_subcategory\": \"auto_parts\",\n    \"rate_limit_seconds\": 3,\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"name\": \"JACars Tires and Rims\",\n    \"enabled\": true,\n    \"list_url\": \"https://www.jacars.net/car-parts/tyres-and-rims/\",\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"app_subcategory\": \"tires_rims\",\n    \"rate_limit_seconds\": 3,\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"name\": \"JACars Auto Services\",\n    \"enabled\": true,\n    \"list_url\": \"https://www.jacars.net/services/\",\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"app_subcategory\": \"repairs\",\n    \"rate_limit_seconds\": 3,\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Alarm Systems and Security\",\n    \"list_url\": \"https://www.jacars.net/car-parts/alarm-systems/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Audio Visual Electronics\",\n    \"list_url\": \"https://www.jacars.net/car-parts/car-audio-and-visual/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Crashed Cars for Spares\",\n    \"list_url\": \"https://www.jacars.net/car-parts/crashed-cars-for-spares/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Car Parts\",\n    \"list_url\": \"https://www.jacars.net/car-parts/other/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Tools\",\n    \"list_url\": \"https://www.jacars.net/car-parts/tools/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Luggage Racks Tow Bars\",\n    \"list_url\": \"https://www.jacars.net/car-parts/luggage-racks-tow-bars/\",\n    \"app_subcategory\": \"auto_parts\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Auto Repair Shops\",\n    \"list_url\": \"https://www.jacars.net/services/auto-repair-shops/\",\n    \"app_subcategory\": \"repairs\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Body Repairs Paint Shops\",\n    \"list_url\": \"https://www.jacars.net/services/body-repairs-paint-shops/\",\n    \"app_subcategory\": \"repairs\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Tuning Shops\",\n    \"list_url\": \"https://www.jacars.net/services/repairing-tuning/\",\n    \"app_subcategory\": \"repairs\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Car Rentals\",\n    \"list_url\": \"https://www.jacars.net/services/car-hire/\",\n    \"app_subcategory\": \"rentals\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Importers Dealers\",\n    \"list_url\": \"https://www.jacars.net/services/importers-dealers/\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Transfer Taxi\",\n    \"list_url\": \"https://www.jacars.net/services/transfer-taxi/\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Moving Services\",\n    \"list_url\": \"https://www.jacars.net/services/moving/\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Services\",\n    \"list_url\": \"https://www.jacars.net/services/other/\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"target_surface\": \"vehicles\",\n    \"app_category\": \"vehicles\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Vehicle Listings\",\n    \"list_url\": \"https://www.jacars.net/other/\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 20\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Real Estate\",\n    \"list_url\": \"https://www.jacars.net/other/real-estate/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"real_estate\",\n    \"app_subcategory\": \"for_sale\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Jobs\",\n    \"list_url\": \"https://www.jacars.net/other/jobs/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"jobs\",\n    \"app_subcategory\": \"business_office\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 5\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Mobile Phones\",\n    \"list_url\": \"https://www.jacars.net/other/telephones/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"electronics\",\n    \"app_subcategory\": \"phones_accessories\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Kids Stuff\",\n    \"list_url\": \"https://www.jacars.net/other/children-and-babies/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"clothing\",\n    \"app_subcategory\": \"kids\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Clothes Footwear Accessories\",\n    \"list_url\": \"https://www.jacars.net/other/clothing-shoes-and-accessories/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"clothing\",\n    \"app_subcategory\": \"accessories\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Computers Peripherals\",\n    \"list_url\": \"https://www.jacars.net/other/computers-and-games/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"electronics\",\n    \"app_subcategory\": \"computers_tablets\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Electronics Appliances\",\n    \"list_url\": \"https://www.jacars.net/other/electronics-and-appliances/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"electronics\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Home Garden Pool\",\n    \"list_url\": \"https://www.jacars.net/other/home-gardens-and-pools/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"buy_sell\",\n    \"app_subcategory\": \"home_garden\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Hobbies Sports Leisure\",\n    \"list_url\": \"https://www.jacars.net/other/hobbies-sports-leisure-and-travel/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"buy_sell\",\n    \"app_subcategory\": \"hobbies_sports\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Animals Pets\",\n    \"list_url\": \"https://www.jacars.net/other/animals/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"other\",\n    \"app_subcategory\": \"pets\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Health Beauty\",\n    \"list_url\": \"https://www.jacars.net/other/beauty-and-health/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"services\",\n    \"app_subcategory\": \"health_beauty\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other Business\",\n    \"list_url\": \"https://www.jacars.net/other/business/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"buy_sell\",\n    \"app_subcategory\": \"business\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  },\n  {\n    \"enabled\": true,\n    \"base_url\": \"https://www.jacars.net\",\n    \"rate_limit_seconds\": 3,\n    \"name\": \"JACars Other General Services\",\n    \"list_url\": \"https://www.jacars.net/other/services/\",\n    \"target_surface\": \"marketplace\",\n    \"app_category\": \"services\",\n    \"app_subcategory\": \"other\",\n    \"extractor_config\": {\n      \"parser\": \"jacars\",\n      \"country\": \"Jamaica\",\n      \"crawl_detail_pages\": true,\n      \"detail_batch_size\": 10,\n      \"require_vehicle_year\": false,\n      \"recent_only\": true,\n      \"max_age_hours\": 720,\n      \"max_listings\": 10\n    }\n  }\n]",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "set-config",
      "name": "Set Config Here",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        420,
        320
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.crawl4aiUrl }}",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ { urls: JSON.parse($json.sourcesJson).filter(source => source.enabled !== false).map(source => source.list_url), browser_config: { type: \"BrowserConfig\", params: { headless: true } }, crawler_config: { type: \"CrawlerRunConfig\", params: { stream: false, cache_mode: \"bypass\" } } } }}",
        "options": {}
      },
      "id": "crawl4ai-http",
      "name": "Crawl4AI",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        320
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {}
      },
      "id": "merge-config-crawl",
      "name": "Merge Config + Crawl Result",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        1020,
        320
      ]
    },
    {
      "parameters": {
        "jsCode": "const merged = $input.first().json;\nconst input = { ...merged, crawl4aiResult: merged };\nconst OWNER = input.githubOwner || 'bisco401';\nconst REPO = input.githubRepo || '6ixo';\nconst BRANCH = input.githubBranch || 'main';\nconst CSV_PATH = input.csvPath || 'data/scraped-listings.csv';\nconst TOKEN = input.githubToken;\nconst DEFAULT_STATUS = input.defaultImportStatus || 'pending';\nlet SOURCES = [];\ntry {\n  SOURCES = typeof input.sourcesJson === 'string' ? JSON.parse(input.sourcesJson || '[]') : input.sourcesJson;\n} catch (error) {\n  throw new Error('sourcesJson is not valid JSON. Do not edit it unless you need to add more sources.');\n}\n\nif (!TOKEN || TOKEN === 'PASTE_GITHUB_TOKEN_HERE') throw new Error('Paste your GitHub token into the Set Config Here node.');\nif (!Array.isArray(SOURCES) || !SOURCES.length) throw new Error('The Set Config Here node needs at least one source in sourcesJson.');\n\nconst httpRequest = async (options) => {\n  const request = {\n    method: options.method || 'GET',\n    uri: options.url,\n    url: options.url,\n    headers: options.headers || {},\n    body: options.body,\n    json: options.json === true,\n    resolveWithFullResponse: true,\n    simple: false\n  };\n  try {\n    const response = await this.helpers.httpRequest(request);\n    const status = response.statusCode || response.status || 200;\n    const body = response.body ?? response;\n    return { status, ok: status >= 200 && status < 300, body };\n  } catch (error) {\n    const status = error.statusCode || error.status || error.response?.status || error.response?.statusCode || 500;\n    const body = error.response?.body || error.response?.data || error.message || '';\n    return { status, ok: status >= 200 && status < 300, body };\n  }\n};\n\nconst csvHeaders = ['id','status','target_surface','app_category','app_subcategory','title','price_text','price_value','currency','city','country','seller','phone','description','image_urls','source_site','source_url','scraped_at','make','model','trim','year','condition','transmission','color','mileage_km','attributes'];\nconst ghHeaders = { authorization: `Bearer ${TOKEN}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28' };\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nconst strip = (value = '') => String(value || '').replace(/<script[\\s\\S]*?<\\/script>/gi, ' ').replace(/<style[\\s\\S]*?<\\/style>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&#8373;|&#x20b5;/gi, 'GH\u00a2').replace(/\\s+/g, ' ').trim();\nconst rx = (pattern, flags = 'i') => pattern ? new RegExp(pattern, flags) : null;\nconst firstMatch = (text, pattern, flags = 'i') => {\n  const re = rx(pattern, flags);\n  if (!re) return '';\n  const match = String(text || '').match(re);\n  return strip(match?.[1] || match?.[0] || '');\n};\nconst absUrl = (value, base) => {\n  try { return new URL(String(value || ''), base || undefined).toString(); }\n  catch { return String(value || '').trim(); }\n};\nconst priceValue = (priceText = '') => {\n  const parsed = Number(String(priceText || '').replace(/[^0-9.]/g, ''));\n  return Number.isFinite(parsed) ? String(parsed) : '';\n};\nconst currency = (priceText = '') => {\n  const text = String(priceText || '');\n  if (/JA\\$|JMD/i.test(text)) return 'JMD';\n  if (/GH\u00a2|GHS/i.test(text)) return 'GHS';\n  if (/\\$/.test(text)) return 'USD';\n  if (/\u00a3/.test(text)) return 'GBP';\n  if (/\u20ac/.test(text)) return 'EUR';\n  return '';\n};\nconst guessYear = (text = '') => String(String(text || '').match(/\\b(19|20)\\d{2}\\b/)?.[0] || '');\nconst parseJacarsSourceDate = (value = '', now = new Date()) => {\n  const text = normalizeWhitespace(value).toLowerCase();\n  if (!text) return '';\n  const posted = new Date(now);\n  const amount = Number(text.match(/\\d+/)?.[0] || 0);\n  if (/just now|today/.test(text)) return posted.toISOString();\n  if (/minute/.test(text)) posted.setMinutes(posted.getMinutes() - amount);\n  else if (/hour/.test(text)) posted.setHours(posted.getHours() - amount);\n  else if (/yesterday/.test(text)) { posted.setDate(posted.getDate() - 1); posted.setHours(12, 0, 0, 0); }\n  else if (/day/.test(text)) posted.setDate(posted.getDate() - amount);\n  else if (/week/.test(text)) posted.setDate(posted.getDate() - (amount * 7));\n  else if (/month/.test(text)) posted.setMonth(posted.getMonth() - amount);\n  else return '';\n  return posted.toISOString();\n};\nconst sortNewestFirst = (rows = []) => rows.slice().sort((a, b) => {\n  const aTime = Date.parse(a.scraped_at || '');\n  const bTime = Date.parse(b.scraped_at || '');\n  return (Number.isFinite(bTime) ? bTime : 0) - (Number.isFinite(aTime) ? aTime : 0);\n});\nconst normalizeCondition = (value = '') => {\n  const text = String(value || '').toLowerCase();\n  if (/like new/.test(text)) return 'like_new';\n  if (/excellent/.test(text)) return 'excellent';\n  if (/good/.test(text)) return 'good';\n  if (/brand new|\\bnew\\b/.test(text)) return 'new';\n  if (/foreign used|local used|used|pre-owned|preowned/.test(text)) return 'used';\n  return '';\n};\nconst csvEscape = (value = '') => {\n  const text = String(value ?? '');\n  return /[\",\\n\\r]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text;\n};\nconst parseCsv = (text = '') => {\n  const rows = [];\n  let row = [];\n  let cell = '';\n  let quoted = false;\n  const pushCell = () => { row.push(cell); cell = ''; };\n  const pushRow = () => { pushCell(); if (row.some((v) => String(v || '').trim())) rows.push(row); row = []; };\n  const input = String(text || '').replace(/^\\uFEFF/, '');\n  for (let i = 0; i < input.length; i += 1) {\n    const char = input[i];\n    const next = input[i + 1];\n    if (quoted) {\n      if (char === '\"' && next === '\"') { cell += '\"'; i += 1; }\n      else if (char === '\"') quoted = false;\n      else cell += char;\n    } else if (char === '\"') quoted = true;\n    else if (char === ',') pushCell();\n    else if (char === '\\n') pushRow();\n    else if (char !== '\\r') cell += char;\n  }\n  if (cell || row.length) pushRow();\n  const headers = (rows.shift() || csvHeaders).map((h) => String(h || '').trim());\n  return rows.map((values) => headers.reduce((acc, header, index) => { if (header) acc[header] = values[index] || ''; return acc; }, {}));\n};\nconst toCsv = (rows = []) => [csvHeaders.join(','), ...rows.map((row) => csvHeaders.map((header) => csvEscape(row[header] || '')).join(','))].join('\\n') + '\\n';\nconst getGithubCsv = async () => {\n  const url = `https://api.github.com/repos/${OWNER}/${REPO}/contents/${encodeURIComponent(CSV_PATH).replace(/%2F/g, '/')}?ref=${BRANCH}`;\n  const res = await httpRequest({ url, headers: ghHeaders, json: true });\n  if (res.status === 404) return { sha: null, text: csvHeaders.join(',') + '\\n' };\n  if (!res.ok) throw new Error(`Could not read ${CSV_PATH}: ${res.status} ${typeof res.body === 'string' ? res.body : JSON.stringify(res.body)}`);\n  const json = typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n  return { sha: json.sha, text: Buffer.from(json.content || '', 'base64').toString('utf8') };\n};\nconst putGithubCsv = async (text, sha) => {\n  const url = `https://api.github.com/repos/${OWNER}/${REPO}/contents/${encodeURIComponent(CSV_PATH).replace(/%2F/g, '/')}`;\n  const body = {\n    message: 'Update scraped listings CSV',\n    branch: BRANCH,\n    content: Buffer.from(text, 'utf8').toString('base64'),\n    ...(sha ? { sha } : {})\n  };\n  const res = await httpRequest({ url, method: 'PUT', headers: { ...ghHeaders, 'content-type': 'application/json' }, body, json: true });\n  if (!res.ok) throw new Error(`Could not update ${CSV_PATH}: ${res.status} ${typeof res.body === 'string' ? res.body : JSON.stringify(res.body)}`);\n  return typeof res.body === 'string' ? JSON.parse(res.body) : res.body;\n};\nconst normalizeWhitespace = (value = '') => strip(String(value || '').replace(/\\s+/g, ' '));\nconst decodeHtml = (value = '') => String(value || '')\n  .replace(/&quot;/g, '\"')\n  .replace(/&#34;/g, '\"')\n  .replace(/&amp;/g, '&')\n  .replace(/&lt;/g, '<')\n  .replace(/&gt;/g, '>')\n  .replace(/&#x2F;/g, '/');\nconst extractAttr = (text = '', attr = 'href') => {\n  const match = String(text || '').match(new RegExp(attr + \"=[\\\"']([^\\\"']+)[\\\"']\", 'i'));\n  return decodeHtml(match?.[1] || '');\n};\nconst splitMakeModel = (title = '') => {\n  const cleaned = normalizeWhitespace(title).replace(/\\b(19|20)\\d{2}\\b.*$/, '').replace(/\\b\\d+[,.]\\d+L\\b/i, '').trim();\n  const multi = ['Mercedes-Benz', 'Land Rover', 'Rolls Royce', 'Alfa Romeo', 'Aston Martin'];\n  const found = multi.find((make) => cleaned.toLowerCase().startsWith(make.toLowerCase()));\n  if (found) return { make: found, model: cleaned.slice(found.length).trim() };\n  const [make = '', ...rest] = cleaned.split(/\\s+/);\n  return { make, model: rest.join(' ') };\n};\nconst extractPhoneFromJacarsDetail = (html = '') => {\n  const text = String(html || '');\n  const whatsapp = text.match(/api\\.whatsapp\\.com\\/send\\?phone=([^&\"']+)/i);\n  if (whatsapp?.[1]) return decodeURIComponent(whatsapp[1]).replace(/\\s+/g, '').trim();\n  const tel = text.match(/href=[\"']tel:([^\"']+)[\"']/i);\n  if (tel?.[1] && !/\\{\\{/.test(tel[1])) return decodeURIComponent(tel[1]).replace(/\\s+/g, '').trim();\n  const visible = text.match(/phone-author-subtext__main[^>]*>([^<]+)<[\\s\\S]{0,140}?phone-author-subtext__mask[^>]*>([^<]*)</i);\n  if (visible?.[1]) return strip(String(visible[1] || '') + String(visible[2] || '')).replace(/\\s+/g, '').trim();\n  return '';\n};\nconst extractJacarsDetailFields = (html = '') => {\n  const text = String(html || '');\n  const field = (label) => normalizeWhitespace(firstMatch(text, '<li[^>]*>\\\\s*\\\\*?\\\\s*' + label + ':\\\\s*([\\\\s\\\\S]*?)<\\\\/li>', 'i'));\n  const markdownField = (label) => normalizeWhitespace(firstMatch(text, '\\\\*\\\\s*' + label + ':\\\\s*([^\\\\n]+)', 'i'));\n  return {\n    phone: extractPhoneFromJacarsDetail(text),\n    color: field('Color') || markdownField('Color'),\n    transmission: field('Gearbox') || markdownField('Gearbox'),\n    mileage_km: (field('Mileage') || markdownField('Mileage')).replace(/[^0-9]/g, ''),\n    description: normalizeWhitespace(firstMatch(text, \"<div[^>]+class=[\\\"'][^\\\"']*announcement-description[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/div>\", 'i'))\n  };\n};\nconst crawlDetailPages = async (urls = []) => {\n  const endpoint = input.crawl4aiUrl;\n  if (!endpoint || !urls.length) return [];\n  const res = await httpRequest({\n    url: endpoint,\n    method: 'POST',\n    body: {\n      urls,\n      browser_config: { type: 'BrowserConfig', params: { headless: true } },\n      crawler_config: { type: 'CrawlerRunConfig', params: { stream: false, cache_mode: 'bypass' } }\n    },\n    json: true\n  });\n  if (!res.ok) throw new Error('Crawl4AI detail crawl failed: ' + res.status + ' ' + (typeof res.body === 'string' ? res.body : JSON.stringify(res.body)));\n  return normalizeCrawlItems(res.body);\n};\n\nconst extractJacarsListings = (source, html) => {\n  const config = source.extractor_config || {};\n  const input = String(html || '');\n  const starts = [];\n  const startRe = /<div[^>]+class=[\"'][^\"']*\\badvert(?:-grid)?\\b[^\"']*(?:\\bjs-item-listing\\b|\\bjs-advert-click\\b)[^\"']*[\"'][^>]*>/gi;\n  let match;\n  while ((match = startRe.exec(input))) starts.push(match.index);\n  const cards = starts.map((start, index) => input.slice(start, starts[index + 1] || input.length));\n  const rows = [];\n  const seen = new Set();\n  const targetSurface = source.target_surface || 'vehicles';\n  const appCategory = source.app_category || 'vehicles';\n  const appSubcategory = source.app_subcategory || 'vehicles';\n  const vehicleCoreListing = appCategory === 'vehicles' && ['vehicles', 'cars'].includes(String(appSubcategory || '').toLowerCase());\n  const requireVehicleYear = config.require_vehicle_year === true || (config.require_vehicle_year !== false && vehicleCoreListing);\n  for (const card of cards) {\n    const id = firstMatch(card, \"data-id=[\\\"']?(\\\\d+)[\\\"']?\", 'i') || firstMatch(card, \"id=[\\\"']?(\\\\d+)[\\\"']?\", 'i');\n    const titleLink = card.match(/<a[^>]+class=[\"'][^\"']*advert(?:-grid)?__content-title[^\"']*[\"'][\\s\\S]*?<\\/a>/i)?.[0] || '';\n    const title = normalizeWhitespace(titleLink || firstMatch(card, 'advert(?:-grid)?__content-title[\\\\s\\\\S]*?>([\\\\s\\\\S]*?)<\\\\/a>', 'i'));\n    if (!title) continue;\n    if (requireVehicleYear && !/\\b(19|20)\\d{2}\\b/.test(title)) continue;\n    const href = extractAttr(titleLink, 'href') || firstMatch(card, \"<a[^>]+class=[\\\"'][^\\\"']*mask[^\\\"']*[\\\"'][^>]+href=[\\\"']([^\\\"']+)\", 'i') || (id ? '/adv/' + id + '/' : '');\n    const sourceUrl = absUrl(href || source.list_url, source.base_url || source.list_url);\n    if (seen.has(sourceUrl)) continue;\n    seen.add(sourceUrl);\n    const priceBlock = card.match(/<a[^>]+class=[\"'][^\"']*advert(?:-grid)?__content-price[^\"']*[\"'][\\s\\S]*?<\\/a>/i)?.[0] || '';\n    let priceText = normalizeWhitespace(priceBlock).replace(/JA\\$\\s*/i, 'JA$ ');\n    if (!priceText) priceText = firstMatch(card, '(JA\\\\$\\\\s?[0-9,.]+(?:\\\\s?JA\\\\$\\\\s?[0-9,.]+)?|Call for price)', 'i');\n    const place = normalizeWhitespace(firstMatch(card, \"<(?:div|span)[^>]+class=[\\\"'][^\\\"']*advert(?:-grid)?__content-place[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/(?:div|span)>\", 'i'));\n    const dateText = normalizeWhitespace(firstMatch(card, \"<(?:div|span)[^>]+class=[\\\"'][^\\\"']*advert(?:-grid)?__content-date[^\\\"']*[\\\"'][^>]*>([\\\\s\\\\S]*?)<\\\\/(?:div|span)>\", 'i'));\n    const sourcePostedAt = parseJacarsSourceDate(dateText) || new Date().toISOString();\n    const seller = normalizeWhitespace(firstMatch(card, 'advert__header-name[\\\\s\\\\S]*?<span>([\\\\s\\\\S]*?)<\\\\/span>', 'i')) || source.name || '';\n    const imageMatches = [\n      ...card.matchAll(/data-background=[\"']([^\"']+)[\"']/gi),\n      ...card.matchAll(/background-image:\\s*url\\((?:&quot;|[\"']?)(.*?)(?:&quot;|[\"']?)\\)/gi),\n      ...card.matchAll(/<img[^>]+src=[\"']([^\"']+)[\"']/gi)\n    ].map((m) => decodeHtml(m[1])).filter(Boolean).map((url) => absUrl(url, source.base_url || source.list_url));\n    const images = [...new Set(imageMatches)].join('|');\n    const year = guessYear(title);\n    const { make, model } = splitMakeModel(title);\n    rows.push({\n      id: id ? 'jacars-' + id : 'csv-' + Math.abs([...sourceUrl].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0)),\n      status: source.default_status || DEFAULT_STATUS,\n      target_surface: targetSurface,\n      app_category: appCategory,\n      app_subcategory: appSubcategory,\n      title,\n      price_text: priceText,\n      price_value: priceValue(priceText),\n      currency: currency(priceText) || 'JMD',\n      city: place,\n      country: config.country || 'Jamaica',\n      seller,\n      phone: '',\n      description: [title, priceText, place, dateText].filter(Boolean).join(' - '),\n      image_urls: images,\n      source_site: source.name || 'JACars',\n      source_url: sourceUrl,\n      scraped_at: sourcePostedAt,\n      make: vehicleCoreListing ? make : '',\n      model: vehicleCoreListing ? model : '',\n      trim: '',\n      year,\n      condition: config.condition || (vehicleCoreListing ? 'used' : normalizeCondition([title, priceText, place, dateText].filter(Boolean).join(' ')) || 'good'),\n      transmission: '',\n      color: '',\n      mileage_km: '',\n      attributes: JSON.stringify({ sourceDate: dateText, sourcePostedAt, parser: 'jacars', sourceCategory: source.name || '', appSubcategory })\n    });\n  }\n  let nextRows = sortNewestFirst(rows);\n  const maxAgeHours = Number(config.max_age_hours || 0);\n  if (config.recent_only !== false && maxAgeHours > 0) {\n    const cutoff = Date.now() - (maxAgeHours * 60 * 60 * 1000);\n    nextRows = nextRows.filter((row) => {\n      const posted = Date.parse(row.scraped_at || '');\n      return Number.isFinite(posted) && posted >= cutoff;\n    });\n  }\n  const maxListings = Number(config.max_listings || 0);\n  if (maxListings > 0) nextRows = nextRows.slice(0, maxListings);\n  return nextRows;\n};\n\nconst extractListings = (source, html) => {\n  const config = source.extractor_config || {};\n  if (String(config.parser || '').toLowerCase() === 'jacars' || /jacars/i.test(source.name || '')) return extractJacarsListings(source, html);\n  const base = source.base_url || source.list_url;\n  const cardRe = rx(config.cardPattern, 'gi');\n  const cards = cardRe ? Array.from(String(html || '').matchAll(cardRe)).map((m) => m[0]) : [html];\n  return cards.map((card) => {\n    const title = firstMatch(card, config.titlePattern || '<h[12][^>]*>([\\\\s\\\\S]*?)<\\\\/h[12]>', 'i') || firstMatch(card, config.fallbackTitlePattern || 'title=\"([^\"]+)\"', 'i');\n    const href = firstMatch(card, config.urlPattern || 'href=\"([^\"]+)\"', 'i');\n    const sourceUrl = absUrl(href || source.list_url, base);\n    const priceText = firstMatch(card, config.pricePattern || '(GH\u00a2|GHS|\\\\$|\u00a3|\u20ac)\\\\s?[0-9,.]+', 'i');\n    const image = firstMatch(card, config.imagePattern || '<img[^>]+src=\"([^\"]+)\"', 'i');\n    const location = firstMatch(card, config.locationPattern || '', 'i');\n    const description = firstMatch(card, config.descriptionPattern || '', 'i') || strip(card).slice(0, 280);\n    const attributes = {\n      employmentType: config.employmentType || '',\n      experienceLevel: config.experienceLevel || '',\n      remote: Boolean(config.remote),\n      tags: Array.isArray(config.tags) ? config.tags : []\n    };\n    const row = {\n      id: sourceUrl ? `csv-${Math.abs([...sourceUrl].reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0))}` : `csv-${Date.now()}`,\n      status: source.default_status || DEFAULT_STATUS,\n      target_surface: source.target_surface || (source.app_category === 'vehicles' ? 'vehicles' : 'marketplace'),\n      app_category: source.app_category || 'electronics',\n      app_subcategory: source.app_subcategory || '',\n      title,\n      price_text: priceText,\n      price_value: priceValue(priceText),\n      currency: currency(priceText),\n      city: config.city || location.split(',')[0]?.trim() || '',\n      country: config.country || '',\n      seller: firstMatch(card, config.sellerPattern || '', 'i') || config.seller || source.name,\n      phone: firstMatch(card, config.phonePattern || '(\\\\+?\\\\d[\\\\d\\\\s().-]{7,}\\\\d)', 'i') || config.phone || '',\n      description,\n      image_urls: image ? absUrl(image, base) : '',\n      source_site: source.name || '',\n      source_url: sourceUrl,\n      scraped_at: new Date().toISOString(),\n      make: firstMatch(card, config.makePattern || '', 'i') || config.make || '',\n      model: firstMatch(card, config.modelPattern || '', 'i') || config.model || '',\n      trim: firstMatch(card, config.trimPattern || '', 'i') || config.trim || '',\n      year: firstMatch(card, config.yearPattern || '', 'i') || guessYear(`${title} ${description}`),\n      condition: firstMatch(card, config.conditionPattern || '', 'i') || normalizeCondition(`${title} ${description}`),\n      transmission: firstMatch(card, config.transmissionPattern || '(Automatic|Manual)', 'i'),\n      color: firstMatch(card, config.colorPattern || '', 'i') || config.color || '',\n      mileage_km: firstMatch(card, config.mileagePattern || '', 'i'),\n      attributes: JSON.stringify(attributes)\n    };\n    return row;\n  }).filter((row) => row.title && row.source_url);\n};\n\nconst normalizeCrawlItems = (value) => {\n  if (!value) return [];\n  if (Array.isArray(value)) return value.flatMap(normalizeCrawlItems);\n  if (Array.isArray(value.results)) return value.results;\n  if (Array.isArray(value.data)) return value.data;\n  if (value.body) return normalizeCrawlItems(value.body);\n  return [value];\n};\nconst crawlItems = normalizeCrawlItems(input.crawl4aiResult || input);\nconst crawlByUrl = new Map();\nfor (const item of crawlItems) {\n  const url = String(item.url || item.redirected_url || '').trim();\n  const text = item.html || item.cleaned_html || item.fit_html || item.markdown?.raw_markdown || item.markdown || item.fit_markdown || '';\n  if (url) crawlByUrl.set(url, text);\n  if (!url && text) crawlByUrl.set('__fallback__', text);\n}\n\nconst existing = await getGithubCsv();\nconst rowsByUrl = new Map(parseCsv(existing.text).map((row) => [String(row.source_url || '').trim(), row]));\nconst summary = [];\nfor (const source of SOURCES.filter((item) => item && item.enabled !== false)) {\n  let fetched = 0;\n  let inserted = 0;\n  try {\n    const html = crawlByUrl.get(source.list_url) || crawlByUrl.get(absUrl(source.list_url, source.base_url)) || crawlByUrl.get('__fallback__') || '';\n    if (!html) throw new Error('Crawl4AI did not return html/markdown for this source URL.');\n    if (/Just a moment|cf-mitigated|challenges.cloudflare.com/i.test(html)) throw new Error('Blocked by anti-bot challenge. Use an allowed source, API, export, saved HTML, or screenshot/OCR for this source.');\n    let scraped = extractListings(source, html);\n    if ((source.extractor_config || {}).crawl_detail_pages && scraped.length) {\n      const batchSize = Math.max(1, Number((source.extractor_config || {}).detail_batch_size || 10));\n      const detailByUrl = new Map();\n      for (let i = 0; i < scraped.length; i += batchSize) {\n        const batchUrls = scraped.slice(i, i + batchSize).map((row) => row.source_url).filter(Boolean);\n        const detailItems = await crawlDetailPages(batchUrls);\n        for (const detail of detailItems) {\n          const url = String(detail.url || detail.redirected_url || '').trim();\n          const detailHtml = detail.html || detail.cleaned_html || detail.fit_html || detail.markdown?.raw_markdown || detail.markdown || '';\n          if (url && detailHtml) detailByUrl.set(url, extractJacarsDetailFields(detailHtml));\n        }\n      }\n      scraped = scraped.map((row) => {\n        const detail = detailByUrl.get(row.source_url) || {};\n        return {\n          ...row,\n          phone: detail.phone || row.phone || '',\n          color: detail.color || row.color || '',\n          transmission: detail.transmission || row.transmission || '',\n          mileage_km: detail.mileage_km || row.mileage_km || '',\n          description: detail.description || row.description || ''\n        };\n      });\n    }\n    fetched = scraped.length;\n    for (const row of scraped) {\n      const key = String(row.source_url || '').trim();\n      if (!key) continue;\n      rowsByUrl.set(key, { ...(rowsByUrl.get(key) || {}), ...row });\n      inserted += 1;\n    }\n    summary.push({ source: source.name, status: 'success', fetched, inserted, mode: 'crawl4ai' });\n  } catch (error) {\n    summary.push({ source: source.name, status: 'failed', fetched, inserted, mode: 'crawl4ai', error: error.message });\n  }\n}\nconst nextRows = Array.from(rowsByUrl.values()).sort((a, b) => String(b.scraped_at || '').localeCompare(String(a.scraped_at || '')));\nawait putGithubCsv(toCsv(nextRows), existing.sha);\nreturn summary.map((row) => ({ json: row }));"
      },
      "id": "scrape-to-csv",
      "name": "Scrape + Update CSV",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        320
      ]
    }
  ],
  "connections": {
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Config Here",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 6 Hours": {
      "main": [
        [
          {
            "node": "Set Config Here",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Config Here": {
      "main": [
        [
          {
            "node": "Crawl4AI",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Config + Crawl Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Crawl4AI": {
      "main": [
        [
          {
            "node": "Merge Config + Crawl Result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Config + Crawl Result": {
      "main": [
        [
          {
            "node": "Scrape + Update CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "name": "6ixo"
    }
  ],
  "triggerCount": 0,
  "updatedAt": "2026-05-05T20:06:58.885Z",
  "versionId": "6ixo-crawl4ai-csv"
}