Build a Price Tracker with Twilio Programmable SMS and Node.js

July 19, 2021
Written by
Reviewed by

Header Image

Introduction

Sitting at your device all day waiting for a price drop can be a cumbersome task. A price notifier will ensure that you never miss out on your most coveted item. Whether you’re looking for the hottest pair of shoes or a couch for your living room, it can all be automated to your benefit and make technology work for you.

In this article, you will learn how to build an SMS price notifier. To do this, you’ll use the Sneaks API to track sneaker prices and deliver SMS alerts whenever the price of a particular sneaker decreases.

This article is divided into two parts: The first will show how to build a basic SMS handler and the second will show how to build an SMS scheduler to send price alerts to subscribers.

Prerequisites

Here is what you will need to follow along with this article:

Setup your app

Create your project structure

You’ll first start off by building the scaffolding for your Node.js project in your preferred directory. Inside your terminal or command prompt, navigate to your preferred directory and enter:

mkdir price-notifier
cd price-notifier

The next step is to initiate a brand new Node.js project and to install the dependencies required for this project:

npm init -y
npm install twilio dotenv express sneaks-api

You will need the twilio package which allows you to use the Twilio Programmable SMS API to send and receive SMS messages. dotenv is used to access environment variables, which is where you will store the Twilio credentials needed to interact with the API. The express package will be used to build your server: this is where you will write the code to capture all incoming SMS messages. Lastly, the sneaks-api package is used to track sneaker prices.

Once your Node.js app is initialized with the dependencies, your next step is to create three new files inside your main directory: index.js, scheduler.js, and .env:

touch index.js scheduler.js .env

The index.js file is where you will  handle and interpret all incoming SMS messages. The scheduler.js file will run automatically on a schedule and will check and notify subscribers if their sneaker has dropped below a certain threshold. The .env file will securely store all of your environmental variables.

Gather your Twilio credentials

To connect to Twilio’s API, you will need your Account SID, Auth Token, and your Twilio phone number. These values can be securely stored in the .env file. Copy and paste the following into your .env file:

TWILIO_ACCOUNT_SID=XXXXXXX 
TWILIO_AUTH_TOKEN=XXXXXXX
TWILIO_NUMBER=XXXXXXX

Once copied in the .env file, replace the XXXXXX placeholders with your actual account credentials. When entering your Twilio phone number, ensure your country code is included. The next step is to start coding out your notifier app!

Handle incoming text messages

The index.js file you created earlier will handle all incoming messages to your Twilio number. You will start off by adding the necessary imports needed for your project. Open up the index.js file in your text editor and copy and paste the code below in your file:

require('dotenv').config();

const SneaksAPI = require('sneaks-api');
const sneaks = new SneaksAPI();

const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: false }));

const MessagingResponse = require('twilio').twiml.MessagingResponse;

The chunk of code above includes and initializes the dotenv, sneaks-api, and express packages. It also imports the MessagingResponse object from the twilio package.

Simulate subscribers

Because you’re building a price notifier that alerts subscribers to a price change of a given product, you’ll need some subscribers to send the alerts to. Normally, in a production app, you would use a database to store your subscribers but in this case you’ll create some fake subscribers and store them as objects in an array.  Copy and paste the following code below where you initialized all of the packages:

const Subscribers = [
  {
    "number": 2025550121,
    "styleID": "FY2903",
    "lastResellPrice": 475
  },
  {
    "number": 1234567890,
    "styleID": "DD0587-600",
    "lastResellPrice": 285
  }
];

The “number” key in each of the objects in the Subscribers array stores the phone number of the subscriber. The “styleID” key stores the style ID of the sneaker being tracked and the “lastResellPrice” key stores the last tracked price of the sneaker.

Your next step is to create a route in your app that will process all incoming SMS messages. Copy the following code and paste it below your existing code in index.js:

app.post('/sms', async (req, res) => {

});

app.listen(3000, () => {
   console.log('Express server listening on port 3000');
});

This code creates a route that allows POST requests to be sent to the /sms endpoint on port 3000 of where your app will be hosted. In this case it will be hosted on your local server and can be visited at http://localhost:3000.

Now the next step is to process incoming SMS messages! Inside your /sms route copy and paste the following:

const twiml = new MessagingResponse();
const SMS = twiml.message();
const recievedSMS = req.body.Body.toLowerCase().trim();
const firstWord = recievedSMS.split(" ")[0];

if (firstWord == 'track'){
   const styleID = recievedSMS.split(" ")[1] || null;

} else {
   SMS.body('To start tracking a sneaker, text \"track\" followed by the sneaker ID.');
}

res.writeHead(200, {'Content-Type': 'text/xml'});
res.end(twiml.toString());

This code will prepare a message and send it out to the user depending on what the user has sent to your Twilio number. If the user has sent a message that says “track”, the code will attempt to parse the part of the message following “track”, which should be the style ID of the sneaker requesting to be tracked.

Next you will need to utilize the sneaks-api to search for the sneaker using the given style ID. The Sneaks API outputs a lot of sneaker attributes when given a style ID so you’ll need to create a wrapper function that outputs only the information you need for the subscriber.

Place this wrapper function below the /sms route:

function sneaksApiFunctionWrapper(styleID) {
   return new Promise((resolve, reject) => {
       sneaks.getProductPrices(styleID, function(err, product){
           const lowestResellSite = Object.keys(product.lowestResellPrice).reduce((a, b) => product.lowestResellPrice[a] > product.lowestResellPrice[b] ? a : b);
           const sneaker = {
               name: product.shoeName,
               image: product.thumbnail,
               site: lowestResellSite,
               price: product.lowestResellPrice[lowestResellSite],
               url: product.resellLinks[lowestResellSite],
               styleID: product.styleID
           };
           resolve(sneaker);
       }, (errorResponse) => {
           reject(errorResponse);
       });
   });
}

Once your wrapper function is added, your app is now ready to call and receive sneaker data from the Sneaks API. Below where your code reads the style ID given by the user, copy and paste the highlighted code:


if(firstWord == 'track'){
   const styleID = recievedSMS.split(" ")[1] || null;
   if(styleID){
       const sneaker = await sneaksApiFunctionWrapper(styleID);
       if(!sneaker){
           SMS.body("Sneaker could not be found");
       } else {
           const sub = {        
               "number": req.body.From,
               "styleID": sneaker.styleID,
               "lastResellPrice": sneaker.price
           };
           Subscribers.push(sub);
           SMS.media(sneaker.image);
           SMS.body(`Current lowest price for ${sneaker.name} is $${String(sneaker.price)} at ${sneaker.site}: ${sneaker.url}\nYou will be notified when the price drops. Reply STOP to opt-out of alerts.`);
       }
   }
}

Your code will now search the style ID given by the user and will insert their phone number, style ID, and the current price of the sneaker in the subscribers collection if a sneaker has been found. It will also send a message back to the user confirming what sneaker they are subscribed to as well as a thumbnail of the sneaker.

Save and close this file, you wont need to edit it anymore!

Build your scheduler

Now that your app is able to interpret all incoming SMS messages, your app needs to send out alerts whenever the price drops for a subscriber's tracked sneaker. The scheduler.js file you created earlier should run at given intervals and should go through the subscribers collection and check whether the price of their tracked sneaker has dropped below a certain threshold. If the price has dropped, it should send out an SMS alert to the user.

At the top of the scheduler.js file insert the following code:

const SneaksAPI = require('sneaks-api');
const sneaks = new SneaksAPI();
const twilio = require('twilio')(
   process.env.TWILIO_ACCOUNT_SID,
   process.env.TWILIO_AUTH_TOKEN
);
const Subscribers = [
   {
      "number": 2025550121,
      "styleID": "FY2903",
      "lastResellPrice": 475
   },
   {
      "number": 1234567890,
      "styleID": "DD0587-600",
      "lastResellPrice": 285
   }
];

This code loads the Twilio API, and uses dotenv to load the API with your account credentials stored in the .env file. It also includes the sneaks-api package and the Subscribers array. Ensure to replace the numbers in the Subscribers  array with your own number so you’re able to test out and see price alerts.

This next section is the main function that loops through all the subscribers, grabs their current shoe price and sends out an SMS alert if the price has dropped. You’ll notice a lot of use of helper functions which you will add in the next section.

(async function() {
   const sneakerMap =  getSneakerMap(subscribers);
   for(const subscriber of subscribers){
       if(sneakerMap[subscriber.styleID].price < subscriber.lastResellPrice - 10){
           notifySubscriber(sneakerMap[subscriber.styleID], subscriber.number);
       }
       subscriber.lastResellPrice = sneakerMap[subscriber.styleID].price;
   }
 })()

The getSneakerMap() function outputs a map of all sneaker style IDs in the Subscribers collection along with their current price. Once this map is generated, the code loops through all the subscribers and checks their current shoe in the map to see if their shoe price has dropped.

If it has dropped by $10, it will then run the notifySubscriber() function which sends out an alert to the user. Lastly the loop will then update the last resell price of the subscribers shoe from the sneaker map and then move on to the next subscriber.

Helper functions

Right below your main function is where you’ll place your helper functions. These helper functions will help your code be a bit more modular and will make your code easier to read.

The getSneakerMap() function takes in the list of subscribers, iterates through them and uses the sneaksApiFunctionWrapper() to grab all the sneaker objects from the sneaks-api. These objects will be placed in a map where its key values are its style ID.

async function getSneakerMap(subscribers){
   var sneakerMap = new Object();

   for (const subscriber of subscribers){
       if (sneakerMap[subscriber.styleID]) continue;
       const sneaker = await sneaksApiFunctionWrapper(subscriber.styleID);
       sneakerMap[subscriber.styleID] = sneaker;
   }

   return sneakerMap;
}

The notifySubscriber() function will run if a subscriber needs to be notified and takes in a sneaker and phone number and sends an SMS alert to the phone number.

function notifySubscriber(sneaker, number){
   console.log(sneaker);
   twilio.messages
   .create({
       body: 'Price Dropped - ' + sneaker.name + ' has dropped to $' + sneaker.price +' on '+ sneaker.site + ': ' + sneaker.url,
       from: process.env.TWILIO_NUMBER,
       to: number
   })
   .then(message => console.log(message.sid));
}

Lastly, the sneaksApiFunctionWrapper() function utilizes the sneaks-api to format and provide the information we need.


 function sneaksApiFunctionWrapper(styleID) {
   return new Promise((resolve, reject) => {
       sneaks.getProductPrices(styleID, function(err, product){
           const lowestResellSite = Object.keys(product.lowestResellPrice).reduce((a, b) => product.lowestResellPrice[a] > product.lowestResellPrice[b] ? a : b);
           const sneaker = {
               name: product.shoeName,
               image: product.thumbnail,
               site: lowestResellSite,
               price: product.lowestResellPrice[lowestResellSite],
               url: product.resellLinks[lowestResellSite],
               styleID: product.styleID
           };
           resolve(sneaker);
       }, (errorResponse) => {
           reject(errorResponse);
       });
   });
}

Test your app

Now that your index.js and scheduler.js files have been built, it's time to test them out!

You will use Crontab to schedule time-based jobs for your scheduler.js file. If you aren't familiar with Cron and Cron expressions, here is a great post on them! 

A typical Unix system includes a Crontab file that holds a list of jobs that run on a schedule. If you’re on a Unix system, open up your terminal and run this command to edit your Crontab file:

crontab -e

Within this file is where you will schedule the scheduler.js file to run in intervals. Your entry within the Crontab file will consist of three things: a Cron expression, path to node (which is typically found in /usr/local/bin/node), and the path to your scheduler.js file.

The Cron entry you will insert on your terminal will look like this:

*/10 * * * * [path/to/node] [path/to/scheduler.js]

This entry will have your scheduler.js file run automatically every 10 minutes.  Paste this entry into your terminal and press esc to exit editing mode and type :wq to save and exit the file.

Now that your scheduler.js file is prepped to run, your next step is to set up the index.js file to run. The way the message handler works is that once an SMS message is sent to your Twilio number, the Twilio API sends that SMS as a POST request to the route in your index.js file and responds back to the user depending on what the user has sent.

In order to do this, you need to tell Twilio where to exactly send the POST request to. You’ll need to use the Twilio CLI to set up a webhook. Open up your terminal and login in to the Twilio CLI:

twilio login

This command will then prompt you for your Twilio Account SID and your Auth Token. Once logged in use the following command to create a webhook, taking care to replace the placeholder with your actual Twilio phone number:

twilio phone-numbers:update "+TWILIO_NUMBER" --sms-url="http://localhost:3000/sms"

This command will spin up a tunnel to your local server and you will be receiving requests from the internet and sending them to your server. Normally, in a production app, you would set up a webhook on a dedicated server that's hosted online, but for testing purposes you will use a local server.

At this point you should be ready to start your server and run your application! In your terminal, redirect to your project directory and type in the following command:

node index.js

Now on your phone, send a message to your Twilio number with the following format: Track XXXX where XXXX is the style ID of the sneaker you want to track. Style ID of the sneaker you want to track can be found on any sneaker resell website. The example used below is Track FY2903. You should receive a confirmation and alerts whenever the price drops and it should look like this:

Screenshot of SMS subscription confirmation
Screenshot of SMS alert

Summary

Keeping tabs on prices adds another item to your already long to-do list. An SMS handler can let you know when your desired price is reached and ready to be in your hands. With you, Node.js, and Twilio, you are one step ahead of price volatility.

In an ever-changing market, a text can be the bridge between you and your most coveted item. Want to continue building the app? Expand the messenger handler and add a search function or set price limits for alerts.

Happy Building!

Dhruv Patel is an Intern Developer on Twilio’s Developer Voices team. You can find Dhruv working in a coffee shop with a glass of cold brew or he can either be reached at dhrpatel [at] twilio.com