cd /news/ai-agents/show-hn-factlabel-catches-ai-agents-… Β· home β€Ί topics β€Ί ai-agents β€Ί article
[ARTICLE Β· art-136255] src=github.com β†— pub= topic=ai-agents verified=true sentiment=↑ positive

Show HN: Factlabel: catches AI agents lying about the data they're reporting on

Factlabel launched as a tool that audits AI-written content against source data, recomputing every figure in code and returning typed pass, review, or block verdicts at a cost of about $0.0006 per audit in under a second. The system targets three failure modes β€” invented figures, false attribution, and spin β€” and annotates agent-written pages with numbered findings, per-finding confidence, and a Trust Facts badge scoring numbers, sourcing, attribution, framing, and completeness. Factlabel runs a three-tier audit: deterministic code checks, a parallel Jev audit matrix, and routing of below-threshold answers to a human instead of guessing.

read13 min views1 publishedSep 21, 2026
Show HN: Factlabel: catches AI agents lying about the data they're reporting on
Image: Michielbdejong (auto-discovered)

A nutrition label for AI-written content. factlabel checks what an AI agent writes about data against the data itself, blocks what doesn't hold up, and shows readers exactly what was checked and why.

AI agents now write the updates people read: fundraising reports, financial summaries, dashboards, briefings. The numbers usually come from real records. The story around them often doesn't. Three failure modes show up again and again:

  • Invented figures. "More than 2,000 new donors joined." The ledger shows 9 donors in all.
  • False attribution. "Driven by an explosion of grassroots momentum." 90% of the money came from one corporate gift.
  • Spin. "Individual giving grew 40%." It fell 30.6%.

Each sentence sounds reasonable, and a reader has no way to check it. Asking another large language model to review the text doesn't solve this: it is slow, costly per word, and can be talked into agreeing.

Ask an LLM to review the text factlabel
Checks the numbers Reads them, doesn't recompute them Recomputes every figure from the source records in code
Can be argued out of a finding Yes: a confident, well-written draft can talk it round No: figures are compared by code, not persuaded
Output A paragraph of prose you now have to trust Typed pass / review / block, with a confidence per finding
Shows its work Sometimes, in more prose Every finding shows its exact steps, the question asked, and the evidence used
Cost per check Priced per output token, scales with how much it writes About $0.0006 per audit, output is free (Jev returns typed answers, not text)
Speed Seconds per call, serial Under a second, ~50 questions answered in parallel
Uncertain calls No native confidence signal Below-threshold answers are routed to a human instead of guessing

factlabel sits between the agent and the reader.

  1. Audits the draft against your source records and documents: every figure, dashboard tile, citation, cause-and-effect claim, and trend.
  2. Enforces publish gates: each draft comes out aspass ,review (a person must look), orblock .
  3. Annotates the agent's own page with numbered findings, a "why this was flagged" explanation for each, and a Trust Facts badge that summarizes the whole page.
  4. Fixes it (optional): code corrects the dashboard numbers from the source data, Claude rewrites the text from the findings, and factlabel audits the rewrite from scratch until it passes or a person needs to step in.

Behind the badge in the corner is a summary of the whole page, not just a grade: a headline, scores for numbers, sourcing, attribution, framing, and completeness, what checked out, and what was checked. Every finding is numbered to match a badge on the page.

Each finding shows the steps that produced it, marked as decided by code or by Jev, with the exact question Jev was asked and its answer probabilities, the evidence used, and the threshold that applied.

The same page after the revise loop: every number traces back to the ledger, the real source of the growth is named, and the decline is stated plainly.

The audit runs in three decoupled tiers:

[agent draft + source data]
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tier 1: deterministic (code only)            β”‚
β”‚  - recompute every metric, break every table β”‚
β”‚    down by its categories                    β”‚
β”‚  - extract and mask figures, match quotes    β”‚
β”‚  - spot causal connectors ("driven by")      β”‚
β”‚  - anomaly profile: the material facts an    β”‚
β”‚    honest draft must disclose                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tier 2: parallel Jev audit matrix            β”‚
β”‚  blinded call (the text only, no data):      β”‚
β”‚   which metric each figure and tile reports, β”‚
β”‚   which cause a sentence credits, which way  β”‚
β”‚   it says each metric moved, how loaded its  β”‚
β”‚   wording is                                 β”‚
β”‚  grounded call (text + verified facts):      β”‚
β”‚   is each claim supported, does each         β”‚
β”‚   citation hold, is each material fact       β”‚
β”‚   disclosed                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Tier 3: supervisory mediation (code)         β”‚
β”‚  - composite rules over atomic answers       β”‚
β”‚  - confident violation      -> block (red)   β”‚
β”‚  - borderline or conflicting -> a person     β”‚
β”‚    reviews (amber)                           β”‚
β”‚  - verified in code, matched with confidence β”‚
β”‚    -> silent pass (green check)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The rule that matters most: a model never decides whether a number is right. Jev only tells code which source metric a sentence is talking about. Code does the comparison, and reads qualifiers literally ("over 90%" fails when the truth is 89%).

Blinded questions. Anything about what the text says is asked without showing Jev the data, so a wrong number in the draft, or the true number in the ledger, can't bias the reading. Whether the text holds up is asked separately, with the data.

Attribution in two passes. Pass A reads the sentence with its numbers masked and asks which cause it credits and for which outcome. Pass B is computed from the records: the credited cause's real share of that outcome, judged against the change when the sentence describes a change. Every reading Jev considers is weighted by its probability, so "individual donors (62%) or something not in the data (36%)" counts as 98% sure the credit isn't backed.

Material omissions. Tier 1 profiles the data for declines, concentration in one source, and growth that came mostly from one place. Each fact is checked with two atomic questions (how the draft treats it, and whether it acknowledges it) that must agree. Growth concentration only counts when the draft reports that growth.

Jev from TypeSafe AI is a decision model, not a chat model. It doesn't write text. It answers typed questions about a shared state in one parallel pass:

  • Choice: pick one option from a list, with a probability for each.
  • Score: place something on a rubric you define.
  • Noul: the probability that a yes/no statement is true.

That fits an audit well. Every answer is one of the options you defined, so results are typed and machine-checkable. Each answer comes with a confidence, so uncertain calls can go to a person instead of blocking on their own. And it is fast and cheap enough to run on every draft: a full audit of the example page asks about 50 questions across two parallel calls, takes under a second, and costs about $0.0006.

Jev can still be wrong, and TypeSafe's published accuracy figures are its own. That's why numbers are checked by code, uncertain answers go to a person, and every judgment is shown to the reader rather than hidden behind a score.

factlabel needs Python 3.10 or later. It isn't on PyPI yet, so install it from GitHub:

pip install "factlabel[claude] @ git+https://github.com/generallymatthew/factlabel.git"

The [claude] extra is only needed for the revise loop. To work on factlabel itself:

git clone https://github.com/generallymatthew/factlabel.git
cd factlabel
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest

The tests don't call any API, so no keys are needed to run them.

You bring your own keys. They are read from environment variables and never stored in the project.

Variable Needed for Get one at
TYPESAFE_API_KEY Audits (Jev) console.typesafe.ai/keys
ANTHROPIC_API_KEY The revise loop only console.anthropic.com
ANTHROPIC_WORKSPACE_ID Only if your Anthropic key isn't scoped to a workspace Anthropic console, Workspaces

The easiest way is a .env file (already git-ignored). Copy the template and fill it in:

cp .env.example .env
set -a; source .env; set +a

Try it on the included example: a fundraising agent's Q3 update that gets the story wrong.

factlabel audit examples/fundraising/spun_draft.json --html out/label.html

factlabel annotate examples/fundraising/spun_draft.json examples/fundraising/dashboard.html \
  -o out/annotated.html

factlabel revise examples/fundraising/spun_draft.json --out out/revised.json

factlabel rebuild out/revised.json examples/fundraising/dashboard.html -o out/rebuilt.html --annotate

Open the HTML files in a browser. examples/fundraising/honest_draft.json is the same update written honestly; it passes.

No API key yet? --no-jev runs only the checks code can do on its own (numbers, dashboard tiles, quotes), and says clearly which checks were skipped.

Command What it does Exit code
factlabel audit CASE.json Audit a draft. --html writes a Trust Facts label,--json the full report 2 block,1 review,0 pass (--fail-on controls which fail)
factlabel annotate CASE.json PAGE.html -o OUT.html Annotate the agent's page in place. --correct swaps wrong numbers for source values 0
factlabel revise CASE.json Audit, fix, and re-audit, up to --max-attempts (default 3) 0 passed,1 review,2 escalated to a person
factlabel rebuild CASE.json PAGE.html -o OUT.html Write a draft's tiles and text into the page. --annotate adds Trust Facts 1 if anything couldn't be placed
factlabel eval EVALS_DIR Score the audit on paired misleading and honest drafts. --runs N measures stability 0

The exit codes make factlabel easy to drop into CI or an agent's publish step.

from factlabel import JevJudge, PublishBlocked, enforce, load_case

draft, sources, metric_specs = load_case("examples/fundraising/spun_draft.json")
try:
    report = enforce(draft, sources, metric_specs=metric_specs, judge=JevJudge())
except PublishBlocked as e:
    print(e)          # draft blocked (F, 0/100): FALSE_ATTRIBUTION, FIGURE_MISMATCH, ...
    report = e.report

To fix drafts with your own agent instead of Claude, pass any function (draft, report, sources) -> draft to revise(). feedback_for(report, sources) gives you findings written for an agent to act on:

from factlabel import JevJudge, feedback_for, load_case, revise

def my_agent(draft, report, sources):
    return my_agent_rewrite(draft, feedback_for(report, sources))

result = revise(draft, sources, my_agent, metric_specs=metric_specs, judge=JevJudge())
result.status     # "passed", "review", or "escalated"

A case file holds the draft, the source data, and the metrics to check against.

{
  "draft": {
    "narrative": "We raised $1.25M in Q3 ...",
    "dashboard": [{"label": "Total raised (Q3)", "value": 1250000, "unit": "USD"}],
    "citations": [{"claim": "...", "quote": "exact words", "source": "board_memo"}]
  },
  "sources": {
    "documents": {"board_memo": "Q3 results were dominated by ..."},
    "tables": {
      "donations": [{"donor": "Acme Corp", "type": "corporate", "quarter": "Q3", "amount": 1125000}]
    }
  },
  "metrics": {
    "q3_total_raised": {
      "op": "sum", "table": "donations", "field": "amount", "where": {"quarter": "Q3"},
      "unit": "USD", "description": "Total dollars raised in Q3"
    }
  }
}

Metric ops: sum, count, count_distinct, mean, min, max, share (a percent, with an optional within scope), pct_change (period_field, from, to), and value for numbers you compute yourself. Write clear description s: Jev uses them to match sentences to metrics.

factlabel also breaks every table down by its categories on its own (by type, by quarter, and so on), so a figure that comes from those breakdowns counts as sourced, and false attribution can be caught without you defining a metric for every slice.

annotate and rebuild work on any HTML page. These optional attributes make matching exact:

<div data-factlabel-tile="Donors">
  <span>Donors</span>
  <b data-factlabel-value>2,140</b>
</div>

<div data-factlabel-narrative>
  <p>The agent's text goes here.</p>
</div>

Without them, tiles are found by their label text and sentences by text search, including sentences that run across tags like <strong>. Readers can deep-link to the panel with #trust-facts or to a finding with #finding-5.

Finding Severity Decided by
Invented figure: no metric or breakdown value has this number block Jev matches (blinded), code checks
Wrong figure: the number disagrees with its source block code
Wrong or unsourced dashboard tile block code (Jev matches tiles, without seeing their values)
Fabricated quote: the words aren't in the cited source block code
Trend inverted or misframed: said up, went down (or flat) block Jev reads the stated direction (blinded), code knows the real one
False / unproven attribution of a cause block / warn Pass A by Jev (blinded), Pass B computed from the records
Material fact left out: a decline, a concentration, growth from one source block / warn profile in code, two Jev answers must agree
Claim contradicted / unsupported by the data block / warn Jev (grounded)
Citation contradicted / unsupported by its source block / warn Jev (grounded)
Rhetorical spin (1 to 5), weak sourcing warn Jev
Genre, ungrounded claims, no caveats note Jev

Supervisory tiers: a Jev-based violation at 85% confidence or more acts on its own; between 50% and 85%, or when two atomic answers disagree, a person reviews it; below 50% it isn't flagged but stays in the report's audit trace. A check passes silently only when code made the comparison and any matching Jev did was at least 90% sure. All of Jev's questions and every threshold live in one file, src/factlabel/questions.py.

factlabel eval scores the audit on paired drafts: a misleading draft and an honest draft of the same update, over the same data. Each misleading draft lists the failure types it should trigger, so precision and recall are measured per failure type.

factlabel eval evals/ --runs 3            # 30 drafts across fundraising, SaaS usage, marketing ROI
factlabel eval evals-holdout/ --runs 3    # a retail domain written after the engine was built

Results against live Jev (jev-1.13), 3 runs each:

Before the tiered engine Now
Misleading drafts blocked 80% 100%
Honest drafts passed 87% 100%
Omission precision 50% 100%
Spin recall 78% 100%
Every other failure type (precision / recall) 100% / 100% 100% / 100%
Jev cost per full run (90 audits) $0.019 $0.034

Read these numbers with care. The engine was tuned on the main suite, so it measures fit, not generalization. The retail domain was written after tuning and run once blind: every misleading draft was blocked, but it exposed two real flaws (attribution judged against the level instead of the change, and a part of a metric read as the whole), and 2 of 3 honest drafts passed. After fixing both, it scores 100% too, so it is no longer truly held out. New example cases are the most useful contribution to this project.

  • Jev is a paid, closed, hosted service. factlabel's code is open source, but its judgment step calls TypeSafe's API with your key. The example audit costs about $0.0003. If you depend on factlabel, know that you depend on TypeSafe too. Everything that decides whether a number is right is plain code in this repository.

  • Claude is optional. Only the revise loop uses it, and a rewrite costs a few cents with Claude Opus 5. Any other model or agent can be plugged in instead.

  • Nothing else is sent anywhere. The annotated pages and labels are self-contained HTML files with no external requests.

  • Jev's answers are probabilities. Treat review as "a person must look", not "probably fine". Tune the thresholds on your own drafts before relying on them.

  • Figure extraction is regex-based and English-only.

  • Trim long source documents before auditing; accuracy drops as irrelevant context grows.

  • The questions were tuned on four synthetic domains. Real agent drafts will find new failure modes; add them to the benchmark before changing the questions.

Contributions are welcome, especially new example cases and better questions. See CONTRIBUTING.md. You don't need any API keys to run the tests.

Apache-2.0. Jev and Claude are used through their own APIs under their providers' terms; no part of either is included here.

── more in #ai-agents 4 stories Β· sorted by recency
── more on @factlabel 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/show-hn-factlabel-ca…] indexed:0 read:13min 2026-09-21 Β· β€”