# Portable proof verification (sandbox)

A binding, `AUTHORIZED`, externally executed authorization carries a **signed portable proof**: a standard
Ed25519 / EdDSA JWS (`typ: quattrino-authz-proof+jwt`) with minimal claims - ids, hashes and the authorized terms;
no PII, no policy internals. Anyone holding the token (your executor, a merchant, another agent) can verify it
**without any Quattrino credential** and must refuse to act unless `valid` is `true`.

Dry runs, denials, approval-pending and Quattrino-executed authorizations never carry a proof.

## Where the proof is

```json
"portable_proof": {
  "proof_id": "proof_m1jdcwepu4jktbclj5r1", "version": "1", "alg": "EdDSA",
  "kid": "sandbox-ed25519-c0c801616c929049", "issuer": "quattrino-sandbox", "environment": "sandbox",
  "audience": "amazon.example", "audience_bound": true,
  "token": "eyJhbGciOiJFZERTQSIsImtpZCI6...",
  "claims_hash": "1e3a...", "issued_at": "2026-09-26T05:07:15Z", "expires_at": "2026-09-26T05:22:15Z",
  "single_use": true, "verification_count": 0
}
```

Also `GET /api/v1/authorizations/{id}/proof` (agent credential) -> `{authorization_id, status, portable_proof}`.
Bind the proof to its verifier with `proof_audience` on the authorize request (merchant domain, service id or any
string you and the counterparty agree on).

## Verify (public, no credential)

```bash
curl -s -X POST https://quattrino.io/api/v1/authorizations/verify -H "Content-Type: application/json" \
  -d '{"proof": "<token>", "audience": "amazon.example",
       "expected": {"amount_minor": 1999, "currency": "USD", "merchant": "amazon"}}'   # expected is optional
```

Real `VALID` response:

```json
{ "valid": true, "status": "VALID", "reason_codes": [], "reasons": [],
  "checks": { "structure": true, "key_known": true, "signature": true, "issuer": true, "not_expired": true,
              "audience": true, "proof_registered": true, "terms_intact": true, "live": true, "authority_intact": true },
  "environment": "sandbox", "issuer": "quattrino-sandbox", "proof_version": "1",
  "notice": "SANDBOX proof: it authorizes a simulated external execution only; no real money moves.",
  "proof": { "proof_id": "proof_...", "authorization_id": "auth_...", "audience": "amazon.example",
             "terms": { "amount_minor": 1999, "currency": "USD", "merchant": "amazon", "action_type": "purchase", "commitment": "ONE_TIME", "effective_maximum_minor": 2000 } },
  "authorization": { "status": "AUTHORIZED", "expires_at": "..." } }
```

It **fails closed**. Real `INVALID` responses (one reason each):

| situation | `reason_codes[0]` | failing check |
|---|---|---|
| verifier passes a different `audience` | `proof_audience_mismatch` | `audience: false` |
| token altered by one character | `proof_signature_invalid` | `signature: false` |
| the authorization was already consumed (outcome recorded) | `proof_already_consumed` | `live: false` |
| expired | `proof_expired` | `not_expired: false` |
| revoked | `authorization_revoked` | `live: false` |
| unknown `kid` / unsupported version / wrong issuer | `proof_unknown_key` / `proof_version_unsupported` / `proof_issuer_mismatch` | |
| `expected` terms differ from the authorized terms | `proof_terms_mismatch` | `terms_intact: false` |

Every reason follows the registry shape (`family: PROOF_INTEGRITY`, `severity: invalid`, `remedy:
"Obtain a fresh proof from the agent; never execute on an INVALID proof."`).

## Offline signature check

`GET /api/v1/public/proof-keys` returns the sandbox verification keys (JWKS-style, `kid` -> Ed25519 public key).
An offline check proves the signature and expiry only; **liveness, single use, revocation and terms** require the
online verifier. Use online verification before executing.

## Close the loop

After executing on a valid proof, the agent reports the outcome with `proof_id` and `proof_verified: true`:
`POST /authorizations/{id}/outcome {"success": true, "actual_amount_minor": 1999, "provider_reference": "...", "proof_id": "proof_...", "proof_verified": true}`.
The outcome records `proof_verified_before_execution: true`; the proof is then `proof_already_consumed` for everyone.

SDK: `auth["portable_proof"]["token"]`, `q.get_proof(id)`, `verify_proof(base, token, audience=...)` (module-level, no key),
`q.proof_keys()`. MCP: `authorization_proof`, `verify_authorization_proof`.

**Sandbox only.** Issuer `quattrino-sandbox`; not production signing; no real money.
