// ==UserScript==
// @name Neopets - Trading Post Item Pricer
// @namespace https://scriptneo.com/
// @version 2.7.0
// @description Trading Post item pricing buttons that undercut the lowest Instant Buy by 1 NP.
// @author Scriptneo
// @match https://www.neopets.com/*
// @grant none
// @run-at document-end
// @downloadURL https://www.scriptneo.com/scripts/download.php?id=42
// @updateURL https://www.scriptneo.com/scripts/download.php?id=42
// ==/UserScript==
(function () {
"use strict";
const API_URL = "https://www.neopets.com/np-templates/ajax/island/tradingpost/tradingpost-list.php";
const TP_INVENTORY_URL = "https://www.neopets.com/np-templates/ajax/island/tradingpost/inventory-list.php";
const TP_PROCESS_URL = "https://www.neopets.com/np-templates/ajax/island/tradingpost/process-tradingpost.php";
const DEFAULTS = {
sort: "newest",
minPrice: 1,
maxPrice: 0,
priceMode: "range",
nextPricePercent: 60,
openLinksNewTab: true,
autoOpenOnTradingPost: true,
autoSearchOnEnter: true,
mode: "single",
batchDelayMs: 350,
batchMaxItems: 10000,
batchReturn: "best",
batchSort: "price",
criteria: "item_exact",
sellerUndercutNP: 1,
sellerListInstantBuy: true,
sellerWishlistTemplate: "Lowest TP Instant Buy - {UNDERCUT} NP"
};
let state = {
running: false,
abort: null,
isOpen: false,
drag: { active: false, offsetX: 0, offsetY: 0 },
liveBatchRows: [],
liveBatchStats: {
processed: 0,
totalItems: 0,
foundItems: 0,
totalIBLots: 0
}
};
function safeText(s) {
return String(s ?? "");
}
function formatNP(n) {
const num = Number(n || 0);
if (!Number.isFinite(num)) return "0";
return num.toLocaleString("en-US");
}
function el(tag, attrs = {}, children = []) {
const node = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === "class") {
node.className = v;
} else if (k === "style") {
node.setAttribute("style", v);
} else if (k.startsWith("on") && typeof v === "function") {
node.addEventListener(k.slice(2), v);
} else {
node.setAttribute(k, String(v));
}
}
for (const child of children) {
if (child == null) continue;
if (typeof child === "string") node.appendChild(document.createTextNode(child));
else node.appendChild(child);
}
return node;
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const t = setTimeout(resolve, ms);
if (signal) {
signal.addEventListener("abort", () => {
clearTimeout(t);
reject(new Error("Aborted"));
}, { once: true });
}
});
}
function stopRun() {
if (state.abort) {
try {
state.abort.abort();
} catch (e) {}
}
state.abort = null;
state.running = false;
setBusy(false);
}
function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}
function isTradingPostPage() {
return /\/island\/tradingpost\.phtml/i.test(location.pathname);
}
function setBusy(busy) {
const btn1 = document.getElementById("tp-ibf-search");
const btn2 = document.getElementById("tp-ibf-batch-run");
const btn3 = document.getElementById("tp-ibf-seller-list");
if (btn1) btn1.disabled = !!busy;
if (btn2) btn2.disabled = !!busy;
if (btn3) btn3.disabled = !!busy;
}
async function tpSearchExactItem(itemName, signal) {
const payload = {
type: "browse",
criteria: DEFAULTS.criteria,
search_string: itemName,
sort: DEFAULTS.sort,
page: 1
};
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
credentials: "include",
body: JSON.stringify(payload),
signal
});
if (!res.ok) {
throw new Error("HTTP " + res.status + " " + res.statusText);
}
const data = await res.json();
if (!data || data.success !== true) {
throw new Error("API returned success=false");
}
return data;
}
function injectStylesOnce() {
if (document.getElementById("tp-ibf-style")) return;
const css = `
#tp-ibf-launcher{
position:fixed;right:16px;bottom:16px;z-index:999999;
border:1px solid #1f6feb;background:#1f6feb;color:#fff;border-radius:999px;
padding:10px 14px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
font-size:13px;font-weight:900;cursor:pointer;box-shadow:0 10px 25px rgba(0,0,0,0.22);
}
#tp-ibf-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.35);z-index:999998;}
#tp-ibf-modal{
position:fixed;left:50%;top:14%;transform:translateX(-50%);
width:560px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);
background:#fff;border:1px solid #d0d7de;border-radius:14px;
box-shadow:0 18px 50px rgba(0,0,0,0.26);z-index:999999;overflow:hidden;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
}
#tp-ibf-header{
display:flex;align-items:center;justify-content:space-between;
padding:12px;border-bottom:1px solid #eaeef2;background:#f6f8fa;
user-select:none;cursor:move;
}
#tp-ibf-title{display:flex;flex-direction:column;gap:2px;}
#tp-ibf-title .h1{font-weight:950;font-size:14px;color:#111;line-height:1.1;}
#tp-ibf-title .h2{font-size:12px;color:#57606a;}
#tp-ibf-header .btns{display:flex;gap:8px;align-items:center;}
.tp-ibf-btn{
border:1px solid #d0d7de;background:#fff;border-radius:10px;
padding:6px 10px;cursor:pointer;font-size:12px;font-weight:900;
}
.tp-ibf-body{padding:12px;overflow:auto;max-height:calc(100vh - 200px);}
.tp-ibf-row{display:flex;gap:8px;align-items:center;margin-bottom:10px;}
.tp-ibf-input{
flex:1;border:1px solid #d0d7de;border-radius:12px;padding:10px 12px;
font-size:13px;outline:none;
}
.tp-ibf-search{
border:1px solid #1f6feb;background:#1f6feb;color:#fff;border-radius:12px;
padding:10px 12px;font-size:13px;font-weight:950;cursor:pointer;white-space:nowrap;
}
.tp-ibf-search:disabled{opacity:0.6;cursor:not-allowed;}
.tp-ibf-box{border:1px solid #eaeef2;border-radius:12px;padding:10px;margin-bottom:10px;background:#fff;}
.tp-ibf-grid{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;}
.tp-ibf-field{display:flex;flex-direction:column;gap:6px;min-width:0;}
.tp-ibf-label{font-size:11px;color:#57606a;font-weight:900;}
.tp-ibf-num{
width:160px;border:1px solid #d0d7de;border-radius:10px;padding:8px 10px;
font-size:12px;outline:none;
}
.tp-ibf-wide{width:220px;}
.tp-ibf-help{font-size:11px;color:#57606a;line-height:1.35;margin-top:6px;}
.tp-ibf-check{
display:flex;gap:8px;align-items:center;font-size:12px;color:#24292f;
cursor:pointer;user-select:none;
}
.tp-ibf-tabs{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px;}
.tp-ibf-tab{
border:1px solid #d0d7de;background:#fff;border-radius:999px;padding:6px 10px;
cursor:pointer;font-size:12px;font-weight:950;color:#24292f;
}
.tp-ibf-tab.active{border-color:#1f6feb;color:#1f6feb;}
#tp-ibf-status{margin:8px 0 10px 0;font-size:12px;color:#57606a;}
#tp-ibf-results .summary{font-size:12px;color:#24292f;font-weight:950;margin-bottom:10px;}
.tp-ibf-card{border:1px solid #d0d7de;border-radius:14px;padding:12px;background:#fff;margin-bottom:10px;}
.tp-ibf-live-empty{
border:1px dashed #d0d7de;background:#f6f8fa;color:#57606a;border-radius:12px;
padding:12px;font-size:12px;line-height:1.35;margin-bottom:10px;
}
.tp-ibf-live-found{
animation:tpIbfFoundFlash 0.8s ease-out 1;
}
@keyframes tpIbfFoundFlash{
0%{box-shadow:0 0 0 3px rgba(26,127,55,0.22);}
100%{box-shadow:none;}
}
.tp-ibf-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;}
.tp-ibf-lotid{font-weight:950;font-size:13px;color:#111;word-break:break-word;}
.tp-ibf-meta{font-size:12px;color:#57606a;display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:4px;}
.tp-ibf-badge{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:900;border:1px solid rgba(0,0,0,0.06);}
.tp-ibf-price{font-weight:950;font-size:14px;color:#1a7f37;text-align:right;}
.tp-ibf-open{
display:inline-block;text-decoration:none;border:1px solid #1f6feb;background:#1f6feb;color:#fff;
border-radius:12px;padding:8px 10px;font-size:12px;font-weight:950;margin-top:6px;float:right;
}
.tp-ibf-items{margin-top:10px;}
.tp-ibf-items .ttl{font-size:11px;color:#57606a;font-weight:950;margin-bottom:6px;}
.tp-ibf-items .list{border:1px solid #eaeef2;border-radius:12px;padding:10px;background:#fff;}
.tp-ibf-items .line{font-size:12px;color:#24292f;line-height:1.35;margin:2px 0;word-break:break-word;}
.tp-ibf-error{border:1px solid #ffebe9;background:#fff1f0;color:#cf222e;border-radius:12px;padding:10px;}
.tp-ibf-error .t1{font-weight:950;margin-bottom:4px;}
.tp-ibf-error .t2{font-size:12px;line-height:1.35;}
.tp-ibf-mini{
font-size:12px;color:#57606a;line-height:1.35;
border:1px solid #eaeef2;background:#f6f8fa;border-radius:12px;padding:10px;margin-bottom:10px;
}
.tp-ibf-smallsel{
border:1px solid #d0d7de;background:#fff;border-radius:10px;padding:8px 10px;
font-size:12px;outline:none;
}
.tp-ibf-seller-panel{
border:1px solid #d0d7de;background:#f6f8fa;border-radius:14px;padding:10px;margin-bottom:10px;
}
.tp-ibf-seller-title{
font-size:13px;font-weight:950;color:#111;margin-bottom:8px;
}
.tp-ibf-seller-actions{
display:flex;gap:8px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin-top:8px;
}
.tp-ibf-success{
border:1px solid #aceebb;background:#dafbe1;color:#116329;border-radius:12px;padding:10px;font-size:12px;line-height:1.35;
}
.tp-ibf-warnbox{
border:1px solid #fff8c5;background:#fff8c5;color:#7d4e00;border-radius:12px;padding:10px;font-size:12px;line-height:1.35;margin-bottom:10px;
}
.tp-ibf-inline-wrap{
display:flex;flex-direction:column;align-items:center;gap:4px;margin-top:6px;
}
.tp-ibf-inline-price-btn{
border:1px solid #1f6feb;background:#1f6feb;color:#fff;border-radius:999px;
padding:4px 8px;font-size:10px;font-weight:950;line-height:1.1;cursor:pointer;
box-shadow:0 2px 7px rgba(0,0,0,0.12);
}
.tp-ibf-inline-price-btn:hover{filter:brightness(0.96);}
.tp-ibf-inline-price-btn:disabled{opacity:0.65;cursor:not-allowed;}
.tp-ibf-inline-status{
font-size:10px;font-weight:900;line-height:1.2;text-align:center;max-width:100%;word-break:break-word;
}
.tp-ibf-inline-status.ok{color:#1a7f37;}
.tp-ibf-inline-status.warn{color:#9a6700;}
.tp-ibf-inline-status.err{color:#cf222e;}
`.trim();
const style = document.createElement("style");
style.id = "tp-ibf-style";
style.textContent = css;
document.head.appendChild(style);
}
function buildLauncher() {
if (document.getElementById("tp-ibf-launcher")) return;
const btn = document.createElement("button");
btn.id = "tp-ibf-launcher";
btn.type = "button";
btn.textContent = "Instant Buy Finder";
btn.addEventListener("click", () => openModal());
document.body.appendChild(btn);
}
function buildModal() {
if (document.getElementById("tp-ibf-modal")) return;
const backdrop = el("div", { id: "tp-ibf-backdrop" });
backdrop.addEventListener("click", () => closeModal());
const modal = el("div", { id: "tp-ibf-modal", role: "dialog", "aria-modal": "true" });
const header = el("div", { id: "tp-ibf-header" }, [
el("div", { id: "tp-ibf-title" }, [
el("div", { class: "h1" }, ["Instant Buy Finder"]),
el("div", { class: "h2" }, ["Exact match only. Instant Buy only. Batch results appear live."])
]),
el("div", { class: "btns" }, [
el("button", { class: "tp-ibf-btn", id: "tp-ibf-stop", type: "button" }, ["Stop"]),
el("button", { class: "tp-ibf-btn", id: "tp-ibf-reset", type: "button" }, ["Reset"]),
el("button", { class: "tp-ibf-btn", id: "tp-ibf-close", type: "button" }, ["Close"])
])
]);
const tabs = el("div", { class: "tp-ibf-tabs" }, [
el("button", { id: "tp-ibf-tab-single", class: "tp-ibf-tab", type: "button" }, ["Single mode"]),
el("button", { id: "tp-ibf-tab-batch", class: "tp-ibf-tab", type: "button" }, ["Batch mode"])
]);
const singleHint = el("div", { id: "tp-ibf-hint-single", class: "tp-ibf-mini" }, [
"Single mode: type the exact item name. Example: \"Varia Stamp\" will not match \"Big Bang Varia Stamp\"."
]);
const batchHint = el("div", { id: "tp-ibf-hint-batch", class: "tp-ibf-mini", style: "display:none;" }, [
"Batch mode: one exact item name per line. Found lots appear immediately as each item finishes searching."
]);
const singleRow = el("div", { id: "tp-ibf-row-single", class: "tp-ibf-row" }, [
el("input", {
id: "tp-ibf-query",
class: "tp-ibf-input",
type: "text",
placeholder: "Exact item name, for example: Varia Stamp"
}),
el("button", { id: "tp-ibf-search", class: "tp-ibf-search", type: "button" }, ["Search"])
]);
const batchRow = el("div", { id: "tp-ibf-row-batch", style: "display:none;margin-bottom:10px;" }, [
el("textarea", {
id: "tp-ibf-batch",
style: "width:100%;min-height:120px;border:1px solid #d0d7de;border-radius:12px;padding:10px 12px;font-size:13px;outline:none;resize:vertical;"
}, [""]),
el("div", { style: "display:flex;gap:8px;align-items:center;justify-content:space-between;margin-top:8px;flex-wrap:wrap;" }, [
el("div", { style: "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" }, [
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Return"]),
el("select", { id: "tp-ibf-batch-return", class: "tp-ibf-smallsel" }, [
el("option", { value: "best" }, ["Best (cheapest per item)"]),
el("option", { value: "all" }, ["All lots (can be long)"])
])
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Sort batch by"]),
el("select", { id: "tp-ibf-batch-sort", class: "tp-ibf-smallsel" }, [
el("option", { value: "price" }, ["Price (low to high)"]),
el("option", { value: "name" }, ["Item name (A-Z)"])
])
])
]),
el("button", { id: "tp-ibf-batch-run", class: "tp-ibf-search", type: "button" }, ["Run batch"])
])
]);
const settingsBox = el("div", { class: "tp-ibf-box" }, [
el("div", { class: "tp-ibf-grid" }, [
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Min NP"]),
el("input", {
id: "tp-ibf-minprice",
class: "tp-ibf-num",
type: "number",
min: "0",
value: String(DEFAULTS.minPrice)
})
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Max NP"]),
el("input", {
id: "tp-ibf-maxprice",
class: "tp-ibf-num",
type: "number",
min: "0",
value: String(DEFAULTS.maxPrice)
})
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Pricing filter"]),
el("select", { id: "tp-ibf-price-mode", class: "tp-ibf-smallsel tp-ibf-wide" }, [
el("option", { value: "range" }, ["Min/Max NP only"]),
el("option", { value: "next_percent" }, ["Deal % vs next higher price"])
])
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Max % of next price"]),
el("input", {
id: "tp-ibf-next-percent",
class: "tp-ibf-num",
type: "number",
min: "1",
max: "100",
value: String(DEFAULTS.nextPricePercent)
}),
el("div", { class: "tp-ibf-help" }, [
"Example: 60 means show 600k only if the next higher lot is 1m or more."
])
]),
el("label", { class: "tp-ibf-check", style: "margin-top:18px;" }, [
el("input", { id: "tp-ibf-newtab", type: "checkbox" }),
el("span", {}, ["Open links in new tab"])
])
])
]);
const sellerPanel = el("div", { class: "tp-ibf-seller-panel" }, [
el("div", { class: "tp-ibf-seller-title" }, ["Seller Auto-Lister"]),
el("div", { class: "tp-ibf-warnbox" }, [
"This searches the exact item, calculates the lowest Instant Buy price, subtracts your undercut, finds the matching item in your inventory, then creates the lot after you confirm."
]),
el("div", { class: "tp-ibf-grid" }, [
el("div", { class: "tp-ibf-field", style: "flex:1;min-width:220px;" }, [
el("div", { class: "tp-ibf-label" }, ["Item name to list"]),
el("input", {
id: "tp-ibf-seller-item",
class: "tp-ibf-input",
type: "text",
placeholder: "Exact inventory item name"
})
]),
el("div", { class: "tp-ibf-field" }, [
el("div", { class: "tp-ibf-label" }, ["Lowest minus NP"]),
el("input", {
id: "tp-ibf-seller-undercut",
class: "tp-ibf-num",
type: "number",
min: "0",
value: String(DEFAULTS.sellerUndercutNP)
})
]),
el("label", { class: "tp-ibf-check", style: "margin-top:18px;" }, [
el("input", { id: "tp-ibf-seller-instant", type: "checkbox" }),
el("span", {}, ["Enable Instant Buy"])
]),
el("div", { class: "tp-ibf-field", style: "flex:1;min-width:220px;" }, [
el("div", { class: "tp-ibf-label" }, ["Wishlist text"]),
el("input", {
id: "tp-ibf-seller-wishlist",
class: "tp-ibf-input",
type: "text",
value: DEFAULTS.sellerWishlistTemplate
}),
el("div", { class: "tp-ibf-help" }, [
"Tokens: {PRICE}, {LOWEST}, {UNDERCUT}, {ITEM}. Leave blank if you only want Instant Buy."
])
])
]),
el("div", { class: "tp-ibf-seller-actions" }, [
el("button", { id: "tp-ibf-seller-copy-query", class: "tp-ibf-btn", type: "button" }, ["Use current search item"]),
el("button", { id: "tp-ibf-seller-list", class: "tp-ibf-search", type: "button" }, ["Find Lowest & List Item"])
])
]);
const body = el("div", { class: "tp-ibf-body" }, [
tabs,
singleHint,
batchHint,
singleRow,
batchRow,
settingsBox,
sellerPanel,
el("div", { id: "tp-ibf-status" }, ["Idle."]),
el("div", { id: "tp-ibf-results" })
]);
modal.appendChild(header);
modal.appendChild(body);
document.body.appendChild(backdrop);
document.body.appendChild(modal);
const newTab = document.getElementById("tp-ibf-newtab");
if (newTab) newTab.checked = DEFAULTS.openLinksNewTab;
const batchReturn = document.getElementById("tp-ibf-batch-return");
if (batchReturn) batchReturn.value = DEFAULTS.batchReturn;
const batchSort = document.getElementById("tp-ibf-batch-sort");
if (batchSort) batchSort.value = DEFAULTS.batchSort;
const priceMode = document.getElementById("tp-ibf-price-mode");
if (priceMode) priceMode.value = DEFAULTS.priceMode;
const sellerInstant = document.getElementById("tp-ibf-seller-instant");
if (sellerInstant) sellerInstant.checked = DEFAULTS.sellerListInstantBuy;
setMode(DEFAULTS.mode);
document.getElementById("tp-ibf-close")?.addEventListener("click", () => closeModal());
document.getElementById("tp-ibf-reset")?.addEventListener("click", () => resetUI());
document.getElementById("tp-ibf-stop")?.addEventListener("click", () => {
stopRun();
setStatus("Stopped.", "warn");
});
document.getElementById("tp-ibf-tab-single")?.addEventListener("click", () => setMode("single"));
document.getElementById("tp-ibf-tab-batch")?.addEventListener("click", () => setMode("batch"));
document.getElementById("tp-ibf-search")?.addEventListener("click", () => runSingle());
document.getElementById("tp-ibf-batch-run")?.addEventListener("click", () => runBatch());
document.getElementById("tp-ibf-seller-list")?.addEventListener("click", () => runAutoList());
document.getElementById("tp-ibf-seller-copy-query")?.addEventListener("click", () => {
const query = (document.getElementById("tp-ibf-query")?.value ?? "").trim();
if (query) {
const item = document.getElementById("tp-ibf-seller-item");
if (item) item.value = query;
setStatus("Copied current search item into Seller Auto-Lister.", "ok");
} else {
setStatus("Enter a Single mode item first, then click Use current search item.", "err");
}
});
document.getElementById("tp-ibf-query")?.addEventListener("keydown", (e) => {
if (!DEFAULTS.autoSearchOnEnter) return;
if (e.key === "Enter") {
e.preventDefault();
runSingle();
}
});
enableDrag(modal, header);
}
function openModal() {
injectStylesOnce();
buildModal();
const backdrop = document.getElementById("tp-ibf-backdrop");
const modal = document.getElementById("tp-ibf-modal");
if (backdrop) backdrop.style.display = "block";
if (modal) modal.style.display = "block";
state.isOpen = true;
setTimeout(() => {
if (getMode() === "single") {
document.getElementById("tp-ibf-query")?.focus();
} else {
document.getElementById("tp-ibf-batch")?.focus();
}
}, 50);
}
function closeModal() {
stopRun();
const backdrop = document.getElementById("tp-ibf-backdrop");
const modal = document.getElementById("tp-ibf-modal");
if (backdrop) backdrop.style.display = "none";
if (modal) modal.style.display = "none";
state.isOpen = false;
}
function resetUI() {
const q = document.getElementById("tp-ibf-query");
const ta = document.getElementById("tp-ibf-batch");
const minp = document.getElementById("tp-ibf-minprice");
const maxp = document.getElementById("tp-ibf-maxprice");
const priceMode = document.getElementById("tp-ibf-price-mode");
const nextPercent = document.getElementById("tp-ibf-next-percent");
const results = document.getElementById("tp-ibf-results");
if (q) q.value = "";
if (ta) ta.value = "";
if (minp) minp.value = String(DEFAULTS.minPrice);
if (maxp) maxp.value = String(DEFAULTS.maxPrice);
if (priceMode) priceMode.value = DEFAULTS.priceMode;
if (nextPercent) nextPercent.value = String(DEFAULTS.nextPricePercent);
if (results) results.innerHTML = "";
state.liveBatchRows = [];
state.liveBatchStats = {
processed: 0,
totalItems: 0,
foundItems: 0,
totalIBLots: 0
};
setStatus("Idle.", "warn");
}
function setStatus(text, kind) {
const status = document.getElementById("tp-ibf-status");
if (!status) return;
let color = "#57606a";
if (kind === "ok") color = "#1a7f37";
if (kind === "warn") color = "#9a6700";
if (kind === "err") color = "#cf222e";
status.textContent = text;
status.style.color = color;
}
function badge(text, bg, fg) {
return el("span", {
class: "tp-ibf-badge",
style: "background:" + bg + ";color:" + fg + ";"
}, [text]);
}
function setMode(mode) {
const m = mode === "batch" ? "batch" : "single";
DEFAULTS.mode = m;
const tabSingle = document.getElementById("tp-ibf-tab-single");
const tabBatch = document.getElementById("tp-ibf-tab-batch");
const hintSingle = document.getElementById("tp-ibf-hint-single");
const hintBatch = document.getElementById("tp-ibf-hint-batch");
const rowSingle = document.getElementById("tp-ibf-row-single");
const rowBatch = document.getElementById("tp-ibf-row-batch");
if (tabSingle) tabSingle.classList.toggle("active", m === "single");
if (tabBatch) tabBatch.classList.toggle("active", m === "batch");
if (hintSingle) hintSingle.style.display = m === "single" ? "block" : "none";
if (hintBatch) hintBatch.style.display = m === "batch" ? "block" : "none";
if (rowSingle) rowSingle.style.display = m === "single" ? "flex" : "none";
if (rowBatch) rowBatch.style.display = m === "batch" ? "block" : "none";
const results = document.getElementById("tp-ibf-results");
if (results) results.innerHTML = "";
setStatus("Mode: " + (m === "single" ? "Single" : "Batch") + ".", "warn");
}
function getMode() {
return DEFAULTS.mode === "batch" ? "batch" : "single";
}
function enableDrag(modal, handle) {
if (!modal || !handle) return;
const onPointerDown = (e) => {
if (e.button !== 0 && e.pointerType !== "touch") return;
state.drag.active = true;
const rect = modal.getBoundingClientRect();
modal.style.transform = "none";
modal.style.left = rect.left + "px";
modal.style.top = rect.top + "px";
state.drag.offsetX = e.clientX - rect.left;
state.drag.offsetY = e.clientY - rect.top;
try {
handle.setPointerCapture(e.pointerId);
} catch (err) {}
e.preventDefault();
};
const onPointerMove = (e) => {
if (!state.drag.active) return;
const modalW = modal.offsetWidth || 560;
const modalH = modal.offsetHeight || 320;
const x = clamp(e.clientX - state.drag.offsetX, 8, window.innerWidth - modalW - 8);
const y = clamp(e.clientY - state.drag.offsetY, 8, window.innerHeight - modalH - 8);
modal.style.left = x + "px";
modal.style.top = y + "px";
};
const onPointerUp = (e) => {
if (!state.drag.active) return;
state.drag.active = false;
try {
handle.releasePointerCapture(e.pointerId);
} catch (err) {}
};
handle.addEventListener("pointerdown", onPointerDown);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
}
function readPriceFilters() {
const minPrice = Number(document.getElementById("tp-ibf-minprice")?.value ?? DEFAULTS.minPrice) || 0;
const maxPrice = Number(document.getElementById("tp-ibf-maxprice")?.value ?? DEFAULTS.maxPrice) || 0;
const priceMode = document.getElementById("tp-ibf-price-mode")?.value || DEFAULTS.priceMode;
const nextPricePercentRaw = Number(document.getElementById("tp-ibf-next-percent")?.value ?? DEFAULTS.nextPricePercent) || DEFAULTS.nextPricePercent;
const nextPricePercent = clamp(nextPricePercentRaw, 1, 100);
return {
minPrice,
maxPrice,
priceMode: priceMode === "next_percent" ? "next_percent" : "range",
nextPricePercent
};
}
function decorateNextPriceDeals(sortedLots, nextPricePercent) {
const pct = clamp(Number(nextPricePercent || DEFAULTS.nextPricePercent), 1, 100);
const multiplier = pct / 100;
for (let i = 0; i < sortedLots.length; i++) {
const lot = sortedLots[i];
const nextLot = sortedLots[i + 1] || null;
const price = Number(lot.instant_buy_amount || 0);
const nextPrice = Number(nextLot?.instant_buy_amount || 0);
delete lot._tpIbfNextPrice;
delete lot._tpIbfDealPercent;
delete lot._tpIbfDealSavings;
if (nextLot && nextPrice > 0) {
lot._tpIbfNextPrice = nextPrice;
lot._tpIbfDealPercent = Math.round((price / nextPrice) * 1000) / 10;
lot._tpIbfDealSavings = Math.max(0, nextPrice - price);
lot._tpIbfPassesNextPercent = price <= (nextPrice * multiplier);
} else {
lot._tpIbfPassesNextPercent = false;
}
}
return sortedLots;
}
function filterAndSortInstantBuy(lots, minPrice, maxPrice, priceMode, nextPricePercent) {
let ibLots = (Array.isArray(lots) ? lots : []).filter(l => Number(l.instant_buy_amount || 0) > 0);
ibLots = ibLots.filter(l => {
const p = Number(l.instant_buy_amount || 0);
if (p < minPrice) return false;
if (maxPrice > 0 && p > maxPrice) return false;
return true;
});
ibLots.sort((a, b) => {
const ap = Number(a.instant_buy_amount || 0);
const bp = Number(b.instant_buy_amount || 0);
if (ap !== bp) return ap - bp;
return Number(b.lot_id || 0) - Number(a.lot_id || 0);
});
ibLots = decorateNextPriceDeals(ibLots, nextPricePercent);
if (priceMode === "next_percent") {
ibLots = ibLots.filter(l => !!l._tpIbfPassesNextPercent);
}
return ibLots;
}
function renderNoLots(label) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
results.appendChild(el("div", { class: "tp-ibf-error" }, [
el("div", { class: "t1" }, ["No Instant Buy lots found"]),
el("div", { class: "t2" }, [
"Exact search: \"" + safeText(label) + "\"."
])
]));
}
function renderSingleResults(lots, query) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
if (!lots.length) {
renderNoLots(query);
return;
}
results.appendChild(el("div", { class: "summary" }, [
"Found " + lots.length + " Instant Buy lot" + (lots.length === 1 ? "" : "s") + " (low to high)"
]));
for (const lot of lots) {
results.appendChild(renderLotCard(lot, false));
}
}
function setupLiveBatchResults(totalItems) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
state.liveBatchRows = [];
state.liveBatchStats = {
processed: 0,
totalItems,
foundItems: 0,
totalIBLots: 0
};
results.innerHTML = "";
results.appendChild(el("div", {
id: "tp-ibf-batch-summary",
class: "summary"
}, [
"Batch started. Searching 0/" + totalItems + ". Found 0 item(s)."
]));
results.appendChild(el("div", {
id: "tp-ibf-batch-empty",
class: "tp-ibf-live-empty"
}, [
"No matching Instant Buy lots found yet. Results will appear here as soon as each item finishes."
]));
results.appendChild(el("div", {
id: "tp-ibf-batch-live-list"
}));
}
function updateLiveBatchSummary() {
const summary = document.getElementById("tp-ibf-batch-summary");
if (!summary) return;
const stats = state.liveBatchStats;
summary.textContent =
"Batch progress: " +
stats.processed +
"/" +
stats.totalItems +
". Found " +
stats.foundItems +
" item" +
(stats.foundItems === 1 ? "" : "s") +
" with " +
stats.totalIBLots +
" Instant Buy lot" +
(stats.totalIBLots === 1 ? "" : "s") +
".";
}
function sortLiveBatchRows(modeSort) {
state.liveBatchRows.sort((a, b) => {
if (modeSort === "name") {
return safeText(a.item).localeCompare(safeText(b.item));
}
const aLot = a.bestLot || (Array.isArray(a.lots) ? a.lots[0] : null);
const bLot = b.bestLot || (Array.isArray(b.lots) ? b.lots[0] : null);
const ap = Number(aLot?.instant_buy_amount || 0);
const bp = Number(bLot?.instant_buy_amount || 0);
if (ap !== bp) return ap - bp;
return safeText(a.item).localeCompare(safeText(b.item));
});
}
function addLiveBatchRow(row, modeSort) {
const empty = document.getElementById("tp-ibf-batch-empty");
const list = document.getElementById("tp-ibf-batch-live-list");
if (!list) return;
if (empty) empty.style.display = "none";
state.liveBatchRows.push(row);
sortLiveBatchRows(modeSort);
list.innerHTML = "";
for (const liveRow of state.liveBatchRows) {
const card = renderBatchCard(liveRow);
card.classList.add("tp-ibf-live-found");
list.appendChild(card);
}
}
function renderBatchResults(rows, label) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
if (!rows.length) {
renderNoLots(label);
return;
}
results.appendChild(el("div", { class: "summary" }, [
"Batch results: " + rows.length + " item" + (rows.length === 1 ? "" : "s")
]));
for (const row of rows) {
results.appendChild(renderBatchCard(row));
}
}
function renderBatchCard(row) {
const modeReturn = document.getElementById("tp-ibf-batch-return")?.value || DEFAULTS.batchReturn;
const card = el("div", { class: "tp-ibf-card" }, [
el("div", { class: "tp-ibf-lotid" }, [safeText(row.item)])
]);
if (modeReturn === "best") {
card.appendChild(el("div", { style: "margin-top:8px;" }, [
renderLotCard(row.bestLot, true)
]));
} else {
card.appendChild(el("div", { style: "margin-top:8px;" }, [
el("div", {
style: "font-size:12px;color:#57606a;font-weight:900;margin-bottom:6px;"
}, [
"Lots (" + row.lots.length + ")"
])
]));
for (const lot of row.lots) {
card.appendChild(renderLotCard(lot, true));
}
}
return card;
}
function renderLotCard(lot, compact) {
const openNewTab = document.getElementById("tp-ibf-newtab")?.checked ?? true;
const link = safeText(lot.link || "");
const href = link.startsWith("//")
? ("https:" + link)
: (link.startsWith("http") ? link : ("https://www.neopets.com" + link.replace(/^\/+/, "/")));
const price = Number(lot.instant_buy_amount || 0);
const items = (lot.items || []).map(it => {
const amount = Number(it.amount || 1);
const name = safeText(it.name);
const sub = safeText(it.sub_name);
return (amount > 1 ? (amount + "x ") : "") + name + (sub ? (" " + sub) : "");
});
const owner = safeText(lot.owner);
const isPremium = !!lot.premium_owner;
const outer = el("div", {
class: compact ? "" : "tp-ibf-card",
style: compact ? "border:1px solid #eaeef2;border-radius:12px;padding:10px;margin-top:8px;background:#fff;" : ""
}, [
el("div", { class: "tp-ibf-top" }, [
el("div", {}, [
el("div", { class: "tp-ibf-lotid" }, ["Lot #" + safeText(lot.lot_id)]),
el("div", { class: "tp-ibf-meta" }, [
el("span", {}, ["Owner: " + owner]),
badge(
isPremium ? "Premium" : "Standard",
isPremium ? "#ddf4ff" : "#f6f8fa",
isPremium ? "#0969da" : "#57606a"
),
badge("Offers: " + safeText(lot.offer_count ?? 0), "#fff8c5", "#9a6700"),
lot._tpIbfNextPrice ? badge("Next: " + formatNP(lot._tpIbfNextPrice) + " NP", "#f6f8fa", "#57606a") : null,
lot._tpIbfDealPercent ? badge(lot._tpIbfDealPercent + "% of next", "#dafbe1", "#1a7f37") : null,
lot._tpIbfDealSavings ? badge("Gap: " + formatNP(lot._tpIbfDealSavings) + " NP", "#ddf4ff", "#0969da") : null
])
]),
el("div", {}, [
el("div", { class: "tp-ibf-price" }, [formatNP(price) + " NP"]),
el("a", {
class: "tp-ibf-open",
href,
target: openNewTab ? "_blank" : "_self",
rel: "noopener noreferrer"
}, ["Open Lot"])
])
]),
el("div", { class: "tp-ibf-items" }, [
el("div", { class: "ttl" }, ["Items"]),
el("div", { class: "list" }, items.length
? items.map(s => el("div", { class: "line" }, [s]))
: [el("div", { class: "line", style: "color:#57606a;" }, ["(No items listed)"])])
])
]);
return outer;
}
function toRawNP(value) {
const raw = safeText(value).replace(/[^\d]/g, "");
const n = Number(raw);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.floor(n));
}
function getRefCk() {
try {
if (typeof window.getCK === "function") {
const ck = window.getCK();
if (ck) return String(ck);
}
} catch (e) {}
const candidates = Array.from(document.scripts || [])
.map(s => s.textContent || "")
.join("\n");
const match = candidates.match(/function\s+getCK\s*\(\)\s*\{\s*return\s+['"]([^'"]+)['"]/i)
|| candidates.match(/["_']_ref_ck["_']\s*:\s*['"]([^'"]+)['"]/i);
return match ? match[1] : "";
}
async function tpLoadInventoryItems(signal) {
const params = new URLSearchParams({
itemType: "",
alpha: "newest"
});
const res = await fetch(TP_INVENTORY_URL + "?" + params.toString(), {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest"
},
credentials: "include",
signal
});
if (!res.ok) {
throw new Error("Inventory HTTP " + res.status + " " + res.statusText);
}
const data = await res.json();
if (!data || data.success !== true || !Array.isArray(data.items)) {
throw new Error(data?.message || "Inventory API returned no items");
}
return data.items;
}
function normalizeItemName(name) {
return safeText(name).replace(/\s+/g, " ").trim().toLowerCase();
}
function findInventoryItemByExactName(items, itemName) {
const target = normalizeItemName(itemName);
return items.find(item => normalizeItemName(item.itemName) === target)
|| items.find(item => normalizeItemName(item.itemName).includes(target));
}
function makeWishlistText(template, data) {
return safeText(template)
.replaceAll("{PRICE}", formatNP(data.price))
.replaceAll("{LOWEST}", formatNP(data.lowest))
.replaceAll("{UNDERCUT}", formatNP(data.undercut))
.replaceAll("{ITEM}", safeText(data.itemName))
.slice(0, 500);
}
function renderSellerSuccess(itemName, price, result) {
const results = document.getElementById("tp-ibf-results");
if (!results) return;
results.innerHTML = "";
results.appendChild(el("div", { class: "tp-ibf-success" }, [
el("strong", {}, ["Lot created successfully."]),
el("div", {}, [
"Listed \"" + safeText(itemName) + "\" with Instant Buy at " + formatNP(price) + " NP."
]),
result?.data?.lot_id ? el("div", {}, ["Lot ID: " + safeText(result.data.lot_id)]) : null
]));
}
async function createTradingPostLotFromScript(selectedItemIds, wishlist, instantBuyAmount, signal) {
const refCk = getRefCk();
if (!refCk) {
throw new Error("Could not find _ref_ck. Open the Trading Post page once, then try again.");
}
const res = await fetch(TP_PROCESS_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
},
credentials: "include",
body: JSON.stringify({
type: "create",
selected_items: selectedItemIds,
wishlist: wishlist,
instant_buy_amount: instantBuyAmount ? String(instantBuyAmount) : null,
_ref_ck: refCk
}),
signal
});
if (!res.ok) {
throw new Error("Create lot HTTP " + res.status + " " + res.statusText);
}
const result = await res.json();
if (!result || result.success !== true) {
throw new Error(result?.error || result?.message || "Trading Post create returned success=false");
}
return result;
}
function nativeSetValue(input, value) {
if (!input) return false;
const proto = input instanceof HTMLTextAreaElement
? HTMLTextAreaElement.prototype
: HTMLInputElement.prototype;
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
if (descriptor && typeof descriptor.set === "function") {
descriptor.set.call(input, String(value));
} else {
input.value = String(value);
}
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
function findInstantBuySection() {
const sections = Array.from(document.querySelectorAll(".tp-section, section, div"));
return sections.find(section => {
const text = safeText(section.textContent).replace(/\s+/g, " ").trim();
return /Instant Buy/i.test(text) && /immediately purchase|Input the amount|Neopoints/i.test(text);
}) || null;
}
function findInstantBuyAmountInput() {
const byPlaceholder = Array.from(document.querySelectorAll("input, textarea"))
.find(input => /input the amount/i.test(input.getAttribute("placeholder") || ""));
if (byPlaceholder) return byPlaceholder;
const section = findInstantBuySection();
if (!section) return null;
return Array.from(section.querySelectorAll("input, textarea"))
.find(input => {
const type = safeText(input.getAttribute("type") || "text").toLowerCase();
return !["checkbox", "radio", "hidden", "submit", "button"].includes(type);
}) || null;
}
function enableInstantBuyToggle() {
const section = findInstantBuySection();
if (!section) return false;
const checkbox = Array.from(section.querySelectorAll('input[type="checkbox"]'))[0] || null;
if (checkbox) {
if (!checkbox.checked) checkbox.click();
checkbox.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
const switches = Array.from(section.querySelectorAll('[role="switch"], button, .toggle, [class*="toggle"], [class*="switch"]'));
const likelySwitch = switches.find(node => {
const text = safeText(node.textContent).trim();
const ariaChecked = node.getAttribute("aria-checked");
const cls = safeText(node.className);
return ariaChecked !== "true" || /toggle|switch/i.test(cls) || text === "";
}) || null;
if (likelySwitch) {
const ariaChecked = likelySwitch.getAttribute("aria-checked");
const className = safeText(likelySwitch.className);
const alreadyOn = ariaChecked === "true" || /active|checked|enabled|on/i.test(className);
if (!alreadyOn) likelySwitch.click();
likelySwitch.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
return false;
}
function selectTradingPostCard(card) {
if (!card || card.classList.contains("bg-item-selection")) return;
card.click();
}
function setInlineStatus(card, message, type) {
if (!card) return;
const status = card.querySelector(".tp-ibf-inline-status");
if (!status) return;
status.className = "tp-ibf-inline-status " + (type || "warn");
status.textContent = message;
}
async function priceTradingPostCard(card, button) {
const nameEl = card?.querySelector(".tp-grid-item-name");
const itemName = safeText(nameEl?.textContent).replace(/\s+/g, " ").trim();
if (!itemName) {
setInlineStatus(card, "Missing item name", "err");
return;
}
if (button) {
button.disabled = true;
button.textContent = "Pricing...";
}
const controller = new AbortController();
try {
setInlineStatus(card, "Searching lowest IB...", "warn");
const data = await tpSearchExactItem(itemName, controller.signal);
const ibLots = filterAndSortInstantBuy(data.lots, 1, 0, "range", DEFAULTS.nextPricePercent);
if (!ibLots.length) {
setInlineStatus(card, "No IB found", "err");
return;
}
const lowest = Number(ibLots[0].instant_buy_amount || 0);
const price = Math.max(1, lowest - 1);
selectTradingPostCard(card);
enableInstantBuyToggle();
await sleep(80);
const amountInput = findInstantBuyAmountInput();
if (!amountInput) {
setInlineStatus(card, "Could not find IB box", "err");
return;
}
if (amountInput.disabled) {
enableInstantBuyToggle();
await sleep(120);
}
nativeSetValue(amountInput, String(price));
amountInput.focus();
amountInput.dispatchEvent(new Event("blur", { bubbles: true }));
setInlineStatus(card, "Set " + formatNP(price) + " NP", "ok");
} catch (e) {
const msg = e && e.message ? e.message : "Pricing failed";
setInlineStatus(card, msg, "err");
} finally {
if (button) {
button.disabled = false;
button.textContent = "Price -1 NP";
}
}
}
function injectInlineTradingPostPriceButtons() {
const cards = Array.from(document.querySelectorAll(".tp-grid-item"));
for (const card of cards) {
if (card.dataset.tpIbfInlinePricer === "1") continue;
const nameEl = card.querySelector(".tp-grid-item-name");
if (!nameEl) continue;
card.dataset.tpIbfInlinePricer = "1";
const wrap = document.createElement("div");
wrap.className = "tp-ibf-inline-wrap";
const btn = document.createElement("button");
btn.type = "button";
btn.className = "tp-ibf-inline-price-btn";
btn.textContent = "Price -1 NP";
btn.title = "Find the lowest exact Instant Buy price, undercut by 1 NP, select this item, enable Instant Buy, and fill the price.";
const status = document.createElement("div");
status.className = "tp-ibf-inline-status";
status.textContent = "";
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
priceTradingPostCard(card, btn);
});
wrap.appendChild(btn);
wrap.appendChild(status);
nameEl.insertAdjacentElement("afterend", wrap);
}
}
function startInlineTradingPostPricerObserver() {
injectInlineTradingPostPriceButtons();
const observer = new MutationObserver(() => {
injectInlineTradingPostPriceButtons();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
async function runAutoList() {
const itemName = (document.getElementById("tp-ibf-seller-item")?.value ?? "").trim();
const undercut = toRawNP(document.getElementById("tp-ibf-seller-undercut")?.value ?? DEFAULTS.sellerUndercutNP);
const instantEnabled = document.getElementById("tp-ibf-seller-instant")?.checked ?? true;
const wishlistTemplate = document.getElementById("tp-ibf-seller-wishlist")?.value ?? DEFAULTS.sellerWishlistTemplate;
if (!itemName) {
setStatus("Enter the exact inventory item name you want to list.", "err");
return;
}
if (!instantEnabled) {
setStatus("Instant Buy must be enabled for lowest minus pricing.", "err");
return;
}
stopRun();
state.running = true;
state.abort = new AbortController();
setBusy(true);
try {
setStatus("Searching Trading Post for the lowest Instant Buy price for \"" + itemName + "\" ...", "warn");
const data = await tpSearchExactItem(itemName, state.abort.signal);
const ibLots = filterAndSortInstantBuy(data.lots, 1, 0, "range", DEFAULTS.nextPricePercent);
if (!ibLots.length) {
setStatus("No Instant Buy lots found for exact \"" + itemName + "\". I did not list your item.", "err");
renderNoLots(itemName);
return;
}
const lowest = Number(ibLots[0].instant_buy_amount || 0);
const listPrice = Math.max(1, lowest - undercut);
renderSingleResults(ibLots, itemName);
setStatus("Lowest Instant Buy is " + formatNP(lowest) + " NP. Loading your inventory ...", "warn");
const inventoryItems = await tpLoadInventoryItems(state.abort.signal);
const invItem = findInventoryItemByExactName(inventoryItems, itemName);
if (!invItem || !invItem.objId) {
setStatus("Could not find \"" + itemName + "\" in your inventory. I did not list anything.", "err");
return;
}
const wishlist = makeWishlistText(wishlistTemplate, {
itemName,
lowest,
undercut,
price: listPrice
});
const ok = window.confirm(
"Create Trading Post lot?\n\n" +
"Item: " + safeText(invItem.itemName) + "\n" +
"Inventory ID: " + safeText(invItem.objId) + "\n" +
"Lowest TP Instant Buy: " + formatNP(lowest) + " NP\n" +
"Your Instant Buy price: " + formatNP(listPrice) + " NP\n\n" +
"This will submit the lot now."
);
if (!ok) {
setStatus("Cancelled before listing. Nothing was submitted.", "warn");
return;
}
setStatus("Creating lot for \"" + safeText(invItem.itemName) + "\" at " + formatNP(listPrice) + " NP ...", "warn");
const result = await createTradingPostLotFromScript([invItem.objId], wishlist, listPrice, state.abort.signal);
setStatus("Lot created. Listed at " + formatNP(listPrice) + " NP.", "ok");
renderSellerSuccess(invItem.itemName, listPrice, result);
} catch (e) {
const msg = e && e.message ? e.message : "Auto-list failed";
setStatus("Auto-list error: " + msg + ".", "err");
} finally {
state.running = false;
state.abort = null;
setBusy(false);
}
}
async function runSingle() {
const query = (document.getElementById("tp-ibf-query")?.value ?? "").trim();
const { minPrice, maxPrice, priceMode, nextPricePercent } = readPriceFilters();
if (!query) {
setStatus("Enter an item name to search.", "err");
renderNoLots("");
return;
}
stopRun();
state.running = true;
state.abort = new AbortController();
setBusy(true);
setStatus("Exact searching \"" + query + "\" ...", "warn");
try {
const data = await tpSearchExactItem(query, state.abort.signal);
const ibLots = filterAndSortInstantBuy(data.lots, minPrice, maxPrice, priceMode, nextPricePercent);
if (!ibLots.length) {
setStatus("No Instant Buy lots found for exact \"" + query + "\".", "err");
} else {
setStatus(
"Done. Found " +
ibLots.length +
" Instant Buy lot" +
(ibLots.length === 1 ? "" : "s") +
". Sorted low to high.",
"ok"
);
}
renderSingleResults(ibLots, query);
} catch (e) {
const msg = e && e.message ? e.message : "Request failed";
setStatus("Error: " + msg + ".", "err");
renderNoLots(query);
} finally {
state.running = false;
state.abort = null;
setBusy(false);
}
}
function parseBatchLines(raw) {
const lines = safeText(raw)
.split(/\r?\n/g)
.map(s => s.trim())
.filter(s => s.length > 0);
const seen = new Set();
const out = [];
for (const line of lines) {
const key = line.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
out.push(line);
}
return out;
}
async function runBatch() {
const raw = document.getElementById("tp-ibf-batch")?.value ?? "";
const items = parseBatchLines(raw);
const { minPrice, maxPrice, priceMode, nextPricePercent } = readPriceFilters();
const modeReturn = document.getElementById("tp-ibf-batch-return")?.value || DEFAULTS.batchReturn;
const modeSort = document.getElementById("tp-ibf-batch-sort")?.value || DEFAULTS.batchSort;
if (!items.length) {
setStatus("Add item names in Batch mode (one per line).", "err");
renderBatchResults([], "Empty list");
return;
}
if (items.length > DEFAULTS.batchMaxItems) {
setStatus("Too many items. Limit is " + DEFAULTS.batchMaxItems + ".", "err");
renderBatchResults([], "Too many items");
return;
}
stopRun();
state.running = true;
state.abort = new AbortController();
setBusy(true);
setupLiveBatchResults(items.length);
try {
for (let i = 0; i < items.length; i++) {
if (state.abort.signal.aborted) {
throw new Error("Aborted");
}
const item = items[i];
setStatus("Batch " + (i + 1) + "/" + items.length + ": exact \"" + item + "\"", "warn");
const data = await tpSearchExactItem(item, state.abort.signal);
const ibLots = filterAndSortInstantBuy(data.lots, minPrice, maxPrice, priceMode, nextPricePercent);
state.liveBatchStats.processed += 1;
state.liveBatchStats.totalIBLots += ibLots.length;
if (ibLots.length) {
state.liveBatchStats.foundItems += 1;
if (modeReturn === "best") {
addLiveBatchRow({ item, bestLot: ibLots[0] }, modeSort);
} else {
addLiveBatchRow({ item, lots: ibLots }, modeSort);
}
}
updateLiveBatchSummary();
if (i < items.length - 1) {
await sleep(DEFAULTS.batchDelayMs, state.abort.signal);
}
}
if (!state.liveBatchRows.length) {
setStatus("Batch done. No Instant Buy lots found (exact match).", "err");
} else {
const label = modeReturn === "best"
? "Batch done. Best price per item. Items found: " + state.liveBatchRows.length + "."
: "Batch done. Lots found: " + state.liveBatchStats.totalIBLots + " across " + state.liveBatchRows.length + " item(s).";
setStatus(label, "ok");
}
updateLiveBatchSummary();
} catch (e) {
const msg = e && e.message ? e.message : "Request failed";
if (String(msg).toLowerCase().includes("aborted")) {
setStatus("Stopped. Results found before stopping are still shown.", "warn");
} else {
setStatus("Error: " + msg + ".", "err");
if (!state.liveBatchRows.length) {
renderBatchResults([], "Error");
}
}
} finally {
state.running = false;
state.abort = null;
setBusy(false);
}
}
function boot() {
injectStylesOnce();
buildLauncher();
startInlineTradingPostPricerObserver();
if (DEFAULTS.autoOpenOnTradingPost && isTradingPostPage()) {
openModal();
}
}
boot();
})();