// ==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( '
' + '' + '' + '' + '' + '' + "
" ); $(".sswInner").css({ "margin-left": "20px" }); $("#lowDiff, #lowerPrice, #finalPrice").css({ "width": "50px" }); // Table headers $('form[action="process_market.phtml"] table tbody tr').first().append( '' + "Ignore" + "" + '' + "Reprice" + "" + '' + "Lowest Prices" + "" ); // 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( '' + '' + "" + '' + '' + "" + '' ); }); // 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 += `${priceText}
`; }); 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("SSW rate limited"); 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( `Rate limited
Waiting ${Math.ceil(waitMs / 1000)}s then retrying...` ); await sleep(waitMs); await resetSearchFlow(); continue; } if (outcome === "timeout") { // Do not stall forever. Mark and move on. item.find(".lowestPriceCell:eq(0)").html("Timeout"); await resetSearchFlow(); return; } } // Too many attempts, skip item.find(".lowestPriceCell:eq(0)").html("Failed after retries"); 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); }); })();