Back

A simple bot that checks Playstation 5 stock 24/7

Jan 1st, 2021

It's quite challenging to get a PS5 these days. Be it COVID-19, huge demand, or something else, the console is out of stock pretty much everywhere. I did not see that coming, and honestly, was not thinking about buying one until early December. The preorder train has long gone, so my only option was to refresh a dozen of websites every now and then. That is a weak strategy against scalpers. But I used it until I listened to another great episode of Syntax podcast. That's when the idea that was floating at the back of my mind matured - "I'm a developer, I should use the skills to automate that and stop wasting time refreshing those pages!". And it turned out to be a fairly easy thing to do.

Another inspiration was Stockinformer, where I liked the alarm feature. I wanted to build something similar of my own using free time over the holidays. An alert system that only notifies when there is a drop. And the buying part I'd then do manually. I didn't want to spend too much time on a code that would probably be forgotten once it's successfully served its purpose. I'm located in Germany, so I focused on EU stores that ship to Germany. If you're here for the code, you can jump straight to it.

Tools

The first version was implemented with Puppeteer, but then I decided to switch to Playwright purely because I wanted to play around with it. Cypress was out mainly because I use it a lot at work already, and playing is more fun when you learn new things along the way! I'm a big fan of TypeScript, but if you're not familiar with it, just ignore the types, at the end of the day, it's the same old JavaScript.

How

Let's get started by spinning up a server:

import { Request, Response } from "express";
const express = require("express");
const app = express();

app.get("/", (req: Request, res: Response) => {
  res.send("Hello World");
  // TODO: Add a corn job here
});

app.listen(3030);

We'll define the list of all the links we want to check like so:

export type Link = {
  name: string;
  url: string;
  dataDefaultAsin?: string; // Amazon-specific id
  type: LinkType;
};

export enum LinkType {
  AMAZON = "AMAZON",
  MEDIAMARKT = "MEDIAMARKT",
  GAMESTOP = "GAMESTOP",
  EURONICS = "EURONICS",
  CYBERPORT = "CYBERPORT",
}

export const links: Link[] = [
  {
    name: "Amazon DE",
    url: "https://www.amazon.de/-/dp/B08H93ZRK9",
    dataDefaultAsin: "B08H93ZRK9",
    type: LinkType.AMAZON,
  },
  {
    name: "Media Markt",
    url: "https://www.mediamarkt.de/de/search.html?query=playstation%205",
    type: LinkType.MEDIAMARKT,
  },
];

The next thing we need is a function that will lunch a headless browser and check every link we just defined:

export const checkPages = async () => {
  const browser = await chromium.launch({ headless: true });
  const browserContext = await browser.newContext();

  for (const link of links) {
    const page = await browserContext.newPage();
    await page.goto(link.url);

    // TODO: Check for link type to decide what logic to use
    await page.close();
  }

  await browserContext.close();
  await browser.close();
};

Inside there we have a for loop where we'll check every link's type to tell Playwright what to look for. To figure that out we'd have to inspect the page and see what we can rely on. In the case of Amazon, that would be something like:

if (link.type === LinkType.AMAZON) {
  if (link.dataDefaultAsin) {
    const variantButton = await page.$(
      `li[data-defaultasin=${link.dataDefaultAsin}] button`
    );
    if (variantButton) {
      // There might be some cookie banners or modals, we ignore them
      await variantButton.click({ force: true });
    }
  }
  const addToCartButton = await page.$(
    "#desktop_buybox_feature_div #addToCart input#add-to-cart-button"
  );
  await handleStockAvailability(link, !!addToCartButton, page);
}

Now it's time to specify how we want to be notified when the shiny new consoles are back in stock. I thought the simple SMS is not enough. It doesn't create enough urgency. I decided that the alarm sound should be dispatched the moment new stock was detected. For that reason, the code is meant to be run locally, on your machine. Also, let's take a snap of the page, just in case:

const handleStockAvailability = async (
  link: Link,
  stockFound: boolean,
  page: Page
) => {
  if (!stockFound) {
    console.log(`Still no stock for ${link.name}`);
    return;
  }
  console.log(`🚨 ${" "}There might be a ${link.name} in stock at ${link.url}`);
  await page.screenshot({
    path: `screenshots/screenshot-${formatISO(new Date())}.png`,
  });
  await sendMessage(link);
  await playSiren();
};

The message is sent via Twilio. You can use a trial mode, that's enough for the purpose. Finally, I picked a nice siren sound from FreeSound to make sure I'll wake up even from the deepest sleep.

Now all that's left is to set up a cron job to run every 5 minutes:

import { Request, Response } from "express";
const express = require("express");
const app = express();

let count = 1;

const task = cron.schedule("*/5 * * * *", async () => {
  console.log(`🚀 ${" "} Running a #${count} cycle`);
  await checkPages();
  count += 1;
  console.log(`💤 ${" "}Sleeping at ${format(new Date(), "PPpp")}`);
});

app.get("/", (req: Request, res: Response) => {
  res.send("Hello World");
  task.start();
});

app.listen(3030);

That's it! Grab the final code and good luck with your hunt! Let me know if that helped you to get one.

Happy New Year! 🎄