TAMPER
SIGNAL
← Docs home

Reference

Mounting the signal

The light is most useful where the numbers live: on the dashboard itself. Everything here re-verifies the chain in the viewer's own browser using Web Crypto, with no build step, no framework requirement, and no server state. There are four surfaces; pick by how much you want to show.

The four surfaces

SurfaceImportFor
Status lighttamper-signal/lightA small pill in the dashboard header. The flagship surface.
Badgetamper-signal/badgeA one-line verdict that expands to a per-stage table.
Data tabtamper-signal/tableThe verified table itself, re-hashed in the browser.
Consoletamper-signal/consoleThe chain as an inspectable pipeline, link by link.

The inline status light

A dark, monospace pill that shows the traffic light and expands to a popover: a per-stage table when green, the caveat list when yellow, the broken link and totals delta when red.

<!-- serve receipts/ statically, then mount in the header -->
<script type="module">
  import { mountTamperSignal } from "/static/light.js";
  mountTamperSignal(document.querySelector("header"), "/receipts/chain.json");
</script>

With a bundler it is the same call from the package:

import { mountTamperSignal } from "tamper-signal/light";

Options

OptionEffect
watchRe-verify every N milliseconds and pulse the pill on a transition.
warnDriftTreat control-total movement as a yellow caveat, matching --warn-drift.
receiptsHrefWhere the popover's "view receipts" link points.
surface: "dark"Invert the pill for a dark host. Default is "light". (invert: true is a shortcut.)
Leave the pill alone

The pill is intentionally dark and monospace so it reads as an instrument, not a host control. Place it at the right end of your header, after your own controls, and do not restyle it to match the page. On a dark host, pass { surface: "dark" } so it inverts cleanly.

React and web component

// React
import { TamperSignal } from "tamper-signal/react";
<TamperSignal chain="/receipts/chain.json" />

// Plain HTML, via tamper-signal/element
<tamper-signal chain="/receipts/chain.json"></tamper-signal>

The badge

A compact verified line that expands into a per-stage table. Good for the top of a report rather than a live header.

import { renderReceiptBadge } from "tamper-signal/badge";
renderReceiptBadge(el, "/receipts/chain.json", pubKeyHex);

Collapsed it reads, in green, ✓ Verified · TikTok export, May 2026 · 48,212 rows · 2 transforms · chain intact, or in red, ✗ Chain broken at transform_aggregate.

The data tab

Renders the attested table from table.json, re-hashes it in the viewer's browser, and compares against the final receipt. A verified data tab means the rows on screen are byte for byte the attested data, not a fresh query that might disagree.

# 1. export the canonical table (Python or Node)
receipts export --chain receipts/chain.json --data dashboard.xlsx

# 2. mount it
<script type="module">
  import { mountReceiptTable } from "/static/table.js";
  mountReceiptTable(document.querySelector("#data-tab"), "/receipts/chain.json");
</script>

The console

The chain as an inspectable pipeline: each link carries the hash it proved, a break severs the link with a break card pinned in place, coverage gaps show up as ghost nodes, and the event log mirrors receipts verify line by line. Every attach helper serves it automatically at /tamper-signal/console.

import { mountReceiptConsole } from "tamper-signal/console";
mountReceiptConsole(el, "/receipts/chain.json");

One-call framework helpers

Each attach helper serves your receipts/ directory and the verifier assets, and returns a ready-to-drop snippet so you do not wire paths by hand.

# Flask
from tamper_signal.flask_ext import attach
signal = attach(app, receipts_dir="receipts/")   # {{ signal.snippet | safe }}

# FastAPI
from tamper_signal.fastapi_ext import attach
signal = attach(app, receipts_dir="receipts/")
// Express
import { tamperSignal } from "tamper-signal/express";
const signal = tamperSignal(app, { receiptsDir: "receipts/" });
// render signal.snippet once in your layout
HostHow
Flasktamper_signal.flask_ext.attach(app, ...)
FastAPItamper_signal.fastapi_ext.attach(app, ...)
ExpresstamperSignal(app, { receiptsDir }) from tamper-signal/express
Next.jsCopy receipts/ into public/receipts/ during the pipeline run, then <TamperSignal chain="/receipts/chain.json" />
Streamlitfrom tamper_signal.streamlit_ext import signal, verified_dataframe (server-side verified, a weaker check)

Flag a broken metric in place

The signal can reach into the page and outline the exact numbers that no longer descend from the source. Add data-receipt-column to any element that displays a metric, naming the column it comes from. When the chain breaks at a link where that column moved, the signal outlines the element and tags it tamper signal: unverified value.

<div class="kpi" data-receipt-column="spend_(usd)">
  $98,441.02
</div>

Column names are the normalized keys from the receipts' control totals: lowercased, spaces turned to underscores. Read them from numeric_sums and null_counts in the receipt files.

A real limit worth knowing

Only plain decimals are summed into numeric_sums. A thousands-grouped string like "289,084" or "1 198 372" is not coerced to a number, so that column never reaches the totals, and data-receipt-column on it cannot flag a change. The fix is upstream: a signed normalize stage that turns the column into plain decimals. The ingest command prints a warning naming such columns, and groupedNumericColumns(records) finds them in code.

Verify your wiring