Individually update your bazaar prices to undercut the lowest by $1 using data from weav3r.dev
// ==UserScript==
// @name Auto Bazaar Pricing
// @namespace https://www.torn.com/
// @version 1.4
// @description Individually update your bazaar prices to undercut the lowest by $1 using data from weav3r.dev
// @author swervelord [3637232]
// @match https://www.torn.com/bazaar.php*
// @grant GM_xmlhttpRequest
// @connect weav3r.dev
// @connect api.torn.com
// ==/UserScript==
(function () {
'use strict';
const SELECTORS = {
linksBar: '[class*="linksContainer___"]',
item: '[data-testid^="item-"]',
row: '[data-testid="sortable-item"], [class*="row___"]',
desc: '[class*="desc___"]',
moneyInput: 'input.input-money:not([type=hidden])'
};
const waitFor = (sel, cb) => {
const el = document.querySelector(sel);
if (el) return cb(el);
const mo = new MutationObserver(() => {
if (!isManagePage()) {
mo.disconnect();
return;
}
const f = document.querySelector(sel);
if (f) {
mo.disconnect();
cb(f);
}
});
routeObservers.add(mo);
mo.observe(document.body, { childList: true, subtree: true });
};
const getJSON = (url) => new Promise((res, rej) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: r => {
if (r.status !== 200) {
rej(new Error(`HTTP ${r.status}`));
return;
}
try {
res(JSON.parse(r.responseText));
} catch (e) {
rej(e);
}
},
onerror: () => rej(new Error('Network error'))
});
});
const setNativeInputValue = (input, value) => {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (!setter) {
input.value = String(value);
} else {
setter.call(input, String(value));
}
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
};
const addStyles = () => {
if (document.querySelector('#undercut-style')) return;
const s = document.createElement('style');
s.id = 'undercut-style';
s.textContent = `
.undercut-label {
display:inline-flex;
align-items:center;
cursor:pointer;
margin-left:8px;
position:relative;
z-index:10;
vertical-align:middle;
}
[data-testid="sortable-item"] .undercut-label,
[class*="row___"] .undercut-label {
margin-left:6px;
}
.undercut-checkbox {
appearance:none;
background:#1f1f1f;
border:1px solid #444;
width:16px;
height:16px;
border-radius:3px;
position:relative;
cursor:pointer;
flex:0 0 auto;
}
.undercut-checkbox:checked::before {
content:'';
position:absolute;
top:2px;
left:5px;
width:4px;
height:8px;
border:solid #666;
border-width:0 2px 2px 0;
transform:rotate(45deg);
}
.undercut-checkbox:disabled {
cursor:wait;
opacity:.55;
}
`;
document.head.appendChild(s);
};
const idCache = new Map();
const routeObservers = new Set();
let started = false;
const isManagePage = () => location.hash.startsWith('#/manage');
const ensureItemCache = async (key) => {
if (idCache.size) return;
const data = await getJSON(`https://api.torn.com/torn/?selections=items&key=${encodeURIComponent(key)}`);
if (!data?.items) throw new Error('items response missing');
Object.entries(data.items).forEach(([id, obj]) => {
if (obj?.name) idCache.set(obj.name.toLowerCase(), Number(id));
});
};
const fetchWeav3r = (id) => getJSON(`https://weav3r.dev/api/marketplace/${id}`);
const getItemName = (itemEl) => {
return itemEl.getAttribute('aria-label') ||
itemEl.querySelector('img[alt]')?.alt ||
itemEl.getAttribute('data-testid')?.replace(/^item-/, '') ||
'';
};
const parseMoney = (value) => {
const n = Number(String(value || '').replace(/[^\d.-]/g, ''));
return Number.isFinite(n) ? n : 0;
};
const findPriceBox = (itemEl) => {
const row = itemEl.closest(SELECTORS.row) || itemEl;
const candidates = [
...itemEl.querySelectorAll(SELECTORS.moneyInput),
...row.querySelectorAll(SELECTORS.moneyInput)
].filter((input, index, arr) => arr.indexOf(input) === index);
if (!candidates.length) return null;
const visible = candidates.filter(input => {
const r = input.getBoundingClientRect();
return r.width > 0 && r.height > 0;
});
const pool = visible.length ? visible : candidates;
const priceLike = pool.filter(input => {
const value = parseMoney(input.value);
const context = (input.closest('label, div, li, tr')?.textContent || '').toLowerCase();
return value >= 1000 ||
context.includes('price') ||
context.includes('cost') ||
input.value.includes(',');
});
return priceLike[0] || (pool.length === 1 ? pool[0] : null);
};
const openRowIfNeeded = async (itemEl) => {
if (findPriceBox(itemEl)) return;
const row = itemEl.closest(SELECTORS.row);
const opener = row?.querySelector('[data-testid="view-icon"], [class*="viewIcon___"], [class*="arrow"], svg');
if (!opener) return;
opener.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
await new Promise(resolve => setTimeout(resolve, 250));
};
const undercutItem = async (itemEl, key) => {
const name = getItemName(itemEl);
try {
if (!name) throw new Error('item name missing');
await ensureItemCache(key);
const id = idCache.get(name.toLowerCase());
if (!id) throw new Error('ID not found');
const { listings = [] } = await fetchWeav3r(id);
if (!listings.length) throw new Error('no listings');
const lowest = Number(listings[0].price);
if (!Number.isFinite(lowest)) throw new Error('listing price missing');
const newP = Math.max(1, lowest - 1);
await openRowIfNeeded(itemEl);
const box = findPriceBox(itemEl);
if (!box) throw new Error('price box missing or ambiguous; expand the row first');
setNativeInputValue(box, newP);
console.info(`[Bazaar] ${name}: set price to ${newP}`);
} catch (e) {
console.error(`[Bazaar] ${name || 'unknown item'}: ${e?.message || e}`);
}
};
const buildSettingsBtn = () => {
if (!isManagePage()) return;
const bar = document.querySelector(SELECTORS.linksBar);
if (!bar || document.querySelector('#undercut-settings-btn')) return;
const existingLink = bar.querySelector('a');
const a = document.createElement('a');
a.id = 'undercut-settings-btn';
a.href = '#';
a.className = existingLink?.className || '';
a.innerHTML = `<span class="linkTitle____NPyM">Settings</span>`;
a.onclick = e => {
e.preventDefault();
e.stopPropagation();
const k = prompt('Enter your Torn API key (requires "items" access):', localStorage.getItem('torn_api_key') || '');
if (k) {
localStorage.setItem('torn_api_key', k.trim());
alert('API key saved');
}
};
bar.prepend(a);
};
const appendCheckbox = (item) => {
if (!isManagePage() || item.querySelector('.undercut-checkbox')) return;
const label = document.createElement('label');
label.className = 'undercut-label';
label.title = 'Undercut lowest Weav3r listing by $1';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'undercut-checkbox';
['pointerdown', 'pointerup', 'click', 'mousedown', 'mouseup']
.forEach(ev => label.addEventListener(ev, e => {
e.stopPropagation();
e.stopImmediatePropagation();
}, true));
checkbox.onchange = async () => {
if (!checkbox.checked) return;
const key = localStorage.getItem('torn_api_key');
if (!key) {
checkbox.checked = false;
alert('Set your Torn API key first using the settings tab.');
return;
}
checkbox.disabled = true;
try {
await undercutItem(item, key);
} finally {
checkbox.disabled = false;
}
};
label.appendChild(checkbox);
const titleTarget = item.querySelector(`${SELECTORS.desc} span`) ||
item.querySelector(SELECTORS.desc) ||
[...item.querySelectorAll('span, div')].find(el => {
const t = el.textContent?.trim() || '';
return t && t.includes(getItemName(item));
}) ||
item;
titleTarget.appendChild(label);
};
const addCheckboxes = () => {
if (!isManagePage()) return;
document.querySelectorAll(SELECTORS.item).forEach(appendCheckbox);
};
const promptForApiKeyIfMissing = () => {
const existing = localStorage.getItem('torn_api_key');
if (!existing) {
const key = prompt('This script requires your Torn API key with "items" access.\nPlease enter it now:');
if (key) localStorage.setItem('torn_api_key', key.trim());
}
};
const stopScriptUi = () => {
document.querySelector('#undercut-style')?.remove();
document.querySelector('#undercut-settings-btn')?.remove();
document.querySelectorAll('.undercut-label').forEach(el => el.remove());
routeObservers.forEach(observer => observer.disconnect());
routeObservers.clear();
started = false;
};
const init = () => {
if (started || !isManagePage()) return;
started = true;
addStyles();
promptForApiKeyIfMissing();
waitFor(SELECTORS.linksBar, buildSettingsBtn);
waitFor(SELECTORS.item, () => {
addCheckboxes();
const mo = new MutationObserver(() => {
if (!isManagePage()) {
stopScriptUi();
return;
}
buildSettingsBtn();
addCheckboxes();
});
routeObservers.add(mo);
mo.observe(document.body, { childList: true, subtree: true });
});
};
const route = () => {
if (isManagePage()) {
init();
} else {
stopScriptUi();
}
};
window.addEventListener('hashchange', route);
route();
})();