Case Study 01  ·  Deep Dive

Golden State Signal

California publishes each stage of its own buying cycle in a different place, and never joins them up. This is what it took to join them, and to make the result answer questions.

One record, as the state ships it Source: eSCPRS export
PO_NUM 4300‑XXXX‑0117  |  DEPT TRANSPORTATION  |  DATE 09/15/2026
DESC NETWORK STORAGE EXPANSION AND ENTERPRISE SUPPORT PER SOW ATTACHMENT B, TERM: 7/1/26 TO 6/30/2029, DELIVERY PER SCHEDULE
Date field says2026‑09‑15
Actual term start2026‑07‑01
Actual term end2029‑06‑30
Confidence tierConfirmed

The date on the record is a delivery date. The contract term is buried in free text, in a format no schema describes. 62% of dated purchase orders look like this. Representative record; field structure and term syntax are as they appear in the source data.

202,232Purchase orders
761,326Line items
$40.76BTracked spend
160California departments
8,793Suppliers
4Data sources joined

Scaled from a nine-department, $17.2B pilot to statewide coverage, and from a single-source purchase archive to four joined sources that track a dollar from budget request through project approval to purchase order.

The problem

Open data that will not answer a question.

Any vendor or reseller selling infrastructure into a state agency wants to know three things: what does this department already own, when does it come up for renewal, and who currently sells to them. California publishes the data that answers all three. None of it is usable as shipped. Two incompatible export formats arrive under the same file extension. There is no product taxonomy. Contract terms live in free-text line descriptions. The same supplier is spelled three different ways in one file.

Solving that was version one. The harder problem turned out to be timing. A purchase order is the record of a decision that was already made, often years earlier. A system that reads only purchase orders arrives after every conversation that could have changed the outcome.

Part A

What was built.

The central idea

Four filings, one demand chain.

California publishes each stage of its own buying cycle in a different place. A budget change proposal says a department asked for money. A CDT project record says the spend got designed and approved. A purchase order says it was finally spent, typically years later, and by then the decision is long made.

Joined on the four-digit state organization code, those become a demand chain. Reading them together tells you where money is moving before it becomes a purchase order, which is the only point at which a vendor conversation can still change the outcome.

BCP “We’re asking” 8,106 observations
737 PDFs, 714 extracted
288 vendor mentions
PAL “It’s designed” 60 observations
Design stage
IPOR “It’s approved” 34 observations
Red / Yellow / Green
94 projects total
eSCPRS PO “It’s bought” 202,232 POs
761,326 line items
$40.76B
Joined on department_code (4-digit)
SourceWhat it isVolume
eSCPRS / Cal eProcure / FI$Cal Purchase orders. What was actually bought. 202,232 POs
761,326 lines
DOF Budget Change Proposals Budget requests. Money asked for, not yet spent. 737 PDFs, 714 extracted
8,106 observations
288 vendor mentions
CDT PAL / IPOR Project pipeline. Design and implementation stages with Red, Yellow, Green ratings. 94 projects
60 PAL + 34 IPOR
DGS statewide awards Contract vehicles. Who is allowed to sell to whom. 94 awards
72 vehicles

Supporting sources: 435 ETC evaluations, 118 workforce signals, 19 lifecycle records.

What it produces

Four deliverables, each with its evidence attached.

Stack

Deliberately boring choices.

Python 3.14 · pandas · openpyxlingest and normalization
SQLite (22 tables, 5 views)single file, no server
Flask (24 routes) + Jinjaserver-rendered, no SPA
Anthropic APIOpus 5 / Sonnet 5 / Haiku 4.5
 tool use · prompt caching · streaming · web_search server tool
Inline SVGcharts, no JS charting library
Cloudflare Workerspublic chat proxy, API key never client-side
GitHub Pagesstatic public site

SQLite and server-rendered HTML are deliberate. This is one analyst’s tool against a 276MB dataset with no concurrent writers. Postgres and a React front end would be resume-driven development. The constraint that would justify them, row-level isolation between paying clients, does not exist yet.

Part B

Everything above is what it does. Everything below is how it works, written for people who build these systems.

5.1  ·  Model placement

Where the model is, and where it deliberately isn’t.

90.5% of classification is deterministic. The model sees 9.5%.

Four stages run most-authoritative-first. Each one only sees what the previous stage could not resolve.

StageMethodResolvedMarginal cost
1Keyword rules on PO title66.1%zero
2UNSPSC, the State’s own product codes24.4%zero
3Keyword rules on line-item text(counted in stage 1)zero
4LLM, instructed to answer Uncategorized rather than guess9.5%billed

Stage 2 is the one worth dwelling on. The State already classifies its own line items, and nobody was reading the field. Switching one department from the summary export to the detail export dropped its uncategorized share from 20.3% to 5.0%, resolving 679 of 898 purchase orders for zero API cost. Across the corpus, 96% of the entire uncategorized problem traced back to files exported without line detail.

Uncategorized residue now sits at 15,000 purchase orders, or 7.4% of the corpus. 96.1% of purchase orders carry line-item detail.

The rule that falls out of this Before adding a model, check whether the answer is already in the data and unread. It usually is, and it is free, deterministic, and auditable.

A model pass I built, measured, and threw away

I ran an LLM enrichment over Caltrans IT Goods to turn terse purchase order titles into readable product descriptions. On inspection it looked like an improvement. Measured, it was not.

7,575enriched rows carrying only 681 distinct texts. The model was converging on boilerplate.
19%was pure hedging language.
74%of rows dropped a model number or SKU that was present in the original.
"HPE ProLiant Compute DL380 Gen12 Performance Heat Sink Kit"
became
"HPE ProLiant or Synergy server platform, option, or support line"

The enrichment was fluent and less useful than the input. Loading it would have buried the best identifying text in the dataset under a vaguer restatement. I kept the column and the round-trip, because they are the right shape for a better source, and dropped the data. The one genuinely useful thing the pass found was contract term dates, which a deterministic extractor now pulls instead.

74% of enriched rows silently dropped a SKU. Fluency is not accuracy, and “looks better on inspection” is not a measurement. A silent degradation rate that high is invisible unless you diff against the input and count.
5.2  ·  Grounding

Three mechanisms, each with evidence.

The model gets tools, never SQL

The advisor answers questions about the data through seven typed tools: spend_breakdown, vendor_presence, renewals, aging_assets, find_purchases, buyers_and_contacts, and list_departments. It never sees a connection string and never writes a query. Tool arguments are validated against the department and category names that actually exist. An unknown value returns a warning rather than being silently dropped, so a typo surfaces as a question instead of an empty result the model then explains away.

Two rules make the output usable rather than merely plausible. Every figure must come from a tool result, with no arithmetic on remembered numbers and no filling gaps from general knowledge. And the dataset’s known weaknesses are written into the system prompt: the uncategorized share, departments loaded without line detail, delivery dates masquerading as contract terms, the thin published-EOL coverage. The model qualifies its own claims instead of being confidently wrong about a hole in the source.

Corroboration as a write gate

Contract terms are buried in free text, written as TERM: 7/1/26 TO 6/30/2029 inside a line description, because 62% of dated purchase orders record a delivery date where the contract term should be.

An extracted date is only written if that date literally appears in California’s own line text. Anything else is reported and dropped. That is a deterministic check on an extraction step, and it is what lets a harvested date carry the same confidence tier as a natively filed one. It is the State’s own statement either way, just written in the wrong box.

Result on Caltrans: 968 of 968 stated terms corroborated. 289 purchase orders gained a real term worth $55.7M, of which $22.7M expires within 12 months. That is renewal signal no query could previously see.

Confidence tiers that survive contact with a customer

Three tiers, kept visually distinct so a guess never reads with the authority of a fact.

TierBasisWhat it is allowed to show
Confirmeda dated term on the purchase order itselfexpiry date and status
Publicizedvendor EOL or EOS bulletindate plus source URL
Inferredage onlya flag and discovery questions, never a date

The strategy generator extends this to cross-source claims with four labels. CORROBORATED means two independent sources agree. REQUESTED ONLY means stated intent with no transaction history, and is always a question, never a finding. TIMING means an expiry and requested money collide. COST CHECK means two filings state the same figure, or a gap between them. The system prompt forbids upgrading a label, and requires ranking plays by label strength before dollar size.

The case where this caught a real error

A reseller ran one of these accounts through a general-purpose assistant and got back “lead with Zero Trust” as the top recommendation for CDCR. Scored against actual spend, CDCR was already established on Zero Trust, at 11.1% penetration and a gap score of 0. The recommendation was backwards. It was pitching them something they had already bought.

The same analysis claimed identity procurement was “strangely weak” based on $12.8M in the Security and Identity category, while identity-adjacent spend sat at $63.8M in End User Computing alone. A category-scoping blind spot read as a market gap.

Both errors are what a fluent model does with a document that does not contain the disconfirming evidence. The fix was structural. The Envision scanner now computes established-versus-gap per theme, and the system prompt carries an explicit rule that established themes are not openings. Regenerated, the same document correctly identifies the department as established on Zero Trust, and elevates the genuine 0.9% penetration gap instead.

The Envision 2026 gap scanner

California publishes its own IT strategy with dated commitments. Ten themes are scored per department: Zero Trust, identity, SOC-as-a-Service, cyber resilience, AI readiness, cloud-smart, system health, digitization, process mining, and accessibility. The score is absence × capacity × peer-proof × priority-weight.

Capacity normalizes against the largest related budget actually observed rather than a fixed divisor, because a constant flattens the scale so far that a $23M budget scores nearly as high as a $3.3B one. Peer-proof counts how many departments have already moved on that theme.

The output is an argument made entirely from the customer’s own published commitments. A stated statewide priority with no departmental spend behind it is budget that has to move.

5.3  ·  Cost

Cost as an engineering constraint.

$2.20 → $0.33 Same research, prompt caching switched on. 85% off identical work.

One department’s workforce research cost $2.20 against 1,027,385 input tokens without cache control, and $0.33 with it. The reason is specific and worth knowing. A resumed server-tool turn replays the entire assistant block, and that block holds every page fetched so far. Without cache_control you re-pay for the whole accumulated context on every hop.

Model tiering, measured on the same task

ModelCostTool callsWhat it actually found
Opus 5$0.4714Three-way vendor overlap, the consolidation argument
Sonnet 5$0.117Two vendors, reseller fragmentation, a 2017 array with no refresh plan
Haiku 4.5$0.0614Same headline vendors, more generic framing

All three correctly reported that a vendor with zero presence had zero presence, and declined to pitch it. Grounding held across the whole range, which means the guardrails are doing the work rather than the model’s capability. What scaled with model strength was depth of investigation. Only Opus went looking for competing platforms. So: Sonnet for exploration, Opus for the opening question and anything going in front of a customer.

Web search runs about $10 per 1,000 searches.

5.4  ·  Data integrity

Four incidents that had no error message.

Two incompatible schemas ship under the same file extension

The same state report exports either one row per purchase order or one row per line item. Conflating them counts line items as purchase orders. 75,196 spreadsheet rows are really 24,171 purchase orders. Format is now detected from the row shape, not the filename.

The files are HTML tables named .xls

pandas refuses them outright, so the reader sniffs leading bytes rather than trusting the extension. And every money column is a formatted string like "$83996". A naive parse turns those into NaN, then 0.00. The ingest completes, row counts look perfect, and every financial figure is silently zero. That failure mode has no error message, which is what makes it dangerous.

Line-level money does not reconcile, so it is banned from spend math

Only 34% of purchase orders have line totals summing to the grand total within 2%. The worst case is an $89.2M purchase order against $43.7M of lines, because amendments and partial lines are not represented. grand_total, taken once per purchase order, is the only source of spend. That is enforced as a standing rule rather than left to whoever writes the next query.

Human judgment outlives the database

The purchases table is disposable. Three separate flags delete and rebuild it. Hand corrections are the one thing that cannot be regenerated from source, so they live in a separate ledger keyed on (department, category, po_number), saved before every ingest and re-applied after load but before categorization, so restored rows never reach the billed stage.

This was added after a supersede operation silently destroyed 13 of the 14 corrections that existed. Those are unrecoverable, which is the reason the ledger exists.

One operational note: the database lives in a OneDrive-synced folder. WAL mode there is a corruption risk, because the sidecar files get touched by sync mid-write, so the ingest scripts that had enabled it were reverted to rollback journaling after an integrity check and checkpoint.

5.5  ·  Prompt engineering

Rule verbosity consumes completion budget.

123 → 1,910 words Same budget, same rule, system prompt compressed from ~1,900 characters to ~700.

I added a rule to the strategy generator’s system prompt covering how to treat project-pipeline data. It ran about 1,900 characters across six nested bullets. The next run spent nearly its entire 24,000 token budget reasoning and emitted 123 words of finished document.

Compressed to roughly 700 characters, same rule and same constraints with less prose, the next run produced 1,910 complete words with no truncation. The system prompt had drifted to 71% rules by volume, and that ratio, not the raw token count, was the problem.

Two things came out of this. Truncation is now detected explicitly with stop_reason == "max_tokens" and surfaced as a loud banner on the document rather than left to be noticed by a reader, because a silently truncated deliverable is the worst failure mode in a paid product. And prompt length is budgeted like any other resource. Every sentence of instruction is a sentence of output you did not buy.

A related constraint worth knowing: past a certain max_tokens, the SDK requires streaming, because a request that might exceed ten minutes cannot use the blocking call. Raising a ceiling changed the call signature.

One API-shape gotcha: the response content array can lead with a thinking block, so content[0] is not reliably the answer. Assuming index 0 made a live chat widget report a connection failure on perfectly successful replies. Both widgets now scan for the first block of type === "text".
5.6  ·  Security boundary

The system prompt lives in the Worker.

The public chat runs through a Cloudflare Worker, and the system prompt is held there rather than in the page. A prompt sent from the browser is a prompt anyone can replace. Read the source, copy the endpoint, POST your own system prompt, and the bill is mine. Held server-side, the Worker only ever runs the assistant it was deployed with. The cost is that editing copy means a redeploy, which is the right trade.

The deployment is deliberately standalone: its own API key, its own budget, its own kill switch, and origin-locked CORS. A public marketing page with unpredictable traffic should never share a key with anything else. One mode dispatches between separate scoped prompts, so a second assistant did not need a second service.

The document assistant is confined to one document. No web, no database, no other departments. That confinement is the product argument, not a limitation. A general-purpose model reading a strategy document reasons past the evidence, and this project has a documented case of exactly that. An assistant that says “the document does not say” is worth more in front of a customer than one that guesses well.

Contact data is split out of every brief and strategy document into a separate CSV rather than embedded inline. One early brief carried 1,588 email addresses in the body of a document intended to be forwarded.
5.7  ·  Honest limits

What I would build next, and what I know is missing.

No formal eval harness.

Model behaviour is checked by spot-diffing outputs and by the corroboration gates described above. That catches silent degradation, and it is how the 74% SKU drop was found, but it is a manual process, not a regression suite. A golden set of documents with assertions on grounding and label discipline is the obvious next build, and it is the gap I would close first.

No retrieval layer.

Everything is SQL against structured data, which is correct for this problem, because the questions are aggregations rather than semantic search. But the 8,106 BCP observations are free text, and finding the right one is currently keyword matching. That is a real embedding use case that I have not built.

No CI, no automated tests.

Verification is by replaying pipelines against known outputs. This has already cost me. I unit-tested a scope-building function in isolation, never called the function that actually runs it, and shipped a parameter-binding bug straight into a paid generation run. Testing the piece you edited instead of the thing that runs it is exactly how a change looks verified and is not.

Observability is print statements and a cost meter.

Adequate for one operator. Not adequate for anyone else.

Single-user by design, for now.

No auth, no multi-tenancy, no deployment of the internal tool. That stays true until there are paying clients who need isolation from each other, at which point SQLite and the trust model both have to change. I would rather make that change against a real requirement than a hypothetical one.

Outcome

Now shipping as an independent business.

Version one was a 450-record HTML dashboard built for my own territory. This is what it became once the question changed from what do I need to what would a vendor pay for. It is not built for one territory or one manufacturer. Every state, county, and large municipality publishes procurement data with the same characteristics, and the taxonomy, the demand chain, and the document generation all generalize.

Live

A public site, a read-only demo, and a sample strategy document, each with a scope-confined chat assistant.

goldenstatesignal.com →

Scope

18,193 lines · 34 Python modules · 24 Flask routes · 22 tables, 5 views · 31 briefs generated to date

Coverage

Buyer of record on 202,231 of 202,232 POs · 12,837 contacts across 176 departments · 38 departments over $100M lifetime spend

Built on

Public data only. No OEM or vendor affiliation.

On how this was built

Architected end to end. Every scoping, data model, and design decision mine. Implementation directed using AI coding tools.