Other By web crawler 124 installs Rating 0.0 (0) approved

Neopets - SSW Shop Autopricer

Automatically price your Neopets shop using SSW data. Scan real-time listings, find competitive prices, and stay undercut for faster sales and smarter profits.
autopricer neopets shop ssw
https://www.scriptneo.com/script/neopets-ssw-shop-autopricer

Version selector


SHA256
fe4b17201528d9a865c37978a77ba2f69e1b9b7d5803f4c27723725f3d5076e5
No scan flags on this version.

Source code

// ==UserScript==
// @name         Neopets: SSW Shop Autopricer
// @namespace    mhm
// @version      1.0.0
// @description  Automatically prices shop items using event-driven detection, with ban handling + timeouts so it never freezes after 1 item.
// @author       mhm (patched by ChatGPT)
// @match        *://www.neopets.com/market.phtml?*type=your*
// @match        *://www.neopets.com/market_your.phtml
// @match        *://www.neopets.com/market.phtml?*
// @match        *://www.neopets.com/market.phtml*
// @match        *://www.neopets.com/market_your.phtml?type=*&order_by=*
// @grant        none
// @downloadURL  https://www.scriptneo.com/scripts/download.php?id=25
// @updateURL    https://www.scriptneo.com/scripts/download.php?id=25
// ==/UserScript==

(function () {
  "use strict";

  function randomIntFromInterval(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
  }

  const minSearchSpeed = 200;
  const maxSearchSpeed = 700;

  // Safety nets
  const SEARCH_TIMEOUT_MS = 15000; // if results never appear, skip and continue
  const BAN_COOLDOWN_MIN_MS = 30000; // wait 30 to 45 seconds by default
  const BAN_COOLDOWN_MAX_MS = 45000;

  let lowerPrice = 0;
  let finalPrice = 0;

  // Basic guards
  if (typeof window.jQuery === "undefined" || typeof window.$ === "undefined") return;
  const $ = window.jQuery;

  const userName = $(".user a").html() || "";

  // Insert control panel above the shop form
  $('p b font:contains("Note:")').parent().parent().before().append(
    '<div id="sswAP">' +
      '<input id="autoPrice" class="sswInner" type="button" value="SSW Auto Price">' +
      '<label class="sswInner" for="lowDiff">Low Price Alert (%):</label><input type="input" name="lowDiff" id="lowDiff" value="90">' +
      '<label class="sswInner" for="lowerPrice">Price Below:</label><input type="input" name="lowerPrice" id="lowerPrice" value="1">' +
      '<label for="finalPrice"> to Price:</label><input type="input" name="finalPrice" id="finalPrice" value="1">' +
      '<input id="priceAfterBan" name="priceAfterBan" class="sswInner" type="checkbox"><label for="priceAfterBan">Price check after ban</label>' +
    "</div>"
  );

  $(".sswInner").css({ "margin-left": "20px" });
  $("#lowDiff, #lowerPrice, #finalPrice").css({ "width": "50px" });

  // Table headers
  $('form[action="process_market.phtml"] table tbody tr').first().append(
    '<td align="center" bgcolor="#dddd77">' +
      "<b>Ignore</b><input id=\"ignoreAll\" type=\"checkbox\">" +
    "</td>" +
    '<td align="center" bgcolor="#dddd77">' +
      "<b>Reprice</b><input id=\"repriceAll\" type=\"checkbox\">" +
    "</td>" +
    '<td align="center" bgcolor="#dddd77">' +
      "<b>Lowest Prices</b>" +
    "</td>"
  );

  // Click functions
  $("#ignoreAll").on("click", () => {
    const ignoreAll = $("#ignoreAll").is(":checked");
    $(".itemIgnore").prop("checked", ignoreAll);
  });

  $("#repriceAll").on("click", () => {
    const repriceAll = $("#repriceAll").is(":checked");
    $(".itemReprice").prop("checked", repriceAll);
  });

  // Append columns
  $('form[action="process_market.phtml"] table tbody tr').not(":first").not(":last").each(function () {
    $(this).append(
      '<td width="50" align="center" bgcolor="#ffffcc">' +
        '<input class="itemIgnore" type="checkbox">' +
      "</td>" +
      '<td width="50" align="center" bgcolor="#ffffcc">' +
        '<input class="itemReprice" type="checkbox">' +
      "</td>" +
      '<td class="lowestPriceCell" width="160" align="center" bgcolor="#ffffcc"></td>'
    );
  });

  // Ensure SSW panel is open (best-effort)
  function ensureSSWOpen() {
    if ($(".sswdrop.panel_hidden").length) {
      $("#sswmenu div.imgmenu").click();
    }
  }

  function submitSSW(itemName) {
    $("#searchstr").val(itemName);
    $("#ssw-criteria").val("exact");
    $("#price-limited").prop("checked", true);
    $("#button-search").click();
  }

  function clickNewSearch() {
    // Some pages use this to reset the UI
    if ($("#button-new-search").length) {
      $("#button-new-search").click();
      return true;
    }
    return false;
  }

  function cleanupResultUI() {
    $("#results_table").remove();
    // Do not wipe #results entirely because Neopets sometimes reuses it
  }

  function getResultsText() {
    const $results = $("#results");
    if (!$results.length) return "";
    return ($results.text() || "").trim();
  }

  function isBannedMessage() {
    const txt = getResultsText();
    return txt.toLowerCase().includes("whoa there");
  }

  function hasAveragePriceMessage() {
    const txt = getResultsText().toLowerCase();
    return txt.includes("average");
  }

  function hasErrorResult() {
    return $("#ssw_error_result").length > 0;
  }

  function hasResultsTable() {
    return $("#results_table").length > 0;
  }

  function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  function waitForOutcome(timeoutMs) {
    return new Promise(resolve => {
      let done = false;

      const finish = (type) => {
        if (done) return;
        done = true;
        try { obs.disconnect(); } catch (e) {}
        clearTimeout(t);
        resolve(type);
      };

      const check = () => {
        if (hasResultsTable()) return finish("table");
        if (hasAveragePriceMessage()) return finish("average");
        if (hasErrorResult()) return finish("error");
        if (isBannedMessage()) return finish("ban");
      };

      const obs = new MutationObserver(() => {
        check();
      });

      // Watch the whole doc because Neopets inserts results in a few places
      obs.observe(document.body, { childList: true, subtree: true });

      // Also do an immediate check in case results already exist
      check();

      const t = setTimeout(() => finish("timeout"), timeoutMs);
    });
  }

  function setLowestPrice(item) {
    let price;
    let median;

    const priceFromRow = ($row) => {
      return parseInt(
        ($row.find("td:nth-child(3)").text() || "")
          .replace(/[, NP]/g, "")
          .trim(),
        10
      );
    };

      price = priceFromRow($("#results_table tbody tr").not(":first").first());

    if (!Number.isFinite(price) || price <= 0) return;

    if (lowerPrice > 0 && finalPrice > 0 && price < lowerPrice) {
      price = finalPrice;
    }

    if (price === 1) {
      item.find("td input").first().val(1);
    } else {
        item.find("td input").first().val(price-1);
    }
  }

  function addPriceCheck(item) {
    const html = ($("#results").html() || "");
    const out = html.substring(html.indexOf(":") + 1).trim();
    item.find(".lowestPriceCell:eq(0)").html(out);
  }

  function findUnderpriced($cell) {
    const firstUrl = $cell.find(".lowestPriceUrl:eq(0)");
    const secondUrl = $cell.find(".lowestPriceUrl:eq(1)");

    const lowestPrice = (firstUrl.text() || "").replace(",", "").replace(" NP", "").trim();
    const secondLowest = (secondUrl.text() || "").replace(",", "").replace(" NP", "").trim();

    const a = parseInt(lowestPrice, 10);
    const b = parseInt(secondLowest, 10);

    if (a > 0 && b > 0) {
      const priceDiff = (1 - (a / b)) * 100;
      if (priceDiff >= parseInt($("#lowDiff").val(), 10)) {
        firstUrl.addClass("redPrice");
        secondUrl.addClass("redPrice");
        alert("Check prices!");
      }
    }
  }

  function addLowestPriceLinks(item) {
    let lowestPriceUrls = "";
    $("#results_table tbody tr").not(":first").each(function () {
      let additionalClass = "";
      const seller = $(this).find("td a").first().text();
      if (seller === userName) additionalClass = "yourPrice";

      const href = $(this).find("td a").first().attr("href") || "#";
      const priceText = $(this).find("td:nth-child(3)").text();

      lowestPriceUrls += `<a class="lowestPriceUrl ${additionalClass}" href="${href}">${priceText}</a><br>`;
    });

    item.find(".lowestPriceCell:eq(0)").html(lowestPriceUrls);

    findUnderpriced(item.find(".lowestPriceCell"));

    $(".redPrice").css({ "color": "orange" });
    $(".yourPrice").css({ "color": "green" });
  }

  async function resetSearchFlow() {
    cleanupResultUI();
    // Click new search if possible
    clickNewSearch();
    // Give UI a beat to settle
    await sleep(150);
  }

  async function doPrice(item) {
    const itemName = item.find("td b").first().html() || "";
    if (!itemName) return;

    if (itemName.indexOf("pin_prefs.phtml") !== -1) return;
    if (item.find(".itemIgnore:eq(0)").is(":checked")) return;

    ensureSSWOpen();

    // We will retry this item if banned and option enabled
    let attempts = 0;

    while (attempts < 5) {
      attempts++;

      submitSSW(itemName);

      const outcome = await waitForOutcome(SEARCH_TIMEOUT_MS);

      if (outcome === "table") {
        setLowestPrice(item);
        addLowestPriceLinks(item);
        await resetSearchFlow();
        return;
      }

      if (outcome === "average") {
        addPriceCheck(item);
        await resetSearchFlow();
        return;
      }

      if (outcome === "error") {
        await resetSearchFlow();
        return;
      }

      if (outcome === "ban") {
        const allowAfterBan = $("#priceAfterBan").is(":checked");

        if (!allowAfterBan) {
          // Stop this item and move on without freezing the whole run
          item.find(".lowestPriceCell:eq(0)").html("<span style=\"color:red\"><b>SSW rate limited</b></span>");
          await resetSearchFlow();
          return;
        }

        // Cooldown then retry same item
        const waitMs = randomIntFromInterval(BAN_COOLDOWN_MIN_MS, BAN_COOLDOWN_MAX_MS);
        item.find(".lowestPriceCell:eq(0)").html(
          `<span style="color:red"><b>Rate limited</b></span><br><span class="small">Waiting ${Math.ceil(waitMs / 1000)}s then retrying...</span>`
        );

        await sleep(waitMs);
        await resetSearchFlow();
        continue;
      }

      if (outcome === "timeout") {
        // Do not stall forever. Mark and move on.
        item.find(".lowestPriceCell:eq(0)").html("<span style=\"color:red\"><b>Timeout</b></span>");
        await resetSearchFlow();
        return;
      }
    }

    // Too many attempts, skip
    item.find(".lowestPriceCell:eq(0)").html("<span style=\"color:red\"><b>Failed after retries</b></span>");
    await resetSearchFlow();
  }

  async function processItems(items) {
    for (let i = 0; i < items.length; i++) {
      const item = $(items[i]);

      const currentVal = parseInt(item.find("td input").first().val(), 10);
      const needsPrice = (currentVal === 0 || item.find(".itemReprice:eq(0)").is(":checked"));

      if (needsPrice) {
        await doPrice(item);
        await sleep(randomIntFromInterval(minSearchSpeed, maxSearchSpeed));
      }
    }
    console.log("Finished pricing");
  }

  $("#autoPrice").on("click", async () => {
    lowerPrice = parseInt($("#lowerPrice").val(), 10) || 0;
    finalPrice = parseInt($("#finalPrice").val(), 10) || 0;

    ensureSSWOpen();

    const items = $('form[action="process_market.phtml"] table tbody tr').not(":first").not(":last");
    await processItems(items);
  });

})();