Skip to main content

arcaeon-receipt

One receipt, five buyers. A hash-chained, witnessed, timestamp-anchored record of what an AI checked, called, was approved to do, watched being typed, or graded, built for the person who has to hand it to a third party: a court, an auditor, a marketplace buyer, an accuser, a hiring center.

pip install arcaeon-receipt

That installs the library and the arcaeon-receipt command. The browser pages (web/), the worked examples (examples/), the test fixtures (tests/fixtures/) and docs/ are in the source distribution, not the wheel; the paths below are relative to it (pip download --no-binary :all: --no-deps arcaeon-receipt, then unpack). The receipt verifier page is also hosted at https://arcaeon.io/verify-receipt (the same page with site navigation added). For call_proxy --tape, add the tape writer: pip install "arcaeon-receipt[tape]".

Every receipt carries, on its face, what it proves and what it does not. That block prints before the results, and it is inside the digest: change a limit sentence and the receipt no longer verifies. A receipt that overstates itself is worse than none.

The five adapters

adapter who must produce it, and to whom proves does not prove
cite Citation Receipt a lawyer filing under an AI-disclosure order, to the court each citation was submitted to CourtListener at time T and returned status S that any case supports the point, is good law, or is quoted right; not Shepard's, not KeyCite
call Receipted Call a seller on an agent-payment marketplace (x402 and kin), to the buyer request digest, response digest, payment-header digest, in sequence, with timing that the answer was correct, or that the payment settled
call.phone_call_receipt Receipted Phone Call a call-taking/dispatch operation, to whoever needs proof a call happened opaque participant ids, start/end timestamps, derived duration, and a transcript hash occurred together the truth of anything said, the real identity behind a participant id, or consent
approval Approval Receipt a team running agents in production, to a SOC 2 / ISO 42001 auditor a named principal decided on an action with this digest before it ran, and what ran matched (or did not) that the principal was human, read it, or was authorized
approval.artifact_approval_receipt Artifact Approval anyone who needs proof a named party signed off on one artifact this approver's credential signed this artifact hash at this time, attested by the approval-text hash authority to approve, competence to judge, or that the artifact is good
authorship Authorship Receipt a writer or student accused by an AI detector, to the accuser the edit stream (typed / pasted / deleted / idle) was recorded as it happened and the final text was bound that a human made the keystrokes, or that pasted text was not generated
ballot Ballot Receipt a trainee finishing a graded simulation, to a hiring center — issued one at a time, or a whole class at once by the academy whose name is sealed inside the digest (--issuer / --cohort / --roster) the score and the ballot are unaltered since the timestamp the score is correct; the sim was not attempted before

Ballot Receipt: one trainee, or a whole class

arcaeon-receipt ballot run.json --trainee "p. lindqvist" --scenario oral_board.set1 \
    --issuer "Westbrook Regional Training Academy" --cohort WRTA-2026-OB-14

arcaeon-receipt ballot --roster roster.csv --issuer "Westbrook Regional Training Academy" \
    --cohort WRTA-2026-OB-14 --namespace wrta-ob-14 --ledger wrta-ob-14.jsonl --out-dir class/

The roster is a CSV with trainee and ballot_path columns (scenario, grader, timestamp optional, each falling back to the flag). One receipt and one exhibit per row, all into one ledger under one namespace, so the chain itself carries the order the class was issued in. One summary line comes back with the count and the ledger tip. A malformed row refuses the whole roster, naming the row number, before anything is minted: a class that comes out silently one receipt short is the failure this driver must not have.

--issuer and --cohort ride inside the body digest, not in a cosmetic header, so the academy's name cannot be changed without breaking verification. What that buys is unalterability, not authentication: the receipt proves the name has not moved since issue, and says nothing about whether the academy that appears on it is who it claims to be. A branded receipt verifies on the CLI and in web/verify-receipt.html with no change to either — extra was already one of the seven digested body fields. See examples/ballots/ for ten plain examples, one branded one, and the example roster.

MCP servers (stdio, local)

Every wrapper below speaks the same JSON-RPC 2.0 over stdio as approval_mcp.py: initialize, tools/list, tools/call; a bad call comes back isError: true and the process stays alive. These are local Python tools an MCP host launches over stdio, not deployed functions.

module wraps tool(s) reaches
approval_mcp approval.py approval_propose / approval_status / approval_executed / approval_list_pending an agent host gating a consequential action on a human decision dropped in a side-channel file
cite_mcp cite.py / cite_batch.py cite_check a lawyer's own MCP-capable agent, checking citations for existence directly instead of shelling out to the CLI
call_mcp call.py call_receipt an agent that just made a paid x402 (or kin) call, issuing its own delivery receipt without a reverse proxy in front of the traffic
authorship_mcp authorship.py / authorship_ingest.py authorship_open / authorship_event / authorship_close / authorship_ingest_export an editor plugin or CLI wrapper other than the browser recorder, driving or ingesting an authorship session directly

Citation Receipt, the first one

export COURTLISTENER_TOKEN=...        # free account at courtlistener.com
arcaeon-receipt cite brief.txt --exhibit brief.exhibit.txt

Exit 0 when every detected citation was found; exit 3 when any citation came back not_found or invalid_reporter, so a pre-filing script stops on a flag. The exhibit is plain text a clerk can read; the JSON receipt is what a stranger verifies:

arcaeon-receipt verify brief.receipt.json --ledger receipts.log.jsonl --ots

Offline demo with a planted fake citation (no token needed):

arcaeon-receipt cite tests/fixtures/brief.txt --fixture tests/fixtures/courtlistener_planted_full.json --no-anchor

Verify: one receipt, or a whole class

arcaeon-receipt verify class/lindqvist.receipt.json --ledger wrta-ob-14.jsonl --ots

arcaeon-receipt verify --batch class/                 # a directory, a glob, or a list of paths
arcaeon-receipt verify --batch examples/ballots/*.receipt.json --ledger examples/ballots/ledger.jsonl

One line per receipt, one summary line, and an exit code a script can gate on:

01_oral_board_pass_clean.receipt.json    VERIFIED       ledger=consistent
02_oral_board_fail_partial.receipt.json  VERIFIED       ledger=consistent
...
11 receipts: 11 VERIFIED, 0 BROKEN, 0 COULD NOT LOOK

Three verdicts, never two. VERIFIED and BROKEN cannot carry a file that will not open, or a receipt whose ledger nobody in this run could find. Folding either into VERIFIED overclaims; folding it into BROKEN accuses a receipt that may be perfectly good. So COULD NOT LOOK is its own count with its own exit code: 0 every receipt VERIFIED, 2 at least one BROKEN, 4 nothing BROKEN but the check COULD NOT LOOK at one or more, 1 the batch itself was refused. A determinable negative still wins — a broken body digest is BROKEN even when the ledger is also missing. (These are the printed words, the same on every Arcaeon surface. The machine values in roster-report (CSV and --json) and the archive manifest are unchanged: ok, FAIL, undetermined, and the verified / failed / undetermined counts.)

Without --ledger, each receipt is checked against the ledger named on its own face if that file sits beside it, which is the shape a bulk export already has. With --ledger, one ledger covers the batch. A receipt that claims a ledger row nobody could check comes back COULD NOT LOOK, not VERIFIED.

The cap is a refusal, not a truncation. 20 receipts per pass, the number published in docs/VENDOR_PACKAGE_SPEC.md next to the words free to verify. Past it, nothing is verified and the message names the cap and the count — a partial pass wearing a finished pass's clothes is worse than an error.

web/verify-receipt.html does the same thing offline in a browser: drop a whole class on the page (same cap, same refusal, same three verdicts, a row each and a summary). The page has no ledger file, so its verdicts are body-digest verdicts and it says so on its own face.

Roster report: the page the training manager opens

arcaeon-receipt roster-report class/ --out report.csv          # or the class ledger
arcaeon-receipt roster-report class/ledger.jsonl --json --out report.json

One row per trainee — receipt id, issuer, cohort, the trainee label the ballot carries, scenario, the score as the receipt states it, issued-at, the verification verdict, ledger status, anchor status when there is one, and the reason behind any verdict that is not ok. Header rows carry the issuer, the cohort, when the report was generated, how many receipts it counted, the three verdict counts, and the scope block's own sentences copied verbatim out of the receipts. Exit codes match verify --batch: 0 all ok, 2 a definite FAIL, 4 something could not be determined.

Verdict counts, never score statistics. No class average, no pass rate, no ranking. The scope block denies exactly one thing — the score is correct — and every one of those three asserts its opposite by arithmetic. This report says what was issued and whether it still verifies; grading is untouched by it (docs/VENDOR_PACKAGE_SPEC.md §6.1). A FAIL is a row with the same columns as every other row: never dropped, never a footnote, because finding the one document that stopped verifying is the whole reason to run it.

It pages; it does not truncate. A class of 25 is verified in two passes of 20 and 5 through the same verify_batch the CLI uses, and the header says so on its face: 2 passes of at most 20 (20, 5); every receipt was verified. The cap's refusal exists so a partial pass is never reported as a finished one, and that failure cannot happen here — every receipt is verified, and no single pass exceeds the published 20.

--json emits the same table, same header, same rows, all values strings: the same table for a machine, not a richer one that could disagree with the CSV. examples/ballots/roster_report_example.csv is a real report over the committed examples, regenerated by arcaeon-receipt roster-report examples/ballots/ and held to that command by the test suite.

Archive: the class in one file, checkable after we are gone

arcaeon-receipt archive class/ --out wrta-ob-14.zip          # or the class ledger

One portable file a coordinator hands to an auditor, a records department, or a successor:

wrta-ob-14.zip
  MANIFEST.json       archive version, generated-at, issuer, cohort, every file's sha256,
                      every receipt's verdict, the verdict counts, the scope block verbatim
  README.txt          how a human with no tools checks it: the free browser verifier, or
                      `pip install arcaeon-receipt`. No account, no contact with us, ever
  roster_report.csv   the coordinator's page, from `roster_report.py` rather than rebuilt
  cohort/             every receipt, every exhibit, the ledger files they name and the
                      witness sidecars beside them, copied byte for byte

It verifies offline. Extract it anywhere, with the network off, and run arcaeon-receipt verify --batch cohort/: every receipt checks against the ledger bundled next to it. That is the export claim — a form they own and can hand to anybody, including us going away — and the test suite proves it by doing it, unzipping into a temp directory with the socket layer shut.

cohort/ is flat because a receipt finds its ledger by looking for that filename beside itself. Sorting receipts and ledgers into separate folders would make every extracted receipt come back COULD NOT LOOK. And the whole ledger file travels, not the rows this class references: a hash chain cannot be subset, and a trimmed ledger would fail its own chain check.

Nothing is mutated on the way in. Receipts are copied as bytes, and the digests in the manifest are recomputed out of the finished archive and compared against both the manifest and the file still on disk before the command returns. A re-serialized receipt — same values, different key order — verifies fine and is still the wrong artifact to hand somebody.

A receipt that fails is packed. With its verdict on the manifest, beside the ones that pass. Exit 0 all VERIFIED, 2 at least one BROKEN inside, 4 nothing BROKEN but something COULD NOT LOOK; the archive is written at all three, because the receipt somebody is looking for is usually the one that stopped verifying. Verdict counts only, same rule as the roster report: nothing in the archive is a statistic about scores.

Deterministic. Same cohort, same stamp, identical bytes: fixed entry order, a fixed zip timestamp instead of the wall clock, and one clock reading shared by the manifest and the roster report. Two archives of one class that differ in bytes make a reader ask which is the real one. examples/class_archive_example.zip is a real archive over the committed examples, regenerated by arcaeon-receipt archive examples/ballots/ and held to that command by the test suite.

What binds a receipt to the world

  1. Body digest: sha256 over canonical JSON of the body (kind, issued time, subject, checks, scope). Any edit breaks it.
  2. Ledger row: the digest is appended to an arcaeon-ledger hash chain, so receipts have a sequence.
  3. Witness pin: the chain head is pinned outside the issuer (hosted arcaeon-witness if ARCAEON_WITNESS_URL / ARCAEON_WITNESS_KEY are set; otherwise a local file the receipt labels self-controlled, which is worth nothing to a stranger and says so).
  4. Anchor: an OpenTimestamps stamp of the body digest, carried inside the receipt as base64. Fresh stamps are calendar-pending and upgrade to a Bitcoin attestation within hours. A verifier reconstructs the stamped file from the receipt alone and runs ots verify; no contact with the issuer is required.

AB 1405 report elements

California Gov. Code §11549.83(d)(1) requires a registered AI auditor's report to the auditee to include six elements. Verbatim, from the leginfo bill text (projects/arcaeon/AB1405_REPORT_ELEMENT_MAP_2026-09-15.md has the full statute-to-field mapping this section implements):

"A registered AI auditor that conducts a covered AI audit shall provide the auditee with a report that includes, but is not limited to, all of the following information: (A) The scope and objectives of the audit. (B) The results of the audit and any documentation necessary to demonstrate the basis of those results. (C) For each deficiency identified in the audit, a description of any technical, operational, or governance measures the AI auditor determines would reasonably address the deficiency, if appropriate. (D) A description of whether the auditee has implemented and adhered to internal safety standards and protocols that are within the scope of the covered AI audit. (E) A description of the limitations of the audit, including any matters within the scope of the audit that were not assessed and any material gaps in the evidence, information, systems, or access available to the AI auditor. (F) A statement indicating that the audit was conducted in accordance with the requirements of this chapter, signed and dated by the AI auditor."

None of this is a compliance claim by the tool, and issuing a receipt with every field filled in does not mean, and is never rendered as meaning, that an audit "complied" with anything.

Element Receipt carries Auditor writes
(A) scope and objectives extra.engagement_scope (core.engagement_scope()) — the engagement-level statement Which systems, what period, against which named requirement
(B) results and documentation checks + subject (already existed) The check content itself
(C) remediation per deficiency checks[].remediation, an optional string Whether a flagged check is a deficiency at all, and what remediation "if appropriate" — the statute's own qualifier — means here. Never generated by this tool.
(D) adherence to internal standards subject.internal_standards_ref (which standard) + checks[].adherence ("adhered" / "not_adhered" / "not_assessed") The finding itself — whether the auditee actually met its own named standard
(E) audit limitations scope["does_not_prove"] (already existed, mandatory) —
(F) signed, dated compliance statement extra.attestation (core.attestation(): statement, auditor name, optional registry id, signed-at date) plus a separate detached receipt["attestation_signature"] (core.attach_attestation_signature()) over the body digest The statement itself and the actual signature — by law, the auditor's act, never this tool's

Inside the digest vs. outside. engagement_scope, checks[].remediation, subject.internal_standards_ref, checks[].adherence, and extra.attestation (statement/auditor_name/auditor_registry_id/signed_at) are all ordinary keys under subject/checks/extra — already-digested BODY_FIELDS — so they're tamper-evident the same way every other body field is: edit one after issue and body_digest_ok goes false. The one exception is the detached attestation_signature: a signature computed over the body digest cannot also be one of the inputs that produced that digest without becoming circular, so it lives as a fourth top-level attachment beside ledger/witness/anchor, outside BODY_FIELDS, by construction. verify_receipt() never uses any of these fields' presence or contents to compute its ok verdict — only that the digested ones, if present, are unaltered since issue.

All four are optional and fully backward-compatible: a receipt minted before this existed has no extra.attestation, no engagement_scope, no internal_standards_ref, no checks[].adherence/remediation, and still verifies byte-for-byte.

Library

from arcaeon_receipt import (cite, call, approval, authorship, ballot, archive,
                             roster_report, verify_receipt)

rc = cite.citation_receipt(open("brief.txt").read(), ledger_path="receipts.log.jsonl")
rc = call.call_receipt(request, response, ledger_path="calls.log.jsonl", seller="acme")
rc = call.phone_call_receipt(["caller-1", "dispatcher-2"], start_iso, end_iso, transcript_sha256,
                             ledger_path="calls.log.jsonl")

rc = approval.artifact_approval_receipt(approval.hash_artifact(pdf_bytes), "dana-approver",
                                        "I approve this release for production",
                                        ledger_path="approvals.log.jsonl")

rc = ballot.ballot_receipt(graded_ballot, trainee="j. alvarez", scenario="oral_board.set1",
                           ledger_path="ballots.log.jsonl", grader="ascenvo.vcs.oral_board",
                           issuer="Westbrook Regional Training Academy",  # optional, sealed
                           cohort="WRTA-2026-OB-14")                      # optional, sealed

rows = ballot.read_roster("roster.csv", scenario_default="oral_board.set1")   # refuses, by row
minted = ballot.mint_cohort(rows, ledger_path="wrta-ob-14.jsonl", namespace="wrta-ob-14",
                            issuer="Westbrook Regional Training Academy",
                            cohort="WRTA-2026-OB-14", out_dir="class/")

gate = approval.ApprovalGate("approvals.log.jsonl", agent="billing-agent")
p = gate.decide(gate.propose(action), principal="dana", decision="approved")
rc = gate.executed(p, action_that_ran)          # checks[0].executed_as_approved

s = authorship.AuthorshipSession("writing.log.jsonl", author="a. writer", document="essay")
s.event("type", text="The quick "); s.event("paste", text="brown fox")
rc = s.close(final_text)                          # typed vs pasted, rolling hash, final digest

report = roster_report.build_report("class/")     # verdict counts, never score statistics
manifest = archive.build_archive("class/", "wrta-ob-14.zip")   # portable, checkable offline

verify_receipt(rc, ledger_path=..., ots=True)     # typed verdicts, never raises

Spans, request bodies and response bodies are digested, never stored, unless the issuer explicitly opts in, and then the receipt says so.

Status

0.1.0, foundation. Python >= 3.9, one dependency (arcaeon-ledger). Tests: py -m pytest -q tests.

MIT.

Release files for arcaeon-receipt 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for arcaeon-receipt 0.2.0
File Size Uploaded
arcaeon_receipt-0.2.0.tar.gz 425.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for arcaeon-receipt 0.2.0
File Interpreter ABI Platform
arcaeon_receipt-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 536.9 kB

Release files / arcaeon_receipt-0.2.0.tar.gz

Download URL arcaeon_receipt-0.2.0.tar.gz
Size 425.7 kB
Tags Source
SHA-256 checksum
How to use checksums
8b95e1603fef51c2daa4bb8f5457f1efe5fe7ae776c68a5c8e59f51b9fa331da
BLAKE2b-256 checksum
How to use checksums
4aaace52c36b71dadfc614c103d035993fe1971f5a811a2c1ae9b1a94ce936cb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / arcaeon_receipt-0.2.0-py3-none-any.whl

Download URL arcaeon_receipt-0.2.0-py3-none-any.whl
Size 111.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9493fec3763df042d5e5a570c1d5a4f4330119fbedf0c16a69785c17df9f2264
BLAKE2b-256 checksum
How to use checksums
fb36de902b08e7c3a7ab77995e896e00cdcc60638b22129cfa9c0fea43f4c9d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release history Release notifications | RSS feed

0.2.1

1 release file

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page