How AqtaCore signs attestation receipts: Ed25519, canonical JSON, and zero-trust verification
Last reviewed 17 May 2026.

A post-hoc log can be edited. A pre-execution attestation cannot. That single sentence is the entire reason AqtaCore™ exists, and it is also the line that does the most work in any conversation we have with a regulated buyer.
This post walks through the cryptography behind that claim. What an attestation receipt actually contains, why it is signed before the downstream model is called, how an auditor verifies it without ever calling an Aqta server, and the small details that decide whether the verifier in Python and the verifier in TypeScript agree byte for byte. None of this is novel cryptography, that is the point. The interesting work is the engineering discipline that keeps everything honest under load.
Why a signed receipt at all
Every AI governance product in production today writes some kind of audit log. The honest question is not whether the log exists, it is whether anyone can tamper with it after the fact. Most systems store their logs in a database the vendor controls. The vendor can edit rows. The vendor can lose rows. Even with the best intentions, a subpoena or an investigation will eventually ask the question: how do you prove this row has not been touched since the model decision?
A signed receipt collapses that whole question into one line. Either the signature verifies against the published public key, or it does not. If it verifies, every byte of the payload is exactly what the gateway saw at the moment the decision was made. If a single byte changes, the signature breaks. There is no in-between.
For a healthcare AI vendor preparing an EU AI Act Article 12 audit trail, or a finserv buyer mapping evidence to DORA Article 6, that binary property is much more useful than “we will give you read-only access to our log database”. It is also much cheaper to verify under scrutiny.
What a receipt actually contains
The full schema lives in ATTESTATION-v1, the open specification we maintain in a public GitHub repository under CC BY 4.0. Every field in the spec is required to be present in a v1.0 receipt. A typical one looks like this:
{
"version": "1.0",
"attestation_id": "att_01HZ8K7E9YJ4PM5XQX6T9V2NCB",
"issued_at": "2026-04-25T14:32:18.114Z",
"issuer": "https://app.aqta.ai",
"trace_id": "tr_01HZ8K7E8XNR4VKF8BP2JM7C2A",
"org_id": "org_01HV...redacted",
"outcome": "BLOCKED",
"policy_applied": ["budget_cap_eur_per_day", "loop_detector"],
"request_hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"model_requested": "gpt-4o",
"cost_prevented_eur": 0,
"public_key": "alWzEnrA_z9McN9z_MFfQCnH9mVgOwRZ26wrI7oix4E",
"signature": "BqK8q2j...detached Ed25519 over canonical bytes..."
}The shape is intentionally narrow. The receipt records what was decided and by which policies, plus the SHA-256 of the prompt so the decision is bound to a specific input, but the receipt does not contain the prompt itself. We cannot leak what we never store. For regulators that demand evidence the prompt was the prompt the policy ran against, we offer a Schnorr zero-knowledge proof on BN254, which is a separate post.
The signing path
Signing happens inside the gateway, on the same request that executes the policy. The order matters. We sign before any downstream model call has a chance to fail or be retried, so the receipt records the decision and not the result. A blocked request gets a receipt. A passed request gets a receipt. A request that times out three minutes later upstream still has a valid receipt for the decision the policy made at request time.
Concretely, the gateway:
- Builds the JSON payload above, minus the
signaturefield. - Serialises it to canonical bytes with
JSON.stringify-equivalent rules: keys sorted, no insignificant whitespace, no trailing newline. In Python, that isjson.dumps(payload, sort_keys=True, separators=(",", ":")). - Signs those bytes with the gateway's persistent Ed25519 private key, using RFC 8032 and a constant-time signing primitive from the
cryptographylibrary. - Encodes the 64-byte signature as base64url and attaches it as the
signaturefield. - Persists the row with a chain hash linking it to the previous receipt for the same org, then returns the receipt to the caller.
The signing key never leaves the gateway. The public key is published at app.aqta.ai/security/pubkey.txt as a base64url-encoded 32-byte value. We document key rotation as a versioned event with at least 90 days of overlap; we have not rotated yet, the current key has been live since April 2026.
The detail that almost broke us: integer coercion
Cross-language interop is where most signed-payload systems quietly fail. The signature must be over the same bytes on every implementation. If the Python issuer encodes a number one way and the TypeScript verifier reads it another way, two correct programs will disagree about whether a perfectly valid receipt verifies.
The specific case that caught us is innocent-looking:cost_prevented_eur. In Python, json.dumps(0.0) gives the string "0.0". In JavaScript, JSON.stringify(0) gives "0". A receipt that records a zero cost saved would round-trip differently across the two languages, and the signature would not verify on whichever side had the “wrong” encoding.
ATTESTATION-v1 §6 makes the rule explicit: numbers that are mathematically integers must be serialised as integers, even when the source language used a float type internally. The reference issuer applies this with one line:
cost_rounded = round(cost_prevented_eur, 6)
if cost_rounded == int(cost_rounded):
cost_rounded = int(cost_rounded)This is the kind of thing that does not show up in unit tests of either implementation in isolation. It only surfaces under interop testing, which is why the spec ships with 14 conformance test vectors that the Python and TypeScript verifiers must both agree on. Six of those 14 are valid receipts that must pass; eight are precise mutations that must fail in a documented way (wrong signature, missing field, extra unknown field, downgraded version, and so on).
Verifying a receipt in five lines, with no Aqta server
The point of the open spec is that you do not have to trust Aqta to validate evidence Aqta produced. Anyone can install the reference verifier and check a receipt against the published public key. In Python:
pip install aqta-verify-receiptfrom aqta_verify_receipt import verify_receipt
# Pin the published public key once, persist it, and reuse on every check.
TRUSTED = "alWzEnrA_z9McN9z_MFfQCnH9mVgOwRZ26wrI7oix4E"
result = verify_receipt(receipt, trusted_public_key=TRUSTED)
assert result.valid, result.reasonThat is the entire trust boundary. Five lines, one dependency (cryptography, for constant-time Ed25519 verification), no network call to Aqta. The TypeScript equivalent is the same shape, on npm as aqta-verify-receipt.
The pin-first pattern matters. fetch_published_public_key() performs a live HTTPS fetch, which is fine for first use, but calling it inside a verification loop collapses the trust model back to “trust the issuer's server right now”, which is exactly what an independently verifiable signature is supposed to avoid. Both verifier libraries warn about this explicitly in their READMEs.
Forward compatibility, on purpose
By default the verifier rejects any receipt containing a field not defined in the version of the spec it was built against. This is the correct default for a security-critical verifier: a receipt containing an unknown field could carry attacker-controlled metadata that downstream systems treat as signed evidence. Strict mode is on by default for that reason.
The spec versioning policy also makes the upgrade story explicit. Patch versions of v1.0.x are clarifications only and never change fields, so a v1.0 verifier keeps working. Minor versions like v1.1.0 may add new optional fields, in which case a v1.0-era verifier will reject v1.1 receipts under the strict allow-list, and you upgrade or set strict_fields=False to keep the cryptographic check while relaxing the structural one. Major versions like v2.0 are explicit breaking changes; you upgrade.
The audit story
For a Big Four auditor preparing an EU AI Act conformity assessment, this is materially different from log-based AI governance products. Their flow with AqtaCore looks like:
- Pull the org's receipt history from the dashboard as a JSONL export. Receipts are independent records, no Aqta service needs to be online for the auditor to start work.
- Pin the published public key once at the start of the engagement. Persist in a KMS or secret manager.
- Run the verifier across every record. Six valid + eight invalid test vectors confirm the verifier is behaving according to spec.
- Spot-check policy mappings against EU AI Act Article 12, log retention against Article 19, and post-market monitoring against Article 16.
None of those steps require a privileged channel to Aqta. The auditor controls the verification pipeline end to end. That is the entire pitch.
What is next
Three things on the immediate roadmap that this post points at but does not cover. First, per-receipt live chain: each receipt includes the SHA-256 of the previous receipt for the same org, so tampering with any historical row breaks every later one. Second, Groth16 SNARKs for the cases where the auditor must verify the policy ran on a specific prompt without ever seeing the prompt. Third, an IETF Internet-Draft submission so the spec lives in a neutral standards venue rather than only on a GitHub repo we control.
If you want to play with the format, the spec, vectors, and both verifier libraries are public.
Resources
- ATTESTATION-v1, the open receipt format specification (CC BY 4.0). Source
- aqta-verify-receipt on PyPI: pip install aqta-verify-receipt. Source
- aqta-verify-receipt on npm: npm install aqta-verify-receipt. Source
- 14 conformance test vectors (6 valid + 8 invalid). Source
- RFC 8032: Edwards-curve Digital Signature Algorithm (EdDSA). Source
- AqtaCore public Ed25519 verification key. Source
AqtaCore is in early access, applications open. Apply if you operate AI in a regulated industry.

Anya Chueayen
Founder of Aqta. Before this, I worked on integrity at social media platforms, the unglamorous side of AI where human behaviour, edge cases, and ethics collide at scale. That work convinced me that responsible AI needs infrastructure, not just good intentions. Based in Dublin, closely watching how regulation is reshaping what we build and how.
Connect on LinkedInRelated Articles
Who's accountable when healthcare AI makes a mistake?
Ireland's Medical Council says doctors remain responsible for AI decisions. But how can they be confident in tools they don't fully understand?
The human supply chain behind AI
Three AI coding assistants, same task. All three imported deprecated libraries. None flagged it.