Navigate
HomeStart here
MusingsResearch & long-form
BuildingProjects & learnings
WorkProfessional practice
RunningTraining & races
AboutValues & identity
Life & PlacesCulture, food, travel, cities
Notes & ArchiveJournals, essays, portfolio
← Back to Building
PATTERNApr 2026

Tag It, Badge It, Block the Export

Six standalone data explorers shipped with embedded reference data that could be exported straight into client deliverables. The fix was a three-part system: tag every data point with its source at ingest, show provenance badges in the UI, and block exports until the data is verified.

data-qualitystandalone-htmlexport-safetyprovenanceai-tools

6
explorers hardened

BLS, Census, FRED, EDGAR, BTS, CFPB

4
data modes

live, imported, prebuilt, unverified

0
unverified exports possible

guard blocks all export paths

~50
lines of JS per tool

total cost of the safety net

Embedded data is invisible until it shows up in a deliverable

The Failure

No signal

Silent Fallback

When the live API failed, the tool fell back to embedded data without telling the user. The numbers looked real. The export button worked. Nothing distinguished demo data from verified data.

Wrong priority

Embedded-First Fetch

The original code checked embedded data first and only hit the API if the series was missing. This meant even users with valid API keys were silently using stale numbers.

No friction

No Export Gate

Excel export, CSV copy, and JSON copy all worked identically regardless of data source. A consultant could export AI-estimated numbers into a client workbook without a single warning.

Three layers, each one catching what the others miss

The Fix: Tag, Badge, Guard

dataMode property

Tag at Ingest

Every data object gets a dataMode the moment it enters the system: embedded-unverified, live-direct, user-imported, or prebuilt-verified. The tag travels with the data everywhere.

Colored pill

Badge in the UI

Green for live API data, blue for user imports, amber for unverified embedded fallback. The user never has to guess where a number came from.

Disabled buttons

Guard the Export

If any selected series uses unverified embedded data, all export buttons are disabled and a warning banner explains why. The only way out is to fetch live data or import your own.

1

The data explorers started as a good idea. Build self-contained HTML tools that consultants can open in any browser, explore government data sources, and export what they need to Excel. No login, no setup, no dependencies. Six tools covering BLS, Census, FRED, EDGAR, BTS, and CFPB. Each one embeds reference data so it works offline.

2

The problem was invisible. Embedded data looked identical to live data. Same charts, same tables, same export buttons. A consultant could open the BLS Explorer, see CPI numbers, click Export to Excel, and drop those numbers into a client report. The numbers were close enough to be believable. They were also unverified, potentially stale, and in some cases AI-estimated.

3

Nobody reads disclaimers. I had a text note in the footer saying the embedded data was approximate. It might as well have been invisible. The only thing that actually prevents bad data from reaching a deliverable is friction at the export boundary. If the button works, people will click it.

4

The fix has three parts, and each one matters because it catches what the others miss.

5

First, tag at ingest. Every data object gets a dataMode property the moment it enters the system. Embedded data is tagged embedded-unverified. Live API responses are tagged live-direct. User imports are tagged user-imported. The tag is not metadata sitting in a separate table. It travels with the data object everywhere it goes.

6

Second, badge in the UI. A colored pill appears next to every series or company showing its provenance. Green means live source data fetched during this session. Blue means user-imported. Amber means unverified embedded fallback. This is not a tooltip buried three clicks deep. It is a visible badge right next to the title.

7

Third, guard the export. This is the one that actually matters. If any selected series uses unverified embedded data, every export path is blocked: Excel, CSV, JSON. The buttons are disabled. A warning banner explains why. The only way to unlock exports is to fetch live data from the API or import your own verified file.

8

The fetch order also changed. The original code checked embedded data first and only hit the API if the series was missing. Now it tries the live API first and falls back to embedded only on failure, with a visible warning. This means users with valid API keys always get live data instead of silently using stale numbers.

9

The whole system costs about 50 lines of JavaScript per tool. Three CSS classes for the badge colors. Three helper functions: getDataModeMeta, isDataUnverified, getExportGuardMessage. One check at the top of each export handler. That is the entire cost of preventing unverified data from reaching a client deliverable.

10

This matters more for AI-generated tools than hand-built ones. When an agent builds a standalone HTML tool, the embedded data is often approximate: rounded from training data, interpolated from partial sources, or estimated from nearby values. The agent does not flag this because it does not distinguish between data it verified and data it generated. The provenance system forces the distinction at the code level so the UI can enforce it at the user level.

// The entire safety net in three functions

function getDataModeMeta(series) {
  if (series.dataMode === 'live-direct')
    return { label: 'Live Source Data', className: 'mode-live' };
  if (series.dataMode === 'user-imported')
    return { label: 'User Imported', className: 'mode-imported' };
  return { label: 'Unverified Fallback', className: 'mode-unverified' };
}

function isUnverified(items) {
  return items.some(s => s.dataMode === 'embedded-unverified');
}

function getExportGuardMessage(items) {
  if (isUnverified(items))
    return 'Export disabled: unverified embedded data.';
  return '';
}

// In every export handler:
var guard = getExportGuardMessage(selectedSeries);
if (guard) { setStatus(guard, 'error'); return; }