Skip to content

Repository files navigation

market-radar

A scheduled intelligence digest. It reads what people publicly say about a product and its competitors across Reddit, X, forums and news, runs two independent LLM analysis passes, and mails a two-column brief on a schedule.

Built for and deployed on Canadian payments, monitoring Interac e-Transfer and the products around it. Every example below is from that deployment.

What you can use this for

The pattern is: pick a topic, let it watch the public web on a cadence, get a short brief instead of a search session.

  • Competitive intelligence. Track what rival products ship, price, and get criticised for. The right-hand column exists for exactly this.
  • Voice of customer. Surface the complaints people post in public but never file as support tickets. Holds, limits, fraud and edge cases show up on Reddit long before they reach a roadmap.
  • Market and sector research. Point it at a category rather than one product and get a recurring read on where it is moving.
  • Founder or analyst coverage of a niche. Replace a manual weekly sweep of the same twenty sources.
  • Launch and incident monitoring. Run it more often around a release to catch reaction while it still matters.

What repoints cleanly, and what does not. The search queries and all six model prompts are configuration, in prompts.json and prompts/*.md, so changing what is monitored is a JSON and markdown edit. The relevance layer is not: the competitor brand list, the low-insight regexes, the blocked-domain keywords and the subreddit browse pairs are hardcoded to Canadian payments in app.py. Repointing this at a different sector means editing those too. They are the part that makes the output worth reading, so they are worth editing rather than deleting.

Demo

Watch the demo on YouTube (https://youtu.be/AN_Qa8JEs7I)

The video walks through a live /email run: scan, analysis, and the digest landing in an inbox.

What it actually scans

Every biweekly run pulls from four public sources. No source is a stub.

Source How it is reached Scope
Reddit Public search.json plus /new feed browsing r/personalfinancecanada, r/canada, r/ontario, r/toronto, r/banking, r/Scams, r/frugalcanada, r/legaladvicecanada, r/CanadianInvestor
X/Twitter twitterapi.io search, plus DuckDuckGo Twitter results 4 dedicated e-Transfer queries, run independently of the DDG query list
RedFlagDeals DuckDuckGo site:forums.redflagdeals.com Canadian deals and banking forums
News and web DuckDuckGo text and news verticals Payments press, fintech launches, regulatory items

Query lists live in prompts.json, not in code: 31 etransfer_queries and 25 competitor_queries. Adding a monitored topic is a JSON edit and a push.

Two things run before the model sees anything:

  • A domain blocklist (_BLOCKED_DOMAINS) drops casino and sportsbook affiliates. They rank well for "e-Transfer" because Canadians use it to fund gambling accounts, and they are pure noise.
  • A quality score (_mention_quality_score) ranks each mention on upvotes, engagement, keyword hits, and length. Results are then stratified by platform before the cut, so high-volume Reddit cannot crowd Twitter out of the pool. Biweekly caps are 17 Reddit, 11 Twitter, 7 other.

Previously sent URLs are remembered and filtered out, so consecutive digests do not repeat themselves.

Two-track prompt architecture

The digest has two columns because the analysis is two independent model calls, dispatched in parallel with asyncio.create_task. Each gets its own corpus and its own prompt file.

Track Prompt Input Output column
Community chatter prompts/etransfer_chatter_prompt.md Reddit, X, forum posts Left: real user quotes about fraud, holds, delays, limits
Market intelligence prompts/market_pulse_prompt.md Press, competitor mentions Right: PayPal, Wise, Wealthsimple, KOHO, Revolut, Neo, Apple Pay, Payments Canada

Splitting them was deliberate. A single call kept letting competitor news bleed into the pain-points column and vice versa. Two calls with narrow corpora and narrow instructions do not have that failure mode.

Prompts are plain markdown, versioned in prompts/. Editing tone or the quality bar does not touch Python. prompts/prompt_recipe.md documents why each rule is there.

Scheduling and delivery

A daily job fires at 14:00 UTC (9am Toronto). It self-guards: the biweekly scan only executes if 14 or more days have passed since the last recorded scan. A second daily job runs the quarterly market-trends report on Nov 1, Feb 1, May 1, and Aug 1. All time math uses ZoneInfo("America/Toronto"), so DST is handled rather than approximated.

Delivery is two paths from one scan:

  • HTML email to EMAIL_TO through Resend or plain SMTP. 1200px table layout, inline CSS, webmail-safe.
  • Plain text to every subscribed Telegram chat.

Admins can force a run at any time with /email. If S3-compatible storage is configured, each send uploads the current workbooks and puts download links in the email footer.

Quickstart

pip install -r requirements.txt
export TELEGRAM_TOKEN=... KIMI_API_KEY=...
python app.py

The bot polls by default. Set WEBHOOK_URL to switch to webhook mode. Send /status in Telegram to confirm which keys and email settings resolved.

How it works

prompts.json (31 + 25 queries)
        │
        ▼
fetch_biweekly_mentions()
  ├── Reddit JSON API      search + /new browse across 9 subreddits
  ├── twitterapi.io        4 dedicated e-Transfer queries
  └── DuckDuckGo           text + news + Twitter verticals
        │
        ▼
  filter: blocklist, SEO-explainer filter, recency (MAX_MENTION_AGE_DAYS, default 120)
  score:  _mention_quality_score()
  route:  _classify_channel_and_source()
        │
        ├──────────────────────────┬──────────────────────────┐
        ▼                          ▼                          │
=== e-TRANSFER COMMUNITY ===   === COMPETITOR INTEL ===        │
=== e-TRANSFER NEWS ===  ──────────────┘                       │
        │                          │                           │
        ▼                          ▼                           │
etransfer_chatter_prompt   market_pulse_prompt                 │
        │  Kimi call A             │  Kimi call B              │
        └──────────┬───────────────┘  (parallel)               │
                   ▼                                           │
          analyze_biweekly()  ◄────────────────────────────────┘
                   │
        ┌──────────┴──────────┐
        ▼                     ▼
  HTML email                Telegram
  (Resend / SMTP)           (plain text, subscribed chats)
        │
        ▼
  source_ledger.xlsx + biweekly_reports.xlsx  (append-only, one row per mention)

Every mention that reaches the model is logged to biweekly_reports.xlsx with its bucket and inclusion flags, so you can audit why a bullet appeared.

Commands

Public:

Command Effect
/start, /help Overview and auto-subscribe
/subscribe, /unsubscribe Toggle biweekly broadcasts for this chat
/status Runtime, schedule, and config snapshot
/scan Run the biweekly scan now
/raw Raw mention payload from the last scan
/prompt Query and source config summary
plain text Follow-up question against the latest report

Admin only (ADMIN_IDS):

Command Effect
/email Scan and send the digest immediately
/quarterly Run the quarterly market-trends report
/smtpcheck Validate email provider config and connectivity
/statefiles Download both workbooks from the running host
/stop Cancel active tasks

Configuration

Required:

TELEGRAM_TOKEN=<telegram bot token>
KIMI_API_KEY=<moonshot kimi api key>

Common optional:

KIMI_API_URL=https://api.moonshot.ai/v1/chat/completions
KIMI_MODEL=kimi-k2.5-preview
TWITTERAPI_IO_KEY=<twitterapi.io key>
ADMIN_IDS=123456789,987654321
MAX_MENTION_AGE_DAYS=120
PORT=3978
WEBHOOK_URL=

Email:

EMAIL_ENABLED=1
EMAIL_PROVIDER=smtp            # smtp | resend
EMAIL_SEND_MODE=weekly         # alert | weekly | always | weekly,alert
EMAIL_FROM=bot@example.com
EMAIL_TO=you@example.com,team@example.com
EMAIL_SUBJECT_PREFIX=Interac Intelligence

# SMTP path
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=you@example.com
SMTP_PASSWORD=<app password>

# Resend path
RESEND_API_KEY=<resend api key>
RESEND_API_URL=https://api.resend.com/emails

State and workbook links:

STATE_DIR=/data                                   # point at a mounted volume in production
ATTACH_STATE_EXCEL_ON_BIWEEKLY=1                  # Telegram-deliver workbooks after each run
STATE_EXCEL_TELEGRAM_CHAT_ID=-1001234567890

WORKBOOK_S3_BUCKET=your-bucket
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
S3_ENDPOINT_URL=                                  # set for R2 or MinIO
WORKBOOK_PUBLIC_BASE_URL=https://files.example.com/my-prefix
WORKBOOK_S3_PREFIX=interac-intel/workbooks

Never hardcode a key. Everything above is read from the environment.

Deploying

Any container host works. The included Dockerfile builds on python:3.12-slim; Procfile runs python app.py.

  1. Push to GitHub and point the host at the repo.
  2. Set at minimum TELEGRAM_TOKEN and KIMI_API_KEY.
  3. Mount a volume and set STATE_DIR to it, otherwise workbooks and scan memory are lost on redeploy.
  4. Verify the sending domain with your email provider.
  5. Send /status in Telegram, then /email.

HANDOFF.md is the operational runbook: env vars, common failure modes, and where to change what.

Known limitations

  • app.py is a 4,888-line monolith. Fetch, filter, scoring, prompt assembly, HTML rendering, Excel logging, and every Telegram handler live in one file. It is the honest cost of shipping this solo and iterating on output quality over 121 commits. Splitting it into sources/, analysis/, render/, and bot/ is the first thing to do next.
  • Subscription state, the last report, and rate-limit counters are in memory and reset on restart.
  • Workbooks and scan memory survive process restarts but not redeploys unless STATE_DIR is a mounted volume.
  • DuckDuckGo result quality depends on upstream indexing. X/Twitter coverage through DDG is thin, which is why twitterapi.io is a separate path.
  • manifest.json is a leftover Microsoft Teams artifact and is unused.

License

MIT. See LICENSE.

About

Scheduled intelligence digest. Scans Reddit, X, forums and news on a topic and its competitors, runs two parallel LLM analysis passes, and mails a two-column brief. Deployed on Canadian payments.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages