{
  "version": "1.0.0",
  "exported_at": "2026-06-02T11:20:00.000Z",
  "project": {
    "name": "REMAX Listing Scraper",
    "description": "Scrapes RE/MAX Paraguay listing results and exports property type, reconstructed title/location, price, listing URL, and status label. Pagination is handled inside one JavaScript step to avoid UScraper's 1000-command graph iteration limit on the ~514-page RE/MAX result set. The script clicks Next, accumulates listing rows into a hidden DOM container, marks completion, then exports once. Best-effort: CAPTCHA, bot protection, slow loading, or major UI changes may require manual intervention.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window size",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 220,
      "config": {
        "url": "https://www.remax.com.py/listings?ListingClass=-1&TransactionTypeUID=-1",
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "selector": ".listing-card[data-testid^=\"listing-card-\"]",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "duration": 3
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "waitForCompletion": true,
        "timeout": 1800,
        "jsCode": "(async () => {\n  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n  const maxPages = 514;\n  const seen = new Set();\n  const rows = [];\n\n  let container = document.querySelector('#uscraper-remax-results');\n  if (!container) {\n    container = document.createElement('div');\n    container.id = 'uscraper-remax-results';\n    container.style.display = 'none';\n    document.body.appendChild(container);\n  }\n  container.innerHTML = '';\n  container.setAttribute('data-complete', 'false');\n  container.setAttribute('data-row-count', '0');\n  container.setAttribute('data-page-count', '0');\n\n  function cleanText(value) {\n    return String(value || '').replace(/\\s+/g, ' ').trim();\n  }\n\n  function cleanSlug(value) {\n    return decodeURIComponent(String(value || '')).replace(/-/g, ' ').replace(/\\s+/g, ' ').trim();\n  }\n\n  function appendGeneratedRow(row) {\n    const rowEl = document.createElement('div');\n    rowEl.className = 'uscraper-remax-row';\n    for (const key of ['tipo_propiedad', 'titulo_ubicacion', 'precio', 'url', 'etiqueta']) {\n      const field = document.createElement(key === 'url' ? 'a' : 'span');\n      field.setAttribute('data-field', key);\n      field.textContent = row[key] || '';\n      if (key === 'url') field.href = row[key] || '';\n      rowEl.appendChild(field);\n    }\n    container.appendChild(rowEl);\n    container.setAttribute('data-row-count', String(rows.length));\n  }\n\n  function extractCard(card) {\n    const link = card.querySelector('a[data-testid=\"listing-card-status-chips-link\"][href*=\"/propiedades/\"]') || card.querySelector('a[href*=\"/propiedades/\"]');\n    if (!link || !link.href) return null;\n\n    const url = link.href;\n    if (seen.has(url)) return null;\n\n    let tipo = '';\n    let tituloUbicacion = '';\n    try {\n      const path = new URL(url).pathname;\n      const after = path.split('/propiedades/')[1] || '';\n      const parts = after.split('/').filter(Boolean);\n      tipo = cleanSlug(parts[0] || '');\n      const location = parts.slice(2, -1).map(cleanSlug).filter(Boolean).join(' ');\n      tituloUbicacion = location ? `Paraguay ${location}` : '';\n    } catch (e) {}\n\n    let precio = '';\n    const priceSelectors = ['[data-testid*=\"price\" i]', '[data-testid*=\"precio\" i]', '[class*=\"price\" i]', '[class*=\"precio\" i]'];\n    for (const selector of priceSelectors) {\n      const el = card.querySelector(selector);\n      const text = cleanText(el && el.textContent);\n      if (text) {\n        precio = text;\n        break;\n      }\n    }\n    if (!precio) {\n      const text = cleanText(card.innerText || card.textContent || '');\n      const match = text.match(/(?:US\\$|USD|Gs\\.?|₲|\\$)\\s*[0-9][0-9.,]*\\s*(?:Mensual)?|[0-9][0-9.,]*\\s*(?:₲|Gs\\.?)\\s*(?:Mensual)?/i);\n      precio = match ? cleanText(match[0]) : '';\n    }\n\n    let etiqueta = '';\n    const chips = Array.from(card.querySelectorAll('.market-status-chip .MuiChip-label, [class*=\"market-status-chip\"] .MuiChip-label'));\n    const chipValues = chips.map(el => cleanText(el.textContent)).filter(Boolean);\n    if (chipValues.length) {\n      etiqueta = chipValues.join(' | ');\n    } else {\n      etiqueta = cleanText(link.textContent || '');\n    }\n\n    seen.add(url);\n    return { tipo_propiedad: tipo, titulo_ubicacion: tituloUbicacion, precio, url, etiqueta };\n  }\n\n  function captureCurrentPage() {\n    const cards = Array.from(document.querySelectorAll('.listing-card[data-testid^=\"listing-card-\"]'));\n    let added = 0;\n    for (const card of cards) {\n      const row = extractCard(card);\n      if (row) {\n        rows.push(row);\n        appendGeneratedRow(row);\n        added++;\n      }\n    }\n    return { cards: cards.length, added };\n  }\n\n  function getFirstListingUrl() {\n    const firstLink = document.querySelector('.listing-card[data-testid^=\"listing-card-\"] a[data-testid=\"listing-card-status-chips-link\"][href*=\"/propiedades/\"], .listing-card[data-testid^=\"listing-card-\"] a[href*=\"/propiedades/\"]');\n    return firstLink ? firstLink.href : '';\n  }\n\n  function getNextButton() {\n    const buttons = Array.from(document.querySelectorAll('button[aria-label*=\"next\" i], [role=\"button\"][aria-label*=\"next\" i]'));\n    return buttons.find(btn => {\n      const disabled = btn.disabled || btn.getAttribute('aria-disabled') === 'true' || btn.classList.contains('Mui-disabled');\n      const visible = !!(btn.offsetWidth || btn.offsetHeight || btn.getClientRects().length);\n      return visible && !disabled;\n    }) || null;\n  }\n\n  async function waitForPageAdvance(previousFirstUrl, previousCount) {\n    const start = Date.now();\n    while (Date.now() - start < 12000) {\n      await sleep(350);\n      const currentFirstUrl = getFirstListingUrl();\n      const currentCount = document.querySelectorAll('.listing-card[data-testid^=\"listing-card-\"]').length;\n      if (currentFirstUrl && currentFirstUrl !== previousFirstUrl) return true;\n      if (currentCount > 0 && previousCount === 0) return true;\n    }\n    return false;\n  }\n\n  try {\n    for (let page = 1; page <= maxPages; page++) {\n      await sleep(page === 1 ? 750 : 900);\n      const beforeFirstUrl = getFirstListingUrl();\n      const result = captureCurrentPage();\n      container.setAttribute('data-page-count', String(page));\n\n      const next = getNextButton();\n      if (!next) break;\n\n      next.scrollIntoView({ block: 'center', inline: 'center' });\n      await sleep(250);\n      next.click();\n\n      const advanced = await waitForPageAdvance(beforeFirstUrl, result.cards);\n      if (!advanced) break;\n    }\n  } catch (err) {\n    container.setAttribute('data-error', String(err && err.message ? err.message : err));\n  } finally {\n    container.setAttribute('data-row-count', String(rows.length));\n    container.setAttribute('data-complete', 'true');\n  }\n\n  return rows.length;\n})();"
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "selector": "#uscraper-remax-results[data-complete=\"true\"] .uscraper-remax-row",
        "timeout": 1800,
        "visible": false
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2640,
      "position_y": 220,
      "config": {
        "rowSelector": "#uscraper-remax-results .uscraper-remax-row",
        "fileName": "remax-listados-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "create",
        "columns": [
          {
            "name": "tipo_propiedad",
            "selector": "[data-field=\"tipo_propiedad\"]",
            "attribute": "text"
          },
          {
            "name": "titulo_ubicacion",
            "selector": "[data-field=\"titulo_ubicacion\"]",
            "attribute": "text"
          },
          {
            "name": "precio",
            "selector": "[data-field=\"precio\"]",
            "attribute": "text"
          },
          {
            "name": "url",
            "selector": "[data-field=\"url\"]",
            "attribute": "href"
          },
          {
            "name": "etiqueta",
            "selector": "[data-field=\"etiqueta\"]",
            "attribute": "text"
          }
        ]
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 3000,
      "position_y": 220,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "end-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 408,
      "position_y": 116,
      "width": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1848,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2568,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 2928,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes RE/MAX Paraguay listing results and exports property type, reconstructed title/location, price, listing URL, and status label. Pagination is handled inside one JavaScript step to avoid UScraper's 1000-command graph iteration limit on the ~514-page RE/MAX result set. The script clicks Next, accumulates listing rows into a hidden DOM container, marks completion, then exports once. Best-effort: CAPTCHA, bot protection, slow loading, or major UI changes may require manual intervention.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(async () => {\n  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n  const maxPag...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 2120,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Extracts rows matching `#uscraper-remax-results .uscraper-remax-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2840,
      "position_y": 200,
      "width": 340,
      "height": 118,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    }
  ]
}