BID · Console
Baseline · Intelligence · Decision
scripts/sec-verify.ts 5,866 bytes · typescript
/**
 * Free SEC-only verification — no Anthropic, no agent layer.
 *
 * Calls the four SEC EDGAR functions directly against the live API for
 * the six major U.S. banks in the demo and prints the actual FY-2024
 * 10-K values for the revenue + operating-expense XBRL concepts.
 *
 * Use this to validate the SEC data path (User-Agent, rate limiting,
 * CIK resolution, XBRL fact filtering) without spending a dollar on
 * Anthropic. The full agent run (`npm run demo`) drives the same four
 * tools through claude-sonnet-4-5 tool use.
 *
 *   npm run sec:verify
 */

import {
  secEdgarCompanies,
  secFinancials,
  secSubmissions,
  type SecCompanyMatch,
} from '../src/tools/retrieval/connectors/sec-edgar.js';

interface FactRow {
  readonly val: number;
  readonly fy?: number;
  readonly fp?: string;
  readonly form?: string;
  readonly end?: string;
  readonly start?: string;
  readonly accn?: string;
  readonly frame?: string;
}

const BANKS: readonly string[] = [
  'JPMorgan Chase',
  'Bank of America',
  'Goldman Sachs',
  'Morgan Stanley',
  'Wells Fargo',
  'Citigroup',
];

/** Concepts banks commonly use to report top-line revenue. */
const REVENUE_CONCEPTS = [
  'Revenues',
  'RevenuesNetOfInterestExpense',
  'InterestAndDividendIncomeOperating',
];
/** Concepts banks commonly use to report total operating expenses. */
const OPEX_CONCEPTS = [
  'NoninterestExpense',
  'OperatingExpenses',
  'CostsAndExpenses',
];

const ALL_CONCEPTS = [...REVENUE_CONCEPTS, ...OPEX_CONCEPTS];

function pickFy2024Annual(rows: readonly FactRow[]): FactRow | null {
  /* Prefer an annual 10-K row whose period covers FY2024
   * (start≤2024-01-31 and end≥2024-12-01) to avoid quarterly rows. */
  const annual = rows.filter(
    r =>
      r.form === '10-K' &&
      r.fp === 'FY' &&
      (r.fy === 2024 || (r.end ?? '').startsWith('2024')) &&
      (r.end ?? '') >= '2024-12-01' &&
      (r.start ?? '') <= '2024-01-31',
  );
  if (annual.length > 0) {
    return annual.sort((a, b) => (b.accn ?? '').localeCompare(a.accn ?? ''))[0]!;
  }
  /* Fall back to any 10-K fy=2024 row. */
  const any = rows.filter(r => r.form === '10-K' && r.fy === 2024);
  return any.length > 0 ? any.sort((a, b) => (b.end ?? '').localeCompare(a.end ?? ''))[0]! : null;
}

function fmtUsd(v: number): string {
  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()}`;
}

async function processBank(searchTerm: string): Promise<void> {
  console.log(`\n──── ${searchTerm} ────`);
  const matches = await secEdgarCompanies(searchTerm);
  if (matches.length === 0) {
    console.log(`  (no CIK match)`);
    return;
  }
  const m = pickBest(matches, searchTerm);
  console.log(`  CIK:    ${m.cik}`);
  console.log(`  Ticker: ${m.ticker}`);
  console.log(`  Name:   ${m.name}`);

  const subs = await secSubmissions(m.cik, '10-K');
  const latestAnnual = subs.filings.find(
    f => f.reportDate.startsWith('2024') || f.filingDate.startsWith('2025'),
  );
  if (latestAnnual) {
    console.log(`  Latest 10-K: ${latestAnnual.accessionNumber} filed=${latestAnnual.filingDate} reportDate=${latestAnnual.reportDate}`);
  }

  const facts = await secFinancials(m.cik, ALL_CONCEPTS.join(','));
  const gaap = ((facts.facts ?? {})['us-gaap'] ?? {}) as Record<
    string,
    { units?: Record<string, FactRow[]> } | undefined
  >;

  console.log(`  Revenue candidates:`);
  for (const concept of REVENUE_CONCEPTS) {
    const usd = gaap[concept]?.units?.USD ?? [];
    const hit = pickFy2024Annual(usd);
    if (hit) {
      console.log(`    ${concept.padEnd(40)} ${fmtUsd(hit.val).padStart(10)}  (period ${hit.start ?? '?'}→${hit.end ?? '?'}, accn ${hit.accn ?? '?'})`);
    } else if (usd.length > 0) {
      console.log(`    ${concept.padEnd(40)} (no FY2024 annual row; ${usd.length} other rows)`);
    } else {
      console.log(`    ${concept.padEnd(40)} (not reported)`);
    }
  }
  console.log(`  Operating-expense candidates:`);
  for (const concept of OPEX_CONCEPTS) {
    const usd = gaap[concept]?.units?.USD ?? [];
    const hit = pickFy2024Annual(usd);
    if (hit) {
      console.log(`    ${concept.padEnd(40)} ${fmtUsd(hit.val).padStart(10)}  (period ${hit.start ?? '?'}→${hit.end ?? '?'}, accn ${hit.accn ?? '?'})`);
    } else if (usd.length > 0) {
      console.log(`    ${concept.padEnd(40)} (no FY2024 annual row; ${usd.length} other rows)`);
    } else {
      console.log(`    ${concept.padEnd(40)} (not reported)`);
    }
  }
}

function pickBest(matches: readonly SecCompanyMatch[], term: string): SecCompanyMatch {
  const upper = term.toUpperCase();
  const exact = matches.find(m => m.ticker === upper || m.name.toUpperCase() === upper);
  if (exact) return exact;
  const tickerByFirstWord: Record<string, string> = {
    'JPMORGAN CHASE': 'JPM',
    'BANK OF AMERICA': 'BAC',
    'GOLDMAN SACHS': 'GS',
    'MORGAN STANLEY': 'MS',
    'WELLS FARGO': 'WFC',
    'CITIGROUP': 'C',
  };
  const wantTicker = tickerByFirstWord[upper];
  if (wantTicker) {
    const hit = matches.find(m => m.ticker === wantTicker);
    if (hit) return hit;
  }
  return matches[0]!;
}

async function main(): Promise<void> {
  console.log('SEC EDGAR direct verification — six U.S. banks, FY-2024 annual (10-K).');
  console.log('User-Agent: MR mitchell.roy@sia-partners.com');
  console.log('Concepts:', ALL_CONCEPTS.join(', '));
  const startedAt = Date.now();
  for (const bank of BANKS) {
    try {
      await processBank(bank);
    } catch (err) {
      console.log(`  ERROR: ${err instanceof Error ? err.message : String(err)}`);
    }
  }
  const elapsedMs = Date.now() - startedAt;
  console.log(`\nDone. Elapsed: ${(elapsedMs / 1000).toFixed(1)}s`);
}

main().catch(err => {
  console.error('Unhandled error:', err);
  process.exit(1);
});