├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ └── feature.yml
└── workflows
│ └── lint.yml
├── .gitignore
├── LICENSE
├── README.md
├── assets
├── icon.svg
├── inventory.png
├── market.png
├── settings.png
└── tradeoffer.png
├── code.user.js
├── eslint.config.mjs
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://steamcommunity.com/id/nuklon/']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report
3 | labels: [bug]
4 | body:
5 | - type: textarea
6 | attributes:
7 | label: Describe the bug
8 | description: A clear and concise description of what the bug is and what page it happens on. Provide a screenshot and console logs if applicable.
9 | validations:
10 | required: true
11 |
12 | - type: input
13 | attributes:
14 | label: URL
15 | description: "The complete URL where the problem was found. I.e: https://steamcommunity.com/market/"
16 | validations:
17 | required: true
18 |
19 | - type: input
20 | attributes:
21 | label: Browser name and version
22 | validations:
23 | required: true
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.yml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea
3 | labels: [enhancement]
4 | body:
5 | - type: textarea
6 | attributes:
7 | label: Is your feature request related to a problem? Please describe.
8 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 | validations:
10 | required: true
11 |
12 | - type: textarea
13 | attributes:
14 | label: Describe the solution you'd like
15 | description: A clear and concise description of what you want to happen.
16 |
17 | - type: textarea
18 | attributes:
19 | label: Describe alternatives you've considered
20 | description: A clear and concise description of any alternative solutions or features you've considered.
21 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | permissions:
4 | contents: read
5 |
6 | on: [push, pull_request]
7 |
8 | jobs:
9 | build:
10 | name: CI
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 |
18 | - name: Install
19 | run: npm install
20 |
21 | - name: Test
22 | run: npm test
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /package-lock.json
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Steam Economy Enhancer
2 |
3 | A free userscript to enhance your Steam Inventory, Steam Market and Steam Tradeoffers.
4 |
5 | It adds the following features to the Steam Market:
6 |
7 | * Detect overpriced and underpriced items.
8 | * Select 5/25/all (overpriced) items and remove them at once.
9 | * (Automatically) relist overpriced items.
10 | * Sort and search items by name, price or date.
11 | * Total price for listings, as seller and buyer.
12 |
13 | It adds the following features to the Steam Inventory:
14 |
15 | * Sell all (selected) items or trading cards automatically.
16 | * Select multiple items simultaneously with *Shift* or *Ctrl*.
17 | * Market sell and buy listings added to the item details.
18 | * Quick sell buttons to sell an item without confirmations.
19 | * Shows the lowest listed price for each item.
20 | * Turn selected items into gems.
21 | * Unpack selected booster packs.
22 |
23 | It adds the following features to the Steam Tradeoffers:
24 |
25 | * A summary of all items from both parties that includes total number of items, number of unique items and item count breakdown (how many of each item there are)
26 | * Select all items of the current page.
27 | * Shows the lowest listed price for each inventory item.
28 |
29 | The pricing can be based on the lowest listed price, the price history and your own minimum and maximum prices.
30 | This can be defined in Steam Economy Enhancer's settings, which you can find at the top of the page near the *Install Steam* button.
31 |
32 | > [!NOTE]
33 | > It is free but there is **NO** support. If you want to add functionality, feel free to submit a PR.
34 |
35 | ### Download
36 |
37 | [Install Steam Economy Enhancer](https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js)
38 |
39 | *[Violentmonkey](https://violentmonkey.github.io/) is required to install.*
40 |
41 | ### Screenshots
42 |
43 |
44 | *Market*
45 |
46 | 
47 |
48 |
49 | *Inventory*
50 |
51 | 
52 |
53 |
54 | *Options*
55 |
56 | 
57 |
58 |
59 | *Trade offers*
60 |
61 | 
62 |
63 |
64 | ### License
65 |
66 | [MIT](LICENSE)
67 |
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/assets/inventory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/inventory.png
--------------------------------------------------------------------------------
/assets/market.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/market.png
--------------------------------------------------------------------------------
/assets/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/settings.png
--------------------------------------------------------------------------------
/assets/tradeoffer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/0bacb415e8fad8bf84fb0d8244f75a53efbe76d7/assets/tradeoffer.png
--------------------------------------------------------------------------------
/code.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Steam Economy Enhancer
3 | // @icon data:image/svg+xml,%0A%3Csvg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 267 267"%3E%3Ccircle cx="133.3" cy="133.3" r="133.3" fill="%2326566c"/%3E%3Cpath fill="%23ebebeb" fill-rule="nonzero" d="m50 133 83-83 84 83-84 84-83-84Zm83 62 62-61-62-62v123Z"/%3E%3C/svg%3E
4 | // @namespace https://github.com/Nuklon
5 | // @author Nuklon
6 | // @license MIT
7 | // @version 7.1.12
8 | // @description Enhances the Steam Inventory and Steam Market.
9 | // @match https://steamcommunity.com/id/*/inventory*
10 | // @match https://steamcommunity.com/profiles/*/inventory*
11 | // @match https://steamcommunity.com/market*
12 | // @match https://steamcommunity.com/tradeoffer*
13 | // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
14 | // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.14.1/jquery-ui.min.js
15 | // @require https://cdnjs.cloudflare.com/ajax/libs/async/3.2.6/async.js
16 | // @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js
17 | // @require https://cdnjs.cloudflare.com/ajax/libs/luxon/3.5.0/luxon.min.js
18 | // @require https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.js
19 | // @require https://raw.githubusercontent.com/kapetan/jquery-observe/ca67b735bb3ae8d678d1843384ebbe7c02466c61/jquery-observe.js
20 | // @require https://raw.githubusercontent.com/rmariuzzo/checkboxes.js/91bec667e9172ceb063df1ecb7505e8ed0bae9ba/src/jquery.checkboxes.js
21 | // @grant unsafeWindow
22 | // @homepageURL https://github.com/Nuklon/Steam-Economy-Enhancer
23 | // @homepage https://github.com/Nuklon/Steam-Economy-Enhancer
24 | // @supportURL https://github.com/Nuklon/Steam-Economy-Enhancer/issues
25 | // @downloadURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
26 | // @updateURL https://raw.githubusercontent.com/Nuklon/Steam-Economy-Enhancer/master/code.user.js
27 | // ==/UserScript==
28 |
29 | /* disable some eslint rules until the code is cleaned up */
30 | /* global unsafeWindow, luxon, jQuery, async, List, localforage */
31 | /* eslint no-undef: off */
32 |
33 | // jQuery is already added by Steam, force no conflict mode.
34 | (function ($, async) {
35 | $.noConflict(true);
36 |
37 | const PAGE_MARKET = 0;
38 | const PAGE_MARKET_LISTING = 1;
39 | const PAGE_TRADEOFFER = 2;
40 | const PAGE_INVENTORY = 3;
41 |
42 | const COLOR_ERROR = '#8A4243';
43 | const COLOR_SUCCESS = '#407736';
44 | const COLOR_PENDING = '#908F44';
45 | const COLOR_PRICE_FAIR = '#496424';
46 | const COLOR_PRICE_CHEAP = '#837433';
47 | const COLOR_PRICE_EXPENSIVE = '#813030';
48 | const COLOR_PRICE_NOT_CHECKED = '#26566c';
49 |
50 | const ERROR_SUCCESS = null;
51 | const ERROR_FAILED = 1;
52 | const ERROR_DATA = 2;
53 |
54 | const marketLists = [];
55 | let totalNumberOfProcessedQueueItems = 0;
56 | let totalNumberOfQueuedItems = 0;
57 | let totalPriceWithFeesOnMarket = 0;
58 | let totalPriceWithoutFeesOnMarket = 0;
59 | let totalScrap = 0;
60 |
61 | const spinnerBlock =
62 | '
';
63 | let numberOfFailedRequests = 0;
64 |
65 | const enableConsoleLog = false;
66 |
67 | const country = typeof unsafeWindow.g_strCountryCode !== 'undefined' ? unsafeWindow.g_strCountryCode : undefined;
68 | const isLoggedIn = typeof unsafeWindow.g_rgWalletInfo !== 'undefined' && unsafeWindow.g_rgWalletInfo != null || typeof unsafeWindow.g_bLoggedIn !== 'undefined' && unsafeWindow.g_bLoggedIn;
69 |
70 | const currentPage = window.location.href.includes('.com/market')
71 | ? window.location.href.includes('market/listings')
72 | ? PAGE_MARKET_LISTING
73 | : PAGE_MARKET
74 | : window.location.href.includes('.com/tradeoffer')
75 | ? PAGE_TRADEOFFER
76 | : PAGE_INVENTORY;
77 |
78 | const market = new SteamMarket(
79 | unsafeWindow.g_rgAppContextData,
80 | getInventoryUrl(),
81 | isLoggedIn ? unsafeWindow.g_rgWalletInfo : undefined
82 | );
83 |
84 | const currencyId =
85 | isLoggedIn &&
86 | market != null &&
87 | market.walletInfo != null &&
88 | market.walletInfo.wallet_currency != null
89 | ? market.walletInfo.wallet_currency
90 | : 3;
91 |
92 | const currencyCountry =
93 | isLoggedIn &&
94 | market != null &&
95 | market.walletInfo != null &&
96 | market.walletInfo.wallet_country != null
97 | ? market.walletInfo.wallet_country
98 | : 'US';
99 |
100 | const currencyCode = unsafeWindow.GetCurrencyCode(currencyId);
101 |
102 | function SteamMarket(appContext, inventoryUrl, walletInfo) {
103 | this.appContext = appContext;
104 | this.inventoryUrl = inventoryUrl;
105 | this.walletInfo = walletInfo;
106 | this.inventoryUrlBase = inventoryUrl.replace('/inventory/json', '');
107 | if (!this.inventoryUrlBase.endsWith('/')) {
108 | this.inventoryUrlBase += '/';
109 | }
110 | }
111 |
112 | function request(url, options, callback) {
113 | let delayBetweenRequests = 300;
114 | let requestStorageHash = 'see:request:last';
115 |
116 | if (url.startsWith('https://steamcommunity.com/market/')) {
117 | requestStorageHash = `${requestStorageHash}:steamcommunity.com/market`;
118 | delayBetweenRequests = 1000;
119 | }
120 |
121 | const lastRequest = JSON.parse(getLocalStorageItem(requestStorageHash) || JSON.stringify({ time: new Date(0), limited: false }));
122 | const timeSinceLastRequest = Date.now() - new Date(lastRequest.time).getTime();
123 |
124 | delayBetweenRequests = lastRequest.limited ? 2.5 * 60 * 1000 : delayBetweenRequests;
125 |
126 | if (timeSinceLastRequest < delayBetweenRequests) {
127 | setTimeout(() => request(...arguments), delayBetweenRequests - timeSinceLastRequest);
128 | return;
129 | }
130 |
131 | lastRequest.time = new Date();
132 | lastRequest.limited = false;
133 |
134 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
135 |
136 | $.ajax({
137 | url: url,
138 | type: options.method,
139 | data: options.data,
140 | success: function (data, statusMessage, xhr) {
141 | if (xhr.status === 429) {
142 | lastRequest.limited = true;
143 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
144 | }
145 |
146 | if (xhr.status >= 400) {
147 | const error = new Error('Http error');
148 | error.statusCode = xhr.status;
149 |
150 | callback(error, data);
151 | } else {
152 | callback(null, data)
153 | }
154 | },
155 | error: (xhr) => {
156 | if (xhr.status === 429) {
157 | lastRequest.limited = true;
158 | setLocalStorageItem(requestStorageHash, JSON.stringify(lastRequest));
159 | }
160 |
161 | const error = new Error('Request failed');
162 | error.statusCode = xhr.status;
163 |
164 | callback(error);
165 | },
166 | dataType: options.responseType
167 | });
168 | };
169 |
170 | function getInventoryUrl() {
171 | if (unsafeWindow.g_strInventoryLoadURL) {
172 | return unsafeWindow.g_strInventoryLoadURL;
173 | }
174 |
175 | let profileUrl = `${window.location.origin}/my/`;
176 |
177 | if (unsafeWindow.g_strProfileURL) {
178 | profileUrl = unsafeWindow.g_strProfileURL;
179 | } else {
180 | const avatar = document.querySelector('#global_actions a.user_avatar');
181 |
182 | if (avatar) {
183 | profileUrl = avatar.href;
184 | }
185 | }
186 |
187 | return `${profileUrl.replace(/\/$/, '')}/inventory/json/`;
188 | }
189 |
190 | //#region Settings
191 | const SETTING_MIN_NORMAL_PRICE = 'SETTING_MIN_NORMAL_PRICE';
192 | const SETTING_MAX_NORMAL_PRICE = 'SETTING_MAX_NORMAL_PRICE';
193 | const SETTING_MIN_FOIL_PRICE = 'SETTING_MIN_FOIL_PRICE';
194 | const SETTING_MAX_FOIL_PRICE = 'SETTING_MAX_FOIL_PRICE';
195 | const SETTING_MIN_MISC_PRICE = 'SETTING_MIN_MISC_PRICE';
196 | const SETTING_MAX_MISC_PRICE = 'SETTING_MAX_MISC_PRICE';
197 | const SETTING_PRICE_OFFSET = 'SETTING_PRICE_OFFSET';
198 | const SETTING_PRICE_MIN_CHECK_PRICE = 'SETTING_PRICE_MIN_CHECK_PRICE';
199 | const SETTING_PRICE_MIN_LIST_PRICE = 'SETTING_PRICE_MIN_LIST_PRICE';
200 | const SETTING_PRICE_ALGORITHM = 'SETTING_PRICE_ALGORITHM';
201 | const SETTING_PRICE_IGNORE_LOWEST_Q = 'SETTING_PRICE_IGNORE_LOWEST_Q';
202 | const SETTING_PRICE_HISTORY_HOURS = 'SETTING_PRICE_HISTORY_HOURS';
203 | const SETTING_INVENTORY_PRICE_LABELS = 'SETTING_INVENTORY_PRICE_LABELS';
204 | const SETTING_TRADEOFFER_PRICE_LABELS = 'SETTING_TRADEOFFER_PRICE_LABELS';
205 | const SETTING_QUICK_SELL_BUTTONS = 'SETTING_QUICK_SELL_BUTTONS';
206 | const SETTING_LAST_CACHE = 'SETTING_LAST_CACHE';
207 | const SETTING_RELIST_AUTOMATICALLY = 'SETTING_RELIST_AUTOMATICALLY';
208 |
209 | const settingDefaults = {
210 | SETTING_MIN_NORMAL_PRICE: 0.05,
211 | SETTING_MAX_NORMAL_PRICE: 2.50,
212 | SETTING_MIN_FOIL_PRICE: 0.15,
213 | SETTING_MAX_FOIL_PRICE: 10,
214 | SETTING_MIN_MISC_PRICE: 0.05,
215 | SETTING_MAX_MISC_PRICE: 10,
216 | SETTING_PRICE_OFFSET: 0.00,
217 | SETTING_PRICE_MIN_CHECK_PRICE: 0.00,
218 | SETTING_PRICE_MIN_LIST_PRICE: 0.03,
219 | SETTING_PRICE_ALGORITHM: 1,
220 | SETTING_PRICE_IGNORE_LOWEST_Q: 1,
221 | SETTING_PRICE_HISTORY_HOURS: 12,
222 | SETTING_INVENTORY_PRICE_LABELS: 1,
223 | SETTING_TRADEOFFER_PRICE_LABELS: 1,
224 | SETTING_QUICK_SELL_BUTTONS: 1,
225 | SETTING_LAST_CACHE: 0,
226 | SETTING_RELIST_AUTOMATICALLY: 0
227 | };
228 |
229 | function getSettingWithDefault(name) {
230 | return getLocalStorageItem(name) || (name in settingDefaults ? settingDefaults[name] : null);
231 | }
232 |
233 | function setSetting(name, value) {
234 | setLocalStorageItem(name, value);
235 | }
236 | //#endregion
237 |
238 | //#region Storage
239 |
240 | const storagePersistent = localforage.createInstance({
241 | name: 'see_persistent'
242 | });
243 |
244 | let storageSession;
245 |
246 | const currentUrl = new URL(window.location.href);
247 | const noCache = currentUrl.searchParams.get('no-cache') != null;
248 |
249 | // This does not work the same as the 'normal' session storage because opening a new browser session/tab will clear the cache.
250 | // For this reason, a rolling cache is used.
251 | if (getSessionStorageItem('SESSION') == null || noCache) {
252 | let lastCache = getSettingWithDefault(SETTING_LAST_CACHE);
253 | if (lastCache > 5) {
254 | lastCache = 0;
255 | }
256 |
257 | setSetting(SETTING_LAST_CACHE, lastCache + 1);
258 |
259 | storageSession = localforage.createInstance({
260 | name: `see_session_${lastCache}`
261 | });
262 |
263 | storageSession.clear(); // Clear any previous data.
264 | setSessionStorageItem('SESSION', lastCache);
265 | } else {
266 | storageSession = localforage.createInstance({
267 | name: `see_session_${getSessionStorageItem('SESSION')}`
268 | });
269 | }
270 |
271 | function getLocalStorageItem(name) {
272 | try {
273 | return localStorage.getItem(name);
274 | } catch (e) {
275 | logConsole(`Failed to get local storage item ${name}, ${e}.`);
276 | return null;
277 | }
278 | }
279 |
280 | function setLocalStorageItem(name, value) {
281 | try {
282 | localStorage.setItem(name, value);
283 | return true;
284 | } catch (e) {
285 | logConsole(`Failed to set local storage item ${name}, ${e}.`);
286 | return false;
287 | }
288 | }
289 |
290 | function getSessionStorageItem(name) {
291 | try {
292 | return sessionStorage.getItem(name);
293 | } catch (e) {
294 | logConsole(`Failed to get session storage item ${name}, ${e}.`);
295 | return null;
296 | }
297 | }
298 |
299 | function setSessionStorageItem(name, value) {
300 | try {
301 | sessionStorage.setItem(name, value);
302 | return true;
303 | } catch (e) {
304 | logConsole(`Failed to set session storage item ${name}, ${e}.`);
305 | return false;
306 | }
307 | }
308 | //#endregion
309 |
310 | //#region Price helpers
311 | function formatPrice(valueInCents) {
312 | return unsafeWindow.v_currencyformat(valueInCents, currencyCode, currencyCountry);
313 | }
314 |
315 | function getPriceInformationFromItem(item) {
316 | const isTradingCard = getIsTradingCard(item);
317 | const isFoilTradingCard = getIsFoilTradingCard(item);
318 | return getPriceInformation(isTradingCard, isFoilTradingCard);
319 | }
320 |
321 | function getPriceInformation(isTradingCard, isFoilTradingCard) {
322 | let maxPrice = 0;
323 | let minPrice = 0;
324 |
325 | if (!isTradingCard) {
326 | maxPrice = getSettingWithDefault(SETTING_MAX_MISC_PRICE);
327 | minPrice = getSettingWithDefault(SETTING_MIN_MISC_PRICE);
328 | } else {
329 | maxPrice = isFoilTradingCard
330 | ? getSettingWithDefault(SETTING_MAX_FOIL_PRICE)
331 | : getSettingWithDefault(SETTING_MAX_NORMAL_PRICE);
332 | minPrice = isFoilTradingCard
333 | ? getSettingWithDefault(SETTING_MIN_FOIL_PRICE)
334 | : getSettingWithDefault(SETTING_MIN_NORMAL_PRICE);
335 | }
336 |
337 | maxPrice = maxPrice * 100.0;
338 | minPrice = minPrice * 100.0;
339 |
340 | const maxPriceBeforeFees = market.getPriceBeforeFees(maxPrice);
341 | const minPriceBeforeFees = market.getPriceBeforeFees(minPrice);
342 |
343 | return {
344 | maxPrice,
345 | minPrice,
346 | maxPriceBeforeFees,
347 | minPriceBeforeFees
348 | };
349 | }
350 |
351 | // Calculates the average history price, before the fee.
352 | function calculateAverageHistoryPriceBeforeFees(history) {
353 | let highest = 0;
354 | let total = 0;
355 |
356 | if (history != null) {
357 | // Highest average price in the last xx hours.
358 | const timeAgo = Date.now() - getSettingWithDefault(SETTING_PRICE_HISTORY_HOURS) * 60 * 60 * 1000;
359 |
360 | history.forEach((historyItem) => {
361 | const d = new Date(historyItem[0]);
362 | if (d.getTime() > timeAgo) {
363 | highest += historyItem[1] * historyItem[2];
364 | total += historyItem[2];
365 | }
366 | });
367 | }
368 |
369 | if (total == 0) {
370 | return 0;
371 | }
372 |
373 | highest = Math.floor(highest / total);
374 | return market.getPriceBeforeFees(highest);
375 | }
376 |
377 | // Calculates the listing price, before the fee.
378 | function calculateListingPriceBeforeFees(histogram) {
379 | if (typeof histogram === 'undefined' ||
380 | histogram == null ||
381 | histogram.lowest_sell_order == null ||
382 | histogram.sell_order_graph == null) {
383 | return 0;
384 | }
385 |
386 | let listingPrice = market.getPriceBeforeFees(histogram.lowest_sell_order);
387 |
388 | const shouldIgnoreLowestListingOnLowQuantity = getSettingWithDefault(SETTING_PRICE_IGNORE_LOWEST_Q) == 1;
389 |
390 | if (shouldIgnoreLowestListingOnLowQuantity && histogram.sell_order_graph.length >= 2) {
391 | const listingPrice2ndLowest = market.getPriceBeforeFees(histogram.sell_order_graph[1][0] * 100);
392 |
393 | if (listingPrice2ndLowest > listingPrice) {
394 | const numberOfListingsLowest = histogram.sell_order_graph[0][1];
395 | const numberOfListings2ndLowest = histogram.sell_order_graph[1][1];
396 |
397 | const percentageLower = 100 * (numberOfListingsLowest / numberOfListings2ndLowest);
398 |
399 | // The percentage should change based on the quantity (for example, 1200 listings vs 5, or 1 vs 25).
400 | if (numberOfListings2ndLowest >= 1000 && percentageLower <= 5) {
401 | listingPrice = listingPrice2ndLowest;
402 | } else if (numberOfListings2ndLowest < 1000 && percentageLower <= 10) {
403 | listingPrice = listingPrice2ndLowest;
404 | } else if (numberOfListings2ndLowest < 100 && percentageLower <= 15) {
405 | listingPrice = listingPrice2ndLowest;
406 | } else if (numberOfListings2ndLowest < 50 && percentageLower <= 20) {
407 | listingPrice = listingPrice2ndLowest;
408 | } else if (numberOfListings2ndLowest < 25 && percentageLower <= 25) {
409 | listingPrice = listingPrice2ndLowest;
410 | } else if (numberOfListings2ndLowest < 10 && percentageLower <= 30) {
411 | listingPrice = listingPrice2ndLowest;
412 | }
413 | }
414 | }
415 |
416 | return listingPrice;
417 | }
418 |
419 | function calculateBuyOrderPriceBeforeFees(histogram) {
420 | if (typeof histogram === 'undefined') {
421 | return 0;
422 | }
423 |
424 | return market.getPriceBeforeFees(histogram.highest_buy_order);
425 | }
426 |
427 | // Calculate the sell price based on the history and listings.
428 | // applyOffset specifies whether the price offset should be applied when the listings are used to determine the price.
429 | function calculateSellPriceBeforeFees(history, histogram, applyOffset, minPriceBeforeFees, maxPriceBeforeFees) {
430 | const historyPrice = calculateAverageHistoryPriceBeforeFees(history);
431 | const listingPrice = calculateListingPriceBeforeFees(histogram);
432 | const buyPrice = calculateBuyOrderPriceBeforeFees(histogram);
433 |
434 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
435 | const shouldUseBuyOrder = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 3;
436 |
437 | // If the highest average price is lower than the first listing, return the offset + that listing.
438 | // Otherwise, use the highest average price instead.
439 | let calculatedPrice = 0;
440 | if (shouldUseBuyOrder && buyPrice !== -2) {
441 | calculatedPrice = buyPrice;
442 | } else if (historyPrice < listingPrice || !shouldUseAverage) {
443 | calculatedPrice = listingPrice;
444 | } else {
445 | calculatedPrice = historyPrice;
446 | }
447 |
448 | let changedToMax = false;
449 | // List for the maximum price if there are no listings yet.
450 | if (calculatedPrice == 0) {
451 | calculatedPrice = maxPriceBeforeFees;
452 | changedToMax = true;
453 | }
454 |
455 |
456 | // Apply the offset to the calculated price, but only if the price wasn't changed to the max (as otherwise it's impossible to list for this price).
457 | if (!changedToMax && applyOffset) {
458 | calculatedPrice = calculatedPrice + getSettingWithDefault(SETTING_PRICE_OFFSET) * 100;
459 | }
460 |
461 |
462 | // Keep our minimum and maximum in mind.
463 | calculatedPrice = clamp(calculatedPrice, minPriceBeforeFees, maxPriceBeforeFees);
464 |
465 |
466 | // In case there's a buy order higher than the calculated price.
467 | if (typeof histogram !== 'undefined' && histogram != null && histogram.highest_buy_order != null) {
468 | const buyOrderPrice = market.getPriceBeforeFees(histogram.highest_buy_order);
469 | if (buyOrderPrice > calculatedPrice) {
470 | calculatedPrice = buyOrderPrice;
471 | }
472 | }
473 |
474 | return calculatedPrice;
475 | }
476 | //#endregion
477 |
478 | //#region Integer helpers
479 | function getRandomInt(min, max) {
480 | return Math.floor(Math.random() * (max - min + 1)) + min;
481 | }
482 |
483 | function getNumberOfDigits(x) {
484 | return (Math.log10((x ^ x >> 31) - (x >> 31)) | 0) + 1;
485 | }
486 |
487 | function padLeftZero(str, max) {
488 | str = str.toString();
489 | return str.length < max ? padLeftZero(`0${str}`, max) : str;
490 | }
491 |
492 | function replaceNonNumbers(str) {
493 | return str.replace(/\D/g, '');
494 | }
495 | //#endregion
496 |
497 | //#region Steam Market
498 |
499 | // Sell an item with a price in cents.
500 | // Price is before fees.
501 | SteamMarket.prototype.sellItem = function (item, price, callback /*err, data*/) {
502 | const url = `${window.location.origin}/market/sellitem/`;
503 |
504 | const options = {
505 | method: 'POST',
506 | data: {
507 | sessionid: readCookie('sessionid'),
508 | appid: item.appid,
509 | contextid: item.contextid,
510 | assetid: item.assetid || item.id,
511 | amount: item.amount || 1,
512 | price: price
513 | },
514 | responseType: 'json'
515 | };
516 |
517 | request(url, options, callback);
518 | };
519 |
520 | // Removes an item.
521 | // Item is the unique item id.
522 | SteamMarket.prototype.removeListing = function (item, isBuyOrder, callback /*err, data*/) {
523 | const url = isBuyOrder
524 | ? `${window.location.origin}/market/cancelbuyorder/`
525 | : `${window.location.origin}/market/removelisting/${item}`;
526 |
527 | const options = {
528 | method: 'POST',
529 | data: {
530 | sessionid: readCookie('sessionid'),
531 | ...(isBuyOrder ? { buy_orderid: item } : {})
532 | },
533 | responseType: 'json'
534 | };
535 |
536 | request(
537 | url,
538 | options,
539 | (error, data) => {
540 | if (error) {
541 | callback(ERROR_FAILED);
542 | return;
543 | }
544 |
545 | callback(ERROR_SUCCESS, data);
546 | }
547 | );
548 | };
549 |
550 | // Get the price history for an item.
551 | //
552 | // PriceHistory is an array of prices in the form [data, price, number sold].
553 | // Example: [["Fri, 19 Jul 2013 01:00:00 +0000",7.30050206184,362]]
554 | // Prices are ordered by oldest to most recent.
555 | // Price is inclusive of fees.
556 | SteamMarket.prototype.getPriceHistory = function (item, cache, callback) {
557 | const shouldUseAverage = getSettingWithDefault(SETTING_PRICE_ALGORITHM) == 1;
558 |
559 | if (!shouldUseAverage) {
560 | // The price history is only used by the "average price" calculation
561 | return callback(ERROR_SUCCESS, null, true);
562 | }
563 |
564 | try {
565 | const market_name = getMarketHashName(item);
566 | if (market_name == null) {
567 | callback(ERROR_FAILED);
568 | return;
569 | }
570 |
571 | const appid = item.appid;
572 |
573 | if (cache) {
574 | const storage_hash = `pricehistory_${appid}+${market_name}`;
575 |
576 | storageSession.getItem(storage_hash).
577 | then((value) => {
578 | if (value != null) {
579 | callback(ERROR_SUCCESS, value, true);
580 | } else {
581 | market.getCurrentPriceHistory(appid, market_name, callback);
582 | }
583 | }).
584 | catch(() => {
585 | market.getCurrentPriceHistory(appid, market_name, callback);
586 | });
587 | } else {
588 | market.getCurrentPriceHistory(appid, market_name, callback);
589 | }
590 | } catch {
591 | return callback(ERROR_FAILED);
592 | }
593 | };
594 |
595 | SteamMarket.prototype.getGooValue = function (item, callback) {
596 | try {
597 | let appid = item.market_fee_app;
598 |
599 | for (const action of item.owner_actions) {
600 | if (!action.link || !action.link.startsWith('javascript:GetGooValue')) {
601 | continue;
602 | }
603 |
604 | const rgMatches = action.link.match(/GetGooValue\( *'%contextid%', *'%assetid%', *'?(?[0-9]+)'?/);
605 |
606 | if (!rgMatches) {
607 | continue;
608 | }
609 |
610 | appid = rgMatches.groups.appid;
611 | break;
612 | }
613 |
614 | const url = `${this.inventoryUrlBase}ajaxgetgoovalue/`;
615 |
616 | const options = {
617 | method: 'GET',
618 | data: {
619 | sessionid: readCookie('sessionid'),
620 | appid: appid,
621 | assetid: item.assetid,
622 | contextid: item.contextid
623 | },
624 | responseType: 'json'
625 | };
626 |
627 | request(
628 | url,
629 | options,
630 | (error, data) => {
631 | if (error) {
632 | callback(ERROR_FAILED, data);
633 | return;
634 | }
635 |
636 | callback(ERROR_SUCCESS, data);
637 | }
638 | );
639 | } catch {
640 | return callback(ERROR_FAILED);
641 | }
642 | //http://steamcommunity.com/auction/ajaxgetgoovalueforitemtype/?appid=582980&item_type=18&border_color=0
643 | // OR
644 | //http://steamcommunity.com/my/ajaxgetgoovalue/?sessionid=xyz&appid=535690&assetid=4830605461&contextid=6
645 | //sessionid=xyz
646 | //appid = 535690
647 | //assetid = 4830605461
648 | //contextid = 6
649 | };
650 |
651 |
652 | // Grinds the item into gems.
653 | SteamMarket.prototype.grindIntoGoo = function (item, callback) {
654 | try {
655 | const url = `${this.inventoryUrlBase}ajaxgrindintogoo/`;
656 |
657 | const options = {
658 | method: 'POST',
659 | data: {
660 | sessionid: readCookie('sessionid'),
661 | appid: item.market_fee_app,
662 | assetid: item.assetid,
663 | contextid: item.contextid,
664 | goo_value_expected: item.goo_value_expected
665 | },
666 | responseType: 'json'
667 | };
668 |
669 | request(
670 | url,
671 | options,
672 | (error, data) => {
673 | if (error) {
674 | callback(ERROR_FAILED, data);
675 | return;
676 | }
677 |
678 | callback(ERROR_SUCCESS, data);
679 | }
680 | );
681 | } catch {
682 | return callback(ERROR_FAILED);
683 | }
684 |
685 | //sessionid = xyz
686 | //appid = 535690
687 | //assetid = 4830605461
688 | //contextid = 6
689 | //goo_value_expected = 10
690 | //http://steamcommunity.com/my/ajaxgrindintogoo/
691 | };
692 |
693 |
694 | // Unpacks the booster pack.
695 | SteamMarket.prototype.unpackBoosterPack = function (item, callback) {
696 | try {
697 | const url = `${this.inventoryUrlBase}ajaxunpackbooster/`;
698 |
699 | const options = {
700 | method: 'POST',
701 | data: {
702 | sessionid: readCookie('sessionid'),
703 | appid: item.market_fee_app,
704 | communityitemid: item.assetid
705 | },
706 | responseType: 'json'
707 | };
708 |
709 | request(
710 | url,
711 | options,
712 | (error, data) => {
713 | if (error) {
714 | callback(ERROR_FAILED, data);
715 | return;
716 | }
717 |
718 | callback(ERROR_SUCCESS, data);
719 | }
720 | );
721 | } catch {
722 | return callback(ERROR_FAILED);
723 | }
724 |
725 | //sessionid = xyz
726 | //appid = 535690
727 | //communityitemid = 4830605461
728 | //http://steamcommunity.com/my/ajaxunpackbooster/
729 | };
730 |
731 | // Get the current price history for an item.
732 | SteamMarket.prototype.getCurrentPriceHistory = function (appid, market_name, callback) {
733 | const url = `${window.location.origin}/market/pricehistory/`;
734 |
735 | const options = {
736 | method: 'GET',
737 | data: {
738 | appid: appid,
739 | market_hash_name: market_name
740 | },
741 | responseType: 'json'
742 | };
743 |
744 | request(
745 | url,
746 | options,
747 | (error, data) => {
748 | if (error) {
749 | callback(ERROR_FAILED);
750 | return;
751 | }
752 |
753 | if (data && (!data.success || !data.prices)) {
754 | callback(ERROR_DATA);
755 | return;
756 | }
757 |
758 | // Multiply prices so they're in pennies.
759 | for (let i = 0; i < data.prices.length; i++) {
760 | data.prices[i][1] *= 100;
761 | data.prices[i][2] = parseInt(data.prices[i][2]);
762 | }
763 |
764 | // Store the price history in the session storage.
765 | const storage_hash = `pricehistory_${appid}+${market_name}`;
766 | storageSession.setItem(storage_hash, data.prices);
767 |
768 | callback(ERROR_SUCCESS, data.prices, false);
769 | }
770 | );
771 | };
772 |
773 | // Get the item name id from a market item.
774 | //
775 | // This id never changes so we can store this in the persistent storage.
776 | SteamMarket.prototype.getMarketItemNameId = function (item, callback) {
777 | try {
778 | const market_name = getMarketHashName(item);
779 | if (market_name == null) {
780 | callback(ERROR_FAILED);
781 | return;
782 | }
783 |
784 | const appid = item.appid;
785 | const storage_hash = `itemnameid_${appid}+${market_name}`;
786 |
787 | storagePersistent.getItem(storage_hash).
788 | then((value) => {
789 | if (value != null) {
790 | callback(ERROR_SUCCESS, value);
791 | } else {
792 | return market.getCurrentMarketItemNameId(appid, market_name, callback);
793 | }
794 | }).
795 | catch(() => {
796 | return market.getCurrentMarketItemNameId(appid, market_name, callback);
797 | });
798 | } catch {
799 | return callback(ERROR_FAILED);
800 | }
801 | };
802 |
803 | // Get the item name id from a market item.
804 | SteamMarket.prototype.getCurrentMarketItemNameId = function (appid, market_name, callback) {
805 | const url = `${window.location.origin}/market/listings/${appid}/${escapeURI(market_name)}`;
806 |
807 | const options = { method: 'GET' };
808 |
809 | request(
810 | url,
811 | options,
812 | (error, data) => {
813 | if (error) {
814 | callback(ERROR_FAILED);
815 | return;
816 | }
817 |
818 | const matches = (/Market_LoadOrderSpread\( (\d+) \);/).exec(data || '');
819 | if (matches == null) {
820 | callback(ERROR_DATA);
821 | return;
822 | }
823 |
824 | const item_nameid = matches[1];
825 |
826 | // Store the item name id in the persistent storage.
827 | const storage_hash = `itemnameid_${appid}+${market_name}`;
828 | storagePersistent.setItem(storage_hash, item_nameid);
829 |
830 | callback(ERROR_SUCCESS, item_nameid);
831 | }
832 | );
833 | };
834 |
835 | // Get the sales listings for this item in the market, with more information.
836 | //
837 | //{
838 | //"success" : 1,
839 | //"sell_order_table" : "Price<\/th> | Quantity<\/th><\/tr> |
---|
0,04\u20ac<\/td> | 311<\/td><\/tr> |
0,05\u20ac<\/td> | 895<\/td><\/tr> |
0,06\u20ac<\/td> | 495<\/td><\/tr> |
0,07\u20ac<\/td> | 174<\/td><\/tr> |
0,08\u20ac<\/td> | 49<\/td><\/tr> |
0,09\u20ac or more<\/td> | 41<\/td><\/tr><\/table>",
840 | //"sell_order_summary" : " |