Bazaar Undercut Alert

When your bazaar is open, a banner warns when your bazaar items are undercut using data from weav3r.dev

  1. // ==UserScript==
  2. // @name Bazaar Undercut Alert
  3. // @namespace https://torn.com/
  4. // @version 1.0
  5. // @author swervelord [3637232]
  6. // @description When your bazaar is open, a banner warns when your bazaar items are undercut using data from weav3r.dev
  7. //
  8. // @match https://www.torn.com/*
  9. //
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_xmlhttpRequest
  13. // @connect api.torn.com
  14. // @connect weav3r.dev
  15. // ==/UserScript==
  16.  
  17. (() => {
  18. const KEY_STORE = 'torn_api_key';
  19. const CHECK_EVERY_MS = 60_000;
  20. const MAX_TORN_CALLS_PER_MIN = 50;
  21. const PLACEHOLDER_PRICE = 1;
  22.  
  23. let queue = [];
  24. let undercutNow = new Map();
  25. let tornCallsThisMinute = 0;
  26. let bannerDismissed = false;
  27. let bazaarOpen = false;
  28.  
  29. const banner = (() => {
  30. const wrap = document.createElement('div');
  31. wrap.id = 'undercutBanner';
  32. wrap.style.cssText = `
  33. position:fixed;top:0;left:0;right:0;
  34. display:none;
  35. align-items:center;
  36. justify-content:center;
  37. padding:3px 8px;
  38. height:20px;
  39. background:#2c2c2c;
  40. color:#ffffff;
  41. font:600 11px/14px "Segoe UI", sans-serif;
  42. z-index:2147483647;
  43. box-shadow:0 1px 4px rgba(0,0,0,0.2);
  44. cursor:pointer;
  45. `;
  46.  
  47. const textSpan = document.createElement('span');
  48. textSpan.style.cssText = `
  49. text-align:center;
  50. width:100%;
  51. overflow:hidden;
  52. white-space:nowrap;
  53. text-overflow:ellipsis;
  54. `;
  55. wrap.appendChild(textSpan);
  56.  
  57. const close = document.createElement('span');
  58. close.textContent = '✕';
  59. close.style.cssText = `
  60. position:absolute;
  61. top:3px;
  62. right:8px;
  63. font-weight:bold;
  64. font-size:10px;
  65. background:#1a1a1a;
  66. color:white;
  67. border-radius:3px;
  68. padding:0 4px;
  69. cursor:pointer;
  70. line-height:14px;
  71. `;
  72. close.addEventListener('click', e => {
  73. e.stopPropagation();
  74. wrap.style.display = 'none';
  75. bannerDismissed = true;
  76. });
  77.  
  78. wrap.addEventListener('click', () => {
  79. if (!bannerDismissed) {
  80. window.open('https://www.torn.com/bazaar.php#/manage', '_blank');
  81. }
  82. });
  83.  
  84. wrap.appendChild(close);
  85. document.body.prepend(wrap);
  86.  
  87. return { wrap, textSpan };
  88. })();
  89.  
  90. const updateBanner = () => {
  91. if (bannerDismissed || !bazaarOpen || !undercutNow.size) {
  92. banner.wrap.style.display = 'none';
  93. return;
  94. }
  95. banner.wrap.style.display = 'flex';
  96. const items = [...undercutNow.values()]
  97. .map(o => `<span style="color:#00c8d6">${o.name}</span>`).join(', ');
  98. banner.textSpan.innerHTML = `Your items have been undercut: ${items}`;
  99. };
  100.  
  101. const apiKey = () => {
  102. let k = GM_getValue(KEY_STORE, '');
  103. if (!k) {
  104. k = prompt('Enter your Torn API key (MINIMAL access, stored only locally):', '');
  105. if (k) GM_setValue(KEY_STORE, k.trim());
  106. }
  107. return k;
  108. };
  109.  
  110. const httpJSON = url => new Promise((resolve, reject) => {
  111. GM_xmlhttpRequest({
  112. method: 'GET',
  113. url,
  114. headers: { accept: 'application/json' },
  115. onload: r => {
  116. try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(e); }
  117. },
  118. onerror: reject,
  119. timeout: 15000
  120. });
  121. });
  122.  
  123. const resetTornThrottle = () => { tornCallsThisMinute = 0; };
  124.  
  125. const refreshBazaarData = async () => {
  126. if (queue.length || tornCallsThisMinute >= MAX_TORN_CALLS_PER_MIN) return;
  127.  
  128. const key = apiKey();
  129. if (!key) return;
  130.  
  131. tornCallsThisMinute++;
  132. try {
  133. const data = await httpJSON(`https://api.torn.com/user/?selections=bazaar&key=${key}`);
  134. bazaarOpen = !!data.bazaar_is_open;
  135.  
  136. if (!bazaarOpen) { updateBanner(); return; }
  137.  
  138. const fresh = Object.values(data.bazaar || {})
  139. .filter(i => i.price > PLACEHOLDER_PRICE)
  140. .map(i => ({ id: i.ID, name: i.name, price: i.price }));
  141.  
  142. queue.push(...fresh);
  143. } catch (err) {
  144. console.error('Torn API error:', err);
  145. }
  146. };
  147.  
  148. const processQueueBatch = async () => {
  149. while (queue.length) {
  150. const { id, name, price } = queue.shift();
  151. try {
  152. const res = await httpJSON(`https://weav3r.dev/api/marketplace/${id}`);
  153. const lowest = (res.listings || []).reduce((m, l) => Math.min(m, l.price), Infinity);
  154.  
  155. if (Number.isFinite(lowest) && price > lowest) {
  156. undercutNow.set(id, { name, our: price, lowest });
  157. } else {
  158. undercutNow.delete(id);
  159. }
  160. } catch (e) {
  161. console.error('weav3r error:', e);
  162. }
  163. }
  164. updateBanner();
  165. };
  166.  
  167. setInterval(resetTornThrottle, CHECK_EVERY_MS);
  168. setInterval(refreshBazaarData, CHECK_EVERY_MS);
  169. setInterval(processQueueBatch, 2000);
  170.  
  171. refreshBazaarData().then(processQueueBatch);
  172. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址