aimarket-provenance
AWR/2 work receipts for every AI output — W3C Verifiable Credentials, eddsa-jcs-2022 over RFC 8785, issued by a did:key.
Infrastructure-layer plugin for AIMarket Hub. Auto-attaches a tamper-evident receipt to every successful /invoke, and exposes public verify endpoints for auditors and end users.
Receipts conform to AWR/2 (Agent Work Receipt 2.0.0). Verification needs no network, no registry, no blockchain and no issuer-specific software: any conformant W3C VC library can check one.
Try it: open the receipt's
receipt_url, copy the JSON, and paste it into verify.modelmarket.dev — client-side Ed25519, no backend, nothing sent anywhere.
Documentation
Every link here is absolute on purpose. A relative link in this file resolves against
pypi.org on the project page and 404s, and these documents are not inside the distribution
either, so a reader has nowhere else to follow them to.
| Document | Description |
|---|---|
| User guide | Install, configure signing key, verify receipts |
| Receipt format | The AWR/2 document this plugin emits, field by field |
| User cases | Compliance, consumer apps, multi-step chains |
| SDK integration | HTTP API, Python, invoke hook behavior |
| AWR/2 specification | The normative format definition |
Why this exists
Regulated and high-trust AI workflows need proof of what model ran, on what input digest, at what time, signed by whom — without storing full prompts anywhere. Provenance is the infrastructure abstract layer between raw invoke results and external audit systems.
What a valid receipt means, exactly: this issuer signed these claims, and the bytes are intact. It does not mean the model ran, that the digests correspond to real payloads, that the price was paid, or that the output is correct (SPEC.md §13.7). Everything AWR provides is attribution.
Features
- Auto-receipt on invoke —
provenance_receipton every invoke response - AWR/2 / W3C Verifiable Credential, Ed25519, whole-document signature
- Content-addressed provenance chains —
parentscommit to the parent receipt's exact bytes - Subcontractors in
parents— a call that hired other providers through the hub commits to their receipts - Anchoring in HISTOR — every receipt's digest goes into a public, append-only receipts log (off unless configured)
- TEE + ZK metadata — carried inside the signature, and never claimed as verified
- Offline public verify — anyone, with no call back to this hub
- Protected attest — Bearer token required for manual receipt creation
- AWR/1 legacy verification — receipts issued before this release still verify
Installation
pip install aimarket-provenance
aimarket serve
That pulls aimarket-hub, awr>=2.0,<3 and fastapi from PyPI. The plugin registers itself
through the aimarket.plugins entry point, so the hub discovers it with no configuration.
The awr package is a hard dependency. It is where RFC 8785 canonicalization, the eddsa-jcs-2022 proof and did:key derivation live, and the plugin implements none of them itself. If it is missing, the plugin raises ProvenanceDependencyError at import and the hub does not start with provenance quietly disabled — a receipt signed by a second canonicalizer verifies for its author and for nobody else, which is what split AWR/1 into two incompatible dialects (SPEC.md §4.3, Appendix D). It is awr on PyPI and needs no special handling.
From a checkout of this monorepo instead, where both live in the tree:
pip install -e awr/reference/python
cd aimarket-hub && pip install -e plugins/aimarket-provenance
Verify:
curl http://localhost:9083/ai-market/v2/plugins | jq '.plugins[] | select(.name=="provenance")'
curl http://localhost:9083/.well-known/ai-market.json | jq '.plugin_extensions.provenance'
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/ai-market/v2/p/provenance/attest |
Bearer (required) | Create a receipt manually |
GET |
/ai-market/v2/p/provenance/receipt/{id} |
Public | Fetch the stored AWR/2 document |
GET |
/ai-market/v2/p/provenance/verify/{id} |
Public | Full verification, SPEC.md §11.1 result |
GET |
/ai-market/v2/p/provenance/anchor/{id} |
Public | Where the receipt stands in HISTOR's receipts log (below) |
GET /verify/{id} accepts ?profile=L0|L1|L2 to request a profile (§10). Profile reason codes are only reported for a profile you asked for (§10.4).
Auto-receipt on invoke
curl -X POST http://localhost:9083/ai-market/v2/invoke \
-H "Content-Type: application/json" \
-d '{
"product_id": "prod-demo",
"capability_id": "translate@v1",
"source_hub": "local",
"input": {"text": "Hello"}
}' | jq '.provenance_receipt'
{
"receipt_id": "urn:uuid:d08663d5-f5e7-4868-805d-b917a429781f",
"verify_url": "https://modelmarket.dev/ai-market/v2/p/provenance/verify/urn:uuid:d08663d5-f5e7-4868-805d-b917a429781f",
"receipt_url": "https://modelmarket.dev/ai-market/v2/p/provenance/receipt/urn:uuid:d08663d5-f5e7-4868-805d-b917a429781f",
"verifier_url": "https://verify.modelmarket.dev",
"awr_version": "2.0.0",
"issuer": "did:key:z6Mkgs7XWJuzXfe7CiuAqEioZxm6GYcb7SQr9yEYWwV7PJod",
"digest_sri": "sha256-…"
}
digest_sri is the receipt's digest: what a parent receipt's parents edge commits to, and what
anchoring sends to HISTOR.
The verify URL changed in 2.0.0. It used to be {AIMARKET_VERIFY_DOMAIN}/r/{short_id}, which was a dead link: the static verifier at verify.modelmarket.dev never reads the request path (nginx serves index.html for /r/… and the page's only lookup is over pasted text), and it can only fetch documents it hosts itself under data/receipts/, never a receipt stored on a hub. Opening such a link showed an empty form. The plugin now emits URLs that work:
verify_urlandreceipt_url— this hub's own public routes, absolute whenAIMARKET_HUB_URLis set and root-relative otherwise;verifier_url— the offline verifier UI, where the document fromreceipt_urlcan be pasted. That is the offline check: it needs nothing from this hub and tells this hub nothing (SPEC.md §13.5, §14).
Manual attest
curl -X POST http://localhost:9083/ai-market/v2/p/provenance/attest \
-H "Authorization: Bearer $AIMARKET_PROVENANCE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model_id": "legal.review@v1@prod-legal",
"provider_hub": "https://provider.example.com",
"input": {"documents": {"hash": "sha256:..."}},
"output": {"risk": "low", "issues": 0},
"status": "succeeded",
"parent_receipts": ["urn:uuid:2c1e…"],
"latency_ms": 4200,
"price_usd": 0.15
}'
Request fields: model_id, input, output are required. status is one of succeeded, failed, refused, timeout, partial (default succeeded) — a receipt for work that did not succeed is a first-class document, because an unverifiable failure is what a dispute usually turns on. provider_hub, latency_ms, price_usd, currency, invocation_nonce, reputation_score, tee_attestation, zk_input_proof, zk_output_proof, settlement and parent_receipts are optional.
parent_receipts changed in 2.0.0. An AWR/2 chain edge commits to the parent's exact bytes (SPEC.md §3.2, §8.1), so an identifier alone is not an edge — in AWR/1 it was, and because id was outside the signature an intermediary could rename a valid receipt and re-point a chain at it (§13.1). You may pass:
- a receipt id this hub stores — the hub loads it and computes the digest itself;
- a full digest reference
{"id": "urn:uuid:…", "digestSRI": "sha256-…"}, when the parent lives elsewhere.
An id that resolves to nothing is a 400. The hub will not sign a commitment to bytes it has never seen, and a verifier must not fetch them (§13.5).
What a receipt looks like
{
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://verify.modelmarket.dev/ns/awr/v2"
],
"id": "urn:uuid:d08663d5-f5e7-4868-805d-b917a429781f",
"type": ["VerifiableCredential", "WorkReceipt", "AIProvenanceReceipt"],
"issuer": { "id": "did:key:z6Mkgs7XWJuzXfe7CiuAqEioZxm6GYcb7SQr9yEYWwV7PJod", "name": "AIMarket Hub" },
"validFrom": "2026-08-01T16:22:50Z",
"awrVersion": "2.0.0",
"credentialSubject": {
"work": {
"modelId": "translate@v1@prod-demo",
"startedAt": "2026-08-01T16:22:48Z",
"completedAt": "2026-08-01T16:22:50Z",
"latencyMs": 2340,
"status": "succeeded"
},
"inputDigest": "sha256-G4ESnt/c8X4GQdS1pUTbbWwQDF7wNB+EXxgEoTlBA5s=",
"outputDigest": "sha256-I6HZM9DzHKwCl/fNyIxyAkNCx8R2+eHWHqQ9ZbeZqbQ=",
"providerHub": "https://hub.example.com",
"payloadSerialization": "json-sorted-compact",
"price": { "currency": "USD", "amount": "0.15" },
"nonce": "20cd34fe-0acf-4173-af48-c78e4d24de35"
},
"hubInfo": { "hubName": "AIMarket Hub", "hubVersion": "3.0.0", "protocolVersion": "v2" },
"proof": {
"type": "DataIntegrityProof",
"cryptosuite": "eddsa-jcs-2022",
"created": "2026-08-01T16:22:50Z",
"verificationMethod": "did:key:z6Mkgs7XWJuzXfe7CiuAqEioZxm6GYcb7SQr9yEYWwV7PJod#z6Mkgs7XWJuzXfe7CiuAqEioZxm6GYcb7SQr9yEYWwV7PJod",
"proofPurpose": "assertionMethod",
"proofValue": "zHrSm2kM5fQq9bb7Q23V1wqasPdiPyujUFp8qcrzHM4kAnbvyZNNgYMJx6ZNgBepEmpydnoxRNTVhxkqNUq793gC"
}
}
Field-by-field, including the hub-specific members (providerHub, payloadSerialization, reputationScore, hubInfo) and how each AWR/1 field maps: docs/receipt-format.md.
Migrating from 1.x (AWR/1 → AWR/2)
| Then (AWR/1, ≤1.1.0) | Now (AWR/2, 2.0.0) | Why |
|---|---|---|
signature over credentialSubject only |
whole document | id, type and issuer were unsigned, so a receipt could be renamed and a chain re-pointed (§13.1) |
| a "JCS" canonicalizer with NFC, code-point key order and 10-decimal floats | RFC 8785, exactly, in awr |
two implementations of one format disagreed on the bytes for any integer |
priceUsd: 0.15, reputationScore: 0.9 (JSON numbers) |
price.amount: "0.15", reputationScore: "0.87" (decimal strings) |
removes the int/float divergence instead of arbitrating it (§4.3) |
Ed25519Signature2018, base64 proofValue |
DataIntegrityProof + eddsa-jcs-2022, multibase z… |
off-the-shelf VC libraries verify the current suite |
issuer.id = did:key: + 32 base64 chars |
a real did:key, and the key is derived from it |
the legacy value was not a DID and named no key |
parentReceipts: ["urn:uuid:…"] |
parents: [{"id":…, "digestSRI":…}] |
content-addressed edges (§8.1) |
| receipts older than 90 days hard-failed | age is a warning (AWR-TIME-002) |
an audit is the main reason old receipts are read (§11.3) |
| TEE attestation "verified" with the receipt issuer's own key | AWR-ENV-001, present and not verified |
the old check proved only that the claimant wrote it down (§7.3) |
AWR/1 issuance is gone. SPEC.md §12 requires an implementation never to issue AWR/1, so there is no flag, parameter or environment variable that produces one — the capability is absent, not guarded.
AWR/1 verification stays. Every receipt already in your database still verifies, reports the AWR-LEGACY-001 warning, names its unsigned fields, and never claims an AWR/2 profile. Nothing is re-signed: §12.2 forbids it, since an issuer cannot honestly re-attest a created timestamp.
One thing to know if you compare notes with another AWR implementation: the pipe-delimited AWR/1 rendering written out in SPEC.md §12.1 (path=leaf, dotted leaf paths) is not the rendering this plugin's AWR/1 issuer produced (key:value, top level only). The two disagree on every receipt, so the reference verifier reports AWR-LEGACY-002 for a receipt this hub signed correctly. aimarket_provenance/legacy.py therefore keeps its own rendering (dialect hub-1) and tries it before the two §12.1 dialects, so both foreign and local AWR/1 documents verify. This is reported upstream as a spec finding.
Anchoring receipts in HISTOR
With AIMARKET_HISTOR_URL set, every issued receipt's digest — never its content — is queued
and a background thread sends the queue to HISTOR's public receipts log, signed with the receipts'
own key: the digest, the issuer's did:key, the issue time and the anchor type, nothing else. A
buyer can then prove to anyone that the receipt existed when it says, and this hub cannot deny,
backdate or replace it. HISTOR being slow or down never touches an invoke: rows wait and are
retried with backoff. HISTOR accepts anchors only from the issuers its operator lists
(HISTOR_RECEIPT_ISSUERS) and only while the issue time is at most 30 days old.
GET /ai-market/v2/p/provenance/anchor/{id} answers pending, anchored (with leaf_index and
proof_url), refused, not_queued, unknown (the log could not be asked) or not_configured.
Anchored rows leave the outbox after AIMARKET_HISTOR_OUTBOX_RETAIN_DAYS; for a receipt with no
row the route asks HISTOR, and only a proof naming this digest under this hub's key reads
anchored ("source": "log").
Receipts issued before anchoring was switched on are queued with their own issue time by
python -m aimarket_provenance.backfill --db data/provenance.db --dry-run # then without --dry-run
It takes only receipts that verify, were issued by this hub's key and are inside HISTOR's window,
and does nothing where AIMARKET_HISTOR_URL is unset. Older receipts cannot be anchored with
their true time and are left alone. A backfilled anchor proves the receipt existed by the moment
HISTOR logged it.
Configuration
| Env Variable | Default | Description |
|---|---|---|
AIMARKET_PROVENANCE_KEY_PATH |
data/provenance_signing_key |
Ed25519 signing key (created on first run) |
AIMARKET_PROVENANCE_API_TOKEN |
(empty) | Bearer token for /attest; empty = /attest returns 503 |
AIMARKET_HUB_URL |
(empty) | Public hub base URL used in verify_url / receipt_url |
AIMARKET_VERIFY_DOMAIN |
https://verify.modelmarket.dev |
Offline verifier UI in verifier_url |
AIMARKET_RECEIPT_CORS_ORIGINS |
verifier + USE origins | Exact browser origins allowed to read public receipt JSON; GET route only |
DATABASE_URL |
SQLite | Optional PostgreSQL for receipt storage |
AIMARKET_HISTOR_URL |
(empty) | HISTOR receipts log to anchor digests in; empty = anchoring off |
AIMARKET_HISTOR_FLUSH_S |
30 |
How often the outbox sends (backs off to 10 min while HISTOR fails) |
AIMARKET_HISTOR_OUTBOX_RETAIN_DAYS |
30 |
Days an anchored row stays in the outbox; 0 keeps every row |
Back up provenance_signing_key. Under AWR/2 the issuer identifier is the key: issuer.id is the did:key derived from it, logged at startup. Lose the key and you cannot issue under that identity again; AWR has no revocation and no rotation mechanism beyond publishing a new did:key (§5.4, §13.6). Historical receipts stay verifiable either way — they carry everything a verifier needs.
Storage
Receipts persist in provenance.db (SQLite) or PostgreSQL when DATABASE_URL is set. Migration 005_provenance_receipts in the hub schema, unchanged: same columns, same indexes, no new migration, so a 1.x database is read by 2.0.0 and vice versa.
Four columns keep their name and meaning while the encoding of new rows changes. Old rows are never rewritten, and which generation a row is, is readable from the row itself (raw_json → proof.type).
| Column | AWR/1 rows | AWR/2 rows |
|---|---|---|
input_hash, output_hash |
64-char hex | SRI string, sha256-<base64> |
issuer_pubkey_b64 |
key asserted by the document | key derived from the did:key |
proof_value |
base64 signature | multibase base58btc, z… |
parent_receipts |
JSON array of id strings | JSON array of digest references |
raw_json holds the document verbatim and is the only column verification reads: §4.2 forbids re-canonicalizing through a lossy intermediate representation, and every other column is exactly such a projection. price_usd REAL stays a float column for querying — the signed value is the decimal string price.amount.
Combine with
| Plugin | Pattern |
|---|---|
aimarket-tee |
TEE attestation under environment.teeAttestation (carried, not verified) |
aimarket-zk |
ZK proof references under environment.zkProof without revealing payloads |
aimarket-reputation |
reputationScore decimal string on attest |
aimarket-channels |
price.amount records what was debited from the channel |
License
Apache-2.0 · Part of AIMarket Hub
Metadata
Release files for aimarket-provenance 2.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aimarket_provenance-2.3.0.tar.gz | 79.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aimarket_provenance-2.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 138.4 kB
Release files / aimarket_provenance-2.3.0.tar.gz
| Download URL | aimarket_provenance-2.3.0.tar.gz |
|---|---|
| Size | 79.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
454ba2ceaf9b3c3cd5cc9c274b16ca1891651e7b778114effa5ee7b37ec5e9e1
|
|
BLAKE2b-256 checksum How to use checksums |
ed107fd9b7417e22b6aeff66dbb577e38bcf69d1d82edc67f19f1748d2acea1d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / aimarket_provenance-2.3.0-py3-none-any.whl
| Download URL | aimarket_provenance-2.3.0-py3-none-any.whl |
|---|---|
| Size | 59.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4597c6b773a95058a5e193baf3f3ad448a9d9882e60501a90ac65f75bc90641f
|
|
BLAKE2b-256 checksum How to use checksums |
8bdad54f9e521f5911e39d09b03914ab1b08044266017580c30465335a129185
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|