Overview
The Price Tracker Bot uses a file-based JSON storage system with in-memory caching. All data is stored in a single file (prices.json) located in the bot’s working directory.
Trade-offs: File-based storage is simple and requires no external dependencies, but may not scale well beyond ~1000 products or multiple concurrent bot instances.
Storage Architecture
In-Memory State
Two global variables maintain runtime state:index.mjs:21-22
Persistence Functions
loadData()
Loads data from disk on bot startup:
index.mjs:24-39
Error handling:
- If file doesn’t exist, initializes empty state
- If JSON parsing fails, resets to empty state
- Logs success/failure status
loadData() is called once at startup (index.mjs:50). Data is never reloaded during runtime.saveData()
Persists current state to disk:
index.mjs:41-48
Characteristics:
- Synchronous write (blocks until complete)
- Pretty-printed JSON (2-space indentation)
- Converts
Setto array for serialization - Silently fails on error (only logs)
File Structure
Root Schema
Theprices.json file has two top-level keys:
| Field | Type | Description |
|---|---|---|
products | Object | Map of sanitized URLs to product objects |
chats | Array | List of active Telegram chat IDs (numbers) |
Product Object Schema
Each entry in theproducts object has this structure:
Field Definitions
url (string)
url (string)
Sanitized Amazon product URL (no query parameters or hash fragments)
- Generated by
sanitizeAmazonURL()(index.mjs:53-61) - Used as the object key in
priceData - Example:
https://amazon.com/dp/B08N5WRWNW
title (string)
title (string)
Product title extracted from Amazon page
- Scraped from multiple selectors (see Web Scraping)
- Displayed in notifications and product lists
- Example:
"Sony WH-1000XM4 Wireless Headphones"
price (number)
price (number)
Current price in USD (or currency detected from page)
- Parsed from price text by removing non-numeric characters
- Updated during each price check
- Used for price drop detection
- Example:
278.00
lowestPrice (number)
lowestPrice (number)
Lowest price ever seen for this product
- Initialized to first scraped price
- Updated when
scraped.price < stored.lowestPrice - Used in notifications to show historical context
- Example:
248.00
index.mjs:190):imageUrl (string | null)
imageUrl (string | null)
Product image URL from Amazon
- Scraped from multiple image selectors
- May be
nullif no image found - Used in Telegram messages with
bot.sendPhoto() - Example:
"https://m.media-amazon.com/images/I/71o8Q5XJS5L._AC_SL1500_.jpg"
lastChecked (string)
lastChecked (string)
ISO 8601 timestamp of last price check
- Updated even if scraping fails
- Used to display “last updated” information
- Format:
YYYY-MM-DDTHH:mm:ss.sssZ - Example:
"2026-03-10T15:30:45.123Z"
index.mjs:187):addedDate (string)
addedDate (string)
ISO 8601 timestamp when product was added
- Set once when product is first tracked
- Preserved during URL edits
- Used in product details view
- Example:
"2026-03-01T10:20:15.000Z"
addedBy (number | null)
addedBy (number | null)
Telegram chat ID of user who added the product
- Used for attribution (not currently displayed)
- May be
nullfor legacy products - Example:
123456789
history (array)
history (array)
Array of price history entries (max 120)Limit enforcement (
- Each entry:
{ date: string, price: number } - Appended on every price check (even if price unchanged)
- Trimmed to last 120 entries when limit exceeded
- Used for chart generation
index.mjs:196):Data Operations
Creating a Product
When a user runs/add [url], a new product object is created:
index.mjs:427-439
The initial
history array contains a single entry with the scraped price.Updating a Product
During price checks, products are updated in place:index.mjs:182-218
Key behaviors:
- Preserves
addedDateandaddedByfrom previous version - Falls back to stored
imageUrlif scraper doesn’t find one - Always appends to
history(even if price unchanged) - Recalculates
lowestPriceon every update
Deleting a Product
Products are removed using JavaScript’sdelete operator:
index.mjs:527, index.mjs:536, index.mjs:667
When deleting all products (
delete_all callback), the entire priceData object is replaced:Editing a Product URL
The/edit command creates a new entry and deletes the old one:
index.mjs:578-596
Chat Tracking
Thechats Set stores Telegram chat IDs for notification delivery:
index.mjs:22
Registration
Chats are registered automatically in two ways:- On
/startcommand (index.mjs:361-364):
- On any message (
index.mjs:705-711):
Usage
Registered chats receive notifications during price checks:index.mjs:223-244
There is currently no
/unsubscribe command. Once registered, a chat continues receiving notifications indefinitely.History Management
History Limit
TheHISTORY_LIMIT constant controls how many price points are retained:
index.mjs:69
Trimming Logic
When appending history, older entries are removed if limit exceeded:index.mjs:195-196
Array.slice(-HISTORY_LIMIT) keeps only the last 120 entries, discarding older ones.Chart Generation
History data is sorted chronologically before charting:index.mjs:317-320
Data Integrity
Sanitization on Keys
All URLs used as keys are sanitized to prevent duplicates:https://amazon.com/dp/B08N5WRWNW?tag=xyzhttps://amazon.com/dp/B08N5WRWNW#reviewshttps://amazon.com/dp/B08N5WRWNW
Fallback Values
The code defensively handles missing fields:Array Cloning
Thehistory array is cloned to prevent mutations:
Migration Considerations
Adding New Fields
New fields can be added with default values:Changing Schema
For breaking changes, implement a migration function:Backup Strategy
For production deployments, implement periodic backups:Performance Analysis
File Size Estimation
With 120 history entries per product:JSON parsing performance degrades around 10-50 MB depending on hardware. Consider migrating to a database beyond 1000 products.
Write Frequency
Data is written to disk on:- Every
/addcommand - Every
/removecommand - Every
/editcommand - Every price check cycle (every 2 hours)
- Every chat registration
- Every
delete_alloperation
Optimization Opportunities
- Lazy writes: Buffer changes and write every N seconds
- Incremental saves: Use append-only logs (event sourcing)
- Database migration: PostgreSQL, MongoDB, or Redis
- Compression: gzip the JSON file
- Indexing: Add secondary indices for faster lookups
Example Data File
A completeprices.json example: