Telegram bot that captures ads (screenshot or link), analyzes them with Mistral's vision model, and logs the results to Supabase.
See plan.md for architecture and known limitations (notably: X/Instagram
links need a screenshot-rendering service to work — see below).
- Supabase CLI installed and logged in (
supabase login) - A Supabase project (
database.newif you need one) - A Telegram bot token from @BotFather (
/newbot) - A Mistral API key from console.mistral.ai
-
Link your project
supabase link --project-ref <your-project-ref>
-
Run the migration
supabase db push
This creates
ad_audits, enables RLS, and creates the privatead-screenshotsstorage bucket. -
Set function secrets
supabase secrets set \ TELEGRAM_BOT_TOKEN=<your-bot-token> \ MISTRAL_API_KEY=<your-mistral-key>
SUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYare injected automatically into deployed Edge Functions — you don't need to set them manually. -
Deploy
supabase functions deploy telegram-bot --no-verify-jwt
--no-verify-jwtis required: Telegram doesn't send a Supabase JWT, so the webhook secret query param is what authenticates requests instead. -
Register the webhook
curl "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook?url=https://<project-ref>.supabase.co/functions/v1/telegram-bot?secret=<TELEGRAM_BOT_TOKEN>"You should get
{"ok":true,"result":true,...}back.
supabase functions serve telegram-bot --env-file .env.exampleUse a tool like ngrok to expose your local server and point Telegram's webhook at that URL temporarily for testing.
/start— intro and command list/history— last 10 analyzed ads for that chat, most recent first/summary— aggregate profile: top brands, dominant targeting type, recurring interest themes, computed in-function from the last 200 rows (no extra AI call — plain aggregation over already-storedraw_ai_analysis)
- Send a plain URL → bot fetches
og:*metadata, analyzes, replies. - Send a screenshot → bot downloads, stores, analyzes, replies.
- Send a forwarded photo with a caption → caption is passed to the model as extra context (useful when the caption contains the original link).
- Send an X/Instagram link → metadata will likely come back empty since
those platforms block unauthenticated scraping; the model still gets the
bare URL and does its best. If these become common, see
plan.mdfor the screenshot-render option.
- No rate limiting yet — one user spamming screenshots means uncapped
Mistral spend. See TODOs in
plan.md. - X/Instagram links return little to no
og:*metadata (login-walled). Not handled specially since most ad links won't be from these platforms — revisit if that assumption changes. mistral-largemodel naming shifts over time — confirm the current vision-capable model name in the Mistral console before deploying.