# Receipts

Every call returns a receipt next to the answer. It records how the call was
handled: the retention basis, the model, the tokens, the cost and who served it.
It carries no prompt and no answer. offrec signs it with Ed25519, so you can check
what we say instead of taking our word for it.

## Where to find it

- `POST /v1/chat/completions`: a top-level `receipt` field on the response. OpenAI
  clients keep unknown fields, so in Python it is `resp.receipt`.
- Streamed (`stream: true`): on the final chunk, with the usage.
- `POST /v1/llm/chat` and `px.chat(...)`: the `receipt` key of the result.
- R: `out$receipt` from `pnotp_chat(...)`.

offrec keeps no copy of the call, so store the receipts you need. The console's Receipts page
shows the receipts your browser tab has seen, checks a receipt you saved, and lists
the billing record of every charged call.

## The fields

| Field | Signed | What it says |
|---|---|---|
| `mode` | yes | `compliant`: the call ran on the zero-retention tier. |
| `zero_retention` | yes | `true` when nothing from the call is kept. |
| `retention_basis` | yes | `zero_retention`, or `zero_retention_abuse_monitored` on Claude, where the provider screens calls live with automated classifiers. |
| `plaintext_stored` | yes | `none`: offrec stored neither prompt nor answer. |
| `monitoring` | yes | `metadata_only`: what offrec records is tokens, cost, model and time. |
| `model` | yes | The id that served the call, in the provider's own format (for example `google/gemini-3.1-flash-lite`). It can differ from the string you sent. |
| `tokens` | yes | Input, output, cached and cache-write tokens. |
| `timestamp` | yes | When the receipt was issued, in ISO 8601 with a `+00:00` offset. Older receipts carry no offset; they are UTC too. |
| `served_by` | yes | Who ran the call (`backend`), who governs retention there (`retention_controlled_by`: `google-cloud`, `aws` or `fireworks`), and on AWS the region and inference profile. |
| `cost` | no | What the call billed, in micro-USD, with the per-1M rates used. Reported, not signed today. |
| `signature` | | The algorithm, `key_id`, your `nonce`, the exact `signed_fields` and the signature `value`. |

Two real `served_by` blocks, from November 4, 2026:

```json
{"backend": "vertex-maas", "self_hosted": false, "retention_controlled_by": "google-cloud",
 "retention_basis": "zero_retention"}

{"backend": "bedrock-anthropic", "self_hosted": false, "retention_controlled_by": "aws",
 "retention_basis": "zero_retention_abuse_monitored", "region": "us-east-1",
 "inference_profile": "global"}
```

A full receipt is printed on the [quickstart](/quickstart#5-read-the-receipt-then-verify-it).

## Verify a receipt

The public key and the exact recipe are published, no key needed:

```bash
curl https://api.offrec.ai/v1/llm/attestation
# {"enabled": true, "alg": "ed25519", "key_id": "0e31ff2bb7807f09",
#  "public_key_base64": "...", "signed_fields": [...],
#  "canonicalization": "json:sorted-keys,no-spaces,utf-8,ensure_ascii=false", ...}
```

The recipe, in any language:

1. Take each field in `signed_fields` from the receipt. A field the receipt lacks is
   `null`.
2. Add `"nonce"` with the value of `receipt.signature.nonce` (it is `null` when you
   sent none).
3. Serialize that object as JSON with sorted keys, no spaces, and real UTF-8 (not
   `\uXXXX` escapes).
4. Verify the Ed25519 signature `base64decode(receipt.signature.value)` over those
   bytes with `public_key_base64`.

In Python:

```python
import base64, json, requests
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

def verify_receipt(receipt: dict) -> str:
    key = requests.get("https://api.offrec.ai/v1/llm/attestation").json()
    sig = receipt["signature"]
    payload = {f: receipt.get(f) for f in sig["signed_fields"]}
    payload["nonce"] = sig["nonce"]
    signed = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
    Ed25519PublicKey.from_public_bytes(base64.b64decode(key["public_key_base64"])).verify(
        base64.b64decode(sig["value"]), signed
    )                                  # raises InvalidSignature on any edit
    return key["key_id"]
```

Pin the `key_id` you verified against, and compare it with `signature.key_id` on
the receipts you store.

## Bind the receipt to your call

Send a `nonce` in the request body and it is placed inside the signed payload:

```python
resp = client.chat.completions.create(
    model="claude-sonnet-5-5",
    messages=[...],
    extra_body={"nonce": "order-4411-a8f3"},      # any string you choose
)
assert resp.receipt["signature"]["nonce"] == "order-4411-a8f3"
```

With your own nonce in it, a receipt can't be passed off as the proof for a
different call. The server does not store nonces; to reject a replayed receipt,
track the ones you issued.

## What a receipt proves, and what it does not

It proves:

- **Integrity.** The signed fields can't be edited without breaking the signature.
- **Non-repudiation.** offrec, holder of the signing key, issued these facts about
  this call and can't later deny them.
- **Freshness, if you send a nonce.** The receipt belongs to the call you made.

It does not prove:

- **That the hardware enforced zero retention.** The signing key lives on offrec's
  infrastructure. This is a vendor signature, not an attestation rooted in secure
  hardware. Confidential-computing inference is research, not a shipped feature.
- **The price.** `cost` is reported on the receipt but is not among the signed
  fields today. Your billing records in the console are the source for charges.
- **Anything about the content.** The receipt never includes the prompt or the
  answer, so the signature commits to nothing about them.

Next: [quickstart](/quickstart) for a first call, [compliant-llm](/compliant-llm)
for the full API.
