// ==UserScript== // @name Neopets - TP Browse API Sniper (ItemDB total value + rarity filter + whitelist + smart exclusions + profit filter, IB only, scanned history, category exclusions + fallback logic + blocked users) // @namespace http://tampermonkey.net/ // @version 4.6.0 // @description Trading Post API sniper using ItemDB. Buys Instant Buy lots when they meet ratio and profit. If no Instant Buy lots exist in the browse results, fallback can open the best lot to offer manually. Optional: if there are Instant Buy lots but none qualify, buy best Instant Buy anyway. // @author Scriptneo.com // @match https://www.neopets.com/island/tradingpost.phtml* // @run-at document-end // @grant none // @downloadURL https://www.scriptneo.com/scripts/download.php?id=21 // @updateURL https://www.scriptneo.com/scripts/download.php?id=21 // ==/UserScript== (function () { "use strict"; const TP_BROWSE_API = "https://www.neopets.com/np-templates/ajax/island/tradingpost/tradingpost-list.php"; const TP_INSTANT_API = "https://www.neopets.com/np-templates/ajax/island/tradingpost/process-tradingpost.php"; const SETTINGS_KEY = "tp_browse_sniper_itemdb_v3_total"; const SCANNED_KEY = SETTINGS_KEY + "_total_scanned"; const LOG_LIMIT = 200; const MAX_SCANNED_ID = 1000; const EVENTS_LOG_KEY = SETTINGS_KEY + "_events_log"; const FOUND_LOG_KEY = SETTINGS_KEY + "_found_log"; const BOUGHT_LOG_KEY = SETTINGS_KEY + "_bought_log"; // NEW: compact purchases log (items + price + lot id + owner) const BOUGHT_ITEMS_LOG_KEY = SETTINGS_KEY + "_bought_items_log"; const BOUGHT_GALLERY_KEY = SETTINGS_KEY + "_bought_gallery"; const GALLERY_LIMIT = 30; const IMAGE_BASE = "https://images.neopets.com"; const defaultSettings = { enabled: false, criteria: 20, searchString: "", sort: "", delayMinMs: 900, delayMaxMs: 1500, // ItemDB logic minItemdbRatio: 2.0, maxLotNP: 0, minProfitNP: 0, // FAST BUY MODE: // If > 0, instantly buys ANY Instant Buy lot priced <= this amount. // Ignores: min ratio, min profit, rarity filters, item/category lists. // Always respects the blocked users list. // ALSO bypasses Dry Run (openInsteadOfBuy) on purpose. cheapAutoBuyMaxNP: 0, // Rarity filter from ItemDB minRarity: 0, maxRarity: 0, // Lists excludedItemsText: "", excludedContainsText: "", alwaysAllowItemsText: "", // Category exclusions (ItemDB category field) excludedCategoriesText: "", // User exclusions (seller/owner blocklist) // These are always respected, including FAST BUY mode. blockedUsersText: "", // _ref_ck handling refCkOverride: "", // If Neopets returns an odd response, still log a "possible" purchase logUnconfirmedPurchases: true, // Behavior when no Instant Buy lots exist at all in the browse response fallbackMode: "open_best_lot", fallbackOpenNewTab: true, // If there are Instant Buy lots but none qualify filters, buy best Instant Buy anyway buyBestInstantBuyIfNoneQualify: false, // Hard veto. If ANY excluded item (exact or contains) is present in a lot, never buy the lot. // NOTE: Fast Buy Mode ignores this entirely. vetoLotIfAnyExcluded: true, // Dry run. Instead of instant buying, open the qualifying lot link in a new tab. // NOTE: Fast Buy Mode bypasses this entirely. openInsteadOfBuy: false }; let settings = loadSettings(); let running = false; let ui = null; // Use Sets for faster lookups let excludedNamesSet = new Set(); let alwaysAllowNamesSet = new Set(); let excludedContains = []; let blockedUserNamesSet = new Set(); // Category exclusions: // We keep both exact set and keyword list for contains matching. let excludedCategoriesExactSet = new Set(); let excludedCategoriesKeywords = []; const itemdbCache = Object.create(null); let scannedLotIds = new Set(); // --------------- Utils --------------- function logConsole(msg, data) { if (data !== undefined) console.log("[TP Browse ItemDB Sniper]", msg, data); else console.log("[TP Browse ItemDB Sniper]", msg); } function safeLsGet(key) { try { return localStorage.getItem(key) || ""; } catch (e) { return ""; } } function safeLsSet(key, value) { try { localStorage.setItem(key, value || ""); } catch (e) {} } function safeLsRemove(key) { try { localStorage.removeItem(key); } catch (e) {} } function appendToLog(textarea, msg, storageKey) { if (!textarea) return; const now = new Date(); const time = now.toLocaleTimeString(); const line = "[" + time + "] " + msg; const lines = textarea.value ? textarea.value.split("\n") : []; lines.push(line); while (lines.length > LOG_LIMIT) lines.shift(); textarea.value = lines.join("\n"); textarea.scrollTop = textarea.scrollHeight; if (storageKey) safeLsSet(storageKey, textarea.value); } function logEvent(msg) { logConsole(msg); if (ui && ui.eventsLog) appendToLog(ui.eventsLog, msg, EVENTS_LOG_KEY); } function logFound(msg) { if (ui && ui.foundLog) appendToLog(ui.foundLog, msg, FOUND_LOG_KEY); } function logBought(msg) { if (ui && ui.boughtLog) appendToLog(ui.boughtLog, msg, BOUGHT_LOG_KEY); } // NEW: compact "bought items" log function logBoughtItemsCompact(lotId, owner, amountPaid, itemsArr, flagText) { const idTxt = lotId != null ? String(lotId) : ""; const ownerTxt = owner ? String(owner) : ""; const paidTxt = (amountPaid != null) ? formatNP(amountPaid) : ""; const flag = flagText ? String(flagText) : ""; let itemsTxt = ""; if (Array.isArray(itemsArr) && itemsArr.length) { const parts = itemsArr.map(it => { const name = it && (it.name || it.item_name) ? String(it.name || it.item_name) : "Unknown item"; const count = Number(it && (it.count || it.amount || 1)); return name + ((Number.isFinite(count) && count > 1) ? " x" + count : ""); }); itemsTxt = parts.join(", "); } let line = ""; if (flag) line += flag + " | "; if (idTxt) line += "Lot #" + idTxt; if (paidTxt) line += (line ? " | " : "") + "Paid " + paidTxt; if (ownerTxt) line += (line ? " | " : "") + "Owner " + ownerTxt; if (itemsTxt) line += (line ? " | " : "") + "Items " + itemsTxt; if (!line) line = "Purchase recorded"; if (ui && ui.boughtItemsLog) appendToLog(ui.boughtItemsLog, line, BOUGHT_ITEMS_LOG_KEY); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function saveSettings() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); } catch (e) { logConsole("Could not save settings", e); } } function loadSettings() { try { const raw = localStorage.getItem(SETTINGS_KEY); if (!raw) { const s = { ...defaultSettings }; normalizeDelaySettings(s); return s; } const parsed = JSON.parse(raw); const merged = { ...defaultSettings, ...parsed }; normalizeDelaySettings(merged); return merged; } catch (e) { const s = { ...defaultSettings }; normalizeDelaySettings(s); return s; } } function normalizeDelaySettings(obj) { const minRaw = Number(obj.delayMinMs); const maxRaw = Number(obj.delayMaxMs); obj.delayMinMs = Math.max(100, Number.isFinite(minRaw) ? minRaw : defaultSettings.delayMinMs); obj.delayMaxMs = Number.isFinite(maxRaw) ? maxRaw : defaultSettings.delayMaxMs; if (obj.delayMaxMs < obj.delayMinMs) obj.delayMaxMs = obj.delayMinMs; } function formatNP(value) { const n = Number(value) || 0; return n.toLocaleString("en-US") + " NP"; } function normalizeBool(v) { if (v === true) return true; if (v === false) return false; const s = String(v).trim().toLowerCase(); return s === "true" || s === "1" || s === "yes" || s === "y"; } function textLooksLikeSuccess(t) { const s = String(t || ""); return /purchased|purchase\s+successful|lot\s+purchased|success|completed|you\s+have\s+purchased|instant\s+buy\s+successful/i.test(s); } function textLooksLikeError(t) { const s = String(t || ""); return /error|failed|insufficient|not\s+enough|invalid|denied|captcha|ref_ck|ref\s*ck|session|logged\s*out/i.test(s); } function extractServerMessage(json) { if (!json || typeof json !== "object") return ""; const direct = json.message || json.error || json.status_message || json.result_message; if (direct) return String(direct); if (json.data && typeof json.data === "object") { const nested = json.data.message || json.data.error || json.data.status_message || json.data.result_message; if (nested) return String(nested); } return ""; } function isJsonSuccess(json, lotId) { if (!json || typeof json !== "object") return false; if (json.error === true) return false; if (typeof json.error === "string" && json.error.trim() !== "") return false; if (Array.isArray(json.errors) && json.errors.length) return false; if (json.data && typeof json.data === "object" && json.data.lot_id != null) { const respLotId = Number(json.data.lot_id); const wantLotId = Number(lotId); if (Number.isFinite(respLotId) && Number.isFinite(wantLotId) && respLotId === wantLotId) { if (normalizeBool(json.success) || normalizeBool(json.data.success)) return true; const msgStrong = extractServerMessage(json); if (msgStrong && textLooksLikeSuccess(msgStrong) && !textLooksLikeError(msgStrong)) return true; } } if (normalizeBool(json.success)) return true; if (normalizeBool(json.ok)) return true; const status = json.status; if (typeof status === "string" && status.toLowerCase() === "success") return true; if (typeof status === "number" && status === 1) return true; if (typeof status === "string" && status.trim() === "1") return true; const result = json.result; if (typeof result === "string" && result.toLowerCase() === "success") return true; if (typeof result === "number" && result === 1) return true; if (normalizeBool(result)) return true; if (json.data && typeof json.data === "object") { if (normalizeBool(json.data.success)) return true; if (normalizeBool(json.data.ok)) return true; const dStatus = json.data.status; if (typeof dStatus === "string" && dStatus.toLowerCase() === "success") return true; if (typeof dStatus === "number" && dStatus === 1) return true; const dResult = json.data.result; if (typeof dResult === "string" && dResult.toLowerCase() === "success") return true; if (typeof dResult === "number" && dResult === 1) return true; if (normalizeBool(dResult)) return true; } const msg = extractServerMessage(json); if (msg && textLooksLikeSuccess(msg) && !textLooksLikeError(msg)) return true; return false; } function buildItemImageUrl(imgUrl) { if (!imgUrl) return ""; const s = String(imgUrl); if (/^https?:\/\//i.test(s)) return s; if (s.startsWith("/")) return IMAGE_BASE + s; return IMAGE_BASE + "/" + s; } function safeLotLink(lot) { const link = lot && (lot.link || lot.lot_link || lot.url || ""); const s = String(link || "").trim(); if (!s) { const lotId = getLotId(lot); if (lotId) return "https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=lot_id&search_string=" + encodeURIComponent(String(lotId)); return ""; } if (s.startsWith("//")) return "https:" + s; if (/^https?:\/\//i.test(s)) return s; return "https://www.neopets.com" + (s.startsWith("/") ? s : ("/" + s)); } function decodeHtmlEntities(str) { try { const el = document.createElement("textarea"); el.innerHTML = String(str || ""); return el.value; } catch (e) { return String(str || ""); } } function normalizeItemNameForMatch(name) { let s = decodeHtmlEntities(name); s = s.replace(/\u00A0/g, " "); s = s.replace(/\s+/g, " "); s = s.trim().toLowerCase(); return s; } function normalizeUsernameForMatch(username) { let s = decodeHtmlEntities(username); s = s.replace(/\u00A0/g, " "); s = s.replace(/^@+/, ""); s = s.replace(/\s+/g, " "); s = s.trim().toLowerCase(); return s; } function debounce(fn, waitMs) { let t = null; return function () { const args = arguments; if (t) clearTimeout(t); t = setTimeout(() => { t = null; fn.apply(null, args); }, waitMs); }; } // --------------- Visual Purchase Gallery --------------- function loadGallery() { try { const raw = safeLsGet(BOUGHT_GALLERY_KEY); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch (e) { return []; } } function saveGallery(arr) { try { const trimmed = Array.isArray(arr) ? arr.slice(0, GALLERY_LIMIT) : []; safeLsSet(BOUGHT_GALLERY_KEY, JSON.stringify(trimmed)); } catch (e) {} } function addPurchaseToGallery(entry) { if (!entry) return; const current = loadGallery(); current.unshift(entry); if (current.length > GALLERY_LIMIT) current.length = GALLERY_LIMIT; saveGallery(current); renderGallery(); } function clearGallery() { safeLsRemove(BOUGHT_GALLERY_KEY); renderGallery(); } function renderGallery() { if (!ui || !ui.boughtGallery) return; const container = ui.boughtGallery; container.innerHTML = ""; const data = loadGallery(); if (!data.length) { const empty = document.createElement("div"); empty.className = "tp-gallery-empty"; empty.textContent = "No purchases yet."; container.appendChild(empty); return; } for (const purchase of data) { const wrap = document.createElement("div"); wrap.className = "tp-purchase-card"; const header = document.createElement("div"); header.className = "tp-purchase-header"; const when = purchase.timeText || ""; const lotId = purchase.lotId != null ? String(purchase.lotId) : ""; const seller = purchase.seller ? String(purchase.seller) : ""; const paid = purchase.amountPaid != null ? formatNP(purchase.amountPaid) : ""; const flag = purchase.flag ? String(purchase.flag) : ""; let headerText = ""; if (when) headerText += when; if (lotId) headerText += (headerText ? " | " : "") + "Lot #" + lotId; if (seller) headerText += (headerText ? " | " : "") + "Seller: " + seller; if (paid) headerText += (headerText ? " | " : "") + "Paid: " + paid; if (flag) headerText += (headerText ? " | " : "") + flag; if (!headerText) headerText = "Purchase"; header.textContent = headerText; wrap.appendChild(header); const itemsRow = document.createElement("div"); itemsRow.className = "tp-purchase-items"; const items = Array.isArray(purchase.items) ? purchase.items : []; for (const it of items) { const item = document.createElement("div"); item.className = "tp-item"; const img = document.createElement("img"); img.className = "tp-item-img"; img.alt = it.name ? String(it.name) : "Item"; img.loading = "lazy"; img.src = buildItemImageUrl(it.imgUrl || it.img_url || ""); const name = document.createElement("div"); name.className = "tp-item-name"; name.textContent = it.name ? String(it.name) : "Unknown item"; const count = document.createElement("div"); count.className = "tp-item-count"; const c = Number(it.count || it.amount || 1); count.textContent = (Number.isFinite(c) && c > 1) ? "x" + c : ""; item.appendChild(img); item.appendChild(name); item.appendChild(count); itemsRow.appendChild(item); } wrap.appendChild(itemsRow); container.appendChild(wrap); } } // --------------- Scanned lot history --------------- function loadScannedLotHistory() { try { const raw = localStorage.getItem(SCANNED_KEY); if (!raw) { scannedLotIds = new Set(); return; } const arr = JSON.parse(raw); if (Array.isArray(arr)) { const cleaned = []; for (const v of arr) { const n = Number(v); if (Number.isFinite(n)) cleaned.push(n); } scannedLotIds = new Set(cleaned); } else { scannedLotIds = new Set(); } logConsole("Loaded scanned lot history with " + scannedLotIds.size + " IDs."); } catch (e) { logConsole("Could not load scanned lot history", e); scannedLotIds = new Set(); } } function saveScannedLotHistory() { try { let arr = Array.from(scannedLotIds); if (arr.length > MAX_SCANNED_ID) { arr = arr.slice(arr.length - MAX_SCANNED_ID); scannedLotIds = new Set(arr); } localStorage.setItem(SCANNED_KEY, JSON.stringify(arr)); } catch (e) { logConsole("Could not save scanned lot history", e); } } function markLotScanned(id) { if (!Number.isFinite(id)) return; if (!scannedLotIds.has(id)) { scannedLotIds.add(id); saveScannedLotHistory(); } } // --------------- ref_ck token --------------- function getRefCk() { if (settings.refCkOverride && settings.refCkOverride.trim() !== "") { return settings.refCkOverride.trim(); } let ck = null; try { if (typeof getCK === "function") ck = getCK(); } catch (e) { logConsole("getCK() threw error", e); } if (ck) { maybeSaveAutoRefCk(ck); return ck; } const input = document.querySelector('input[name="_ref_ck"]'); if (input && input.value) { ck = input.value; maybeSaveAutoRefCk(ck); return ck; } const meta = document.querySelector('meta[name="_ref_ck"]'); if (meta && meta.content) { ck = meta.content; maybeSaveAutoRefCk(ck); return ck; } const m = document.cookie.match(/(?:^|;\s*)_ref_ck=([^;]+)/); if (m) { ck = decodeURIComponent(m[1]); maybeSaveAutoRefCk(ck); return ck; } logEvent("Warning: could not find _ref_ck token. Instant buy will fail."); return null; } function maybeSaveAutoRefCk(ck) { if (!ck) return; if (!settings.refCkOverride) { settings.refCkOverride = ck; saveSettings(); logEvent("Auto detected _ref_ck and saved it in settings."); if (ui && ui.refCkInput) ui.refCkInput.value = ck; } } function detectRefCkFromPage() { let ck = null; try { if (typeof getCK === "function") ck = getCK(); } catch (e) { logConsole("getCK() threw error during detectRefCkFromPage", e); } if (ck) return ck; const input = document.querySelector('input[name="_ref_ck"]'); if (input && input.value) return input.value; const meta = document.querySelector('meta[name="_ref_ck"]'); if (meta && meta.content) return meta.content; const m = document.cookie.match(/(?:^|;\s*)_ref_ck=([^;]+)/); if (m) return decodeURIComponent(m[1]); return null; } // --------------- Lists (kept for normal mode) --------------- function rebuildExcludedList() { const raw = settings.excludedItemsText || ""; const arr = raw .split(/\r?\n/) .map(line => normalizeItemNameForMatch(line)) .filter(Boolean); excludedNamesSet = new Set(arr); logEvent("Parsed " + excludedNamesSet.size + " excluded items."); } function rebuildContainsExcludedList() { const raw = settings.excludedContainsText || ""; excludedContains = raw .split(/\r?\n/) .map(line => normalizeItemNameForMatch(line)) .filter(Boolean); logEvent("Parsed " + excludedContains.length + " contains-excluded keywords."); } function rebuildAlwaysAllowList() { const raw = settings.alwaysAllowItemsText || ""; const arr = raw .split(/\r?\n/) .map(line => normalizeItemNameForMatch(line)) .filter(Boolean); alwaysAllowNamesSet = new Set(arr); logEvent("Parsed " + alwaysAllowNamesSet.size + " always allow items."); } function rebuildExcludedCategoryList() { const raw = settings.excludedCategoriesText || ""; const arr = raw .split(/\r?\n/) .map(line => normalizeItemNameForMatch(line)) .filter(Boolean); excludedCategoriesExactSet = new Set(arr); excludedCategoriesKeywords = arr.slice(); logEvent("Parsed " + excludedCategoriesExactSet.size + " excluded categories (exact and contains)."); } function rebuildBlockedUsersList() { const raw = settings.blockedUsersText || ""; const arr = raw .split(/\r?\n/) .map(line => normalizeUsernameForMatch(line)) .filter(Boolean); blockedUserNamesSet = new Set(arr); logEvent("Parsed " + blockedUserNamesSet.size + " blocked users."); } function splitLotByExclusions(lot) { const excluded = []; const allowed = []; if (!Array.isArray(lot.items)) return { excluded, allowed }; for (const item of lot.items) { if (!item || !item.name) continue; const displayName = String(item.name); const lower = normalizeItemNameForMatch(displayName); if (!lower) continue; if (excludedNamesSet.has(lower)) { excluded.push(displayName); continue; } if (excludedContains.length) { const hit = excludedContains.some(word => word && lower.includes(word)); if (hit) { excluded.push(displayName); continue; } } allowed.push(displayName); } return { excluded, allowed }; } // --------------- ItemDB helpers --------------- async function getItemdbInfo(itemName) { if (!itemName) return null; const key = normalizeItemNameForMatch(itemName); if (!key) return null; if (Object.prototype.hasOwnProperty.call(itemdbCache, key)) return itemdbCache[key]; const url = "https://itemdb.com.br/api/v1/items/" + encodeURIComponent(itemName); try { const response = await fetch(url, { credentials: "omit" }); if (!response.ok) { logConsole("ItemDB HTTP error for " + itemName, response.status); itemdbCache[key] = null; return null; } const item = await response.json(); let price = null; let rarity = null; let category = null; if (item && item.price && typeof item.price.value === "number") price = Number(item.price.value); if (item && typeof item.rarity === "number") rarity = Number(item.rarity); if (item && typeof item.category === "string") category = String(item.category); const info = { price, rarity, category, isNC: !!(item && item.isNC), isBD: !!(item && item.isBD), type: (item && typeof item.type === "string" ? item.type : null) }; itemdbCache[key] = info; return info; } catch (err) { logConsole("ItemDB fetch error for " + itemName, err); itemdbCache[key] = null; return null; } } function isCategoryExcluded(categoryStr) { const c = normalizeItemNameForMatch(categoryStr || ""); if (!c) return false; if (excludedCategoriesExactSet.size > 0 && excludedCategoriesExactSet.has(c)) return true; if (excludedCategoriesKeywords.length) { return excludedCategoriesKeywords.some(k => k && c.includes(k)); } return false; } // --------------- Browse API --------------- async function browseViaPost() { const payload = { type: "browse", criteria: String(settings.criteria || "20"), search_string: String(settings.searchString || ""), sort: String(settings.sort || "") }; logEvent("Calling tradingpost-list.php via POST JSON."); let res; try { res = await fetch(TP_BROWSE_API + "?", { method: "POST", credentials: "include", headers: { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "x-requested-with": "XMLHttpRequest" }, body: JSON.stringify(payload) }); } catch (e) { logEvent("POST browse network error. See console."); logConsole("POST browse fetch error", e); return { lots: null, raw: null }; } let text; try { text = await res.text(); } catch (e) { logEvent("POST browse could not read response text."); logConsole("POST browse text error", e); return { lots: null, raw: null }; } if (!text) { logEvent("POST browse returned empty response."); return { lots: null, raw: "" }; } let json; try { json = JSON.parse(text); } catch (e) { logEvent("POST browse response not valid JSON. Raw logged to console."); logConsole("POST raw response", text); return { lots: null, raw: text }; } logConsole("POST browse JSON parsed", json); let lots = null; if (Array.isArray(json.lots)) lots = json.lots; else if (json.data && Array.isArray(json.data.lots)) lots = json.data.lots; else if (Array.isArray(json.results)) lots = json.results; if (!lots) { logEvent("POST browse JSON has no lots array. See console."); logConsole("POST browse JSON with no lots", json); return { lots: null, raw: json }; } logEvent("POST browse returned " + lots.length + " lots."); return { lots, raw: json }; } async function browseViaGet() { const params = new URLSearchParams({ type: "browse", criteria: String(settings.criteria || "20"), search_string: String(settings.searchString || ""), sort: String(settings.sort || "") }); const url = TP_BROWSE_API + "?" + params.toString(); logEvent("Falling back to GET browse: " + url); let res; try { res = await fetch(url, { method: "GET", credentials: "include", headers: { "Accept": "application/json, text/plain, */*", "x-requested-with": "XMLHttpRequest" } }); } catch (e) { logEvent("GET browse network error. See console."); logConsole("GET browse fetch error", e); return []; } let text; try { text = await res.text(); } catch (e) { logEvent("GET browse could not read response text."); logConsole("GET browse text error", e); return []; } if (!text) { logEvent("GET browse returned empty response."); return []; } let json; try { json = JSON.parse(text); } catch (e) { logEvent("GET browse response not valid JSON. Raw logged to console."); logConsole("GET raw response", text); return []; } logConsole("GET browse JSON parsed", json); let lots = null; if (Array.isArray(json.lots)) lots = json.lots; else if (json.data && Array.isArray(json.data.lots)) lots = json.data.lots; else if (Array.isArray(json.results)) lots = json.results; if (!lots) { logEvent("GET browse JSON has no lots array. See console."); logConsole("GET browse JSON with no lots", json); return []; } logEvent("GET browse returned " + lots.length + " lots."); return lots; } async function fetchBrowseLots() { const postResult = await browseViaPost(); if (postResult.lots && Array.isArray(postResult.lots)) return postResult.lots; return await browseViaGet(); } // --------------- Instant buy --------------- function getLotId(lot) { if (lot && "lot_id" in lot) return lot.lot_id; if (lot && "lotId" in lot) return lot.lotId; if (lot && "id" in lot) return lot.id; return null; } function getLotOwner(lot) { if (!lot || typeof lot !== "object") return ""; const directFields = [ lot.owner, lot.owner_username, lot.username, lot.user_name, lot.seller, lot.seller_username ]; for (const value of directFields) { if (typeof value === "string" && value.trim() !== "") return value.trim(); } if (lot.user && typeof lot.user === "object") { if (typeof lot.user.username === "string" && lot.user.username.trim() !== "") return lot.user.username.trim(); if (typeof lot.user.name === "string" && lot.user.name.trim() !== "") return lot.user.name.trim(); } if (lot.owner && typeof lot.owner === "object") { if (typeof lot.owner.username === "string" && lot.owner.username.trim() !== "") return lot.owner.username.trim(); if (typeof lot.owner.name === "string" && lot.owner.name.trim() !== "") return lot.owner.name.trim(); } return ""; } function isLotOwnerBlocked(lot) { if (!blockedUserNamesSet.size) return false; const owner = getLotOwner(lot); if (!owner) return false; return blockedUserNamesSet.has(normalizeUsernameForMatch(owner)); } function getLotPrice(lot) { if (lot && typeof lot.instant_buy_amount === "number" && lot.instant_buy_amount > 0) return lot.instant_buy_amount; const n = Number(lot && lot.instant_buy_amount); if (Number.isFinite(n) && n > 0) return n; return null; } async function instantBuyLot(lotId, price, lotItems, ratio, profit, totalValue, lotOwner) { const refCk = getRefCk(); if (!refCk) { logEvent("Instant buy failed: no _ref_ck token."); return false; } const payload = { type: "instant_buy", lot_id: Number(lotId), _ref_ck: refCk }; logEvent("Attempting instant buy. Lot " + lotId + ", price " + formatNP(price)); let res; try { res = await fetch(TP_INSTANT_API, { method: "POST", credentials: "include", headers: { "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "x-requested-with": "XMLHttpRequest" }, body: JSON.stringify(payload) }); } catch (e) { logEvent("Instant buy request network error. See console."); logConsole("Instant fetch error", e); return false; } const redirected = !!res.redirected; const finalUrl = String(res.url || ""); const contentType = String(res.headers.get("content-type") || "").toLowerCase(); let text = ""; try { text = await res.text(); } catch (e) { logEvent("Instant buy could not read response body. HTTP " + res.status + "."); logConsole("Instant read body error", e); text = ""; } const httpInfo = "HTTP " + res.status + (res.ok ? " OK" : " not OK") + ", redirected " + (redirected ? "yes" : "no") + (finalUrl ? ", final " + finalUrl : "") + (contentType ? ", ct " + contentType : "") + ", body " + (text ? text.length : 0) + " chars"; logConsole("Instant buy response info: " + httpInfo); let json = null; let parsedJson = false; let success = false; let successMode = ""; let itemsFromResponse = null; let sellerFromResponse = ""; let paidFromResponse = null; if (text) { try { json = JSON.parse(text); parsedJson = true; logConsole("Instant buy JSON", json); if (isJsonSuccess(json, lotId)) { success = true; successMode = "confirmed"; } if (json && json.data && typeof json.data === "object") { if (typeof json.data.seller === "string") sellerFromResponse = json.data.seller; if (typeof json.data.amount_paid === "number") paidFromResponse = Number(json.data.amount_paid); if (Array.isArray(json.data.items)) itemsFromResponse = json.data.items; else if (Array.isArray(json.items)) itemsFromResponse = json.items; else if (Array.isArray(json.data.purchased_items)) itemsFromResponse = json.data.purchased_items; } if (!success) { const msg = extractServerMessage(json); if (msg && textLooksLikeSuccess(msg) && !textLooksLikeError(msg)) { success = true; successMode = "confirmed"; } } } catch (e) { parsedJson = false; logConsole("Instant raw response (not JSON)", text); if (textLooksLikeSuccess(text) && !textLooksLikeError(text)) { success = true; successMode = "confirmed"; } } } if (!success && settings.logUnconfirmedPurchases) { const combined = (extractServerMessage(json) || "") + " " + (text || ""); const looksBad = textLooksLikeError(combined); const looksLikeNeoRedirect = redirected && (finalUrl.includes("/island/tradingpost.phtml") || finalUrl.includes("tradingpost.phtml")); const okButEmpty = res.ok && (!text || text.trim() === "") && (res.status === 200 || res.status === 204); const okHtml = res.ok && contentType.includes("text/html") && !looksBad && (redirected || finalUrl.includes("tradingpost.phtml")); const okAmbiguous = res.ok && !looksBad && (looksLikeNeoRedirect || okButEmpty || okHtml || (text && textLooksLikeSuccess(text))); if (okAmbiguous) { success = true; successMode = "unconfirmed"; logEvent("Instant buy response ambiguous but likely success. Logging as POSSIBLE BUY. " + httpInfo); } } if (success) { const seller = sellerFromResponse || lotOwner || ""; const amountPaid = (paidFromResponse != null && Number.isFinite(paidFromResponse)) ? paidFromResponse : price; let items = null; if (itemsFromResponse && itemsFromResponse.length) items = itemsFromResponse; else if (Array.isArray(lotItems) && lotItems.length) items = lotItems; let itemListText = ""; let galleryItems = []; if (items && items.length) { const parts = items.map(it => { const name = it.name || it.item_name || "Unknown item"; const count = Number(it.count || it.amount || 1); return name + (count > 1 ? " x" + count : ""); }); itemListText = parts.join(", "); galleryItems = items.map(it => { return { name: it.name || it.item_name || "Unknown item", count: Number(it.count || it.amount || 1), imgUrl: it.img_url || it.imgUrl || it.image || it.image_url || "" }; }); } const ownerLabel = seller ? " from " + seller : ""; const flag = (successMode === "unconfirmed") ? "POSSIBLE BUY" : "Bought"; logEvent("Instant buy " + (successMode === "unconfirmed" ? "possibly succeeded" : "success") + " for lot " + lotId + (seller ? " (seller " + seller + ")" : "") + "."); let msg = flag + " LOT ID #" + lotId + ownerLabel + " for " + formatNP(amountPaid) + "!"; const hasRatio = typeof ratio === "number" && Number.isFinite(ratio); const hasProfit = typeof profit === "number" && Number.isFinite(profit); const hasTotal = typeof totalValue === "number" && Number.isFinite(totalValue); if (hasTotal || hasRatio || hasProfit) { const statBits = []; if (hasTotal) statBits.push("total ItemDB " + formatNP(totalValue)); if (hasRatio) statBits.push("ratio " + ratio.toFixed(2)); if (hasProfit) statBits.push("profit " + formatNP(profit)); msg += " [" + statBits.join(" | ") + "]"; } if (itemListText) msg += " Items: " + itemListText; if (successMode === "unconfirmed") msg += " (" + httpInfo + ")"; logBought(msg); // NEW: also write the compact items log logBoughtItemsCompact( lotId, seller, amountPaid, items || [], (successMode === "unconfirmed") ? "POSSIBLE" : "" ); const now = new Date(); addPurchaseToGallery({ time: now.getTime(), timeText: now.toLocaleString(), lotId: Number(lotId), seller: seller, amountPaid: amountPaid, flag: (successMode === "unconfirmed") ? "POSSIBLE" : "", items: galleryItems }); return true; } if (parsedJson && json) { const msg = extractServerMessage(json) || "unknown error"; logEvent("Instant buy failed for lot " + lotId + ". Server said: " + msg); } else { logEvent("Instant buy failed for lot " + lotId + ". Response did not look like success. (" + httpInfo + ")"); } return false; } // --------------- Value calculation per lot (normal mode) --------------- async function computeLotValue(lot) { const lotId = Number(getLotId(lot) || 0); const lotOwner = getLotOwner(lot); const price = getLotPrice(lot); const link = safeLotLink(lot); if (!Array.isArray(lot.items) || !lot.items.length) { return { lotId, lotOwner, price, link, totalValue: 0, bestName: null, bestValue: 0, ratio: null, profit: null, excluded: [], allowed: [], whitelistHits: [], rarityInfo: [], rarityHits: [], rarityFilterActive: false, minRarity: 0, maxRarity: 0, categoryInfo: [], categoryExcludedHits: [], droppedByCategoryCount: 0 }; } const { excluded, allowed } = splitLotByExclusions(lot); const minRarity = Number(settings.minRarity) || 0; const maxRarity = Number(settings.maxRarity) || 0; const rarityFilterActive = minRarity > 0 || maxRarity > 0; let totalValue = 0; let bestName = null; let bestValue = 0; const rarityInfo = []; const rarityHits = []; const whitelistHits = []; const categoryInfo = []; const categoryExcludedHits = []; let droppedByCategoryCount = 0; for (const name of allowed) { const lower = normalizeItemNameForMatch(name); const isWhitelisted = alwaysAllowNamesSet.has(lower); if (isWhitelisted) whitelistHits.push(name); const info = await getItemdbInfo(name); if (!info) continue; const value = info.price; const rarity = info.rarity; const category = info.category; if (category) categoryInfo.push(name + " [" + category + "]"); const categoryBlocked = !!category && isCategoryExcluded(category); if (categoryBlocked && !isWhitelisted) { droppedByCategoryCount += 1; categoryExcludedHits.push(name + " [" + category + "]"); continue; } if (typeof value === "number" && value > 0) { totalValue += value; if (value > bestValue) { bestValue = value; bestName = name; } } if (typeof rarity === "number") { rarityInfo.push(name + " r" + rarity); const okMin = minRarity > 0 ? rarity >= minRarity : true; const okMax = maxRarity > 0 ? rarity <= maxRarity : true; if (okMin && okMax) rarityHits.push(name + " r" + rarity); } } let ratio = null; let profit = null; if (price != null && Number(price) > 0) { ratio = totalValue > 0 ? (totalValue / price) : 0; profit = totalValue - price; } return { lotId, lotOwner, price, link, totalValue, bestName, bestValue, ratio, profit, excluded, allowed, whitelistHits, rarityInfo, rarityHits, rarityFilterActive, minRarity, maxRarity, categoryInfo, categoryExcludedHits, droppedByCategoryCount }; } function lotMeetsFilters(calc) { const price = calc.price; if (price == null || !(Number(price) > 0)) return false; const minRatio = Number(settings.minItemdbRatio) || 0; const minProfit = Number(settings.minProfitNP) || 0; const ratio = typeof calc.ratio === "number" ? calc.ratio : 0; const profit = typeof calc.profit === "number" ? calc.profit : (calc.totalValue - price); if (calc.totalValue <= 0) return false; if (calc.rarityFilterActive) { const hasRarityHit = Array.isArray(calc.rarityHits) && calc.rarityHits.length > 0; const hasWhitelist = Array.isArray(calc.whitelistHits) && calc.whitelistHits.length > 0; if (!hasRarityHit && !hasWhitelist) return false; } if (minRatio > 0 && ratio < minRatio) return false; if (minProfit > 0 && profit < minProfit) return false; if (settings.maxLotNP > 0 && price > settings.maxLotNP) return false; return true; } function compareBestInstantBuy(a, b) { const ar = Number(a.ratio || 0); const br = Number(b.ratio || 0); if (ar !== br) return br - ar; const ap = Number(a.profit || 0); const bp = Number(b.profit || 0); if (ap !== bp) return bp - ap; const at = Number(a.totalValue || 0); const bt = Number(b.totalValue || 0); if (at !== bt) return bt - at; return Number(b.lotId || 0) - Number(a.lotId || 0); } function compareBestFallbackOpen(a, b) { const at = Number(a.totalValue || 0); const bt = Number(b.totalValue || 0); if (at !== bt) return bt - at; return Number(b.lotId || 0) - Number(a.lotId || 0); } function openLotForManualOffer(calc) { const url = calc && calc.link ? calc.link : ""; if (!url) return; const newTab = !!settings.fallbackOpenNewTab; try { if (newTab) window.open(url, "_blank", "noopener,noreferrer"); else window.location.href = url; } catch (e) { logConsole("Could not open fallback lot", e); } } // --------------- Scan loop --------------- async function scanOnce() { const lots = await fetchBrowseLots(); if (!Array.isArray(lots) || !lots.length) { logEvent("No lots found this cycle."); return; } const cheapCap = Math.floor(Number(settings.cheapAutoBuyMaxNP) || 0); const fastBuyActive = cheapCap > 0; let instantBuyLotsSeen = 0; const ibCalcs = []; const nonIbCalcs = []; for (const lot of lots) { const lotIdRaw = getLotId(lot); if (!lotIdRaw) { logConsole("Lot missing id field", lot); continue; } const lotId = Number(lotIdRaw); if (!Number.isFinite(lotId) || lotId <= 0) continue; if (scannedLotIds.has(lotId)) continue; const price = getLotPrice(lot); const lotOwner = getLotOwner(lot); if (isLotOwnerBlocked(lot)) { markLotScanned(lotId); logEvent("Skipped lot " + lotId + ": seller/owner is blocked (" + lotOwner + ")."); continue; } // FAST BUY MODE: buy immediately, ignore item/value/category filters. // IMPORTANT: This bypasses Dry Run on purpose, but still respects blocked users. if (fastBuyActive && price != null && Number(price) > 0 && Number(price) <= cheapCap) { markLotScanned(lotId); logEvent("FAST BUY TRIGGERED: buying lot " + lotId + " for " + formatNP(price) + " (cap " + formatNP(cheapCap) + "). Ignoring item/value/category filters and bypassing Dry Run. Blocked users are still skipped."); await instantBuyLot( lotId, price, Array.isArray(lot.items) ? lot.items : [], null, null, null, lotOwner ); await sleep(250); continue; } // Normal mode below (ItemDB + filters + exclusion lists) const calc = await computeLotValue(lot); markLotScanned(lotId); if (settings.vetoLotIfAnyExcluded && Array.isArray(calc.excluded) && calc.excluded.length > 0) { logEvent("Skipped lot " + calc.lotId + ": excluded item present (" + calc.excluded.join(", ") + ")."); continue; } if (!calc.allowed.length && calc.excluded.length) { logEvent("Skipped lot " + calc.lotId + ": all items are from exclusions list (" + calc.excluded.join(", ") + ")."); continue; } if (!calc.allowed.length && !calc.excluded.length) { logEvent("Skipped lot " + calc.lotId + ": no usable item names found."); continue; } if (calc.totalValue > 0) { const priceText = (calc.price != null) ? formatNP(calc.price) : "no IB"; const ratioText = (calc.price != null && typeof calc.ratio === "number") ? calc.ratio.toFixed(2) : "n/a"; const profitText = (calc.price != null && typeof calc.profit === "number") ? formatNP(calc.profit) : "n/a"; let foundMsg = "Lot " + calc.lotId + ": total allowed ItemDB value " + formatNP(calc.totalValue) + " vs price " + priceText + " (ratio " + ratioText + ", profit " + profitText + ")" + (calc.bestName ? " [best allowed item " + calc.bestName + " at " + formatNP(calc.bestValue) + "]" : ""); if (calc.rarityInfo.length) foundMsg += " [rarities: " + calc.rarityInfo.join(", ") + "]"; if (calc.whitelistHits.length) foundMsg += " [whitelist: " + calc.whitelistHits.join(", ") + "]"; if (calc.categoryInfo.length) foundMsg += " [categories: " + calc.categoryInfo.join(", ") + "]"; if (calc.categoryExcludedHits.length) foundMsg += " [category excluded: " + calc.categoryExcludedHits.join(", ") + "]"; logFound(foundMsg); } else { let reason = "Skipped lot " + calc.lotId + ": no ItemDB value found for any allowed items."; if (excludedCategoriesKeywords.length && calc.droppedByCategoryCount > 0) { reason += " Note: " + calc.droppedByCategoryCount + " allowed items were ignored due to excluded categories."; } logEvent(reason); continue; } if (calc.price != null && Number(calc.price) > 0) { instantBuyLotsSeen += 1; ibCalcs.push(calc); } else { nonIbCalcs.push(calc); } } if (instantBuyLotsSeen > 0) { const qualifying = ibCalcs.filter(c => lotMeetsFilters(c)); if (qualifying.length) { qualifying.sort(compareBestInstantBuy); for (const c of qualifying) { if (settings.openInsteadOfBuy) { let okOpen = false; try { window.open(c.link, "_blank", "noopener,noreferrer"); okOpen = true; } catch (e) { logConsole("Could not open lot in new tab", e); okOpen = false; } logEvent("DRY RUN: qualifying lot " + c.lotId + " opened in new tab. Purchase skipped. " + (okOpen ? "" : "(open failed)")); await sleep(250); continue; } logEvent("Lot " + c.lotId + " flagged to buy (qualifies)."); const lotObj = lots.find(x => Number(getLotId(x)) === Number(c.lotId)) || {}; const ok = await instantBuyLot( c.lotId, c.price, Array.isArray(lotObj.items) ? lotObj.items : [], c.ratio, c.profit, c.totalValue, c.lotOwner ); if (ok) await sleep(300); } return; } if (settings.buyBestInstantBuyIfNoneQualify) { const best = ibCalcs.slice().sort(compareBestInstantBuy)[0]; if (best) { if (settings.openInsteadOfBuy) { let okOpen = false; try { window.open(best.link, "_blank", "noopener,noreferrer"); okOpen = true; } catch (e) { logConsole("Could not open lot in new tab", e); okOpen = false; } logEvent("DRY RUN: no IB qualified, opening best IB lot anyway: " + best.lotId + ". Purchase skipped. " + (okOpen ? "" : "(open failed)")); await sleep(250); return; } logEvent("No Instant Buy lots qualified filters. buyBestInstantBuyIfNoneQualify is ON, buying best IB lot anyway: " + best.lotId + "."); const lotObj = lots.find(x => Number(getLotId(x)) === Number(best.lotId)) || {}; await instantBuyLot( best.lotId, best.price, Array.isArray(lotObj.items) ? lotObj.items : [], best.ratio, best.profit, best.totalValue, best.lotOwner ); await sleep(300); return; } } logEvent("Instant Buy lots exist, but none qualified this cycle. No purchase made."); return; } if (settings.fallbackMode === "open_best_lot") { if (!nonIbCalcs.length) { logEvent("No Instant Buy lots found, and no other usable lots to fallback open."); return; } const bestFallback = nonIbCalcs.slice().sort(compareBestFallbackOpen)[0]; if (bestFallback) { logEvent("No Instant Buy lots found this cycle. Fallback: opening best lot for manual offer: Lot " + bestFallback.lotId + " (ItemDB total " + formatNP(bestFallback.totalValue) + ")."); openLotForManualOffer(bestFallback); return; } } logEvent("No Instant Buy lots found this cycle. Fallback is disabled."); } async function mainLoop() { while (running) { try { await scanOnce(); } catch (e) { logEvent("Error in scan loop. See console."); logConsole("Error in scan loop", e); } normalizeDelaySettings(settings); const delay = randInt(settings.delayMinMs, settings.delayMaxMs); logEvent("Waiting " + delay + " ms before next scan."); await sleep(delay); } logEvent("Browse loop stopped."); } function startSniper() { if (running) return; running = true; settings.enabled = true; normalizeDelaySettings(settings); saveSettings(); logEvent("Browse loop started. Delay min " + settings.delayMinMs + " ms, max " + settings.delayMaxMs + " ms."); updateStartStopButtons(); mainLoop(); } function stopSniper() { running = false; settings.enabled = false; saveSettings(); updateStartStopButtons(); } // --------------- UI (modal) --------------- function injectStyles() { const css = ` #tpOpenBtnSmart { position: fixed; bottom: 100px; right: 15px; z-index: 999997; padding: 6px 10px; border-radius: 8px; border: 1px solid #555; background: #222; color: #fff; font-size: 12px; cursor: pointer; } #tpModalOverlaySmart { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); z-index: 999998; display: none; justify-content: center; align-items: center; } #tpModalSmart { background: #111; color: #fff; border-radius: 8px; padding: 10px; width: 560px; max-height: 85vh; overflow: auto; box-shadow: 0 0 10px rgba(0, 0, 0, 0.8); font-family: Arial, sans-serif; font-size: 12px; } #tpModalSmart h3 { margin: 0 0 6px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; } #tpModalCloseSmart { cursor: pointer; font-weight: bold; } #tpModalSmart label { display: block; margin: 4px 0 2px; } #tpModalSmart input[type="text"], #tpModalSmart input[type="number"], #tpModalSmart select, #tpModalSmart textarea { width: 100%; box-sizing: border-box; padding: 3px 4px; border-radius: 4px; border: 1px solid #555; background: #000; color: #fff; font-size: 12px; } #tpModalSmart textarea { resize: vertical; } #tpModalSmart .row { display: flex; gap: 6px; } #tpModalSmart .row > div { flex: 1; } #tpModalSmart button { margin-top: 4px; padding: 4px 8px; font-size: 12px; border-radius: 4px; border: 1px solid #555; background: #333; color: #fff; cursor: pointer; } #tpModalSmart button:disabled { opacity: 0.5; cursor: default; } .tp-log-label { margin-top: 6px; font-weight: bold; } .tp-log-box { height: 70px; font-family: monospace; background: #000; color: #0f0; } #tpEventsLogSmart { height: 90px; } .tp-log-controls { display: flex; justify-content: flex-end; margin-top: 2px; gap: 4px; } #tpHintSmart { margin-top: 6px; font-size: 10px; color: #ccc; } .tp-checkbox-row { display: flex; align-items: center; gap: 6px; margin-top: 6px; } .tp-checkbox-row input { width: auto; } #tpBoughtGalleryWrapSmart { margin-top: 6px; border: 1px solid #555; background: #000; border-radius: 6px; padding: 6px; } #tpBoughtGallerySmart { display: flex; flex-direction: column; gap: 8px; max-height: 260px; overflow: auto; } .tp-gallery-empty { color: #aaa; font-size: 11px; padding: 6px; } .tp-purchase-card { border: 1px solid #333; border-radius: 6px; padding: 6px; background: #050505; } .tp-purchase-header { font-size: 11px; color: #cfcfcf; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tp-purchase-items { display: flex; flex-wrap: wrap; gap: 6px; } .tp-item { width: 110px; border: 1px solid #222; background: #0b0b0b; border-radius: 6px; padding: 6px; text-align: center; } .tp-item-img { width: 50px; height: 50px; object-fit: contain; image-rendering: pixelated; background: #000; border-radius: 6px; border: 1px solid #222; } .tp-item-name { margin-top: 4px; font-size: 10px; color: #e6e6e6; line-height: 12px; height: 24px; overflow: hidden; } .tp-item-count { margin-top: 2px; font-size: 10px; color: #7fe7ff; min-height: 12px; } `.trim(); const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); } function createUI() { const openBtn = document.createElement("button"); openBtn.id = "tpOpenBtnSmart"; openBtn.textContent = "TP API Sniper"; document.body.appendChild(openBtn); const overlay = document.createElement("div"); overlay.id = "tpModalOverlaySmart"; overlay.innerHTML = `