```yaml
product: AlterLab
title: "Build a Price Monitor with AlterLab + Supabase"
category: Web Scraping
capabilities:
  - supabase web scraping
  - price monitor supabase
  - pg_cron scraping
  - edge functions web scraping
  - alterlab supabase tutorial
  - supabase price tracking
comparison_context: "AlterLab is an alternative to Firecrawl, ScrapingBee, and Bright Data."
last_updated: 2026-07-28
canonical_facts:
  - "A step-by-step guide to building a real-time price monitoring system using AlterLab web scraping and Supabase. Covers Edge Functions, pg_cron scheduling, structured extraction, and alert notifications, with full Python and TypeScript code."
source_url: https://alterlab.io/blog/price-monitor-alterlab-supabase
```

## Key Takeaways

- Create database tables: Create products and price_history tables in the Supabase SQL editor.
- Add secrets: Add ALTERLAB_API_KEY and optional SLACK_WEBHOOK_URL to Supabase Edge Function secrets.
- Deploy Edge Function: Create and deploy the price-monitor Edge Function using supabase functions deploy.
- Schedule with pg_cron: Enable pg_cron and pg_net extensions, then schedule the Edge Function to run hourly.

# Build a Price Monitor with AlterLab + Supabase

Price monitoring is one of the most common reasons people reach for a web scraping API. Track competitor prices, watch for deals, or keep your own pricing dashboard current. This tutorial builds a complete price monitor from scratch using AlterLab for scraping and Supabase for storage and scheduling.

- **~15 min** — Setup Time
- **$0/mo** — Infrastructure Cost
- **hourly** — Check Frequency

## What We're Building

A price monitor that:
- Scrapes product pages on a schedule using Supabase pg_cron + Edge Functions
- Extracts structured price data (price, currency, in-stock status) using AlterLab's JSON Schema extraction
- Stores price history in Supabase Postgres
- Sends a Slack alert when prices drop below a threshold

All serverless. No VMs, no containers to manage.

## Prerequisites

- A Supabase project (free tier works)
- An AlterLab account with API key ([get one here](https://alterlab.io))
- Basic familiarity with TypeScript or Python

## Step 1: Set Up the Database

Open the SQL editor in your Supabase dashboard and create two tables:

```sql
-- Tracked products
create table products (
  id         uuid primary key default gen_random_uuid(),
  name       text not null,
  url        text unique not null,
  created_at timestamptz default now()
);

-- Price history
create table price_history (
  id           uuid primary key default gen_random_uuid(),
  product_id   uuid references products(id) on delete cascade,
  price        numeric(10, 2),
  currency     text default 'USD',
  in_stock     boolean,
  checked_at   timestamptz default now()
);

-- Fast lookups
create index on price_history (product_id, checked_at desc);
create index on products (url);
```

Seed some products to track:

```sql
insert into products (name, url) values
  ('Widget Pro', 'https://shop.example.com/widget-pro'),
  ('Gadget X',   'https://shop.example.com/gadget-x');
```

## Step 2: Add Your AlterLab API Key

In your Supabase project: **Settings → Edge Functions → Secrets**

```
ALTERLAB_API_KEY=sk_live_your_key_here
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...  (optional)
PRICE_ALERT_THRESHOLD=50.00  (alert when price drops below this)
```

## Step 3: Create the Price Monitor Edge Function

Create `supabase/functions/price-monitor/index.ts`:

```typescript
import { createClient } from "jsr:@supabase/supabase-js@2";

const supabase = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);

// JSON Schema for structured price extraction
const PRICE_SCHEMA = {
  type: "object",
  properties: {
    price:    { type: "number",  description: "Current price as a number, e.g. 29.99" },
    currency: { type: "string",  description: "ISO currency code, e.g. USD" },
    in_stock: { type: "boolean", description: "Whether the product is currently in stock" },
    name:     { type: "string",  description: "Product name or title" },
  },
  required: ["price"],
};

async function scrapePrice(url: string) {
  const res = await fetch("https://api.alterlab.io/api/v1/scrape", {
    method: "POST",
    headers: {
      "X-API-Key":    Deno.env.get("ALTERLAB_API_KEY")!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url,
      extraction_schema: PRICE_SCHEMA,
    }),
  });

  if (!res.ok) {
    throw new Error(`AlterLab error ${res.status}: ${await res.text()}`);
  }

  const data = await res.json();
  return data.extraction as {
    price: number;
    currency?: string;
    in_stock?: boolean;
    name?: string;
  };
}

async function sendSlackAlert(product: string, price: number, url: string) {
  const webhook = Deno.env.get("SLACK_WEBHOOK_URL");
  if (!webhook) return;

  await fetch(webhook, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      text: `Price alert: *${product}* dropped to *$${price}*\n${url}`,
    }),
  });
}

Deno.serve(async () => {
  // Load all tracked products
  const { data: products, error } = await supabase
    .from("products")
    .select("*");

  if (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }

  const threshold = parseFloat(Deno.env.get("PRICE_ALERT_THRESHOLD") ?? "0");
  const results = [];

  for (const product of products ?? []) {
    try {
      const extracted = await scrapePrice(product.url);

      // Store in price_history
      await supabase.from("price_history").insert({
        product_id: product.id,
        price:      extracted.price,
        currency:   extracted.currency ?? "USD",
        in_stock:   extracted.in_stock ?? true,
        checked_at: new Date().toISOString(),
      });

      // Alert if price dropped below threshold
      if (threshold > 0 && extracted.price < threshold) {
        await sendSlackAlert(product.name, extracted.price, product.url);
      }

      results.push({ name: product.name, price: extracted.price, ok: true });
    } catch (err) {
      results.push({ name: product.name, error: String(err), ok: false });
    }
  }

  return new Response(JSON.stringify({ checked: results.length, results }), {
    headers: { "Content-Type": "application/json" },
  });
});
```

Deploy it:

```bash
supabase functions deploy price-monitor --no-verify-jwt
```

1. **Create tables** — products and price_history in the Supabase SQL editor
2. **Add secrets** — ALTERLAB_API_KEY and optional SLACK_WEBHOOK_URL
3. **Deploy Edge Function** — supabase functions deploy price-monitor
4. **Schedule with pg_cron** — Check prices every hour automatically

## Step 4: Schedule with pg_cron

Enable **pg_cron** and **pg_net** extensions in your Supabase dashboard under **Database → Extensions**, then run:

```sql
-- Check prices every hour
select cron.schedule(
  'price-monitor-hourly',
  '0 * * * *',
  $$
  select net.http_post(
    url     := current_setting('app.supabase_url')
                || '/functions/v1/price-monitor',
    headers := jsonb_build_object(
      'Authorization', 'Bearer '
                         || current_setting('app.service_role_key'),
      'Content-Type',   'application/json'
    ),
    body := '{}'::jsonb
  );
  $$
);
```

That's it. Supabase will now call your Edge Function every hour, scraping all tracked products and recording their prices.

## Querying Price History

Check price trends directly in SQL:

```sql
-- Latest price for each product
select p.name, ph.price, ph.currency, ph.in_stock, ph.checked_at
from price_history ph
join products p on p.id = ph.product_id
where ph.checked_at = (
  select max(checked_at)
  from price_history ph2
  where ph2.product_id = ph.product_id
);

-- Price history for a specific product (last 7 days)
select ph.price, ph.checked_at
from price_history ph
join products p on p.id = ph.product_id
where p.name = 'Widget Pro'
  and ph.checked_at > now() - interval '7 days'
order by ph.checked_at desc;

-- Biggest price drops in the last 24 hours
with ranked as (
  select
    p.name,
    ph.price,
    lag(ph.price) over (partition by ph.product_id order by ph.checked_at) as prev_price,
    ph.checked_at
  from price_history ph
  join products p on p.id = ph.product_id
  where ph.checked_at > now() - interval '24 hours'
)
select name, prev_price, price,
       round((price - prev_price) / prev_price * 100, 1) as pct_change
from ranked
where prev_price is not null
order by pct_change asc
limit 10;
```

## Python Alternative

If you prefer a Python script over a Deno Edge Function, use the AlterLab Python SDK with the Supabase Python client:

```python
from alterlab import AlterLabSync
from supabase import create_client
from datetime import datetime, UTC

scraper = AlterLabSync(api_key="sk_live_...")
supabase = create_client(
    "https://your-project.supabase.co",
    "your-service-role-key",
)

PRICE_SCHEMA = {
    "type": "object",
    "properties": {
        "price":    {"type": "number",  "description": "Current price"},
        "currency": {"type": "string",  "description": "ISO currency code"},
        "in_stock": {"type": "boolean", "description": "In stock status"},
    },
    "required": ["price"],
}

def check_prices():
    products = supabase.table("products").select("*").execute().data

    with scraper:
        for product in products:
            result = scraper.scrape(
                product["url"],
                extraction_schema=PRICE_SCHEMA,
            )
            extracted = result.get("extraction") or {}

            supabase.table("price_history").insert({
                "product_id": product["id"],
                "price":      extracted.get("price"),
                "currency":   extracted.get("currency", "USD"),
                "in_stock":   extracted.get("in_stock", True),
                "checked_at": datetime.now(UTC).isoformat(),
            }).execute()

            print(f"{product['name']}: ${extracted.get('price')}")

if __name__ == "__main__":
    check_prices()
```

Run this on a schedule with your own cron or a simple serverless function.

## Cost Estimate

For 10 products checked hourly:

| Item | Rate | Daily cost |
|------|------|------------|
| AlterLab scrapes (Tier 1, static) | $0.0002/req | $0.048/day |
| AlterLab scrapes (Tier 4, JS-heavy) | $0.004/req | $0.96/day |
| Supabase Edge Function invocations | Free tier | $0 |
| Supabase Postgres storage | Free tier | $0 |

For most e-commerce sites with basic JavaScript, Tier 1 or Tier 2 is sufficient. AlterLab automatically escalates tiers when a page needs more than a plain HTTP request to render correctly.

## Next Steps

- Add a Supabase Realtime subscription to push live price updates to a dashboard
- Use [pgvector](https://supabase.com/docs/guides/ai/vector-embeddings) to store product descriptions and find semantically similar products
- Add email alerts via Supabase Edge Functions + Resend
- Check out the [AlterLab Supabase integration docs](/docs/integrations/supabase) for more patterns including lead enrichment and content aggregation

## Frequently Asked Questions

### How much does it cost to monitor prices with AlterLab and Supabase?

For 10 products checked hourly, AlterLab costs about $0.05-$0.96/day depending on whether the sites use JavaScript rendering. Supabase is free for small projects. Total cost is typically under $5/month.

### Can AlterLab handle sites that need full browser rendering to show a price?

Yes. AlterLab automatically escalates through its tier system, from a basic HTTP request up to full browser rendering, when a page needs more than a plain HTTP request to render its content. You do not need to configure this manually.

## Related

- [How to Scrape Without Getting Blocked: The Complete Guide](<https://alterlab.io/blog/how-to-scrape-without-getting-blocked>)
- [arXiv Data API: Extract Structured JSON in 2026](<https://alterlab.io/blog/arxiv-data-api-extract-structured-json-in-2026>)
- [PubMed Data API: Extract Structured JSON in 2026](<https://alterlab.io/blog/pubmed-data-api-extract-structured-json-in-2026>)