- Home
- Case studies
- Turning FCC filings into roughly $77M of tracked political ad spend
Turning FCC filings into roughly $77M of tracked political ad spend
a US political advisory practice
Challenge
Political TV ad spending is public record, and almost nobody can read it. Every station files its political ad contracts as PDFs in the FCC Public Inspection Files, in layouts that differ station by station and change without notice. The advertiser on the contract is rarely the political entity actually paying: PACs, committees and campaign arms all buy under their own names.
So a question as basic as “how much did each coalition spend in this market last week” had no answer short of a person opening filings one at a time. Our client needed that number weekly, and needed to be able to defend every dollar of it back to the document it came from.
Approach
We chose LLM-first extraction over template parsing, and that decision shaped everything else. Layout rules are cheap to write and break the first time a station changes its contract format, which these stations do without warning. A multimodal model reading the document the way a person would does not care about layout.
Three principles followed from it:
- Model per stage, one gateway. Different stages want different models, so classification, extraction and entity resolution each route to the model that suits them. All of it goes through a single gateway client, so no provider SDK ever reaches business logic and swapping a model is a configuration change.
- Confidence thresholds instead of silent trust. A low-confidence extraction or a low-confidence entity match goes to a human review queue. It is neither trusted quietly nor dropped quietly, which are the two failure modes that make an intelligence product untrustworthy.
- Every figure keeps its provenance. Each spend record carries the URL of the filing it came from, all the way through to the query API.
Solution
We built a four-stage pipeline, each stage consuming the previous stage’s persisted output.
- Acquisition. Polls the FCC Public Files API per station, deduplicates by URL and by content hash, stores the PDFs, and logs every crawl attempt for audit.
- Document processing. Classifies each filing as politically relevant, extracts contract line items verbatim (advertiser, gross amount, flight dates, spot counts) with a multimodal model, then validates the result against business rules.
- Intelligence. Resolves the raw advertiser name to a canonical political entity, then attributes the spend to the beneficiary candidate through PAC-to-candidate relationships.
- Query API. REST endpoints for weekly coalition spend, multi-week trends, per-entity breakdown and record-level drill-down.
Two pieces are worth naming. Entity resolution checks a normalized alias cache before it spends an LLM call, and every human review decision writes back into that cache, so a repeat advertiser costs nothing: resolution drops from around 3 seconds to under 100 milliseconds on a cache hit. And the internal admin console talks only to the REST API, never to the database, which makes the console a live integration test of the same API surface the client depends on.
Results
The platform ran in production against the real FCC API, not a sample set.
- Roughly $77M in political TV ad spend tracked and attributed across two coalitions.
- 1,664 documents through full classification and extraction at a 98.8% success rate, with 20 failures.
- 44 TV stations polled. 5,321 documents discovered, 4,961 correctly deduplicated on re-poll, and zero polling failures.
- The proof-of-concept acceptance criterion was 75% or better extraction accuracy. A two-week validation window came in at 80% or better, so the criterion passed.
Behind those figures sit 52,768 materialized spend records. We publish the roughly $77M as an approximation rather than a precise total on purpose, because part of that record set was still awaiting the repair described below when these numbers were read.
What made it hard
The hardest problem was one nobody had noticed: reprocessing a document published a second extraction without retiring the spend records derived from the first one. An audit found that 25,086 of 52,768 production records, 47.5%, derived from superseded extractions, and that 410 documents were being counted on two generations at once. The dollar totals were inflated and nothing in the system said so.
The tempting fix was a destructive migration. We shipped the careful one instead: atomic extraction publication with a current-generation marker, spend reconciliation on every republish, and an opt-in, idempotent, chunked repair script for the historical records. An idempotent repair can be run, stopped and re-run against live data. A destructive migration gets one attempt.
The second hard problem was consistency without transactions. The database client offered no client-side transactions, so every consistency-critical multi-step operation moved into stored procedures that take a row lock and re-check their preconditions under that lock. Entity deletion re-counts references while holding the lock, so a concurrent alias insert either survives or fails loudly. Resolution review claims the line item first, so a losing concurrent reviewer writes nothing at all rather than overwriting a decision it never saw.
Along the way we also found and fixed an attribution bug that silently reported a failed relationship query as direct spend, and a batch eligibility bug that quietly stopped working past 1,000 processed line items. Both were the same species of defect as the double-counting one: not a crash, just a wrong number with no alarm attached.
How the pipeline is put together
Acquisition
Polls the public filings API per station, deduplicates by URL and by content hash, and logs every crawl attempt
Document processing
Classifies each filing, extracts contract line items with a multimodal model, then validates against business rules
Intelligence
Resolves each raw advertiser name to a canonical entity, then attributes the spend to the beneficiary
Query API
Weekly totals, multi-week trends, per-entity breakdown and record-level drill-down
Low-confidence extractions and low-confidence entity matches route to a human review queue rather than being trusted or dropped quietly.
Next case study
90%
lower monthly automation cost, from $500 to $50
From $500 to $50 a month: rebuilding an agency automation stack
a digital product agency
Read the case study
Your process is probably on this list in some form
The reading that costs a person a day a week, the report nobody wants to compile, the questions that interrupt the same manager. Tell us which one is yours and we will tell you whether it is worth automating.