// ==UserScript==
// @name Torn Price Filler [Market+Bazaar] by Rosti powered by Weav3r.dev
// @namespace https://github.com/Rosti-dev
// @version 0.9.5
// @description On "Fill" click on the Item Market or Bazaar, autofills the price with the lowest market price minus $1 (customizable), and shows a price popup.
// @author Rosti
// @license MIT License
// @match https://www.torn.com/page.php?sid=ItemMarket*
// @match https://www.torn.com/bazaar.php*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @connect weav3r.dev
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js
// @run-at document-idle
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM.xmlHttpRequest
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
//Credits:
// -Silmaril [2665762] for the Bazaar filler script and the Item Market Price filler (MIT License)
// -Weav3r for providing the Bazaar lisitings API (Without API keys)
// -Chedburn for absolutely making the user experience worse by a huge margin due to his "vision" forcing people to use scripts or burn 5% of every purchase.
(async function() {
'use strict';
// --- SHARED CONFIGURATION AND STATE ---
const itemUrl = "https://api.torn.com/torn/{itemId}?selections=items&key={apiKey}&comment=PriceFiller";
const marketUrlV2 = "https://api.torn.com/v2/market?id={itemId}&selections=itemMarket&key={apiKey}&comment=PriceFiller";
const bazaarUrl = "https://weav3r.dev/api/marketplace/{itemId}";
let priceDeltaRaw = GM_getValue("silmaril-torn-price-filler-price-delta", '-1[0]');
let apiKey = localStorage.getItem("silmaril-torn-price-filler-apikey") ?? '###PDA-APIKEY###';
let showPricesPopup = GM_getValue("silmaril-torn-price-filler-show-prices-popup", true);
let keepPopupOpen = localStorage.getItem("silmaril-torn-price-filler-keep-popup-open") === 'true';
let recentFilledInput = null;
let popupOffsetX = localStorage.getItem("silmaril-torn-price-filler-popup-offset-x") ?? 0;
let popupOffsetY = 0;
let isDragging = false;
let startX, startY;
let closePopupTimer = null;
let refreshInterval = null;
const LOADING_THE_PRICES = 'Loading the prices...';
// --- SHARED UTILITIES ---
const RateLimiter = {
requestQueue: [],
maxRequests: 60,
interval: 60 * 1000,
async processRequest(requestFn) {
const now = Date.now();
this.requestQueue = this.requestQueue.filter(timestamp => now - timestamp < this.interval);
if (this.requestQueue.length >= this.maxRequests) {
const oldestRequest = this.requestQueue[0];
const timeToWait = this.interval - (now - oldestRequest);
console.warn(`[PriceFiller] Rate limit reached. Waiting ${timeToWait}ms.`);
await new Promise(resolve => setTimeout(resolve, timeToWait + 100));
return this.processRequest(requestFn);
}
this.requestQueue.push(Date.now());
return requestFn();
}
};
function GM_addStyle_TornPDA(s) {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = s;
document.head.appendChild(style);
};
class StringBuilder {
constructor() { this.parts = []; }
append(str) { this.parts.push(str); return this; }
toString() { return this.parts.join(''); }
}
function formatNumberWithCommas(number) {
return new Intl.NumberFormat('en-US').format(number);
}
function performOperation(number, operation) {
const match = operation.match(/^([-+]?)(\d+(?:\.\d+)?)(%)?$/);
if (!match) throw new Error('Invalid operation string');
const [, operator, operand, isPercentage] = match;
const operandValue = parseFloat(operand);
const adjustedOperand = isPercentage ? (number * operandValue) / 100 : operandValue;
switch (operator) {
case '+': return number + adjustedOperand;
case '-': return number - adjustedOperand;
default: return number + adjustedOperand;
}
}
function findParentByCondition(element, conditionFn) {
let currentElement = element;
while (currentElement) {
if (conditionFn(currentElement)) return currentElement;
currentElement = currentElement.parentElement;
}
return null;
}
function getApiKey() {
const keySources = [
'tornpda_api_key',
'rosti-torn-price-filler'
];
for (const key of keySources) {
const value = localStorage.getItem(key);
if (value) return value;
}
return null;
}
function checkApiKey() {
apiKey = getApiKey();
if (!apiKey || apiKey.length !== 16) {
let userInput = prompt("Please enter a PUBLIC Torn API Key:");
if (userInput && userInput.length === 16) {
apiKey = userInput;
localStorage.setItem("rosti-torn-price-filler", userInput);
} else {
alert("Invalid API Key. Please provide a valid 16-character key.");
return false;
}
}
return true;
}
// --- API CALLS ---
async function GetPrices(itemId) {
let requestUrl = priceDeltaRaw.includes('[market]') ? itemUrl : marketUrlV2;
requestUrl = requestUrl.replace("{itemId}", itemId).replace("{apiKey}", apiKey);
return fetch(requestUrl)
.then(response => response.json())
.then(data => {
if (data.error) {
console.error("[PriceFiller] Error fetching market data:", data.error.error);
return [];
}
if (priceDeltaRaw.includes('[market]')) {
return [{"price": data.items[itemId].market_value, "amount": 1}];
}
return data.itemmarket?.listings || [];
})
.catch(error => {
console.error("[PriceFiller] Error fetching market data:", error);
return [];
});
}
async function GetBazaarPrices(itemId) {
return RateLimiter.processRequest(() => new Promise((resolve) => {
const url = bazaarUrl.replace('{itemId}', itemId);
GM.xmlHttpRequest({
method: "GET",
url: url,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
resolve(data?.listings?.map(l => ({ price: l.price, amount: l.quantity })) || []);
} catch (e) {
console.error("[PriceFiller] Error parsing bazaar data:", e);
resolve([]);
}
},
onerror: function(error) {
console.error("[PriceFiller] Error fetching bazaar data:", error);
resolve([]);
}
});
}));
}
// --- POPUP UI ---
function addCustomFillPopup() {
const popup = document.createElement('div');
popup.className = 'silmaril-price-filler-popup';
popup.style.display = 'none';
popup.innerHTML = `
<div class="silmaril-price-filler-popup-close" title="Close">×</div>
<b class="silmaril-price-filler-popup-draggable" style="cursor: move; user-select: none;">Drag from here</b>
<div class="silmaril-price-filler-popup-body" style="margin-top: 2px;"></div>
<div class="silmaril-price-filler-popup-footer" style="margin-top: 4px;">
<input type="checkbox" id="silmaril-price-filler-keep-open" style="margin: 0; vertical-align: middle;" />
<label for="silmaril-price-filler-keep-open" style="margin-left: 4px; vertical-align: middle;">Keep open</label>
</div>
`;
popup.querySelector('.silmaril-price-filler-popup-close').onclick = hideAllFillPopups;
document.body.appendChild(popup);
const dragHandle = popup.querySelector('.silmaril-price-filler-popup-draggable');
const startDrag = (e) => {
isDragging = true;
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
startX = clientX - popup.offsetLeft;
startY = clientY - popup.offsetTop;
e.preventDefault();
};
const drag = (e) => {
if (isDragging) {
const clientX = e.clientX || e.touches[0].clientX;
const clientY = e.clientY || e.touches[0].clientY;
popup.style.left = (clientX - startX) + "px";
popup.style.top = (clientY - startY) + "px";
}
};
const endDrag = () => {
if (isDragging) {
isDragging = false;
localStorage.setItem("silmaril-torn-price-filler-popup-offset-x", popup.style.left);
localStorage.setItem("silmaril-torn-price-filler-popup-offset-y", popup.style.top);
}
};
dragHandle.addEventListener("mousedown", startDrag);
document.addEventListener("mousemove", drag);
document.addEventListener("mouseup", endDrag);
dragHandle.addEventListener("touchstart", startDrag);
document.addEventListener("touchmove", drag);
document.addEventListener("touchend", endDrag);
}
function showCustomFillPopup(contentHTML) {
const popup = document.querySelector('.silmaril-price-filler-popup');
popup.querySelector('.silmaril-price-filler-popup-body').innerHTML = contentHTML;
popup.querySelectorAll('.silmaril-price-filler-popup-price').forEach(row => {
row.addEventListener('click', (e) => {
if (recentFilledInput) {
recentFilledInput.forEach(x => { x.value = parseInt(e.target.getAttribute('data-price')) - 1 });
recentFilledInput[0].dispatchEvent(new Event("input", { bubbles: true }));
}
});
});
}
function hideAllFillPopups() {
const popup = document.querySelector('.silmaril-price-filler-popup');
if (popup) popup.style.display = 'none';
clearInterval(refreshInterval);
}
function GetPricesBreakdown(marketPrices, bazaarPrices) {
const marketTaxFactor = 1 - 0.05;
const sb = new StringBuilder();
sb.append('<div class="silmaril-price-filler-popup-table">');
const buildColumn = (title, prices, includeTax) => {
sb.append(`<div class="silmaril-price-filler-popup-col"><b>${title}</b>`);
if (prices && prices.length > 0) {
for (let i = 0; i < Math.min(prices.length, 5); i++) {
const item = prices[i];
if (typeof item !== "object" || item.amount === undefined || item.price === undefined) continue;
let priceText = `${item.amount} x ${formatNumberWithCommas(item.price)}`;
if (includeTax) {
priceText += ` (${formatNumberWithCommas(Math.round(item.price * marketTaxFactor))})`;
}
sb.append(`<span class="silmaril-price-filler-popup-price" data-price=${item.price}>${priceText}</span>`);
}
} else {
sb.append('<span>No listings found.</span>');
}
sb.append('</div>');
};
buildColumn('Item Market', marketPrices, true);
buildColumn('Bazaar', bazaarPrices, false);
sb.append('</div>');
return sb.toString();
}
async function handleFillClick(event, itemId, priceInputs, quantityCallback) {
if (!checkApiKey()) return;
clearTimeout(closePopupTimer);
clearInterval(refreshInterval);
recentFilledInput = priceInputs;
const popup = document.querySelector('.silmaril-price-filler-popup');
if (popup && showPricesPopup) {
const rect = event.currentTarget.getBoundingClientRect();
const savedX = localStorage.getItem("silmaril-torn-price-filler-popup-offset-x");
const savedY = localStorage.getItem("silmaril-torn-price-filler-popup-offset-y");
popup.style.left = savedX ? savedX : `${window.scrollX + rect.left - 250}px`;
popup.style.top = savedY ? savedY : `${window.scrollY + rect.top + 4}px`;
popup.style.display = 'block';
popup.querySelector('.silmaril-price-filler-popup-body').innerHTML = LOADING_THE_PRICES;
}
const updatePopupContent = async () => {
let [marketPrices, bazaarPrices] = await Promise.all([GetPrices(itemId), GetBazaarPrices(itemId)]);
if (showPricesPopup) {
const breakdown = GetPricesBreakdown(marketPrices, bazaarPrices);
showCustomFillPopup(breakdown);
}
return { marketPrices, bazaarPrices };
};
let { marketPrices, bazaarPrices } = await updatePopupContent();
if (showPricesPopup) {
const startRefresh = () => {
clearInterval(refreshInterval);
refreshInterval = setInterval(updatePopupContent, 60000);
};
const keepOpenCheckbox = popup.querySelector('#silmaril-price-filler-keep-open');
keepOpenCheckbox.checked = keepPopupOpen;
if (keepOpenCheckbox.checked) {
startRefresh();
} else {
closePopupTimer = setTimeout(hideAllFillPopups, 2000);
}
keepOpenCheckbox.onchange = () => {
keepPopupOpen = keepOpenCheckbox.checked;
localStorage.setItem("silmaril-torn-price-filler-keep-popup-open", keepPopupOpen);
if (keepPopupOpen) {
clearTimeout(closePopupTimer);
startRefresh();
} else {
clearInterval(refreshInterval);
closePopupTimer = setTimeout(hideAllFillPopups, 3000);
}
};
}
const GetPrice = (prices) => {
if (!prices || prices.length === 0) return '';
if (priceDeltaRaw.includes('[median]')) {
const sortedPrices = prices.map(p => p.price).sort((a, b) => a - b);
const mid = Math.floor(sortedPrices.length / 2);
const median = sortedPrices.length % 2 === 0 ? (sortedPrices[mid - 1] + sortedPrices[mid]) / 2 : sortedPrices[mid];
let priceDelta = priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(median, priceDelta));
} else if (priceDeltaRaw.includes('[market]')) {
let priceDelta = priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('['));
return Math.round(performOperation(prices[0].price, priceDelta));
} else {
let marketSlotOffset = priceDeltaRaw.includes('[') ? parseInt(priceDeltaRaw.substring(priceDeltaRaw.indexOf('[') + 1, priceDeltaRaw.indexOf(']'))) : 0;
let priceDeltaWithoutMarketOffset = priceDeltaRaw.includes('[') ? priceDeltaRaw.substring(0, priceDeltaRaw.indexOf('[')) : priceDeltaRaw;
return Math.round(performOperation(prices[Math.min(marketSlotOffset, prices.length - 1)].price, priceDeltaWithoutMarketOffset));
}
};
const isBazaar = window.location.href.includes("bazaar.php");
const pricesToUse = isBazaar ? bazaarPrices : marketPrices;
let price = GetPrice(pricesToUse);
priceInputs.forEach(x => { x.value = price });
priceInputs[0].dispatchEvent(new Event("input", { bubbles: true }));
if (quantityCallback) {
quantityCallback();
}
}
// --- PAGE-SPECIFIC INITIALIZERS ---
function addDisablePopupCheckbox(container, insertBeforeElement, isMarket) {
if (document.getElementById('disable-popup-checkbox-container')) return;
const checkboxContainer = document.createElement('div');
checkboxContainer.id = 'disable-popup-checkbox-container';
checkboxContainer.style.display = 'inline-flex';
checkboxContainer.style.alignItems = 'center';
if(isMarket) {
checkboxContainer.style.marginLeft = '10px';
} else {
checkboxContainer.style.marginRight = '10px';
}
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'disable-popup-checkbox';
checkbox.checked = showPricesPopup;
checkbox.style.margin = '0';
const label = document.createElement('label');
label.htmlFor = 'disable-popup-checkbox';
label.textContent = 'Show Popup';
label.style.marginLeft = '5px';
label.style.fontWeight = 'normal';
checkbox.addEventListener('change', () => {
showPricesPopup = checkbox.checked;
GM_setValue("silmaril-torn-price-filler-show-prices-popup", showPricesPopup);
});
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
if (insertBeforeElement) {
container.insertBefore(checkboxContainer, insertBeforeElement);
} else {
container.appendChild(checkboxContainer);
}
}
function initMarketPage() {
const observerTarget = document.querySelector("#item-market-root");
if (!observerTarget) return;
const observer = new MutationObserver(mutations => {
mutations.forEach(mutationRaw => {
const mutation = mutationRaw.target;
const isAddItems = window.location.href.includes('#/addListing');
const isViewItems = window.location.href.includes('#/viewListing');
if (isAddItems || isViewItems) {
const selector = '[class*=itemRowWrapper___]:not(.price-filler-processed) > [class*=itemRow___]:not([class*=grayedOut___]) [class^=priceInputWrapper___]';
mutation.querySelectorAll(selector).forEach(x => addMarketFillButton(x));
}
});
// Add checkbox for item market
const controlsContainer = document.querySelector('[class*="controls___"]');
if (controlsContainer) {
addDisablePopupCheckbox(controlsContainer, null, true);
}
});
observer.observe(observerTarget, { childList: true, subtree: true });
}
function addMarketFillButton(itemPriceElement) {
if (itemPriceElement.querySelector('.price-filler-button')) return;
const wrapperParent = findParentByCondition(itemPriceElement, (el) => String(el.className).includes('itemRowWrapper___'));
if (!wrapperParent) return;
wrapperParent.classList.add('price-filler-processed');
const itemIdString = wrapperParent.querySelector('[class^=itemRow___] [type=button][class^=viewInfoButton___]')?.getAttribute('aria-controls');
const itemImage = wrapperParent.querySelector('[class*=viewInfoButton] img');
const itemId = window.location.href.includes('#/addListing')
? (itemIdString?.match(/-(\d+)-/)?.[1] || -1)
: (itemImage?.src.match(/\/(\d+)\//)?.[1] || -1);
if (itemId === -1) return;
const span = document.createElement('span');
span.className = 'price-filler-button input-money-symbol';
span.style.position = "relative";
span.onclick = (e) => {
const priceInputs = Array.from(itemPriceElement.querySelectorAll('input.input-money'));
const quantityInputs = findParentByCondition(e.target, (el) => String(el.className).includes('info___'))
?.querySelectorAll('[class^=amountInputWrapper___] .input-money-group > .input-money');
handleFillClick(e, itemId, priceInputs, () => {
if (quantityInputs && quantityInputs.length > 0) {
if (quantityInputs[0].value.length === 0 || parseInt(quantityInputs[0].value) < 1) {
quantityInputs[0].value = Number.MAX_SAFE_INTEGER;
quantityInputs[1].value = Number.MAX_SAFE_INTEGER;
}
quantityInputs[0].dispatchEvent(new Event("input", { bubbles: true }));
}
});
};
const input = document.createElement('input');
input.type = 'button';
input.className = 'wai-btn';
span.appendChild(input);
itemPriceElement.querySelector('.input-money-group').prepend(span);
}
function initBazaarPage() {
const processBazaarItems = () => {
// For #/add page
$("ul.items-cont li.clearfix").each(function() {
const targetElement = $(this).find("div.title-wrap div.name-wrap")[0];
if (targetElement && !$(this).hasClass("disabled") && !targetElement.querySelector('.torn-bazaar-fill-qty-price')) {
addBazaarFillButtons(targetElement);
}
});
// For #/manage page
$("div[class*=row___]").each(function() {
const targetElement = $(this).find("div[class*=item___] div[class*=desc___]")[0];
if (targetElement && !targetElement.querySelector('.torn-bazaar-fill-qty-price')) {
addBazaarFillButtons(targetElement);
}
});
// Add checkbox for bazaar on manage page
if (window.location.hash.includes('#/manage')) {
const linksContainer = document.querySelector('.linksContainer___LiOTN');
const addItemsLink = document.querySelector('a[href="#/add"]');
if (linksContainer && addItemsLink && !document.getElementById('disable-popup-checkbox-container')) {
addDisablePopupCheckbox(linksContainer, addItemsLink, false);
}
}
};
const observerTarget = document.querySelector(".content-wrapper");
if (!observerTarget) return;
const observer = new MutationObserver(processBazaarItems);
observer.observe(observerTarget, { childList: true, subtree: true });
window.addEventListener('hashchange', () => {
// A brief delay to allow the page to render after hash change
setTimeout(processBazaarItems, 100);
});
processBazaarItems(); // Initial run
}
function addBazaarFillButtons(element) {
const outerSpanFill = document.createElement('span');
outerSpanFill.className = 'btn-wrap torn-bazaar-fill-qty-price';
const innerSpanFill = document.createElement('span');
innerSpanFill.className = 'btn';
const inputElementFill = document.createElement('input');
inputElementFill.type = 'button';
inputElementFill.value = "Fill";
inputElementFill.className = 'torn-btn';
innerSpanFill.appendChild(inputElementFill);
outerSpanFill.appendChild(innerSpanFill);
element.append(outerSpanFill);
$(outerSpanFill).on("click", "input", function(event) {
event.stopPropagation();
const itemRow = $(this).closest('li.clearfix, div[class*=row___]');
const image = itemRow.find("div.image-wrap img, div.imgContainer___tEZeE img")[0];
const itemId = image.src.match(/\/(\d+)\//)?.[1];
if (!itemId) return;
let priceInputs;
// Check for mobile view on the manage page
if (window.location.hash.includes('#/manage') && window.innerWidth <= 784) {
priceInputs = Array.from(itemRow.find('.priceMobile___cpt8p .input-money-group input'));
} else {
priceInputs = Array.from(itemRow.find("div.price div input, div[class*=price___] div.input-money-group input"));
}
const quantityInput = itemRow.find("div.amount input")[0];
const quantityElement = itemRow.find('span.t-hide span:last-child');
const quantity = quantityElement.length > 0 ? quantityElement.text().trim() : 1;
handleFillClick(event, itemId, priceInputs, () => {
if (quantityInput) {
quantityInput.value = quantity;
quantityInput.dispatchEvent(new Event("keyup", { bubbles: true }));
}
});
});
}
// --- SCRIPT INITIALIZATION ---
function init() {
GM_addStyle_TornPDA(`
.silmaril-price-filler-popup { background: var(--tooltip-bg-color); padding: 5px 10px; border-radius: 5px; border: 1px solid #888; box-shadow: 0 2px 8px 0 #0006; color: var(--info-msg-font-color); z-index: 99999; position: absolute; font-size: 0.9em; line-height: 1.2; pointer-events: auto; }
.silmaril-price-filler-popup-close { position: absolute; top: 1px; right: 5px; font-size: 1.2em; color: #aaa; cursor: pointer; }
.silmaril-price-filler-popup-price { cursor: pointer; display: block; margin-bottom: 1px; }
.silmaril-price-filler-popup-table { display: flex; gap: 15px; }
.silmaril-price-filler-popup-col { display: flex; flex-direction: column; gap: 2px; }
.price-filler-button { position: relative; }
.btn-wrap.torn-bazaar-fill-qty-price { float: right; margin-left: auto; z-index: 99999; }
`);
addCustomFillPopup();
const href = window.location.href;
if (href.includes("page.php?sid=ItemMarket")) {
initMarketPage();
} else if (href.includes("bazaar.php")) {
initBazaarPage();
}
GM_registerMenuCommand("Change Price Delta", () => {
const newPriceDelta = prompt("Enter new price delta (e.g., -1, -10%, -1[median], -1[market]):", priceDeltaRaw);
if (newPriceDelta) {
priceDeltaRaw = newPriceDelta;
GM_setValue("silmaril-torn-price-filler-price-delta", newPriceDelta);
}
});
}
init();
})();