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

Neopets - Trading Post Item Pricer

Trading Post item pricing buttons that undercut the lowest Instant Buy by 1 NP.
item pricer neopets trading post
https://www.scriptneo.com/script/neopets-trading-post-item-pricer

Version selector


SHA256
dc0704f7163f8af9056b35d78869b7d330d528030c86238e66a3be369a5d40ff
No scan flags on this version.

Source code

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