// ==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);
});
})();