Overview / Runs / 20260729T044318-0be937
Recorded from DataHub Core

Payments · Fraud screening

Is this transaction fraudulent?

A payments company wants to block fraudulent transactions in real time, before the money moves.

Download

What is being audited

synthetic dataset

Model

fraud_screening_v1_leaky

The version being considered for release

Its id in DataHub urn:li:mlModel:(urn:li:dataPlatform:mlflow,hindsight.fraud_screening_v1_leaky,PROD)

Feature examined

disputes_on_account

built by the pipeline fraud_feature_pipeline — one column out of everything the model was shown

Decision made 10 Jan 2026, 09:00 UTC Nothing after this was knowable
Feature's data arrived 19 Jan 2026, 09:00 UTC The feature reached past the moment the decision was made.

Where the feature came from

  1. Source table disputes_after_authorisation dispute_filed_at rows appear after the decision
  2. Feature fraud_feature_pipeline disputes_on_account
  3. Model fraud_screening_v1_leaky
Table view of this path
StepTable ColumnStatus
1 disputes_after_authorisation dispute_filed_at rows appear after the decision
2 fraud_feature_pipeline disputes_on_account knowable at decision time
3 fraud_screening_v1_leaky knowable at decision time

Records re-tested 4,000 Audited 29 Jul 2026, 04:43 UTC Run 20260729T044318-0be937

In plain English

This model was cheating.

It scored perfect in testing. When we rebuilt it using only information that existed at decision time, it dropped to solid. About 48% of its apparent skill came from seeing the future.

BLOCK confirmed 48% of the skill was borrowed

The simplest way to think about it

Imagine a student who aces a practice exam. Impressive - until you notice the answer sheet was sitting on the desk. The score was real; the ability was not. Retake the exam without the answers and you learn what they actually know.

The exam
Is this transaction fraudulent?
The answer sheet
The model was given a 'disputes on this account' count - but disputes are filed days after the fraud happens, so the number was effectively the answer.
Retaking it fairly
Rebuild the data as it looked on the day, then re-test

How Hindsight knew

Nobody told Hindsight which feature was suspicious. It read DataHub — the catalog that records every table, column and pipeline in the company, and which column was built from which. Following that map backwards from the model showed one feature drawing on disputes_after_authorisation — a table whose rows only appear after the instant the transaction is authorised or declined.

Running on metadata recorded from a real DataHub instance. Connect DataHub to publish the finding back into the catalog.

What actually happened

  1. Second 0 A card is swiped. Authorise or decline, right now. Everything the model is allowed to know stops here.
  2. Day 9 The cardholder notices and files a dispute. This is how the fraud became known at all.
  3. Later Training data joins disputes to transactions with no time filter. The dispute count is now a near-perfect label in disguise.
  4. Testing Fraud detection looks solved. In production the same feature is always zero.

The problem, on a calendar

One fact arrived after the decision

Everything left of the red line was knowable when the decision had to be made. Everything right of it did not exist yet. Watch the bottom row cross the line.

transactions_at_decision_time prediction_time

the decision itself

account_history_point_in_time account_age_days

all of it existed before the decision

fraud_feature_pipeline_safe account_age_days

stops at the decision - cleared for release

fraud_feature_pipeline disputes_on_account
+9 days

still collecting data 9 days after the decision

Stays before the decision Feature under audit Reaches past the decision
Timeline of data availability relative to the prediction cutoff on 10 Jan 2026. The table below repeats every value.
Table view of the timeline
Asset Column Availability Status
transactions_at_decision_time prediction_time ends at the cutoff Source
account_history_point_in_time account_age_days ends at the cutoff Source
fraud_feature_pipeline_safe account_age_days ends at the cutoff Clear
fraud_feature_pipeline disputes_on_account extends 9 days past the cutoff Violation

How certain is this?

We read the code that built it The code that builds the feature reads from a table whose rows only appear after the decision, with nothing stopping it. That is proof, not an estimate — no retraining needed.
We rebuilt it and re-tested Performance fell when future data was removed, but not far enough to cross the configured confirmation policy. This route is supporting evidence only.

Either one is enough on its own. Hindsight reports which fired rather than blending them, so you can see exactly what settled it.

Why you can believe this

Four ways this tool could be fooling you, and what stops each

“It probably flags everything.”

A legitimate feature with a larger importance score than the leaked one is audited on every run, and must come back clear. It does.

“It can only ever say no.”

One scenario is expected to pass, and does — the same model after the repair. A gate with no “yes” is not a gate.

“The numbers were tuned to look good.”

The data generator is frozen and its seed committed. Whatever it produces is published — including a confirmation policy that was missed by 5.1 points, disclosed rather than hidden.

“An LLM decided this.”

It cannot. A language model may explain the evidence; reaching a verdict runs through deterministic code with no model in the path.

Why nobody noticed

The dispute table is joined on account ID with no time filter at all. It looks like any other account attribute.

What it costs

The model appears near-perfect in evaluation and then blocks almost nothing in production, because at authorisation time the dispute count is always zero.

What to do now

  1. Do not release this model version.
  2. Only count disputes filed before the transaction timestamp.
  3. Re-run the audit; the score you get then is the real one.

Pre-production release audit

The evidence behind that conclusion

Every claim above traces to a measurement below. Nothing here is inferred by a language model.

Release decision

BLOCK Deterministic verdict: confirmed
Release decision BLOCK verdict: confirmed
Records tested 4,000 4,000 excluded post-cutoff
Advantage lost 48.1% under point-in-time rebuild
Audit runtime 0.081s fraud-screening-post-authorisation-dispute

What the agent actually did

Backend activity

11 operations

Every DataHub call, SQL check and validation step, in order. Entries marked LIVE hit a real DataHub instance during this request; RECORDED entries replay responses captured from a real instance so this page works without Docker.

Directional column lineage

One feature reached across the decision

Temporal violation

The timeline above the fold shows this on a calendar. Here is the exact finding from the transformation itself:

The post-outcome source is referenced, but no directional available_at <= prediction_time cutoff was found. The transformation does not reference available_at.

Calibration regression fixture ยท constant across scenarios

Importance gets this exactly backwards

This controlled regression pair is deliberately held constant across audits: it proves the gate does not confuse importance with illegal availability.

Planted leaked feature post-outcome feature
0.21
Legitimate control pre-cutoff control
0.24

The longer bar is the safe one. Hindsight blocks the shorter feature and clears the longer, because importance measures whether a feature is useful — never whether the information was allowed to exist yet. A detector built on ablation flags exactly the wrong one here.

Table view of the comparison
FeatureAblation deltaVerdict
days_since_last_payment 0.356811 confirmed
prior_delinquencies 0.279669 clear_for_release

Counterfactual test

Point-in-time reconstruction

Supporting evidence
Observed training AUC1.000000
Honest reconstructed AUC0.828290

48.1%

of the apparent feature advantage disappeared

AUC 1.000000 is expected: this synthetic planted leak is total by construction. Real leakage can be subtler, and the generator is frozen rather than tuned to look realistic.

False-positive defence

Strong signal, still safe

Clear

The legitimate account_age_days feature remains predictive because it existed before the decision. It must come back clear on every run.

Observed and reconstructed AUC0.922858
Ablation delta0.279669
Advantage retained100%
Verdictclear_for_release

Inspectable execution

Evidence trace

  1. 01 Transformation Verification violation
  2. 02 Point In Time Reconstruction failed
  3. 03 Deterministic Verdict confirmed
  4. 04 Safe Control clear for release
  5. 05 Writeback awaiting human approval

Smallest safe repair

Cut off future knowledge

SQL verified
evt.available_at <= src.prediction_time

The proposal is verified independently. Hindsight never merges or applies pipeline code automatically.

Human approval boundary

Publish evidence to DataHub

Dry-run is the default. Explicit approval writes the field tag, structured verdict, linked audit Document and active incident — then re-reads every one to prove it persisted.

Write-back is disabled on the public demo. Against a live instance this form publishes four records once a person ticks the approval box: a tag on the offending column, the verdict as a structured property, an audit document, and an open incident. Each one is read back afterwards, and a write that cannot be re-read is treated as a failure.

Clone the repository and run uv run hindsight serve against your own DataHub to exercise it. The example record it produces is in examples/audit_document.md.