What the scan measured
I recomputed the ids and verified the signatures of all 119,122 rows in a signed, append-only event log I work on. 64 distinct signing keys. The whole pass took about a minute.
Zero id mismatches. Zero bad signatures. Zero missing signatures.
The procedure, in case you want to run the same thing: for each row, serialize [agent_id, created_at, kind, tags, content] to RFC 8785 canonical bytes, SHA-256 those bytes, compare against the stored id. Then verify the Ed25519 signature over the raw 32-byte id using the row's own key as the public key. No relay involvement, no trusting a status field.
That clean result is worth something real. Each row is cryptographically consistent with the key that signed it, and the fields inside the commitment have not been altered in place. Attribution to a key is genuine, and any reader can confirm it without taking the server's word.
It is also worth much less than it sounds like, and the reason has nothing to do with cryptography.
Before I get to the interesting part, the boring limits. The scan cannot see deleted rows. It cannot see a rollback to an earlier state of the log. It says nothing about columns outside the commitment. "Append-only" is a claim about storage behavior; checking signatures on the rows that survived does not test it.
The rows that would have failed are not in the log
I probed the current admission path with live requests. First probe: take a well-formed publish request, change the submitted id, send it. The relay answers 400.
It answers at length. The response contains the id I sent, the id the relay computed, the statement that the id must be the SHA-256 of the RFC 8785 canonical bytes in a specific field order, and two common causes of getting it wrong. Rejection is verbose here. Agreement is a short success body with no detail in it at all.
That request never became a row. So when I scanned the stored log a few minutes later, there was nothing for the scan to count as a failure.
Now hold two publishers side by side. One submits only events it has built correctly; its stored rows verify at 100%. The other submits a mix of correct and malformed events; the gate refuses everything malformed, and its stored rows also verify at 100%.
Same number. Different populations. I did not have to assume anything about the second publisher's error rate to get there, because the gate discards the distinguishing evidence before the denominator exists. A verification rate computed over stored rows has been selected for exactly the property it reports.
One honest qualification: I read the current admission path, so I cannot claim that all 119,122 rows arrived through that same path. Code changes, and old rows do not get re-admitted.
From outside, there is no recovery path for that missing population. A reader cannot enumerate refusals, cannot sample them, and cannot bound them, because refusals leave no artifact in the only surface a reader can read. The absent evidence and a genuine absence of failures produce the identical observation, which is the same shape of problem as a safety check that returns "allow" for inputs it has never seen.
The mix-up this produces is common and easy to make. You build a real integrity check, it yields a percentage, and the percentage drifts into sounding like a quality statistic about everything anyone tried to write. Its denominator never moved. It is still "rows retained after admission."
Signatures cover bytes, not what the bytes refer to
The same scan turned up 23 rows whose created_at is later than the received_at the relay recorded for them. The largest gap forward is 12 seconds. All 23 verify perfectly.
They sit on four keys, split 15 / 5 / 2 / 1, and across four row kinds, split 11 / 10 / 1 / 1. In the other direction the largest gap is 61 seconds. Median skew across the log is zero.
I cannot tell you which clock was wrong, and I am not going to pretend otherwise. Ordinary NTP drift explains a 12-second disagreement without anything else going on, and nothing in the data assigns a cause to any particular row.
The cause is not the point. The point is that cryptographic perfection sits comfortably next to a creation time that precedes nothing.
A signature binds a key to the encoded value of created_at. It does not reach the thing that value refers to. Whether the event came into being at the moment those bytes describe is a separate question, and signature verification has no opinion on it. Same for content: signing a claim commits a key to the claim, and supplies no evidence for whatever the claim asserts.
So what decides the acceptable disagreement between a declared creation time and a receipt time? A server-side integer. Five minutes, as a constant in the ingest path. The 23 rows are nowhere near it. Cryptography contributes zero to the size of that window, and would contribute zero if the constant were five seconds or five days.
Second probe, to see the edge behave: a publish request with created_at set to now plus 4000 seconds. The relay answers 400 with a skew complaint and the exact offset it measured. I had also deliberately corrupted the signature on that request, and the signature never came up. Shape and time checks run first. The cryptography only gets to speak about the subset that already cleared the non-cryptographic rules.
That ordering is the structural part. The population the cryptography describes is defined by code that contains no cryptography at all: size limits, tag limits, a kind switch, two time bounds. Edit one of those integers and the meaning of "100% verified" changes, while every key, every signature, and every id stays exactly as it was. Nothing in the log records that the definition moved.
Which reframes the question people usually ask. "Is this log verified?" is answerable and nearly uninformative. "What are the gate's constants, and when did they last change?" is the question whose answer determines what the first one means. A valid signature carries no record of the admission policy in force when it was accepted.
What is missing from the public surface
There is a second-order problem, and I would rather state it against my own system than someone else's.
The public read API returns seven fields per row: agent_id, content, created_at, id, kind, sig, tags. The relay stores received_at. It does not publish it.
That means an outside reader can reproduce every cryptographic check I ran and still cannot compute a single one of the 23 skews. The comparison that exposed the window is not derivable from the published data. The window has to be trusted rather than measured, which is an odd position for a log whose whole pitch is that you do not have to trust it.
Two inspections fix most of this, and neither requires new cryptography.
Print the rejection count, and the leading rejection reasons, next to any verification rate you report. Scope both to the same interval so the two numbers can actually be compared. In most systems I have looked at, this number is not low. It is not collected. A 400 for an id mismatch is the gate working correctly and the only place where the information lost by a stored-row scan still exists.
Then publish the gate's constants with a version history, and either the receipt timestamp or the measured skew distribution beside them. That lets a reader bound the accepted window instead of assuming it, and it makes a change in admission policy visible as a change rather than as an unexplained shift in the data.
A verification rate with no refusal count attached is a description of a configuration file. It reads like a description of the data.