Build a Costco Price Drop Alert Bot

Automate Costco price tracking and get SMS alerts when prices drop.

Introduction

30 Second Summary

You know that feeling when you spot a deal at Costco and think, "I wish I'd bought this last week when it was cheaper"? What if your phone could just tell you the moment prices drop, without you ever checking?

In this project, you will build a Python bot that automatically checks Costco protein bar prices every day and sends you a text message whenever it detects a price drop.

What You'll Build

You'll have a price surveillance system running on your Mac that fetches live Costco prices, compares them against yesterday's data, and fires off an SMS alert the second something gets cheaper.

By the end of this project, you'll have:

  • A working Python script that pulls live Costco protein bar prices from your terminal, no browser required.
  • Real SMS alerts delivered to your phone showing the old price, new price, and exactly how much you save.
  • A cron job that runs the bot automatically every morning on your Mac while you sleep.
  • A local JSON price history file that tracks prices over time, making comparisons smarter with every check.
  • Secret Mission: Add a target price threshold so you only get texted when prices drop below your personal number.

Are there any prerequisites?

You'll need macOS (this project uses Terminal and cron), a code editor like VS Code or Cursor, and a Costco membership so the scraper can access member pricing.

You'll also need a credit card to upgrade your Twilio account for custom SMS messaging. The first 100 messages after upgrading are free, so actual SMS costs are $0 for this project.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters to you personally.

Set Up Your Python Environment and Accounts

Before your bot can check prices or send texts, you need the right tools and credentials in place.

In this step, you will verify Python is installed, set up a project folder with its dependencies, create accounts on Apify and Twilio, and configure your credentials as environment variables.

In this step, get ready to:
  • Verify Python 3 and install project dependencies.
  • Create free Apify and upgraded Twilio accounts.
  • Configure environment variables for API access.
Open Terminal and verify Python

macOS ships with Python 3 pre-installed. You just need to confirm the version is 3.8 or higher.

  • Press Cmd+Space to open Spotlight Search.
  • Type Terminal and press Enter to open it.
  • Check your Python version by running this command:
python3 --version

✔️ I see version 3.8 or higher

You see something like Python 3.11.6 or higher. Python is ready to go.

ⓧ I see a version lower than 3.8

You need Python 3.8 or higher for this project. macOS sometimes ships with an older default.

  • Visit python.org/downloads and download the latest Python 3 installer for macOS.
  • Run the installer and follow the prompts.
  • Close and reopen Terminal, then run python3 --version again to confirm the new version.

ⓧ Command not found

Python 3 is not installed on your Mac. Install it from the official source.

  • Visit python.org/downloads and download the latest Python 3 installer for macOS.
  • Run the installer and follow the prompts.
  • Close and reopen Terminal, then run python3 --version to confirm installation.
Create your project folder and virtual environment

A virtual environment keeps your project's packages isolated from your system Python so nothing conflicts.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir costco-price-bot
cd costco-price-bot
  • Confirm you are inside the folder by running:
pwd

You should see a path ending in /Desktop/costco-price-bot.

  • Create a virtual environment by running:
python3 -m venv venv
  • Activate the virtual environment by running:
source venv/bin/activate

Your terminal prompt now shows (venv) at the start. This confirms the virtual environment is active.

  • Install the two packages your project needs by running:
pip install requests twilio

You should see output listing the packages being downloaded and installed, ending with a success message.

Seeing a permission error or command not found?

Make sure you see (venv) in your prompt. If not, re-run source venv/bin/activate before trying the install again.

help me fix a pip install error in my virtual environment

Create your Apify and Twilio accounts

You need two API services. Apify scrapes Costco prices for you. Twilio sends SMS messages to your phone.

  • Open your browser and go to apify.com.
  • Click Sign up and create a free account. No credit card is required.
  • Once signed in, click your profile icon in the top-right corner and select Settings.
  • Click the API & Integrations tab.
  • Copy your API token and save it somewhere safe (a text file or password manager).

What is Apify?

Apify is a web scraping platform that handles anti-bot protections for you. Costco uses aggressive bot detection that blocks direct scraping attempts. Apify's pre-built scraper navigates around these protections so you can focus on the interesting logic.

  • Open a new tab and go to twilio.com.
  • Click Sign up and create a free account.
  • Once signed in, click Upgrade in the Twilio Console.
  • Add a payment method (credit card or PayPal) to complete the upgrade.

Why do I need to upgrade my Twilio account?

Twilio trial accounts can only send SMS using pre-defined templates. Custom message bodies (like your price drop alerts) require an upgraded account.

After upgrading, you get 100 free SMS messages that never expire. This project will use far fewer than that, so your actual SMS cost is $0.

  • In the Twilio Console, navigate to Products & Services and click Numbers and Senders.
  • Click the Phone Numbers tab.
  • Click Set up a new phone number.
  • Follow the prompts to select a US phone number with SMS capability, then click Purchase.
  • Note the phone number you just purchased (you will need it shortly).

Now grab your Account SID and Auth Token. These are on the Twilio Console homepage.

  • Click the Workbench tab at the bottom of the Twilio Console.
  • In the Overview tab, find your Account SID and Auth Token.
  • Click the eye icon to reveal the Auth Token, then copy both values somewhere safe.

Keep your credentials private

Never share your Auth Token, Account SID, or Apify token publicly. Treat them like passwords.

Set your environment variables

Environment variables let your Python scripts access API credentials without hardcoding them in your code. You will export them in your current terminal session and add them to your shell profile for persistence.

  • In your Terminal (still inside costco-price-bot with (venv) active), export your credentials by running these commands, replacing each placeholder with your real values:
export APIFY_TOKEN="your-apify-token-here"
export TWILIO_ACCOUNT_SID="your-account-sid-here"
export TWILIO_AUTH_TOKEN="your-auth-token-here"
export TWILIO_FROM_NUMBER="your-twilio-number-here"
export MY_PHONE_NUMBER="your-personal-number-here"

What are these variables?

  • APIFY_TOKEN authenticates your requests to the Apify scraping API.
  • TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN authenticate your requests to Twilio.
  • TWILIO_FROM_NUMBER is the Twilio phone number that sends your SMS alerts.
  • MY_PHONE_NUMBER is your personal phone number that receives alerts.
  • Verify the variables are set by running:
echo $APIFY_TOKEN

You should see your Apify token printed back. If it is blank, re-run the export command for that variable.

These exports only last for this terminal session. To make them persist across new sessions, add them to your shell profile.

  • Open your shell profile for editing by running:
open ~/.zshrc

If the file does not exist

Run touch ~/.zshrc first to create it, then run the open command again.

  • Add the same five export lines to the bottom of the file.
  • Save and close the file.

Not sure which shell profile to use?

macOS uses zsh as its default shell. If you have customized your shell to bash, add the exports to ~/.bash_profile instead.

help me figure out which shell profile file to edit on my Mac

Verify everything is connected

Before you move on, confirm that Python can find and load the Twilio package with your credentials ready.

  • Before you run the next command, predict: do you think it will print "Ready" or throw an error?
  • Test that the Twilio SDK loads correctly by running:
python3 -c "from twilio.rest import Client; print('Ready')"

You should see Ready printed to your terminal. This confirms Python, the virtual environment, and the Twilio SDK are all working together.

Seeing ModuleNotFoundError: No module named 'twilio'?

This means you are running python3 outside the virtual environment. Check that your prompt starts with (venv). If not, run source venv/bin/activate and try again.

help me fix a ModuleNotFoundError for twilio in my Python virtual environment

Your environment is ready and both API accounts are configured. Next up, you will write a Python script that pulls live Costco protein bar prices into your terminal.

Fetch Live Costco Protein Bar Prices

Your environment is ready with Python, your virtual environment, and all the credentials loaded. Now it is time to actually pull live pricing data from Costco.

Before you can detect price drops, you need a working data pipeline that reaches into Costco's product catalog and brings back real numbers. In this step, you will write a Python script that calls the Apify API to scrape Costco protein bar prices and print them in your terminal.

In this step, get ready to:
  • Write a function that sends a POST request to the Apify Costco scraper actor.
  • Run the script and see real Costco protein bar prices printed in your terminal.
Create your price fetching script

Costco uses aggressive bot protection that blocks direct scraping attempts. Instead of fighting that, you will use an Apify actor that handles the anti-bot measures for you.

  • Create a new file called fetch_prices.py in your project folder by running:
touch fetch_prices.py
  • Open fetch_prices.py in your code editor and paste the following code:
import os
import json
import requests

# Load your Apify token from environment variables
APFIY_TOKEN = os.environ["APIFY_TOKEN"]
SEARCH_TERM = "protein bars"


def fetch_costco_prices():
    # Tell the user what we're searching for
    print(f"Searching Costco for: {SEARCH_TERM}")

    # The Apify endpoint that runs the Costco scraper and returns results
    url = "https://api.apify.com/v2/actors/rigelbytes~costco-scraper/run-sync-get-dataset-items"
    params = {"token": APIFY_TOKEN}
    payload = {
        "searchQueries": [SEARCH_TERM],
        "maxItems": 10
    }

    # Send the POST request (timeout set high because Costco is slow)
    response = requests.post(url, json=payload, params=params, timeout=300)
    response.raise_for_status()

What does this code do?

APIFY_TOKEN pulls your API token from the environment variable you set in Step 1. SEARCH_TERM defines what product to search for on Costco.

The URL points to the Apify REST API endpoint that runs the Costco scraper actor synchronously and returns the results directly. The searchQueries payload tells it to search for protein bars, and maxItems caps results at 10 to stay within free tier usage.

The 300-second timeout accounts for Costco being a slow data source. The actor can take 1-2 minutes to respond.

  • Directly below that code in the same file, paste the response handling and runner:
    # Parse the JSON response into a list of products
    products = response.json()
    print(f"Found {len(products)} products\n")

    # Print each product's price and name
    for product in products:
        name = product.get("name", "Unknown")
        price = product.get("price", "N/A")
        print(f"  ${price} - {name}")

    return products


if __name__ == "__main__":
    fetch_costco_prices()

What does this section do?

response.json() parses the API response into a Python list of product dictionaries. The loop iterates through each product, safely extracting the name and price fields with fallback defaults.

The if __name__ == "__main__" block means the function runs when you execute the file directly with python3.

  • Save fetch_prices.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before running it.

ⓧ I'd like to double check the full code

Your complete fetch_prices.py should look like this:

import os
import json
import requests


APFIY_TOKEN = os.environ["APIFY_TOKEN"]
SEARCH_TERM = "protein bars"


def fetch_costco_prices():
    print(f"Searching Costco for: {SEARCH_TERM}")

    url = "https://api.apify.com/v2/actors/rigelbytes~costco-scraper/run-sync-get-dataset-items"
    params = {"token": APIFY_TOKEN}
    payload = {
        "searchQueries": [SEARCH_TERM],
        "maxItems": 10
    }

    response = requests.post(url, json=payload, params=params, timeout=300)
    response.raise_for_status()

    products = response.json()
    print(f"Found {len(products)} products\n")

    for product in products:
        name = product.get("name", "Unknown")
        price = product.get("price", "N/A")
        print(f"  ${price} - {name}")

    return products


if __name__ == "__main__":
    fetch_costco_prices()
Run the script and see live prices

Your script is ready. Before you run it, think about what you expect to happen. Do you think the API will return data immediately, or will it take a moment?

  • Run the price fetcher by executing:
python3 fetch_prices.py

Why does it take so long?

The Apify actor is scraping Costco in real time when you call it. Costco is a slow data source, so expect the first call to take 1-2 minutes before results appear. Do not cancel the command. Let it finish.

You should see output like this, with real Costco protein bar names and prices printed to your terminal:

Searching Costco for: protein bars
Found 10 products

  $25.99 - Kirkland Signature Protein Bars
  $32.99 - Premier Protein Shake
  ...

Not seeing any output after 5 minutes?

Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.

Confirm your APIFY_TOKEN environment variable is set by running echo $APIFY_TOKEN. If it prints nothing, re-export it from Step 1.

If you see a 401 error, your token may be incorrect. Double check it in your Apify account under Settings > Integrations. Help me debug my Apify API call

You have live Costco prices flowing into your terminal. Next up, you will wire up Twilio to send yourself a text message so you can confirm SMS delivery works before combining it with price checking.

Send Yourself a Test Text Message

You have live Costco prices flowing into your terminal. The next piece is sending text messages to your phone.

Testing SMS delivery on its own isolates any messaging problems early. If something breaks later when you combine prices with texting, you will know the issue is in your price logic, not the Twilio setup.

In this step, get ready to:
  • Create a Python script that sends an SMS using the Twilio API.
  • Run the script and confirm the message arrives on your phone.
Create the test SMS script

The Twilio Python SDK gives you a Client object that handles authentication and API calls. You initialize it once with your credentials, then call client.messages.create() to fire off a text.

  • Create a new file called send_test_sms.py in your project folder by running:
touch send_test_sms.py
  • Open send_test_sms.py in your code editor and add the following code:
import os
from twilio.rest import Client


# Load Twilio credentials from environment variables
TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"]
TWILIO_AUTH_TOKEN = os.environ["TWILIO_AUTH_TOKEN"]
TWILIO_FROM_NUMBER = os.environ["TWILIO_FROM_NUMBER"]
MY_PHONE_NUMBER = os.environ["MY_PHONE_NUMBER"]


def send_test():
    # Initialize the Twilio client with your account credentials
    client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

    # Send an SMS message
    message = client.messages.create(
        body="Hello from your Costco Price Bot! If you got this, SMS is working.",
        from_=TWILIO_FROM_NUMBER,
        to=MY_PHONE_NUMBER
    )

    print(f"Message sent! SID: {message.sid}")


if __name__ == "__main__":
    send_test()

What does this code do?

  • The four os.environ lines pull your Twilio credentials from the environment variables you set in Step 1. This keeps secrets out of your code.
  • Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) creates an authenticated connection to the Twilio API.
  • client.messages.create() sends the SMS. The from_ parameter is your Twilio number, and to is your personal number.
  • message.sid is a unique ID that Twilio assigns to every message. Printing it confirms the API call succeeded.
  • Save send_test_sms.py.

Seeing a red underline on the import?

If your editor flags from twilio.rest import Client as unresolved, make sure your virtual environment is activated (you should see (venv) in your terminal prompt) and that you ran pip install twilio inside it.

help me fix my Twilio import error in send_test_sms.py

✔️ Awesome, I've got everything!

Great. Double check you have saved send_test_sms.py before moving on.

ⓧ I'd like to double check the full code

import os
from twilio.rest import Client


TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"]
TWILIO_AUTH_TOKEN = os.environ["TWILIO_AUTH_TOKEN"]
TWILIO_FROM_NUMBER = os.environ["TWILIO_FROM_NUMBER"]
MY_PHONE_NUMBER = os.environ["MY_PHONE_NUMBER"]


def send_test():
    client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

    message = client.messages.create(
        body="Hello from your Costco Price Bot! If you got this, SMS is working.",
        from_=TWILIO_FROM_NUMBER,
        to=MY_PHONE_NUMBER
    )

    print(f"Message sent! SID: {message.sid}")


if __name__ == "__main__":
    send_test()
Run the script and confirm delivery

The script is ready. Before you run it, think about what you expect to happen. Will you see output in the terminal, on your phone, or both?

  • Run the test script:
python3 send_test_sms.py

You should see a line like Message sent! SID: SM... printed in your terminal. Within 30 seconds, your phone buzzes with the test message.

  • Check your phone. You should see a text that reads: "Hello from your Costco Price Bot! If you got this, SMS is working."

What is the SID?

The SID (String Identifier) is a unique ID that Twilio assigns to every message. It looks like SM followed by a long string of letters and numbers. If you see it printed, the API call succeeded regardless of whether the message has physically arrived yet.

No text message arriving?

  • Confirm your MY_PHONE_NUMBER environment variable includes the country code (e.g. +1 for US numbers). Run echo $MY_PHONE_NUMBER to check.
  • Verify your Twilio account is upgraded. Trial accounts restrict SMS to pre-defined templates and will silently fail with custom message bodies.
  • Check that TWILIO_FROM_NUMBER is the Twilio number you purchased, not your personal number. Run echo $TWILIO_FROM_NUMBER to verify.

Help me debug why my Twilio SMS is not arriving

SMS delivery is confirmed. You now have two independent building blocks: price fetching and text messaging. Next up, you will wire them together into one script that detects price drops and alerts you automatically.

Detect Price Drops and Send Alerts

You can fetch live Costco prices and you can send yourself a text message. Those two pieces work independently.

Now you combine them into a single script that compares today's prices against yesterday's and fires off an alert when something drops. The catch: the very first run has nothing to compare against, so it will NOT find any drops. That's expected and exactly what you'll prove in this step.

In this step, get ready to:
  • Build a combined script that fetches prices, tracks history, and detects drops.
  • Wire in SMS alerting so drops trigger a text to your phone.
  • Simulate a price drop and confirm you receive an alert.
Create the main script with imports and price fetching

This new script needs credentials for both Apify and Twilio because it fetches prices AND sends texts. It also needs a reliable path to a local JSON file where it stores price history between runs.

  • Create a new file called check_prices.py in your project folder. Add the imports and constants by pasting this code:
import os
import json
import requests
from datetime import datetime
from twilio.rest import Client


APOFY_TOKEN = os.environ["APIFY_TOKEN"]
TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"]
TWILIO_AUTH_TOKEN = os.environ["TWILIO_AUTH_TOKEN"]
TWILIO_FROM_NUMBER = os.environ["TWILIO_FROM_NUMBER"]
MY_PHONE_NUMBER = os.environ["MY_PHONE_NUMBER"]

SEARCH_TERM = "protein bars"
HISTORY_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "price_history.json")

What does this code do?

The imports pull in everything you need: requests for the Apify API, Client for Twilio SMS, datetime for timestamping when you last checked a price, and json for reading and writing your price history file.

HISTORY_FILE uses os.path.abspath(__file__) to build a path relative to the script itself. This means the JSON file always ends up next to your script, even if you run it from a different directory later (like a cron job will).

The price-fetching logic is the same function you already built in fetch_prices.py. You're including it directly here so this script is self-contained.

  • Add the fetch_costco_prices() function below the constants by pasting this code:

def fetch_costco_prices():
    print(f"Searching Costco for: {SEARCH_TERM}")

    url = "https://api.apify.com/v2/actors/rigelbytes~costco-scraper/run-sync-get-dataset-items"
    params = {"token": APIFY_TOKEN}
    payload = {
        "searchQueries": [SEARCH_TERM],
        "maxItems": 10
    }

    response = requests.post(url, json=payload, params=params, timeout=300)
    response.raise_for_status()

    products = response.json()
    print(f"Found {len(products)} products\n")

    for product in products:
        name = product.get("name", "Unknown")
        price = product.get("price", "N/A")
        print(f"  ${price} - {name}")

    return products

What does this code do?

This is identical to what you built in Step 2. It calls the Apify Costco scraper, prints each product's name and price, and returns the full product list for the rest of the script to use.

  • Save check_prices.py.
  • Verify your imports are correct by running this command:
python3 -c "import check_prices; print('Imports OK')"

You should see Imports OK printed with no errors.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
  • If it's not active, run source venv/bin/activate from your project folder.

Still stuck? Help me debug a ModuleNotFoundError when importing check_prices.py

Add history tracking and price comparison logic

To detect a drop, you need to know what the price was last time you checked. Two tiny helper functions handle reading and writing a JSON file that stores this history.

  • Add the load_history() and save_history() functions below fetch_costco_prices() by pasting this code:

def load_history():
    if os.path.exists(HISTORY_FILE):
        with open(HISTORY_FILE, "r") as f:
            return json.load(f)
    return {}


def save_history(history):
    with open(HISTORY_FILE, "w") as f:
        json.dump(history, f, indent=2)

What does this code do?

  • load_history() checks if price_history.json exists. If it does, it reads the stored prices. If not (first run), it returns an empty dictionary.
  • save_history() writes the current prices back to that file with nice formatting (indent=2) so you can read it later.

Now for the core logic. The find_price_drops() function loops through every product, compares its current price to what's stored in history, and collects any drops.

  • Add the find_price_drops() function below save_history() by pasting this code:

def find_price_drops(products, history):
    drops = []

    for product in products:
        name = product.get("name", "Unknown")
        price = product.get("price")

        if price is None:
            continue

        price = float(price)
        product_key = name

        if product_key in history:
            old_price = history[product_key]["price"]
            if price < old_price:
                drops.append({
                    "name": name,
                    "old_price": old_price,
                    "new_price": price,
                    "savings": round(old_price - price, 2)
                })

        history[product_key] = {
            "price": price,
            "last_checked": datetime.now().isoformat()
        }

    return drops

What does this code do?

  • For each product, it skips any without a price (some Costco items show as unavailable).
  • If the product already exists in history AND the current price is lower than the stored price, it records a drop with both prices and the savings amount.
  • Either way, it updates history with the current price and a timestamp. This means the first run always stores prices without finding drops.
  • Save check_prices.py.
  • Verify there are no syntax errors by running this command:
python3 -c "import check_prices; print('All functions loaded')"

You should see All functions loaded printed. This confirms Python can parse all four functions without errors.

Seeing a SyntaxError?

  • Check that each function definition starts with def and ends its first line with a colon.
  • Make sure the code inside each function is indented with 4 spaces, not tabs.
  • Check that you have matching opening and closing braces in the drops.append({...}) call inside find_price_drops().

Need help? Help me fix a SyntaxError in my check_prices.py file

Wire in SMS alerts and the main function

The script can now fetch prices and compare them. It still needs a function that formats a drop into a text message, and a main() function that orchestrates the whole flow.

  • Add the send_sms_alert() function below find_price_drops() by pasting this code:

def send_sms_alert(drops):
    client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)

    message_body = "COSTCO PRICE DROP!\n\n"
    for drop in drops:
        message_body += (
            f"{drop['name'][:50]}\n"
            f"${drop['old_price']:.2f} -> ${drop['new_price']:.2f} "
            f"(save ${drop['savings']:.2f})\n\n"
        )

    message = client.messages.create(
        body=message_body,
        from_=TWILIO_FROM_NUMBER,
        to=MY_PHONE_NUMBER
    )

    print(f"SMS alert sent! SID: {message.sid}")

What does this code do?

  • It builds a message body listing every drop: the product name (truncated to 50 characters), the old price, the new price, and how much you save.
  • It uses the same client.messages.create() pattern you tested in send_test_sms.py, but with a dynamic body instead of a hardcoded string.
  • Add the main() function and the entry point below send_sms_alert() by pasting this code:

def main():
    products = fetch_costco_prices()

    if not products:
        print("No products found. Check your search term.")
        return

    history = load_history()
    drops = find_price_drops(products, history)
    save_history(history)

    if drops:
        print(f"\n{len(drops)} price drop(s) detected!")
        send_sms_alert(drops)
    else:
        print("\nNo price drops detected. Prices saved for next check.")


if __name__ == "__main__":
    main()

What does this code do?

  • main() orchestrates the entire flow: fetch current prices, load the stored history, compare them, save the updated history, and send an alert if drops exist.
  • If no products come back from Apify (network issue or empty results), it exits early with a message instead of crashing.
  • The if __name__ == "__main__" block ensures main() only runs when you execute the file directly, not when importing it.
  • Save check_prices.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before running it.

ⓧ I'd like to double check the full code

Scroll up and confirm each chunk is present in order: imports and constants, then fetch_costco_prices(), then load_history() and save_history(), then find_price_drops(), then send_sms_alert(), then main() with the if __name__ block at the very end.

Run the bot and simulate a price drop

Time to see this in action. Before you run it, think about what should happen: there is no price_history.json file yet. What will find_price_drops() do when history is an empty dictionary?

  • Run the script by executing this command:
python3 check_prices.py

The Apify scraper takes 1-2 minutes to respond (Costco is a slow data source). After it finishes, you see the product list followed by:

No price drops detected. Prices saved for next check.

This is exactly right. With no history to compare against, there can be no drops. The script saved today's prices as the baseline.

  • Confirm the history file was created by running:
ls price_history.json

You should see price_history.json listed. The bot created it on its first run.

Now simulate a price drop. You'll manually increase a stored price so the next run thinks it dropped.

  • Open price_history.json in your code editor.
  • Find any product's "price" value and increase it by 5 (for example, change 25.99 to 30.99).
  • Save the file.

Why does raising the stored price simulate a drop?

The script compares today's live price against the stored history price. If the stored price is higher than the live price, it looks like a drop. By bumping the stored number up by $5, you trick the script into seeing a $5 "drop" when it fetches the same live price again.

Before you run it again, what do you expect to happen this time? The stored price is now $5 higher than the live price.

  • Run the script again:
python3 check_prices.py

After the Apify scraper finishes, you should see a message like 1 price drop(s) detected! followed by SMS alert sent! with a message SID. Check your phone. You should have a text showing the product name, the old price, the new price, and how much you "saved."

No SMS arrived?

  • Check that your terminal printed SMS alert sent! SID: with a valid SID. If it did, the message is in transit. Wait 30 seconds.
  • If you see an authentication error, double-check your TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables are still set in this terminal session.
  • Make sure you upgraded your Twilio account. Trial accounts cannot send custom message bodies.

Still stuck? Help me troubleshoot why my Twilio SMS is not arriving in check_prices.py

Your price bot works. It fetches live prices, compares them to stored history, and texts you when something drops. Next up, you'll automate this so it runs every morning without you lifting a finger.

Automate with a Daily Cron Job

Your price checker works perfectly when you run it by hand. But the whole point of a bot is to never think about it.

A cron job is a built-in macOS scheduler that runs commands on a timer. In this step, you will wrap your script in a shell file and schedule it to run every morning at 7 AM. You wake up to alerts if prices dropped overnight.

In this step, get ready to:
  • Create a wrapper shell script that runs your price checker with all credentials baked in.
  • Schedule the script to run daily using a cron job.
  • Test the automation and confirm it logs output.
Create the wrapper shell script

Cron jobs run in a stripped-down environment. They do not load your shell profile, so environment variables you exported earlier will not be available. The wrapper script solves this by activating the virtual environment and exporting credentials itself before running check_prices.py.

  • In your code editor, create a new file called run_checker.sh inside your project folder (the same folder that contains check_prices.py).
  • Paste the following code into run_checker.sh:
#!/bin/bash

# Move to the directory where this script lives
cd "$(dirname "$0")"

# Activate the virtual environment
source venv/bin/activate

# Set credentials for the APIs
export APIFY_TOKEN="your-apify-token-here"
export TWILIO_ACCOUNT_SID="your-account-sid-here"
export TWILIO_AUTH_TOKEN="your-auth-token-here"
export TWILIO_FROM_NUMBER="your-twilio-number-here"
export MY_PHONE_NUMBER="your-phone-number-here"

# Run the price checker and log all output
python3 check_prices.py >> checker.log 2>&1

echo "---Run completed: $(date)---" >> checker.log

What does this script do?

  • cd "$(dirname "$0")" moves into the folder where the script lives, so relative paths like venv/ and checker.log resolve correctly no matter where cron runs the script from.
  • source venv/bin/activate loads the virtual environment so requests and twilio are importable.
  • The five export lines provide the API credentials that check_prices.py reads from environment variables.
  • >> checker.log 2>&1 appends both normal output and errors into a log file so you can review runs later.
  • Replace each placeholder (your-apify-token-here, your-account-sid-here, etc.) with your actual credentials.
  • Save run_checker.sh.

The script needs execute permission before macOS will run it. Make it executable by running this command:

chmod +x run_checker.sh
  • Test the wrapper manually by running this command:
./run_checker.sh
  • Wait for it to complete (this takes 1-2 minutes while Apify fetches Costco data).
  • Check the log output by running:
cat checker.log

You should see the price checker output followed by a timestamped completion line like ---Run completed: Mon Jul 28 10:05:32 PDT 2026---.

Script not running?

  • If you see Permission denied, make sure you ran chmod +x run_checker.sh first.
  • If you see errors about missing modules, confirm the venv folder exists in the same directory as run_checker.sh.
  • If credentials are wrong, double-check you replaced all five placeholders with your actual values.

help me debug my run_checker.sh script

Schedule the cron job

Now that the wrapper runs correctly on its own, you will tell macOS to run it automatically every morning. First, you need the full path to your script.

  • Get the full path to your project folder by running:
pwd
  • Note the path that prints (for example, /Users/yourname/Desktop/costco-bot). You will need it in the next command.
  • Set nano as your crontab editor so you get a beginner-friendly interface by running:
export EDITOR=nano
  • Open your crontab for editing by running:
crontab -e

What will I see?

Nano opens with a blank file (or your existing crontab if you have one). You will type your schedule line here. The controls are shown at the bottom of the screen.

  • Type the following line into the editor, replacing the path with the output from your pwd command:
0 7 * * * [[PROJECT_PATH="full path from pwd"]]/run_checker.sh

What does the cron schedule mean?

The five values before the command are: minute, hour, day-of-month, month, day-of-week. 0 7 * * * means "at minute 0, hour 7, every day, every month, every weekday." That is 7:00 AM daily.

  • Save the file by pressing Ctrl+O, then press Enter to confirm.
  • Exit nano by pressing Ctrl+X.
  • Verify your cron job was saved by running:
crontab -l

You should see your cron line printed back, showing the path to run_checker.sh and the 0 7 * * * schedule.

Crontab empty after saving?

  • Make sure you pressed Ctrl+O (the letter O, not zero) to write the file before exiting.
  • If macOS asks for permission to access your files, click Allow. Cron needs this to run scripts.

help me set up my crontab on macOS

The job is scheduled for 7 AM, but you do not want to wait until tomorrow to know it works. Temporarily change the schedule to every minute for a quick test.

  • Open the crontab again by running:
crontab -e
  • Change 0 7 * * * to * * * * * (this means every minute).
  • Save with Ctrl+O, then Enter, then exit with Ctrl+X.
  • Wait about 60-90 seconds for the job to trigger.

Before you check the log, what do you expect to see? Think about what run_checker.sh appends each time it runs.

  • Check the log to confirm the cron job ran by running:
cat checker.log

You should see a NEW timestamped completion line at the bottom, added automatically by cron. This proves the automation is working.

  • Now change the schedule back to daily. Open the crontab one last time:
crontab -e
  • Change * * * * * back to 0 7 * * *.
  • Save with Ctrl+O, Enter, then exit with Ctrl+X.
  • Confirm the final schedule by running:
crontab -l

You should see 0 7 * * * followed by the full path to run_checker.sh. Your bot now checks Costco prices every morning at 7 AM without you lifting a finger.

Your Mac needs to be awake

Cron jobs only fire when your Mac is powered on and awake. If your Mac is asleep at 7 AM, the job will not run until the next morning when the Mac is awake at that time. For a learning project this is perfectly fine.

✔️ Awesome, I've got everything!

Great. Double check you have saved run_checker.sh and confirmed crontab -l shows your daily schedule.

ⓧ I'd like to double check the full code

Here is the complete run_checker.sh for reference:

#!/bin/bash

cd "$(dirname "$0")"

source venv/bin/activate

export APIFY_TOKEN="your-apify-token-here"
export TWILIO_ACCOUNT_SID="your-account-sid-here"
export TWILIO_AUTH_TOKEN="your-auth-token-here"
export TWILIO_FROM_NUMBER="your-twilio-number-here"
export MY_PHONE_NUMBER="your-phone-number-here"

python3 check_prices.py >> checker.log 2>&1

echo "---Run completed: $(date)---" >> checker.log

And your crontab entry should look like:

0 7 * * * /Users/yourname/Desktop/costco-bot/run_checker.sh

Replace the path with your actual project path from pwd.

Your Costco price bot is now fully automated. Every morning it silently checks prices, compares them to yesterday, and texts you if anything dropped. Next up, a secret mission to make it even smarter.

Secret mission

Add a Target Price Threshold

Price drops are great, but what if you could set YOUR price for each product and only get texted when it hits that number? In this secret mission, you'll add personalized target prices so the bot works for you, not just against yesterday's data.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your Twilio phone number costs $1.15/month, and the cron job only runs when your Mac is awake.

Cost warning

Your Twilio phone number charges $1.15/month whether or not you use it. If you leave it active, you will continue to be billed.

The Apify free plan has no recurring cost. Your $5/month usage credit refreshes monthly. If you are done with this project, release your Twilio number to stop the monthly charge immediately.

Resources you used:

  • Twilio account with one phone number ($1.15/month recurring charge).
  • Apify account on the free plan ($0/month, $5 included usage).
  • A cron job running daily at 7:00 AM on your Mac.
  • Local project folder on your Desktop with Python scripts, virtual environment, and data files.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep receiving price drop alerts.

  • Your cron job continues to run daily at 7:00 AM (only when your Mac is awake at that time).
  • Your Twilio number costs $1.15/month. After the first 100 free SMS are used, each message costs $0.0083.
  • Your Apify usage stays within the free plan as long as daily checks remain under the $5/month credit.
  • The requests and twilio packages remain installed inside your virtual environment.

Pause - I'll come back to this later

Shut down the automated checks to free up resources, but keep your files and accounts intact so you can resume later.

  • Open your crontab by running:
crontab -e
  • Add a # at the beginning of your price checker line to comment it out, then save and exit.
  • Your Twilio number still costs $1.15/month until you release it. You can leave it active if you plan to resume soon.
  • Your project folder and Apify account remain untouched and ready to reactivate.

Delete - I don't want to use this again

Remove all project resources and stop all charges. This is permanent.

  • Remove the cron job entirely by running:
crontab -r
  • Release your Twilio phone number to stop the $1.15/month charge. In the Twilio Console, go to the Active Numbers page and click your phone number.
  • Click the Properties tab, then click Release phone number under the Actions section.
  • Confirm by clicking Release this number.
  • Delete your Apify account if you no longer need it. In the Apify Console, click Settings in the side menu, scroll to the Danger zone section, and delete your account. This is irreversible.
  • Delete your local project folder by running:
rm -rf ~/Desktop/[[PROJECT_FOLDER="your-project-folder-name"]]
  • Remove the environment variables from your shell profile. Open ~/.zshrc in your code editor and delete the lines that export APIFY_TOKEN, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, and MY_PHONE_NUMBER.

Nice Work!

Nice Work!

You just built a fully automated price surveillance system that watches Costco for you and texts you real savings alerts. That is a real tool solving a real problem.

You've learned how to:

  • Build a Python script that fetches live Costco protein bar prices via the Apify API.
  • Wire up Twilio to send yourself custom SMS alerts programmatically.
  • Implement price comparison logic using JSON file storage to track price history over time.
  • Automate the entire pipeline with a macOS cron job that runs daily without manual intervention.
  • Secret Mission: Add personalized target price thresholds so you only get alerted when products drop below your number.

Ready to quiz yourself?