# wais-pod — Full Documentation for AI Agents

> This file contains the complete documentation for wais-pod, optimized for consumption by AI agents and LLMs. It combines the API reference, integration guide, and protocol decisions into a single document.

---

# Overview

wais-pod is the core library of the WAIS (Web Agent Interaction Standard) ecosystem. It enables AI agents to authenticate as delegates of human users using Proof of Delegation (PoD) tokens — signed JWTs that prove an agent has authorization to act on behalf of a specific human, with scoped permissions and time limits.

The library provides:
- Token issuance and verification (ES256/JWT)
- DPoP proof-of-possession binding (RFC 9449)
- Selective Disclosure JWTs for privacy-preserving identity
- 30+ standard scopes with risk levels
- Confirmation/payment flows for high-risk actions
- Drop-in FastAPI integration with dual auth (API key + WAIS)
- agents.json manifest parser for service discovery

Package: pip install wais-pod
FastAPI extra: pip install wais-pod[site]
Python: >= 3.10
Dependencies: cryptography >= 41.0.0 (only runtime dep)

---

# Architecture

```
Human (Alice)
  │
  ├── delegates to ──▶ WAIS Platform (pod.deeger.io)
  │                        │
  │                        ├── issues PoD token (signed JWT)
  │                        ├── issues SD-JWT passport (selective disclosure identity)
  │                        └── binds token to agent's DPoP key pair
  │
  └── Agent (Claude, GPT, etc.)
        │
        ├── discovers site via GET /.well-known/agents.json
        ├── registers via POST /wais/api/register (shares email via SD-JWT)
        └── calls API with:
              Authorization: DPoP <pod_token>
              DPoP: <dpop_proof>
              ↓
        Site (api.serphub.dev)
              ├── verifies token signature against platform JWKS
              ├── verifies DPoP proof (sender-constraining)
              ├── identifies user by hash
              ├── checks scopes
              └── returns API response (same as API key auth)
```

---

# Site Integration (FastAPI)

## Step 1: Initialize

```python
from pod.site import WAISAuth
from contextlib import asynccontextmanager
from fastapi import FastAPI, Depends

wais = WAISAuth(
    site_url="https://serphub.deeger.io",           # site identity (token audience)
    api_base_url="https://api.serphub.deeger.io",   # where API endpoints live (for DPoP htu)
    platform_urls=["https://pod.deeger.io"],         # trusted token issuers
)

@asynccontextmanager
async def lifespan(app):
    await wais.setup()  # fetches platform JWKS, retries 3x
    yield

app = FastAPI(lifespan=lifespan)
```

IMPORTANT: Set api_base_url when your API is behind a reverse proxy (nginx, Cloudflare, etc.). This ensures DPoP htu verification uses the correct public URL instead of http://localhost:8000. URL resolution chain:
1. api_base_url + request.path (deterministic, recommended)
2. X-Forwarded-Proto/Host headers (proxy-dependent)
3. site_url + request.path (fallback)

## Step 2: Serve agents.json

```python
@app.get("/.well-known/agents.json")
async def agents_manifest():
    return wais.agents_json(
        name="SerpHub",
        description="Search API for AI agents",
        auth_methods=["wais-pod", "api-key"],
        actions=[
            {
                "id": "search",
                "description": "Search the web",
                "endpoint": "/v1/search",
                "method": "POST",
                "scope": "api.access",
                "risk_level": "low",
                "input_schema": {"type": "object", "required": ["query"], "properties": {"query": {"type": "string"}}},
            },
        ],
        data_requirements={"registration": {"required_claims": ["email"], "optional_claims": ["name"]}},
    )
```

## Step 3: Dual auth endpoints

```python
@app.post("/v1/search")
async def search(auth=Depends(wais.require(
    scopes=["api.access"],
    api_key_validator=your_existing_validator,
))):
    if auth.is_wais:
        user = db.get_by_wais_hash(auth.user_hash)
    else:
        user = auth.api_key_data["user"]
    return do_search(user)
```

AuthResult fields:
- auth.is_wais: bool — True if WAIS token auth
- auth.user_hash: str — SHA-256 hash of user (WAIS only)
- auth.scopes: list[str] — authorized scopes
- auth.constraints: Constraints — spending limits
- auth.client: str — client identifier
- auth.api_key_data: dict — API key validator result
- auth.payment_context: dict — tokenized payment info

## Step 4: Registration endpoint

```python
@app.post("/wais/api/register")
async def register(request: Request):
    result = wais.verify(request)
    user_hash = result.token.delegation.user_hash

    # Verify SD-JWT identity
    from pod.sd_jwt import SDJWTVerifier
    identity = request.headers.get("X-WAIS-Identity", "")
    vr = SDJWTVerifier.verify(identity, issuer_public_key_pem=platform_pem, required_claims=["email"])
    email = vr.claims["email"]  # only disclosed claims are visible

    user = db.get_by_email(email) or db.create_user(email=email)
    user.wais_hash = user_hash
    db.save(user)
    return {"status": "registered"}
```

---

# Complete API Reference

## pod.token

### Constraints
Fields: max_transaction_amount (dict|None), require_confirmation_above (dict|None), allowed_domains (list[str]|None), geo_restrictions (list[str]|None)
Methods:
- exceeds_amount(amount: float, currency: str) -> bool
- needs_confirmation(amount: float, currency: str) -> bool
- to_dict() -> dict

### DelegationPayload
Fields: user_hash (str), user_verified (bool), consent_timestamp (int), scopes (list[str]), constraints (Constraints), payment_context (dict|None)
Methods:
- hash_user_id(user_id: str) -> str [static] — creates "sha256:{hex}"
- has_scope(scope: str) -> bool — supports wildcard patterns like "catalog.*"
- has_all_scopes(scopes: list[str]) -> bool
- to_dict() -> dict

### PoDToken
Fields: alg (str, default "ES256"), typ (str, default "at+jwt"), kid (str|None), iss (str), sub (str), aud (str), iat (int), exp (int), jti (str|None), client_id (str|None), scope (str, space-separated), cnf (dict|None, e.g. {"jkt": "thumbprint"}), delegation (DelegationPayload)
Properties: is_expired (bool), header (dict), payload (dict)

## pod.issuer

### PoDIssuer
Constructor: PoDIssuer(platform_url: str, private_key_path: str|None = None, private_key_pem: bytes|None = None, key_id: str|None = None)
Methods:
- generate_keypair() -> tuple[bytes, bytes] [static] — returns (private_pem, public_pem)
- create_token(agent_session: str, audience: str, user_hash: str, scopes: list[str], constraints: dict|None = None, ttl_seconds: int = 3600, user_verified: bool = True, agent_public_key_jwk: dict|None = None, client_id: str|None = None, payment_context: dict|None = None) -> str

## pod.verifier

### VerificationResult
Fields: valid (bool), token (PoDToken|None), reason (str|None)
Methods:
- requires_confirmation(action_amount: float = 0, currency: str = "EUR") -> bool
- exceeds_limit(action_amount: float = 0, currency: str = "EUR") -> bool

### PoDVerifier
Constructor: PoDVerifier(trusted_platforms: dict[str,str]|None = None, registry_url: str|None = None, allow_expired: bool = False, clock_skew_seconds: int = 30)
Methods:
- add_trusted_platform(platform_url: str, public_key_path: str)
- add_trusted_platform_pem(platform_url: str, public_key_pem: bytes)
- verify(token_string: str, required_scopes: list[str]|None = None, expected_audience: str|None = None, expected_audiences: set[str]|None = None, dpop_proof: str|None = None, http_method: str|None = None, http_url: str|None = None) -> VerificationResult

12 verification steps: (1) parse JWT, (2) typ = at+jwt or WAIS-PoD, (3) alg = ES256, (4) match issuer, (5) verify signature, (6) build PoDToken, (7) iat not future, (8) exp not past (with clock skew), (9) jti exists, (10) audience match (multi-audience), (11) user_verified, (12) scopes + DPoP

## pod.dpop

### DPoPKeyPair
Constructor: DPoPKeyPair(private_key: EC private key)
Methods:
- generate() -> DPoPKeyPair [static]
- public_jwk -> dict [property] — {kty, crv, x, y}
- thumbprint() -> str — SHA-256 JWK Thumbprint (RFC 7638)
- create_proof(method: str, url: str, access_token: str) -> str

### DPoPVerifier
Constructor: DPoPVerifier(max_age_seconds: int = 300)
Methods:
- verify(dpop_proof: str, access_token: str, expected_method: str, expected_url: str, token_jkt: str) -> DPoPVerificationResult

### DPoPVerificationResult
Fields: valid (bool), reason (str|None)

## pod.sd_jwt

### SDJWTIssuer
Constructor: SDJWTIssuer(private_key, issuer_url: str)
- from_pem(private_key_pem: bytes, issuer_url: str) -> SDJWTIssuer [classmethod]
- create_credential(user_data: dict, subject: str = "", credential_type: str = "UserIdentityCredential", ttl_seconds: int = 31536000) -> tuple[str, dict[str, str]]
  Returns: (sd_jwt_with_disclosures "jwt~disc1~disc2~", disclosure_map {"claim": "disc"})

### SDJWTHolder
- create_presentation(sd_jwt_full: str, disclosed_claims: list[str], disclosure_map: dict[str, str]) -> str [static]
  Returns: "jwt~selected_disc1~selected_disc2~"

### SDJWTVerifier
- verify(presentation: str, issuer_public_key_pem: bytes, required_claims: list[str]|None = None, expected_issuer: str|None = None) -> SDJWTVerificationResult

### SDJWTVerificationResult
Fields: valid (bool), claims (dict|None), reason (str|None)

## pod.scopes

### Scopes (class with constants and class methods)

Standard scopes by category:
- Universal: account.create (low), account.read (low), account.modify (medium), account.delete (critical), subscription.create (high), subscription.read (low), subscription.modify (high), subscription.cancel (high)
- E-Commerce: catalog.browse (low), catalog.compare (low), cart.modify (medium), checkout.execute (high), order.track (low), return.initiate (medium), return.complete (high)
- SaaS: service.browse (low), service.subscribe (high), service.manage (high), api.access (low), api.keys.create (medium), api.keys.revoke (high)
- Travel: availability.search (low), booking.create (high), booking.modify (high), booking.cancel (high), claim.submit (medium)
- Financial: quote.request (low), payment.execute (critical), dispute.file (medium), policy.modify (high)
- Government: appointment.book (medium), form.submit (high), document.request (high), status.check (low)
- Healthcare: appointment.schedule (medium), prescription.renew (high), records.access (critical)

Methods:
- is_standard_scope(scope: str) -> bool
- is_custom_scope(scope: str) -> bool — True if starts with "x-"
- risk_level(scope: str) -> str — returns standard risk or "medium" for custom
- is_high_risk(scope: str) -> bool — True if high or critical
- validate_risk_override(scope: str, declared_risk: str) -> str — sites can RAISE never LOWER
- Convenience bundles: universal_read(), ecommerce_browse(), ecommerce_full(), saas_browse(), saas_full(), travel_full()

## pod.confirmation

### PaymentInfo
Fields: method (str), provider (str), amount (float), currency (str), url (str|None), expires_at (int|None)
Methods: to_dict(), from_dict(data) [classmethod]

### Resolution
Fields: mode (str, "poll"), endpoint (str), interval_seconds (int, 5), max_attempts (int, 60), timeout_seconds (int, 300), statuses (dict, {pending:202, completed:200, failed:422, expired:410})
Methods: to_dict(), from_dict(data) [classmethod]

### ConfirmationChallenge
Fields: challenge_id (str, auto-generated), action (str), risk_level (str), expires_at (int), display_to_user (dict), approval_methods (list[str]), payment (PaymentInfo|None), resolution (Resolution|None)
Methods:
- create(action, risk_level="high", ttl_seconds=300, ...) [classmethod]
- is_expired -> bool [property]
- requires_payment -> bool [property]
- to_dict() -> {"wais_confirmation": {...}}

### ConfirmationResponse
Fields: challenge_id (str), approved (bool), approval_method (str), approved_at (int), user_hash (str), platform_signature (str)
- is_well_formed -> bool [property]
- to_dict() -> {"wais_confirmation_response": {...}}

## pod.site

### WAISAuth
Constructor: WAISAuth(site_url: str, api_base_url: str|None = None, platform_urls: list[str]|None = None, allow_expired: bool = False, clock_skew_seconds: int = 30)

Methods:
- setup() [async] — fetch JWKS from all platforms, 3 retries with backoff [1s, 2s]
- extract_token(request) -> str|None [static] — checks: DPoP header > Bearer (no API key) > X-WAIS-PoD
- verify(request, required_scopes=None) -> VerificationResult — raises HTTPException(403) on failure
- require(scopes=None, api_key_validator=None) -> FastAPI Depends returning AuthResult
- agents_json(name, description, actions=None, scopes=None, ...) -> dict — new format (actions array) or legacy (flat scopes)

DPoP URL resolution: api_base_url+path > X-Forwarded-Proto/Host > site_url+path. Never returns raw localhost URL.

### AuthResult
Fields: is_wais (bool), client (str), user_hash (str), scopes (list[str]), wais_result (VerificationResult|None), api_key_data (dict|None)
Properties: constraints, payment_context

## pod.manifest

### WAISManifest
Constructor: WAISManifest(data: dict)
- from_url(url) [async classmethod] — fetches agents.json (requires httpx)
- from_dict(data) [classmethod]

Properties: site_url, api_base_url (falls back to site_url), name, description, provider_urls

Methods:
- resolve_endpoint(action_id) -> str — api_base_url + action endpoint
- get_action(action_id) -> dict|None
- list_actions() -> list[dict]
- get_effective_risk(action_id) -> str — respects override rules
- get_required_scopes(action_id) -> list[str]
- get_all_scopes() -> list[str]
- is_async(action_id) -> bool
- get_resolution(action_id) -> Resolution|None
- get_registration_claims() -> (required: list[str], optional: list[str])
- to_dict() -> dict

---

# Protocol Rules

1. Token audience (aud) = site_url (discovery domain), NEVER api_base_url
2. DPoP htu = exact request URL (routed to api_base_url)
3. Sites can RAISE risk levels, NEVER lower them. checkout.execute is always >= "high"
4. Async resolution status codes: 202=pending, 200=completed, 422=failed, 410=expired
5. WAIS never touches money — sites provide Stripe/PayPal payment links, agents poll for completion
6. Transport: Authorization: DPoP <token> + DPoP: <proof> headers. Fallback: X-WAIS-PoD
7. Custom scopes use x- prefix (e.g. x-serphub:bulk), default risk: medium
8. SD-JWT: users carry a privacy-preserving passport. Sites request only needed claims
9. user_hash = sha256 of user ID. Same human = same hash across all sites via same platform
10. A WAIS Platform (like pod.deeger.io) issues tokens and stores the user's digital passport. It is NOT a payment processor. Future providers: Anthropic, OpenAI, Google.
