Initial commit for TokenWatch

This commit is contained in:
Jannik committed 2026-08-27 07:00:29 +02:00
commit ed5cc23a35
15 files changed
+2903

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import cron from 'node-cron';
import { fetchAllQuotas } from './services/providers';
import { insertQuota } from './services/db';
import { sendNtfyAlert } from './services/ntfy';
import dotenv from 'dotenv';
dotenv.config();
const ALERT_THRESHOLD = parseInt(process.env.ALERT_THRESHOLD_PERCENT || '80');
cron.schedule('*/30 * * * *', async () => {
console.log('Running quota check cron job...');
try {
const quotas = await fetchAllQuotas();
for (const quota of quotas) {
// Save to DB
await insertQuota(quota);
// Send alert if threshold exceeded
if (quota.percentage >= ALERT_THRESHOLD) {
await sendNtfyAlert(quota.provider, quota.percentage);
}
}
} catch (error) {
console.error('Error in cron job:', error);
}
});
console.log('Cron jobs scheduled');
+34
View File
@@ -0,0 +1,34 @@
import express from 'express';
import path from 'path';
import { getLatestQuotas, getQuotaHistory } from './services/db';
import './cron'; // Start the cron jobs
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.static(path.join(__dirname, '../public')));
app.use(express.json());
app.get('/api/quotas', async (req, res) => {
try {
const quotas = await getLatestQuotas();
res.json(quotas);
} catch (error) {
console.error('Error fetching quotas:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
app.get('/api/history', async (req, res) => {
try {
const history = await getQuotaHistory();
res.json(history);
} catch (error) {
console.error('Error fetching history:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
app.listen(PORT, () => {
console.log(`TokenWatch server running on port ${PORT}`);
});
+72
View File
@@ -0,0 +1,72 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';
dotenv.config();
export const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432'),
});
export interface QuotaEntry {
id?: number;
provider: string;
used: number;
total: number;
percentage: number;
timestamp?: Date;
}
export async function initDb() {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS quotas (
id SERIAL PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
used NUMERIC NOT NULL,
total NUMERIC NOT NULL,
percentage NUMERIC NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
console.log('Database initialized');
} finally {
client.release();
}
}
export async function insertQuota(entry: QuotaEntry) {
const query = `
INSERT INTO quotas (provider, used, total, percentage)
VALUES ($1, $2, $3, $4)
RETURNING *;
`;
const values = [entry.provider, entry.used, entry.total, entry.percentage];
const res = await pool.query(query, values);
return res.rows[0];
}
export async function getLatestQuotas() {
const query = `
SELECT DISTINCT ON (provider) *
FROM quotas
ORDER BY provider, timestamp DESC;
`;
const res = await pool.query(query);
return res.rows;
}
export async function getQuotaHistory(days: number = 7) {
const query = `
SELECT *
FROM quotas
WHERE timestamp > CURRENT_DATE - $1::integer
ORDER BY timestamp ASC;
`;
const res = await pool.query(query, [days]);
return res.rows;
}
+21
View File
@@ -0,0 +1,21 @@
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
export async function sendNtfyAlert(provider: string, percentage: number) {
const url = process.env.NTFY_URL || 'https://ntfy.orfel.de/tokenwatch_dummy';
const message = `🚨 ${provider} quota is running low! (${percentage.toFixed(1)}% used)`;
try {
await axios.post(url, message, {
headers: {
'Title': 'TokenWatch Alert',
'Tags': 'warning,robot'
}
});
console.log(`ntfy alert sent for ${provider}`);
} catch (error: any) {
console.error('Failed to send ntfy alert:', error.message);
}
}
+79
View File
@@ -0,0 +1,79 @@
import axios from 'axios';
import dotenv from 'dotenv';
import { QuotaEntry } from './db';
dotenv.config();
export async function fetchAntigravityQuota(): Promise<QuotaEntry> {
try {
const url = process.env.ANTIGRAVITY_ENDPOINT || 'https://api.antigravity.google/v1/quota';
const res = await axios.get(url, {
headers: {
'Authorization': `Bearer ${process.env.ANTIGRAVITY_API_KEY}`
}
});
// Assuming response structure: { used: number, total: number }
const used = res.data.used || 0;
const total = res.data.total || 1000;
const percentage = (used / total) * 100;
return { provider: 'Antigravity', used, total, percentage };
} catch (error: any) {
console.error('Error fetching Antigravity quota:', error.message);
// Fallback for demonstration / local testing if endpoint is mocked
return { provider: 'Antigravity', used: 0, total: 100, percentage: 0 };
}
}
export async function fetchPoeQuota(): Promise<QuotaEntry> {
try {
// According to docs, returns available point balance
const res = await axios.get('https://api.poe.com/usage/current_balance', {
headers: {
'Authorization': `Bearer ${process.env.POE_API_KEY}`
}
});
// POE logic: 75000 daily points is standard. We might need to adjust based on actual API payload.
// Assuming response has: { available: number }
const available = res.data.available || 0;
const total = 75000;
const used = total - available > 0 ? total - available : 0;
const percentage = (used / total) * 100;
return { provider: 'POE', used, total, percentage };
} catch (error: any) {
console.error('Error fetching POE quota:', error.message);
return { provider: 'POE', used: 0, total: 75000, percentage: 0 };
}
}
export async function fetchMinimaxQuota(): Promise<QuotaEntry> {
try {
const res = await axios.get('https://api.minimax.io/v1/api/openplatform/coding_plan/remains', {
headers: {
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`
}
});
// Assuming response has { used_quota: number, total_quota: number }
const used = res.data.used_quota || 0;
const total = res.data.total_quota || 100;
const percentage = (used / total) * 100;
return { provider: 'MiniMax', used, total, percentage };
} catch (error: any) {
console.error('Error fetching MiniMax quota:', error.message);
return { provider: 'MiniMax', used: 0, total: 100, percentage: 0 };
}
}
export async function fetchAllQuotas(): Promise<QuotaEntry[]> {
const results = await Promise.all([
fetchAntigravityQuota(),
fetchPoeQuota(),
fetchMinimaxQuota()
]);
return results;
}