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 - 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 /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"
}
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.
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 |
DATABASE_URL |
SQLite | Optional PostgreSQL for receipt storage |
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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aimarket_provenance-2.0.0.tar.gz.
File metadata
- Download URL: aimarket_provenance-2.0.0.tar.gz
- Upload date:
- Size: 65.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c871324b267bfe49f052eb9eb402283081e035573642a26f66b88c817b1b94b
|
|
| MD5 |
447a263f6bcb7d998b6e6b2c5ee89ff9
|
|
| BLAKE2b-256 |
d40e3a4c5dcf6c0316eb8483d641933d176ad9d0450dcf381a879fb0bc55f07b
|
File details
Details for the file aimarket_provenance-2.0.0-py3-none-any.whl.
File metadata
- Download URL: aimarket_provenance-2.0.0-py3-none-any.whl
- Upload date:
- Size: 46.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
50017eeb98a836d3dea565c97ad49b8a091c0c65d982f3ce460f5831c5b0cc27
|
|
| MD5 |
6459c9105af9691ea56129b8abba6346
|
|
| BLAKE2b-256 |
4d898772ba939c882e67d0680e9a4930b58e0e3c579e030e309150a095db094e
|