├── NeweggBot.js ├── README.md ├── config_template.json └── package.json /NeweggBot.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer-extra') 2 | const stealthPlugin = require('puppeteer-extra-plugin-stealth') 3 | const readline = require("readline") 4 | const log4js = require("log4js") 5 | const config = require('./config.json') 6 | 7 | log4js.configure({ 8 | appenders: { 9 | out: { type: 'stdout' }, 10 | app: { type: 'file', filename: 'application.log' } 11 | }, 12 | categories: { 13 | default: { appenders: [ 'out', 'app' ], level: 'trace' } 14 | } 15 | }) 16 | 17 | const logger = log4js.getLogger("Newegg Shopping Bot") 18 | logger.level = "trace" 19 | 20 | var TFA_wait = config.TFA_base_wait 21 | 22 | /** 23 | * Sign into wegg 24 | * @param {*} page The page containing the element 25 | */ 26 | async function signin(page, rl) { 27 | //probably want to change code to looking at the specific html elements for determining which step/field page is asking for 28 | 29 | //look for email field and input email 30 | try { 31 | await page.waitForSelector('#labeled-input-signEmail', { timeout: 2500 }) 32 | await page.waitForSelector('button.btn.btn-orange', { timeout: 2500 }) 33 | await page.type('#labeled-input-signEmail', config.email) 34 | await page.click('button.btn.btn-orange') 35 | } catch (signEmailInputErr) { 36 | logger.error("No email selector found") 37 | } 38 | 39 | await page.waitForTimeout(1500) 40 | 41 | //look for password field and input password 42 | try { 43 | await page.waitForSelector('#labeled-input-password', { timeout: 2500 }) 44 | await page.waitForSelector('button.btn.btn-orange') 45 | await page.type('#labeled-input-password', config.password) 46 | await page.click('button.btn.btn-orange') 47 | 48 | await page.waitForTimeout(1500) 49 | 50 | try { 51 | await page.waitForSelector('#labeled-input-password', { timeout: 500 }) 52 | } catch (passwordSelectorErr) { 53 | logger.trace("Logged in") 54 | TFA_wait = config.TFA_base_wait 55 | return true 56 | } 57 | } catch (passwordInputErr) { 58 | //Waiting 30-60s and reloading allows a bypass of 2FA 59 | if (config.skip_TFA && !config.do_first_TFA) { 60 | logger.warn(`email 2FA is being asked, will reload in ${TFA_wait}s to skip it`) 61 | await page.waitForTimeout(TFA_wait * 1000) 62 | TFA_wait = Math.min(TFA_wait + config.TFA_wait_add, config.TFA_wait_cap) 63 | 64 | if (!page.url().includes('signin')) { 65 | logger.info("2FA inputted while waiting") 66 | logger.trace("Logged in") 67 | TFA_wait = config.TFA_base_wait 68 | } 69 | 70 | return false 71 | } 72 | 73 | logger.warn("Manual authorization code required by Newegg. This should only happen once.") 74 | 75 | var tempFACode = true 76 | 77 | while (page.url().includes('signin')) { 78 | if (tempFACode == true) { 79 | tempFACode = false 80 | 81 | rl.question('What is the 6 digit 2FA code? ', async function(FACode) { 82 | logger.info(`Inputting code ${FACode} into 2FA field`) 83 | 84 | await page.waitForSelector('input[aria-label="verify code 1"]') 85 | await page.waitForSelector('input[aria-label="verify code 2"]') 86 | await page.waitForSelector('input[aria-label="verify code 3"]') 87 | await page.waitForSelector('input[aria-label="verify code 4"]') 88 | await page.waitForSelector('input[aria-label="verify code 5"]') 89 | await page.waitForSelector('input[aria-label="verify code 6"]') 90 | await page.waitForSelector('#signInSubmit', { timeout: 2500 }) 91 | 92 | await page.type('input[aria-label="verify code 1"]', FACode) 93 | await page.waitForTimeout(500) 94 | await page.click('#signInSubmit') 95 | await page.waitForTimeout(2500) 96 | 97 | tempFACode = true 98 | }) 99 | } 100 | 101 | await page.waitForTimeout(500) 102 | } 103 | 104 | logger.trace("Logged in") 105 | config.do_first_TFA=false 106 | TFA_wait = config.TFA_base_wait 107 | 108 | return true 109 | } 110 | 111 | return false 112 | } 113 | 114 | /** 115 | * Check the wishlist and see if the "Add to Cart" button is disabled or not, then press it 116 | * @param {*} page The page containing the element 117 | */ 118 | async function check_wishlist(page) { 119 | const buttonElementName = 'button.btn.btn-primary.btn-large.list-subtotal-button' 120 | try { 121 | //find a non disabled subtotal button, if none is found then errors out 122 | await page.waitForSelector(buttonElementName, { timeout: 2000 }) 123 | if (await page.evaluate(element => element.disabled, await page.$(buttonElementName)) == true) throw 'No items found' 124 | } catch (err) { 125 | logger.error(err) 126 | var nextCheckInSeconds = config.refresh_time + Math.floor(Math.random() * Math.floor(config.randomized_wait_ceiling)) 127 | logger.info(`The next attempt will be performed in ${nextCheckInSeconds} seconds`) 128 | await page.waitForTimeout(nextCheckInSeconds * 1000) 129 | return false 130 | } 131 | 132 | await page.click(buttonElementName) 133 | logger.trace("Item(s) added to cart, checking cart") 134 | return true 135 | } 136 | 137 | /** 138 | * Check the cart and make sure the subtotal is within the max price 139 | * @param {*} page The page containing the element 140 | */ 141 | async function check_cart(page, removed = false) { 142 | const amountElementName = '.summary-content-total' 143 | try { 144 | await page.waitForSelector(amountElementName, { timeout: 2000 }) 145 | var text = await page.evaluate(element => element.textContent, await page.$(amountElementName)) 146 | var price = parseInt(text.split('$')[1]) 147 | logger.info(`Subtotal of cart is ${price}`) 148 | 149 | if (price === 0) { 150 | if (removed) 151 | logger.error("The last item removed exceeds the max price, cannot purchase item") 152 | else 153 | logger.error("There are no items in the cart, item possibly went out of stock when adding to cart") 154 | 155 | return false 156 | } else if (price > config.price_limit) { 157 | if (config.over_price_limit_behavior === "stop") { 158 | logger.error("Subtotal exceeds limit, stopping Newegg Shopping Bot process") 159 | 160 | while (true) { 161 | 162 | } 163 | } else if (config.over_price_limit_behavior === "remove") { 164 | logger.warn("Subtotal exceeds limit, removing an item from cart") 165 | 166 | await page.waitForSelector('button.btn.btn-mini.btn-tertiary', { timeout: 5000 }) 167 | var button = await page.$$('button.btn.btn-mini') 168 | await button[2].click() 169 | 170 | logger.trace("Successfully removed an item, checking cart") 171 | await page.waitForTimeout(500) 172 | 173 | return await check_cart(page, true) 174 | } else { 175 | logger.error("Price exceeds limit") 176 | } 177 | 178 | return false 179 | } 180 | 181 | logger.trace("Cart checked, attempting to purchase") 182 | return true 183 | } catch (err) { 184 | logger.error(err.message) 185 | return false 186 | } 187 | } 188 | 189 | /** 190 | * Input the Credit Verification Value (CVV) 191 | * @param {*} page The page containing the element 192 | */ 193 | async function inputCVV(page) { 194 | while (true) { 195 | logger.info("Waiting for CVV input element") 196 | try { 197 | await page.waitForSelector("[placeholder='CVV2']", { timeout: 3000 }) 198 | await page.focus("[placeholder='CVV2']", { timeout: 5000 }) 199 | await page.type("[placeholder='CVV2']", config.cv2) 200 | logger.info("CVV data inputted") 201 | break 202 | } catch (err) { 203 | logger.warn("Cannot find CVV input element") 204 | } 205 | } 206 | 207 | await page.waitForTimeout(250) 208 | try { 209 | const [button] = await page.$x("//button[contains(., 'Review your order')]") 210 | if (button) { 211 | logger.info("Review Order") 212 | await button.click() 213 | } 214 | } catch (err) { 215 | logger.error("Cannot find the Review Order button") 216 | logger.error(err) 217 | } 218 | } 219 | 220 | /** 221 | * Submit the order 222 | * @param {*} page The page containing the order form 223 | */ 224 | async function submitOrder(page) { 225 | await page.waitForSelector('#btnCreditCard:not([disabled])', { timeout: 3000 }) 226 | await page.waitForTimeout(500) 227 | 228 | if (config.auto_submit) { 229 | await page.click('#btnCreditCard') 230 | logger.info("Completed purchase") 231 | } else { 232 | logger.warn("Order not submitted because 'auto_submit' is not enabled") 233 | } 234 | } 235 | 236 | async function run() { 237 | logger.info("Newegg Shopping Bot Started") 238 | logger.info("Please don't scalp, just get whatever you need for yourself") 239 | 240 | //#block-insecure-private-network-requests 241 | //#enable-web-authentication-cable-v2-support 242 | //#allow-sxg-certs-without-extension 243 | //#same-site-by-default-cookies 244 | //#cookies-without-same-site-must-be-secure 245 | //#safe-browsing-enhanced-protection-message-in-interstitials 246 | //#dns-httpssvc 247 | //#trust-tokens 248 | //#use-first-party-set 249 | //#enable-network-logging-to-file 250 | puppeteer.use(stealthPlugin()) 251 | const browser = await puppeteer.launch({ 252 | headless: config.headless, 253 | defaultViewport: { width: 1920, height: 1080 }, 254 | executablePath: config.browser_executable_path, 255 | userDataDir: "./myDataDir", 256 | args: [ 257 | '--unsafely-treat-insecure-origin-as-secure=http://example.com' 258 | ] 259 | }) 260 | const [page] = await browser.pages() 261 | await page.setCacheEnabled(true) 262 | 263 | const rl = readline.createInterface({ 264 | input: process.stdin, 265 | output: process.stdout 266 | }) 267 | 268 | // Main loop 269 | while (true) { 270 | try { 271 | await page.goto('https://secure.newegg.' + config.site_domain + '/wishlist/md/' + config.wishlist, { waitUntil: 'networkidle0' }) 272 | 273 | if (page.url().includes("/wishlist/md/")) { 274 | if (await check_wishlist(page) && await check_cart(page)) break 275 | } else if (page.url().includes("signin")) { 276 | //need to signin every so often 277 | await signin(page, rl) 278 | } else if (page.url().includes("areyouahuman")) { 279 | logger.error("Human captcha test, waiting 1s and reloading") 280 | await page.waitForTimeout(1000) 281 | } else { 282 | logger.error(`redirected to "${page.url()}" for some reason`) 283 | await page.waitForTimeout(1000) 284 | } 285 | } catch (err) { 286 | logger.error(err) 287 | continue 288 | } 289 | } 290 | 291 | rl.close() 292 | 293 | // Continuely attempts to press the Checkout/Continue checkout buttons, until getting to last checkout button 294 | // This way no time is wasted in saying "Wait 10s" after pressing a button, no easy way to wait for networkidle after an ajax request 295 | while (true) { 296 | try { 297 | let button 298 | 299 | if (page.url().includes("Cart")) { 300 | button = await page.waitForXPath("//button[contains(., 'Secure Checkout')]", { timeout: 1000 }) 301 | } else if (page.url().includes("checkout")) { 302 | button = await page.waitForXPath("//button[contains(., 'Continue to')]", { timeout: 1000 }) 303 | } else { 304 | await page.waitForTimeout(1000) 305 | continue 306 | } 307 | 308 | await page.waitForTimeout(500) 309 | 310 | if (button) { 311 | await button.click() 312 | } 313 | } catch (err) { 314 | try { 315 | if (config.multi_step_order) { 316 | await page.waitForXPath("//button[contains(., 'Review your order')]", { timeout: 500 }) 317 | break 318 | } else { 319 | await page.waitForSelector('#btnCreditCard:not([disabled])', { timeout: 500 }) 320 | break 321 | } 322 | } catch (err) { 323 | continue 324 | } 325 | } 326 | } 327 | 328 | //CVV and order submit stuff 329 | try { 330 | await inputCVV(page) 331 | await submitOrder(page) 332 | } catch (err) { 333 | logger.error("Cannot find the Place Order button") 334 | logger.warn("Please make sure that your Newegg account defaults for: shipping address, billing address, and payment method have been set.") 335 | } 336 | } 337 | 338 | run() 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeweggBot 2 | Autonomously buy products from Newegg as soon as they become available 3 | 4 | This bot is very much still in the early stages, and more than a little rough around the edges. Expect the occasional hiccups if you decide to use it. 5 | 6 | ## Installation 7 | You will require [Node.js 14](https://nodejs.org/en/) to run this. 8 | After installing via git or by downloading the code and extracting it, navigate to the folder where the files are located via powershell(or equivalent console) and run `npm install` command. If you end up experiencing the error `Error: Could not find browser revision latest` when running, you may also need to run the command `PUPPETEER-PRODUCT=firefox npm i puppeteer`. 9 | 10 | 11 | ## Configuration 12 | Once that is finished, create a copy of config_template.json and name it config.json. Inside you will find the very basic customization options. 13 | - `cv2` refers to the three digit code on the back of your credit card. 14 | - `skip_TFA` refers to whether email 2FA should be skipped by waiting a set amount of time. Failsafe in case of 2FA coming up unexpectedly 15 | - `do_first_TFA` refers to whether the first email 2FA should be skipped or not. I'd recommend inputting the first 2FA as it will be stored in the cookies/localStorage of the browser and not need to be asked again. 16 | - `TFA_base_wait` refers to the base time to wait when skipping an email 2FA. 17 | - `TFA_wait_add` refers to the amount of time to add to the 2FA wait time after a failed wait attempt. 18 | - `TFA_wait_cap` refers to the max wait time for skipping a 2FA. 19 | - `wishlist` refers to Newegg's wishlist number found at the end of the wishlist page URL. For example, the item number for 'https://secure.newegg.com/wishlist/md/12341234' is 12341234. This bot can attempt to buy multiple items at once if multiple items in the wishlist are in stock. Be cautious with as there are no checks in place to ensure that only one item of a certain type is purchased, so if by chance two cards you're attempting to purchase come in stock at the same time, the bot would attempt to purchase both. 20 | - `auto_submit` refers to whether or not you want the bot to complete the checkout process. Setting it to 'true' will result in the bot completing the purchase, while 'false' will result in it completing all the steps up to but not including finalizing the purchase. It is mostly intended as a means to test that the bot is working without actually having it buy something. Note: nothing in place for confirmation in a headless session. Meaning the bot will stay at the last step without a way for you to confirm an order. If this is on, highly recommended to do a non-headless session. 21 | - `price_limit` refers to the maximum cart subtotal price that the bot will allow. 22 | - `over_price_limit_behavior` refers to the desired behavior for cases in which the subtotal price exceeds the specified `price_limit`. *"stop"* will instruct the bot to stop the process when the cart is over the limit allowing you to remove which items you want and continue through the checkout manually (note: wont work in headless session, no console UI for showing all items in cart, removing items, or confirming if you wish to continue). *"remove"* will remove items starting from the last entry of the cart until the subtotal price is under or equal to the limit. 23 | - `multi_step_order` refers to whether you have the extra step "review your order" before being able to place the order. I believe this is location based but have no confirmations on it. So far from my limited understanding, US will be false and CAN will be true. 24 | - `refresh_time` refers to the duration to wait in seconds between add-to-cart attempts. This should be specified as a number, rather than a string. 25 | - `randomized_wait_ceiling` This value will set the ceiling on the random number of seconds to be added to the `refresh_time`. While not guaranteed, this should help to prevent - or at least delay - IP bans based on consistent traffic/timing. This should be specified as a number, rather than a string. 26 | - `site_domain` refers to which site domain you want the bot to use ('ca' or 'com', potentially others aswell). 27 | - `headless` refers to whether you want the bot to run a headless browser session. Meaning, whether the browser itself will be visible or not. However, with very minor testing this will incur the email 2FA multiple times due to NE seeing it as an untrsuted browser (probably from bot detection). 28 | - `browser_executable_path` This will set the path to the browser to be used by the bot. Depending on the browser selected, you *may* need to install additional packages. 29 | 30 | ## Usage 31 | After installation and configuration, the bot can then be run by using either `node neweggbot.js` or the `npm start` script. 32 | 33 | It is important if you've never used your Newegg account before that you setup your account with a valid address and payment information, and then run through the checkout process manually making any changes to shipping and payment as Newegg requests. You don't need to complete that purchase, just correct things so that when you click `Secure Checkout` from the cart, it brings you to `Review`, not `Shipping` or `Payment`. 34 | 35 | At the moment, in the event that a card comes in stock, but goes out of stock before the bot has been able to complete the purchase, it will likely break, and you will need to restart it. In general, there are very likely to be occasional issues that break the bot and require you to restart it. -------------------------------------------------------------------------------- /config_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "email":"email@email.com", 3 | "password":"supercoolpassword", 4 | "cv2":"123", 5 | "skip_2FA":true, 6 | "do_first_TFA":true, 7 | "TFA_base_wait":45, 8 | "TFA_wait_add":15, 9 | "TFA_wait_cap":75, 10 | "wishlist":"00000000", 11 | "auto_submit":true, 12 | "price_limit":"2000", 13 | "over_price_limit_behavior": "stop", 14 | "multi_step_order":true, 15 | "refresh_time": 10, 16 | "randomized_wait_ceiling": 10, 17 | "site_domain": "com", 18 | "headless":false, 19 | "browser_executable_path": "C:/Progra~2/Google/Chrome/Application/chrome.exe" 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neweggbot", 3 | "version": "1.0.1", 4 | "description": "Shopping Bot for NewEgg", 5 | "main": "NeweggBot.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node neweggbot.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Ataraksia/NeweggBot.git" 13 | }, 14 | "author": "Ataraksia", 15 | "contributors": [ 16 | "Cololophier", 17 | "northmatt" 18 | ], 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/Ataraksia/NeweggBot/issues" 22 | }, 23 | "homepage": "https://github.com/Ataraksia/NeweggBot#readme", 24 | "dependencies": { 25 | "log4js": "^6.3.0", 26 | "puppeteer": "^7.1.0", 27 | "puppeteer-extra": "^3.1.17", 28 | "puppeteer-extra-plugin-stealth": "^2.7.5" 29 | } 30 | } 31 | --------------------------------------------------------------------------------