No description
  • Python 99.7%
  • Shell 0.2%
  • Batchfile 0.1%
Find a file
Andrew b7b6be7754 Raise BFMR request timeout to 90s
BFMR's API now routinely takes 20-30s per page (measured live), so the
15s timeout made nearly every poll fail with a request timeout.
2026-09-09 15:05:35 -07:00
groups Raise BFMR request timeout to 90s 2026-09-09 15:05:35 -07:00
.env.example Now able to disable buyinggroups 2026-08-04 08:25:30 -07:00
.gitignore Added @everyone 2026-08-03 18:18:34 -07:00
discord_notifier.py MOD payout-only embed with Discord channel link; fix Discord 400 spam 2026-08-22 01:40:02 -07:00
LICENSE Fixed json and added license 2026-08-04 00:21:56 -07:00
logger.py MOD payout-only embed with Discord channel link; fix Discord 400 spam 2026-08-22 01:40:02 -07:00
main.py MOD payout-only embed with Discord channel link; fix Discord 400 spam 2026-08-22 01:40:02 -07:00
README.md MOD payout-only embed with Discord channel link; fix Discord 400 spam 2026-08-22 01:40:02 -07:00
requirements.txt first commit 2026-08-03 17:42:34 -07:00
run.bat Added persistence and updated README and fixed MOD seen 2026-08-03 17:53:27 -07:00
run.sh Added persistence and updated README and fixed MOD seen 2026-08-03 17:53:27 -07:00
state.py first commit 2026-08-03 17:42:34 -07:00
test_script.py MOD payout-only embed with Discord channel link; fix Discord 400 spam 2026-08-22 01:40:02 -07:00

BuyingGroupAlerts

Polls buying-group deal APIs and posts new deals to Discord as they appear. Currently supports:

  • BFMR (groups/bfmr/)
  • MaxOutDeals (groups/maxoutdeals/)

Each group runs its own poll loop on its own thread with its own interval and its own Discord webhook, so groups are fully independent of each other.

Setup

Windows:

python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
copy .env.example .env

Linux / macOS:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env

Fill in .env with your credentials (see Configuration below).

Running

python main.py

On first run for each group, it records the currently active deals as a baseline and sends no alerts (so you don't get spammed with everything that's already live). From then on, only genuinely new deals trigger an alert.

State is persisted per group in seen_<group>.json at the project root (gitignored).

Running persistently (survives reboots)

main.py runs forever in the foreground (one thread per group, looping on its own interval), so to keep it alive across restarts you need to hand it to the OS's own persistence mechanism instead of just leaving a terminal open.

Two wrapper scripts are included so these all cd to the project folder first, regardless of what directory they're invoked from:

  • run.bat (Windows) — resolves its own location, then runs venv\Scripts\python.exe main.py.
  • run.sh (Linux/macOS) — resolves its own location, then runs venv/bin/python main.py. Make it executable once: chmod +x run.sh.

Windows: Task Scheduler

Run once from an elevated (Admin) prompt to register a task that starts run.bat whenever you log in:

schtasks /create /tn "BuyingGroupAlerts" /tr "C:\path\to\BuyingGroupAlerts\run.bat" /sc onlogon /rl highest
  • Adjust the path to wherever you cloned the repo.
  • /sc onlogon starts it at login under your own account — no stored password needed. If you want it to start even when nobody's logged in, use /sc onstart instead, which will prompt for your account password (/ru//rp) since it needs to run outside a login session.

Useful follow-ups:

schtasks /run /tn "BuyingGroupAlerts"      # start it right now, without waiting for next logon
schtasks /query /tn "BuyingGroupAlerts"    # check status
schtasks /delete /tn "BuyingGroupAlerts" /f  # remove the task

Prefer a GUI? Open Task Scheduler → Create Task → set a Trigger of "At log on" → set an Action of "Start a program" pointing at run.bat.

Linux: screen + crontab

screen keeps main.py running in a detached session you can reattach to for logs; a crontab @reboot entry restarts that session automatically after a reboot.

Start it manually in a detached screen session:

screen -dmS buyinggroupalerts ./run.sh

Reattach to see live output, detach again with Ctrl+A then D:

screen -r buyinggroupalerts
screen -ls          # list running sessions

To have it come back automatically after a reboot, add this to crontab -e (use the absolute path, since cron's working directory can't be relied on):

@reboot screen -dmS buyinggroupalerts /path/to/BuyingGroupAlerts/run.sh

Testing

python test_script.py            # offline logic tests + live API/Discord calls
python test_script.py --offline  # offline logic tests only, no network calls

The offline tests cover embed formatting, dedup logic, and (for MOD) row-to-deal grouping, all using synthetic data. The live tests hit both real APIs and send a sample embed to each group's Discord webhook.

MaxOutDeals rate limit: their API only allows one request per 5 minutes. If a live MOD test/poll gets firewall-blocked, that's why — wait and retry rather than hammering it.

Configuration

All config lives in .env (see .env.example for the full template).

Variable Group Description
BFMR_API_KEY, BFMR_API_SECRET BFMR API credentials
BFMR_BASE_URL BFMR API base URL
MOD_USER_ID, MOD_EMAIL, MOD_API_TOKEN MOD API credentials
MOD_BASE_URL MOD API base URL
DISCORD_WEBHOOK_URL both Default webhook; used by BFMR, and by MOD as a fallback
MOD_DISCORD_WEBHOOK_URL MOD Optional separate webhook, so MOD alerts can post to their own channel
POLL_INTERVAL_SECONDS both Default poll interval (seconds); used as a fallback
BFMR_POLL_INTERVAL_SECONDS BFMR Optional override of the default interval
MOD_POLL_INTERVAL_SECONDS MOD Optional override of the default interval
BFMR_ENABLED BFMR Set to false/0/no/off to stop this group from polling entirely. Defaults to enabled.
MOD_ENABLED MOD Same, for MaxOutDeals.

Architecture

Each group is a self-contained package under groups/<name>/ with three files:

  • client.py — talks to the group's API, returns raw deal data.
  • formatter.py — turns a deal into a Discord embed.
  • poller.py — dedup/filtering logic; exposes get_baseline_ids(client) and poll_once(client, seen_ids).

main.py registers each group in a GROUPS dict and runs one thread per group, polling on that group's own interval. discord_notifier.py and state.py are shared across all groups.

BFMR

Deals come back as clean JSON, one deal per API response with nested items and retailer links. poll_once alerts on any deal not yet seen, skipping ones whose reservation is closed.

MaxOutDeals

The API returns CSV, not JSON, with one row per (deal, retailer) pair — the same deal repeats across multiple rows for its different retailers and color/variant options. group_deals() in poller.py collapses these flat rows into deals:

  • A deal is identified by (DEAL, TITLE, BATCH). If MOD reissues the same product under a new BATCH (e.g. a restock), it's treated as a new deal and will alert again.
  • Within a deal, rows are further grouped by COMMIT ITEM, since that's where MOD encodes color/variant — there's no separate structured color field, so it's grouped by exact string match rather than parsed.
  • Each retailer row's COMMIT STORE + COMMIT LINK becomes one entry in that variant's retailer list.
  • A deal is only suppressed as "at capacity" (and skipped) if every retailer row for it is full.
  • "Closing Time" is the earliest COMMIT EXPIRES across all of a deal's retailer rows, since that field varies per retailer rather than being deal-wide.
  • Only TOTAL (the payout) is shown as "Payout Price" — MOD's PRICE/COMMISSION columns aren't reliable, so there's no Retail Price or Commission field, unlike BFMR.
  • There's no single canonical "commit" link for a MOD deal (each retailer has its own), so the embed title isn't a hyperlink. The "Deal Link" field instead points at the official MaxOutDeals Discord: the below-cost channel if FLAGS contains BELOW COST, otherwise the general deals channel. Per-retailer commit links are listed inside each item's Retailers list.

To add a new group, create groups/<name>/ with the same three-file shape, then register it in main.py's GROUPS dict (poll_interval_env should point at a new <NAME>_POLL_INTERVAL_SECONDS env var, and enabled_env at a new <NAME>_ENABLED env var). New groups should also ping @everyone on alerts (via discord_notifier.EVERYONE_PING), matching BFMR and MOD, unless told otherwise.