import { readFileSync, writeFileSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const WATCHLIST = JSON.parse(
readFileSync(join(__dirname, "watchlist.json"), "utf-8")
);
const STATE_PATH = join(__dirname, "state.json");
const API_BASE = "https://api.secapi.ai";
const API_KEY = process.env.SECAPI_API_KEY;
function loadState() {
try {
return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
} catch {
return { last_checked: null, seen_accession_numbers: [] };
}
}
function saveState(state) {
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}
async function fetchRecentFilings(ticker) {
const url = new URL(`${API_BASE}/v1/filings`);
url.searchParams.set("ticker", ticker);
url.searchParams.set("form_type", WATCHLIST.form_types.join(","));
url.searchParams.set("limit", "10");
url.searchParams.set("sort", "filed_at:desc");
const res = await fetch(url.toString(), {
headers: { "x-api-key": API_KEY },
});
if (!res.ok) {
throw new Error(`API error ${res.status}: ${await res.text()}`);
}
return res.json();
}
async function sendSlackAlert(filing) {
const blocks = [
{
type: "header",
text: {
type: "plain_text",
text: `New ${filing.form} Filing: ${filing.ticker}`,
},
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Company:*\n${filing.company_name}` },
{ type: "mrkdwn", text: `*Form:*\n${filing.form}` },
{ type: "mrkdwn", text: `*Filed:*\n${filing.filed_at}` },
{
type: "mrkdwn",
text: `*Accession:*\n${filing.accession_number}`,
},
],
},
{
type: "section",
text: {
type: "mrkdwn",
text: filing.description
? `*Description:* ${filing.description}`
: "_No description available_",
},
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "View Filing" },
url: `https://www.sec.gov/Archives/edgar/data/${filing.cik}/${filing.accession_number.replace(/-/g, "")}`,
},
],
},
];
await fetch(WATCHLIST.slack_webhook_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ blocks }),
});
}
export async function run() {
const state = loadState();
const seen = new Set(state.seen_accession_numbers);
const newFilings = [];
console.log(
`Checking ${WATCHLIST.tickers.length} tickers for new ${WATCHLIST.form_types.join("/")} filings...`
);
for (const ticker of WATCHLIST.tickers) {
try {
const response = await fetchRecentFilings(ticker);
const filings = response.data || [];
for (const filing of filings) {
if (!seen.has(filing.accession_number)) {
newFilings.push(filing);
seen.add(filing.accession_number);
}
}
} catch (err) {
console.error(`Error fetching filings for ${ticker}:`, err.message);
}
}
console.log(`Found ${newFilings.length} new filing(s).`);
for (const filing of newFilings) {
console.log(
` -> ${filing.ticker}: ${filing.form} (${filing.accession_number})`
);
await sendSlackAlert(filing);
}
// Update state
state.last_checked = new Date().toISOString();
state.seen_accession_numbers = Array.from(seen).slice(-500); // Keep last 500
saveState(state);
return {
checked: WATCHLIST.tickers.length,
new_filings: newFilings.length,
last_checked: state.last_checked,
};
}
// Run directly
run().then(console.log).catch(console.error);