┌─────────────────────────────────┐
│ $5 VPS (Ubuntu) │
│ │
│ ┌───────────┐ ┌──────────┐ │
│ │ Node.js │ │ Puppeteer│ │
│ │ Process │──▶│ / Playwright│ │
│ │ (Gateway) │ │ (Browser)│ │
│ └─────┬─────┘ └──────────┘ │
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Cron Scheduler ││
│ │ (every 5min / 30min / 1h) ││
│ └─────┬──────────────────────┘│
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Message Routing ││
│ │ → Feishu / Telegram / Slack││
│ └────────────────────────────┘│
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ $5 VPS (Ubuntu) │
│ │
│ ┌───────────┐ ┌──────────┐ │
│ │ Node.js │ │ Puppeteer│ │
│ │ Process │──▶│ / Playwright│ │
│ │ (Gateway) │ │ (Browser)│ │
│ └─────┬─────┘ └──────────┘ │
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Cron Scheduler ││
│ │ (every 5min / 30min / 1h) ││
│ └─────┬──────────────────────┘│
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Message Routing ││
│ │ → Feishu / Telegram / Slack││
│ └────────────────────────────┘│
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ $5 VPS (Ubuntu) │
│ │
│ ┌───────────┐ ┌──────────┐ │
│ │ Node.js │ │ Puppeteer│ │
│ │ Process │──▶│ / Playwright│ │
│ │ (Gateway) │ │ (Browser)│ │
│ └─────┬─────┘ └──────────┘ │
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Cron Scheduler ││
│ │ (every 5min / 30min / 1h) ││
│ └─────┬──────────────────────┘│
│ │ │
│ ┌─────▼──────────────────────┐│
│ │ Message Routing ││
│ │ → Feishu / Telegram / Slack││
│ └────────────────────────────┘│
└─────────────────────────────────┘
const cron = require('node-cron'); // Run every 5 minutes — check for urgent stuff
cron.schedule('*/5 * * * *', async () => { await checkUrgentAlerts();
}); // Run every 30 minutes — routine monitoring cron.schedule('*/30 * * * *', async () => { await monitorGitHubPRs(); await scanForNewTasks();
}); // Run every hour — reports and summaries
cron.schedule('0 * * * *', async () => { await sendHourlyDigest();
});
const cron = require('node-cron'); // Run every 5 minutes — check for urgent stuff
cron.schedule('*/5 * * * *', async () => { await checkUrgentAlerts();
}); // Run every 30 minutes — routine monitoring cron.schedule('*/30 * * * *', async () => { await monitorGitHubPRs(); await scanForNewTasks();
}); // Run every hour — reports and summaries
cron.schedule('0 * * * *', async () => { await sendHourlyDigest();
});
const cron = require('node-cron'); // Run every 5 minutes — check for urgent stuff
cron.schedule('*/5 * * * *', async () => { await checkUrgentAlerts();
}); // Run every 30 minutes — routine monitoring cron.schedule('*/30 * * * *', async () => { await monitorGitHubPRs(); await scanForNewTasks();
}); // Run every hour — reports and summaries
cron.schedule('0 * * * *', async () => { await sendHourlyDigest();
});
function shouldRun(jobName, intervalMinutes) { const stateFile = `./cron-state/${jobName}.json`; const now = Date.now(); try { const { lastRun } = JSON.parse(fs.readFileSync(stateFile)); if (now - lastRun < intervalMinutes * 60 * 1000) return false; } catch { /* first run */ } fs.writeFileSync(stateFile, JSON.stringify({ lastRun: now })); return true;
}
function shouldRun(jobName, intervalMinutes) { const stateFile = `./cron-state/${jobName}.json`; const now = Date.now(); try { const { lastRun } = JSON.parse(fs.readFileSync(stateFile)); if (now - lastRun < intervalMinutes * 60 * 1000) return false; } catch { /* first run */ } fs.writeFileSync(stateFile, JSON.stringify({ lastRun: now })); return true;
}
function shouldRun(jobName, intervalMinutes) { const stateFile = `./cron-state/${jobName}.json`; const now = Date.now(); try { const { lastRun } = JSON.parse(fs.readFileSync(stateFile)); if (now - lastRun < intervalMinutes * 60 * 1000) return false; } catch { /* first run */ } fs.writeFileSync(stateFile, JSON.stringify({ lastRun: now })); return true;
}
const { chromium } = require('playwright'); async function checkWebsite(url, selector) { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); // Check if a specific element exists (price drop? new item?) const element = await page.$(selector); const found = !!element; if (found) { const text = await element.innerText(); await notify(`Found match on ${url}: ${text}`); } await browser.close(); return found;
}
const { chromium } = require('playwright'); async function checkWebsite(url, selector) { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); // Check if a specific element exists (price drop? new item?) const element = await page.$(selector); const found = !!element; if (found) { const text = await element.innerText(); await notify(`Found match on ${url}: ${text}`); } await browser.close(); return found;
}
const { chromium } = require('playwright'); async function checkWebsite(url, selector) { const browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); // Check if a specific element exists (price drop? new item?) const element = await page.$(selector); const found = !!element; if (found) { const text = await element.innerText(); await notify(`Found match on ${url}: ${text}`); } await browser.close(); return found;
}
async function sendFeishuMessage(webhookUrl, text) { await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ msg_type: "text", content: { text } }) });
} async function sendTelegramMessage(botToken, chatId, text) { await fetch( `https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }) } );
}
async function sendFeishuMessage(webhookUrl, text) { await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ msg_type: "text", content: { text } }) });
} async function sendTelegramMessage(botToken, chatId, text) { await fetch( `https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }) } );
}
async function sendFeishuMessage(webhookUrl, text) { await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ msg_type: "text", content: { text } }) });
} async function sendTelegramMessage(botToken, chatId, text) { await fetch( `https://api.telegram.org/bot${botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'Markdown' }) } );
}
async function monitorPRs() { // Using GitHub REST API (no library needed) const response = await fetch( 'https://api.github.com/repos/:owner/:repo/pulls?state=open&per_page=30', { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'automation-bot' } } ); const prs = await response.json(); for (const pr of prs) { const lastCheck = getLastCheckTime(pr.id); // Check for new comments/reviews since last check const comments = await fetch(pr.comments_url, { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } }).then(r => r.json()); const newComments = comments.filter(c => new Date(c.created_at) > new Date(lastCheck) ); if (newComments.length > 0) { await notify(`PR #${pr.number} has ${newComments.length} new comment(s)!`); } updateCheckTime(pr.id); }
}
async function monitorPRs() { // Using GitHub REST API (no library needed) const response = await fetch( 'https://api.github.com/repos/:owner/:repo/pulls?state=open&per_page=30', { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'automation-bot' } } ); const prs = await response.json(); for (const pr of prs) { const lastCheck = getLastCheckTime(pr.id); // Check for new comments/reviews since last check const comments = await fetch(pr.comments_url, { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } }).then(r => r.json()); const newComments = comments.filter(c => new Date(c.created_at) > new Date(lastCheck) ); if (newComments.length > 0) { await notify(`PR #${pr.number} has ${newComments.length} new comment(s)!`); } updateCheckTime(pr.id); }
}
async function monitorPRs() { // Using GitHub REST API (no library needed) const response = await fetch( 'https://api.github.com/repos/:owner/:repo/pulls?state=open&per_page=30', { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}`, 'User-Agent': 'automation-bot' } } ); const prs = await response.json(); for (const pr of prs) { const lastCheck = getLastCheckTime(pr.id); // Check for new comments/reviews since last check const comments = await fetch(pr.comments_url, { headers: { 'Authorization': `token ${process.env.GITHUB_TOKEN}` } }).then(r => r.json()); const newComments = comments.filter(c => new Date(c.created_at) > new Date(lastCheck) ); if (newComments.length > 0) { await notify(`PR #${pr.number} has ${newComments.length} new comment(s)!`); } updateCheckTime(pr.id); }
}
# /etc/systemd/system/automation-bot.service
[Unit]
Description=Automation Bot
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/bot
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=10 [Install]
WantedBy=multi-user.target
# /etc/systemd/system/automation-bot.service
[Unit]
Description=Automation Bot
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/bot
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=10 [Install]
WantedBy=multi-user.target
# /etc/systemd/system/automation-bot.service
[Unit]
Description=Automation Bot
After=network.target [Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/bot
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=10 [Install]
WantedBy=multi-user.target
sudo systemctl enable automation-bot
sudo systemctl start automation-bot
# Now it auto-restarts on crash!
sudo systemctl enable automation-bot
sudo systemctl start automation-bot
# Now it auto-restarts on crash!
sudo systemctl enable automation-bot
sudo systemctl start automation-bot
# Now it auto-restarts on crash!
#!/bin/bash
# healthcheck.sh — run via system cron every 5 min
if ! pgrep -f "node index.js" > /dev/null; then echo "$(date): Bot is DOWN! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi # Also check: is it actually responding?
curl -sf http://localhost:3000/health || { echo "$(date): Bot not responding!" >> /var/log/bot-health.log sudo systemctl restart automation-bot
}
#!/bin/bash
# healthcheck.sh — run via system cron every 5 min
if ! pgrep -f "node index.js" > /dev/null; then echo "$(date): Bot is DOWN! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi # Also check: is it actually responding?
curl -sf http://localhost:3000/health || { echo "$(date): Bot not responding!" >> /var/log/bot-health.log sudo systemctl restart automation-bot
}
#!/bin/bash
# healthcheck.sh — run via system cron every 5 min
if ! pgrep -f "node index.js" > /dev/null; then echo "$(date): Bot is DOWN! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi # Also check: is it actually responding?
curl -sf http://localhost:3000/health || { echo "$(date): Bot not responding!" >> /var/log/bot-health.log sudo systemctl restart automation-bot
}
#!/bin/bash
# Check memory usage, restart if > 80%
MEM_PCT=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}')
if [ "$MEM_PCT" -gt 80 ]; then echo "$(date): Memory at ${MEM_PCT}%! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi
#!/bin/bash
# Check memory usage, restart if > 80%
MEM_PCT=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}')
if [ "$MEM_PCT" -gt 80 ]; then echo "$(date): Memory at ${MEM_PCT}%! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi
#!/bin/bash
# Check memory usage, restart if > 80%
MEM_PCT=$(free | awk '/Mem/{printf "%.0f", $3/$2*100}')
if [ "$MEM_PCT" -gt 80 ]; then echo "$(date): Memory at ${MEM_PCT}%! Restarting..." >> /var/log/bot-health.log sudo systemctl restart automation-bot
fi
process.on('SIGTERM', async () => { console.log('Shutting down gracefully...'); await saveState(); process.exit(0); });
process.on('SIGTERM', async () => { console.log('Shutting down gracefully...'); await saveState(); process.exit(0); });
process.on('SIGTERM', async () => { console.log('Shutting down gracefully...'); await saveState(); process.exit(0); }); - Check GitHub repos for new issues I could contribute to
- Monitor my pull requests for review comments
- Send daily status reports to my team
- Scan websites for price changes or content updates
- Push notifications when something important happened - Run 24/7 without me thinking about it
- Interact with web pages like a real browser
- Send messages to multiple platforms (Slack, Discord, email)
- Execute code and shell commands
- Cost less than a cup of coffee per month - Log everything. When something breaks at 3 AM, you need to know WHAT was happening, not just THAT it broke. Structured JSON logs > console.log.
- Don't poll too aggressively. Every API has rate limits. Start conservative (every 5-15 min), speed up only when you need to.
- Graceful shutdown. Handle SIGTERM so you can save state before dying: - Secrets belong in environment variables. Never hardcode tokens. Use a .env file (gitignored) or a secrets manager.
- Start small. My first version did ONE thing: check one GitHub repo every hour. Once that worked reliably for a week, I added another task. Rinse and repeat. - Content generation (auto-publish blog posts)
- Price monitoring (track products across sites)
- Competitor analysis (scrape and compare)
- Customer support (auto-respond to common queries)
- Data pipelines (collect → transform → store)