How I got a Nintendo Switch using NodeJS
Tags: javascript, typescript, node, automation
Published: 2020-12-23 19:10:35

---
title: How I got a Nintendo Switch using NodeJS
published: true
description: Using NodeJS to send an alert to your phone when hot holiday items are available.
tags: javascript,typescript,nodejs,automation
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/amlkowcxuft3ylujo592.png
---

Have you missed out on a hot holiday gift? Me too... until I used NodeJS to send my phone an alert to buy!

---

This holiday season I saw a deal for a Nintendo Switch bundle which was perfect for my brother and his family. It was on sale for $299 but, unfortunately, by the time I went to get it there were no more available and third-party scalpers were selling it for over $500. Bummer.

However, I noticed that Amazon’s stock would occasionally come available with that $299 price tag every now and then; but I was never lucky enough to check at the right time.

So what does a software engineer do? Write a script to monitor the availability and send a message to my phone when it’s available to buy. And, it actually worked!

---

{% github rgthree/pricewatch no-readme %}

---

## How it works.

The script is three pieces that work together:

1. An `AmazonPriceChecker` that fetches the Amazon product web page by its id and, using JSDOM, looks for the current price
1. A `TelegramBot` that simply encapsulates making a simple request to alert my phone via my [personal Telegram bot](https://dev.to/rgthree/personal-telegram-bot-for-alerting-your-phone-w-o-code-1oih).
1. And a main server file that ties it all together to run the check loop, check the price threshold, and utilize the bot to alert my phone.

## Set up

For this project, we'll need very few dependencies.

- If you haven't already, you'll need to [install NodeJS](https://nodejs.dev/learn/how-to-install-nodejs)
- You'll also need TypeScript. I recommend installing globally:

    ```shell

     npm install -g typescript

    ```
- You'll then need `jsdom` and `node-fetch`. You can create a `package.json` file similar to below in your project directory and run `npm install`:

    ```json
    {
      "name": "rgthree-pricewatch",
      "description": "Watch prices.",
      "version": "0.0.1",
      "private": true,
      "dependencies": {
        "jsdom": "^16.4.0",
        "node-fetch": "^2.6.1"
      },
      "devDependencies": {
        "@types/jsdom": "11.0.4",
        "@types/node": "^12.12.2",
        "@types/node-fetch": "^2.5.7"
      }
    }
    ```
    <figcaption>package.json</figcaption>

## The AmazonPriceChecker

All we need this to do is fetch the Amazon product's web page by its product id and, using JSDOM, look for the current price in the DOM, and return it if found along with the url of the product itself.

One thing to keep in mind is we’re fetching the web page with our server. We’ll override the User-Agent so it looks like a browser, but the response back will be raw HTML and possibly different than the markup we see when using Amazon as JavaScript is likely modifying the page substantially after that raw HTML come back.

So, to find how to scrape the price we'll use the view-source feature in a browser to see exactly what our script will see instead of the DevTools.

Luckily, it wasn't too hard to find Amazon is wrapping the price in an element with the id of `priceblock_ourprice`. _(At least for the Nintendo; it's possible other products have different markup.)_

All together, our `AmazonPriceChecker` looks like this:

```TypeScript
import fetch from 'node-fetch';
import {JSDOM} from 'jsdom';

/**
 * Given a product id, will try to find the current price of the item on the
 * Amazon page.
 */
export class AmazonPriceChecker {

  private readonly url: string;

  constructor(id: string) {
    this.url = `https://www.amazon.com/gp/product/${id}`;
  }

  /**
   * Returns a payload of the url for the product and it's current price, if
   * found. If the price cannot be determined, it will be `NaN`.
   */
  async check() {
    try {
      const response = await this.fetchProductPage();
      const body = await response.text();
      const doc = new JSDOM(body).window.document;
      let price = Number(doc.querySelector('#priceblock_ourprice')?.textContent?.trim().replace('$',''));
      return {url: this.url, price};
    } catch(e) {
      throw new Error(e);
    }
  }

  private async fetchProductPage() {
    return await fetch(this.url, {
      method: 'get',
      headers: {
        'accept-language': 'en-US',
        'Accept': 'text/html,application/xhtml+xml',
        // Make our request look like a browser.
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.11 Safari/537.36',
      }
    });
  }
}
```
<figcaption>amazon_watcher.ts</figcaption>


## Our TelegramBot

For the next part of our script we want to alert our phone. I've previously written about how to create a personal Telegram Bot here:

{% post rgthree/personal-telegram-bot-for-alerting-your-phone-w-o-code-1oih %}

All we need is our bot’s Api Key and the chat id our bot belongs to that we'll ping.

```TypeScript
import fetch from 'node-fetch';

/**
 * Super simple telegram wrapper that sends messages to a bot specific chat.
 */
export class TelegramBot {

  constructor(private botApiKey: string, private chatId: string) {}

  async sendMessage(text: string) {
    return await fetch(`https://api.telegram.org/bot${this.botApiKey}/sendMessage?chat_id=${this.chatId}&text=${encodeURIComponent(text)}`);
  }
}
```
<figcaption>telegrambot.ts</figcaption>

> NOTE: You don't _have_ to use Telegram to alert yourself. You can modify the code here to send an email, or trigger IFTTT in some way, etc. Options are up to you, I've chosen to use Telegram because I already had a personal bot setup to ping my phone :)



## Tying it all together

Now that we have our two separate pieces, we'll tie them together in our main server file where we'll loop to check every two minutes.

```TypeScript
import {AmazonPriceChecker} from './amazon_watcher';
import {TelegramBot} from './telegrambot';

const TELEGRAM_API_KEY = 'YOUR_API_KEY';
const TELEGRAM_CHAT_ID = 'YOUR_API_KEYCHAT_ID';

// The Amazon product id. The XXX in
// https://www.amazon.com/dp/XXX or https://www.amazon.com/gp/product/XXX
const AMAZON_PRODUCT_ID = 'B08KB652Q2';
const TARGET_PRICE = 300;

const MS_MINUTES = 1000 * 60;
const BASE_TIMEOUT = MS_MINUTES * 2;

const telegram = new TelegramBot(TELEGRAM_API_KEY, TELEGRAM_CHAT_ID);
const priceChecker = new AmazonPriceChecker(AMAZON_PRODUCT_ID);

/**
 * Checks the price with `priceChecker`, issues a message with `telegram` if
 * it meets our threshold, and schedules another check.
 */
async function check() {
  let timeout = BASE_TIMEOUT;
  try {
    const {url, price} = await priceChecker.check();

    if (price) {
      if (price <= TARGET_PRICE) {
        telegram.sendMessage(`Price is: ${price}. Checking again in ${timeout / MS_MINUTES} minutes. ${url}`);
      }
    } else {
      // If we can't parse the price, maybe something's wrong. We'll slow down
      // our interval a bit.
      timeout += MS_MINUTES * 5;
      telegram.sendMessage(`Could not parse price. Trying again in ${timeout / MS_MINUTES}. ${url}`);
    }

  } catch(e) {
    timeout += MS_MINUTES * 5;
    telegram.sendMessage(`There was an error fetching the price. Will check again in ${timeout / MS_MINUTES} minutes.`);
    console.error(e);
  }

  // Rinse & repeat.
  setTimeout(() => { check(); }, timeout);
}

// Start it!
check();

console.log('Checker Started. Stop with Ctrl + C.');
```
<figcaption>server.ts</figcaption>

## Start it up

First, run the TypeScript compiler which will generate JavaScript files from our neatly typed TypeScript files:

```shell
tsc
```

And then run our server file with NodeJs:

```shell
node server.js
```

And that's it! This triggered an alert on my phone in about 4 hours or so and I was able to open Amazon directly to the product page and get the Switch, which was good because when I checked again four minutes later it shot back up to $500!

## Enhance!

This is just a base that worked for me. We can take this add more checkers for other online stores, different products, turn it into a full "bot" that actually buys the product, etc. Consider this just a starting point of where we can go from here. Maybe for next holiday season.

{% github rgthree/pricewatch no-readme %}