Skip to main content

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_receipt on every invoke response
  • AWR/2 / W3C Verifiable Credential, Ed25519, whole-document signature
  • Content-addressed provenance chains — parents commit 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_url and receipt_url — this hub's own public routes, absolute when AIMARKET_HUB_URL is set and root-relative otherwise;
  • verifier_url — the offline verifier UI, where the document from receipt_url can 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)

Source distribution for aimarket-provenance 2.3.0
File Size Uploaded
aimarket_provenance-2.3.0.tar.gz 79.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aimarket-provenance 2.3.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

2.3.0 This release

2 release files

2.0.0

2 release files

1.1.0

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