Evidence receipts

Every decision produces a signed receipt; every later state change (approval, execution, cancellation) produces a new receipt chained to the previous one by hash. Receipts are the product's proof layer: provider-neutral, exportable JSON, verifiable without trusting mnd8t's database.

Shape

{
  "receipt_version": "1.0",
  "receipt_id": "evd_…",
  "organisation_id": "org_…",
  "principal": { "type": "ORGANISATION", "reference": "acme_ltd" },
  "agent": { "id": "agt_…", "external_id": "procurement_agent_12" },
  "mandate": { "id": "mnd_…", "version": 3, "policy_hash": "sha256:…" },
  "intent": {
    "id": "int_…", "amount_minor": 84000, "policy_amount_minor": 84000,
    "policy_currency": "GBP", "asset": "USDC",
    "counterparty": "supplier_acme_cloud", "destination": "0x…",
    "purpose": "cloud_infrastructure", "submitted_at": "…"
  },
  "decision": {
    "status": "ESCALATE", "effective_status": "ESCALATE", "mode": "ENFORCE",
    "reasons": ["HUMAN_APPROVAL_REQUIRED"], "decided_at": "…"
  },
  "approval":  { "status": "APPROVED", "approver_reference": "usr_…", "comment": null, "acted_at": "…" },
  "execution": { "status": "CONFIRMED", "provider": "SIMULATED", "external_reference": "tx_…", "confirmed_at": "…", "error": null },
  "revocation": null,
  "previous_receipt_hash": "sha256:…",   // null on the first receipt
  "signature": { "algorithm": "Ed25519", "key_id": "key_…", "value": "base64…" }
}

Canonicalisation

Signatures cover the canonical form of the receipt without its signature field: object keys sorted by UTF-16 code units, no whitespace, standard JSON string escaping, and integers only (the serialiser rejects floats — all money is minor units). This is a strict subset of RFC 8785 JCS sufficient for receipt content. The same algorithm is implemented server-side and in the @mnd8t/sdk client, so a receipt verifies identically wherever you check it.

Verifying

  1. Server: GET /v1/evidence-receipts/{id}/verify
  2. SDK (local, independent):
    const envelope = await client.receipts.get(id);
    const { valid, reason } = await client.receipts.verify(envelope.receipt);
    
  3. Your own code: the receipt is canonicalised JSON with an Ed25519 signature — any standard crypto library can verify it against the published public key.

Public keys are served at GET /.well-known/mandate-keys.json ({ keys: [{ key_id, algorithm, public_key_pem }] }); pick the key matching signature.key_id.

Any field change — an amount, a reason code, a timestamp — breaks verification. The previous_receipt_hash chain makes the sequence of states for one decision tamper-evident as a whole.

Key management

Signing keys are Ed25519, loaded from a secret manager and never committed anywhere. Each key has an id that appears on every receipt it signs, so receipts stay verifiable across a key rotation as long as the retired public key remains published.

8 min read · Need something that isn't here? Browse all documentation