Tanilo
Trust

What you can check yourself, and what we still owe you.

No live counters, no endpoint you have to trust us to keep up. Everything below either verifies offline against a published key, or is named as open with no date attached to it.

01Keys

Keys in the published JWKS

tanilo-2026-10-ed25519-7d885da9 — current. Tanilo's signing key, in use since 2026-10-02. Its receipt-signing paths are /evaluate and /v1/verify-facts, both of which run the check themselves; it has no path that signs a verdict supplied by the caller. Those receipts name issuer tanilo.io and this page's key set. It also signs the separately identified key-continuity statement. Its JWKS entry declares role: evaluated, and every receipt it signs carries role: evaluated in the protected header.

ao-composed-2026-06-ed25519-c3abfce3 — retired 2026-10-02, when Tanilo's key went live. It signed /evaluate and /v1/verify-facts receipts on agentoracle.co, both of which run the check themselves; neither signs a verdict supplied by the caller. It stays published so existing signatures under it can still be checked; rotation alone does not invalidate them, and receipt validity, freshness and relying-party acceptance remain separate checks. From 23 June to 28 August 2026 it also signed bytes supplied by the caller through /v1/sign and /v1/sign/batch, and stub verdicts through that host's /v1/compose and /v1/v_gate. Since 28 August the sign routes refuse to sign and the stub routes don't issue. A forged payload could carry any timestamp, so no signature under this key shows on its own that the service issued the payload. It carries no role: it signed both kinds of bytes, so neither role value is true of every signature under it, and under the issuance-path role rule, signatures under it resolve to role unknown. That is separate from signature validity, and a relying party that requires an established issuance path treats unknown as its own fail.

ao-composed-2026-07-ed25519-3d44ba27 — retired; withdrawn 24 September 2026, when its private key was deleted. Its only signing path was the API gateway's /v1/compose (27 July to 24 September), which signed verdicts the caller supplied and whose handler returns 410 since 28 September 2026 (unauthenticated requests are refused with 401 before reaching it). Its signatures prove the bytes are unchanged, not that anything was evaluated. Its JWKS entry declares role: bytes_attested.

ao-receipt-2026-04-ed25519-f2753b7c — legacy. Published 29 April 2026 for the v0.1 receipt format. It signed the attached sample receipt in the spec repo. We found no signing path for it in the current service code. We haven't established where its private key is held or whether it still exists, so we say so here instead of guessing. No role: its full signing history is not established.

ao-fixture-detached-rfc7797-2026-07-ed25519-0f8bf2a5 — test fixture only. Its private key is published in the spec repo (examples/jwks-fixture-detached.json) so the fixture can be reproduced. Anyone can sign with it, so a signature from it proves nothing about issuance. No role.

Algorithm
Ed25519 (RFC 8032), canonicalized per RFC 8785 (JCS) before signing
Published keys
tanilo.io/.well-known/jwks.json — the same key set is served at api.tanilo.io/.well-known/jwks.json and agentoracle.co/.well-known/jwks.json. Each key carries a tanilo_status member (current, retired, legacy or fixture), tanilo_retired_at where it applies, and, where the key's history supports it, a role member (evaluated or bytes_attested) declaring what signatures under that key attest.
Key continuity
tanilo.io/.well-known/key-continuity.json — a statement listing every key above with its status, signed on 2026-10-02 by both the retiring key ao-composed-2026-06-ed25519-c3abfce3 and tanilo-2026-10-ed25519-7d885da9. It ties the older kids to this publisher, so the signature on a receipt issued under a retired key can still be checked with key material from tanilo.io. It does not establish that any older payload was evaluated.
Rotation policy
New signing key at least every 12 months, or immediately if a private key may have been exposed. Retired keys stay published in the JWKS so existing signatures under them can still be checked; rotation alone does not invalidate them, and receipt validity, freshness and relying-party acceptance remain separate checks.

Tanilo signs with tanilo-2026-10-ed25519-7d885da9 since 2026-10-02, a new key with no signing history before Tanilo; ao-composed-2026-06-ed25519-c3abfce3 is not used for new issuance. Retired and legacy keys stay published so existing signatures under them can still be checked; rotation alone does not invalidate them, and receipt validity, freshness and relying-party acceptance remain separate checks. Receipts issued before 2026-10-02 are not re-signed: they are exactly as issued. Full account of both signing paths: /incidents/2026-08-25-canned-verdicts ↗.

02Second implementation

A byte-identical second implementation, built by a different party from the published text. AgentTrust implemented the verification envelope from the specification; the fixtures and vectors that prove it landed in argentum-core/examples/conformance/agenttrust-v1 (PR #33, merged 2026-07-16).

03Verify a receipt yourself

Same commands as the homepage. Nothing here requires calling Tanilo — only the published key.

offline verification
$ pip install tanilo-receipt-verify
$ curl -s https://raw.githubusercontent.com/TKCollective/tanilo-receipt-spec/0f3b0cd8db5bb33c55aa73c1229c8398be96a37e/examples/live-receipts/evaluate-demo-2026-09-30.json -o receipt.json \
&& curl -s https://tanilo.io/.well-known/jwks.json -o jwks.json
$ python3 -c "import json; from tanilo_receipt_verify import verify; r=verify(json.load(open('receipt.json')), jwks_by_issuer={'https://tanilo.io/.well-known/jwks.json': json.load(open('jwks.json'))}); print(r.status, r.canonical_sha256)"

This is the same receipt as the homepage, issued by /evaluate on 2026-09-30. Until 2026-09-30 this section showed a receipt signed through the gateway route withdrawn on 24 September 2026, over a verdict the caller supplied; see the incident record.

Node and browser verifiers use the same JCS + Ed25519 semantics and produce the same canonical hash byte-for-byte. Full registry entry: /receipt-registry.

04What's still open

No date

Issuance-path assertion

A receipt doesn't say which route produced it. Before 2026-10-02, c3abfce3 remained the receipt-signing key after its caller-supplied signing paths closed on 28 August; at cutover it stops new receipt issuance; 3d44ba27 signed only caller-supplied verdicts and was withdrawn on 24 September 2026. A receipt shows its key ID, but not what that key was used for; that mapping is stated here, not in the receipt. The fix is a role parameter in the JWS protected header, checked against a role member on the signing key in the published JWKS. The role-in-header design is Pote's (poteshniy, AgentTrust); it is not yet normative text in the filed -02 draft. Update 2026-10-02: the issuer side is live. The Tanilo key's JWKS entry declares role: evaluated and every receipt it signs carries role: evaluated in the protected header; 3d44ba27 is declared bytes_attested; c3abfce3 carries no role, so under the issuance-path role rule, signatures under it resolve to role unknown. That is separate from signature validity, and a relying party that requires an established issuance path treats unknown as its own fail. The values are evaluated and bytes_attested, as agreed with Pote on 2 September 2026. Under the proposed issuance-path role rule, absence of a role gives an issuance-path resolution of unknown; it does not by itself make the receipt malformed. Existing signed bytes are unchanged, and signature validity, freshness and relying-party acceptance remain separate. Issuer-side annotation is live; verifier-side role resolution (role_resolution in tanilo-receipt-verify) and the draft format text remain unshipped, with no date, so this item stays open until both ship.

Defense in depth, not the fix

Kid separation

Giving evaluation receipts and sign-only receipts distinct key identifiers was the first fix proposed for the same-kid conflation above. It's still worth doing operationally, but the issuance-path assertion is the durable fix — it survives key rotation and doesn't require a verifier to keep a mapping table current.

In the spec, not yet in production

instrument_failure emission

The -02 IETF draft defines instrument_failure so a non-evaluation can be signed and recorded instead of producing no receipt at all. The running service hasn't been updated to emit it yet.

05Filed specification

The receipt format is filed as an IETF Internet-Draft: draft-krausz-verification-state (current revision -02). It defines the on-wire semantics — act/halt verdict, canonicalization, signer binding, and the reason-code vocabulary referenced above.