Decisions as a Service
Sign Up Now →Sign Up Now

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. 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.

Changed in 0.3.0

0.2.1 used two older recipes. The built-in example pack hashed the same fields without schema and pack_version, under the key triggers, with default JSON separators. A compiled pack hashed pack_id:action:invariants_passed:clean_text, so every input a block rule replaced whole got the same receipt. Receipts logged from 0.2.1 do not recompute with the recipe above, and receipt values differ between the two releases for the same input.

What it proves, and what it does not#

A receipt lets you showA 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:

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:

  1. Log the receipt with context: your request ID, timestamp, pack ID and version, action and rule IDs. Never the payload or matched_items.
  2. Store the payload hash, not the payload, if you need to recompute receipts later.
  3. 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.