# Create a card for each product
for product in products:
    price_history = db.get_price_history(product.url)
    if price_history:
        ...
        # Create a card-like container for each product
        with st.expander(df["name"][0], expanded=False):
            st.markdown("---")
            col1, col2 = st.columns([1, 3])

            with col1:
                if price_history[0].main_image_url:
                    st.image(price_history[0].main_image_url, width=200)
                st.metric(
                    label="Current Price",
                    value=f"{price_history[0].price} {price_history[0].currency}",
                )

```

The expander shows the product name as its title and contains:

1. A divider line
2. Two columns:
   - Left column: Product image (if available) and current price metric
   - Right column (shown in next section)

The price is displayed using Streamlit’s metric component which shows the current price and currency.

Here is the rest of the code:

```python
            ...

            with col2:
                # Create price history plot
                fig = px.line(
                    df,
                    x="timestamp",
                    y="price",
                    title=None,
                )
                fig.update_layout(
                    xaxis_title=None,
                    yaxis_title="Price",
                    showlegend=False,
                    margin=dict(l=0, r=0, t=0, b=0),
                    height=300,
                )
                fig.update_xaxes(tickformat="%Y-%m-%d %H:%M", tickangle=45)
                fig.update_yaxes(tickprefix=f"{price_history[0].currency} ", tickformat=".2f")
                st.plotly_chart(fig, use_container_width=True)

```

In the right column, we create an interactive line plot using Plotly Express to visualize the price history over time. The plot shows price on the y-axis and timestamp on the x-axis. The layout is customized to remove the title, adjust axis labels and formatting, and optimize the display size. The timestamps are formatted to show date and time, with angled labels for better readability. Prices are displayed with 2 decimal places and a dollar sign prefix. The plot is rendered using Streamlit’s `plotly_chart` component and automatically adjusts its width to fill the container.

After this step, the UI must be fully functional and ready to track products. For example, here is what mine looks like after adding a couple of products:

![Screenshot of a price tracking dashboard showing multiple product listings with price history charts, product images, and current prices for Amazon items](https://www.firecrawl.dev/images/blog/price-tracking/finished.png)

But notice how the price history chart doesn’t show anything. That’s because we haven’t populated it by checking the product price in regular intervals. Let’s do that in the next couple of steps. For now, commit the latest changes we’ve made:

```bash
git add .
git commit -m "Display product price histories for each product in the dashboard"

```

* * *

Let’s take a brief moment to summarize the steps we took so far and what’s next. So far, we’ve built a Streamlit interface that allows users to add product URLs and displays their current prices and basic information. We’ve implemented the database schema, created functions to scrape product data, and designed a clean UI with price history visualization. The next step is to set up automated price checking to populate our history charts and enable proper price tracking over time.

### Step 8: Adding new price entries for existing products

Now, we want to write a script that adds new price entries in the `price_histories` table for each product in `products` table. We call this script `check_prices.py`:

```python
import os
from database import Database
from dotenv import load_dotenv
from firecrawl import FirecrawlApp
from scraper import scrape_product

load_dotenv()

db = Database(os.getenv("POSTGRES_URL"))
app = FirecrawlApp()

```

At the top, we are importing the functions and packages and initializing the database and a Firecrawl app. Then, we define a simple `check_prices` function:

```python
def check_prices():
    products = db.get_all_products()

    for product in products:
        try:
            updated_product = scrape_product(product.url)
            db.add_price(updated_product)
            print(f"Added new price entry for {updated_product['name']}")
        except Exception as e:
            print(f"Error processing {product.url}: {e}")

if __name__ == "__main__":
    check_prices()

```

In the function body, we retrieve all products URLs, retrieve their new price data with `scrape_product` function from `scraper.py` and then, add a new price entry for the product with `db.add_price`.

If you run the function once and refresh the Streamlit app, you must see a line chart appear for each product you are tracking:

![Screenshot of a price tracking dashboard showing a line chart visualization of product price history over time, with price on the y-axis and dates on the x-axis](https://www.firecrawl.dev/images/blog/price-tracking/linechart.png)

Let’s commit the changes in this step:

```bash
git add .
git commit -m "Add a script for checking prices of existing products"

```

### Step 9: Check prices regularly with GitHub actions

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate various software workflows directly from your GitHub repository. In our case, it’s particularly useful because we can set up automated price checks to run the `check_prices.py` script at regular intervals (e.g., daily or hourly) without manual intervention. This ensures we consistently track price changes and maintain an up-to-date database of historical prices for our tracked products.

So, the first step is creating a new GitHub repository for our project and pushing existing code to it:

```bash
git remote add origin https://github.com/yourusername/price-tracker.git
git push origin main

```

Then, return to your terminal and create this directory structure:

```bash
mkdir -p .github/workflows
touch .github/workflows/check_prices.yml

```

The first command creates a new directory structure `.github/workflows` using the `-p` flag to create parent directories if they don’t exist.

The second command creates an empty YAML file called `check_prices.yml` inside the workflows directory. GitHub Actions looks for workflow files in this specific location - any YAML files in the `.github/workflows` directory will be automatically detected and processed as workflow configurations. These YAML files define when and how your automated tasks should run, what environment they need, and what commands to execute. In our case, this file will contain instructions for GitHub Actions to periodically run our price checking script. Let’s write it:

```yaml
name: Price Check

on:
  schedule:
    # Runs every 3 minutes
    - cron: "*/3 * * * *"
  workflow_dispatch: # Allows manual triggering

```

Let’s break down this first part of the YAML file:

The `name: Price Check` line gives our workflow a descriptive name that will appear in the GitHub Actions interface.

The `on:` section defines when this workflow should be triggered. We’ve configured two triggers:

1. A schedule using cron syntax `*/3 * * * *` which runs the workflow every 3 minutes. The five asterisks represent minute, hour, day of month, month, and day of week respectively. The `*/3` means “every 3rd minute”. The 3-minute interval is for debugging purposes, we will need to choose a wider interval later on to respect the free limits of GitHub actions.

2. `workflow_dispatch` enables manual triggering of the workflow through the GitHub Actions UI, which is useful for testing or running the check on-demand.


Now, let’s add the rest:

```yaml
jobs:
  check-prices:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.10"
          cache: "pip"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run price checker
        env:
          FIRECRAWL_API_KEY: ${{ secrets.FIRECRAWL_API_KEY }}
          POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
        run: python check_prices.py

```

Let’s break down this second part of the YAML file:

The `jobs:` section defines the actual work to be performed. We have one job named `check-prices` that runs on an Ubuntu virtual machine ( `runs-on: ubuntu-latest`).

Under `steps:`, we define the sequence of actions:

1. First, we checkout our repository code using the standard `actions/checkout@v4` action

2. Then we set up Python 3.10 using `actions/setup-python@v5`, enabling pip caching to speed up dependency installation

3. Next, we install our Python dependencies by upgrading `pip` and installing requirements from our `requirements.txt` file. At this point, it is essential that you were keeping a complete dependency file based on the installs we made in the project.

4. Finally, we run our price checker script, providing two environment variables:
   - `FIRECRAWL_API_KEY`: For accessing the web scraping service
   - `POSTGRES_URL`: For connecting to our database

Both variables must be stored in our GitHub repository as secrets for this workflow file to run without errors. So, navigate to the repository you’ve created for the project and open its Settings. Under “Secrets and variables” > “Actions”, click on “New repository secret” button to add the environment variables we have in the `.env` file one-by-one.

Then, return to your terminal, commit the changes and push:

```bash
git add .
git commit -m "Add a workflow to check prices regularly"
git push origin main

```

Next, navigate to your GitHub repository again and click on the “Actions” tab:

![Screenshot of GitHub Actions interface showing workflow runs and manual trigger button for automated price tracking application](https://www.firecrawl.dev/images/blog/price-tracking/actions.png)

From there, you can run the workflow manually (click “Run workflow” and refresh the page). If it is executed successfully, you can return to the Streamlit app and refresh to see the new price added to the chart.

### Step 10: Setting up Discord for notifications

Now that we know our scheduling workflow works, the first order of business is setting a wider check interval in the workflow file. Even though our first workflow run was manually, the rest happen automatically.

```bash
on:
  schedule:
    # Runs every 6 hours
    - cron: "0 0,6,12,18 * * *"
  workflow_dispatch: # Allows manual triggering

```

The cron syntax `0 0,6,12,18 * * *` can be broken down as follows:

- First `0`: Run at minute 0
- `0,6,12,18`: Run at hours 0 (midnight), 6 AM, 12 PM (noon), and 6 PM
- First `*`: Run every day of the month
- Second `*`: Run every month
- Third `*`: Run every day of the week

So this schedule will check prices four times daily: at midnight, 6 AM, noon, and 6 PM (UTC time). This spacing helps stay within GitHub Actions’ free tier limits while still catching most price changes.

Now, commit and push the changes:

```bash
git add .
git commit -m "Set a wider check interval in the workflow file"
git push origin main

```

Now comes the interesting part. Each time the workflow is run, we want to compare the current price of the product to its original price when we started tracking it. If the difference between these two prices exceeds a certain threshold like 5%, this means there is a discount happening for the product and we want to send a notification.

The easiest way to set this up is by using Discord webhooks. So, if you don’t have one already, go to Discord.com and create a new account (optionally, download the desktop app as well). Then, setting up Discord notifications requires a few careful steps:

1. **Create a discord server**
   - Click the ”+” button in the bottom-left corner of Discord
   - Choose “Create My Own” → “For me and my friends”
   - Give your server a name (e.g., “Price Alerts”)
2. **Create a channel for alerts**
   - Your server comes with a #general channel by default
   - You can use this or create a new channel called #price-alerts
   - Right-click the channel you want to use
3. **Set up the webhook**
   - Select “Edit Channel” from the right-click menu
   - Go to the “Integrations” tab
   - Click “Create Webhook”
   - Give it a name like “Price Alert Bot”
   - The webhook URL will be generated automatically
   - Click “Copy Webhook URL” - this is your unique notification endpoint
4. **Secure the webhook URL**
   - Never share or commit your webhook URL directly
   - Add it to your `.env` file as `DISCORD_WEBHOOK_URL`
   - Add it to your GitHub repository secrets
   - The URL should look something like: `https://discord.com/api/webhooks/...`

This webhook will serve as a secure endpoint that our price tracker can use to send notifications directly to your Discord channel.

Webhooks are automated messages sent from apps to other apps in real-time. They work like a notification system - when something happens in one app, it automatically sends data to another app through a unique URL. In our case, we’ll use Discord webhooks to automatically notify us when there’s a price drop. Whenever our price tracking script detects a significant discount, it will send a message to our Discord channel through the webhook URL, ensuring we never miss a good deal.

After copying the webhook URL, you should save it as environment variable to your `.env` file:

```python
echo "DISCORD_WEBHOOK_URL='THE-URL-YOU-COPIED'" >> .env

```

Now, create a new file called `notifications.py` and paste the following contents:

```python
from dotenv import load_dotenv
import os
import aiohttp
import asyncio

load_dotenv()

async def send_price_alert(
    product_name: str, old_price: float, new_price: float, url: str
):
    """Send a price drop alert to Discord"""
    drop_percentage = ((old_price - new_price) / old_price) * 100

    message = {
        "embeds": [\
            {\
                "title": "Price Drop Alert! 🎉",\
                "description": f"**{product_name}**\nPrice dropped by {drop_percentage:.1f}%!\n"\
                f"Old price: ${old_price:.2f}\n"\
                f"New price: ${new_price:.2f}\n"\
                f"[View Product]({url})",\
                "color": 3066993,\
            }\
        ]
    }

    try:
        async with aiohttp.ClientSession() as session:
            await session.post(os.getenv("DISCORD_WEBHOOK_URL"), json=message)
    except Exception as e:
        print(f"Error sending Discord notification: {e}")

```

The `send_price_alert` function above is responsible for sending price drop notifications to Discord using webhooks. Let’s break down what’s new:

1. The function takes 4 parameters:
   - `product_name`: The name of the product that dropped in price
   - `old_price`: The previous price before the drop
   - `new_price`: The current lower price
   - `url`: Link to view the product
2. It calculates the percentage drop in price using the formula: `((old_price - new_price) / old_price) * 100`

3. The notification is formatted as a Discord embed - a rich message format that includes:
   - A title with a celebration emoji
   - A description showing the product name, price drop percentage, old and new prices
   - A link to view the product
   - A green color (3066993 in decimal)
4. The message is sent asynchronously using `aiohttp` to post to the Discord webhook URL stored in the environment variables

5. Error handling is included to catch and print any issues that occur during the HTTP request


This provides a clean way to notify users through Discord whenever we detect a price drop for tracked products.

To check the notification system works, add this main block to the end of the script:

```python
if __name__ == "__main__":
    asyncio.run(send_price_alert("Test Product", 100, 90, "https://www.google.com"))

```

`asyncio.run()` is used here because `send_price_alert` is an async function that needs to be executed in an event loop. `asyncio.run()` creates and manages this event loop, allowing the async HTTP request to be made properly. Without it, we wouldn’t be able to use the `await` keyword inside `send_price_alert`.

To run the script, install `aiohttp`:

```python
pip install aiohttp
echo "aiohttp\n" >> requirements.txt
python notifications.py

```

If all is well, you should get a Discord message in your server that looks like this:

![Screenshot of a Discord notification showing a price drop alert with product details, original price, new discounted price and percentage savings](https://www.firecrawl.dev/images/blog/price-tracking/alert.png)

Let’s commit the changes we have:

```bash
git add .
git commit -m "Set up Discord alert system"

```

Also, don’t forget to add the Discord webhook URL to your GitHub repository secrets!

### Step 11: Sending Discord alerts when prices drop

Now, the only step left is adding a price comparison logic to `check_prices.py`. In other words, we want to use the `send_price_alert` function if the new scraped price is lower than the original. This requires a revamped `check_prices.py` script:

```python
import os
import asyncio
from database import Database
from dotenv import load_dotenv
from firecrawl import FirecrawlApp
from scraper import scrape_product
from notifications import send_price_alert

load_dotenv()

db = Database(os.getenv("POSTGRES_URL"))
app = FirecrawlApp()