Initial release — Crucix Intelligence Engine v2.0.0

26-source OSINT intelligence engine with live Jarvis dashboard,
auto-refresh via SSE, optional LLM layer (4 providers), delta/memory
system, and Telegram breaking news alerts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
calesthio
2026-03-12 23:45:46 -07:00
commit ef2c6470fb
53 changed files with 8709 additions and 0 deletions

220
apis/BRIEFING_PROMPT.md Normal file
View File

@@ -0,0 +1,220 @@
# Crucix Intelligence Briefing Protocol
When the user says "brief me", "what's the latest", "what's going on", or asks for a world update, the goal is to answer one question first:
**How can the user leverage this information?**
The briefing is not a neutral recap. It is a leverage-first intelligence note built from cross-domain signals, historical pattern matching, and a concrete point of view.
## What the analyst must do
- detect regime shifts early
- connect hard data and weak signals
- distinguish what matters from noise
- form a coherent worldview
- map that worldview into positioning, hedging, and watchlists
The user wants signal, judgment, and utility.
## Step 1: Gather Inputs
Run the full Crucix sweep:
```bash
cd C:/Users/ishan/Documents/Crucix && node apis/briefing.mjs 2>&1
```
Also gather:
- live market context via Alpaca MCP for broad indexes, rates proxies, commodities, metals, and crypto
- breaking developments from the last 6 hours via web search
- official statements, policy moves, or confirmed reports that materially change the read
## Step 2: Think Before Writing
Before drafting, answer these questions internally:
1. What changed?
2. Which signals are confirmed by multiple sources?
3. What regime is emerging?
4. What is likely to happen next if this continues?
5. What can the user do with that information now?
Do not overweight noisy social sources. Treat Telegram, Reddit, and similar feeds as accelerants unless confirmed by harder data.
## Step 3: Use the Standard Output Order
Always structure the briefing in this order:
1. Leverageable Ideas
2. Executive Thesis
3. Situation Awareness
4. Pattern Recognition
5. Historical Parallels
6. Market and Asset Implications
7. Decision Board
8. Source Integrity
## Section Requirements
### 1. Leverageable Ideas
Start here. This is the most important section.
Provide 3-5 specific ideas. Each idea must include:
- thesis
- instrument, sector, geography, or behavior
- why now
- time horizon: days, weeks, or months
- catalyst(s) to watch
- invalidation criteria
- confidence: High, Medium, or Low
Examples:
- "Accumulate gold over the next 1-3 months if conflict-energy stress continues to broaden."
- "Buy downside protection if health or macro stress signals keep confirming across official and market data."
Bad output:
- "Watch metals"
- "Keep an eye on volatility"
Good output:
- "Gold remains the cleanest hedge against war-driven inflation stress; accumulate on consolidation with a 1-3 month horizon."
### 2. Executive Thesis
State the worldview clearly:
- the 1-3 most important things happening
- the regime you believe is forming
- the single most important implication for the user
Write this as a strong view, not hedged filler.
### 3. Situation Awareness
Identify the top 3-5 global developments right now.
For each:
- what happened
- who is involved
- why it matters
- what changes because of it
Categories:
- CONFLICT
- ECONOMIC
- HEALTH
- CLIMATE
- TECHNOLOGY
- POLICY
### 4. Pattern Recognition
This is the core of Crucix.
Cross-correlate across sources and surface non-obvious patterns such as:
- conflict plus energy plus inflation
- macro weakness plus market stress
- health signals plus travel or sentiment shifts
- sanctions plus logistics or trade anomalies
- weather plus shipping plus supply chain disruption
For each major pattern, state:
- evidence
- why it matters
- whether it is strengthening, stable, or fading
- what would invalidate the interpretation
### 5. Historical Parallels
Ask: what does this rhyme with?
Useful comparisons may include:
- early 2020 health-risk buildup
- 2007-2008 financial deterioration
- 2021-2022 inflation and commodity shock
- pre-invasion 2022 Europe escalation
- prior oil, metals, or volatility regimes
For each parallel:
- what matched
- what is different this time
- what happened next historically
- where the current setup sits in that sequence
### 6. Market and Asset Implications
Translate the worldview into consequences for:
- equities
- bonds and rates
- commodities
- gold and silver
- oil and gas
- crypto
- sectors, countries, or themes likely to outperform or underperform
Be explicit on direction when the evidence supports it.
### 7. Decision Board
Close with a concise action board:
- best long
- best hedge
- best watchlist item
- biggest unresolved question
- what to monitor in the next 24-72 hours
### 8. Source Integrity
Briefly state:
- which sources returned meaningful data
- which were degraded, stale, missing, or stubbed
- where the thesis relies on hard data versus softer signals
## Quality Bar
The briefing should read like a private note from a sharp global macro and intelligence analyst:
- early
- synthetic
- opinionated
- evidence-backed
- useful for action
Avoid:
- generic recaps
- long raw-data summaries
- false precision
- unsupported conviction
- laundry lists without a thesis
## Handling Uncertainty
If the evidence is mixed:
- give the base case
- give the upside or escalation case
- give the downside or de-escalation case
If confidence is low, still provide the best current interpretation and explain what confirmation is needed next.
## Remember
- The product is valuable when it spots a shift before the crowd.
- The user wants a worldview they can use.
- Always start with leverage.

106
apis/BRIEFING_TEMPLATE.md Normal file
View File

@@ -0,0 +1,106 @@
# Crucix Briefing Template
Use this output shape for every intelligence briefing.
## 1. Leverageable Ideas
### Idea 1
- Thesis:
- Exposure:
- Why now:
- Time horizon:
- Catalysts:
- Invalidation:
- Confidence:
### Idea 2
- Thesis:
- Exposure:
- Why now:
- Time horizon:
- Catalysts:
- Invalidation:
- Confidence:
### Idea 3
- Thesis:
- Exposure:
- Why now:
- Time horizon:
- Catalysts:
- Invalidation:
- Confidence:
## 2. Executive Thesis
- Regime forming:
- What matters most:
- Main implication for the user:
## 3. Situation Awareness
### Event 1
- Category:
- What happened:
- Why it matters:
- What changes:
### Event 2
- Category:
- What happened:
- Why it matters:
- What changes:
### Event 3
- Category:
- What happened:
- Why it matters:
- What changes:
## 4. Pattern Recognition
### Pattern 1
- Evidence:
- Interpretation:
- Direction:
- Invalidation:
### Pattern 2
- Evidence:
- Interpretation:
- Direction:
- Invalidation:
## 5. Historical Parallels
### Parallel 1
- Analog:
- What matches:
- What is different:
- What happened next:
- Current position in sequence:
## 6. Market and Asset Implications
- Equities:
- Bonds and rates:
- Commodities:
- Gold and silver:
- Oil and gas:
- Crypto:
- Sector and country effects:
## 7. Decision Board
- Best long:
- Best hedge:
- Best watchlist item:
- Biggest unresolved question:
- Monitor in the next 24-72 hours:
## 8. Source Integrity
- Strong sources this run:
- Weak or degraded sources:
- Hard-data core:
- Soft-signal support:

124
apis/briefing.mjs Normal file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env node
// Crucix Master Orchestrator — runs all intelligence sources in parallel
// Outputs structured JSON for Claude to synthesize into actionable briefing
import './utils/env.mjs'; // Load API keys from .env
import { pathToFileURL } from 'node:url';
// === Tier 1: Core OSINT & Geopolitical ===
import { briefing as gdelt } from './sources/gdelt.mjs';
import { briefing as opensky } from './sources/opensky.mjs';
import { briefing as firms } from './sources/firms.mjs';
import { briefing as ships } from './sources/ships.mjs';
import { briefing as safecast } from './sources/safecast.mjs';
import { briefing as acled } from './sources/acled.mjs';
import { briefing as reliefweb } from './sources/reliefweb.mjs';
import { briefing as who } from './sources/who.mjs';
import { briefing as ofac } from './sources/ofac.mjs';
import { briefing as opensanctions } from './sources/opensanctions.mjs';
import { briefing as adsb } from './sources/adsb.mjs';
// === Tier 2: Economic & Financial ===
import { briefing as fred } from './sources/fred.mjs';
import { briefing as treasury } from './sources/treasury.mjs';
import { briefing as bls } from './sources/bls.mjs';
import { briefing as eia } from './sources/eia.mjs';
import { briefing as gscpi } from './sources/gscpi.mjs';
import { briefing as usaspending } from './sources/usaspending.mjs';
import { briefing as comtrade } from './sources/comtrade.mjs';
// === Tier 3: Weather, Environment, Technology, Social ===
import { briefing as noaa } from './sources/noaa.mjs';
import { briefing as epa } from './sources/epa.mjs';
import { briefing as patents } from './sources/patents.mjs';
import { briefing as bluesky } from './sources/bluesky.mjs';
import { briefing as reddit } from './sources/reddit.mjs';
import { briefing as telegram } from './sources/telegram.mjs';
import { briefing as kiwisdr } from './sources/kiwisdr.mjs';
// === Tier 4: Live Market Data ===
import { briefing as yfinance } from './sources/yfinance.mjs';
export async function runSource(name, fn, ...args) {
const start = Date.now();
try {
const data = await fn(...args);
return { name, status: 'ok', durationMs: Date.now() - start, data };
} catch (e) {
return { name, status: 'error', durationMs: Date.now() - start, error: e.message };
}
}
export async function fullBriefing() {
console.error('[Crucix] Starting intelligence sweep — 26 sources...');
const start = Date.now();
const results = await Promise.allSettled([
// Tier 1: Core OSINT & Geopolitical
runSource('GDELT', gdelt),
runSource('OpenSky', opensky),
runSource('FIRMS', firms),
runSource('Maritime', ships),
runSource('Safecast', safecast),
runSource('ACLED', acled),
runSource('ReliefWeb', reliefweb),
runSource('WHO', who),
runSource('OFAC', ofac),
runSource('OpenSanctions', opensanctions),
runSource('ADS-B', adsb),
// Tier 2: Economic & Financial
runSource('FRED', fred, process.env.FRED_API_KEY),
runSource('Treasury', treasury),
runSource('BLS', bls, process.env.BLS_API_KEY),
runSource('EIA', eia, process.env.EIA_API_KEY),
runSource('GSCPI', gscpi),
runSource('USAspending', usaspending),
runSource('Comtrade', comtrade),
// Tier 3: Weather, Environment, Technology, Social
runSource('NOAA', noaa),
runSource('EPA', epa),
runSource('Patents', patents),
runSource('Bluesky', bluesky),
runSource('Reddit', reddit),
runSource('Telegram', telegram),
runSource('KiwiSDR', kiwisdr),
// Tier 4: Live Market Data
runSource('YFinance', yfinance),
]);
const sources = results.map(r => r.status === 'fulfilled' ? r.value : { status: 'failed', error: r.reason?.message });
const totalMs = Date.now() - start;
const output = {
crucix: {
version: '2.0.0',
timestamp: new Date().toISOString(),
totalDurationMs: totalMs,
sourcesQueried: sources.length,
sourcesOk: sources.filter(s => s.status === 'ok').length,
sourcesFailed: sources.filter(s => s.status !== 'ok').length,
},
sources: Object.fromEntries(
sources.filter(s => s.status === 'ok').map(s => [s.name, s.data])
),
errors: sources.filter(s => s.status !== 'ok').map(s => ({ name: s.name, error: s.error })),
timing: Object.fromEntries(
sources.map(s => [s.name, { status: s.status, ms: s.durationMs }])
),
};
console.error(`[Crucix] Sweep complete in ${totalMs}ms — ${output.crucix.sourcesOk}/${sources.length} sources returned data`);
return output;
}
// Run and output when executed directly
const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entryHref && import.meta.url === entryHref) {
const data = await fullBriefing();
console.log(JSON.stringify(data, null, 2));
}

24
apis/save-briefing.mjs Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { fullBriefing } from './briefing.mjs';
function formatTimestamp(date = new Date()) {
return date.toISOString().replace(/[:]/g, '-').replace(/\.\d{3}Z$/, 'Z');
}
const runsDir = join(process.cwd(), 'runs');
await mkdir(runsDir, { recursive: true });
const data = await fullBriefing();
const json = JSON.stringify(data, null, 2);
const timestamp = formatTimestamp(new Date(data.crucix.timestamp));
const runFile = join(runsDir, `briefing_${timestamp}.json`);
const latestFile = join(runsDir, 'latest.json');
await writeFile(runFile, json, 'utf8');
await writeFile(latestFile, json, 'utf8');
console.error(`[Crucix] Saved UTF-8 briefing to ${runFile}`);
console.log(json);

316
apis/sources/acled.mjs Normal file
View File

@@ -0,0 +1,316 @@
// ACLED — Armed Conflict Location & Event Data
// Auth strategy (tries in order):
// 1. Cookie-based session: POST /user/login?_format=json → session cookie
// 2. OAuth Bearer token: POST /oauth/token → Authorization header
// Set ACLED_EMAIL and ACLED_PASSWORD in .env (your myACLED login credentials).
// Data endpoint: GET https://acleddata.com/api/acled/read
import { daysAgo } from '../utils/fetch.mjs';
import '../utils/env.mjs';
const LOGIN_URL = 'https://acleddata.com/user/login?_format=json';
const TOKEN_URL = 'https://acleddata.com/oauth/token';
const API_BASE = 'https://acleddata.com/api/acled/read';
// Session cache
let sessionCache = { cookies: null, token: null, method: null, expires: 0 };
// Strategy 1: Cookie-based session login (mirrors browser login)
async function loginCookie(email, password) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
try {
const res = await fetch(LOGIN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: email, pass: password }),
redirect: 'manual',
signal: controller.signal,
});
clearTimeout(timer);
// Collect Set-Cookie headers
const setCookies = res.headers.getSetCookie?.() || [];
const cookieStr = setCookies.map(c => c.split(';')[0]).join('; ');
if (res.ok && cookieStr) {
return { cookies: cookieStr };
}
// Some Drupal sites return 303 redirect on successful login — cookies still set
if (res.status >= 300 && res.status < 400 && cookieStr) {
return { cookies: cookieStr };
}
const errText = await res.text().catch(() => '');
return { error: `Cookie login failed (HTTP ${res.status}): ${errText.slice(0, 200)}` };
} catch (e) {
clearTimeout(timer);
const cause = e.cause ? `${e.cause.message || e.cause.code || e.cause}` : '';
return { error: `Cookie login error: ${e.message}${cause}` };
}
}
// Strategy 2: OAuth2 password grant
async function loginOAuth(email, password) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
try {
const body = new URLSearchParams({
username: email,
password: password,
grant_type: 'password',
client_id: 'acled',
});
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) {
const errText = await res.text().catch(() => '');
return { error: `OAuth failed (HTTP ${res.status}): ${errText.slice(0, 200)}` };
}
const data = await res.json();
if (!data.access_token) {
return { error: `OAuth response missing access_token: ${JSON.stringify(data).slice(0, 200)}` };
}
return { token: data.access_token };
} catch (e) {
clearTimeout(timer);
const cause = e.cause ? `${e.cause.message || e.cause.code || e.cause}` : '';
return { error: `OAuth error: ${e.message}${cause}` };
}
}
// Try both auth strategies
async function authenticate() {
const email = process.env.ACLED_EMAIL;
const password = process.env.ACLED_PASSWORD;
if (!email || !password) {
return { error: 'No ACLED credentials. Set ACLED_EMAIL and ACLED_PASSWORD in .env.' };
}
// Return cached session if still valid
if (sessionCache.method && Date.now() < sessionCache.expires) {
return sessionCache;
}
const errors = [];
const debug = process.argv.includes('--debug');
// Try OAuth first (official programmatic method per ACLED docs)
const oauthResult = await loginOAuth(email, password);
if (oauthResult.token) {
if (debug) console.error(`[ACLED DEBUG] OAuth OK — token: ${oauthResult.token.slice(0, 20)}...`);
sessionCache = { cookies: null, token: oauthResult.token, method: 'oauth', expires: Date.now() + 23 * 60 * 60 * 1000 };
return sessionCache;
}
errors.push(`OAuth: ${oauthResult.error}`);
if (debug) console.error(`[ACLED DEBUG] OAuth failed: ${oauthResult.error}`);
// Fall back to cookie-based session
const cookieResult = await loginCookie(email, password);
if (cookieResult.cookies) {
if (debug) console.error(`[ACLED DEBUG] Cookie OK — cookies: ${cookieResult.cookies.slice(0, 80)}...`);
sessionCache = { cookies: cookieResult.cookies, token: null, method: 'cookie', expires: Date.now() + 12 * 60 * 60 * 1000 };
return sessionCache;
}
errors.push(`Cookie: ${cookieResult.error}`);
return { error: `All ACLED auth methods failed.\n${errors.join('\n')}` };
}
// Build headers based on auth method
function authHeaders(session) {
const headers = { 'User-Agent': 'Crucix/1.0', 'Content-Type': 'application/json' };
if (session.method === 'cookie' && session.cookies) {
headers['Cookie'] = session.cookies;
} else if (session.method === 'oauth' && session.token) {
headers['Authorization'] = `Bearer ${session.token}`;
}
return headers;
}
// Event type constants
export const EVENT_TYPES = [
'Battles',
'Explosions/Remote violence',
'Violence against civilians',
'Protests',
'Riots',
'Strategic developments',
];
// Query conflict events with flexible filters
export async function getEvents(opts = {}) {
const {
limit = 500,
eventDateStart,
eventDateEnd,
eventType,
country,
region,
} = opts;
const session = await authenticate();
if (session.error) return { error: session.error };
const params = new URLSearchParams({ _format: 'json', limit: String(limit) });
if (eventDateStart && eventDateEnd) {
params.set('event_date', `${eventDateStart}|${eventDateEnd}`);
params.set('event_date_where', 'BETWEEN');
}
if (eventType) params.set('event_type', eventType);
if (country) params.set('country', country);
if (region) params.set('region', String(region));
const debug = process.argv.includes('--debug');
try {
const url = `${API_BASE}?${params}`;
const hdrs = authHeaders(session);
if (debug) {
console.error(`[ACLED DEBUG] Data request: GET ${url}`);
console.error(`[ACLED DEBUG] Headers: ${JSON.stringify(hdrs)}`);
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 25000);
const res = await fetch(url, {
headers: hdrs,
signal: controller.signal,
});
clearTimeout(timer);
if (debug) console.error(`[ACLED DEBUG] Data response: HTTP ${res.status}`);
if (!res.ok) {
const errText = await res.text().catch(() => '');
if (debug) console.error(`[ACLED DEBUG] Error body: ${errText.slice(0, 500)}`);
if (res.status === 401 || res.status === 403) {
// Clear cache and report
sessionCache = { cookies: null, token: null, method: null, expires: 0 };
const hint = res.status === 403
? '\n→ Fix: Log in at https://acleddata.com/user/login, then:\n'
+ ' 1. Accept Terms of Use (profile → Terms of Use button → check the box)\n'
+ ' 2. Complete all required profile fields\n'
+ ' 3. Ensure your account has the "API" access group\n'
+ ' Contact access@acleddata.com if issues persist.'
: '';
return { error: `ACLED data access denied (HTTP ${res.status}, auth method: ${session.method}). Response: ${errText.slice(0, 300)}${hint}` };
}
return { error: `HTTP ${res.status}: ${errText.slice(0, 200)}` };
}
const data = await res.json();
// ACLED may return a 200 with an error status in the body
if (data?.status && data.status !== 200) {
return { error: `ACLED API error: status ${data.status}${data.message || 'Unknown error'}` };
}
return data;
} catch (e) {
if (e.name === 'AbortError') {
return { error: 'ACLED data request timed out (25s)' };
}
const rootCause = e.cause ? `${e.cause.message || e.cause.code || e.cause}` : '';
return { error: `ACLED data error: ${e.message}${rootCause ? ' → ' + rootCause : ''}` };
}
}
// Summarize events by a given field
function groupBy(events, field) {
const map = {};
for (const e of events) {
const key = e[field] || 'Unknown';
if (!map[key]) map[key] = { count: 0, fatalities: 0 };
map[key].count += 1;
map[key].fatalities += parseInt(e.fatalities, 10) || 0;
}
return map;
}
// Briefing — last 7 days of global conflict events
export async function briefing() {
if (!process.env.ACLED_EMAIL || !process.env.ACLED_PASSWORD) {
return {
source: 'ACLED',
timestamp: new Date().toISOString(),
status: 'no_credentials',
message: 'Set ACLED_EMAIL and ACLED_PASSWORD in .env. Register at https://acleddata.com/user/register',
};
}
const start = daysAgo(7);
const end = daysAgo(0);
const data = await getEvents({
eventDateStart: start,
eventDateEnd: end,
limit: 2000,
});
if (data?.error) {
return { source: 'ACLED', timestamp: new Date().toISOString(), error: data.error };
}
let events = data?.data || [];
// Enrich all events with numeric lat/lon
events = events.map(e => ({
...e,
lat: parseFloat(e.latitude) || null,
lon: parseFloat(e.longitude) || null,
}));
const totalFatalities = events.reduce(
(sum, e) => sum + (parseInt(e.fatalities, 10) || 0), 0
);
const byRegion = groupBy(events, 'region');
const byType = groupBy(events, 'event_type');
const byCountry = groupBy(events, 'country');
const topCountries = Object.entries(byCountry)
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 10)
.reduce((obj, [k, v]) => { obj[k] = v; return obj; }, {});
const deadliestEvents = events
.filter(e => parseInt(e.fatalities, 10) > 0)
.sort((a, b) => (parseInt(b.fatalities, 10) || 0) - (parseInt(a.fatalities, 10) || 0))
.slice(0, 15)
.map(e => ({
date: e.event_date,
type: e.event_type,
subType: e.sub_event_type,
country: e.country,
location: e.location,
fatalities: parseInt(e.fatalities, 10) || 0,
lat: parseFloat(e.latitude) || null,
lon: parseFloat(e.longitude) || null,
notes: e.notes?.slice(0, 200),
}));
return {
source: 'ACLED',
timestamp: new Date().toISOString(),
period: { start, end },
totalEvents: events.length,
totalFatalities,
byRegion,
byType,
topCountries,
deadliestEvents,
};
}
if (process.argv[1]?.endsWith('acled.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

302
apis/sources/adsb.mjs Normal file
View File

@@ -0,0 +1,302 @@
// ADS-B Exchange — Unfiltered Flight Tracking (including Military)
// Unlike FlightRadar24/FlightAware, ADS-B Exchange does NOT filter military aircraft.
// Public feed access varies; RapidAPI tier available for programmatic use.
// This module attempts the public endpoints and falls back to a documented stub.
import { safeFetch } from '../utils/fetch.mjs';
// Known endpoints (availability may change)
const ENDPOINTS = {
// v2 API via RapidAPI (requires ADSB_API_KEY)
rapidApi: 'https://adsbexchange-com1.p.rapidapi.com/v2',
// Public globe feed (may be rate-limited or blocked for automated access)
publicFeed: 'https://globe.adsbexchange.com/data/aircraft.json',
// Alternative: aircraft within bounding box
publicTrace: 'https://globe.adsbexchange.com/data/traces',
};
// Known military aircraft types and ICAO type designators
const MILITARY_TYPES = {
// Reconnaissance / ISR
'RC135': 'RC-135 Rivet Joint (SIGINT)',
'E3CF': 'E-3 Sentry AWACS',
'E3TF': 'E-3 Sentry AWACS',
'E6B': 'E-6B Mercury (TACAMO)',
'EP3': 'EP-3 Aries (SIGINT)',
'P8': 'P-8 Poseidon (Maritime Patrol)',
'P8A': 'P-8A Poseidon',
'RQ4': 'RQ-4 Global Hawk (UAV)',
'RQ4B': 'RQ-4B Global Hawk',
'U2': 'U-2 Dragon Lady',
'MQ9': 'MQ-9 Reaper (UAV)',
'MQ1': 'MQ-1 Predator (UAV)',
'E8': 'E-8 JSTARS',
// Tankers
'KC135': 'KC-135 Stratotanker',
'KC10': 'KC-10 Extender',
'KC46': 'KC-46 Pegasus',
// Bombers
'B52': 'B-52 Stratofortress',
'B1': 'B-1B Lancer',
'B2': 'B-2 Spirit',
// Transport / Special
'C17': 'C-17 Globemaster III',
'C5': 'C-5 Galaxy',
'C130': 'C-130 Hercules',
'VC25': 'VC-25 (Air Force One)',
'E4B': 'E-4B Nightwatch (Doomsday Plane)',
'C32': 'C-32 (Air Force Two)',
'C40': 'C-40 Clipper',
};
// Known military ICAO hex ranges (partial — US military allocations)
const MIL_HEX_RANGES = [
{ start: 0xADF7C8, end: 0xAFFFFF, country: 'US Military' },
{ start: 0xAE0000, end: 0xAFFFFF, country: 'US Military (alt)' },
{ start: 0x43C000, end: 0x43CFFF, country: 'UK Military' },
{ start: 0x3F0000, end: 0x3FFFFF, country: 'France Military' },
{ start: 0x3CC000, end: 0x3CFFFF, country: 'Germany Military' },
];
// Interesting callsign patterns that suggest military/government flights
const MIL_CALLSIGN_PATTERNS = [
/^RCH/, // US AMC (Air Mobility Command) — strategic airlift
/^REACH/, // US AMC alternate
/^DUKE/, // Often military special ops
/^IRON/, // US military
/^JAKE/, // Military
/^NAVY/, // US Navy
/^TOPCAT/, // E-6B Mercury
/^DARKST/, // Dark Star / classified
/^GORDO/, // USAF
/^BISON/, // B-52
/^DEATH/, // B-1B
/^DOOM/, // E-4B
/^SAM/, // Special Air Mission (VIP)
/^EXEC/, // Executive transport
/^PCSF/, // Chinese military
/^CHN/, // Chinese military
/^RF/, // Russian Air Force
/^RFF/, // Russian Air Force
];
// Check if an ICAO hex code falls in known military ranges
function isMilitaryHex(hex) {
if (!hex) return false;
const num = parseInt(hex, 16);
if (isNaN(num)) return false;
return MIL_HEX_RANGES.find(r => num >= r.start && num <= r.end) || null;
}
// Check if a callsign matches military patterns
function isMilitaryCallsign(callsign) {
if (!callsign) return false;
const cs = callsign.trim().toUpperCase();
return MIL_CALLSIGN_PATTERNS.some(p => p.test(cs));
}
// Check if aircraft type is a known military type
function isMilitaryType(typeCode) {
if (!typeCode) return false;
const tc = typeCode.toUpperCase().replace(/[^A-Z0-9]/g, '');
return MILITARY_TYPES[tc] || null;
}
// Classify an aircraft from ADS-B data
function classifyAircraft(ac) {
const hex = ac.hex || ac.icao || ac.icao24 || null;
const callsign = ac.flight || ac.callsign || ac.call || '';
const type = ac.t || ac.type || ac.typecode || '';
const mil = ac.mil || ac.military || false;
const milHex = isMilitaryHex(hex);
const milCall = isMilitaryCallsign(callsign);
const milType = isMilitaryType(type);
const isMilitary = !!(mil || milHex || milCall || milType);
return {
hex,
callsign: callsign.trim(),
type,
typeDescription: milType || null,
latitude: ac.lat || ac.latitude || null,
longitude: ac.lon || ac.longitude || null,
altitude: ac.alt_baro || ac.alt_geom || ac.altitude || null,
speed: ac.gs || ac.speed || null,
heading: ac.track || ac.heading || null,
squawk: ac.squawk || null,
isMilitary,
militaryMatch: milHex?.country || (milCall ? 'callsign pattern' : null) || (milType ? 'type match' : null),
registration: ac.r || ac.registration || null,
seen: ac.seen || ac.last_contact || null,
};
}
// Attempt to fetch from RapidAPI (requires ADSB_API_KEY)
async function fetchViaRapidApi(apiKey) {
if (!apiKey) return null;
// Get all military aircraft
const data = await safeFetch(`${ENDPOINTS.rapidApi}/mil`, {
timeout: 20000,
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com',
},
});
return data;
}
// Attempt to fetch from public feed
async function fetchPublicFeed() {
const data = await safeFetch(ENDPOINTS.publicFeed, { timeout: 15000 });
return data;
}
// Get military aircraft from available sources
export async function getMilitaryAircraft(apiKey) {
// Try RapidAPI first if key available
if (apiKey) {
const data = await fetchViaRapidApi(apiKey);
if (data && !data.error) {
const aircraft = data.ac || data.aircraft || [];
if (Array.isArray(aircraft)) {
return aircraft.map(classifyAircraft).filter(a => a.isMilitary);
}
}
}
// Try public feed
const pubData = await fetchPublicFeed();
if (pubData && !pubData.error) {
const aircraft = pubData.ac || pubData.aircraft || pubData.states || [];
if (Array.isArray(aircraft)) {
return aircraft.map(classifyAircraft).filter(a => a.isMilitary);
}
}
return null; // all sources failed
}
// Get all aircraft in a geographic bounding box via RapidAPI
export async function getAircraftInArea(lat, lon, radiusNm = 250, apiKey) {
if (!apiKey) {
return { error: 'ADSB_API_KEY required for area search', hint: 'Set ADSB_API_KEY (RapidAPI key)' };
}
const data = await safeFetch(
`${ENDPOINTS.rapidApi}/lat/${lat}/lon/${lon}/dist/${radiusNm}/`,
{
timeout: 20000,
headers: {
'X-RapidAPI-Key': apiKey,
'X-RapidAPI-Host': 'adsbexchange-com1.p.rapidapi.com',
},
}
);
if (data && !data.error) {
const aircraft = data.ac || data.aircraft || [];
if (Array.isArray(aircraft)) return aircraft.map(classifyAircraft);
}
return data;
}
// Briefing — attempt to get military flight data, document what's available
export async function briefing() {
const apiKey = process.env.ADSB_API_KEY || process.env.RAPIDAPI_KEY || null;
const militaryAircraft = await getMilitaryAircraft(apiKey);
// If we got data, analyze it
if (militaryAircraft && militaryAircraft.length > 0) {
// Group by military match type
const byCountry = {};
const reconAircraft = [];
const bombers = [];
const tankers = [];
const vipTransport = [];
for (const ac of militaryAircraft) {
const country = ac.militaryMatch || 'Unknown';
byCountry[country] = (byCountry[country] || 0) + 1;
const desc = (ac.typeDescription || '').toLowerCase();
if (desc.includes('sigint') || desc.includes('awacs') || desc.includes('patrol') ||
desc.includes('global hawk') || desc.includes('dragon lady') || desc.includes('jstars')) {
reconAircraft.push(ac);
} else if (desc.includes('stratofortress') || desc.includes('lancer') || desc.includes('spirit')) {
bombers.push(ac);
} else if (desc.includes('tanker') || desc.includes('extender') || desc.includes('pegasus')) {
tankers.push(ac);
} else if (desc.includes('air force one') || desc.includes('nightwatch') ||
desc.includes('air force two') || desc.includes('special air')) {
vipTransport.push(ac);
}
}
const signals = [];
if (reconAircraft.length > 5) {
signals.push(`HIGH ISR ACTIVITY: ${reconAircraft.length} reconnaissance/surveillance aircraft airborne`);
}
if (bombers.length > 0) {
signals.push(`BOMBERS AIRBORNE: ${bombers.length} strategic bombers detected`);
}
if (tankers.length > 8) {
signals.push(`ELEVATED TANKER OPS: ${tankers.length} aerial refueling aircraft active (possible surge)`);
}
if (vipTransport.length > 0) {
signals.push(`VIP AIRCRAFT: ${vipTransport.length} VIP/continuity-of-government aircraft airborne`);
}
return {
source: 'ADS-B Exchange',
timestamp: new Date().toISOString(),
status: 'live',
totalMilitary: militaryAircraft.length,
byCountry,
categories: {
reconnaissance: reconAircraft.slice(0, 20),
bombers: bombers.slice(0, 10),
tankers: tankers.slice(0, 10),
vipTransport: vipTransport.slice(0, 5),
},
militaryAircraft: militaryAircraft.slice(0, 50), // cap for briefing size
signals: signals.length > 0 ? signals : ['Military flight activity within normal patterns'],
};
}
// No data available — return stub with integration documentation
return {
source: 'ADS-B Exchange',
timestamp: new Date().toISOString(),
status: apiKey ? 'error' : 'no_key',
militaryAircraft: [],
message: apiKey
? 'ADS-B Exchange API returned no data. The endpoint may be temporarily unavailable.'
: 'No ADS-B Exchange API key configured. Set ADSB_API_KEY for military flight tracking.',
signals: ['ADS-B data unavailable — cannot assess military flight activity'],
integrationGuide: {
step1: 'Sign up at https://rapidapi.com/adsbexchange/api/adsbexchange-com1',
step2: 'Subscribe to the free tier (500 requests/month)',
step3: 'Set ADSB_API_KEY=<your-rapidapi-key> in .env',
features: [
'Unfiltered military aircraft tracking (unlike FlightRadar24)',
'Real-time position, altitude, speed, heading',
'ICAO hex code identification for military registrations',
'Geographic area search within radius',
'Dedicated /mil endpoint for military-only feed',
],
},
complementarySource: 'OpenSky (opensky.mjs) provides partial military coverage for free',
knownMilitaryTypes: MILITARY_TYPES,
};
}
// Run standalone
if (process.argv[1]?.endsWith('adsb.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

162
apis/sources/bls.mjs Normal file
View File

@@ -0,0 +1,162 @@
// BLS — Bureau of Labor Statistics
// CPI, unemployment, nonfarm payrolls, PPI. No auth required (v1 API).
// v2 with registration key supports more requests; v1 is rate-limited but functional.
import { safeFetch, daysAgo } from '../utils/fetch.mjs';
const V1_BASE = 'https://api.bls.gov/publicAPI/v1/timeseries/data/';
const V2_BASE = 'https://api.bls.gov/publicAPI/v2/timeseries/data/';
// Key economic series
const SERIES = {
'CUUR0000SA0': 'CPI-U All Items',
'CUUR0000SA0L1E': 'CPI-U Core (ex Food & Energy)',
'LNS14000000': 'Unemployment Rate',
'CES0000000001': 'Nonfarm Payrolls (thousands)',
'WPUFD49104': 'PPI Final Demand',
};
// Fetch a single series via GET (v1, no key needed)
export async function getSeriesV1(seriesId) {
return safeFetch(`${V1_BASE}/${seriesId}`);
}
// Fetch one or more series via POST (v2 if key available, v1 otherwise)
export async function getSeries(seriesIds, opts = {}) {
const { startYear, endYear, apiKey } = opts;
const now = new Date();
const start = startYear || String(now.getFullYear() - 1);
const end = endYear || String(now.getFullYear());
const base = apiKey ? V2_BASE : V1_BASE;
const payload = {
seriesid: Array.isArray(seriesIds) ? seriesIds : [seriesIds],
startyear: start,
endyear: end,
};
if (apiKey) payload.registrationkey = apiKey;
try {
const res = await fetch(base, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return await res.json();
} catch (e) {
return { error: e.message };
}
}
// Extract the latest observation from a BLS series response
function latestFromSeries(seriesData) {
if (!seriesData?.data?.length) return null;
// BLS returns data sorted by year desc, period desc
// Filter out unavailable values (BLS uses "-" for missing data)
const valid = seriesData.data.filter(d => d.value !== '-' && d.value !== '.');
if (!valid.length) return null;
const sorted = [...valid].sort((a, b) => {
const ya = parseInt(a.year), yb = parseInt(b.year);
if (ya !== yb) return yb - ya;
// period is M01..M12 or M13 (annual avg) or Q01..Q05
return b.period.localeCompare(a.period);
});
return sorted[0];
}
// Get the two most recent observations to compute month-over-month change
function momChange(seriesData) {
if (!seriesData?.data?.length || seriesData.data.length < 2) return null;
const sorted = [...seriesData.data]
.filter(d => d.period.startsWith('M') && d.period !== 'M13' && d.value !== '-' && d.value !== '.')
.sort((a, b) => {
const ya = parseInt(a.year), yb = parseInt(b.year);
if (ya !== yb) return yb - ya;
return b.period.localeCompare(a.period);
});
if (sorted.length < 2) return null;
const curr = parseFloat(sorted[0].value);
const prev = parseFloat(sorted[1].value);
if (isNaN(curr) || isNaN(prev) || prev === 0) return null;
return {
current: curr,
previous: prev,
change: +(curr - prev).toFixed(4),
changePct: +(((curr - prev) / prev) * 100).toFixed(4),
currentPeriod: `${sorted[0].year}-${sorted[0].period}`,
previousPeriod: `${sorted[1].year}-${sorted[1].period}`,
};
}
// Briefing — pull latest CPI, unemployment, payrolls
export async function briefing(apiKey) {
const seriesIds = Object.keys(SERIES);
const resp = await getSeries(seriesIds, { apiKey });
if (resp.error) {
return { source: 'BLS', error: resp.error, timestamp: new Date().toISOString() };
}
if (resp.status !== 'REQUEST_SUCCEEDED' || !resp.Results?.series?.length) {
return {
source: 'BLS',
error: resp.message?.[0] || 'BLS API returned no data',
rawStatus: resp.status,
timestamp: new Date().toISOString(),
};
}
const indicators = [];
const signals = [];
for (const s of resp.Results.series) {
const id = s.seriesID;
const label = SERIES[id] || id;
const latest = latestFromSeries(s);
const mom = momChange(s);
if (!latest) {
indicators.push({ id, label, value: null, date: null });
continue;
}
const value = parseFloat(latest.value);
const period = `${latest.year}-${latest.period}`;
indicators.push({
id,
label,
value,
period,
date: latest.year + '-' + latest.period.replace('M', '').padStart(2, '0'),
momChange: mom ? mom.change : null,
momChangePct: mom ? mom.changePct : null,
});
// Generate signals
if (id === 'LNS14000000' && value > 5.0) {
signals.push(`Unemployment elevated at ${value}%`);
}
if (id === 'CUUR0000SA0' && mom && mom.changePct > 0.4) {
signals.push(`CPI-U MoM jump: ${mom.changePct}% (${mom.previousPeriod} -> ${mom.currentPeriod})`);
}
if (id === 'CUUR0000SA0L1E' && mom && mom.changePct > 0.3) {
signals.push(`Core CPI MoM rising: ${mom.changePct}%`);
}
if (id === 'CES0000000001' && mom && mom.change < -50) {
signals.push(`Nonfarm payrolls dropped by ${Math.abs(mom.change)}K`);
}
}
return {
source: 'BLS',
timestamp: new Date().toISOString(),
indicators,
signals,
};
}
if (process.argv[1]?.endsWith('bls.mjs')) {
const data = await briefing(process.env.BLS_API_KEY);
console.log(JSON.stringify(data, null, 2));
}

77
apis/sources/bluesky.mjs Normal file
View File

@@ -0,0 +1,77 @@
// Bluesky — AT Protocol social intelligence
// No auth required for public search. Real-time social sentiment on geopolitical/market topics.
// Public API: app.bsky.feed.searchPosts (full-text search, sorted by latest)
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://public.api.bsky.app/xrpc';
// Rate-limit-safe delay
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// Search public posts by query string
export async function searchPosts(query, opts = {}) {
const { limit = 25, sort = 'latest' } = opts;
const params = new URLSearchParams({
q: query,
limit: String(limit),
sort,
});
return safeFetch(`${BASE}/app.bsky.feed.searchPosts?${params}`);
}
// Compact a post for briefing output
function compactPost(post) {
const record = post?.record || post;
const author = post?.author;
return {
text: (record?.text || '').slice(0, 200),
author: author?.handle || author?.displayName || 'unknown',
date: record?.createdAt || null,
likes: post?.likeCount ?? 0,
};
}
// Categorize posts by topic bucket based on keyword matching
function categorize(posts, keywords) {
return posts.filter(p =>
keywords.some(k => p.text?.toLowerCase().includes(k))
);
}
// Briefing — search key geopolitical/market terms and categorize
export async function briefing() {
const searchQueries = [
{ label: 'conflict', q: 'Iran war OR missile strike OR sanctions' },
{ label: 'markets', q: 'market crash OR oil prices OR gold OR recession' },
{ label: 'health', q: 'pandemic OR outbreak OR epidemic' },
];
const allPosts = [];
const topicResults = {};
for (const { label, q } of searchQueries) {
const result = await searchPosts(q, { limit: 25 });
const posts = (result?.posts || []).map(compactPost);
topicResults[label] = posts;
allPosts.push(...posts);
// Small delay between searches to be polite to the API
await delay(1500);
}
return {
source: 'Bluesky',
timestamp: new Date().toISOString(),
topics: {
conflict: topicResults.conflict || [],
markets: topicResults.markets || [],
health: topicResults.health || [],
},
};
}
// Run standalone
if (process.argv[1]?.endsWith('bluesky.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

201
apis/sources/comtrade.mjs Normal file
View File

@@ -0,0 +1,201 @@
// UN Comtrade — Global Trade Data
// Public preview endpoint requires no key. Full API needs free registration.
// Tracks commodity trade flows between nations: crude oil, gas, gold, semiconductors, arms.
// Reporter codes: 842 (US), 156 (China), 276 (Germany), 392 (Japan), 826 (UK), 643 (Russia), 356 (India)
import { safeFetch, daysAgo, today } from '../utils/fetch.mjs';
const BASE = 'https://comtradeapi.un.org/public/v1';
// Strategic commodity codes (HS classification)
const STRATEGIC_COMMODITIES = {
'2709': 'Crude Petroleum',
'2711': 'Natural Gas (LNG & Pipeline)',
'7108': 'Gold (unwrought/semi-manufactured)',
'8542': 'Semiconductors (Electronic Integrated Circuits)',
'93': 'Arms & Ammunition',
'2844': 'Radioactive Elements (Nuclear)',
'8471': 'Computers & Processing Units',
'2701': 'Coal',
'7601': 'Aluminium (unwrought)',
'2612': 'Uranium & Thorium Ores',
};
// Key reporter/partner country codes
const COUNTRIES = {
842: 'United States',
156: 'China',
276: 'Germany',
392: 'Japan',
826: 'United Kingdom',
643: 'Russia',
356: 'India',
410: 'South Korea',
158: 'Taiwan',
380: 'Italy',
};
// Get trade data for a specific reporter, commodity, and period
export async function getTradeData(opts = {}) {
const {
reporterCode = 842, // default: US
period = new Date().getFullYear(),
cmdCode = '2709', // default: crude oil
flowCode = 'M', // M = imports, X = exports
partnerCode = null, // null = all partners
} = opts;
const params = new URLSearchParams({
reporterCode: String(reporterCode),
period: String(period),
cmdCode,
flowCode,
});
if (partnerCode) params.set('partnerCode', String(partnerCode));
return safeFetch(`${BASE}/preview/C/A/HS?${params}`, { timeout: 20000 });
}
// Get bilateral trade between two countries for a commodity
export async function getBilateralTrade(reporter, partner, cmdCode, period) {
return getTradeData({
reporterCode: reporter,
partnerCode: partner,
cmdCode,
period: period || new Date().getFullYear(),
});
}
// Check multiple commodities for a given reporter
async function checkReporterCommodities(reporterCode, commodityCodes, period) {
const results = [];
for (const cmdCode of commodityCodes) {
const data = await getTradeData({
reporterCode,
cmdCode,
period,
flowCode: 'M', // imports
});
results.push({
commodity: STRATEGIC_COMMODITIES[cmdCode] || cmdCode,
cmdCode,
data,
});
}
return results;
}
// Compact a trade record for briefing output
function compactRecord(rec) {
return {
reporter: rec.reporterDesc || rec.reporterCode,
partner: rec.partnerDesc || rec.partnerCode,
commodity: rec.cmdDesc || rec.cmdCode,
flow: rec.flowDesc || rec.flowCode,
value: rec.primaryValue || rec.cifvalue || rec.fobvalue || null,
quantity: rec.qty || rec.netWgt || null,
unit: rec.qtyUnitAbbr || rec.qtyUnitDesc || null,
period: rec.period,
};
}
// Detect anomalies in trade data (unusually large flows, new partners, etc.)
function detectAnomalies(tradeRecords) {
const signals = [];
if (!Array.isArray(tradeRecords) || tradeRecords.length === 0) return signals;
const values = tradeRecords
.map(r => r.value)
.filter(v => typeof v === 'number' && v > 0);
if (values.length > 2) {
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const stdDev = Math.sqrt(values.reduce((a, v) => a + (v - avg) ** 2, 0) / values.length);
tradeRecords.forEach(r => {
if (typeof r.value === 'number' && r.value > avg + 2 * stdDev) {
signals.push(
`OUTLIER: ${r.commodity} trade with ${r.partner} = $${(r.value / 1e9).toFixed(2)}B ` +
`(mean: $${(avg / 1e9).toFixed(2)}B)`
);
}
});
}
return signals;
}
// Briefing — check recent trade data for key commodities, detect anomalies
export async function briefing() {
const currentYear = new Date().getFullYear();
const prevYear = currentYear - 1;
// Key combinations to check: US imports of strategic commodities
const keyCommodities = ['2709', '2711', '7108', '8542', '93'];
const keyReporters = [842, 156]; // US, China
const tradeFlows = [];
const signals = [];
for (const reporter of keyReporters) {
for (const cmdCode of keyCommodities) {
// Try current year first, fall back to previous year
let data = await getTradeData({
reporterCode: reporter,
cmdCode,
period: currentYear,
flowCode: 'M',
});
// Comtrade returns data in different structures; normalize
let records = data?.data || data?.dataset || [];
if (!Array.isArray(records)) records = [];
// If no current year data, try previous year
if (records.length === 0) {
data = await getTradeData({
reporterCode: reporter,
cmdCode,
period: prevYear,
flowCode: 'M',
});
records = data?.data || data?.dataset || [];
if (!Array.isArray(records)) records = [];
}
const compact = records.slice(0, 10).map(compactRecord);
if (compact.length > 0) {
tradeFlows.push({
reporter: COUNTRIES[reporter] || reporter,
commodity: STRATEGIC_COMMODITIES[cmdCode] || cmdCode,
cmdCode,
topPartners: compact,
totalRecords: records.length,
});
// Run anomaly detection
const anomalies = detectAnomalies(compact);
signals.push(...anomalies);
}
}
}
return {
source: 'UN Comtrade',
timestamp: new Date().toISOString(),
tradeFlows,
signals: signals.length > 0
? signals
: ['No significant trade anomalies detected in sampled commodities'],
status: tradeFlows.length > 0 ? 'ok' : 'no_data',
note: 'Comtrade data often lags 1-2 months. Recent periods may be incomplete.',
coveredCommodities: STRATEGIC_COMMODITIES,
coveredCountries: COUNTRIES,
};
}
// Run standalone
if (process.argv[1]?.endsWith('comtrade.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

158
apis/sources/eia.mjs Normal file
View File

@@ -0,0 +1,158 @@
// EIA — US Energy Information Administration
// Oil prices, natural gas, crude inventories. Free API key required.
// Gracefully degrades without key.
import { safeFetch } from '../utils/fetch.mjs';
import '../utils/env.mjs';
const BASE = 'https://api.eia.gov/v2';
// Series definitions with their v2 API paths
const OIL_SERIES = {
wti: {
label: 'WTI Crude Oil ($/bbl)',
path: '/petroleum/pri/spt/data/',
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RWTC'] } },
},
brent: {
label: 'Brent Crude Oil ($/bbl)',
path: '/petroleum/pri/spt/data/',
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RBRTE'] } },
},
};
const GAS_SERIES = {
henryHub: {
label: 'Henry Hub Natural Gas ($/MMBtu)',
path: '/natural-gas/pri/fut/data/',
params: { frequency: 'daily', 'data[0]': 'value', facets: { series: ['RNGWHHD'] } },
},
};
const INVENTORY_SERIES = {
crudeStocks: {
label: 'US Crude Oil Inventories (thousand barrels)',
path: '/petroleum/stoc/wstk/data/',
params: { frequency: 'weekly', 'data[0]': 'value', facets: { series: ['WCESTUS1'] } },
},
};
// Build the URL for a v2 API query
function buildUrl(apiKey, path, params, length = 10) {
const url = new URL(`${BASE}${path}`);
url.searchParams.set('api_key', apiKey);
if (params.frequency) url.searchParams.set('frequency', params.frequency);
if (params['data[0]']) url.searchParams.set('data[0]', params['data[0]']);
url.searchParams.set('sort[0][column]', 'period');
url.searchParams.set('sort[0][direction]', 'desc');
url.searchParams.set('length', String(length));
// Add facets
if (params.facets) {
for (const [facetKey, facetValues] of Object.entries(params.facets)) {
facetValues.forEach((v, i) => {
url.searchParams.set(`facets[${facetKey}][]`, v);
});
}
}
return url.toString();
}
// Fetch a single EIA series
export async function fetchSeries(apiKey, seriesDef, length = 10) {
const url = buildUrl(apiKey, seriesDef.path, seriesDef.params, length);
return safeFetch(url);
}
// Extract latest value from EIA response
function extractLatest(resp) {
const data = resp?.response?.data;
if (!data?.length) return null;
return {
value: parseFloat(data[0].value),
period: data[0].period,
unit: data[0]['unit-name'] || data[0].unit || null,
};
}
// Extract recent values for trend analysis
function extractRecent(resp, count = 5) {
const data = resp?.response?.data;
if (!data?.length) return [];
return data.slice(0, count).map(d => ({
value: parseFloat(d.value),
period: d.period,
}));
}
// Briefing — oil prices, gas prices, inventories
export async function briefing(apiKey) {
if (!apiKey) {
return {
source: 'EIA',
error: 'No EIA API key. Register free at https://www.eia.gov/opendata/register.php',
hint: 'Set EIA_API_KEY environment variable',
timestamp: new Date().toISOString(),
};
}
const [wtiResp, brentResp, gasResp, inventoryResp] = await Promise.all([
fetchSeries(apiKey, OIL_SERIES.wti),
fetchSeries(apiKey, OIL_SERIES.brent),
fetchSeries(apiKey, GAS_SERIES.henryHub),
fetchSeries(apiKey, INVENTORY_SERIES.crudeStocks),
]);
const signals = [];
// Oil prices
const wti = extractLatest(wtiResp);
const brent = extractLatest(brentResp);
const wtiRecent = extractRecent(wtiResp, 5);
const brentRecent = extractRecent(brentResp, 5);
if (wti && wti.value > 100) signals.push(`WTI crude above $100 at $${wti.value}/bbl`);
if (wti && wti.value < 50) signals.push(`WTI crude below $50 at $${wti.value}/bbl — supply glut or demand destruction`);
if (brent && wti && (brent.value - wti.value) > 10) {
signals.push(`Brent-WTI spread wide at $${(brent.value - wti.value).toFixed(2)} — supply/logistics divergence`);
}
// Gas prices
const gas = extractLatest(gasResp);
if (gas && gas.value > 6) signals.push(`Natural gas elevated at $${gas.value}/MMBtu`);
if (gas && gas.value > 9) signals.push(`Natural gas crisis-level at $${gas.value}/MMBtu`);
// Inventories
const inv = extractLatest(inventoryResp);
const invRecent = extractRecent(inventoryResp, 5);
// Check week-over-week inventory change
if (invRecent.length >= 2) {
const weekChange = invRecent[0].value - invRecent[1].value;
if (Math.abs(weekChange) > 5000) {
const direction = weekChange > 0 ? 'build' : 'draw';
signals.push(`Large crude inventory ${direction}: ${weekChange > 0 ? '+' : ''}${(weekChange / 1000).toFixed(1)}M barrels`);
}
}
return {
source: 'EIA',
timestamp: new Date().toISOString(),
oilPrices: {
wti: wti ? { ...wti, label: OIL_SERIES.wti.label, recent: wtiRecent } : null,
brent: brent ? { ...brent, label: OIL_SERIES.brent.label, recent: brentRecent } : null,
spread: wti && brent ? +(brent.value - wti.value).toFixed(2) : null,
},
gasPrice: gas ? { ...gas, label: GAS_SERIES.henryHub.label } : null,
inventories: {
crudeStocks: inv ? { ...inv, label: INVENTORY_SERIES.crudeStocks.label, recent: invRecent } : null,
},
signals,
};
}
if (process.argv[1]?.endsWith('eia.mjs')) {
const data = await briefing(process.env.EIA_API_KEY);
console.log(JSON.stringify(data, null, 2));
}

206
apis/sources/epa.mjs Normal file
View File

@@ -0,0 +1,206 @@
// EPA RadNet — Radiation Monitoring Network
// No auth required. Government open data via Envirofacts REST API.
// Monitors ambient radiation levels across the US via fixed monitoring stations.
// Complements Safecast (citizen science) with official government readings.
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://enviro.epa.gov/enviro/efservice';
// RadNet analytical results endpoint
const RADNET_ANALYTICAL = `${BASE}/RADNET_ANALYTICAL_RESULTS`;
// RadNet auxiliary data
const RADNET_AUX = `${BASE}/RADNET_AUX`;
// Key US cities with RadNet monitoring stations
const MONITORING_STATIONS = {
washingtonDC: { label: 'Washington, DC', state: 'DC' },
newYork: { label: 'New York, NY', state: 'NY' },
losAngeles: { label: 'Los Angeles, CA', state: 'CA' },
chicago: { label: 'Chicago, IL', state: 'IL' },
seattle: { label: 'Seattle, WA', state: 'WA' },
denver: { label: 'Denver, CO', state: 'CO' },
honolulu: { label: 'Honolulu, HI', state: 'HI' },
anchorage: { label: 'Anchorage, AK', state: 'AK' },
miami: { label: 'Miami, FL', state: 'FL' },
sanFrancisco: { label: 'San Francisco, CA', state: 'CA' },
};
// Analyte types that indicate concerning radiation
const KEY_ANALYTES = [
'GROSS BETA',
'GROSS ALPHA',
'IODINE-131',
'CESIUM-137',
'CESIUM-134',
'STRONTIUM-90',
'TRITIUM',
'URANIUM',
'PLUTONIUM',
];
// Normal background radiation thresholds (pCi/L or pCi/m3 depending on medium)
const THRESHOLDS = {
'GROSS BETA': { normal: 1.0, elevated: 5.0, unit: 'pCi/m3' },
'GROSS ALPHA': { normal: 0.05, elevated: 0.15, unit: 'pCi/m3' },
'IODINE-131': { normal: 0.01, elevated: 0.1, unit: 'pCi/m3' },
'CESIUM-137': { normal: 0.01, elevated: 0.1, unit: 'pCi/m3' },
'CESIUM-134': { normal: 0.001, elevated: 0.01, unit: 'pCi/m3' },
};
// Get recent RadNet analytical results (JSON)
export async function getAnalyticalResults(opts = {}) {
const { rows = 50, startRow = 0 } = opts;
return safeFetch(
`${RADNET_ANALYTICAL}/ROWS/${startRow}:${startRow + rows}/JSON`,
{ timeout: 25000 }
);
}
// Get results filtered by state
export async function getResultsByState(state, opts = {}) {
const { rows = 25, startRow = 0 } = opts;
return safeFetch(
`${RADNET_ANALYTICAL}/ANA_STATE/${state}/ROWS/${startRow}:${startRow + rows}/JSON`,
{ timeout: 25000 }
);
}
// Get results filtered by analyte type
export async function getResultsByAnalyte(analyte, opts = {}) {
const { rows = 25, startRow = 0 } = opts;
const encoded = encodeURIComponent(analyte);
return safeFetch(
`${RADNET_ANALYTICAL}/ANA_TYPE/${encoded}/ROWS/${startRow}:${startRow + rows}/JSON`,
{ timeout: 25000 }
);
}
// Compact a reading for briefing output
function compactReading(r) {
return {
location: r.ANA_CITY || r.LOCATION || 'Unknown',
state: r.ANA_STATE || r.STATE || null,
analyte: r.ANA_TYPE || r.ANALYTE_NAME || null,
result: r.ANA_RESULT != null ? parseFloat(r.ANA_RESULT) : null,
unit: r.RESULT_UNIT || r.ANA_UNIT || null,
collectDate: r.COLLECT_DATE || r.SAMPLE_DATE || null,
medium: r.SAMPLE_TYPE || r.MEDIUM || null,
};
}
// Check a reading against known thresholds
function checkReading(reading) {
if (reading.result === null || reading.result <= 0) return null;
const threshold = THRESHOLDS[reading.analyte?.toUpperCase()];
if (!threshold) return null;
if (reading.result > threshold.elevated) {
return {
level: 'ELEVATED',
reading,
threshold: threshold.elevated,
ratio: (reading.result / threshold.elevated).toFixed(1),
};
}
if (reading.result > threshold.normal * 3) {
return {
level: 'ABOVE_NORMAL',
reading,
threshold: threshold.normal,
ratio: (reading.result / threshold.normal).toFixed(1),
};
}
return null;
}
// Briefing — get recent radiation readings from EPA network, flag anomalies
export async function briefing() {
const readings = [];
const signals = [];
// Fetch recent analytical results (broad pull)
const recentData = await getAnalyticalResults({ rows: 100 });
const recentRecords = Array.isArray(recentData) ? recentData : [];
// Compact all readings
const allReadings = recentRecords.map(compactReading);
readings.push(...allReadings);
// Also try to pull key analytes specifically
const analyteResults = await Promise.all(
['GROSS BETA', 'IODINE-131', 'CESIUM-137'].map(async analyte => {
const data = await getResultsByAnalyte(analyte, { rows: 20 });
const records = Array.isArray(data) ? data : [];
return { analyte, records: records.map(compactReading) };
})
);
for (const { analyte, records } of analyteResults) {
// Add any records not already in our list
for (const r of records) {
if (!readings.some(existing =>
existing.location === r.location &&
existing.collectDate === r.collectDate &&
existing.analyte === r.analyte
)) {
readings.push(r);
}
}
}
// Check all readings against thresholds
for (const reading of readings) {
const alert = checkReading(reading);
if (alert) {
if (alert.level === 'ELEVATED') {
signals.push(
`ELEVATED ${reading.analyte} at ${reading.location}, ${reading.state}: ` +
`${reading.result} ${reading.unit || ''} (${alert.ratio}x threshold) [${reading.collectDate}]`
);
} else {
signals.push(
`ABOVE NORMAL ${reading.analyte} at ${reading.location}, ${reading.state}: ` +
`${reading.result} ${reading.unit || ''} (${alert.ratio}x normal) [${reading.collectDate}]`
);
}
}
}
// Summarize by state
const byState = {};
for (const r of readings) {
const st = r.state || 'UNK';
if (!byState[st]) byState[st] = { count: 0, analytes: new Set() };
byState[st].count++;
if (r.analyte) byState[st].analytes.add(r.analyte);
}
// Convert sets to arrays for JSON
const stateSummary = Object.fromEntries(
Object.entries(byState).map(([st, info]) => [
st,
{ count: info.count, analytes: [...info.analytes] },
])
);
return {
source: 'EPA RadNet',
timestamp: new Date().toISOString(),
totalReadings: readings.length,
readings: readings.slice(0, 50), // cap for briefing size
stateSummary,
signals: signals.length > 0
? signals
: ['All EPA RadNet readings within normal background levels'],
monitoredAnalytes: KEY_ANALYTES,
thresholds: THRESHOLDS,
note: 'RadNet data may lag by hours to days. Near-real-time gamma data updates more frequently.',
};
}
// Run standalone
if (process.argv[1]?.endsWith('epa.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

150
apis/sources/firms.mjs Normal file
View File

@@ -0,0 +1,150 @@
// NASA FIRMS — Fire Information for Resource Management System
// Detects active fires/thermal anomalies globally within 3 hours of satellite pass.
// Detects military strikes, explosions, wildfires, industrial fires.
import '../utils/env.mjs';
const FIRMS_BASE = 'https://firms.modaps.eosdis.nasa.gov/api/area/csv';
// Parse FIRMS CSV response into structured data
function parseCSV(rawText) {
if (!rawText || typeof rawText !== 'string') return [];
const lines = rawText.trim().split('\n');
if (lines.length < 2) return [];
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const vals = line.split(',');
const obj = {};
headers.forEach((h, i) => { obj[h.trim()] = vals[i]?.trim(); });
return obj;
});
}
// Fetch fires in a bounding box
async function fetchFires(opts = {}) {
const {
west = -180, south = -90, east = 180, north = 90,
days = 1,
source = 'VIIRS_SNPP_NRT',
} = opts;
const key = process.env.FIRMS_MAP_KEY;
if (!key) return { error: 'No FIRMS_MAP_KEY' };
const url = `${FIRMS_BASE}/${key}/${source}/${west},${south},${east},${north}/${days}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 25000);
try {
const res = await fetch(url, {
signal: controller.signal,
headers: { 'User-Agent': 'Crucix/1.0' },
});
clearTimeout(timer);
if (!res.ok) return { error: `HTTP ${res.status}` };
const text = await res.text();
return parseCSV(text);
} catch (e) {
clearTimeout(timer);
return { error: e.message };
}
}
// Key conflict/hotspot zones
const HOTSPOTS = {
middleEast: { west: 30, south: 12, east: 65, north: 42, label: 'Middle East' },
ukraine: { west: 22, south: 44, east: 41, north: 53, label: 'Ukraine' },
iran: { west: 44, south: 25, east: 63, north: 40, label: 'Iran' },
sudanHorn: { west: 21, south: 2, east: 52, north: 23, label: 'Sudan / Horn of Africa' },
myanmar: { west: 92, south: 9, east: 102, north: 29, label: 'Myanmar' },
southAsia: { west: 60, south: 5, east: 98, north: 37, label: 'South Asia' },
};
// Analyze fire detections for potential military/strike activity
function analyzeFires(fires, regionLabel) {
if (!Array.isArray(fires) || fires.length === 0) {
return { region: regionLabel, totalDetections: 0, highConfidence: 0, highIntensity: [], summary: 'No detections' };
}
const highConf = fires.filter(f => f.confidence === 'h' || f.confidence === 'high');
const nomConf = fires.filter(f => f.confidence === 'n' || f.confidence === 'nominal');
// High intensity fires (FRP > 10 MW) — potential strikes, industrial fires, large explosions
const highIntensity = fires
.filter(f => parseFloat(f.frp) > 10)
.map(f => ({
lat: parseFloat(f.latitude),
lon: parseFloat(f.longitude),
brightness: parseFloat(f.bright_ti4),
frp: parseFloat(f.frp),
date: f.acq_date,
time: f.acq_time,
confidence: f.confidence,
daynight: f.daynight,
}))
.sort((a, b) => b.frp - a.frp)
.slice(0, 15);
// Night detections are more significant (less likely agricultural burning)
const nightFires = fires.filter(f => f.daynight === 'N');
return {
region: regionLabel,
totalDetections: fires.length,
highConfidence: highConf.length,
nominalConfidence: nomConf.length,
nightDetections: nightFires.length,
highIntensity,
avgFRP: fires.reduce((sum, f) => sum + (parseFloat(f.frp) || 0), 0) / fires.length,
};
}
// Briefing
export async function briefing() {
const key = process.env.FIRMS_MAP_KEY;
if (!key) {
return {
source: 'NASA FIRMS',
timestamp: new Date().toISOString(),
status: 'no_key',
message: 'Set FIRMS_MAP_KEY for satellite fire/strike detection. Free at https://firms.modaps.eosdis.nasa.gov/api/area/',
};
}
// Fetch all hotspots in parallel
const entries = Object.entries(HOTSPOTS);
const rawResults = await Promise.all(
entries.map(async ([key, box]) => {
const fires = await fetchFires({ ...box, days: 2 });
return { key, label: box.label, fires };
})
);
const hotspots = rawResults.map(r => {
if (r.fires?.error) return { region: r.label, error: r.fires.error };
return analyzeFires(r.fires, r.label);
});
// Generate signals
const signals = [];
for (const h of hotspots) {
if (h.highIntensity?.length > 5) {
signals.push(`HIGH INTENSITY FIRES in ${h.region}: ${h.highIntensity.length} detections >10MW FRP`);
}
if (h.nightDetections > 20) {
signals.push(`ELEVATED NIGHT ACTIVITY in ${h.region}: ${h.nightDetections} night detections (potential strikes/combat)`);
}
}
return {
source: 'NASA FIRMS',
timestamp: new Date().toISOString(),
status: 'active',
hotspots,
signals,
};
}
if (process.argv[1]?.endsWith('firms.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

108
apis/sources/fred.mjs Normal file
View File

@@ -0,0 +1,108 @@
// FRED — Federal Reserve Economic Data
// 840,000+ time series. Free API key required.
// Key indicators: yield curve, CPI, unemployment, money supply, GDP, fed funds rate
import { safeFetch, today, daysAgo } from '../utils/fetch.mjs';
const BASE = 'https://api.stlouisfed.org/fred';
// Key series IDs for macro intelligence
const KEY_SERIES = {
// Yield curve & rates
DFF: 'Fed Funds Rate',
DGS2: '2-Year Treasury Yield',
DGS10: '10-Year Treasury Yield',
DGS30: '30-Year Treasury Yield',
T10Y2Y: '10Y-2Y Spread (Yield Curve)',
T10Y3M: '10Y-3M Spread',
// Inflation
CPIAUCSL: 'CPI All Items',
CPILFESL: 'Core CPI (ex Food & Energy)',
PCEPI: 'PCE Price Index',
MICH: 'Michigan Inflation Expectations',
// Labor
UNRATE: 'Unemployment Rate',
PAYEMS: 'Nonfarm Payrolls',
ICSA: 'Initial Jobless Claims',
// Money & credit
M2SL: 'M2 Money Supply',
WALCL: 'Fed Balance Sheet Total Assets',
// Fear gauges
VIXCLS: 'VIX (Fear Index)',
BAMLH0A0HYM2: 'High Yield Spread (Credit Stress)',
// Commodities via FRED
DCOILWTICO: 'WTI Crude Oil',
GOLDAMGBD228NLBM: 'Gold Price (London Fix)',
// Housing
MORTGAGE30US: '30-Year Mortgage Rate',
// Global
DTWEXBGS: 'USD Trade Weighted Index',
};
// Get latest value for a series
async function getSeriesLatest(seriesId, apiKey) {
const params = new URLSearchParams({
series_id: seriesId,
api_key: apiKey,
file_type: 'json',
sort_order: 'desc',
limit: '5',
observation_start: daysAgo(90),
});
return safeFetch(`${BASE}/series/observations?${params}`);
}
// Briefing — pull all key indicators
export async function briefing(apiKey) {
if (!apiKey) {
return {
source: 'FRED',
error: 'No FRED API key. Get one free at https://fred.stlouisfed.org/docs/api/api_key.html',
hint: 'Set FRED_API_KEY environment variable',
};
}
const entries = Object.entries(KEY_SERIES);
const results = await Promise.all(
entries.map(async ([id, label]) => {
const data = await getSeriesLatest(id, apiKey);
const obs = data?.observations;
if (!obs?.length) return { id, label, value: null, date: null, recent: [] };
const latest = obs.find(o => o.value !== '.');
const validObs = obs.filter(o => o.value !== '.');
return {
id,
label,
value: latest ? parseFloat(latest.value) : null,
date: latest?.date || null,
recent: validObs.slice(0, 5).map(o => parseFloat(o.value)),
};
})
);
// Compute derived signals
const get = (id) => results.find(r => r.id === id)?.value;
const yieldCurve10y2y = get('T10Y2Y');
const yieldCurve10y3m = get('T10Y3M');
const vix = get('VIXCLS');
const hySpread = get('BAMLH0A0HYM2');
const signals = [];
if (yieldCurve10y2y !== null && yieldCurve10y2y < 0) signals.push('YIELD CURVE INVERTED (10Y-2Y) — recession signal');
if (yieldCurve10y3m !== null && yieldCurve10y3m < 0) signals.push('YIELD CURVE INVERTED (10Y-3M) — stronger recession signal');
if (vix !== null && vix > 30) signals.push(`VIX ELEVATED at ${vix} — high fear/volatility`);
if (vix !== null && vix > 40) signals.push(`VIX EXTREME at ${vix} — crisis-level fear`);
if (hySpread !== null && hySpread > 5) signals.push(`HIGH YIELD SPREAD WIDE at ${hySpread}% — credit stress`);
return {
source: 'FRED',
timestamp: new Date().toISOString(),
indicators: results.filter(r => r.value !== null),
signals,
};
}
if (process.argv[1]?.endsWith('fred.mjs')) {
const data = await briefing(process.env.FRED_API_KEY);
console.log(JSON.stringify(data, null, 2));
}

123
apis/sources/gdelt.mjs Normal file
View File

@@ -0,0 +1,123 @@
// GDELT — Global Database of Events, Language, and Tone
// No auth required. Updates every 15 minutes. Monitors news in 100+ languages.
// DOC 2.0 API: full-text search across last 3 months of global news
// GEO 2.0 API: geolocation mapping of events
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://api.gdeltproject.org/api/v2';
// Search recent global events/articles by keyword
export async function searchEvents(query = '', opts = {}) {
const {
mode = 'ArtList', // ArtList, TimelineVol, TimelineVolInfo, TimelineTone, TimelineLang, TimelineSourceCountry
maxRecords = 75,
timespan = '24h', // e.g. "24h", "7d", "3m"
format = 'json',
sortBy = 'DateDesc', // DateDesc, DateAsc, ToneDesc, ToneAsc
} = opts;
// If no query, use broad geopolitical terms
const q = query || 'conflict OR crisis OR military OR sanctions OR war OR economy';
const params = new URLSearchParams({
query: q,
mode,
maxrecords: String(maxRecords),
timespan,
format,
sort: sortBy,
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// Get tone/sentiment timeline for a topic
export async function toneTrend(query, timespan = '7d') {
const params = new URLSearchParams({
query,
mode: 'TimelineTone',
timespan,
format: 'json',
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// Get volume timeline for a topic (how much coverage)
export async function volumeTrend(query, timespan = '7d') {
const params = new URLSearchParams({
query,
mode: 'TimelineVol',
timespan,
format: 'json',
});
return safeFetch(`${BASE}/doc/doc?${params}`);
}
// GEO API — geographic event mapping
export async function geoEvents(query = '', opts = {}) {
const {
mode = 'PointData',
timespan = '24h',
format = 'GeoJSON',
maxPoints = 500,
} = opts;
const q = query || 'conflict OR military OR protest OR explosion';
const params = new URLSearchParams({
query: q,
mode,
timespan,
format,
maxpoints: String(maxPoints),
});
return safeFetch(`${BASE}/geo/geo?${params}`);
}
// Compact article for briefing
function compactArticle(a) {
return {
title: a.title,
url: a.url,
date: a.seendate,
domain: a.domain,
language: a.language,
country: a.sourcecountry,
};
}
// GDELT rate limit: 1 request per 5 seconds
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// Briefing mode — get top global events summary (sequential due to rate limit)
export async function briefing() {
// Single broad query to stay within rate limits
const all = await searchEvents(
'conflict OR military OR economy OR crisis OR war OR sanctions OR tariff OR strike OR outbreak',
{ maxRecords: 50, timespan: '24h' }
);
const articles = (all?.articles || []).map(compactArticle);
// Categorize by keyword matching in titles
const categorize = (keywords) => articles.filter(a =>
keywords.some(k => a.title?.toLowerCase().includes(k))
);
return {
source: 'GDELT',
timestamp: new Date().toISOString(),
totalArticles: articles.length,
allArticles: articles,
conflicts: categorize(['military', 'conflict', 'war', 'strike', 'missile', 'attack', 'bomb', 'troops']),
economy: categorize(['economy', 'recession', 'inflation', 'market', 'sanctions', 'tariff', 'trade', 'gdp']),
health: categorize(['pandemic', 'outbreak', 'epidemic', 'disease', 'virus', 'health']),
crisis: categorize(['crisis', 'disaster', 'emergency', 'refugee', 'famine']),
};
}
// Run standalone
if (process.argv[1]?.endsWith('gdelt.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

166
apis/sources/gscpi.mjs Normal file
View File

@@ -0,0 +1,166 @@
// GSCPI — NY Fed Global Supply Chain Pressure Index
// Measures global supply chain stress (standard deviations from historical average).
// Values above 0 = above average pressure. Above 1.0 = elevated. Below -1.0 = unusually loose.
// Data fetched directly from NY Fed — no API key required.
const GSCPI_CSV_URL = 'https://www.newyorkfed.org/medialibrary/research/interactives/data/gscpi/gscpi_interactive_data.csv';
// Fetch and parse the GSCPI CSV from the NY Fed
// The CSV is wide-format: each column is a revision vintage, last column is latest estimate.
// Uses raw fetch instead of safeFetch because safeFetch truncates non-JSON to 500 chars.
export async function getGSCPI(months = 12) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
const res = await fetch(GSCPI_CSV_URL, {
signal: controller.signal,
headers: { 'User-Agent': 'Crucix/1.0' },
});
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const text = await res.text();
return { data: parseCSV(text, months) };
} catch (e) {
return { error: e.message || 'Failed to fetch GSCPI data', data: [] };
}
}
// Parse the wide-format CSV, extracting the latest vintage value for each date
function parseCSV(text, months) {
const lines = text.trim().split('\n').filter(l => l.trim() && !l.startsWith(','));
if (lines.length < 2) return [];
// Header row tells us column count; we want the last non-empty column for each row
const results = [];
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split(',');
const dateStr = cols[0]?.trim();
if (!dateStr) continue;
// Find the last non-empty, non-#N/A value (latest vintage estimate)
let value = null;
for (let j = cols.length - 1; j >= 1; j--) {
const v = cols[j]?.trim();
if (v && v !== '#N/A' && v !== '') {
const num = parseFloat(v);
if (!isNaN(num)) {
value = num;
break;
}
}
}
if (value === null) continue;
// Parse date from "31-Jan-2026" format to "2026-01"
const date = parseNYFedDate(dateStr);
if (date) {
results.push({ date, value });
}
}
// Sort newest first
results.sort((a, b) => b.date.localeCompare(a.date));
return results.slice(0, months);
}
// Parse "31-Jan-2026" -> "2026-01"
function parseNYFedDate(str) {
const months = {
Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06',
Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12',
};
const parts = str.split('-');
if (parts.length !== 3) return null;
const mon = months[parts[1]];
const year = parts[2];
if (!mon || !year) return null;
return `${year}-${mon}`;
}
// Detect trend from an array of {date, value} sorted newest-first
function detectTrend(history) {
if (history.length < 3) return 'insufficient data';
// Compare recent 3 months direction
const recent = history.slice(0, 3);
let rising = 0;
let falling = 0;
for (let i = 0; i < recent.length - 1; i++) {
// history is newest-first, so recent[0] is latest
if (recent[i].value > recent[i + 1].value) rising++;
else if (recent[i].value < recent[i + 1].value) falling++;
}
if (rising > falling) return 'rising';
if (falling > rising) return 'falling';
return 'stable';
}
// Briefing — latest GSCPI, trend, and signals
export async function briefing() {
const result = await getGSCPI(12);
if (result.error) {
return {
source: 'NY Fed GSCPI',
error: result.error,
timestamp: new Date().toISOString(),
};
}
const history = result.data;
const trend = detectTrend(history);
const signals = [];
const latest = history.length > 0 ? history[0] : null;
if (latest) {
if (latest.value > 2.0) {
signals.push(`GSCPI extremely elevated at ${latest.value.toFixed(2)} — severe supply chain stress`);
} else if (latest.value > 1.0) {
signals.push(`GSCPI elevated at ${latest.value.toFixed(2)} — above-normal supply chain pressure`);
} else if (latest.value < -1.0) {
signals.push(`GSCPI at ${latest.value.toFixed(2)} — unusually loose supply chains`);
}
if (trend === 'rising' && latest.value > 0) {
signals.push('Supply chain pressure trending higher');
}
if (trend === 'falling' && latest.value > 1.0) {
signals.push('Supply chain pressure elevated but improving');
}
}
// Check month-over-month change
if (history.length >= 2) {
const mom = history[0].value - history[1].value;
if (Math.abs(mom) > 0.5) {
const dir = mom > 0 ? 'surged' : 'dropped';
signals.push(`GSCPI ${dir} ${Math.abs(mom).toFixed(2)} points month-over-month`);
}
}
return {
source: 'NY Fed GSCPI',
timestamp: new Date().toISOString(),
latest: latest ? {
value: latest.value,
date: latest.date,
interpretation: latest.value > 1.0 ? 'elevated' :
latest.value > 0 ? 'above average' :
latest.value > -1.0 ? 'below average' : 'unusually loose',
} : null,
trend,
history,
signals,
};
}
if (process.argv[1]?.endsWith('gscpi.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

306
apis/sources/kiwisdr.mjs Normal file
View File

@@ -0,0 +1,306 @@
// KiwiSDR Network — Global software-defined radio receiver network
// No auth required. ~900 public HF receivers worldwide (0-30 MHz).
// Useful for SIGINT awareness: HF band activity, receiver distribution,
// detecting unusual radio configurations in conflict zones.
// Data source: receiverbook.de (embeds full receiver list as JS variable)
import { safeFetch } from '../utils/fetch.mjs';
const RECEIVERBOOK_URL = 'https://www.receiverbook.de/map?type=kiwisdr';
// Fetch the full list of public KiwiSDR receivers from receiverbook.de
export async function getAllReceivers() {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 20000);
const res = await fetch(RECEIVERBOOK_URL, {
headers: { 'User-Agent': 'Crucix/1.0' },
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) return { error: `HTTP ${res.status}` };
const html = await res.text();
// Extract embedded JS: var receivers = [...];
const match = html.match(/var\s+receivers\s*=\s*(\[[\s\S]*?\]);/);
if (!match) return { error: 'Could not parse receiver data from page' };
const sites = JSON.parse(match[1]);
// Flatten: each site has a .receivers[] array of individual SDRs
const flat = [];
for (const site of sites) {
const [lon, lat] = site.location?.coordinates || [NaN, NaN];
const country = site.label?.split(',').pop()?.trim() || '';
for (const rx of (site.receivers || [site])) {
flat.push({
name: rx.label || site.label || '',
location: site.label || '',
lat, lon,
country,
url: rx.url || site.url || '',
version: rx.version || '',
antenna: '',
users: 0, usersMax: 0,
offline: false,
snr: NaN,
tdoa: null,
bands: '',
});
}
}
return flat;
} catch (e) {
return { error: e.message };
}
}
// Regions of intelligence interest with bounding boxes
const REGIONS_OF_INTEREST = {
middleEast: { lamin: 12, lomin: 30, lamax: 42, lomax: 65, label: 'Middle East' },
ukraine: { lamin: 44, lomin: 22, lamax: 53, lomax: 41, label: 'Ukraine / Eastern Europe' },
taiwan: { lamin: 20, lomin: 115, lamax: 28, lomax: 125, label: 'Taiwan Strait' },
baltics: { lamin: 53, lomin: 19, lamax: 60, lomax: 29, label: 'Baltic Region' },
southChinaSea: { lamin: 5, lomin: 105, lamax: 23, lomax: 122, label: 'South China Sea' },
koreanPeninsula:{ lamin: 33, lomin: 124, lamax: 43, lomax: 132, label: 'Korean Peninsula' },
iran: { lamin: 25, lomin: 44, lamax: 40, lomax: 63, label: 'Iran' },
sahel: { lamin: 10, lomin: -17, lamax: 20, lomax: 25, label: 'Sahel / West Africa' },
};
// HF band classifications for intelligence relevance
const HF_BANDS = {
vlf: { min: 0, max: 0.3, label: 'VLF (submarine/military comms)' },
lf: { min: 0.3, max: 0.5, label: 'LF (navigation/time signals)' },
mf: { min: 0.5, max: 1.8, label: 'MF (AM broadcast/maritime)' },
hf160m: { min: 1.8, max: 2.0, label: '160m amateur' },
hf80m: { min: 3.5, max: 4.0, label: '80m amateur' },
hf60m: { min: 5.3, max: 5.4, label: '60m amateur/utility' },
hf49m: { min: 5.9, max: 6.2, label: '49m shortwave broadcast' },
hf40m: { min: 7.0, max: 7.3, label: '40m amateur' },
hf31m: { min: 9.4, max: 9.9, label: '31m shortwave broadcast' },
hf30m: { min: 10.1, max: 10.15,label: '30m amateur' },
hf25m: { min: 11.6, max: 12.1, label: '25m shortwave broadcast' },
hf20m: { min: 14.0, max: 14.35,label: '20m amateur' },
hf17m: { min: 18.068,max: 18.168,label: '17m amateur' },
hf15m: { min: 21.0, max: 21.45,label: '15m amateur' },
hf11m: { min: 25.67, max: 26.1, label: '11m broadcast/CB' },
hfMilitary:{ min: 2.0, max: 30.0, label: 'HF military/utility (general)' },
};
// Check if a receiver falls within a bounding box
function inBounds(rx, box) {
if (isNaN(rx.lat) || isNaN(rx.lon)) return false;
return rx.lat >= box.lamin && rx.lat <= box.lamax && rx.lon >= box.lomin && rx.lon <= box.lomax;
}
// Map a receiver to a continent based on coordinates
function getContinent(lat, lon) {
if (isNaN(lat) || isNaN(lon)) return 'Unknown';
if (lat >= 15 && lat <= 72 && lon >= -170 && lon <= -50) return 'North America';
if (lat >= -60 && lat < 15 && lon >= -90 && lon <= -30) return 'South America';
if (lat >= 35 && lat <= 72 && lon >= -25 && lon <= 45) return 'Europe';
if (lat >= -35 && lat <= 37 && lon >= -25 && lon <= 55) return 'Africa';
if (lat >= 0 && lat <= 72 && lon >= 45 && lon <= 180) return 'Asia';
if (lat >= -50 && lat <= 0 && lon >= 95 && lon <= 180) return 'Oceania';
if (lat >= 35 && lat < 45 && lon >= 25 && lon <= 45) return 'Middle East';
return 'Other';
}
// Classify the frequency range of a receiver
function classifyFrequency(rx) {
// KiwiSDR receivers typically cover 0-30 MHz
// Some entries have frequency info in various fields
const maxFreq = parseFloat(rx.max_freq ?? rx.sdr_hu?.max_freq ?? 30);
const minFreq = parseFloat(rx.min_freq ?? rx.sdr_hu?.min_freq ?? 0);
return { minFreq, maxFreq };
}
// Normalize receiver data (already flat from getAllReceivers)
function normalizeReceiver(rx, idx) {
return {
name: (rx.name || `Receiver-${idx}`).slice(0, 100),
location: (rx.location || '').slice(0, 80),
lat: parseFloat(rx.lat) || NaN,
lon: parseFloat(rx.lon) || NaN,
users: parseInt(rx.users ?? 0, 10),
usersMax: parseInt(rx.usersMax ?? 0, 10),
antenna: (rx.antenna || '').slice(0, 80),
bands: (rx.bands || '').slice(0, 60),
offline: rx.offline === true,
snr: parseFloat(rx.snr ?? NaN),
tdoa: rx.tdoa ?? null,
country: rx.country || '',
};
}
// Briefing — analyze the global KiwiSDR network
export async function briefing() {
const raw = await getAllReceivers();
// Handle errors
if (raw?.error) {
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'error',
message: raw.error,
};
}
// The API may return an array directly or an object with a receivers list
let rxList;
if (Array.isArray(raw)) {
rxList = raw;
} else if (raw && typeof raw === 'object') {
// Try common keys
rxList = raw.receivers || raw.rx || raw.sdrs || raw.data || Object.values(raw);
// If the object values are receiver objects, flatten
if (!Array.isArray(rxList)) {
rxList = Object.values(raw).filter(v => v && typeof v === 'object' && !Array.isArray(v));
}
} else {
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'error',
message: 'Unexpected data format from KiwiSDR API',
};
}
// Normalize all receivers
const allRx = rxList.map((rx, i) => normalizeReceiver(rx, i));
const onlineRx = allRx.filter(r => !r.offline);
const offlineRx = allRx.filter(r => r.offline);
// --- Geographic distribution by country ---
const byCountry = {};
for (const rx of onlineRx) {
const c = rx.country || 'Unknown';
byCountry[c] = (byCountry[c] || 0) + 1;
}
// Sort by count descending, take top 20
const topCountries = Object.entries(byCountry)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([country, count]) => ({ country, count }));
// --- Continental distribution ---
const byContinent = {};
for (const rx of onlineRx) {
const continent = getContinent(rx.lat, rx.lon);
byContinent[continent] = (byContinent[continent] || 0) + 1;
}
// --- Receivers in regions of interest ---
const conflictZoneReceivers = {};
for (const [key, box] of Object.entries(REGIONS_OF_INTEREST)) {
const rxInRegion = onlineRx.filter(rx => inBounds(rx, box));
conflictZoneReceivers[key] = {
region: box.label,
count: rxInRegion.length,
receivers: rxInRegion.slice(0, 10).map(rx => ({
name: rx.name,
location: rx.location,
lat: rx.lat,
lon: rx.lon,
users: rx.users,
antenna: rx.antenna,
country: rx.country,
})),
};
}
// --- Activity analysis (users connected) ---
const activeRx = onlineRx
.filter(r => r.users > 0)
.sort((a, b) => b.users - a.users);
const totalUsers = onlineRx.reduce((sum, r) => sum + r.users, 0);
const totalCapacity = onlineRx.reduce((sum, r) => sum + r.usersMax, 0);
const topActive = activeRx.slice(0, 15).map(rx => ({
name: rx.name,
location: rx.location,
country: rx.country,
users: rx.users,
usersMax: rx.usersMax,
lat: rx.lat,
lon: rx.lon,
antenna: rx.antenna,
}));
// --- TDOA-capable receivers (direction finding / geolocation) ---
const tdoaCapable = onlineRx.filter(r => r.tdoa !== null && r.tdoa > 0);
// --- Antenna analysis (identify unusual/specialized setups) ---
const antennaTypes = {};
for (const rx of onlineRx) {
if (rx.antenna) {
const key = rx.antenna.toLowerCase().trim();
antennaTypes[key] = (antennaTypes[key] || 0) + 1;
}
}
// --- Utilization metrics ---
const utilizationPct = totalCapacity > 0
? ((totalUsers / totalCapacity) * 100).toFixed(1)
: '0.0';
const highUtilization = onlineRx
.filter(r => r.usersMax > 0 && (r.users / r.usersMax) >= 0.8)
.map(rx => ({
name: rx.name,
location: rx.location,
country: rx.country,
users: rx.users,
usersMax: rx.usersMax,
}));
// --- Generate signals ---
const signals = [];
// High user count (unusual listening activity)
if (totalUsers > onlineRx.length * 0.5) {
signals.push(`HIGH LISTENER ACTIVITY: ${totalUsers} total users across ${onlineRx.length} receivers (${utilizationPct}% utilization)`);
}
// Conflict zone coverage
for (const [key, info] of Object.entries(conflictZoneReceivers)) {
if (info.count > 0) {
const activeInZone = info.receivers.filter(r => r.users > 0);
if (activeInZone.length > 0) {
signals.push(`ACTIVE LISTENING in ${info.region}: ${activeInZone.length}/${info.count} receivers have users connected`);
}
}
}
// High utilization receivers
if (highUtilization.length > 5) {
signals.push(`${highUtilization.length} receivers at >80% capacity — elevated HF monitoring demand`);
}
return {
source: 'KiwiSDR',
timestamp: new Date().toISOString(),
status: 'active',
network: {
totalReceivers: allRx.length,
online: onlineRx.length,
offline: offlineRx.length,
totalUsers,
totalCapacity,
utilizationPct: parseFloat(utilizationPct),
tdoaCapable: tdoaCapable.length,
},
geographic: {
byContinent,
topCountries,
},
conflictZones: conflictZoneReceivers,
topActive,
highUtilization: highUtilization.slice(0, 10),
signals,
};
}
if (process.argv[1]?.endsWith('kiwisdr.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

75
apis/sources/noaa.mjs Normal file
View File

@@ -0,0 +1,75 @@
// NOAA / National Weather Service — Severe weather alerts & climate events
// No auth required. Real-time alerts.
import { safeFetch } from '../utils/fetch.mjs';
const NWS_BASE = 'https://api.weather.gov';
// Get all active weather alerts (US)
export async function getActiveAlerts(opts = {}) {
const {
severity = null, // Extreme, Severe, Moderate, Minor
urgency = null, // Immediate, Expected, Future
event = null, // e.g. "Tornado Warning", "Hurricane Warning"
limit = 50,
} = opts;
const params = new URLSearchParams({ limit: String(limit), status: 'actual' });
if (severity) params.set('severity', severity);
if (urgency) params.set('urgency', urgency);
if (event) params.set('event', event);
return safeFetch(`${NWS_BASE}/alerts/active?${params}`, {
headers: { 'Accept': 'application/geo+json' },
});
}
// Get severe alerts only
export async function getSevereAlerts() {
return getActiveAlerts({ severity: 'Extreme,Severe' });
}
// Briefing — severe weather events that could impact markets/supply chains
export async function briefing() {
const alerts = await getSevereAlerts();
const features = alerts?.features || [];
// Categorize by impact type
const hurricanes = features.filter(f => /hurricane|typhoon|tropical/i.test(f.properties?.event));
const tornadoes = features.filter(f => /tornado/i.test(f.properties?.event));
const floods = features.filter(f => /flood/i.test(f.properties?.event));
const winter = features.filter(f => /blizzard|ice storm|winter/i.test(f.properties?.event));
const fire = features.filter(f => /fire/i.test(f.properties?.event));
const other = features.filter(f => {
const e = f.properties?.event || '';
return !/hurricane|typhoon|tropical|tornado|flood|blizzard|ice storm|winter|fire/i.test(e);
});
return {
source: 'NOAA/NWS',
timestamp: new Date().toISOString(),
totalSevereAlerts: features.length,
summary: {
hurricanes: hurricanes.length,
tornadoes: tornadoes.length,
floods: floods.length,
winterStorms: winter.length,
wildfires: fire.length,
other: other.length,
},
topAlerts: features.slice(0, 15).map(f => ({
event: f.properties?.event,
severity: f.properties?.severity,
urgency: f.properties?.urgency,
headline: f.properties?.headline,
areas: f.properties?.areaDesc,
onset: f.properties?.onset,
expires: f.properties?.expires,
})),
};
}
if (process.argv[1]?.endsWith('noaa.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

143
apis/sources/ofac.mjs Normal file
View File

@@ -0,0 +1,143 @@
// OFAC — US Treasury Office of Foreign Assets Control Sanctions
// No auth required. Monitors the Specially Designated Nationals (SDN) list
// and consolidated sanctions list for changes.
import { safeFetch } from '../utils/fetch.mjs';
const EXPORTS_BASE = 'https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports';
// SDN list endpoints
const SDN_XML_URL = `${EXPORTS_BASE}/SDN.XML`;
const SDN_ADVANCED_URL = `${EXPORTS_BASE}/SDN_ADVANCED.XML`;
const CONS_ADVANCED_URL = `${EXPORTS_BASE}/CONS_ADVANCED.XML`;
// Parse basic info from SDN XML (publish date, entry count)
function parseSDNMetadata(xml) {
if (!xml || xml.error) return { error: xml?.error || 'No data returned' };
const raw = xml.rawText || '';
// Extract publish date
const publishDate = raw.match(/<Publish_Date>(.*?)<\/Publish_Date>/)?.[1]
|| raw.match(/<publish_date>(.*?)<\/publish_date>/i)?.[1]
|| null;
// Count SDN entries
const entryMatches = raw.match(/<sdnEntry>/gi);
const entryCount = entryMatches ? entryMatches.length : null;
// Extract record count if present
const recordCount = raw.match(/<Record_Count>(.*?)<\/Record_Count>/)?.[1]
|| raw.match(/<records_count>(.*?)<\/records_count>/i)?.[1]
|| null;
return {
publishDate,
entryCount,
recordCount: recordCount ? parseInt(recordCount, 10) : null,
hasData: raw.length > 0,
dataSize: raw.length,
};
}
// Fetch SDN list metadata (smaller initial chunk via timeout)
export async function getSDNMetadata() {
// The full SDN XML is large; safeFetch will get the first 500 chars
// which should include the header/publish date
const data = await safeFetch(SDN_XML_URL, { timeout: 20000 });
return parseSDNMetadata(data);
}
// Fetch advanced SDN data (includes more structured info)
export async function getSDNAdvanced() {
const data = await safeFetch(SDN_ADVANCED_URL, { timeout: 20000 });
return parseSDNMetadata(data);
}
// Fetch consolidated list metadata
export async function getConsolidatedMetadata() {
const data = await safeFetch(CONS_ADVANCED_URL, { timeout: 20000 });
return parseSDNMetadata(data);
}
// Parse recent SDN entries from XML snippet
function parseRecentEntries(xml) {
if (!xml || xml.error) return [];
const raw = xml.rawText || '';
const entries = [];
const entryRegex = /<sdnEntry>([\s\S]*?)<\/sdnEntry>/gi;
let match;
let count = 0;
while ((match = entryRegex.exec(raw)) !== null && count < 20) {
const content = match[1];
const uid = content.match(/<uid>(.*?)<\/uid>/i)?.[1];
const lastName = content.match(/<lastName>(.*?)<\/lastName>/i)?.[1];
const firstName = content.match(/<firstName>(.*?)<\/firstName>/i)?.[1];
const sdnType = content.match(/<sdnType>(.*?)<\/sdnType>/i)?.[1];
// Extract programs
const programs = [];
const progRegex = /<program>(.*?)<\/program>/gi;
let progMatch;
while ((progMatch = progRegex.exec(content)) !== null) {
programs.push(progMatch[1]);
}
if (uid || lastName) {
entries.push({
uid,
name: [firstName, lastName].filter(Boolean).join(' '),
type: sdnType,
programs,
});
count++;
}
}
return entries;
}
// Briefing — report on sanctions list status and metadata
export async function briefing() {
const [sdnMeta, advancedMeta] = await Promise.all([
getSDNMetadata(),
getSDNAdvanced(),
]);
// Try to extract any entries visible in the advanced data
const sampleEntries = parseRecentEntries(
await safeFetch(SDN_ADVANCED_URL, { timeout: 25000 })
);
return {
source: 'OFAC Sanctions',
timestamp: new Date().toISOString(),
lastUpdated: sdnMeta.publishDate || advancedMeta.publishDate || 'unknown',
sdnList: {
publishDate: sdnMeta.publishDate,
entryCount: sdnMeta.entryCount,
recordCount: sdnMeta.recordCount,
dataAvailable: sdnMeta.hasData,
},
advancedList: {
publishDate: advancedMeta.publishDate,
entryCount: advancedMeta.entryCount,
recordCount: advancedMeta.recordCount,
dataAvailable: advancedMeta.hasData,
},
sampleEntries: sampleEntries.slice(0, 10),
endpoints: {
sdnXml: SDN_XML_URL,
sdnAdvanced: SDN_ADVANCED_URL,
consolidatedAdvanced: CONS_ADVANCED_URL,
},
};
}
// Run standalone
if (process.argv[1]?.endsWith('ofac.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

View File

@@ -0,0 +1,112 @@
// OpenSanctions — Global Sanctions & PEP Aggregator
// No auth required for basic queries. Aggregates sanctions data from
// OFAC, EU, UN, and 30+ other sources into a unified searchable dataset.
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://api.opensanctions.org';
// Search sanctioned entities by name/keyword
export async function searchEntities(query, opts = {}) {
const { limit = 20, schema, topics } = opts;
const params = new URLSearchParams({
q: query,
limit: String(limit),
});
if (schema) params.set('schema', schema); // e.g. "Person", "Company", "Organization"
if (topics) params.set('topics', topics); // e.g. "sanction", "crime", "poi"
return safeFetch(`${BASE}/search/default?${params}`, { timeout: 15000 });
}
// Get available datasets/collections
export async function getCollections() {
return safeFetch(`${BASE}/collections`, { timeout: 15000 });
}
// Get details about a specific dataset
export async function getDataset(name) {
return safeFetch(`${BASE}/datasets/${name}`, { timeout: 15000 });
}
// Get a specific entity by ID
export async function getEntity(entityId) {
return safeFetch(`${BASE}/entities/${entityId}`, { timeout: 15000 });
}
// Compact entity for briefing output
function compactEntity(e) {
return {
id: e.id,
name: e.caption || e.name,
schema: e.schema,
datasets: e.datasets,
topics: e.topics,
countries: e.properties?.country || [],
lastSeen: e.last_seen,
firstSeen: e.first_seen,
};
}
// Compact search results
function compactSearchResult(result, query) {
const entities = (result?.results || []).map(compactEntity);
return {
query,
totalResults: result?.total || 0,
entities: entities.slice(0, 10),
};
}
// Key entities/subjects to monitor for sanctions intelligence
const BRIEFING_QUERIES = [
'Iran',
'Russia',
'North Korea',
'Syria',
'Venezuela',
'Wagner',
];
// Briefing — search for notable sanctioned entities across key targets
export async function briefing() {
// Run searches in parallel
const results = await Promise.all(
BRIEFING_QUERIES.map(async (query) => {
const data = await searchEntities(query, { limit: 10, topics: 'sanction' });
return compactSearchResult(data, query);
})
);
// Also fetch dataset metadata for context
const collections = await getCollections();
const datasetSummary = Array.isArray(collections)
? collections.slice(0, 10).map(c => ({
name: c.name,
title: c.title,
entityCount: c.entity_count,
lastUpdated: c.updated_at,
}))
: [];
// Aggregate totals
const totalSanctionedEntities = results.reduce(
(sum, r) => sum + (r.totalResults || 0), 0
);
return {
source: 'OpenSanctions',
timestamp: new Date().toISOString(),
recentSearches: results,
totalSanctionedEntities,
datasets: datasetSummary,
monitoringTargets: BRIEFING_QUERIES,
};
}
// Run standalone
if (process.argv[1]?.endsWith('opensanctions.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

96
apis/sources/opensky.mjs Normal file
View File

@@ -0,0 +1,96 @@
// OpenSky Network — Real-time flight tracking
// Free for research. 4,000 API credits/day (no auth), 8,000 with account.
// Tracks all aircraft with ADS-B transponders including many military.
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://opensky-network.org/api';
// Get all current flights (global state vector)
export async function getAllFlights() {
return safeFetch(`${BASE}/states/all`, { timeout: 30000 });
}
// Get flights in a bounding box (lat/lon)
export async function getFlightsInArea(lamin, lomin, lamax, lomax) {
const params = new URLSearchParams({
lamin: String(lamin),
lomin: String(lomin),
lamax: String(lamax),
lomax: String(lomax),
});
return safeFetch(`${BASE}/states/all?${params}`, { timeout: 20000 });
}
// Get flights by specific aircraft (ICAO24 hex codes)
export async function getFlightsByIcao(icao24List) {
const icao = Array.isArray(icao24List) ? icao24List : [icao24List];
const params = icao.map(i => `icao24=${i}`).join('&');
return safeFetch(`${BASE}/states/all?${params}`, { timeout: 20000 });
}
// Get departures from an airport in a time range
export async function getDepartures(airportIcao, begin, end) {
const params = new URLSearchParams({
airport: airportIcao,
begin: String(Math.floor(begin / 1000)),
end: String(Math.floor(end / 1000)),
});
return safeFetch(`${BASE}/flights/departure?${params}`);
}
// Get arrivals at an airport
export async function getArrivals(airportIcao, begin, end) {
const params = new URLSearchParams({
airport: airportIcao,
begin: String(Math.floor(begin / 1000)),
end: String(Math.floor(end / 1000)),
});
return safeFetch(`${BASE}/flights/arrival?${params}`);
}
// Key hotspot regions for monitoring
const HOTSPOTS = {
middleEast: { lamin: 12, lomin: 30, lamax: 42, lomax: 65, label: 'Middle East' },
taiwan: { lamin: 20, lomin: 115, lamax: 28, lomax: 125, label: 'Taiwan Strait' },
ukraine: { lamin: 44, lomin: 22, lamax: 53, lomax: 41, label: 'Ukraine Region' },
baltics: { lamin: 53, lomin: 19, lamax: 60, lomax: 29, label: 'Baltic Region' },
southChinaSea: { lamin: 5, lomin: 105, lamax: 23, lomax: 122, label: 'South China Sea' },
koreanPeninsula: { lamin: 33, lomin: 124, lamax: 43, lomax: 132, label: 'Korean Peninsula' },
};
// Briefing — check hotspot regions for flight activity
export async function briefing() {
const hotspotEntries = Object.entries(HOTSPOTS);
const results = await Promise.all(
hotspotEntries.map(async ([key, box]) => {
const data = await getFlightsInArea(box.lamin, box.lomin, box.lamax, box.lomax);
const states = data?.states || [];
return {
region: box.label,
key,
totalAircraft: states.length,
// states format: [icao24, callsign, origin_country, ...]
byCountry: states.reduce((acc, s) => {
const country = s[2] || 'Unknown';
acc[country] = (acc[country] || 0) + 1;
return acc;
}, {}),
// Flag potentially interesting (military often have no callsign or specific patterns)
noCallsign: states.filter(s => !s[1]?.trim()).length,
highAltitude: states.filter(s => s[7] && s[7] > 12000).length, // >12km altitude
};
})
);
return {
source: 'OpenSky',
timestamp: new Date().toISOString(),
hotspots: results,
};
}
if (process.argv[1]?.endsWith('opensky.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

205
apis/sources/patents.mjs Normal file
View File

@@ -0,0 +1,205 @@
// USPTO PatentsView — Patent Intelligence
// No auth required. Tracks patent filings in strategic technology areas.
// API v1: https://search.patentsview.org/api/v1/patent/
// Useful for detecting R&D trends, tech competition, state-backed innovation.
import { safeFetch, daysAgo } from '../utils/fetch.mjs';
const BASE = 'https://search.patentsview.org/api/v1';
// Strategic technology domains and their search terms
const STRATEGIC_DOMAINS = {
ai: {
label: 'Artificial Intelligence',
terms: ['artificial intelligence', 'machine learning', 'deep learning', 'neural network', 'large language model'],
},
quantum: {
label: 'Quantum Computing',
terms: ['quantum computing', 'quantum processor', 'qubit', 'quantum entanglement', 'quantum cryptography'],
},
nuclear: {
label: 'Nuclear Technology',
terms: ['nuclear fusion', 'nuclear reactor', 'nuclear fuel', 'uranium enrichment', 'small modular reactor'],
},
hypersonic: {
label: 'Hypersonic & Advanced Propulsion',
terms: ['hypersonic', 'scramjet', 'directed energy weapon', 'railgun', 'advanced propulsion'],
},
semiconductor: {
label: 'Semiconductor & Chip Technology',
terms: ['semiconductor', 'integrated circuit', 'lithography', 'chip fabrication', 'transistor'],
},
biotech: {
label: 'Biotechnology & Synthetic Biology',
terms: ['synthetic biology', 'gene editing', 'CRISPR', 'mRNA', 'bioweapon'],
},
space: {
label: 'Space & Satellite Technology',
terms: ['satellite', 'space launch', 'orbital', 'space debris', 'anti-satellite'],
},
};
// Search patents by keyword query
export async function searchPatents(query, opts = {}) {
const {
since = daysAgo(90),
limit = 10,
sort = 'patent_date',
sortDir = 'desc',
} = opts;
// PatentsView v1 API uses query params with JSON values
const q = JSON.stringify({
_and: [
{ _gte: { patent_date: since } },
{ _text_any: { patent_abstract: query } },
],
});
const f = JSON.stringify([
'patent_id',
'patent_title',
'patent_date',
'patent_abstract',
'assignee_organization',
'patent_type',
]);
const o = JSON.stringify({ [sort]: sortDir });
const params = new URLSearchParams({
q,
f,
o,
s: String(limit),
});
return safeFetch(`${BASE}/patent/?${params}`, { timeout: 20000 });
}
// Search by assignee organization
export async function searchByAssignee(orgName, opts = {}) {
const { since = daysAgo(180), limit = 10 } = opts;
const q = JSON.stringify({
_and: [
{ _gte: { patent_date: since } },
{ _contains: { assignee_organization: orgName } },
],
});
const f = JSON.stringify([
'patent_id',
'patent_title',
'patent_date',
'patent_abstract',
'assignee_organization',
]);
const o = JSON.stringify({ patent_date: 'desc' });
const params = new URLSearchParams({
q,
f,
o,
s: String(limit),
});
return safeFetch(`${BASE}/patent/?${params}`, { timeout: 20000 });
}
// Compact patent record for briefing output
function compactPatent(p) {
return {
id: p.patent_id,
title: p.patent_title,
date: p.patent_date,
assignee: p.assignee_organization || 'Unknown',
type: p.patent_type,
};
}
// Search a single domain, combining its keyword terms
async function searchDomain(domain, since) {
const terms = domain.terms.join(' ');
const data = await searchPatents(terms, { since, limit: 10 });
// PatentsView v1 returns { patents: [...] } or similar
const patents = data?.patents || data?.results || [];
if (!Array.isArray(patents)) return [];
return patents.map(compactPatent);
}
// Briefing — search recent patents in key strategic tech areas
export async function briefing() {
const since = daysAgo(90);
const domainEntries = Object.entries(STRATEGIC_DOMAINS);
const recentPatents = {};
const signals = [];
// Run all domain searches in parallel
const results = await Promise.all(
domainEntries.map(async ([key, domain]) => {
const patents = await searchDomain(domain, since);
return { key, label: domain.label, patents };
})
);
let totalFound = 0;
for (const { key, label, patents } of results) {
recentPatents[key] = patents;
totalFound += patents.length;
if (patents.length > 0) {
// Identify dominant assignees (potential state-backed programs)
const assigneeCounts = {};
patents.forEach(p => {
if (p.assignee && p.assignee !== 'Unknown') {
assigneeCounts[p.assignee] = (assigneeCounts[p.assignee] || 0) + 1;
}
});
// Flag organizations with high patent density in strategic areas
Object.entries(assigneeCounts).forEach(([org, count]) => {
if (count >= 3) {
signals.push(`HIGH ACTIVITY: ${org} filed ${count} ${label} patents in last 90 days`);
}
});
}
}
// Track key defense/intelligence organizations specifically
const watchOrgs = [
'Raytheon', 'Lockheed Martin', 'Northrop Grumman', 'BAE Systems',
'China Academy', 'Huawei', 'SMIC', 'Samsung', 'TSMC',
'US Department', 'Navy', 'Air Force', 'Army', 'DARPA',
];
for (const { patents } of results) {
for (const p of patents) {
if (watchOrgs.some(org => p.assignee?.toLowerCase().includes(org.toLowerCase()))) {
signals.push(`WATCH ORG: "${p.title}" by ${p.assignee} (${p.date})`);
}
}
}
return {
source: 'USPTO Patents',
timestamp: new Date().toISOString(),
searchWindow: `${since} to ${new Date().toISOString().split('T')[0]}`,
totalFound,
recentPatents,
signals: signals.length > 0
? signals
: ['No unusual patent filing patterns detected in strategic domains'],
domains: Object.fromEntries(
domainEntries.map(([key, domain]) => [key, domain.label])
),
};
}
// Run standalone
if (process.argv[1]?.endsWith('patents.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

107
apis/sources/reddit.mjs Normal file
View File

@@ -0,0 +1,107 @@
// Reddit — social sentiment intelligence
// Reddit now requires OAuth for API access (public JSON API returns 403).
// Gracefully degrades when not authenticated.
// To enable: register an app at https://www.reddit.com/prefs/apps/ and set
// REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET in .env
import { safeFetch } from '../utils/fetch.mjs';
import '../utils/env.mjs';
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
const SUBREDDITS = [
'worldnews',
'geopolitics',
'economics',
'wallstreetbets',
'commodities',
];
// Get OAuth token using client credentials flow (application-only)
async function getToken() {
const clientId = process.env.REDDIT_CLIENT_ID;
const clientSecret = process.env.REDDIT_CLIENT_SECRET;
if (!clientId || !clientSecret) return null;
try {
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const res = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Crucix/1.0 intelligence-engine',
},
body: 'grant_type=client_credentials',
});
if (!res.ok) return null;
const data = await res.json();
return data.access_token || null;
} catch {
return null;
}
}
// Fetch hot posts — tries OAuth first, then falls back to public endpoint
export async function getHot(subreddit, opts = {}) {
const { limit = 10, token = null } = opts;
if (token) {
// Use OAuth endpoint
return safeFetch(`https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1`, {
headers: {
'Authorization': `Bearer ${token}`,
'User-Agent': 'Crucix/1.0 intelligence-engine',
},
});
}
// Try public endpoint (may 403)
return safeFetch(`https://www.reddit.com/r/${subreddit}/hot.json?limit=${limit}&raw_json=1`, {
headers: { 'User-Agent': 'Crucix/1.0 intelligence-engine' },
});
}
function compactPost(child) {
const d = child?.data;
if (!d) return null;
return {
title: d.title,
score: d.score ?? 0,
comments: d.num_comments ?? 0,
url: d.url,
created: d.created_utc ? new Date(d.created_utc * 1000).toISOString() : null,
};
}
export async function briefing() {
const token = await getToken();
if (!token && !process.env.REDDIT_CLIENT_ID) {
return {
source: 'Reddit',
timestamp: new Date().toISOString(),
status: 'no_key',
message: 'Reddit requires OAuth. Register at https://www.reddit.com/prefs/apps/ (script type), set REDDIT_CLIENT_ID and REDDIT_CLIENT_SECRET in .env',
};
}
const subredditResults = {};
for (const sub of SUBREDDITS) {
const result = await getHot(sub, { limit: 10, token });
const children = result?.data?.children || [];
subredditResults[sub] = children.map(compactPost).filter(Boolean);
await delay(token ? 1000 : 2000);
}
return {
source: 'Reddit',
timestamp: new Date().toISOString(),
subreddits: subredditResults,
};
}
if (process.argv[1]?.endsWith('reddit.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

152
apis/sources/reliefweb.mjs Normal file
View File

@@ -0,0 +1,152 @@
// ReliefWeb — UN OCHA humanitarian crisis tracking
// Requires approved appname since Nov 2025. Register at https://apidoc.reliefweb.int/parameters#appname
// Falls back to HDX (Humanitarian Data Exchange) if ReliefWeb API returns 403.
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://api.reliefweb.int/v1';
// Register your own appname at https://apidoc.reliefweb.int/parameters#appname
// and replace this value. Without an approved appname the API returns 403.
const APPNAME = process.env.RELIEFWEB_APPNAME || 'crucix';
const HDX_BASE = 'https://data.humdata.org/api/3/action';
// POST-based search for reports (ReliefWeb API v1 POST format)
async function rwPost(endpoint, body) {
const url = `${BASE}/${endpoint}?appname=${APPNAME}`;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Crucix/1.0',
},
body: JSON.stringify(body),
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) {
const errBody = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${errBody.slice(0, 200)}`);
}
return await res.json();
} catch (e) {
return { error: e.message, source: url };
}
}
// Search recent reports via ReliefWeb API (POST method)
export async function searchReports(opts = {}) {
const { query = '', limit = 15 } = opts;
const body = {
limit,
fields: {
include: [
'title',
'date.created',
'country.name',
'disaster_type.name',
'url_alias',
'source.name',
],
},
sort: ['date.created:desc'],
};
if (query) {
body.query = { value: query };
}
return rwPost('reports', body);
}
// Get active disasters via ReliefWeb API (POST method)
export async function getDisasters(opts = {}) {
const { limit = 15 } = opts;
const body = {
limit,
fields: {
include: ['name', 'date.created', 'country.name', 'type.name', 'status'],
},
filter: {
field: 'status',
value: 'ongoing',
},
sort: ['date.created:desc'],
};
return rwPost('disasters', body);
}
// Fallback: search HDX (Humanitarian Data Exchange) for crisis datasets
async function hdxFallback(limit = 15) {
const data = await safeFetch(
`${HDX_BASE}/package_search?q=crisis+OR+disaster+OR+emergency&rows=${limit}&sort=metadata_modified+desc`
);
if (data?.result?.results) {
return data.result.results.map(pkg => ({
title: pkg.title,
date: pkg.metadata_modified,
source: pkg.dataset_source || pkg.organization?.title,
countries: pkg.groups?.map(g => g.display_name),
url: `https://data.humdata.org/dataset/${pkg.name}`,
}));
}
return [];
}
// Briefing — get latest humanitarian crises
export async function briefing() {
const [reports, disasters] = await Promise.all([
searchReports({ limit: 15 }),
getDisasters({ limit: 15 }),
]);
const rwFailed = !!reports?.error || !!disasters?.error;
let latestReports = [];
let activeDisasters = [];
let hdxDatasets = [];
if (!rwFailed) {
latestReports = (reports?.data || []).map(r => ({
title: r.fields?.title,
date: r.fields?.date?.created,
countries: r.fields?.country?.map(c => c.name),
disasterType: r.fields?.disaster_type?.map(d => d.name),
source: r.fields?.source?.map(s => s.name),
url: r.fields?.url_alias
? `https://reliefweb.int${r.fields.url_alias}`
: null,
}));
activeDisasters = (disasters?.data || []).map(d => ({
name: d.fields?.name,
date: d.fields?.date?.created,
countries: d.fields?.country?.map(c => c.name),
type: d.fields?.type?.map(t => t.name),
status: d.fields?.status,
}));
} else {
// Fallback to HDX when ReliefWeb returns 403 (unapproved appname)
hdxDatasets = await hdxFallback(15);
}
return {
source: rwFailed ? 'HDX (Humanitarian Data Exchange) — ReliefWeb fallback' : 'ReliefWeb (UN OCHA)',
timestamp: new Date().toISOString(),
...(rwFailed
? {
rwError: reports?.error || disasters?.error,
rwNote: 'ReliefWeb API requires an approved appname since Nov 2025. Set RELIEFWEB_APPNAME env var after registering at https://apidoc.reliefweb.int/parameters#appname',
hdxDatasets,
}
: {
latestReports,
activeDisasters,
}),
};
}
if (process.argv[1]?.endsWith('reliefweb.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

82
apis/sources/safecast.mjs Normal file
View File

@@ -0,0 +1,82 @@
// Safecast — Global radiation monitoring (150M+ readings)
// No auth required. CC0 public domain. Citizen-science network.
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://api.safecast.org';
// Get recent measurements in an area
export async function getMeasurements(opts = {}) {
const {
latitude = null,
longitude = null,
distance = 100, // km
limit = 50,
since = null,
} = opts;
const params = new URLSearchParams({ limit: String(limit) });
if (latitude && longitude) {
params.set('latitude', String(latitude));
params.set('longitude', String(longitude));
params.set('distance', String(distance * 1000)); // meters
}
if (since) params.set('since', since);
return safeFetch(`${BASE}/measurements.json?${params}`);
}
// Key nuclear sites to monitor
const NUCLEAR_SITES = {
zaporizhzhia: { lat: 47.51, lon: 34.58, label: 'Zaporizhzhia NPP (Ukraine)', radius: 100 },
chernobyl: { lat: 51.39, lon: 30.1, label: 'Chernobyl Exclusion Zone', radius: 50 },
bushehr: { lat: 28.83, lon: 50.89, label: 'Bushehr NPP (Iran)', radius: 100 },
yongbyon: { lat: 39.8, lon: 125.75, label: 'Yongbyon (North Korea)', radius: 100 },
fukushima: { lat: 37.42, lon: 141.03, label: 'Fukushima Daiichi', radius: 50 },
dimona: { lat: 31.0, lon: 35.15, label: 'Dimona (Israel)', radius: 100 },
};
// Briefing — check radiation levels near key nuclear sites
export async function briefing() {
const results = await Promise.all(
Object.entries(NUCLEAR_SITES).map(async ([key, site]) => {
const data = await getMeasurements({
latitude: site.lat,
longitude: site.lon,
distance: site.radius,
limit: 10,
});
const measurements = Array.isArray(data) ? data : [];
const values = measurements.map(m => m.value).filter(v => typeof v === 'number');
const avgCPM = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : null;
return {
site: site.label,
key,
recentReadings: values.length,
avgCPM,
maxCPM: values.length > 0 ? Math.max(...values) : null,
// Normal background: 10-80 CPM. >100 CPM warrants attention.
anomaly: avgCPM !== null && avgCPM > 100,
lastReading: measurements[0]?.captured_at || null,
};
})
);
const anomalies = results.filter(r => r.anomaly);
return {
source: 'Safecast',
timestamp: new Date().toISOString(),
sites: results,
signals: anomalies.length > 0
? anomalies.map(a => `ELEVATED RADIATION at ${a.site}: ${a.avgCPM?.toFixed(1)} CPM (normal: 10-80)`)
: ['All monitored nuclear sites within normal radiation levels'],
};
}
if (process.argv[1]?.endsWith('safecast.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

66
apis/sources/ships.mjs Normal file
View File

@@ -0,0 +1,66 @@
// Ship/Vessel Tracking — aisstream.io (free real-time global AIS)
// Also includes fallback to public vessel tracking data
// Detects: dark ships, sanctions evasion, naval deployments, port congestion
import { safeFetch } from '../utils/fetch.mjs';
// aisstream.io requires a WebSocket connection for real-time data
// For briefing mode, we'll use snapshot-based approaches
// MarineTraffic-style density estimation via public endpoints
// The real power comes from running a persistent WebSocket listener
// Key maritime chokepoints to monitor
const CHOKEPOINTS = {
straitOfHormuz: { label: 'Strait of Hormuz', lat: 26.5, lon: 56.5, note: '20% of world oil' },
suezCanal: { label: 'Suez Canal', lat: 30.5, lon: 32.3, note: '12% of world trade' },
straitOfMalacca: { label: 'Strait of Malacca', lat: 2.5, lon: 101.5, note: '25% of world trade' },
babElMandeb: { label: 'Bab el-Mandeb', lat: 12.6, lon: 43.3, note: 'Red Sea gateway' },
taiwanStrait: { label: 'Taiwan Strait', lat: 24.0, lon: 119.0, note: '88% of largest container ships' },
bosporusStrait: { label: 'Bosphorus', lat: 41.1, lon: 29.1, note: 'Black Sea access' },
panamaCanal: { label: 'Panama Canal', lat: 9.1, lon: -79.7, note: '5% of world trade' },
capeOfGoodHope: { label: 'Cape of Good Hope', lat: -34.4, lon: 18.5, note: 'Suez alternative' },
};
// For non-realtime briefing, use web-searchable vessel data
export async function briefing() {
const hasKey = !!process.env.AISSTREAM_API_KEY;
return {
source: 'Maritime/AIS',
timestamp: new Date().toISOString(),
status: hasKey ? 'ready' : 'limited',
message: hasKey
? 'AIS stream connected — use WebSocket listener for real-time data'
: 'Set AISSTREAM_API_KEY for real-time global vessel tracking (free at aisstream.io)',
chokepoints: CHOKEPOINTS,
monitoringCapabilities: [
'Dark ship detection (AIS transponder shutoffs)',
'Sanctions evasion (ship-to-ship transfers)',
'Naval deployment tracking',
'Port congestion (vessel dwell time)',
'Chokepoint traffic anomalies',
'Oil tanker route changes',
],
hint: 'For now, I can use web search to check maritime news and shipping disruptions',
};
}
// WebSocket listener setup (for persistent monitoring)
export function getWebSocketConfig(apiKey) {
return {
url: 'wss://stream.aisstream.io/v0/stream',
message: JSON.stringify({
APIKey: apiKey,
BoundingBoxes: Object.values(CHOKEPOINTS).map(cp => [
[cp.lat - 2, cp.lon - 2],
[cp.lat + 2, cp.lon + 2],
]),
}),
};
}
if (process.argv[1]?.endsWith('ships.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

336
apis/sources/telegram.mjs Normal file
View File

@@ -0,0 +1,336 @@
// Telegram — public channel intelligence from conflict zones and OSINT analysts
// Primary mode: Bot API with TELEGRAM_BOT_TOKEN (getUpdates, getChat)
// Fallback mode: Scrape public channel web previews at https://t.me/s/{channel}
// Monitors conflict zones (Ukraine, Middle East), geopolitics, and OSINT channels.
import { safeFetch } from '../utils/fetch.mjs';
import '../utils/env.mjs';
function delay(ms) { return new Promise(r => setTimeout(r, ms)); }
// Curated list of well-known public OSINT / conflict / geopolitics channels
// All verified to have public web previews enabled at https://t.me/s/{id}
const CHANNELS = [
{ id: 'intelslava', label: 'Intel Slava Z', topic: 'conflict', note: 'Conflict updates, pro-Russian perspective' },
{ id: 'legitimniy', label: 'Legitimniy', topic: 'conflict', note: 'Ukrainian politics & conflict analysis' },
{ id: 'wartranslated', label: 'War Translated', topic: 'conflict', note: 'Conflict translations & OSINT' },
{ id: 'ukraine_frontline', label: 'Ukraine Frontline', topic: 'conflict', note: 'Frontline situation updates' },
{ id: 'middleeastosint', label: 'Middle East OSINT', topic: 'osint', note: 'Middle East open source intel' },
{ id: 'mod_russia', label: 'Russian MoD', topic: 'conflict', note: 'Russian Ministry of Defense official' },
{ id: 'CIG_telegram', label: 'Conflict Intel Team', topic: 'osint', note: 'Conflict Intelligence Team analysis' },
{ id: 'RVvoenkor', label: 'Voenkor RV', topic: 'conflict', note: 'Russian military correspondent' },
{ id: 'readovkanews', label: 'Readovka', topic: 'conflict', note: 'Russian conflict news aggregator' },
{ id: 'DeepStateUA', label: 'DeepState Ukraine', topic: 'conflict', note: 'Ukrainian frontline maps & analysis' },
{ id: 'operativnoZSU', label: 'ZSU Operative', topic: 'conflict', note: 'Ukrainian armed forces updates' },
{ id: 'GeneralStaffZSU', label: 'General Staff ZSU', topic: 'conflict', note: 'Ukrainian General Staff official' },
];
// Urgent keywords that flag high-priority posts
const URGENT_KEYWORDS = [
'breaking', 'urgent', 'alert', 'missile', 'strike', 'explosion',
'nuclear', 'chemical', 'ceasefire', 'escalation', 'invasion',
'offensive', 'airstrike', 'casualties', 'retreat', 'advance',
'nato', 'mobilization', 'coup', 'assassination', 'drone',
];
// ─── Bot API mode ───────────────────────────────────────────────────────────
const botBase = () => `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}`;
// Get recent updates the bot has received
export async function getUpdates(opts = {}) {
const { limit = 100, offset = 0 } = opts;
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
return safeFetch(`${botBase()}/getUpdates?${params}`);
}
// Get info about a chat/channel by username
export async function getChat(chatId) {
const params = new URLSearchParams({ chat_id: chatId.startsWith('@') ? chatId : `@${chatId}` });
return safeFetch(`${botBase()}/getChat?${params}`);
}
// Compact a Bot API message for briefing output
function compactBotMessage(msg) {
return {
text: (msg.text || msg.caption || '').slice(0, 300),
date: msg.date ? new Date(msg.date * 1000).toISOString() : null,
chat: msg.chat?.title || msg.chat?.username || 'unknown',
views: msg.views || 0,
hasMedia: !!(msg.photo || msg.video || msg.document),
};
}
// Fetch updates via Bot API and organize by channel
async function fetchBotUpdates() {
const result = await getUpdates({ limit: 100 });
if (!result?.ok || !Array.isArray(result.result)) {
return { error: result?.description || 'Bot API request failed' };
}
const messages = result.result
.map(u => u.message || u.channel_post || u.edited_channel_post)
.filter(Boolean)
.map(compactBotMessage);
return { messages, count: messages.length };
}
// ─── Web preview scraping fallback ──────────────────────────────────────────
// Fetch raw HTML from a URL (safeFetch truncates non-JSON to 500 chars, too short)
async function fetchHTML(url, timeoutMs = 15000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9',
},
});
clearTimeout(timer);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.text();
} catch (e) {
clearTimeout(timer);
return null;
}
}
// Parse messages from Telegram web preview HTML (https://t.me/s/channel)
// The HTML contains <div class="tgme_widget_message_wrap"> blocks with message content.
function parseWebPreview(html, channelId) {
if (!html) return [];
const messages = [];
// Each message sits inside a tgme_widget_message_wrap div
// We extract using the data-post attribute which has the format "channel/msgId"
const msgBlockRegex = /class="tgme_widget_message_wrap[^"]*"[\s\S]*?data-post="([^"]*)"([\s\S]*?)(?=class="tgme_widget_message_wrap|$)/gi;
// Simpler: split on message boundaries using data-post
const postRegex = /data-post="([^"]+)"([\s\S]*?)(?=data-post="|$)/gi;
let match;
while ((match = postRegex.exec(html)) !== null && messages.length < 20) {
const postId = match[1]; // e.g. "intelslava/12345"
const block = match[2];
// Extract message text from tgme_widget_message_text
const textMatch = block.match(/class="tgme_widget_message_text[^"]*"[^>]*>([\s\S]*?)<\/div>/i);
let text = '';
if (textMatch) {
text = textMatch[1]
.replace(/<br\s*\/?>/gi, '\n') // preserve line breaks
.replace(/<[^>]+>/g, '') // strip HTML tags
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'")
.replace(/&nbsp;/g, ' ')
.trim()
.slice(0, 300);
}
// Extract view count
const viewsMatch = block.match(/class="tgme_widget_message_views"[^>]*>([\s\S]*?)<\/span>/i);
let views = 0;
if (viewsMatch) {
const raw = viewsMatch[1].trim();
if (raw.endsWith('K')) views = parseFloat(raw) * 1000;
else if (raw.endsWith('M')) views = parseFloat(raw) * 1000000;
else views = parseInt(raw, 10) || 0;
}
// Extract datetime
const timeMatch = block.match(/datetime="([^"]+)"/i);
const date = timeMatch ? timeMatch[1] : null;
// Check for media (photos, videos)
const hasMedia = /tgme_widget_message_photo|tgme_widget_message_video/i.test(block);
if (text || hasMedia) {
messages.push({
postId,
text,
date,
views,
hasMedia,
channel: channelId,
});
}
}
return messages;
}
// Scrape a single channel's web preview
async function scrapeChannel(channelId) {
const url = `https://t.me/s/${channelId}`;
const html = await fetchHTML(url);
if (!html) return { channel: channelId, error: 'Failed to fetch', posts: [] };
// Extract channel title from page
const titleMatch = html.match(/class="tgme_channel_info_header_title[^"]*"[^>]*>([\s\S]*?)<\/span>/i)
|| html.match(/<title>(.*?)<\/title>/i);
const title = titleMatch
? titleMatch[1].replace(/<[^>]+>/g, '').trim()
: channelId;
const posts = parseWebPreview(html, channelId);
return { channel: channelId, title, posts, postCount: posts.length };
}
// ─── Analysis helpers ───────────────────────────────────────────────────────
// Flag urgent/high-priority posts
function flagUrgent(post) {
const lower = (post.text || '').toLowerCase();
const matched = URGENT_KEYWORDS.filter(k => lower.includes(k));
return matched.length > 0 ? matched : null;
}
// Score a post's significance (views + urgency + length)
function significanceScore(post) {
let score = 0;
score += Math.min(post.views / 1000, 50); // views weight (capped)
const urgentFlags = flagUrgent(post);
if (urgentFlags) score += urgentFlags.length * 10; // urgency weight
if (post.text?.length > 100) score += 5; // substantive text bonus
if (post.hasMedia) score += 3; // media bonus
return score;
}
// Group posts by topic based on the channel config
function groupByTopic(allPosts, channelMeta) {
const groups = {};
for (const post of allPosts) {
const meta = channelMeta.find(c => c.id === post.channel);
const topic = meta?.topic || 'other';
if (!groups[topic]) groups[topic] = [];
groups[topic].push(post);
}
return groups;
}
// ─── Briefing ───────────────────────────────────────────────────────────────
export async function briefing() {
const token = process.env.TELEGRAM_BOT_TOKEN;
// Try Bot API first if token is available
if (token) {
try {
const botData = await fetchBotUpdates();
if (!botData.error && botData.count > 0) {
const enriched = botData.messages.map(m => ({
...m,
urgentFlags: flagUrgent(m),
score: significanceScore(m),
}));
const urgent = enriched.filter(m => m.urgentFlags).sort((a, b) => b.score - a.score);
const top = enriched.sort((a, b) => b.score - a.score).slice(0, 15);
return {
source: 'Telegram',
timestamp: new Date().toISOString(),
status: 'bot_api',
totalMessages: botData.count,
urgentPosts: urgent.slice(0, 10),
topPosts: top,
note: 'Data from Bot API getUpdates. Bot must be added to channels to receive posts.',
};
}
// If bot returned no messages, fall through to web scraping
} catch { /* fall through to scraping */ }
}
// Fallback: scrape public channel web previews (no auth needed)
const results = [];
const errors = [];
// Fetch channels in batches of 3 to avoid rate limiting
for (let i = 0; i < CHANNELS.length; i += 3) {
const batch = CHANNELS.slice(i, i + 3);
const batchResults = await Promise.all(
batch.map(ch => scrapeChannel(ch.id))
);
results.push(...batchResults);
// Delay between batches to be respectful
if (i + 3 < CHANNELS.length) await delay(1500);
}
// Collect all posts and separate errors
const allPosts = [];
const channelSummaries = [];
for (const r of results) {
const meta = CHANNELS.find(c => c.id === r.channel);
if (r.error) {
errors.push({ channel: r.channel, error: r.error });
}
// Enrich posts with urgency flags and scores
const enriched = (r.posts || []).map(p => ({
...p,
urgentFlags: flagUrgent(p),
score: significanceScore(p),
}));
allPosts.push(...enriched);
channelSummaries.push({
channel: r.channel,
title: r.title || meta?.label || r.channel,
topic: meta?.topic || 'other',
postCount: r.postCount || 0,
reachable: !r.error,
});
}
// Sort all posts by significance
allPosts.sort((a, b) => b.score - a.score);
// Separate urgent posts
const urgentPosts = allPosts.filter(p => p.urgentFlags).slice(0, 15);
// Group by topic
const byTopic = groupByTopic(allPosts, CHANNELS);
const topicSummary = {};
for (const [topic, posts] of Object.entries(byTopic)) {
topicSummary[topic] = {
totalPosts: posts.length,
urgentCount: posts.filter(p => p.urgentFlags).length,
topPosts: posts.sort((a, b) => b.score - a.score).slice(0, 5),
};
}
return {
source: 'Telegram',
timestamp: new Date().toISOString(),
status: token ? 'bot_api_empty_fallback_scrape' : 'web_scrape',
method: 'Public channel web preview scraping (no auth required)',
channelsMonitored: channelSummaries.length,
channelsReachable: channelSummaries.filter(c => c.reachable).length,
totalPosts: allPosts.length,
urgentPosts,
byTopic: topicSummary,
channels: channelSummaries,
errors: errors.length > 0 ? errors : undefined,
topPosts: allPosts.slice(0, 15),
hint: token
? undefined
: 'Set TELEGRAM_BOT_TOKEN in .env for Bot API access. Create a bot via @BotFather on Telegram.',
};
}
// ─── CLI runner ─────────────────────────────────────────────────────────────
if (process.argv[1]?.endsWith('telegram.mjs')) {
console.log('Telegram OSINT — fetching public channel intelligence...\n');
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

80
apis/sources/treasury.mjs Normal file
View File

@@ -0,0 +1,80 @@
// US Treasury Fiscal Data — Government debt, spending, yields
// No auth required. Daily updates.
import { safeFetch, today, daysAgo } from '../utils/fetch.mjs';
const BASE = 'https://api.fiscaldata.treasury.gov/services/api/fiscal_service';
// Debt to the Penny (daily national debt)
export async function getDebtToThePenny(days = 30) {
const params = new URLSearchParams({
'fields': 'record_date,tot_pub_debt_out_amt,intragov_hold_amt,debt_held_public_amt',
'sort': '-record_date',
'page[size]': '30',
'filter': `record_date:gte:${daysAgo(days)}`,
});
return safeFetch(`${BASE}/v2/accounting/od/debt_to_penny?${params}`);
}
// Daily Treasury Statement (government cash flow)
export async function getDailyStatement(days = 7) {
const params = new URLSearchParams({
'fields': 'record_date,account_type,close_today_bal',
'sort': '-record_date',
'page[size]': '20',
'filter': `record_date:gte:${daysAgo(days)}`,
});
return safeFetch(`${BASE}/v1/accounting/dts/deposits_withdrawals_operating_cash?${params}`);
}
// Treasury yield curves (average interest rates on debt)
export async function getAvgInterestRates() {
const params = new URLSearchParams({
'fields': 'record_date,security_desc,avg_interest_rate_amt',
'sort': '-record_date',
'page[size]': '50',
'filter': `record_date:gte:${daysAgo(30)}`,
});
return safeFetch(`${BASE}/v2/accounting/od/avg_interest_rates?${params}`);
}
// Briefing — key treasury data
export async function briefing() {
const [debt, rates] = await Promise.all([
getDebtToThePenny(14),
getAvgInterestRates(),
]);
const debtData = debt?.data || [];
const latestDebt = debtData[0];
const signals = [];
if (latestDebt) {
const totalDebt = parseFloat(latestDebt.tot_pub_debt_out_amt);
if (totalDebt > 36_000_000_000_000) {
signals.push(`National debt at $${(totalDebt / 1e12).toFixed(2)}T`);
}
}
return {
source: 'US Treasury',
timestamp: new Date().toISOString(),
debt: debtData.slice(0, 5).map(d => ({
date: d.record_date,
totalDebt: d.tot_pub_debt_out_amt,
publicDebt: d.debt_held_public_amt,
intragovDebt: d.intragov_hold_amt,
})),
interestRates: (rates?.data || []).slice(0, 20).map(r => ({
date: r.record_date,
security: r.security_desc,
rate: r.avg_interest_rate_amt,
})),
signals,
};
}
if (process.argv[1]?.endsWith('treasury.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

View File

@@ -0,0 +1,119 @@
// USAspending — Federal spending, defense contracts, procurement signals
// No auth required. Updated daily.
import { safeFetch, daysAgo } from '../utils/fetch.mjs';
const BASE = 'https://api.usaspending.gov/api/v2';
// Award type codes — required by the spending_by_award endpoint
// Contracts: A=BPA Call, B=Purchase Order, C=Delivery Order, D=Definitive Contract
// Grants: 02=Block Grant, 03=Formula Grant, 04=Project Grant, 05=Cooperative Agreement
// Direct payments: 06=Direct Payment (unrestricted), 07=Direct Payment (specified use)
// Loans: 08=Direct Loan, 09=Guaranteed/Insured Loan
// IDVs: IDV_A=GWAC, IDV_B=IDC, IDV_B_A=IDC / IDV, IDV_B_B=IDC / Multiple Award,
// IDV_B_C=IDC / FSS, IDV_C=FSS, IDV_D=BOA, IDV_E=BPA
const CONTRACT_CODES = ['A', 'B', 'C', 'D'];
const ALL_AWARD_CODES = ['A', 'B', 'C', 'D', '02', '03', '04', '05', '06', '07', '08', '09'];
// Search recent awards/contracts
export async function searchAwards(opts = {}) {
const {
keywords = ['defense', 'military'],
limit = 20,
sortField = 'Award Amount',
order = 'desc',
awardTypeCodes = CONTRACT_CODES,
days = 30,
} = opts;
const body = {
filters: {
keywords,
time_period: [{ start_date: daysAgo(days), end_date: daysAgo(0) }],
award_type_codes: awardTypeCodes,
},
fields: [
'Award ID',
'Recipient Name',
'Award Amount',
'Description',
'Awarding Agency',
'Start Date',
'Award Type',
],
limit,
page: 1,
sort: sortField,
order,
};
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
const res = await fetch(`${BASE}/search/spending_by_award/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) {
const errBody = await res.text().catch(() => '');
return { error: `HTTP ${res.status}: ${errBody.slice(0, 300)}`, results: [] };
}
return res.json();
} catch (e) {
return { error: e.message, results: [] };
}
}
// Get top agencies by spending
export async function getAgencySpending() {
return safeFetch(`${BASE}/references/toptier_agencies/`);
}
// Search for defense-specific spending
export async function getDefenseSpending(days = 30) {
return searchAwards({
keywords: ['defense', 'military', 'missile', 'ammunition', 'aircraft', 'naval'],
limit: 20,
sortField: 'Award Amount',
order: 'desc',
awardTypeCodes: CONTRACT_CODES,
days,
});
}
// Briefing
export async function briefing() {
const [defense, agencies] = await Promise.all([
getDefenseSpending(14),
getAgencySpending(),
]);
return {
source: 'USAspending',
timestamp: new Date().toISOString(),
recentDefenseContracts: (defense?.results || []).slice(0, 10).map(r => ({
awardId: r['Award ID'],
recipient: r['Recipient Name'],
amount: r['Award Amount'],
description: r['Description'],
agency: r['Awarding Agency'],
date: r['Start Date'],
type: r['Award Type'],
})),
topAgencies: (agencies?.results || []).slice(0, 10).map(a => ({
name: a.agency_name,
budget: a.budget_authority_amount,
obligations: a.obligated_amount,
outlays: a.outlay_amount,
})),
...(defense?.error ? { defenseError: defense.error } : {}),
};
}
if (process.argv[1]?.endsWith('usaspending.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

89
apis/sources/who.mjs Normal file
View File

@@ -0,0 +1,89 @@
// WHO — World Health Organization Global Health Observatory
// No auth required. Disease outbreak monitoring.
import { safeFetch } from '../utils/fetch.mjs';
const GHO_BASE = 'https://ghoapi.azureedge.net/api';
const DON_API = 'https://www.who.int/api/news/diseaseoutbreaknews';
// Get GHO indicator data
export async function getIndicator(code, opts = {}) {
const { filter = '', top = 20 } = opts;
let url = `${GHO_BASE}/${code}?$top=${top}&$orderby=TimeDim desc`;
if (filter) url += `&$filter=${filter}`;
return safeFetch(url);
}
// Key health indicators
const INDICATORS = {
MDG_0000000020: 'TB incidence (per 100k)',
MALARIA_EST_CASES: 'Malaria estimated cases',
WHOSIS_000001: 'Life expectancy at birth',
UHC_INDEX_REPORTED: 'UHC Service Coverage Index',
};
// Get Disease Outbreak News via WHO JSON API
// The old RSS feed at /feeds/entity/don/en/rss.xml returns 404.
// This JSON endpoint returns ~50 items; OData $orderby is ignored by
// the server, so we sort client-side by PublicationDate descending.
export async function getOutbreakNews() {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 15000);
const res = await fetch(DON_API, {
signal: controller.signal,
headers: { 'User-Agent': 'Crucix/1.0' },
});
clearTimeout(timer);
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
}
const data = await res.json();
const items = data?.value || [];
// Sort by PublicationDate descending (server ignores $orderby)
items.sort((a, b) => {
const da = new Date(a.PublicationDate || 0);
const db = new Date(b.PublicationDate || 0);
return db - da;
});
return items.map(item => ({
title: item.Title,
date: item.PublicationDate,
donId: item.DonId || null,
url: item.ItemDefaultUrl
? `https://www.who.int/emergencies/disease-outbreak-news${item.ItemDefaultUrl}`
: null,
summary: (item.Summary || item.Overview || '').replace(/<[^>]*>/g, '').slice(0, 300) || null,
}));
} catch (e) {
return { error: e.message };
}
}
// Briefing
export async function briefing() {
const outbreaks = await getOutbreakNews();
return {
source: 'WHO',
timestamp: new Date().toISOString(),
diseaseOutbreakNews: Array.isArray(outbreaks) ? outbreaks.slice(0, 15) : [],
outbreakError: Array.isArray(outbreaks) ? null : outbreaks.error,
monitoringCapabilities: [
'Disease Outbreak News (DONs)',
'Global health indicators (GHO)',
'Pandemic early warning signals',
'Cross-reference with GDELT health event mentions',
],
};
}
if (process.argv[1]?.endsWith('who.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

130
apis/sources/yfinance.mjs Normal file
View File

@@ -0,0 +1,130 @@
// Yahoo Finance — Live market quotes (no API key required)
// Provides real-time prices for stocks, ETFs, crypto, commodities
// Replaces the need for Alpaca or any paid market data provider
import { safeFetch } from '../utils/fetch.mjs';
const BASE = 'https://query1.finance.yahoo.com/v8/finance/chart';
// Symbols to track — covers broad market, rates, commodities, crypto, volatility
const SYMBOLS = {
// Indexes / ETFs
SPY: 'S&P 500',
QQQ: 'Nasdaq 100',
DIA: 'Dow Jones',
IWM: 'Russell 2000',
// Rates / Credit
TLT: '20Y+ Treasury',
HYG: 'High Yield Corp',
LQD: 'IG Corporate',
// Commodities
'GC=F': 'Gold',
'SI=F': 'Silver',
'CL=F': 'WTI Crude',
'BZ=F': 'Brent Crude',
'NG=F': 'Natural Gas',
// Crypto
'BTC-USD': 'Bitcoin',
'ETH-USD': 'Ethereum',
// Volatility
'^VIX': 'VIX',
};
async function fetchQuote(symbol) {
try {
const url = `${BASE}/${encodeURIComponent(symbol)}?range=5d&interval=1d&includePrePost=false`;
const data = await safeFetch(url, {
timeout: 8000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
});
const result = data?.chart?.result?.[0];
if (!result) return null;
const meta = result.meta || {};
const quotes = result.indicators?.quote?.[0] || {};
const closes = quotes.close || [];
const timestamps = result.timestamp || [];
// Get current price and previous close
const price = meta.regularMarketPrice ?? closes[closes.length - 1];
const prevClose = meta.chartPreviousClose ?? meta.previousClose ?? closes[closes.length - 2];
const change = price && prevClose ? price - prevClose : 0;
const changePct = prevClose ? (change / prevClose) * 100 : 0;
// Build 5-day history
const history = [];
for (let i = 0; i < timestamps.length; i++) {
if (closes[i] != null) {
history.push({
date: new Date(timestamps[i] * 1000).toISOString().split('T')[0],
close: Math.round(closes[i] * 100) / 100,
});
}
}
return {
symbol,
name: SYMBOLS[symbol] || meta.shortName || symbol,
price: Math.round(price * 100) / 100,
prevClose: Math.round((prevClose || 0) * 100) / 100,
change: Math.round(change * 100) / 100,
changePct: Math.round(changePct * 100) / 100,
currency: meta.currency || 'USD',
exchange: meta.exchangeName || '',
marketState: meta.marketState || 'UNKNOWN',
history,
};
} catch (e) {
return { symbol, name: SYMBOLS[symbol] || symbol, error: e.message };
}
}
export async function briefing() {
return collect();
}
export async function collect() {
const symbols = Object.keys(SYMBOLS);
const results = await Promise.allSettled(
symbols.map(s => fetchQuote(s))
);
const quotes = {};
let ok = 0;
let failed = 0;
for (const r of results) {
const q = r.status === 'fulfilled' ? r.value : null;
if (q && !q.error) {
quotes[q.symbol] = q;
ok++;
} else {
failed++;
const sym = q?.symbol || 'unknown';
quotes[sym] = q || { symbol: sym, error: 'fetch failed' };
}
}
// Categorize for easy dashboard consumption
return {
quotes,
summary: {
totalSymbols: symbols.length,
ok,
failed,
timestamp: new Date().toISOString(),
},
indexes: pickGroup(quotes, ['SPY', 'QQQ', 'DIA', 'IWM']),
rates: pickGroup(quotes, ['TLT', 'HYG', 'LQD']),
commodities: pickGroup(quotes, ['GC=F', 'SI=F', 'CL=F', 'BZ=F', 'NG=F']),
crypto: pickGroup(quotes, ['BTC-USD', 'ETH-USD']),
volatility: pickGroup(quotes, ['^VIX']),
};
}
function pickGroup(quotes, symbols) {
return symbols.map(s => quotes[s]).filter(Boolean);
}

32
apis/utils/env.mjs Normal file
View File

@@ -0,0 +1,32 @@
// Load .env file for API keys
// Searches: project root .env first, then apis/.env as fallback
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const paths = [
resolve(__dirname, '..', '..', '.env'), // project root
resolve(__dirname, '..', '.env'), // apis/.env (legacy)
];
function loadEnv(filePath) {
try {
const content = readFileSync(filePath, 'utf-8');
let loaded = 0;
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
const val = trimmed.slice(eq + 1).trim();
if (!process.env[key]) { process.env[key] = val; loaded++; }
}
return loaded;
} catch { return -1; }
}
for (const p of paths) {
if (loadEnv(p) >= 0) break;
}

42
apis/utils/fetch.mjs Normal file
View File

@@ -0,0 +1,42 @@
// Shared fetch utility with timeout, retries, and error handling
export async function safeFetch(url, opts = {}) {
const { timeout = 15000, retries = 1, headers = {} } = opts;
let lastError;
for (let i = 0; i <= retries; i++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const res = await fetch(url, {
signal: controller.signal,
headers: { 'User-Agent': 'Crucix/1.0', ...headers },
});
clearTimeout(timer);
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`HTTP ${res.status}: ${body.slice(0, 200)}`);
}
const text = await res.text();
try { return JSON.parse(text); } catch { return { rawText: text.slice(0, 500) }; }
} catch (e) {
lastError = e;
// GDELT needs 5s between requests, others are fine with shorter delays
if (i < retries) await new Promise(r => setTimeout(r, 2000 * (i + 1)));
}
}
return { error: lastError?.message || 'Unknown error', source: url };
}
export function ago(hours) {
return new Date(Date.now() - hours * 3600000).toISOString();
}
export function today() {
return new Date().toISOString().split('T')[0];
}
export function daysAgo(n) {
const d = new Date();
d.setDate(d.getDate() - n);
return d.toISOString().split('T')[0];
}