Trading Post By web crawler 15 installs Rating 0.0 (0) approved

Neopets - Trading Post Instant Buy Finder

Neopets Trading Post Instant Buy Finder helps you locate instant buy lots instantly. Discover underpriced trades, flip items for profit, and dominate the Trading Post.
finder instant buy neopets trading post
Install
https://www.scriptneo.com/script/neopets-trading-post-instant-buy-finder

Version selector


SHA256
dd3563c76c43ef2cc377066a32cd37345ae827c7be6266cf94fd40bba043d550
No scan flags on this version.

Source code

// ==UserScript==
// @name         Neopets Trading Post Instant Buy Finder (Exact Match + Draggable + Single/Batch)
// @namespace    https://scriptneo.com/
// @version      2.3.0
// @description  Draggable modal to search Trading Post by exact item name using criteria=item_exact. Single mode searches one item. Batch mode searches many items. Instant Buy lots only, sorted lowest to highest. One request per item, no pagination.
// @author       Derek
// @match        https://www.neopets.com/island/tradingpost.phtml*
// @grant        none
// @run-at       document-end
// @downloadURL  https://www.scriptneo.com/scripts/download.php?id=24
// @updateURL    https://www.scriptneo.com/scripts/download.php?id=24
// ==/UserScript==

(function () {
    "use strict";

    const API_URL = "https://www.neopets.com/np-templates/ajax/island/tradingpost/tradingpost-list.php";

    const DEFAULTS = {
        sort: "newest",
        minPrice: 1,
        maxPrice: 0, // 0 = no max
        openLinksNewTab: true,
        autoOpenOnTradingPost: true,
        autoSearchOnEnter: true,

        // Mode
        mode: "single", // "single" | "batch"

        // Batch tuning
        batchDelayMs: 350,
        batchMaxItems: 500,
        batchReturn: "best", // "best" | "all"
        batchSort: "price", // "price" | "name"

        // IMPORTANT: exact match API criteria
        criteria: "item_exact"
    };

    let state = {
        running: false,
        abort: null,
        isOpen: false,
        drag: { active: false, offsetX: 0, offsetY: 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");
        if (btn1) btn1.disabled = !!busy;
        if (btn2) btn2.disabled = !!busy;
    }

    // ------------------------
    // API (Exact match)
    // ------------------------
    async function tpSearchExactItem(itemName, signal) {
        const payload = {
            type: "browse",
            criteria: DEFAULTS.criteria, // "item_exact"
            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;
    }

    // ------------------------
    // Styles + UI
    // ------------------------
    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-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-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;
}
        `.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 (criteria=item_exact). Instant Buy only. Lowest price first."])
            ]),
            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. Returns cheapest lot per item (Best) or all lots (All)."
        ]);

        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("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 body = el("div", { class: "tp-ibf-body" }, [
            tabs,
            singleHint,
            batchHint,
            singleRow,
            batchRow,
            settingsBox,
            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);

        // Defaults
        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;

        // Tabs initial
        setMode(DEFAULTS.mode);

        // Actions
        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-query")?.addEventListener("keydown", (e) => {
            if (!DEFAULTS.autoSearchOnEnter) return;
            if (e.key === "Enter") {
                e.preventDefault();
                runSingle();
            }
        });

        // Draggable
        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 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 (results) results.innerHTML = "";

        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]);
    }

    // ------------------------
    // Mode toggle
    // ------------------------
    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";
    }

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

    // ------------------------
    // Search logic
    // ------------------------
    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;
        return { minPrice, maxPrice };
    }

    function filterAndSortInstantBuy(lots, minPrice, maxPrice) {
        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);
        });

        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 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")
                    ])
                ]),
                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;
    }

    async function runSingle() {
        const query = (document.getElementById("tp-ibf-query")?.value ?? "").trim();
        const { minPrice, maxPrice } = 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);

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

        const resultsRows = [];
        let totalIBLots = 0;

        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);

                totalIBLots += ibLots.length;

                if (modeReturn === "best") {
                    if (ibLots.length) resultsRows.push({ item, bestLot: ibLots[0] });
                } else {
                    if (ibLots.length) resultsRows.push({ item, lots: ibLots });
                }

                if (i < items.length - 1) await sleep(DEFAULTS.batchDelayMs, state.abort.signal);
            }

            if (modeSort === "name") {
                resultsRows.sort((a, b) => safeText(a.item).localeCompare(safeText(b.item)));
            } else {
                resultsRows.sort((a, b) => {
                    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));
                });
            }

            if (!resultsRows.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: " + resultsRows.length + ".")
                    : ("Batch done. Lots found: " + totalIBLots + " across " + resultsRows.length + " item(s).");
                setStatus(label, "ok");
            }

            renderBatchResults(resultsRows, modeReturn === "best" ? "Best per item" : "All lots");
        } catch (e) {
            const msg = e && e.message ? e.message : "Request failed";
            if (String(msg).toLowerCase().includes("aborted")) setStatus("Stopped.", "warn");
            else setStatus("Error: " + msg + ".", "err");
            renderBatchResults([], "Error");
        } finally {
            state.running = false;
            state.abort = null;
            setBusy(false);
        }
    }

    // ------------------------
    // Boot
    // ------------------------
    function boot() {
        injectStylesOnce();
        buildLauncher();
        if (DEFAULTS.autoOpenOnTradingPost && isTradingPostPage()) openModal();
    }

    boot();
})();