Receipts
Every result carries a receipt_hash. It lets you tie a logged decision back to the pack, the outcome and the input that produced it, without storing the input itself.
What a receipt is#
A receipt is a deterministic fingerprint of the pack, the decision and the input. In the next release every engine, the built-in example pack and every pack you compile, computes it with one recipe, helixor.decision_receipt.v1:
import hashlib, json
body = json.dumps({
"schema": "helixor.decision_receipt.v1",
"pack_id": result.pack_id,
"pack_version": engine.version, # the pack's version string
"action": result.action,
"invariants_passed": result.invariants_passed,
"rule_ids": [t.rule_id for t in result.triggers], # fired rules, in trigger order
"sha256_payload": hashlib.sha256(input_text.encode("utf-8")).hexdigest(),
}, sort_keys=True, separators=(",", ":"))
receipt = "hx_proof_" + hashlib.sha256(body.encode("utf-8")).hexdigest()[:24]
input_text is the text that was evaluated: the string you passed, or the value of the single text key of a dict. The digest is of the input, never of remedy.clean_text, so two different inputs withheld by the same rule get different receipts. The same input evaluated by the same pack version always gives the same receipt. There is no timestamp or nonce inside it.
Known issue in 0.2.1
0.2.1 uses two older recipes. The built-in example pack hashes the same fields without schema and pack_version, under the key triggers, with default JSON separators. A compiled pack hashes pack_id:action:invariants_passed:clean_text, so every input a block rule replaces whole gets the same receipt. Receipts from 0.2.1 do not recompute with the recipe above, and the receipt values shown in the tutorials are 0.2.1 values. Keep your own request ID next to each receipt.
Fixed in the next release: one recipe for both engines, over the input digest, as above. Receipt values change for the built-in pack too, because schema and pack_version are now in the hashed body; they stay deterministic per input.
What it proves, and what it does not#
| A receipt lets you show | A receipt does not show |
|---|---|
| Which pack decided, what it decided and which rules fired, for a given input you still hold. | When the decision happened. Record your own timestamp. |
| That a logged decision matches an input, by recomputing it. | That the log entry was not altered. Receipts are not signed or keyed, so anyone can compute one. |
| That two decisions were made on the same input, by comparing receipts. | That no decisions were deleted. Receipts are not chained. |
Receipts are 96 bits (24 hex characters): ample for correlating decisions, not intended as a cryptographic commitment. Signed, chained receipts with inclusion proofs are Planned.
Recompute a receipt#
If you kept the input, or only its SHA-256, you can confirm a logged receipt. Log the pack version with each decision; the recipe needs it. This works for the built-in pack and for packs you compile, with the next release:
import hashlib
import json
from helixor_runtime import HelixorEngine
def decision_receipt(pack_id, pack_version, action, invariants_passed, rule_ids, payload_sha256):
"""Recompute a helixor.decision_receipt.v1 receipt."""
body = json.dumps({
"schema": "helixor.decision_receipt.v1",
"pack_id": pack_id,
"pack_version": pack_version,
"action": action,
"invariants_passed": invariants_passed,
"rule_ids": rule_ids,
"sha256_payload": payload_sha256,
}, sort_keys=True, separators=(",", ":"))
return "hx_proof_" + hashlib.sha256(body.encode("utf-8")).hexdigest()[:24]
engine = HelixorEngine()
text = "Employee note: SSN is 123-45-6789."
result = engine.evaluate(text)
# What you log for each decision: no payload, only its hash.
entry = {
"pack": result.pack_id,
"pack_version": engine.version,
"action": result.action,
"passed": result.invariants_passed,
"rules": [t.rule_id for t in result.triggers],
"payload_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
"receipt": result.receipt_hash,
}
recomputed = decision_receipt(entry["pack"], entry["pack_version"], entry["action"],
entry["passed"], entry["rules"], entry["payload_sha256"])
print(entry["receipt"], recomputed == entry["receipt"])
hx_proof_e59bd412e9b1e31e4ca16d1d True
Run against the runtime source on 2026-09-28. The same function verified receipts from a compiled pack, including two different inputs blocked by the same rule, which now get different receipts. A verification API in the runtime is Planned.
Make your audit trail tamper-evident#
Until signed receipts ship, add integrity where you store them:
- Log the receipt with context: your request ID, timestamp, pack ID and version, action and rule IDs. Never the payload or
matched_items. - Store the payload hash, not the payload, if you need to recompute receipts later.
- Chain or sign the log. Include the hash of the previous entry in each entry, or sign batches with a key held in your key-management service, and write to append-only (write-once) storage.
import hashlib, json
class ChainedAuditLog:
def __init__(self, sink):
self.sink, self.prev = sink, "0" * 64
def append(self, entry: dict) -> str:
record = dict(entry, prev=self.prev)
line = json.dumps(record, sort_keys=True)
self.prev = hashlib.sha256(line.encode()).hexdigest()
self.sink.write(line + "\n")
return self.prev
See Operational excellence for what to log and Security for key handling.