/**
* SEC verification — six banks, last ~1 year of financials.
*
* Exercises the post-CIK SEC connectors against live EDGAR for six
* U.S. banks:
*
* - secCompanyConcept for each bank × {Revenues / RevenuesNetOfInterestExpense,
* NoninterestExpense, NetIncomeLoss, Assets} — pulls the time
* series and picks rows reported in the past 365 days.
*
* - secXbrlFrames for CY2024 Revenues to show the cross-company
* snapshot — every U.S. filer that reported Revenues for FY-2024
* ends up in one frame; we narrow to the six banks of interest.
*
* - secInsiderForm4 (list mode) for one bank — last 90 days of
* insider filings to sanity-check the ownership connector.
*
* Zero Anthropic spend — agents not involved.
*
* npm run sec:verify:banks
*/
import {
secEdgarCompanies,
secCompanyConcept,
secXbrlFrames,
secInsiderForm4,
type SecConceptRow,
} from '../src/tools/retrieval/connectors/index.js';
const BANKS = ['JPM', 'BAC', 'GS', 'MS', 'WFC', 'C'] as const;
interface ConceptPick {
readonly key: string;
readonly tag: string;
readonly fallbackTags?: readonly string[];
readonly unit: string;
readonly kind: 'duration' | 'instant';
}
const CONCEPTS: readonly ConceptPick[] = [
{ key: 'revenue', tag: 'Revenues', fallbackTags: ['RevenuesNetOfInterestExpense'], unit: 'USD', kind: 'duration' },
{ key: 'opex', tag: 'NoninterestExpense', unit: 'USD', kind: 'duration' },
{ key: 'netIncome',tag: 'NetIncomeLoss', unit: 'USD', kind: 'duration' },
{ key: 'assets', tag: 'Assets', unit: 'USD', kind: 'instant' },
];
const ONE_YEAR_AGO_ISO = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
function fmtUsd(v: number): string {
if (Math.abs(v) >= 1e12) return `$${(v / 1e12).toFixed(2)}T`;
if (Math.abs(v) >= 1e9) return `$${(v / 1e9).toFixed(2)}B`;
if (Math.abs(v) >= 1e6) return `$${(v / 1e6).toFixed(1)}M`;
return `$${v.toLocaleString()}`;
}
/** From an array of concept rows, keep rows whose end date is within
* the past `daysBack` days and (for duration concepts) cover ~1Q or
* ~1FY. Return them sorted newest first. */
function recentRows(rows: readonly SecConceptRow[], kind: 'duration' | 'instant', daysBack = 400): SecConceptRow[] {
const cutoff = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const out: SecConceptRow[] = [];
for (const r of rows) {
if (!r.end || r.end < cutoff) continue;
if (kind === 'duration') {
if (!r.start || !r.fp || !r.form) continue;
if (!(r.form === '10-K' || r.form === '10-Q')) continue;
if (!(r.fp === 'FY' || r.fp === 'Q1' || r.fp === 'Q2' || r.fp === 'Q3')) continue;
} else {
if (!(r.form === '10-K' || r.form === '10-Q')) continue;
}
out.push(r);
}
/* Dedup on (end, fp, form) — the same period may appear in multiple
* filings; keep the latest filing (max accn). */
const byKey = new Map<string, SecConceptRow>();
for (const r of out) {
const k = `${r.end}|${r.fp ?? ''}|${r.form ?? ''}`;
const prev = byKey.get(k);
if (!prev || (r.accn ?? '') > (prev.accn ?? '')) byKey.set(k, r);
}
return [...byKey.values()].sort((a, b) => (b.end ?? '').localeCompare(a.end ?? ''));
}
async function resolveCik(ticker: string): Promise<string | null> {
const matches = await secEdgarCompanies(ticker);
const exact = matches.find(m => m.ticker === ticker);
return (exact ?? matches[0])?.cik ?? null;
}
async function pullBank(ticker: string): Promise<void> {
const cik = await resolveCik(ticker);
if (!cik) {
console.log(`\n──── ${ticker} ──── (no CIK)`);
return;
}
console.log(`\n──── ${ticker} CIK=${cik} ────`);
for (const c of CONCEPTS) {
const tagsToTry = [c.tag, ...(c.fallbackTags ?? [])];
let used: { tag: string; rows: readonly SecConceptRow[]; recents: SecConceptRow[] } | null = null;
for (const tag of tagsToTry) {
try {
const cc = await secCompanyConcept(cik, 'us-gaap', tag, c.unit);
const rows = cc.units[c.unit] ?? [];
const recents = recentRows(rows, c.kind);
if (recents.length > 0) {
used = { tag, rows, recents };
break;
}
} catch (err) {
if ((err as { category?: string }).category === 'no-content') continue;
throw err;
}
}
if (!used) {
console.log(` ${c.key.padEnd(10)} (no rows in last ~400d under ${tagsToTry.join('/')})`);
continue;
}
const recents = used.recents;
console.log(` ${c.key.padEnd(10)} via ${used.tag} (${c.unit}) — newest ${Math.min(5, recents.length)}:`);
for (const r of recents.slice(0, 5)) {
const periodLabel = c.kind === 'duration'
? `${r.start ?? '?'}→${r.end}`
: `as of ${r.end}`;
const tag = `${r.form ?? '?'} ${r.fp ?? ''} fy=${r.fy ?? '?'}`.padEnd(20);
console.log(` ${tag} ${periodLabel} ${fmtUsd(r.val).padStart(10)} accn ${r.accn ?? '?'}`);
}
}
}
async function frameSnapshot(): Promise<void> {
/* Banks split between two revenue tags — pull both frames and merge. */
const tags = ['Revenues', 'RevenuesNetOfInterestExpense'] as const;
const targetCiks = new Set<string>();
for (const t of BANKS) {
const cik = await resolveCik(t);
if (cik) targetCiks.add(String(parseInt(cik, 10)));
}
console.log('\n════════════ Cross-company frame: CY2024 revenue (both common bank tags) ════════════');
for (const tag of tags) {
const frame = await secXbrlFrames('us-gaap', tag, 'USD', 'CY2024');
console.log(`\nframe us-gaap/${tag}/USD/CY2024 — label="${frame.label}" total=${frame.pts}`);
const ours = frame.rows
.filter(r => targetCiks.has(String(r.cik)))
.sort((a, b) => b.val - a.val);
for (const r of ours) {
console.log(` CIK ${String(r.cik).padStart(7)} ${r.entityName.padEnd(34)} ${fmtUsd(r.val).padStart(10)} ${r.start ?? '?'}→${r.end}`);
}
if (ours.length === 0) console.log(' (none of our six banks reported under this tag)');
}
}
async function insiderSpotCheck(): Promise<void> {
console.log('\n════════════ Insider Form 4 spot check: JPM, last 90 days ════════════');
const cik = await resolveCik('JPM');
if (!cik) { console.log(' no CIK'); return; }
const dateFrom = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const f4 = await secInsiderForm4(cik, { dateFrom, maxFilings: 10 });
console.log(`filings returned: ${f4.filings.length} (window from ${dateFrom})`);
for (const f of f4.filings.slice(0, 10)) {
console.log(` ${f.accessionNumber} filed=${f.filingDate} primary=${f.primaryDocument}`);
}
}
async function main(): Promise<void> {
console.log('SEC verification — six banks, last ~1 year of financials.');
console.log(`User-Agent: MR mitchell.roy@sia-partners.com (cutoff: ${ONE_YEAR_AGO_ISO})`);
const startedAt = Date.now();
for (const t of BANKS) {
try {
await pullBank(t);
} catch (err) {
console.log(` ${t} ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
}
try {
await frameSnapshot();
} catch (err) {
console.log(`frame ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
try {
await insiderSpotCheck();
} catch (err) {
console.log(`insider ERROR: ${err instanceof Error ? err.message : String(err)}`);
}
console.log(`\nDone. Elapsed: ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
}
main().catch(err => {
console.error('Unhandled error:', err);
process.exit(1);
});