Subito Monitoring
# Subito Monitoring
[](https://github.com/Alexxanddr/subito-monitoring/actions/workflows/ci.yaml)
[](LICENSE)
A single-user, open source Subito.it search and watch service: autonomous polling,
SQLite state and delivery queue, direct Telegram notifications, and eight MCP tools
for Hermes or any MCP client. No LLM, dashboard, external database or Vinted runtime.
MIT licensed; the monitoring core is adapted from Vinted Monitoring with attribution.
**Integration status (2026-11-03): limited live validation passed.** The production
provider now uses ordinary system `curl`, which succeeded on macOS and in the Linux
container. Page 2, all four orders, Motori, Immobili, valid empty results and a narrow
Market silent baseline/rescan were checked against actual Subito responses. Chrome
also confirmed page 2 and the site's real ordering URL. No Telegram message was sent;
long-running availability is not guaranteed. See [verification](docs/verification.md).
Subito's terms prohibit automated extraction without permission. Network access is
**disabled by default** (`SM_UPSTREAM_ENABLED=false`). Obtain appropriate permission
before enabling it; this switch does not grant permission or bypass a block.
401/403/challenges persistently suspend access until an administrator investigates.
## Quick start with Docker
Public image: **`ghcr.io/alexxanddr/subito-monitoring:latest`**, Linux **amd64/arm64**.
Use a version or commit tag for a repeatable deployment; see [publishing](docs/publishing.md).
The image includes Python, curl and CA certificates. Chrome is not required.
```sh
git clone https://github.com/Alexxanddr/subito-monitoring.git
cd subito-monitoring
cp .env.example .env
# Edit .env privately before starting:
# - replace SM_HTTP_TOKEN with a random secret of at least 32 characters;
# - set both Telegram variables for watches, or leave both unset for search only;
# - set SM_UPSTREAM_ENABLED=true only after reviewing Subito access permission.
chmod 600 .env
docker compose pull
docker compose up -d --no-build
curl --fail http://127.0.0.1:8000/health/ready
```
The example bearer value is intentionally invalid: startup refuses the unchanged
placeholder. Compose binds HTTP to loopback on port 8000 and mounts a persistent
named volume at `/data`. Never use `docker compose down -v` on real state.
Set actual service DNS in `SM_ALLOWED_HOSTS` before connecting remote MCPHub.
Every MCP call requires the bearer token; health probes contain booleans only.
Readiness proves the polling task is running, not that Subito scans are fresh.
## Local installation and development
Python **3.12–3.14**, system **curl >=8.4**, and a local filesystem with POSIX locks
are required (Linux/macOS). On Debian/Ubuntu install `curl` and `ca-certificates`;
macOS ships curl, which can be checked with `curl --version`.
```sh
uv sync --frozen --no-editable --extra dev
uv run --no-sync subito-monitoring
```
The default transport is MCP stdio, state is `data/monitor.sqlite3`, and Subito
network access is off. Inject environment settings through your supervisor/client;
the local CLI does not automatically load `.env`. Stdio owns the database until
stdin closes. For Hermes-independent monitoring use supervised HTTP or Compose.
Do not run a second stdio owner alongside the HTTP service on the same database.
To build the image locally instead of pulling the published image:
```sh
docker compose up --build -d
```
See [all environment variables and MCP examples](docs/configuration.md) and
[backup, restore, upgrades and troubleshooting](docs/operations.md).
## MCP tools
| Tool | Effect |
| --- | --- |
| `search_items` | Immediate one native page; no watch, state baseline or Telegram send |
| `create_watch` | Persistent query, silent initial complete baseline |
| `list_watches` | Queries, baseline, errors and last successful scan |
| `update_watch` | Replace query or change name/interval; query change resets baseline |
| `pause_watch` / `resume_watch` | Change polling; queued deliveries remain |
| `delete_watch` | Delete watch; retain global history and queued deliveries |
| `monitoring_status` | Freshness, incomplete coverage, suspension and delivery backlog |
“Show the 10 newest ThinkCentre i5 under EUR 150” → `search_items`:
```json
{"query":{"text":"ThinkCentre i5","section":"market","max_price":"150","order":"newest_first"},"limit":10,"page":1}
```
“Monitor ThinkCentre i5 under EUR 150 every five minutes” → `create_watch`:
```json
{"name":"ThinkCentre i5","query":{"text":"ThinkCentre i5","section":"market","max_price":"150","order":"newest_first"},"interval_seconds":300}
```
Watches default to 600 seconds, validated 60–604800. Supported query orders:
`newest_first`, `relevance`, `price_low_to_high`, `price_high_to_low`; watches require
`newest_first`. Text searches Italy sale listings. `section` selects `market`,
`motori` or `immobili`; default `all` combines known supported categories and
**excludes Lavoro**, including jobs, services and work equipment. `marketplace: IT`
is the legacy country field, not a Subito section. Motori/Immobili use the observed
category paths; Market/all are filtered locally by the first-party category catalog.
Motori/Immobili and a narrow Market query have limited live validation.
`relevance` is available for Market/all, not Motori/Immobili.
Price filtering is local:
missing prices are excluded when a range is set. Native pages contain at most 30
ordinary listings; promoted gallery listings are intentionally excluded. A limit
of 1–30 truncates the current native page, and incrementing `page` skips its unused
remainder. Sparse price matches can return fewer than requested. No individual subcategory,
location, radius, time filters, photos or price-drop alerts in this version.
## Reliability and operations
Baseline and associations commit atomically only after reaching an explicit last
page within the scan budget and checking the first page for newly arrived IDs.
Failures never advance state. IDs are the numeric listing ID validated against its
URN and URL; there is no maximum-ID/timestamp heuristic. Global ID/destination
history suppresses reposted IDs across overlapping watches; a different ID is new.
Baseline records are distinguished from successfully sent messages. Pending sends
survive restart, pause, edits and deletion. A crash after Telegram accepted a
message but before the SQLite commit can duplicate it: delivery is at least once.
A mutable paginated site is not an exhaustive snapshot. The default scan budget
is five native pages plus a first-page check; broad searches may never complete a
baseline. Narrow the text query. Downtime can lose ads that disappear before a
successful scan; no timestamp recovery window is promised. Process health does
not prove scan freshness or Telegram delivery.
- [Configuration, transports and Hermes/MCPHub](docs/configuration.md)
- [Baseline, recovery, backup, security and troubleshooting](docs/operations.md)
- [Generic Kubernetes manifests](examples/kubernetes/README.md)
- [Distributable Hermes skill](skills/subito-monitoring/SKILL.md)
- [Contributing](CONTRIBUTING.md), [security](SECURITY.md), [changelog](CHANGELOG.md)
## Build and check
```sh
uv run --no-sync pytest -q
uv run --no-sync ruff check .
uv run --no-sync ruff format --check .
docker build -t subito-monitoring:test .
python3 scripts/container_check.py
```
Pull requests run offline tests, lint, a full-history secret scan, a locked-dependency
export check, and container/volume verification. Pushes to `master` additionally
build and test **native amd64 and arm64** images before publishing the multi-platform
`latest` and `sha-<commit>` tags. Release tags publish versioned images only after
the same checks; version tags must match `pyproject.toml`. CI never contacts Subito
or Telegram and needs no personal registry token.
See [publishing and release procedure](docs/publishing.md), [verification evidence](docs/verification.md),
and [dependency license inventory](docs/dependency-licenses.md). This project is
unaffiliated with Subito. No cluster, MCPHub or Hermes installation is implied by
a public image; those are separate administrator operations.
TDQS
Scored across 8 tools
list_watches and monitoring_status both report freshness/errors, creating some overlap, though one is explicitly unauthenticated and the other is authenticated with Telegram backlog. search_items is clearly immediate versus persistent watches, and pause/resume/update/delete are distinct lifecycle actions.
Seven of eight tools follow a clear verb_noun pattern: list_watches, pause_watch, search_items, create_watch, update_watch, resume_watch, delete_watch. monitoring_status breaks the verb pattern as a noun phrase, but the overall convention remains readable and predictable.
Eight tools is well-scoped for this domain, covering the full watch lifecycle, status inspection, and an immediate search path. Each tool has a clear role without obvious bloat.
The surface covers create/list/update/delete plus pause/resume and status for persistent searches, and list_watches exposes baseline and freshness state. A dedicated get_watch by ID is absent, but it is a minor gap agents can work around via list_watches.