The problem
An agent did something at nine in the morning. At four in the afternoon money left an account, or a customer got the wrong medication dose, or a trading position moved. Somebody wants to know whether the first thing led to the second.
The way this is answered today is that an engineer opens a tracing tool and assembles a story from log lines and span identifiers, emitted by systems instrumented to different standards by different teams at different times. The engineer connects them with judgment. The judgment is usually good. It is also unfalsifiable by anybody who was not in the room.
Name the parties. The operator asserts that the chain is complete and that the intermediate steps were routine. The claimant asserts that something happened in the middle that the operator is not showing, because the operator chose which spans to include. A regulator asserts nothing and asks for the trace, which arrives as a spreadsheet the operator produced. An insurer asked to pay asserts that the causal story is speculative.
The core failure is that a log is an unordered pile of assertions with no internal test for completeness. If a step is missing, the two neighbouring steps still look fine. Nothing in the log format objects. This gets worse with time: retention policies drop old spans, systems get rebuilt, and the reconstruction happens months later from whatever survived.
The problem sharpens when the acting party is autonomous. A human decision maker leaves a trail outside the system: an email, a conversation, a memory. An agent leaves nothing outside its own emissions. If a step is missing from the emitted record, that step did not happen as far as any later investigation can tell, and there is no witness to say otherwise. Whoever controls the emission controls the entire history.
The second failure is time. Reconstructed chains routinely contain steps whose recorded times run backwards, because clocks differ between systems and because ordering was inferred from arrival rather than from occurrence. A chain where the effect precedes its cause is not evidence, but nothing in a spreadsheet flags it.
The third failure is the opposite of missing evidence. A causal chain assembled for a dispute tends to accumulate prose. Somebody writes a description of the harm into a field. That description is the operator's characterisation of the claimant's injury, written by the party with an interest, and once it is inside a signed artifact it travels with it forever and gets quoted back. An instrument that wants to be neutral has to refuse to carry that text at all.
What the receipt binds
The schema is at https://thehiveryiq.com/.well-known/schemas/causal-path-v1.json.
| Field | Meaning |
|---|---|
receipt_type | Fixed to causal.path. |
schema | Fixed to r1.0.0. |
path_id | Caller chosen identifier for this path. |
tenant_id | The tenant the path belongs to. |
origin_ref | The first receipt in the chain, with receipt_id, payload_sha256 and receipt_type. |
terminus_ref | The last receipt in the chain, with the same three fields. |
steps | The intermediates. Each carries index, receipt_id, payload_sha256, receipt_type, input_digest_sha256, output_digest_sha256 and occurred_at. |
claimed_effect | The outcome being traced to, carried as descriptor_sha256, a category and an occurred_at. |
continuity | One of unbroken, broken or empty. |
evaluated_at | The UTC instant of evaluation. |
boundary | The fixed non attestation string, a schema constant. |
The steps array can be empty. A zero step path is a direct origin to terminus claim, and it verifies when the origin's output digest equals the terminus input digest.
What is deliberately not in the signed body
The description of the claimed effect is absent. What appears is claimed_effect.descriptor_sha256, a 64 character lowercase hex digest of a descriptor this service never receives, plus a category and an instant. The party holding the descriptor can prove later which text it committed to. Nobody else, including the verifying service, learns what the harm was said to be.
That absence is enforced by NO_HARM_TEXT rather than left to good manners. The gate first requires descriptor_sha256 to be exactly 64 lowercase hexadecimal characters. It then walks every string in the signed body looking for readable prose, defined by the pattern of whitespace followed by a lowercase letter. Values that are known non prose are exempt: 64 character hex digests, RFC 3339 instants in the UTC Z form, values matching the identifier pattern of letters, digits, dot, underscore, colon, percent, slash and hyphen, and members of the fixed enum set. Anything else that reads like a sentence stops verification.
The step receipt bodies themselves are also absent. The path carries each step's payload_sha256 and nothing more of the body. The commitment is the shared typed receipt digest in src/typed/canon.js: canonical JSON with keys sorted recursively, arrays in order, no whitespace, every code unit above U+007F escaped as a lowercase \uXXXX sequence, then lowercase hex SHA-256 over the UTF-8 bytes. The Ed25519 signature covers the ASCII string
hive-receipt <receipt_id> <payload_sha256> <ts>
with hive-receipt as the domain separation label.
The chain arithmetic uses a second layer of digests that belong to the step receipts rather than to this receipt. input_digest_sha256 and output_digest_sha256 are read from the linked step bodies. The path's copies of them are checked against the linked bodies, and it is the linked bodies that drive continuity.
The gates
Verification runs in this order and stops at the first failure.
- SCHEMA. Validates the envelope against the causal path v1 schema, including the boundary constant. A failure means this is not a well formed instrument of this type.
- ISSUER_KEY_MATCH. Resolves
envelope.key_idin the trusted keyring. A failure means the key does not resolve there, or no resolver was supplied. - PAYLOAD_DIGEST. Recomputes the canonical digest of
signed_bodyagainstpayload_sha256. A failure means the body was altered after signing. - RECEIPT_ID. Requires the identifier to parse and its type segment to equal
signed_body.receipt_type. A failure means the identifier and the body disagree. - SIGNATURE. Verifies Ed25519 over the signing string against the trusted key, then a caller supplied key if available. A failure means no available key produced this signature.
- SIGNER_IDENTITY. Fails when only a caller supplied key verified, reporting consistent bytes from an untrusted signer.
- ORIGIN_LINK. Requires the supplied origin envelope to match
origin_refexactly on identifier, recomputed body digest and type. A failure means the origin named is not the origin produced. - TERMINUS_LINK. The same check against
terminus_ref. A failure means the endpoint of the chain was swapped. - ORIGIN_INTEGRITY. Runs the origin envelope through its cryptographic verifier. A failure reports the origin's own failed gate and reason.
- TERMINUS_INTEGRITY. The same for the terminus.
- STEP_LINKS. Requires
stepReceiptsto be an array of signed envelopes, and for every declared step requires a supplied envelope whose identifier, recomputed canonical payload digest and type all match the step entry exactly. Distinct failure reasons cover a step with no supplied envelope, a supplied envelope with no signed body, a body whose canonical digest cannot be recomputed at all, and a body that simply does not match. A failure means a step was declared without being produced, or the produced one is not the one committed to. - STEP_INTEGRITY. Runs every supplied intermediate receipt through its own cryptographic verifier and reports the label of the failing step along with the gate and reason it tripped. A failure means an intermediate is internally broken even though the path referenced it correctly.
- STEP_DIGEST_FIDELITY. For every step, requires the path's copies of
input_digest_sha256,output_digest_sha256andoccurred_atto equal the values on the linked receipt. A failure names the step index and the field, and means the path restated a step's values differently from the step itself. - STEP_INDEX_DENSE. Requires each step's declared
indexto equal its position in the array. A failure means the sequence skips or repeats a position, which is the shape of a chain with a step quietly removed. - PATH_CONTINUITY. Requires the origin body to carry
output_digest_sha256and the terminus body to carryinput_digest_sha256, then walks the chain requiring each prior output digest to equal the next linked step's input digest, and finally requires the last step's output digest to equal the terminus input digest. On a zero step path it requires the origin output digest to equal the terminus input digest directly. Failure messages name the index where the chain breaks. A failure means there is an unreceipted gap. - TEMPORAL_MONOTONIC. Requires the origin, every step, the terminus and the claimed effect to carry valid
occurred_atinstants and requires the sequence not to decrease. A failure names the sequence index whose recorded time precedes its cause. - EFFECT_AFTER_TERMINUS. Requires
claimed_effect.occurred_atto be at or after the terminus instant. A failure means the outcome is being traced to something that happened after it. - CONTINUITY_RECOMPUTE. Recomputes the continuity value from the chain and compares it to the stated
continuity. A failure means the caller asserted a finding the chain does not produce. - NO_HARM_TEXT. Requires the effect descriptor to be an opaque 64 character hex digest and rejects readable prose anywhere in the signed body. A failure names the location and means narrative text reached an artifact that must not carry it.
The boundary
This receipt attests that a named sequence of receipts forms a digest continuous chain from one origin receipt to one terminus receipt with no unreceipted gap between them, that each step's declared input digest equals the prior step's declared output digest, that the recorded times do not run backwards, and that the continuity finding was recomputed by this service rather than supplied by the caller. Digest continuity is not causation. This receipt does not attest that the origin caused the terminus, that the terminus caused the claimed effect, that the claimed effect occurred at all, or that any harm was suffered by anyone, and this service never receives the description of the claimed effect. It does not attest that the chain presented is the only chain, that no parallel or intervening cause existed outside it, or that the steps chosen are the ones a court, regulator, or investigator would consider relevant. It establishes no legal liability and is not an expert opinion on causation.
Four words in the middle of that paragraph carry most of the weight: digest continuity is not causation. They are inside the signed bytes of every receipt of this type, checked as a schema constant.
An instrument named "causal path" that did not say this would be dangerous. Somebody would put it in front of a decision maker who reads "causal" and hears "caused". The receipt refuses that reading in its own text, so the misreading has to survive a document that contradicts it.
Saying so makes the receipt more useful. Without it, the operator produces a trace, the claimant argues the trace is incomplete and that some other cause intervened, the operator argues the trace speaks for itself, and both spend a year on discovery. With this receipt, the completeness question inside the presented chain is settled immediately: it is unbroken, and the arithmetic is rerunnable by the other side from the same receipts. Whether a parallel cause existed outside the chain is untouched, and the receipt says so. The argument is now about one thing rather than two, and it is about the thing that actually requires human judgment.
The same applies to the harm. The receipt cannot be quoted as agreeing that harm occurred, because it never saw the description and says it never saw it. The claimant's own account of the injury stays the claimant's.
Adversarial cases
These come from test/causal-path.test.mjs.
A broken digest at step two fails PATH_CONTINUITY. The chain is presented as unbroken with one link that does not join. The gate walks the linked receipt bodies rather than the path's own restated digests, and reports the index where the break occurs.
An effect before its cause fails TEMPORAL_MONOTONIC. A chain assembled from systems with disagreeing clocks, or assembled backwards to fit a conclusion, will contain a step whose recorded time precedes its predecessor.
A forged continuity value fails CONTINUITY_RECOMPUTE. The direct attack is to write unbroken over a chain that is not. The stated value is never an input to the derivation.
A skipped index fails STEP_INDEX_DENSE. This is the removal attack. Delete an inconvenient step and the array positions no longer match the declared indices. Renumbering to hide the gap does not help, since PATH_CONTINUITY still finds the digest discontinuity the removed step used to bridge.
A terminus reference mismatch fails TERMINUS_LINK. Swapping in a different endpoint after the fact changes the recomputed digest. Readable prose in a schema permitted identifier fails NO_HARM_TEXT, which schema validation would have accepted.
An absent intermediate envelope fails STEP_LINKS. Declaring a step and not producing it is the cheapest forgery available. A body mutation after signing fails the same gate, because the recomputed canonical digest stops matching the path's committed value.
A body mutation with a matching path reference fails STEP_INTEGRITY. This is the more careful version of the same attack. The forger edits the step body and also updates the path's payload_sha256 so STEP_LINKS is satisfied. STEP_INTEGRITY then runs the step's own cryptographic verifier, which finds a body that no longer matches the step's own signature.
A declared step output digest differing from the receipt fails STEP_DIGEST_FIDELITY. Here the forger leaves the step receipts alone and edits only the path's restated digests to make the chain appear to join. The gate compares the path's copies against the linked bodies field by field.
Internally continuous fabricated steps without receipts fail STEP_LINKS. This is the most instructive case. Somebody invents a set of steps whose input and output digests chain together perfectly and writes them into the path. The arithmetic is flawless. There are no receipts behind them, so the gate that requires a supplied signed envelope per step rejects the path before continuity is ever computed. Internal consistency is not evidence here. Signed intermediates are.
Two positive cases confirm the shape: a valid three step chain verifies, and a valid zero step path verifies.
Performance
From hive-verifier-api/benchmarks/results-latest.json, measured at 2026-08-09T22:28:01.099Z at commit e167719f7d815809c6f55c5dde4495edf1c5615f over 200 iterations:
| Measure | Value |
|---|---|
| Mint p50 | 8.404 ms |
| Mint p95 | 9.774 ms |
| Verify p50 | 8.174 ms |
| Verify p95 | 11.387 ms |
| Artifact size | 2447 bytes |
Local harness measurement on one machine and one Node version. The run recorded Node v20.20.1 on linux/x64 with two Intel Xeon cores at 2.90 GHz. It is a reproducible measurement of this code, not a production latency guarantee. Verify cost grows with chain length, since STEP_INTEGRITY runs a full cryptographic verification per intermediate.
How to run it
- Host:
https://thehiveryiq.com/v1 - Verify route:
/verify/causal-path, no credentials required - Mint route:
/mint/causal/path, requiresAuthorization: Bearer $HIVE_TYPED_MINT_TOKEN
curl -sS -X POST https://thehiveryiq.com/v1/verify/causal-path -H 'content-type: application/json' -d '{"receipt": <a receipt of this type>}'
Mint fails closed with 401 when the token is missing or wrong.
Where it sits in the canon
Family: provenance. Category: typed receipt contract.
It composes over other typed receipts rather than over one specific type. The origin, the steps and the terminus are each signed receipts with their own verifiers, and this instrument runs those verifiers rather than trusting the references. Anything that carries an input digest and an output digest in its signed body can be a step.
It does not replace those step receipts. It says nothing about what any of them mean. It does not replace an investigation, an expert report, or a finding of liability, and the boundary rules all three out in the signed bytes.
Status
production_deployed.