watching is a self-hosted personal monitoring tool.
You define a task such as:
- "Tell me when there is credible news about Kimi's IPO timing"
- "Tell me when a company starts talking about listing in Hong Kong"
The app then:
- generates search queries for the task with
OpenAI - searches with
SerpApi - evaluates returned candidates with
OpenAI - stores task runs and state in
SQLite - sends a WhatsApp message through
Baileysif a result qualifies
This project runs as plain Node.js scripts. There is no compile step.
Node.jsSerpApiOpenAI APISQLiteviabetter-sqlite3Baileysfor WhatsApp notifications
- Node.js
20+ - npm
- a
SerpApikey - an
OpenAIAPI key - a valid
Baileysauth/session directory if you want real WhatsApp delivery
Install dependencies:
npm installCreate your local env file:
cp .env.example .envThen fill in the important values in .env:
SERPAPI_API_KEY=...
OPENAI_API_KEY=...
BAILEYS_AUTH_PATH=/absolute/path/to/your/baileys/sessionPut each task’s WhatsApp group or user JID in config/tasks.json under notify.destination (or pass --destination to add-task). Use npm run list-whatsapp-groups to list group JIDs.
SERPAPI_API_KEY: required for searchOPENAI_API_KEY: required for query generation and evaluationOPENAI_MODEL: defaults togpt-4o-miniTASKS_FILE_PATH: defaults to./config/tasks.jsonDB_PATH: defaults to./data/watching.dbLOG_FILE_PATH: defaults to./logs/watching.logBAILEYS_AUTH_PATH: path to an authenticated Baileys session directoryPOLL_INTERVAL_MS: worker poll interval, default60000SERPAPI_ENGINE: default engine, defaultgoogle_newsDEFAULT_LOCALE: default locale, defaulten-USDEFAULT_COUNTRY: default country, defaultusDEFAULT_MAX_RESULTS: default max results per queryDRY_RUN_NOTIFY=1: do not send real WhatsApp messagesDEBUG=1: enable verbose logs
config/tasks.json: editable task definitionsdata/watching.db: SQLite databaselogs/watching.log: runtime log filesrc/commands/: CLI commandssrc/providers/: SerpApi and OpenAI integrationswhatsapp-kit/: portable WhatsApp / Baileys module (pair, list groups, notifier). Copy the whole folder into another Node project; seewhatsapp-kit/README.md.
Create a new monitoring task from plain language:
npm run add-task -- --goal "Notify me when there is credible news about Kimi IPO timing"You can also set more fields:
npm run add-task -- \
--name kimi-ipo-date \
--goal "Notify me when there is credible news about Kimi or Moonshot AI IPO timing, filing schedule, exchange listing, or prospectus progress." \
--schedule "every 12 hours" \
--criteria "Only notify when the result strongly suggests an IPO timeline, listing plan, exchange filing, prospectus, or official company statement." \
--locale en-US \
--destination 8613812345678Notes:
- the task name is auto-generated from the goal if you do not pass
--name - the command asks
OpenAIto generate aqueryPlan - the task is written to
config/tasks.json - the task is synced into SQLite
npm run list-tasksThis prints a table with:
- task id
- task name
- whether it is enabled
- schedule
- locale
- last run time
- last run status
If your Baileys auth path is not linked yet, run (no phone number required):
npm run pair-whatsappThe command prints a QR code in the terminal. Open WhatsApp on your phone → Linked devices → Link a device, then scan it.
Optional auth directory:
node src/commands/pair-whatsapp.js --auth-path /path/to/baileys/sessionImportant:
- with
npm run, pass flags after--if you add any - if you see 401 or “connection failure” while scanning QR, local session files are usually bad: run
npm run pair-whatsapp -- --reset(or delete your Baileys auth directory), then try again - right after a successful QR scan, WhatsApp may close the socket once (e.g. 515); the command reconnects with a short delay — you do not need to scan again
- pairing uses the live WhatsApp Web client revision (
web.whatsapp.com/sw.js), not only the version baked into the Baileys repo - stop the worker (or anything else using
BAILEYS_AUTH_PATH) while you runpair-whatsapp, or the session folder may conflict - after the script prints your JID it waits ~2.5s so the session can save; don’t Ctrl+C until you see the “Done. Session is saved” lines — otherwise you may have to pair again
- you only need
pair-whatsapponce per auth folder; normal use isnpm run worker(orlist-whatsapp-groups), not pairing every time - once connected, it prints:
Raw JIDBase JIDName
If your session is already paired, list the available groups:
npm run list-whatsapp-groupsThis prints each group's:
- subject
- JID
- owner
- participant count
Use the selected group JID as the destination for watching.
npm run run-once -- --task kimi-ipo-dateRun all configured tasks:
npm run run-once -- --allnpm run workerThis starts the internal scheduler loop and checks due tasks every POLL_INTERVAL_MS.
Scheduling is handled inside the app, not by system cron.
Examples of supported schedule values:
every 12 hoursdailyhourly- a 5-field cron expression such as
0 8,20 * * *
every 12 hoursmeans "run when 12 hours have passed since the last run"- it is not anchored to exact wall-clock times unless you use a cron expression
- the worker checks whether a task is due on each poll
Before run-once or the worker runs searches, src/validate-task.js checks:
.envhasSERPAPI_API_KEY,OPENAI_API_KEY, andBAILEYS_AUTH_PATH- each task has a non-empty
queryPlan(engine + queries) andnotify.destination
A misconfigured task fails immediately with a clear error instead of burning API usage. The worker skips invalid tasks and logs the error.
The app sends a WhatsApp message only when:
- the task run succeeds
- a candidate result is evaluated by
OpenAI OpenAIreturns:match = trueconfidence = high
task.notify.destinationis set (WhatsApp user or group JID)BAILEYS_AUTH_PATHpoints to a valid authenticated session
If no notification is sent, the task can still show completed. That only means the run finished successfully.
Tasks live in config/tasks.json.
Example:
{
"tasks": [
{
"name": "kimi-ipo-date",
"goal": "Notify me when there is credible news about Kimi or Moonshot AI IPO timing, official filing schedule, listing exchange, or prospectus progress.",
"schedule": "every 12 hours",
"criteria": "Only notify when the result strongly suggests an IPO timeline, listing plan, exchange filing, prospectus, or official company statement. Suppress weak speculation.",
"locale": "en-US",
"enabled": true,
"queryPlan": {
"engine": "google_news",
"hl": "en",
"gl": "us",
"maxResultsPerQuery": 5,
"summary": "Generated by OpenAI",
"queries": [
{
"query": "Kimi IPO date announcement",
"reason": "Look for direct timing announcements"
}
]
},
"notify": {
"destination": "8613812345678"
}
}
]
}You can edit this file manually if needed.
Logs go to:
- terminal output
logs/watching.log
Useful ways to inspect logs:
tail -f logs/watching.logless logs/watching.logThe logs include:
- search keywords
- returned SerpApi results
- OpenAI prompts
- OpenAI parsed responses
- dedupe decisions
- notification attempts
- task and worker summaries
The app stores state in data/watching.db.
Main tables:
taskstask_runscandidatesdecisionsnotificationsseen_items
This lets the app avoid duplicate alerts and preserve task history.
- configure
.env - add a task
- inspect generated task queries in
config/tasks.json - run it once:
npm run run-once -- --task kimi-ipo-date- inspect:
logs/watching.logconfig/tasks.jsonnpm run list-tasks
- once satisfied, start the worker:
npm run workerPossible reasons:
- no result matched strongly enough
task.notify.destinationis missing or empty (set it inconfig/tasks.jsonoradd-task --destination)BAILEYS_AUTH_PATHis not pointing to a valid session- the task results were all deduped
Check:
logs/watching.logThat file now contains:
- query strings
- search results
- OpenAI prompt messages
- OpenAI responses
Use:
DRY_RUN_NOTIFY=1Then the app will go through the notification path without sending a real message.
The project already includes a sample task:
kimi-ipo-date
You can list it with:
npm run list-tasks