Added simple API, updated README

This commit is contained in:
Vítor Vieira 2025-11-28 18:19:30 +00:00
parent 5d2710350e
commit a7a7e2058c
8 changed files with 109 additions and 5 deletions

View file

@ -15,7 +15,7 @@ npm install
### 2. Run the Bot ### 2. Run the Bot
**Basic:** **With default parameters:**
```bash ```bash
node index.js node index.js
``` ```
@ -55,7 +55,7 @@ cp .env.example .env
# THRESHOLD=0.01 # THRESHOLD=0.01
# Start services # Start services
docker compose up -d docker compose up -d --build
# View logs # View logs
docker compose logs -f bot docker compose logs -f bot

View file

@ -1,10 +1,13 @@
services: services:
bot: bot:
build: . build: .
ports:
- "3000:3000"
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
environment: environment:
- PORT=3000
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
- PAIRS=${PAIRS:-BTC-USD} - PAIRS=${PAIRS:-BTC-USD}
- INTERVAL=${INTERVAL:-5000} - INTERVAL=${INTERVAL:-5000}
@ -21,7 +24,7 @@ services:
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U uphold -d uphold_alerts"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-uphold} -d ${POSTGRES_DB:-uphold_alerts}"]
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5

View file

@ -4,6 +4,7 @@ import { prefetchRates } from "./src/api.js";
import { Bot } from "./src/bot.js"; import { Bot } from "./src/bot.js";
import logger from "./src/logger.js"; import logger from "./src/logger.js";
import { initDB, insertIntoDB, closePool } from "./src/db.js"; import { initDB, insertIntoDB, closePool } from "./src/db.js";
import { startServer } from "./src/server.js";
process.on("uncaughtException", (err) => { process.on("uncaughtException", (err) => {
logger.fatal(err, "Uncaught exception. Application crashing..."); logger.fatal(err, "Uncaught exception. Application crashing...");
@ -69,6 +70,7 @@ async function main() {
); );
await initDB(); await initDB();
startServer();
try { try {
await prefetchRates(pairs); await prefetchRates(pairs);

View file

@ -1,6 +1,7 @@
import { BigNumber } from "bignumber.js"; import { BigNumber } from "bignumber.js";
import PQueue from "p-queue"; import PQueue from "p-queue";
import logger from "./logger.js"; import logger from "./logger.js";
import { incrementReq } from "./stats.js";
const queue = new PQueue({ interval: 1000, intervalCap: 10 }); const queue = new PQueue({ interval: 1000, intervalCap: 10 });
@ -19,8 +20,17 @@ export async function fetchRate(pair) {
clearTimeout(timeout); clearTimeout(timeout);
if (res.status === 429) throw new Error("Rate limited"); if (res.status === 429) {
if (!res.ok) throw new Error(`HTTP ${res.status}`); incrementReq("ratelimit");
throw new Error("Rate limited");
}
if (!res.ok) {
incrementReq("fail");
throw new Error(`HTTP ${res.status}`);
}
incrementReq("success");
const data = await res.json(); const data = await res.json();

View file

@ -1,5 +1,6 @@
import { fetchRate } from "./api.js"; import { fetchRate } from "./api.js";
import logger from "./logger.js"; import logger from "./logger.js";
import { updatePair, incrementAlerts } from "./stats.js";
export class Bot { export class Bot {
constructor(config, onAlert) { constructor(config, onAlert) {
@ -32,6 +33,8 @@ export class Bot {
try { try {
const price = await fetchRate(this.pair); const price = await fetchRate(this.pair);
updatePair(this.pair, price.toFixed(6));
if (!this.lastPrice) { if (!this.lastPrice) {
this.lastPrice = price; this.lastPrice = price;
logger.info(`[${this.pair}] Initial price: ${price.toFixed(2)}`); logger.info(`[${this.pair}] Initial price: ${price.toFixed(2)}`);
@ -42,6 +45,7 @@ export class Bot {
.multipliedBy(100); .multipliedBy(100);
if (change.abs().gte(this.threshold)) { if (change.abs().gte(this.threshold)) {
incrementAlerts();
const dir = change.gt(0) ? "UP" : "DOWN"; const dir = change.gt(0) ? "UP" : "DOWN";
logger.info( logger.info(
{ {

View file

@ -111,6 +111,14 @@ export async function insertIntoDB(data) {
} }
} }
export async function getRecentAlerts(limit = 50) {
const res = await pool.query(
"SELECT * FROM alerts ORDER BY created_at DESC LIMIT $1",
[limit],
);
return res.rows;
}
export async function closePool() { export async function closePool() {
if (pool) { if (pool) {
await pool.end(); await pool.end();

46
src/server.js Normal file
View file

@ -0,0 +1,46 @@
import http from "node:http";
import { URL } from "node:url";
import { stats } from "./stats.js";
import { getRecentAlerts } from "./db.js";
import logger from "./logger.js";
const PORT = process.env.PORT || 3000;
const sendJSON = (res, data, status = 200) => {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(data, null, 2));
};
const server = http.createServer(async (req, res) => {
try {
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const { pathname, searchParams } = parsedUrl;
if (req.method === "GET" && pathname === "/health") {
return sendJSON(res, { status: "ok", uptime: process.uptime() });
}
if (req.method === "GET" && pathname === "/stats") {
return sendJSON(res, stats);
}
if (req.method === "GET" && pathname === "/alerts") {
const limit = parseInt(searchParams.get("limit")) || 50;
const alerts = await getRecentAlerts(limit);
return sendJSON(res, { count: alerts.length, data: alerts });
}
// 404
sendJSON(res, { error: "Not Found" }, 404);
} catch (err) {
logger.error(err, "API Error");
sendJSON(res, { error: "Internal Server Error" }, 500);
}
});
export const startServer = () => {
server.listen(PORT, () => {
logger.info(`[API] Server listening on port ${PORT}`);
});
return server;
};

31
src/stats.js Normal file
View file

@ -0,0 +1,31 @@
export const stats = {
startedAt: new Date().toISOString(),
api: {
totalRequests: 0,
successful: 0,
failed: 0,
rateLimited: 0,
},
bot: {
totalAlerts: 0,
activePairs: {},
},
};
export const incrementReq = (type) => {
stats.api.totalRequests++;
if (type === "success") stats.api.successful++;
if (type === "fail") stats.api.failed++;
if (type === "ratelimit") stats.api.rateLimited++;
};
export const updatePair = (pair, price) => {
stats.bot.activePairs[pair] = {
currentPrice: price,
lastUpdate: new Date().toISOString(),
};
};
export const incrementAlerts = () => {
stats.bot.totalAlerts++;
};