A local Python bot that checks Ricardo and Tutti for newly discovered listings, filters them against your search and price rules, and sends matching items to Telegram. Built for sourcing second-hand electronics in Switzerland.
I used this tool in my electronics reselling business to find listings without constantly checking both marketplaces manually. It helped me generate approximately CHF 1,500 per month through that business. The bot handled discovery and alerts; I handled evaluating items, buying, and reselling them.
That figure describes my own experience, not a promised return from running the software.
- Searches Ricardo by keyword and optional category, and Tutti through saved-search/category URLs.
- Filters by CHF price range, buy-now availability, device type, and shipping or pickup location.
- Optionally compares asking prices with a reference-price table you maintain.
- Stores seen listings in SQLite to avoid processing the same item on every pass.
- Sends Telegram alerts with the listing link, price, location, photo when available, and optional screening notes.
- Optionally screens listing text and photos with Gemini or Cloudflare Workers AI, with provider fallback when configured.
- Runs repeated checks with configurable delays, random timing variation, and a pause after a detected marketplace challenge.
This is a local command-line application. It has no web dashboard and does not run on GitHub after you push the code. Your computer, Chrome session, and bot process must remain running. It does not buy, bid, or pay for items.
An experimental Tutti seller-messaging feature is also included, separately from Telegram alerts. It is disabled in the example configuration.
- Python 3.11 or newer.
- Google Chrome and a desktop session. The launcher includes macOS paths and Linux executable discovery; Windows launch support is not implemented.
- A Telegram bot token and chat ID if you want phone notifications.
- Optional provider credentials for AI screening.
Run commands from the repository root.
git clone https://github.com/sinhos/ricardo-bot.git
cd ricardo-bot
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[notify]"
cp .env.example .env
cp config.example.yaml config.yaml
cp reference_prices.example.yaml reference_prices.yamlEdit config.yaml to set your searches and budgets. The supplied watch is an example, not a suggested purchase or current valuation. config.yaml, reference_prices.yaml, and .env are ignored by Git so personal settings stay local.
The default browser mode attaches to your installed Chrome, so a separate Playwright browser download is not needed for this setup.
- Create a bot through @BotFather in Telegram.
- Put its token in
TELEGRAM_BOT_TOKENin.env. - Open a private chat with your bot and send it a message.
- Run the helper to find your chat ID, save it to
.env, and send a confirmation:
python -m src.setup_telegramYou can also fill in TELEGRAM_CHAT_ID manually and check it with:
python -m src.main --test-telegramWithout Telegram credentials, matching listings are printed to the console.
python -m src.loginIn the Chrome window, sign in to Ricardo if needed, complete any human verification manually, and confirm a listing opens. Leave the window open. The bot uses a separate local profile at .ricardo_profile/ and connects through localhost:9222.
Check once with console output, without marking listings as seen:
python -m src.main --once --no-ai --no-telegram --no-storeWith the supplied configuration, seller messaging is disabled. If you later enable it, the flags above do not disable seller messages; keep messaging.enabled: false for read-only checks.
Start continuous monitoring and Telegram alerts:
python -m src.mainStop with Ctrl+C. Closing the terminal or letting the computer sleep interrupts monitoring.
Each entry in watches defines Ricardo search terms and filtering rules. For example:
watches:
- name: "iPhone 15"
search_terms: ["iPhone 15"]
category: phone
category_id: 49329
min_price: 100
max_price: 400
discount_pct: 0.0
require_reference_price: falseYou can also run an ad hoc Ricardo search:
python -m src.main --term "iPhone 15" --max-price 400 --category phone --category-id 49329 --onceThis replaces the configured Ricardo watches for that run; enabled Tutti searches still run. Use python -m src.main --help for all flags.
| Setting | Meaning |
|---|---|
poll.interval_seconds |
Base pause after a full sweep; example: 180 seconds. |
poll.jitter_seconds |
Random variation around that pause; example: ±90 seconds. |
poll.per_search_delay_seconds |
Delay between searches; example: 3 seconds. |
poll.block_backoff_seconds |
Pause after a detected challenge during the loop; example: 1,800 seconds. |
--limit |
Maximum listings fetched per search; default: 30. |
The example therefore waits roughly 90–270 seconds after each completed sweep. Search and screening time add to the interval. Alerts are sent when a check finds a match; there is no built-in exact-time scheduler or Telegram quiet-hours setting. The first run considers all fetched listings unseen, even if they were published earlier.
Listings marked as offering shipping pass the location check. Pickup-only listings, including those with unknown shipping availability, are checked against filters.reachable_cities, which accepts city names or postal codes. With require_shipping: true and an empty city list, those listings are rejected.
Set tutti.enabled: true and add entries under tutti.searches, following the commented example in config.example.yaml. Copy a full saved-search/category URL from your browser; the current source expects URLs, not free-text keywords. Verify that each URL returns the intended category and location.
Tutti shipping availability is treated as unknown, so configure reachable pickup cities or set filters.require_shipping: false. A nonempty reachable-city list still restricts pickup locations. For a Tutti search with a nonzero discount_pct, matching reference prices are required. Keep discount_pct: 0.0 to use simple price bounds.
Add your own CHF estimates to reference_prices.yaml, then set a watch's discount_pct to a value such as 0.30. The price filter uses:
buy threshold = reference price × (1 − discount)
The hard max_price still applies. The most specific matching model label is used, with condition-based prices where available. For Ricardo watches, require_reference_price: true rejects unpriced models when the discount filter is active.
python -m src.seed_prices can overwrite the local table with median asking prices from its built-in model list. Review those numbers yourself: asking prices are not completed-sale prices, and the calculation does not include fees, shipping, repairs, or resale costs.
python -m pip install -e ".[all]"Set ai.enabled: true and configure GOOGLE_API_KEY (or GEMINI_API_KEY), or CLOUDFLARE_ACCOUNT_ID plus CLOUDFLARE_API_TOKEN, in .env. ai.provider selects gemini or cloudflare; when both are configured, the other can serve as a quota fallback. Model names are configurable; availability, quotas, and pricing depend on the provider.
AI screening sends listing text and photos to the configured external provider. It can reject obvious junk, wrong-item matches, and listings it cannot screen. gate_on_legit: true adds the confidence/recommendation gate. If provider quota is exhausted without a usable fallback, the loop continues with the ordinary filters. AI assessments are not authentication or scam guarantees.
The Tutti integration can draft or send an offer for qualifying listings with reference prices. messaging.enabled: true enables the feature; auto_send: false records drafts, while auto_send: true attempts real messages through the logged-in browser.
The feature checks a daily cap, duplicate contacts, offer bounds, and send_hours_start/send_hours_end using the computer's local time. Those hours apply only to seller messaging. UI selectors are best-effort, and a successful click is not proof of delivery. Keep this feature off unless you have reviewed its behavior. Use the SQLite store when enabling it; --no-store removes persistent duplicate and daily-count checks. tutti.price_factor currently adjusts the seller-offer calculation, not the listing price filter.
src/main.py Polling loop and processing pipeline
src/config.py Configuration models
src/browser.py Chrome attachment and browser lifecycle
src/sources/ Ricardo and Tutti fetching/parsing
src/filters.py Price, device, shipping, and location rules
src/pricing.py Reference-price matching
src/screening.py Optional AI screening and provider fallback
src/notify.py Telegram notifications
src/store.py SQLite history and duplicate tracking
src/messaging.py Experimental Tutti seller messaging
config.example.yaml Shareable configuration template
.env.example Credential variable names, with empty values
- Marketplace markup and access checks can change. These are browser integrations, not official marketplace APIs; live compatibility needs periodic checking.
- Complete verification manually when prompted. The loop backs off after detected challenges. Review each marketplace's rules before running automated checks.
- SQLite tracks seen items rather than continuously rechecking each listing's price. Delivery is not guaranteed: items are marked seen before Telegram delivery, and failed alerts may not be retried on the next sweep.
- Keep browser profiles, databases,
.env, and debug HTML private. They can contain session or personal data and are excluded by.gitignore. Keep the Chrome debugging port local. - This project is independent of Ricardo, Tutti, Telegram, and the AI providers.