# Verify a deed

Install the guard, issue a challenge, verify. No server is involved
anywhere in this path.

## Verifying a deed

The **deed guard** is the piece you install. It owns exactly the gap the
verifier cannot: the anti-replay challenge is state only your app holds, and a
verifier that managed it would be a server — the thing this product removes.

It ships in TypeScript, Python, Go and Rust, with thin adapters for Express,
FastAPI and `net/http`. Rust has no adapter on purpose: it has no single
dominant web framework, and wiring the guard into axum, actix or poem is a
handful of idiomatic lines either way.

Two endpoints and you are done:

```
GET  /challenge   → { "challenge": "…" }   the guard mints and remembers it
GET  /anything    ← X-Grantor-Deed: <base64url deed>
                    X-Grantor-Challenge: <the challenge>
```

Three properties are worth knowing, because they are the difference between
the guard and a hand-rolled check:

- **The challenge travels in its own header** and is never read out of the
  deed. Taking it from the token would let the caller pick their own nonce,
  which is the entire replay defence gone.
- **The challenge is burned before verification**, not after. Otherwise a
  flood of bogus deeds becomes a free probe for which challenges are live.
  The cost is that a legitimate client whose own deed was malformed must
  fetch a new challenge, which is the right trade.
- **The guard mints no session and sets no cookie.** Your session format,
  expiry and flags are yours; they are not ours to choose.

### Error codes

The same strings in every language, so your handling ports. Full reference,
including the operator-facing origin-vouch self-check codes:
[Errors](errors.md).

| Code | Meaning | HTTP |
|---|---|---|
| `MissingDeed` / `MissingChallenge` | the caller sent neither header | 401 |
| `UnknownChallenge` | unissued, expired, or already spent | 401 |
| `BadDeedEncoding` | not base64url JSON | 401 |
| `UnsupportedMode` | not a mode this verifier accepts | 401 |
| `WrongAudience` | minted for a different app | 401 |
| `ChallengeMismatch` | bound to a different challenge | 401 |
| `Expired` | past its own `exp` | 401 |
| `Malformed` | wrong fields for its claimed mode | 401 |
| `BadProof` | signature or ZK proof failed | 401 |
| `StaleRoot` | membership root too old — possible revocation | 401 |
| `TenantInactive` | **billing** — the tenant has lapsed | 401 |
| `Chain` | the RPC read failed | **503** |

`Chain` is 503 rather than 401 deliberately. An RPC outage is not the caller's
fault, and answering 401 sends clients into a login loop that discards good
credentials and cannot succeed.

The verifier **fails closed**: if the chain cannot be read, no deed is
accepted. That is what makes the on-chain billing check load-bearing rather
than advisory.

### The default in-memory challenge store

Correct for one process and wrong the moment you run two — a challenge issued
by one instance is not found by the other, so every other login fails. Put a
shared store (Redis, your database) behind the `ChallengeStore` interface
before scaling out.

## See also

- [Getting started](getting-started.md) — the deed path, end to end.
- [Wallet login](wallet-login.md) and [Agent tokens](agent-tokens.md) — how these deeds get minted in the first place.
- [Sovereign tier](../sovereign-tier.md) — discovery, origin binding, origin provenance, and the exact order `verify_deed` checks things in.
- [Errors](errors.md) — the full error reference.
- [`../llms-full.txt`](../llms-full.txt) — the entire product in one file.
