// ==UserScript==
// @name Neopets - Trading Post Bulk Delete Helper
// @version 1.1
// @description Adds a panel to select, delete selected, or delete all of your visible Trading Post lots.
// @author Fixed Version
// @match *://www.neopets.com/island/tradingpost.phtml*
// @match *://neopets.com/island/tradingpost.phtml*
// @grant none
// @icon https://images.neopets.com/tradingpost/assets/images/trade_icon.png
// @downloadURL https://www.scriptneo.com/scripts/download.php?id=28
// @updateURL https://www.scriptneo.com/scripts/download.php?id=28
// ==/UserScript==
(function () {
"use strict";
const TPBH = {
panelId: "tpbh-panel",
statusId: "tpbh-status",
listId: "tpbh-list",
endpointLots: "/np-templates/ajax/island/tradingpost/get-lots.php?sort=newest",
endpointDelete: "/np-templates/ajax/island/tradingpost/delete-lot.php",
delayBetweenDeletesMs: 900,
requireTypedConfirm: true
};
let lots = [];
let selectedLotIds = new Set();
let isDeleting = false;
ready(function () {
addCSS();
addPanel();
loadLots();
});
function ready(fn) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", fn);
} else {
fn();
}
}
function qs(selector, root = document) {
return root.querySelector(selector);
}
function qsa(selector, root = document) {
return Array.from(root.querySelectorAll(selector));
}
function sleep(ms) {
return new Promise(function (resolve) {
window.setTimeout(resolve, ms);
});
}
function setStatus(message, type = "") {
const status = qs("#" + TPBH.statusId);
if (!status) {
return;
}
status.textContent = message;
status.className = "tpbh-status";
if (type) {
status.classList.add(type);
}
}
function addPanel() {
if (qs("#" + TPBH.panelId)) {
return;
}
const panel = document.createElement("div");
panel.id = TPBH.panelId;
panel.innerHTML = `
<div class="tpbh-header">
<div>
<div class="tpbh-title">Trading Post Bulk Delete</div>
<div class="tpbh-subtitle">Select trades, then delete/cancel them in a controlled batch.</div>
</div>
<button type="button" id="tpbh-refresh" class="tpbh-btn tpbh-btn-neutral">Refresh</button>
</div>
<div class="tpbh-warning">
Deleting/cancelling lots costs NP. Your example response shows <strong>15,000 NP</strong> for one deleted lot.
</div>
<div class="tpbh-actions">
<button type="button" id="tpbh-select-all" class="tpbh-btn tpbh-btn-neutral">Select All Visible</button>
<button type="button" id="tpbh-clear" class="tpbh-btn tpbh-btn-neutral">Clear Selection</button>
<button type="button" id="tpbh-delete-selected" class="tpbh-btn tpbh-btn-danger">Delete Selected</button>
<button type="button" id="tpbh-delete-all" class="tpbh-btn tpbh-btn-dark">Delete All Loaded</button>
</div>
<div id="${TPBH.statusId}" class="tpbh-status">Loading your trades...</div>
<div id="${TPBH.listId}" class="tpbh-list"></div>
`;
const target =
qs(".tp-main-content") ||
qs("#tp_app__2025") ||
qs("#content") ||
qs("#main") ||
document.body;
target.insertBefore(panel, target.firstChild);
qs("#tpbh-refresh").addEventListener("click", function () {
if (!isDeleting) {
loadLots();
}
});
qs("#tpbh-select-all").addEventListener("click", function () {
lots.forEach(function (lot) {
selectedLotIds.add(String(getLotId(lot)));
});
renderLots();
updateSelectionStatus();
});
qs("#tpbh-clear").addEventListener("click", function () {
selectedLotIds.clear();
renderLots();
updateSelectionStatus();
});
qs("#tpbh-delete-selected").addEventListener("click", function () {
deleteSelectedLots();
});
qs("#tpbh-delete-all").addEventListener("click", function () {
deleteAllLoadedLots();
});
}
async function loadLots() {
try {
selectedLotIds.clear();
setStatus("Loading your Trading Post lots...", "tpbh-working");
const response = await fetch(TPBH.endpointLots, {
method: "GET",
credentials: "include",
headers: {
"x-requested-with": "XMLHttpRequest"
},
cache: "no-store"
});
if (!response.ok) {
throw new Error("Could not load trades. HTTP " + response.status);
}
const data = await response.json();
console.log("[TPBH] get-lots response:", data);
const rawLots = normalizeLotsResponse(data);
lots = rawLots.filter(function (lot) {
return getLotId(lot);
});
renderLots();
if (lots.length === 0) {
setStatus("No active trades found.", "tpbh-success");
} else {
setStatus("Loaded " + lots.length + " trade(s).", "tpbh-success");
}
} catch (error) {
console.error("[TPBH] loadLots error:", error);
setStatus("ERROR: " + error.message, "tpbh-error");
}
}
function normalizeLotsResponse(data) {
if (!data) {
return [];
}
if (Array.isArray(data)) {
return data;
}
if (Array.isArray(data.lots)) {
return data.lots;
}
if (Array.isArray(data.trades)) {
return data.trades;
}
if (Array.isArray(data.results)) {
return data.results;
}
if (data.data && Array.isArray(data.data.lots)) {
return data.data.lots;
}
if (data.data && Array.isArray(data.data.trades)) {
return data.data.trades;
}
return [];
}
function renderLots() {
const list = qs("#" + TPBH.listId);
if (!list) {
return;
}
if (!lots.length) {
list.innerHTML = `<div class="tpbh-empty">No active trades found.</div>`;
return;
}
list.innerHTML = lots.map(function (lot) {
const lotId = String(getLotId(lot));
const selected = selectedLotIds.has(lotId);
const title = getLotTitle(lot);
const wish = getLotWishlist(lot);
const itemCount = getLotItemCount(lot);
const offerCount = getLotOfferCount(lot);
const instantBuy = getLotInstantBuy(lot);
return `
<label class="tpbh-lot ${selected ? "tpbh-lot-selected" : ""}" data-lot-id="${escapeHtml(lotId)}">
<input type="checkbox" class="tpbh-check" value="${escapeHtml(lotId)}" ${selected ? "checked" : ""}>
<div class="tpbh-lot-body">
<div class="tpbh-lot-top">
<strong>Lot ${escapeHtml(lotId)}</strong>
<span>${escapeHtml(itemCount)} item(s)</span>
</div>
<div class="tpbh-lot-title">${escapeHtml(title)}</div>
${wish ? `<div class="tpbh-lot-wish">Wishlist: ${escapeHtml(wish)}</div>` : ""}
<div class="tpbh-lot-meta">
<span>Offers: ${escapeHtml(offerCount)}</span>
${instantBuy ? `<span>Instant Buy: ${escapeHtml(instantBuy)} NP</span>` : ""}
</div>
</div>
</label>
`;
}).join("");
qsa(".tpbh-check", list).forEach(function (checkbox) {
checkbox.addEventListener("change", function () {
const lotId = String(checkbox.value);
if (checkbox.checked) {
selectedLotIds.add(lotId);
} else {
selectedLotIds.delete(lotId);
}
renderLots();
updateSelectionStatus();
});
});
}
function updateSelectionStatus() {
if (!lots.length) {
setStatus("No active trades found.", "tpbh-success");
return;
}
setStatus(
selectedLotIds.size + " selected out of " + lots.length + " loaded trade(s).",
"tpbh-working"
);
}
async function deleteSelectedLots() {
const selected = Array.from(selectedLotIds);
if (!selected.length) {
alert("Please select at least one trade first.");
return;
}
await confirmAndDelete(selected, "selected");
}
async function deleteAllLoadedLots() {
const all = lots.map(function (lot) {
return String(getLotId(lot));
}).filter(Boolean);
if (!all.length) {
alert("No trades are loaded.");
return;
}
await confirmAndDelete(all, "all loaded");
}
async function confirmAndDelete(lotIds, label) {
if (isDeleting) {
return;
}
const estimatedFee = lotIds.length * 15000;
const message =
"You are about to delete/cancel " + lotIds.length + " " + label + " trade(s).\n\n" +
"Estimated fee using your latest example: " + formatNumber(estimatedFee) + " NP\n\n" +
"Items should be returned to your inventory, but this action cannot be undone from the script.\n\n" +
(
TPBH.requireTypedConfirm
? "Type DELETE to continue."
: "Press OK to continue."
);
if (TPBH.requireTypedConfirm) {
const typed = window.prompt(message, "");
if (typed !== "DELETE") {
setStatus("Cancelled. Nothing was deleted.", "tpbh-success");
return;
}
} else if (!window.confirm(message)) {
setStatus("Cancelled. Nothing was deleted.", "tpbh-success");
return;
}
await deleteLotsSequentially(lotIds);
}
async function deleteLotsSequentially(lotIds) {
isDeleting = true;
toggleButtons(true);
let successCount = 0;
let failCount = 0;
let totalCost = 0;
let totalItemsReturned = 0;
let totalOffersDeleted = 0;
const failed = [];
try {
for (let i = 0; i < lotIds.length; i++) {
const lotId = String(lotIds[i]);
setStatus(
"Deleting trade " + (i + 1) + " / " + lotIds.length + " | Lot " + lotId + "...",
"tpbh-working"
);
try {
const result = await deleteSingleLot(lotId);
console.log("[TPBH] delete result for lot " + lotId + ":", result);
if (result && result.success === true) {
successCount++;
totalCost += Number(result.deletion_cost || 0);
totalItemsReturned += Number(result.items_returned || 0);
totalOffersDeleted += Number(result.offers_deleted || 0);
selectedLotIds.delete(lotId);
lots = lots.filter(function (lot) {
return String(getLotId(lot)) !== lotId;
});
renderLots();
setStatus(
"Deleted " + successCount + " / " + lotIds.length +
" | Cost: " + formatNumber(totalCost) + " NP" +
" | Items returned: " + formatNumber(totalItemsReturned),
"tpbh-working"
);
} else {
failCount++;
failed.push({
lotId: lotId,
error: result && (result.error || result.message)
? result.error || result.message
: "Unknown error"
});
}
} catch (error) {
failCount++;
failed.push({
lotId: lotId,
error: error.message
});
}
await sleep(TPBH.delayBetweenDeletesMs);
}
if (failCount > 0) {
console.warn("[TPBH] Failed deletes:", failed);
setStatus(
"Done. Deleted " + successCount +
". Failed " + failCount +
". Cost: " + formatNumber(totalCost) + " NP. Check console.",
"tpbh-error"
);
} else {
setStatus(
"Done. Deleted " + successCount +
" trade(s). Cost: " + formatNumber(totalCost) +
" NP. Items returned: " + formatNumber(totalItemsReturned) +
". Offers deleted: " + formatNumber(totalOffersDeleted) + ".",
"tpbh-success"
);
}
} finally {
isDeleting = false;
toggleButtons(false);
window.setTimeout(function () {
loadLots();
}, 900);
}
}
async function deleteSingleLot(lotId) {
const numericLotId = Number(lotId);
if (!Number.isInteger(numericLotId) || numericLotId <= 0) {
throw new Error("Invalid lot ID: " + lotId);
}
const response = await fetch(TPBH.endpointDelete, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"x-requested-with": "XMLHttpRequest"
},
body: JSON.stringify({
lot_id: numericLotId
})
});
const text = await response.text();
if (!response.ok) {
throw new Error("HTTP " + response.status + ": " + text.slice(0, 200));
}
try {
return JSON.parse(text);
} catch (error) {
console.error("[TPBH] Non-JSON delete response:", text);
return {
success: false,
message: "Non-JSON response from delete-lot.php"
};
}
}
function toggleButtons(disabled) {
qsa("#" + TPBH.panelId + " button").forEach(function (button) {
button.disabled = disabled;
});
}
function getLotId(lot) {
return (
lot.lot_id ||
lot.lotId ||
lot.id ||
lot.trade_id ||
lot.tradeId ||
""
);
}
function getLotTitle(lot) {
if (lot.title) {
return lot.title;
}
if (lot.name) {
return lot.name;
}
if (lot.item_name) {
return lot.item_name;
}
if (Array.isArray(lot.lot_items) && lot.lot_items.length) {
return lot.lot_items.map(function (item) {
return item.name || item.item_name || item.itemName || "Item";
}).slice(0, 3).join(", ");
}
if (Array.isArray(lot.items) && lot.items.length) {
return lot.items.map(function (item) {
return item.name || item.item_name || item.itemName || "Item";
}).slice(0, 3).join(", ");
}
return "Trading Post Lot";
}
function getLotWishlist(lot) {
return (
lot.wishlist ||
lot.wish_list ||
lot.description ||
lot.lot_wishlist ||
lot.lotWishlist ||
""
);
}
function getLotItemCount(lot) {
if (typeof lot.item_count !== "undefined") {
return lot.item_count;
}
if (typeof lot.itemCount !== "undefined") {
return lot.itemCount;
}
if (typeof lot.count !== "undefined") {
return lot.count;
}
if (Array.isArray(lot.lot_items)) {
return lot.lot_items.length;
}
if (Array.isArray(lot.items)) {
return lot.items.length;
}
return "?";
}
function getLotOfferCount(lot) {
if (typeof lot.offer_count !== "undefined") {
return lot.offer_count;
}
if (typeof lot.offers_count !== "undefined") {
return lot.offers_count;
}
if (typeof lot.offerCount !== "undefined") {
return lot.offerCount;
}
if (typeof lot.offers !== "undefined") {
if (Array.isArray(lot.offers)) {
return lot.offers.length;
}
return lot.offers;
}
return "0";
}
function getLotInstantBuy(lot) {
const value =
lot.instant_buy_amount ||
lot.instantBuyAmount ||
lot.instant_buy ||
lot.instantBuy ||
"";
if (!value) {
return "";
}
return formatNumber(value);
}
function formatNumber(value) {
const number = Number(String(value).replace(/,/g, ""));
if (!Number.isFinite(number)) {
return String(value);
}
return new Intl.NumberFormat("en-US").format(number);
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function addCSS() {
if (qs("#tpbh-style")) {
return;
}
const style = document.createElement("style");
style.id = "tpbh-style";
style.textContent = `
#tpbh-panel {
background: #fff;
border: 3px solid #6d4b9b;
border-radius: 12px;
margin: 16px auto;
padding: 14px;
max-width: 980px;
color: #111;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.18);
font-family: Arial, sans-serif;
box-sizing: border-box;
}
.tpbh-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.tpbh-title {
font-size: 20px;
font-weight: 800;
color: #4f2f78;
}
.tpbh-subtitle {
font-size: 13px;
color: #444;
margin-top: 2px;
}
.tpbh-warning {
background: #fff3cd;
border: 1px solid #e7c76b;
color: #4d3900;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
font-size: 13px;
}
.tpbh-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.tpbh-btn {
border: 0;
border-radius: 8px;
padding: 9px 12px;
font-weight: 800;
cursor: pointer;
font-size: 13px;
}
.tpbh-btn:disabled {
opacity: 0.55;
cursor: default;
}
.tpbh-btn-neutral {
background: #e9e4f2;
color: #2c1d3d;
}
.tpbh-btn-danger {
background: #b71c1c;
color: #fff;
}
.tpbh-btn-dark {
background: #2b1d3a;
color: #fff;
}
.tpbh-status {
margin: 10px 0;
padding: 9px 10px;
border-radius: 8px;
background: #eef1f5;
color: #222;
font-size: 13px;
font-weight: 700;
}
.tpbh-status.tpbh-working {
background: #e6f0ff;
color: #0f3d75;
}
.tpbh-status.tpbh-success {
background: #dff3e3;
color: #145c25;
}
.tpbh-status.tpbh-error {
background: #f8d7da;
color: #842029;
}
.tpbh-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 10px;
margin-top: 10px;
}
.tpbh-lot {
display: flex;
gap: 10px;
align-items: flex-start;
border: 2px solid #ddd;
border-radius: 10px;
padding: 10px;
background: #fafafa;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.tpbh-lot:hover {
border-color: #8a6db1;
background: #f6f0ff;
}
.tpbh-lot-selected {
border-color: #5e2ca5;
background: #efe4ff;
}
.tpbh-check {
margin-top: 4px;
transform: scale(1.25);
}
.tpbh-lot-body {
min-width: 0;
flex: 1;
}
.tpbh-lot-top {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 13px;
color: #222;
margin-bottom: 4px;
}
.tpbh-lot-title {
font-weight: 800;
color: #222;
font-size: 14px;
margin-bottom: 4px;
overflow-wrap: anywhere;
}
.tpbh-lot-wish {
font-size: 12px;
color: #444;
margin-bottom: 4px;
overflow-wrap: anywhere;
}
.tpbh-lot-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 12px;
color: #555;
}
.tpbh-empty {
padding: 14px;
border: 1px dashed #bbb;
border-radius: 8px;
color: #555;
background: #fafafa;
}
@media (max-width: 600px) {
#tpbh-panel {
margin: 10px;
padding: 10px;
}
.tpbh-header {
flex-direction: column;
align-items: stretch;
}
.tpbh-actions {
flex-direction: column;
}
.tpbh-btn {
width: 100%;
}
}
`;
document.head.appendChild(style);
}
})();