RedFlagDeals alerts for your Discord server, running as one small Rust service with its own SQLite database. It watches RFD Hot Deals, groups duplicate threads, follows engagement, and updates existing Discord messages as deals change.
The Docker Compose profile caps the bot at 384 MiB, limits CPU/log growth, and clears Gemini credentials so ordinary alerts have no AI API cost. Optional AI can still be configured when running the binary directly or with an explicit Compose override. Free hosting depends on the provider's compute and networking allowances; a small container does not guarantee an indefinitely free VM.
Choose all deals, tech deals, warm/hot deals, or hot deals. Optional Gemini title cleanup is available; ordinary alerts work without an AI account.
Add the hosted bot to your server · Host your own copy · Commands and filters
For GCP, use the Cloud Run deployment profile: small request-driven Rust workers, separate signed-command services and durable Firestore checkpoints. Both scale to zero; the VM/Gateway instructions below remain available. The maintainer-hosted bot now uses this profile; see the live migration record.
The maintainer-hosted rfdSOLO bot can serve multiple Discord servers. You only need Discord and Manage Server or administrator permission in your server; the maintainer runs the service and supplies its credentials.
- Use Add the hosted bot to your server above and select your server. The invite requests View Channels, Send Messages, and Embed Links.
- Create or choose a text channel such as
#rfd-deals. Make sure the bot has those three permissions in that channel, including any private-channel overrides. - Run
/rfd subscribe, select that channel, and choose a filter from the menu, such as Warm + Hot (tech). Use All deals for the broadest feed. - Run
/rfd listto confirm the saved subscription. Matching deals from the currently observed feed arrive on a subsequent poll, normally within a few minutes. A filter with no qualifying deals will stay quiet. The confirmation means the subscription was saved; it does not verify channel permissions.
Subscriptions deliver server channel posts, not personal DMs. Everyone who
can view the channel can read its alerts. For personal notifications, set that
channel's Discord notification preference to All Messages (or All);
alerts do not ping @everyone or individual members. Discord's
notification guide
explains channel overrides and device settings. Members without Manage Server
can ask a server admin to set up the channel.
Use /rfd unsubscribe to stop a channel/filter subscription. Several channels
or servers can subscribe independently. The hosted option depends on the
maintainer's running service; you can also host your own copy.
If installation only adds commands and no bot member, use the invite above: it
explicitly requests both the bot and applications.commands scopes and a server
installation. A commands-only or personal-account installation cannot deliver
these scheduled channel alerts. See Discord's
bot installation flow.
You need Docker with Compose and a Discord application of your own. These steps run an independent bot under your control. The architecture record explains the Rust/native SQLite design, and the release review records the audit findings and validation.
Clone the public project:
git clone https://github.com/pauljones0/rfd-discord-bot.git
cd rfd-discord-bot-
Create an application in the Discord Developer Portal. Copy its Application ID and bot token. Leave Interactions Endpoint URL empty; this bot receives commands through an outbound Gateway connection. No public domain, port forwarding, reverse proxy, or VPN is needed.
-
Under Installation, enable Guild Install with the
botandapplications.commandsscopes. Give the bot View Channels, Send Messages, and Embed Links permissions. Use the install link to add it to your server. If others will install your instance, enable Public Bot on the Bot page and register commands globally (leaveDISCORD_GUILD_IDblank). This bot uses server installation; personal User Install is not needed. Privileged intents can remain disabled. Discord's app setup guide explains the portal settings; this project uses Gateway rather than the HTTP transport in that tutorial. -
Run
python3 scripts/configure_discord.pyto paste your Application ID, optional Public Key, and bot token at the prompts. The helper accepts raw values or labels such asApplication ID: 123456789012345678. Token input is hidden. It writes an owner-only.env, preserves other settings, and can be rerun to update credentials. A public key is saved for reference; Gateway commands need the bot token. You can press Enter to add the token later. Use--env-file /path/to/.envfor a different config location. Alternatively, copy.env.exampleto.envand fill inDISCORD_APP_IDandDISCORD_BOT_TOKENmanually. Optionally setDISCORD_GUILD_IDto register commands immediately in one server. Leave it blank for global commands. -
Build, register the command, and start:
docker compose build docker compose run --rm bot check-config docker compose run --rm bot check-discord docker compose run --rm bot register docker compose up -d
-
In Discord, use
/rfd subscribe, select a channel, and choose a filter. Allow the bot to send messages and embed links in that channel. The next poll normally starts within three minutes. Only users with Manage Server or administrator permission can manage subscriptions.
Use a dedicated application for this standalone deployment. Running it with
another bot process's token can cause competing Gateway sessions and command
ownership problems. Registration upserts only /rfd; it does not bulk-delete
other application commands. See Discord's command documentation.
| Command | What it does |
|---|---|
/rfd subscribe channel filter |
Adds or updates a channel/filter subscription |
/rfd unsubscribe channel [filter] |
Removes one filter, or all RFD filters in that channel |
/rfd list |
Lists this server's subscriptions |
Management replies are private to the person issuing the command. A channel can have several filters; an eligible deal is still posted only once in that channel.
| Filter | Included deals |
|---|---|
| All deals | Every accepted RFD deal |
| Tech only | Accepted deals classified as tech |
| Warm + Hot (all) | Discount-backed deals reaching the warm or hot threshold |
| Warm + Hot (tech) | The same, limited to tech |
| Hot only (all) | Discount-backed deals reaching the hot threshold |
| Hot only (tech) | The same, limited to tech |
Warm/hot status uses the existing RFD engagement rules, with a fallback when RFD does not expose view counts. It is an alert filter, not an assurance that an item is worth buying. Existing messages retain their IDs in SQLite so ordinary restarts do not repost the same deal. With no subscriptions, the bot skips scraping and title-cleaning work.
The supplied Compose file loads .env. A native binary reads environment
variables directly; it does not search other projects for configuration.
| Variable | Default / purpose |
|---|---|
DISCORD_APP_ID |
Required: your application ID |
DISCORD_BOT_TOKEN |
Required: your bot token |
DISCORD_GUILD_ID |
Optional registration scope; blank means global |
RFD_POLL_INTERVAL |
3m, delay between completed polls |
RFD_POLL_TIMEOUT |
4m, time limit for one poll |
DISCORD_UPDATE_INTERVAL |
10m, minimum spacing between edits |
MAX_STORED_DEALS |
2000, retained deal limit |
LOG_LEVEL |
INFO |
SELECTORS_CONFIG_PATH |
Optional JSON selector override; blank uses the embedded configuration |
SQLITE_PATH |
data/rfd.sqlite natively; /data/rfd.sqlite in Compose |
LISTEN_ADDR |
127.0.0.1:8080, local health checks |
GEMINI_API_KEY |
Optional title cleanup; blank disables it |
GEMINI_MODELS |
Required only when enabling Gemini; comma-separated supported model IDs in fallback order |
AMAZON_AFFILIATE_TAG |
Optional; blank removes Amazon affiliate tags |
BESTBUY_AFFILIATE_PREFIX |
Optional HTTPS redirect prefix; blank keeps direct links |
Gemini is used only to shorten deal titles. Categories, popularity thresholds, and discount checks determine which deals qualify for a subscription. To enable cleanup with a lightweight model, set your own key and an explicit model ID:
GEMINI_API_KEY=your-own-key
GEMINI_MODELS=gemini-3.5-flash-liteThis example was verified in September 2026. Check Google's model catalog for availability in your account. Model names are configured explicitly; installing a newer SDK does not automatically switch them. Titles are cleaned in small batches during the same poll and saved even when the source listing is unchanged. Cleanup has a 30-second budget per poll; failed or slow cleanup leaves original titles usable and missing cleanup can retry later. An exhausted model/key configuration pauses cleanup for its saved cooldown instead of sending requests on every poll. Existing Discord message ownership is preserved.
The Cloud Run profile uses a dedicated unbilled Gemini project and one worker-only key instead of the native multi-key/model fallback. It verifies billing/key ownership, reserves a persistent 20-request daily cap, and continues ordinary alerts if optional cleanup fails. Crux and command services receive no Gemini key.
There are no embedded personal affiliate IDs. eBay product links found inside RFD threads are normalized to direct item URLs. This does not run an eBay monitor.
The scraper uses pooled Rust HTTP requests and HTML parsing. A browser, Python, Node, local LLM server, cloud database, and the original combined bot are not needed.
docker compose logs -f bot
docker compose ps
docker compose exec bot /rfd-bot healthcheck
docker compose run --rm bot check-storage
docker compose downThe named rfd-data volume retains subscriptions and deal/message history across
container replacements. check-storage opens or initializes that volume and
checks SQLite without contacting Discord. Back up the volume while the bot is
stopped, or use SQLite's backup API; do not copy only the main database file while
WAL writes are active. Removing the data volume also removes notification history.
No ports are published. The health server is on loopback inside the container.
/health reports database status and the last poll timestamps/failure flag;
/health/discord reports Gateway readiness and response counters. Gateway
disconnects are retried automatically. A scrape failure is logged and retried on
the next scheduled poll; it is not treated as proof that no deals exist.
If commands do not arrive, check that the application's Interactions Endpoint
URL is empty and the bot token belongs to the configured application ID. Startup
checks both and binds its SQLite database to that application ID. An existing
binding must match; a nonempty database without a binding is rejected instead of
silently adopting another bot's history. check-discord checks the application
and the bot's permissions in subscribed channels without posting messages. With
no subscriptions yet, it cannot check a destination channel.
If commands are missing, rerun register; global registration may
take time to appear. If alerts cannot be sent, inspect the bot's channel permissions
and logs. Remote HTML changes, rate limiting, or an unavailable optional Gemini
model may require updated selectors or settings.
New installations can start directly with /rfd subscribe. Existing deployments
can import subscriptions and deal/message history using the offline import
command. The original combined database is a different schema and must not be
mounted as this bot's database.
Follow the migration procedure to rehearse the import into a new
database, preserve notification receipts, and stop the old RFD producer before
activating the new one. Imported messages remain in Discord and retain their
deduplication receipts; messages authored by the old application are not edited
by the new application. Set MAX_STORED_DEALS high enough to retain the imported
history, including room for new deals.
Requires Rust 1.95 or later. Docker builds static musl binaries for amd64/Arm64.
cargo fmt --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cargo build --release --lockedTests run against local HTTP/WebSocket fixtures and temporary SQLite databases. They do not need real credentials, post to real channels, or scrape live sites. The full local pipeline test exercises scraping, SQLite persistence, Discord notification formatting/delivery, overlapping filters, and restart deduplication. Gateway tests exercise command receipt, acknowledgement, disconnection, and resume.
| Package | Responsibility |
|---|---|
src/main.rs |
Startup, scheduling, registration, preflight, offline import, health checks |
src/commands.rs, src/discord.rs |
RFD commands and outbound Discord Gateway transport |
src/client.rs, src/parse.rs |
RFD HTML and detail parsing |
src/processor.rs, src/dedupe.rs, src/reconcile.rs |
Deduplication, enrichment, filtering, message updates |
src/notifier.rs |
RFD Discord embeds and HTTP delivery |
src/storage.rs, src/db.rs |
This bot's SQLite tables and subscriptions |
src/ai.rs |
Optional Gemini title cleanup |
The domain interfaces stay small: processor storage, scraper, notifier, validator, and optional title analyzer. Deal reconciliation and title batches belong to one poll; transport does not own eligibility rules. Successful channel receipts are saved per deal even when another channel fails, and missing destinations retry on unchanged observed deals. Do not import another bot to reuse one formatter or storage method. See the extraction record and validation.
The canonical repository is
pauljones0/rfd-discord-bot.
It contains only the standalone RFD service. Older clones of the combined bot
belong to the separately maintained homelab-bots project; do not pull this
repository into an existing combined deployment. See migration differences
and the cutover procedure.
For people who want alerts, share the Add the hosted bot to your server link at the top and the subscription steps. They use the running service; each server manages its own channel subscriptions.
For people who want to run an independent instance, share this repository or its
source archive. They supply their own Discord application and .env; no
credentials or existing subscriptions are included. The Git and Docker ignore
rules exclude local environment files, databases, and logs. The existing
license and source attribution are preserved.
SQLite uses one connection, WAL with FULL commit synchronization, a 2 MiB page cache target, a 256-page automatic checkpoint threshold, and a 1 MiB retained WAL target after resets. Startup and daily maintenance enforce MAX_STORED_DEALS even without subscribers. Thread aliases have a transactional index that is backfilled when an older standalone database is first opened; row deletion also removes its aliases. Deleted database pages are reused rather than constantly vacuuming the file. External readers can pin a growing WAL, so keep read transactions short and monitor disk usage.
Receipt/history retention uses the row cap rather than an age TTL: deleting a still-visible old thread's identity could repost it. Expiry metadata in old imports remains readable. A resurfacing thread whose record has been pruned can be considered new. Normal HTML responses are limited to 5 MiB decoded bytes.
See Oracle deployment for checked runtime bundles,
consistent backups and moving the same setup between VMs. The retired optimized
Go implementation is retained only in benchmarks/go-reference for reproducible
CPU/RSS comparisons and parity fixture generation.
See Oracle validation for the uploaded release bundles and current free-compute capacity blocker.