Article 12 of the EU AI Act requires high-risk AI systems to "technically allow for the automatic recording of events (logs) over the lifetime of the system." Most teams read this and enable logging. That may satisfy the letter of the obligation today.
It will not survive the first audit.
A log you hold in your own database is mutable. You can alter, delete, or back-date any row in it — and nothing in the log itself would show that you did. A market-surveillance authority, an external auditor, or a counterparty in a dispute has no way to verify the log's integrity independently. At best, they can ask you to confirm it. That is not evidence. That is your word.
This guide shows the implementation gap and how to close it: from mutable logs to verifiable records, with working Python code.
What Articles 12 and 19 actually require.
Three obligations apply to the record itself.
- Article 12 — automatic event recording. High-risk AI systems must technically allow for the automatic recording of events over the lifetime of the system, sufficient to identify risk situations and support post-market monitoring. The word "automatic" matters: these are machine-generated records of what the system did, not manual entries after the fact.
- Article 19 — retention of automatically generated logs. Providers must retain those logs for at least six months. The retention must be provable — not merely asserted. Backups in a mutable store prove nothing about whether the records are original.
- Article 72 — post-market monitoring. When a market-surveillance authority or notified body asks for the records behind a decision, you need to produce a self-contained artefact they can verify without asking you to confirm anything.
On timing: the main high-risk obligations (Annex III stand-alone) were moved to December 2027 by the May 2026 Omnibus agreement — confirm against the Official Journal, as publication was pending at the time of writing. The content-marking obligations (Article 50, August 2026) and GPAI obligations (August 2025) are not delayed. Penalties for the high-risk record-keeping obligations reach €15M or 3% of global turnover.
For a broader reading of how the Act treats decision records, see The EU AI Act wants logs you can prove.
The implementation gap.
The gap has two parts.
Integrity. A row in a relational database, a line in a log file, or an event in an observability system can be modified after the fact. Nothing in the record prevents this. A verifiable record computes a cryptographic digest of its contents at capture time; any subsequent modification breaks the digest. This is instant and requires no authority to check.
Existence in time. Even an immutable record is only as trustworthy as the party holding it, unless its existence at a specific time is committed to public infrastructure that the holding party cannot control. An anchor commits the record's digest to a public append-only log; its timestamp becomes a mathematical fact, checkable by any independent party with no account and no trust in the record's producer.
A database row gives you neither. A hash chain gives you integrity but not operator-independent existence in time. A public anchor gives you both.
For the distinction in detail, see Tamper-evident is not tamper-proof.
Step 1 — install.
pip install "determs[anchor]"
The base package captures and verifies records locally. The
[anchor] extra adds OpenTimestamps for public anchoring.
Nothing else is required; no account, no API key.
Step 2 — wrap your LLM client.
Drop the wrapper around your existing Anthropic or OpenAI client. Every call then produces a structured record automatically — no change to the rest of your code.
import anthropic
from determs.anthropic import wrap as wrap_anthropic
from determs.storage import FileStorage
client = wrap_anthropic(
anthropic.Anthropic(),
agent_id="credit-risk-model", # stable identifier for this AI system
storage=FileStorage("./records"),
)
# Use exactly as before.
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=512,
messages=[{"role": "user", "content": "Review application #A-20411..."}],
)
# A record was written to ./records/<action_id>.json.
# Nothing was sent to a remote service. Everything stays local.
The record is plain JSON. It carries the agent identifier, model name,
the full input, the model response, and four SHA-256 digests computed
over RFC 8785 canonical forms: subject_digest,
record_digest, input_digest, and
output_digest. You can store it in S3, Postgres, a file
archive, or hand it to a compliance team on a USB key.
Step 3 — verify a record.
At any point — now, in six months, in a dispute — any party with the record and the CLI can verify it:
$ determs verify --record ./records/<action_id>.json
{
"verified": true,
"profile": "ai.agent.action",
"record_digest": "44f2b54990f7df71a16ff603b606225bcf2800004fa107c42950076c4b581fe4",
"checks": {
"subject_digest": true,
"record_digest": true,
"input_digest": true,
"output_digest": true
}
}
If any byte of the record has been modified since capture — a content
string, a tool argument, a timestamp — the digests diverge and
verify exits non-zero. No quiet corruption. No authority
required to run the check.
Step 4 — anchor records before the retention window.
Anchoring commits each record's digest to public infrastructure. Only the digest is transmitted — the decision payload (inputs, outputs, any personal data) never leaves your environment.
import json, glob
from determs.anchor import anchor_record
# Anchor any un-anchored records.
for path in glob.glob("./records/*.json"):
vdr = json.load(open(path))
if not vdr.get("anchor"):
vdr = anchor_record(vdr) # network call — submits digest to OpenTimestamps
json.dump(vdr, open(path, "w"), indent=2)
OpenTimestamps returns a pending proof immediately. The proof
is upgraded to complete — including a Bitcoin block reference —
within a few hours, once the aggregate Merkle root is committed to the
chain. You can run determs verify again after ~2 hours to
confirm the upgrade.
Anchor records as close to capture time as practical. Article 19's retention window requires that the records' existence in time be provable across the period; the anchor's block timestamp establishes that provably.
Step 5 — build an evidence pack for the compliance window.
At audit time — or proactively at each reporting period — bundle the anchored records for a system and period into a self-verifying evidence pack. Only digests and anchors enter the pack; the decision payloads stay in your environment.
import json, glob
from determs.registry import entry_from_vdr
from determs.compliance import build_evidence_pack
# Load all anchored records for this system and period.
records = [json.load(open(f)) for f in glob.glob("./records/*.json")]
entries = [entry_from_vdr(r) for r in records if r.get("anchor")]
pack = build_evidence_pack(
entries,
system="credit-risk-model",
period_from="2026-01-01T00:00:00Z",
period_to="2026-06-30T23:59:59Z", # ≥ 6 months, per Art. 19
)
json.dump(pack, open("./evidence-pack-H1-2026.json", "w"), indent=2) The pack is a self-contained JSON artefact. An auditor verifies it with:
from determs.compliance import verify_evidence_pack
import json
pack = json.load(open("./evidence-pack-H1-2026.json"))
report = verify_evidence_pack(pack)
# report["pack_digest_ok"] — membership integrity check
# report["anchors"] — per-record anchor status against public infra No account with Determs is required. No running service. The proof resolves against public infrastructure.
The mapping.
How each obligation lines up with what this implementation provides:
- Art. 12 — automatic event recording. Each model call produces a structured VDR at the moment it happens. No human step. The record is machine-generated and self-describing.
- Art. 12 — sufficient for risk identification. The VDR carries the agent identifier, model name and version, full input digest, full output digest, and a timestamp. The subject field is retained locally; its digest is in the record for independent verification.
- Art. 19 — retain logs ≥ 6 months. Anchoring commits existence-in-time to public infrastructure. The anchor's block timestamp is independently provable across the retention window, not merely asserted.
- Art. 72 — post-market monitoring. The evidence pack is a portable, self-verifying artefact. An authority can verify it independently with open-source tooling and public infrastructure — no access to your systems required.
- GDPR co-constraint. Only the digest is published to public infrastructure. The decision payload — inputs, outputs, any personal data — stays in your environment.
What this is — and what it isn't.
This is the evidence layer: verifiable, provably-retained records of what your AI system did. It is not a complete compliance programme. It will not classify your system under Annex III, write your risk assessment, or stand in for a DPIA.
It is also honest about its own scope: an anchor proves a record existed at time T and is unaltered. It does not prove the underlying decision was correct, or that the action described in the record actually occurred exactly as described. The record is accountable, not correct — meaning the producer is locked into a specific claim. That accountability is what an audit needs; correctness validation is a separate layer, yours to address with testing, human review, and your compliance programme.
For the distinction between tamper-evident and operator-independent verification, see Tamper-evident is not tamper-proof. For the conceptual background on why a log you hold yourself is not evidence, see The EU AI Act wants logs you can prove.
If you operate a high-risk AI system and are working through what Article 12 requires in practice, the format is open (CC-BY-4.0), the implementation is real, and you can verify a record yourself in the browser before writing a line of code. Read the spec or open a discussion if you have implementation questions.