This is the BBW tracker! A device I made because we missed one too many candle sales at our favorite smell store. It uses an ESP32 to scrape the BBW "Top Offers" section and uses the results to determine whether or not key items are on sale. The design is mostly intentional.. I didn't expect the infill to be visible but I wanted to overlay a few layers of clear filament over it and I didn't think to increase the "Bottom Shell Thickness" which I've since fixed in other projects. And the back is held on by rubber bands. Which kind of looks nice, I guess? lol😅
First, I'll cover a little bit about the hardware then we'll talk through the software firmware!  The project is an Arduino IDE Sketch and it is relatively straightforward but, I will say, it may be a little difficult to replicate as you'll have to retrieve session keys and stuff on your own (but we'll get to that).
Hardware
The BBW tracker uses 6 5mm green LEDs which are hooked up to IO pins through 220ohm resistors to control current. The ESP sits in a socket so I can easily remove it. There is one extra pin on the socket that is unused and should be ignored (it's down below the red USB wire).
This is the circuit diagram showing accurate pinouts and wiring.
Here you can see the assembly, I tried to design it with dividers so the LEDs would glow out of the front. Though using thin white segments was probably not the best idea! It definitely bleeds but you can clearly see which items are marked as on sale and which ones are not still!
 It's powered through the +5v and GND from a USB cable soldered directly to the VIN and GND pins on the ESP32!  I had hoped that was possible because this box wouldn't properly fit a USB cable plugged into the ESP! 
3D-Printed Box
 
Not much to say here! It's a box with proper sizing for my pcb that fits everything. The front is, I think 1mm, of clear PLA with white in the background and black lettering!
I would recommend you make your own as mine doesn't currently latch and it's held together by rubber bands... lol But if people want I can upload the model somewhere as whatever file type is useful for y'all! I just don't have a good way to share now and ultimately it's a box! It's the functionality that matters most!
Firmware
The firmware here is pretty straightforward as well!
I'm trying to avoid directly naming the company because I'm not associated with them. But they have an "All Offers" section under Offers on their main page. This page seems to list all of their current offers and it lists them in a way we can pull down, parse, and analyze!
The most difficult part was figuring out how to get the data reliably. It's a 2 part system, you need a session token to call the slots URL and that token expires at some point, though I haven't yet run into that. The number 2592000 is visible as an expiration in the returned object but I'm not 100% confident whether it's milliseconds or seconds nor am I sure of exactly which token is expiring, though I'd think the refresh token because mine has been running on the same session token. I have tested this for around a month now and the system has only needed an occasional reboot. Speaking of..
Errors and Debugging
A lot of information is provided as output through the USB serial connection viewable by using the "Serial Monitor" in Arduino IDE or monitoring with ESP-IDF.
The GPIO2 LED on my ESP32 Devkit v1/WROOM board is used as an ERROR signal to indicate there is a problem. After stabilizing the firmware and letting it run, I've seen several errors but they were always remedied by a restart and are usually days apart. I'd have to assume some issue with the internet connection or a major AWS outage (lol)
The Tokens
This part was interesting. I used Chrome's Developer Tools to artificially slow the connection down so I was able to see the page load piece by piece until I saw the main list of Top Offers load. I found a "slots" request that had "top-offers" at the end and decided to investigate. There were several that seemed to end with "=top-offers" but the list always seems to be in the one that looks like the screenshot.
 
The data inside showed a list of offers I could retrieve and search through for specific strings like "3-wick candles" or "hand soap" and drive each specific status LED based on whether or not they're found!
However, requests to this endpoint must be made with a token. I think this is using something like Salesforce Commerce Cloud So I also had to find the token URL..
To explain this like it was a linear process would not do it justice.
I spent quite a while trying to find some way to identify the token URL. What ended up working was to use "Incognito Mode" so that my cookies would be flushed and it thought I needed a new token every time. Then I just used the Dev Tools filtering to watch for "token" and sure enough:
 
However, quick snag, this grant_type is "client_credentials" but we want "refresh_token". What ended up working for me in the end was to just wait in a non-incognito window for a while and then refresh the page with Dev Tools still filtered for "token". Then, since it's already established a session with you, it'll call refresh to keep the token alive instead!
Success! I copied a curl request from dev tools and then emulated the important parts in code and tested it out to great success! Eventually.. but that's not important lol
These URLS could just be pasted into my sketch and everything worked! After I wrote it all so that it worked.
The refresh_token and client_id come from the Response payload in the token request or can be retrieved independently by running the curl request copied.
Success!!
From there, I wrote the sketch below! I have blanked out anything that was potentially proprietary with fill in the blanks (make sure to fill them all in! check for channel_id too) and hopefully the above can help you learn to make your own!
Firmware Code
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | #include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> const char* ssid = "YOUR_WIFI_SSID"; const char* password = "YOUR_WIFI_PASS"; // Refresh token endpoint const char* token_url = "YOUR_TOKEN_URL"; // Slots endpoint const char* slots_url = "YOUR_SLOTS_URL"; // Values from your curl const char* client_id = "YOUR_CLIENT_ID"; const char* refresh_token = "YOUR_REFRESH_TOKEN"; const int CANDLE_LED = 25; //13; const int HANDSOAP_LED = 26; //12; const int MOISTURE_LED = 27; // 14; const int TRAVEL_LED = 14; const int DETERG_LED = 12; const int BOOSTER_LED = 13; const int ERROR_LED = 2; String accessToken; unsigned long lastRefresh = 0; unsigned long tokenTTL = 0; String getAccessToken() { HTTPClient http; http.begin(token_url); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.addHeader("User-Agent", "ESP32"); String body = "grant_type=refresh_token"; body += "&refresh_token=" + String(refresh_token); body += "&client_id=" + String(client_id); body += "&channel_id=(FILLTHISIN!!!_It's the NameOfTheCompany written like that)"; body += "&dnt=1"; int code = http.POST(body); String token; if (code > 0) { String payload = http.getString(); Serial.println("Token response:"); Serial.println(payload); DynamicJsonDocument doc(2048); DeserializationError err = deserializeJson(doc, payload); if (!err) { token = doc["access_token"].as<String>(); tokenTTL = doc["expires_in"].as<unsigned long>() * 1000UL; // ms lastRefresh = millis(); } } else { Serial.printf("Token request error: %d\n", code); } http.end(); return token; } void checkOffers() { if (accessToken == "") { accessToken = getAccessToken(); if (accessToken == "") { digitalWrite(ERROR_LED, HIGH); digitalWrite(BOOSTER_LED, HIGH); digitalWrite(CANDLE_LED, HIGH); return; } } HTTPClient http; http.begin(slots_url); http.addHeader("Authorization", "Bearer " + accessToken); http.addHeader("User-Agent", "ESP32"); int code = http.GET(); if (code > 0) { String payload = http.getString(); Serial.println("Slots response received"); String lower = payload; lower.toLowerCase(); if (lower.indexOf("3-wick candles") != -1) { Serial.println("Candle Sale FOUND!"); digitalWrite(CANDLE_LED, HIGH); } else { Serial.println("Candle Sale NOT found."); digitalWrite(CANDLE_LED, LOW); } if (lower.indexOf("hand soap") != -1) { Serial.println("Hand soap Sale FOUND!"); digitalWrite(HANDSOAP_LED, HIGH); } else { Serial.println("Hand soap Sale NOT found."); digitalWrite(HANDSOAP_LED, LOW); } if (lower.indexOf("moisturizer") != -1) { Serial.println("moisturizer Sale FOUND!"); digitalWrite(MOISTURE_LED, HIGH); } else { Serial.println("moisturizer Sale NOT found."); digitalWrite(MOISTURE_LED, LOW); } if (lower.indexOf("travel") != -1) { Serial.println("travel Sale FOUND!"); digitalWrite(TRAVEL_LED, HIGH); } else { Serial.println("travel Sale NOT found."); digitalWrite(TRAVEL_LED, LOW); } if (lower.indexOf("detergent") != -1) { Serial.println("detergent Sale FOUND!"); digitalWrite(DETERG_LED, HIGH); } else { Serial.println("detergent Sale NOT found."); digitalWrite(DETERG_LED, LOW); } if (lower.indexOf("booster") != -1) { Serial.println("booster Sale FOUND!"); digitalWrite(BOOSTER_LED, HIGH); } else { Serial.println("booster Sale NOT found."); digitalWrite(BOOSTER_LED, LOW); } } else { Serial.printf("Slots request error: %d\n", code); digitalWrite(ERROR_LED, HIGH); digitalWrite(TRAVEL_LED, HIGH); digitalWrite(CANDLE_LED, HIGH); } http.end(); } void setup() { Serial.begin(115200); pinMode(CANDLE_LED, OUTPUT); pinMode(HANDSOAP_LED, OUTPUT); pinMode(MOISTURE_LED, OUTPUT); pinMode(TRAVEL_LED, OUTPUT); pinMode(DETERG_LED, OUTPUT); pinMode(BOOSTER_LED, OUTPUT); pinMode(ERROR_LED, OUTPUT); WiFi.begin(ssid, password); Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(".\n"); } Serial.println("Connected!"); accessToken = getAccessToken(); } void loop() { // Refresh token before expiry (5 min cushion) if (millis() - lastRefresh > (tokenTTL - 300000)) { accessToken = getAccessToken(); } checkOffers(); delay(60000); // check every 1 min } | 



.png)


 
