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