# IOTA Web Guardian — Detailed MVP Specification

> **Version**: 0.1.0
> **Status**: Draft
> **Authors**: Paolo Alberti, Niccolò Normani
> **Network**: IOTA Rebased (Move VM)
> **Identity SDK**: IOTA Identity 2.x

---

## Table of Contents

1. [Problem Statement](#1-problem-statement)
2. [Solution Overview](#2-solution-overview)
3. [Actors & Roles](#3-actors--roles)
4. [System Architecture](#4-system-architecture)
5. [Services — Detailed Design](#5-services--detailed-design)
   - 5.1 [Guardian Proxy](#51-guardian-proxy)
   - 5.2 [Certificate Verifier Service](#52-certificate-verifier-service)
   - 5.3 [x402 Payment Facilitator](#53-x402-payment-facilitator)
   - 5.4 [Secrets Manager](#54-secrets-manager)
   - 5.5 [AI Agent Registration App](#55-ai-agent-registration-app)
   - 5.6 [Certifier Service](#56-certifier-service)
   - 5.7 [Super Certifier Admin](#57-super-certifier-admin)
   - 5.8 [AI Agent CLI](#58-ai-agent-cli)
6. [Data Models](#6-data-models)
7. [Key Flows](#7-key-flows)
8. [API Specifications](#8-api-specifications)
9. [Technology Stack](#9-technology-stack)
10. [Deployment Guide](#10-deployment-guide)
11. [Security Considerations](#11-security-considerations)
12. [Multi-Agent Support](#12-multi-agent-support)
13. [MVP Scope & Roadmap](#13-mvp-scope--roadmap)

---

## 1. Problem Statement

AI systems (ChatGPT, Perplexity, Claude, etc.) crawl and scrape websites at massive scale to answer user queries. End users increasingly get answers directly from these AI systems without ever visiting the source website. Content creators therefore lose:

- **Traffic** — fewer page views means less ad revenue and reduced organic discovery.
- **Attribution** — their work is consumed without credit.
- **Compensation** — AI companies monetise content without payment to original authors.

Existing tools (robots.txt, rate-limiting, WAFs) are ineffective: they either block everyone indiscriminately or are trivially bypassed.

---

## 2. Solution Overview

**IOTA Web Guardian** is a content protection layer that intelligently intercepts requests to a website and differentiates human visitors from AI agents/bots.

| Visitor type | Behaviour |
|---|---|
| Human (browser) | Forwarded transparently to origin — zero friction |
| Verified AI agent + payment | Forwarded to origin after credential verification and micropayment |
| Unverified AI agent | Redirected to onboarding / identification flow |

The key primitives are:

- **IOTA Identity 2.x** (DID + Verifiable Credentials) to uniquely and verifiably identify AI agents.
- **HTTP 402** protocol (`x402`) to facilitate on-chain IOTA micropayments per content access.
- A **Reverse Proxy Guardian** with near-zero integration cost for website operators.
- A **Trust Chain** managed by KYA/KYC-certified Certifiers who vet AI Agent companies.

---

## 3. Actors & Roles

### 3.1 AI Agent

An autonomous software process operated by an AI company that accesses web content programmatically (scraper, crawler, RAG pipeline, etc.).

- Must hold a valid **DID** anchored on the IOTA Tangle.
- Must carry a **Verifiable Credential (VC)** issued by a trusted Certifier.
- Must pay a per-request or per-session micropayment in IOTA tokens.
- Presents credentials and payment proof in HTTP request headers.

### 3.2 AI Agent Company

The legal entity that operates one or more AI Agents. Responsible for:

- Registering on the **AI Agent Registration App**.
- Completing KYA/KYC with a **Certifier**.
- Managing private keys via the **Secrets Manager**.
- Keeping Verifiable Credentials up to date (renewals, revocations).

### 3.3 Certifier

A real-world organisation (law firm, compliance body, auditor) that:

- Has been approved by the **Super Certifier** as a trusted entity.
- Publishes its own DID on IOTA Tangle.
- Reviews KYA/KYC submissions from AI Agent Companies.
- Issues signed **Verifiable Credentials** to approved companies.
- Is listed in the **Super Certifier's DID Document** on the IOTA Tangle.

### 3.4 Super Certifier

The root of the trust chain. For the MVP this is the IOTA Foundation or a designated body. Responsibilities:

- Approving new Certifiers.
- Publishing the Super Certifier DID Document on IOTA Tangle, which serves as the authoritative list of trusted Certifiers.
- Revoking Certifiers in case of misconduct.

### 3.5 Content Creator / Website Operator

The person or organisation that owns a website whose content should be protected. They:

- Deploy the **Guardian Proxy** in front of their origin server.
- Configure which endpoints to protect and at what price.
- Receive IOTA micropayments directly to their wallet address.

### 3.6 Human Visitor

A regular user browsing via a standard web browser. They receive content with zero additional friction — the Guardian passes them through transparently.

---

## 4. System Architecture

The system is composed of two distinct planes: a **shared infrastructure plane** and a **per-website integration layer**.

### Shared Infrastructure (hosted centrally or self-hosted)

The **Certificate Verifier Service (CVS)** is the trust backbone. It resolves AI agent DIDs from the IOTA Tangle, checks whether the issuing Certifier is listed in the Super Certifier's DID Document, and verifies the ownership of the Verifiable Presentation and the cryptographic signature of the Verifiable Credential.

The **x402 Payment Facilitator** handles the HTTP 402 micropayment lifecycle: issues a payment challenge containing the recipient wallet address, amount, and a nonce; monitors the IOTA Tangle for the expected transfer; and once confirmed, returns a payment receipt to the Guardian. The Guardian then issues a short-lived signed JWT (`X-Guardian-Token`) that it can validate locally on all subsequent requests without further network calls.

The **AI Agent Registration App** is a pure business-logic service: it manages user accounts, registration state, KYA document uploads, and the agent's submitted public key. It has no direct IOTA connection — DID publication is performed by the Certifier Service at approval time.

The **Certifier Service** (Certifier Dashboard + backend) is the authority for agent identity issuance. When approving a registration request it publishes the agent's DID Document to the IOTA Tangle (using the agent's public key submitted during registration) and signs the Verifiable Credential — all in one approval action. The agent's private key never leaves the agent; the Certifier only receives and uses the public key. VC signing is a local cryptographic operation; only DID Document publication requires a Tangle write.

The **Super Certifier Admin** is a minimal backend and web UI dedicated to managing the Super Certifier's DID Document on the IOTA Tangle. It is the only service authorised to publish updates to the trusted certifiers list. It has its own `identity_iota` + `iota-sdk` dependency and Secrets Manager access.

All Rust backend services — and the AI Agent CLI — use a shared **Secrets Manager** crate (`iota-guardian-secrets`) built in two layers. The first layer provides concrete adapters (`FileSecretsBackend`, `AwsKmsSecretsBackend`) implementing the [`secret-storage`](https://github.com/iotaledger/secret-storage) crate's `KeysStorage` trait — a lightweight IOTA-standard abstraction that exposes signing without ever exposing private key bytes. The second layer derives IOTA Identity-compatible storage from those adapters: `GuardianJwkStorage<B>` implements `JwkStorage` by delegating to any `KeysStorage` backend, and `GuardianKeyIdStorage` implements `KeyIdStorage` for the `MethodDigest → KeyId` mapping. Services that call `JwkDocumentExt` (DID operations, VC signing) receive a `GuardianStorage<B>` value; services that only do raw signing (x402, AI Agent CLI) use the `KeysStorage` backend directly.

### IOTA Network Interaction Map

| Service | Tangle operation | SDK |
|---|---|---|
| CVS | DID Document resolution (read), VC verification (read) | `identity_iota`, `iota-sdk` |
| x402 Payment Facilitator | IOTA L1 payment monitoring (read) | `iota-sdk` |
| Certifier Service backend | Agent DID generation (write), VC signing — local crypto | `identity_iota`, `iota-sdk` |
| Super Certifier Admin | Certifier DID generation (write), Super Certifier DID Document publish/update (write) | `identity_iota`, `iota-sdk` |
| AI Agent CLI | VP signing — local crypto (proves DID ownership); IOTA L1 micropayment (send) | `iota-sdk` |
| Content Creator | Receives IOTA L1 payments | external IOTA wallet |

### Per-Website Integration Layer (the Guardian)

The Guardian is the **only piece of software a content creator needs to deploy**. Everything else is consumed as a remote API. Its role is to intercept every inbound request, classify it (human vs. AI agent), and enforce the credential-and-payment gate on protected paths.

Because most content creators run blogs — predominantly WordPress — the Guardian is designed to fit naturally into that world without requiring server access or infrastructure knowledge. It ships in two forms:

- **WordPress Plugin (PHP)** — the primary integration path. Installed directly from the WordPress admin dashboard in under two minutes. No server access, no binary, no DNS change. The plugin intercepts requests via WordPress hooks before content is rendered, and calls the centrally hosted CVS and x402 APIs over HTTPS.
- **Standalone Reverse Proxy (Rust binary)** — for developers, VPS operators, and non-WordPress deployments. Sits in front of any origin server; the origin requires no changes. Can also be composed behind an existing Nginx or Caddy instance by changing a single `proxy_pass` directive.

---

## 5. Services — Detailed Design

### 5.1 Guardian

**Languages**: PHP (WordPress plugin), Rust (standalone reverse proxy)
**Deployment**: WordPress Plugin (primary) | Standalone binary | Docker | Nginx/Caddy upstream

#### Role

The Guardian is the **only component a content creator deploys**. It intercepts every inbound HTTP request on protected paths and enforces the credential-and-payment requirement on AI agents. Human visitors and unidentified traffic pass through with zero overhead.

The Guardian does not do cryptography or blockchain I/O directly — it delegates those responsibilities to the CVS and x402 services, which can be centrally hosted by IOTA Web Guardian and consumed over HTTPS.

#### Protocol Headers

The Guardian uses **two custom headers** for the IOTA Web Guardian protocol:

- `X-Agent-VP` — a signed VP JWT that contains everything the Guardian needs: the agent's DID (`iss` claim), the enclosed Verifiable Credential, and the challenge nonce. No separate DID header is needed — the DID is extracted from the VP JWT.
- `X-Guardian-Token` — a self-contained JWT **issued by the Guardian itself** after both VP+VC verification and payment have been confirmed. The Guardian verifies it locally on subsequent requests — no CVS, x402, or Tangle call needed.

#### Request Handling Logic

Every request on a protected path goes through the same pipeline, regardless of integration mode:

1. **JWT fast path.** The Guardian checks for an `X-Guardian-Token` JWT. If present, it verifies the JWT signature locally using the Guardian's own signing key and checks `exp`. The JWT is self-contained so no CVS or x402 call is needed. A valid token means the agent has been verified and has paid; the request is forwarded immediately.

2. **JA3 / protocol gate.** No valid `X-Guardian-Token`. The Guardian inspects the TLS fingerprint (JA3). A request with a browser-like JA3 and no `X-Agent-VP` header is treated as human traffic and forwarded to the origin unchanged. A request with a non-browser JA3 and no `X-Agent-VP` is a rogue bot — the Guardian returns `HTTP 401` with a challenge nonce. An absent `X-Agent-VP` header alone is not sufficient to classify a request as human, because a bot can omit the header intentionally.

3. **Identity gate.** The Guardian generates a fresh challenge nonce and returns `HTTP 401 Unauthorized`:
   ```
   HTTP/1.1 401 Unauthorized
   Content-Type: application/json
   X-Identify-At: https://registration.iotaguardian.io

   {
     "error": "identity_required",
     "challenge": "uuid:550e8400-e29b-41d4-a716-446655440000",
     "identify_at": "https://registration.iotaguardian.io"
   }
   ```
   The agent constructs a VP JWT embedding this nonce, signs it, and retries with `X-Agent-VP` set.

4. **VP + VC verification.** `X-Agent-VP` is present with a challenge. The Guardian calls `POST /v1/verify` on the CVS passing the VP JWT and the expected challenge nonce. The CVS verifies:
   - **VP signature** — proves the agent controls the private key of its DID.
   - **VC signature** — proves the enclosed credential was issued by a trusted Certifier.
   An invalid result returns `HTTP 403 Forbidden`.

5. **Payment gate.** VP + VC valid, no `X-Guardian-Token`. The Guardian builds the `PAYMENT-REQUIRED` header (x402 v2 `PaymentRequired` JSON, base64-encoded) using nonce C already issued in step 3, and returns `HTTP 402 Payment Required`. Once the agent pays on IOTA L1 (nonce C embedded in tx metadata) and retries with `X-Agent-VP` + `PAYMENT-SIGNATURE: <base64(PaymentPayload)>`, the Guardian calls x402 `POST /v1/verify` to confirm the payment on the Tangle. On confirmation, the **Guardian itself issues** a `X-Guardian-Token` JWT, returns the origin response, and sets `X-Guardian-Token` + `PAYMENT-RESPONSE` (x402 `SettlementResponse`, base64-encoded) response headers.

Once steps 4 and 5 both pass, the request is forwarded to the origin and the response is returned with `X-Guardian-Token` in the headers.

---

#### Integration Mode A — WordPress Plugin (Primary)

For bloggers and content creators on any PHP-based host — including shared hosting. No server access, no binary, no DNS change required.

**Installation**: Search "IOTA Web Guardian" in the WordPress Plugin Directory and click Install.

**Configuration** (WP Admin → Settings → IOTA Web Guardian):
- IOTA wallet address to receive payments
- Price per request (in nano-IOTA)
- URL path patterns to protect (e.g. `/blog/*`, `/?p=*`)
- CVS endpoint URL (defaults to the hosted service)
- x402 endpoint URL (defaults to the hosted service)

**JWT signing**: The plugin uses **HMAC-SHA256** (`firebase/php-jwt`) with a 256-bit secret generated on first activation and stored in `wp-options`. PHP's built-in `openssl_random_pseudo_bytes()` is used for key generation. The risk is scoped to content access control — if the DB is compromised the site is already owned, so storing the HMAC secret there is an acceptable trade-off for shared-hosting compatibility.

**Technical implementation**: Hooks into `template_redirect` (before WordPress renders any content) and `rest_api_init` (REST API routes). On each matching request it runs the pipeline above, issuing `wp_send_json()` 401/402 and calling `exit` before WordPress renders. On success it lets WordPress continue normally.

```php
add_action('template_redirect', 'iota_guardian_intercept', 1);
add_filter('rest_pre_dispatch', 'iota_guardian_intercept_rest', 10, 3);
```

No theme or template modifications required.

**Bot classification in Mode A**: The WordPress Plugin cannot perform JA3 TLS fingerprinting — PHP-level code has no access to TLS handshake data. It instead uses an opt-in model: any request that does not carry an `X-Agent-VP` header is treated as human traffic and passed through to WordPress unchanged. Compliant AI agents always identify themselves with `X-Agent-VP`. Rogue bots that omit the header access content freely in MVP — this is an accepted trade-off identical to the v0.1 scope described in §11.4. A future v0.2 enhancement will add an optional call to a dedicated hosted microservice that classifies traffic based on request patterns, returning a classification signal the plugin can act on.

---

#### Integration Mode B — Standalone Reverse Proxy

For VPS operators, developers, or any non-WordPress deployment. A single Rust binary sits in front of the origin server; the origin requires no changes.

**Configuration** (`guardian.toml`):

```toml
[guardian]
listen = "0.0.0.0:8080"
origin = "http://localhost:3000"

[guardian.protected_paths]
patterns = ["/api/**", "/content/**", "/articles/**"]

[guardian.pricing]
per_request_nano_iota = 1000
recipient_address = "iota1qp..."

[guardian.services]
certificate_verifier_url = "https://cvs.iotaguardian.io"
payment_facilitator_url  = "https://x402.iotaguardian.io"

[guardian.headers]
vp_header            = "X-Agent-VP"          # signed VP JWT (contains DID + VC + challenge)
token_header         = "X-Guardian-Token"    # Guardian-issued session JWT
# PAYMENT-REQUIRED, PAYMENT-SIGNATURE, PAYMENT-RESPONSE are standard x402 v2 headers (not configurable)

[guardian.jwt]
signing_key_ref    = "guardian-jwt-key"   # resolved by Secrets Manager
token_ttl_seconds  = 300                  # 5-minute session window
```

To compose it with an existing Nginx instance, a single directive change suffices:

```nginx
# Before
location / { proxy_pass http://localhost:3000; }

# After
location / { proxy_pass http://localhost:8080; }  # Guardian sits in front
```

#### Key Design Decisions

- **JWT-first**: Every protected request checks `X-Guardian-Token` before any service call. The JWT is Guardian-issued and self-contained (embeds agent DID + payment reference) — no CVS, x402, or Tangle call needed on repeat requests within the 5-minute window.
- **Single VP header**: `X-Agent-VP` contains everything — agent DID (in `iss`), VC, and challenge nonce. No separate `X-Agent-DID` header needed.
- **Guardian issues the JWT**: The session token is signed by the Guardian's own key, not by x402. x402 only verifies the IOTA payment; the Guardian decides to grant access and issues the token.
- **JA3 for human/bot classification**: An absent `X-Agent-VP` header does not mean the request is from a human — a bot can omit it to avoid identification. TLS fingerprinting (JA3) is the reliable signal. Browser-like JA3 → pass through. Non-browser JA3 → enforce protocol.
- **401 before 402**: Identity must be established before payment is requested. No VP header + bot JA3 → `401` with challenge nonce; VP verified but no payment → `402` with x402 v2 `PAYMENT-REQUIRED` header.
- **Delegated crypto**: The Guardian delegates VP+VC verification to CVS and payment Tangle verification to x402. Its only cryptographic operation is signing/verifying its own JWT.
- **Pass-through for unprotected paths**: Paths that don't match the configured patterns are forwarded with zero overhead — human readers are never affected.

#### Guardian JWT Format (`X-Guardian-Token`)

The JWT payload is identical across both integration modes:

```json
{
  "sub": "did:iota:testnet:0x...",   // agent's DID (from VP iss claim)
  "iss": "iota-guardian",        // Guardian is the sole issuer
  "iat": 1700000000,
  "exp": 1700000300,             // 5-minute TTL
  "path": "/content/**",         // path glob covered by this token
  "tx":  "0x..."                 // IOTA L1 payment tx reference
}
```

The signing algorithm differs by mode:

| Mode | Algorithm | Key storage |
|---|---|---|
| WordPress Plugin (Mode A) | HMAC-SHA256 (`HS256`) | 256-bit secret in `wp-options` |
| Standalone Rust Proxy (Mode B) | Ed25519 (`EdDSA`) | `iota-guardian-secrets` (file or AWS KMS) |

Both modes verify the JWT locally with no external service call.

---

### 5.2 Certificate Verifier Service

**Language**: Rust
**Crate deps**: `axum`, `tokio`, `iota-sdk`, `identity_iota`, `serde`
**Deployment**: Local binary | Docker container | AWS ECS / Lambda | Kubernetes pod

#### Role

The CVS serves two read-only purposes:

- **DID resolution**: resolves any DID Document from the IOTA Tangle — used internally for VP/VC verification and by any other service that needs to inspect a DID Document.
- **VP + VC verification**: given a VP JWT and the expected challenge nonce, performs two sequential cryptographic checks:
  - **VP signature** (proof of DID ownership) — resolves the agent's DID Document, verifies the VP JWT signature against the agent's public key, and confirms the challenge nonce matches. This proves the presenter controls the private key of the claimed DID.
  - **VC signature** (proof of certification) — extracts the VC from the VP's `verifiableCredential` claim, resolves the issuing Certifier's DID Document, checks the Certifier is in the trusted registry, verifies the VC cryptographic signature, checks expiry and revocation status.

The CVS performs no write operations. Agent DID generation is owned by the Certifier Service; Certifier DID generation and Super Certifier DID Document updates are owned by the Super Certifier Admin.

#### Endpoints

```
POST /v1/verify   → verify a VP (VP signature + enclosed VC)
GET  /v1/health
```

#### Verification Logic

**Phase A — VP signature (proof of DID ownership)**

1. Decode the VP JWT from the request.
2. Extract the `iss` claim from the VP JWT payload (agent's DID).
3. Resolve the agent's DID Document from the IOTA Tangle.
4. Verify the VP JWT signature against the agent's public key in the DID Document.
5. Verify the `challenge` claim in the VP matches the nonce provided by the Guardian in its `401` response — prevents replay attacks.

**Phase B — VC signature (proof of certification)**

6. Extract the VC JWT from the VP payload's `vp.verifiableCredential[0]` field.
7. Decode the VC JWT and extract the `iss` claim (Certifier's DID).
8. Resolve the Certifier's DID Document from the IOTA Tangle (use in-memory cache if available).
9. Check that the Certifier's DID is in the trusted list resolved from the Super Certifier's DID Document (in-memory cache, refreshed every 10 min from the Tangle).
10. Verify the VC's cryptographic signature against the Certifier's public key.
11. Check `exp` claim — reject if expired.
12. Check revocation status via the VC's `credentialStatus` field (IOTA Revocation Bitmap 2022 or StatusList2021).
13. Verify the VC `sub` claim matches the agent's DID from Phase A step 2.
14. Return `{valid: true, subject_did: "...", certifier_did: "...", expires_at: "..."}`.

#### Trusted Certifiers Cache

At startup (and every 10 minutes), the CVS resolves the Super Certifier's DID Document from the Tangle and builds an in-memory set of trusted Certifier DIDs. Per-request trust checks are then pure in-memory lookups — no network call:

```rust
// At startup / periodic refresh — returns Err if the DID Document is missing or malformed
let super_doc = iota_client.resolve_did(&super_certifier_did).await?;
let trusted_dids: HashSet<String> = super_doc
    .service()
    .iter()
    .find(|s| s.type_() == "TrustedCertifiersRegistry")
    .and_then(|s| parse_certifier_list(s.service_endpoint()))
    .ok_or_else(|| anyhow::anyhow!(
        "Super Certifier DID Document missing or malformed TrustedCertifiersRegistry service"
    ))?;

// Per-request check (in-memory, no I/O)
let is_trusted = trusted_dids.contains(&issuer_did);
```

If the Super Certifier DID Document is missing or cannot be parsed at startup (or during a refresh), the CVS returns an error rather than silently producing an empty trust set. An empty trust set would reject all agents with no indication of misconfiguration.

#### Configuration

```toml
[cvs]
listen = "0.0.0.0:8081"
iota_node_url = "https://api.testnet.iota.cafe"  # IOTA Rebased node
super_certifier_did = "did:iota:testnet:0x..."  # DID of the Super Certifier
trusted_certifiers_refresh_seconds = 600   # refresh interval for in-memory certifier list
```

---

### 5.3 x402 Payment Facilitator

**Language**: Rust
**Crate deps**: `axum`, `tokio`, `iota-sdk`, `serde`, `uuid`, `base64`
**Deployment**: Local binary | Docker | Cloud | On-premise

#### Role

Implements the [x402 v2 protocol](https://github.com/coinbase/x402) payment verification for IOTA L1 native transfers. The facilitator:

1. Verifies that an IOTA L1 transaction exists on the Tangle matching the expected amount, recipient, and nonce.
2. Returns an x402 `SettlementResponse` to the Guardian.

**The x402 Facilitator does NOT issue JWTs and does NOT generate payment challenges.** The Guardian builds the `PAYMENT-REQUIRED` 402 response directly (it knows the payTo address, amount, and already holds the nonce C from the 401 challenge). The facilitator only verifies already-executed on-chain payments.

> **IOTA scheme note**: x402 v2 currently defines `exact` scheme implementations for EVM (EIP-3009 gasless authorization) and SVM networks. IOTA L1 native transfers do not have a gasless authorization primitive, so the agent must execute the transaction first, then prove it via `tx_id`. Our implementation extends x402 with a custom `"iota:rebased"` network entry under the `"exact"` scheme. The `PAYMENT-SIGNATURE` payload field carries `tx_id` + `nonce` instead of an EIP-3009 signature.

#### Flow Detail

```
Guardian          x402 Facilitator          IOTA Tangle         AI Agent
    │                    │                       │                   │
    │  Guardian builds PAYMENT-REQUIRED header   │                   │
    │  (payTo, amount, nonce C — no x402 call)   │                   │
    │                    │                       │                   │
    │ HTTP 402 + PAYMENT-REQUIRED header         │                   │
    │────────────────────────────────────────────────────────────────►
    │                    │                       │                   │
    │                    │                       │  send IOTA tx     │
    │                    │                       │  nonce C in meta  │
    │                    │                       │◄──────────────────│
    │                    │                       │                   │
    │  agent retries: X-Agent-VP + PAYMENT-SIGNATURE header         │
    │◄───────────────────────────────────────────────────────────────│
    │                    │                       │                   │
    │ POST /v1/verify    │                       │                   │
    │ {paymentPayload,   │                       │                   │
    │  paymentRequirements}                      │                   │
    │────────────────────►                       │                   │
    │                    │ verify tx on Tangle   │                   │
    │                    │──────────────────────►│                   │
    │                    │ {tx confirmed}        │                   │
    │                    │◄──────────────────────│                   │
    │ SettlementResponse │                       │                   │
    ◄────────────────────│                       │                   │
    │                    │                       │                   │
    │ Guardian issues X-Guardian-Token JWT       │                   │
    │ forwards origin response + PAYMENT-RESPONSE header            │
    │────────────────────────────────────────────────────────────────►
```

#### 402 Response Format (Guardian-generated)

The Guardian builds this response itself — no x402 call needed. The `PAYMENT-REQUIRED` header carries a base64-encoded `PaymentRequired` JSON object (x402 v2):

```
HTTP/1.1 402 Payment Required
Content-Type: application/json
PAYMENT-REQUIRED: <base64(PaymentRequired JSON)>

{ "error": "payment required", "x402Version": 2 }
```

Decoded `PAYMENT-REQUIRED` header value:

```json
{
  "x402Version": 2,
  "error": "PAYMENT-SIGNATURE header is required",
  "resource": {
    "url": "https://example.com/content/article-123"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "iota:rebased",
      "amount": "1000",
      "asset": "iota",
      "payTo": "iota1qp...",
      "maxTimeoutSeconds": 60,
      "extra": {
        "nonce": "uuid:C",
        "decimals": 9
      }
    }
  ]
}
```

`extra.nonce` is the same nonce C issued in the 401 challenge — one nonce binds identity (VP) and payment together.

#### Payment Payload Format (Agent-generated)

The agent sends a base64-encoded `PaymentPayload` (x402 v2) in the `PAYMENT-SIGNATURE` header on retry:

```json
{
  "x402Version": 2,
  "resource": { "url": "https://example.com/content/article-123" },
  "accepted": {
    "scheme": "exact",
    "network": "iota:rebased",
    "amount": "1000",
    "asset": "iota",
    "payTo": "iota1qp...",
    "maxTimeoutSeconds": 60,
    "extra": { "nonce": "uuid:C", "decimals": 9 }
  },
  "payload": {
    "tx_id": "0xTANGLE_TX_HASH...",
    "nonce": "uuid:C"
  }
}
```

`payload.nonce` must match `accepted.extra.nonce` and the `challenge` claim in the VP JWT — the facilitator verifies all three match.

#### Settlement Response Format (Facilitator → Guardian)

The facilitator returns an x402 v2 `SettlementResponse`. The Guardian maps `transaction` to the `tx` claim in the Guardian JWT:

```json
{
  "success": true,
  "transaction": "0xTANGLE_TX_HASH...",
  "network": "iota:rebased",
  "payer": "iota1q..."
}
```

#### Endpoints

```
POST /v1/verify     → verify IOTA payment (x402 VerifyResponse)
GET  /v1/supported  → list supported schemes/networks (x402 SupportedResponse)
GET  /v1/health
```

`POST /v1/settle` is not implemented — the agent already executed the IOTA transaction before retrying; there is no gasless authorization to settle server-side.

#### Configuration

```toml
[x402]
listen = "0.0.0.0:8082"
iota_node_url = "https://api.testnet.iota.cafe"
payment_confirmation_timeout_seconds = 60
min_confirmations = 1
```

---

### 5.4 Secrets Manager

**Language**: Rust (standalone library crate — `iota-guardian-secrets`)
**Crate deps**: `secret-storage` (iotaledger), `identity_iota`, `aws-sdk-kms`, `serde`, `serde_json`, `tokio`, `zeroize`
**Used by**: CVS, x402, Certifier Service, Super Certifier Admin, **AI Agent CLI**

#### Role

A pluggable cryptographic key management library built in two layers:

1. **Layer 1 — Secret Storage backends**: Concrete implementations of the [`secret-storage`](https://github.com/iotaledger/secret-storage) crate's `KeysStorage` trait. Two backends are provided: a local file backend (dev) and an AWS KMS backend (production/HSM).
2. **Layer 2 — IOTA Identity adapters**: A `JwkStorage` adapter is derived from any `KeysStorage` backend, giving services that use IOTA Identity's `JwkDocumentExt` API (DID Document operations, VC signing) a standards-compliant storage implementation with zero extra key management code. A companion `KeyIdStorage` adapter stores the `MethodDigest → KeyId` mapping required by Identity.

Services that only perform raw signing (x402, AI Agent CLI) use Layer 1 directly. Services that interact with IOTA Identity (CVS, Certifier Service, Super Certifier Admin) receive a `Storage<GuardianJwkStorage<B>, GuardianKeyIdStorage>` value.

#### Layer 1 — `secret-storage` Trait (`KeysStorage`)

The `secret-storage` crate defines the following composable sub-traits (generics omitted for clarity):

```rust
// KeysStorage = KeyGenerate + KeySign + KeyDelete + KeyExist + KeyGet
// (re-exported as a single supertrait for convenience)

pub trait KeyGenerate<K, I> {
    async fn generate_key(&self) -> Result<(I, K::PublicKey)>;
    async fn generate_key_with_options(&self, opts: Options) -> Result<(I, K::PublicKey)>;
}

pub trait KeySign<K: SignatureScheme, I> {
    /// Returns a Signer bound to the key identified by `key_id`.
    /// The private key is NEVER exposed — signing is delegated.
    async fn get_signer(&self, key_id: I) -> Result<impl Signer<K>>;
}

pub trait Signer<K: SignatureScheme> {
    type KeyId;
    async fn sign(&self, data: K::Input) -> Result<K::Signature>;
    async fn public_key(&self) -> Result<K::PublicKey>;
    fn key_id(&self) -> &Self::KeyId;
}

pub trait KeyDelete<I> { async fn delete_key(&self, key_id: I) -> Result<()>; }
pub trait KeyExist<I>  { async fn key_exists(&self, key_id: I) -> Result<bool>; }
pub trait KeyGet<K, I> { async fn get_public_key(&self, key_id: I) -> Result<K::PublicKey>; }
```

#### Backend 1 — File Backend (Local / Dev)

Implements `KeysStorage` using a local JSON file (`~/.iota-guardian/keys.json`). Keys are stored as JWK objects. File path configurable via `GUARDIAN_KEYS_FILE` env var or TOML config.

```json
{
  "x402-signing-key": {
    "kty": "OKP", "crv": "Ed25519",
    "x": "<base64url-public>",
    "d": "<base64url-private>"
  }
}
```

Not recommended for production — no HSM protection.

#### Backend 2 — AWS KMS Backend (Production / HSM)

Implements `KeysStorage` using AWS KMS (`SIGN_VERIFY`, `ECC_NIST_P256` or `KEY_SPEC_ECC_NIST_P384` — Ed25519 via external key import). Private keys **never leave** the HSM. All signing operations are delegated to the KMS API. Supports key aliases (e.g., `alias/iota-guardian-x402`). IAM roles control access.

```toml
[secrets]
backend = "aws-kms"          # or "file"
aws_region = "eu-west-1"
aws_kms_key_alias_prefix = "alias/iota-guardian-"
```

#### Layer 2 — IOTA Identity Adapters

##### `GuardianJwkStorage<B>` — implements `JwkStorage`

Wraps any `B: KeysStorage` and exposes the `JwkStorage` interface required by `identity_iota`'s `JwkDocumentExt`:

```rust
// JwkStorage trait (identity_iota::storage)
pub trait JwkStorage {
    async fn generate(&self, key_type: KeyType, alg: JwsAlgorithm) -> KeyStorageResult<JwkGenOutput>;
    async fn insert(&self, jwk: Jwk)                                -> KeyStorageResult<KeyId>;
    async fn sign(&self, key_id: &KeyId, data: &[u8], public_key: &Jwk)
                                                                    -> KeyStorageResult<Vec<u8>>;
    async fn delete(&self, key_id: &KeyId)                          -> KeyStorageResult<()>;
    async fn exists(&self, key_id: &KeyId)                          -> KeyStorageResult<bool>;
}

pub struct GuardianJwkStorage<B> { backend: Arc<B> }

impl<B: KeysStorage + Send + Sync> JwkStorage for GuardianJwkStorage<B> {
    async fn generate(&self, _key_type: KeyType, _alg: JwsAlgorithm) -> KeyStorageResult<JwkGenOutput> {
        let (key_id, pub_key) = self.backend.generate_key().await?;
        let jwk = ed25519_public_key_to_jwk(&pub_key)?;
        Ok(JwkGenOutput::new(KeyId::new(key_id.to_string()), jwk))
    }

    async fn sign(&self, key_id: &KeyId, data: &[u8], _pub_key: &Jwk) -> KeyStorageResult<Vec<u8>> {
        let signer = self.backend.get_signer(key_id.as_str().to_owned()).await?;
        let sig = signer.sign(data).await?;
        Ok(sig.to_bytes().to_vec())
    }

    async fn delete(&self, key_id: &KeyId) -> KeyStorageResult<()> {
        self.backend.delete_key(key_id.as_str().to_owned()).await?;
        Ok(())
    }

    async fn exists(&self, key_id: &KeyId) -> KeyStorageResult<bool> {
        Ok(self.backend.key_exists(key_id.as_str().to_owned()).await?)
    }

    async fn insert(&self, _jwk: Jwk) -> KeyStorageResult<KeyId> {
        // Key import is not supported: use generate() for new keys.
        Err(KeyStorageError::new(
            KeyStorageErrorKind::Unsupported,
            "GuardianJwkStorage does not support key import via insert(); use generate() instead",
        ))
    }
}
```

##### `GuardianKeyIdStorage` — implements `KeyIdStorage`

Stores the `MethodDigest → KeyId` mapping needed by `identity_iota` to resolve which key backs a given verification method. File-based for MVP; DB-backed in v0.2.

```rust
// KeyIdStorage trait (identity_iota::storage)
pub trait KeyIdStorage {
    async fn insert_key_id(&self, method_digest: MethodDigest, key_id: KeyId) -> KeyIdStorageResult<()>;
    async fn get_key_id(&self, method_digest: &MethodDigest)                  -> KeyIdStorageResult<KeyId>;
    async fn delete_key_id(&self, method_digest: &MethodDigest)               -> KeyIdStorageResult<()>;
}

pub struct GuardianKeyIdStorage { path: PathBuf }   // JSON file: { "<digest_hex>": "<key_id>" }
```

##### Convenience Type Alias

```rust
/// The concrete Storage type used by all IOTA Identity-facing services.
pub type GuardianStorage<B> = identity_iota::storage::Storage<
    GuardianJwkStorage<B>,
    GuardianKeyIdStorage,
>;

// Usage in a service (Certifier Service backend example):
let storage: GuardianStorage<FileSecretsBackend> = GuardianStorage::new(
    GuardianJwkStorage::new(Arc::new(FileSecretsBackend::new(&config.keys_path)?)),
    GuardianKeyIdStorage::new(&config.key_ids_path),
);
// Pass to JwkDocumentExt for DID Document generation and VC signing:
document
    .generate_method(&storage, KeyType::from_str("Ed25519")?, JwsAlgorithm::EdDSA, None, MethodScope::VerificationMethod)
    .await?;
```

#### Key References

Each service references keys by a logical string identifier (e.g., `"certifier-signing-key"`). The backend maps the identifier to a KMS key alias or a JSON file entry. This decouples service config from key management infrastructure.

#### Usage by Service

| Service | Layer used | Storage type |
|---|---|---|
| CVS | None — read-only verification | (no Secrets Manager needed; uses DID Document public keys from Tangle) |
| x402 Facilitator | Layer 1 (raw sign) | `FileBackend \| KmsBackend` directly |
| Certifier Service | Layer 2 (Identity) | `GuardianStorage<FileBackend \| KmsBackend>` |
| Super Certifier Admin | Layer 2 (Identity) | `GuardianStorage<FileBackend \| KmsBackend>` |
| AI Agent CLI | Layer 1 (raw sign) | `FileBackend` (+ `jsonwebtoken` for VP JWT) |

> **Note on CVS**: The CVS resolves DID Documents from the Tangle and verifies cryptographic signatures using the public keys embedded in those documents. It never generates keys or signs data — no local key storage is required.

---

### 5.5 AI Agent Registration App

**Frontend**: SvelteKit
**Backend API**: Rust (axum), **crate deps**: `axum`, `tokio`, `serde`, `sqlx`, `uuid`
**Deployment**: Web app — hosted service or self-hostable Docker image

#### Role

A web application that enables AI Agent Companies to:

1. Create an account and submit company details for KYA, including their agent's **public key**.
2. Complete the challenge-response proof-of-ownership (the Certifier Service issues a nonce; the agent signs it with its private key and submits the signature).
3. Submit a certification request to a Certifier.
4. Track the status of their request (polling via UUID).
5. Download and inspect their issued Verifiable Credential and DID.
6. Configure their AI agent to present credentials correctly.

#### Frontend Screens

Based on the UI design screenshots:

**Screen 0 — AI Agent Registration**
- Company name, legal entity, jurisdiction
- Contact email and website
- Description of AI agent's purpose and scraping behaviour
- Upload: company registration documents (KYA artifacts)
- **Agent public key** (hex/base64) — the agent generates its own keypair locally via the CLI's `setup` command and pastes the public key here; the private key stays on the agent
- Challenge-response step: after submission, the app displays a challenge nonce that the agent must sign (via `iota-guardian-agent register`) and paste back to prove key ownership

**Screen 1 — Certifier's Certification Request Dashboard** *(Certifier view)*
- Table of pending certification requests
- Columns: Company name, DID, Submitted date, Status, Actions
- Actions: View KYA details, Approve (issue VC), Reject (with reason)
- Filter by: status, date range, certifier jurisdiction

**Screen 2 — AI Agent Registration Status Dashboard**
- Shows current status of submitted requests
- UUID-based polling (auto-refresh every 30s)
- Status states: `PENDING` → `UNDER_REVIEW` → `APPROVED` | `REJECTED`
- Timeline view of status transitions
- On approval: Download VC button becomes active

**Screen 3 — AI Agent Verifiable Credential Dashboard**
- View issued VC details (issuer, subject, expiry, claims)
- Download VC in JWT or JSON-LD format
- Integration guide: how to configure the AI agent to send credentials
- Renewal reminder (30 days before expiry)
- Revocation status badge

#### Backend API (Rust)

```
POST /v1/registrations                    → submit registration (company details + public key); returns challenge nonce
GET  /v1/registrations/{uuid}/challenge   → issue a fresh challenge nonce for the agent to sign
POST /v1/registrations/{uuid}/challenge   → submit signed challenge to prove key ownership
GET  /v1/registrations/{uuid}             → poll status
GET  /v1/credentials/{uuid}               → download issued VC + DID
GET  /v1/certifiers                       → list trusted certifiers (from static config)
POST /v1/certifiers/{did}/submit          → submit request to specific certifier
```

#### DID Generation Note

The Registration App does **not** generate DIDs. No IOTA SDK, no Tangle connection, no cryptographic operations occur here. DID generation happens later, inside the Certifier Service, at the moment of approval (see §5.6). The Registration App only stores the submission and relays status back to the company.

#### Trusted Certifiers List

`GET /v1/certifiers` returns the list of trusted certifiers seeded from the Registration App's TOML configuration file at startup. No Tangle query is performed. For MVP, a single certifier entry is sufficient. The list is static for the lifetime of the process.

```toml
[[certifiers]]
did          = "did:iota:testnet:0xCERTIFIER..."
name         = "Acme Compliance Ltd"
jurisdiction = "EU"
```

---

### 5.6 Certifier Service (Dashboard + Backend)

**Frontend**: SvelteKit (routes: `/` dashboard, `/register` self-registration wizard)
**Backend API**: Rust (axum), **crate deps**: `axum`, `tokio`, `identity_iota`, `iota-sdk`, `serde`, `sqlx`, `ed25519-dalek`
**Deployment**: Same hosting as Registration App (multi-tenant, role-based)

#### Role

A dedicated interface for Certifiers to:

1. **Self-register** (route `/register`): generate an Ed25519 keypair in-browser, submit the public key + organisation details, complete a challenge-response to prove key ownership, then wait for Super Certifier Admin approval.
2. Review incoming agent certification requests on the dashboard.
3. Approve requests: generate the agent's DID and issue a signed Verifiable Credential using the Certifier's own private key (loaded from the browser session).
4. Reject requests with a reason.

#### Certifier Key Ownership

Each Certifier generates their own Ed25519 keypair during self-registration. **The private key never leaves the certifier operator's browser** — it is stored only in `sessionStorage` (as a JWK) and never sent to the server. The Certifier Service server only ever receives the public key and — at VC signing time — the ephemeral 32-byte seed used to sign one VC.

- **Key generation**: `crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign'])` in the `/register` wizard.
- **Session persistence**: JWK stored at `sessionStorage['certifierKey']`. Because `/register` and `/` are the same SvelteKit app (same origin), the key is automatically available in the dashboard after registration — no manual import needed.
- **PEM backup**: the private key is downloadable as a PKCS8 PEM file. If the operator starts a new browser session, they reload it via the dashboard sidebar "Load Key" panel (`crypto.subtle.importKey('pkcs8', ...)`).
- **VC signing**: the `certifier_private_key_hex` field (32-byte Ed25519 seed, hex-encoded, derived from the JWK's `d` field) is required in the approve request body. The backend uses it to sign the VC JWT and immediately discards it — it is never persisted.

#### Certifier Self-Registration Flow

```
1. Open http://localhost:5174/register
2. Fill in organisation name and jurisdiction
3. Click "Generate Keypair" — Ed25519 keypair generated in browser
4. Download private key PEM (keep this safe for future sessions)
5. Click "Register" → POST /v1/self-register {name, jurisdiction, public_key_hex}
   Backend: insert certifier (status: challenge_issued), return challenge nonce
   Key stored in sessionStorage['certifierKey']
6. Browser auto-signs the nonce with the in-memory key
7. POST /v1/self-register/:id/challenge {signature_hex}
   Backend: verify Ed25519 signature → status: challenge_passed
8. Wizard shows "Pending admin approval" and polls GET /v1/self-register/:id every 5 s
9. Super Certifier Admin approves → backend publishes certifier DID to IOTA Tangle → status: active
10. Wizard detects active → shows "Approved!" with DID → link to Dashboard
11. Dashboard opens with key already in sessionStorage — ready to issue VCs
```

#### Approval Flow (Backend)

When approving an agent request, the backend:

1. **Publishes the agent's DID Document** to IOTA Tangle (uses the gas wallet, not the certifier's key).
2. **Signs the VC JWT** with the `certifier_private_key_hex` provided in the request body (ephemeral — never stored).

`certifier_private_key_hex` is **required** — the backend returns `400` if absent. There is no server-side key file fallback.

#### Backend API (Self-Registration)

```
POST /v1/self-register                    → register certifier (insert + issue challenge nonce)
POST /v1/self-register/:id/challenge      → verify Ed25519 signature → status: challenge_passed
GET  /v1/self-register/:id               → poll status
```

#### Backend API (Agent Requests)

```
GET  /v1/requests                         → list agent registration requests
GET  /v1/requests/:id                     → request detail (+ VC if approved)
POST /v1/requests/:id/approve             → approve: publish agent DID + issue signed VC JWT
     Body: { "certifier_private_key_hex": "<64 hex chars>" }   ← required
POST /v1/requests/:id/reject              → reject request
GET  /v1/health
```

---

### 5.7 Super Certifier Admin

**Frontend**: SvelteKit — minimal admin panel
**Backend API**: Rust (axum), **crate deps**: `axum`, `tokio`, `identity_iota`, `iota-sdk`, `serde`, `sqlx`
**Deployment**: Internal tool — single Docker container, accessible only to the Super Certifier operator

#### Role

The Super Certifier Admin is a restricted internal tool for managing the **root of trust** of the entire system. Its job is:

1. Reviewing and approving/rejecting certifier applications submitted via the Certifier Service self-registration flow.
2. Publishing and updating the Super Certifier's DID Document on the IOTA Tangle — the authoritative list of trusted Certifiers.

Certifiers **self-register** via the Certifier Service (`/register`). The Super Certifier Admin has no "Add Certifier" form — it only acts on applications that have already passed the challenge-response verification (status: `challenge_passed`).

**Certifier approval flow**: A Certifier Candidate self-registers at the Certifier Service, generates an Ed25519 keypair in the browser, and completes a challenge-response exchange that verifies they own the private key. The Super Certifier Admin then sees the certifier with status `challenge_passed`. On approval, the Super Certifier Admin backend publishes the Certifier's DID Document to the IOTA Tangle and marks the certifier `active`. The Certifier's private key never leaves their browser.

#### Frontend Screens

**Screen: Trusted Certifiers Management**
- Table of certifiers with statuses: `pending`, `challenge_issued`, `challenge_passed`, `active`, `revoked`
- **Approve** / **Reject** action buttons for certifiers in `challenge_passed` state
- On Approve: backend publishes certifier DID to IOTA Tangle → status `active`; DID shown in modal
- On Reject: status → `revoked`
- **Publish DID Document** button — triggers Super Certifier DID Document re-publication to Tangle (includes all `active` certifiers)
- Status indicator: API online/offline

#### Backend API

```
GET  /v1/certifiers                        → list all certifiers
POST /v1/certifiers/:id/approve            → approve certifier: publish DID to Tangle → status: active
POST /v1/certifiers/:id/reject             → reject certifier → status: revoked
POST /v1/publish                           → re-sign and publish Super Certifier DID Document to Tangle
GET  /v1/health
```

#### DID Document Publish Flow (Backend)

```rust
// 1. Load Super Certifier's DID Document (current state from DB)
let mut doc = load_super_certifier_document().await?;

// 2. Update the TrustedCertifiersRegistry service endpoint
let updated_service = build_trusted_certifiers_service(&certifier_list);
doc.remove_service(&service_id);
doc.insert_service(updated_service)?;

// 3. Sign with Super Certifier's key via GuardianStorage (private key bytes never exposed)
let storage: GuardianStorage<FileSecretsBackend> = /* injected via config */;
doc.sign_self(&storage, "#key-1", ProofOptions::default()).await?;

// 4. Publish to IOTA Tangle
iota_client.publish_did_document(&doc).await?;

// 5. Record the publish operation in the audit log
```

`GuardianStorage` delegates all signing to the `secret-storage` `KeysStorage` backend. Private key bytes are never extracted from storage — the backend exposes only an opaque `Signer<K>` that performs signing internally. Services that use `JwkDocumentExt` (DID operations, VC signing) must receive a `GuardianStorage` rather than a raw keypair.

#### Configuration

```toml
[super_certifier_admin]
listen = "0.0.0.0:8083"
iota_node_url = "https://api.testnet.iota.cafe"
super_certifier_did = "did:iota:testnet:0x..."
signing_key_ref = "super-certifier-key"
```

---

### 5.8 AI Agent CLI

**Language**: Rust (binary crate)
**Crate deps**: `clap`, `iota-sdk`, `iota-guardian-secrets`, `jsonwebtoken`, `reqwest`, `tokio`, `serde`, `serde_json`, `tracing`
**Deployment**: Single binary; distributed as a standalone tool for AI Agent operators and hackathon demos

#### Role

A command-line tool that implements the complete AI agent access protocol — keypair generation, DID registration (with challenge-response proof of ownership), Verifiable Presentation (VP) signing, and x402 micropayment — from the perspective of a conforming AI Agent. For the MVP it serves two purposes:

1. **Demo tool** — demonstrates the full end-to-end flow at the hackathon.
2. **Reference implementation** — shows AI Agent operators exactly what cryptographic operations are required to access a Guardian-protected endpoint.

The CLI is the **only place the agent's private key lives**. It uses `iota-guardian-secrets` for key storage. The private key never leaves the CLI — neither the Registration App nor the Certifier Service ever see it.

#### Commands

```
iota-guardian-agent setup
    Generates an Ed25519 keypair locally via the Secrets Manager and writes
    the initial config file. The private key is stored in the local key file
    and never shared. Prints the public key for use during registration.

iota-guardian-agent register --registration-url <url> --vc-path <path>
    Submits a registration request to the Registration App including company
    details and the agent's public key (provided interactively or via flags).
    Handles the challenge-response loop:
      1. Receives a challenge (nonce) from the Certifier Service.
      2. Signs the challenge with the agent's private key (local crypto).
      3. Sends back the signature + public key to prove DID ownership.
    Polls until the Certifier approves, then downloads the issued VC to
    <vc-path>.

iota-guardian-agent fetch <url>
    Performs a credentialed, paid GET request to <url>:
      1. If a cached X-Guardian-Token is valid: sends request with that header
         only — the Guardian verifies it locally and forwards immediately.
      2. Otherwise: sends request without identity headers → receives 401 with
         challenge nonce.
      3. Creates a VP JWT wrapping the stored VC and embedding the challenge
         nonce. Signs with the agent's private key (local crypto, no Tangle I/O).
         Retries with X-Agent-VP: <vp-jwt>.
      4. On HTTP 402: reads PAYMENT-REQUIRED header (base64 PaymentRequired JSON),
         sends the IOTA L1 transfer via iota-sdk (nonce C embedded in tx metadata).
         Retries with X-Agent-VP + PAYMENT-SIGNATURE: <base64(PaymentPayload)>.
      5. Guardian verifies payment via x402 POST /v1/verify, issues X-Guardian-Token
         JWT in the response header (+ PAYMENT-RESPONSE header). CLI caches the
         token (5-minute TTL) and prints the response body to stdout.
      6. Subsequent calls within the TTL use only X-Guardian-Token (step 1).

iota-guardian-agent status
    Shows the current agent config: DID, public key, VC expiry, cached tokens.
```

#### VP Signing Flow (Local Crypto)

The agent signs a Verifiable Presentation using its own private key. This proves to the Guardian/CVS that the sender controls the DID corresponding to the public key in the DID Document on the Tangle. **The private key never leaves the CLI.**

```rust
// 1. Load agent's keypair from own Secrets Manager
let keypair = secrets.get_keypair(&config.agent_key_ref).await?;

// 2. Load stored VC JWT from disk
let vc_jwt: String = std::fs::read_to_string(&config.vc_path)?;

// 3. Build VP payload (W3C-compatible JSON object)
let vp_payload = serde_json::json!({
    "iss": config.agent_did,        // holder DID
    "vp": {
        "@context": ["https://www.w3.org/2018/credentials/v1"],
        "type": ["VerifiablePresentation"],
        "holder": config.agent_did,
        "verifiableCredential": [vc_jwt],
    },
    "challenge": challenge_nonce,
    "iat": now_unix,
    "exp": now_unix + 300,          // 5-minute window
});

// 4. Sign as JWT — local crypto, no Tangle I/O, no identity_iota needed
//    iota-guardian-secrets.sign() returns the Ed25519 signature bytes;
//    jsonwebtoken encodes header + payload + signature into a compact JWT.
let encoding_key = EncodingKey::from_ed_der(&keypair.private_key_der());
let vp_jwt = jsonwebtoken::encode(
    &Header::new(Algorithm::EdDSA),
    &vp_payload,
    &encoding_key,
)?;

// 5. Send vp_jwt as X-Agent-VP header value
```

> **MVP trade-off — private key bytes in CLI**: `jsonwebtoken::EncodingKey::from_ed_der()` requires raw DER-encoded private key bytes. For the MVP file backend, `FileSecretsBackend` exposes the private key bytes in memory to satisfy this requirement. This is acceptable because: (a) the private key is stored locally on the agent's own machine; (b) the file backend is used only by the CLI, never by server-side services. In v0.2, when using the AWS KMS backend, a custom `EncodingKey` wrapper will delegate signing to the KMS API without exposing key material.

#### x402 Payment Flow

```rust
// On receiving HTTP 402: decode PAYMENT-REQUIRED header (base64 → PaymentRequired JSON)
let payment_required_b64 = response.headers()
    .get("PAYMENT-REQUIRED")
    .and_then(|v| v.to_str().ok())
    .expect("x402 PAYMENT-REQUIRED header missing");
let payment_required: PaymentRequired =
    serde_json::from_slice(&base64::decode(payment_required_b64)?)?;
let req = &payment_required.accepts[0]; // pick the iota:rebased option

// Send IOTA L1 transfer; embed nonce C in transaction metadata
let tx_id = iota_client
    .send_transfer(
        &config.wallet_address,
        &req.pay_to,
        req.amount.parse::<u64>()?,
        &req.extra["nonce"].as_str().unwrap(), // nonce C in tx metadata
    )
    .await?;

// Build x402 v2 PaymentPayload and base64-encode it for PAYMENT-SIGNATURE header
let payment_payload = serde_json::json!({
    "x402Version": 2,
    "resource": payment_required.resource,
    "accepted": req,
    "payload": {
        "tx_id": tx_id,
        "nonce": req.extra["nonce"],
    }
});
let payment_sig = base64::encode(serde_json::to_string(&payment_payload)?);

// Retry: Guardian verifies payment via x402 POST /v1/verify,
// issues its own Guardian JWT in X-Guardian-Token response header.
// The CLI does NOT call x402 directly; the Guardian handles verification.
let response = http_client
    .get(&url)
    .header("X-Agent-VP", &vp_jwt)
    .header("PAYMENT-SIGNATURE", &payment_sig)
    .send()
    .await?;

// Cache the Guardian-issued JWT for subsequent requests (5-min TTL)
let guardian_token = response.headers()
    .get("X-Guardian-Token")
    .and_then(|v| v.to_str().ok())
    .map(|s| s.to_owned());
```

#### Configuration

Config is stored at `~/.iota-guardian-agent/config.toml`:

```toml
[agent]
did            = "did:iota:testnet:0x..."
agent_key_ref  = "agent-did-key"          # key reference in Secrets Manager
vc_path        = "~/.iota-guardian-agent/credential.jwt"
wallet_address = "iota1qp..."             # IOTA L1 address for sending payments

[secrets]
backend    = "file"
file_path  = "~/.iota-guardian-agent/keys.json"

[services]
registration_app_url = "https://register.iotaguardian.io"
x402_url             = "https://x402.iotaguardian.io"

[network]
iota_node_url = "https://api.testnet.iota.cafe"
```

---

## 6. Data Models

### 6.1 DID Document (IOTA Identity 2.x)

```json
{
  "@context": [
    "https://www.w3.org/ns/did/v1",
    "https://w3id.org/security/suites/ed25519-2020/v1"
  ],
  "id": "did:iota:testnet:0x1a2b3c...",
  "controller": "did:iota:testnet:0x1a2b3c...",
  "verificationMethod": [
    {
      "id": "did:iota:testnet:0x1a2b3c...#key-1",
      "type": "Ed25519VerificationKey2020",
      "controller": "did:iota:testnet:0x1a2b3c...",
      "publicKeyMultibase": "z6Mkf..."
    }
  ],
  "authentication": ["did:iota:testnet:0x1a2b3c...#key-1"],
  "assertionMethod": ["did:iota:testnet:0x1a2b3c...#key-1"],
  "service": [
    {
      "id": "did:iota:testnet:0x1a2b3c...#agent-info",
      "type": "AIAgentService",
      "serviceEndpoint": "https://mycompany.com/ai-agent"
    }
  ]
}
```

### 6.2 Verifiable Credential

```json
{
  "@context": [
    "https://www.w3.org/2018/credentials/v1",
    "https://iotaguardian.io/contexts/v1"
  ],
  "id": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
  "type": ["VerifiableCredential", "AIAgentCredential"],
  "issuer": "did:iota:testnet:0xCERTIFIER...",
  "issuanceDate": "2024-01-01T00:00:00Z",
  "expirationDate": "2025-01-01T00:00:00Z",
  "credentialSubject": {
    "id": "did:iota:testnet:0xAGENT...",
    "companyName": "Acme AI Corp",
    "legalJurisdiction": "EU",
    "agentType": "web-scraper",
    "certificationLevel": "standard",
    "contactEmail": "compliance@acmeai.com"
  },
  "credentialStatus": {
    "id": "https://certifier.example.com/status/1#42",
    "type": "RevocationBitmap2022"
  },
  "proof": {
    "type": "Ed25519Signature2020",
    "created": "2024-01-01T00:00:00Z",
    "verificationMethod": "did:iota:testnet:0xCERTIFIER...#key-1",
    "proofPurpose": "assertionMethod",
    "proofValue": "z3sByXRKc..."
  }
}
```

### 6.3 Registration Request (Database)

```sql
CREATE TABLE registration_requests (
  uuid             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  status           TEXT NOT NULL DEFAULT 'PENDING',
  -- PENDING | UNDER_REVIEW | APPROVED | REJECTED
  company_name     TEXT NOT NULL,
  legal_entity     TEXT NOT NULL,
  jurisdiction     TEXT NOT NULL,
  contact_email    TEXT NOT NULL,
  website          TEXT,
  agent_type       TEXT NOT NULL,
  agent_public_key TEXT NOT NULL,    -- hex-encoded Ed25519 public key submitted at registration
  challenge_nonce  TEXT,             -- set when challenge is issued; cleared after verification
  agent_did        TEXT,             -- set by Certifier Service at approval time
  certifier_did    TEXT,
  KYA_docs_ref     TEXT,             -- S3 key or file path
  reject_reason    TEXT,
  vc_uuid          UUID,             -- set on approval
  created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

---

## 7. Key Flows

### 7.1 AI Agent Company Onboarding

The AI Agent Company first runs `iota-guardian-agent setup` locally to generate an Ed25519 keypair. The private key is stored in the agent's local Secrets Manager file. The public key is printed for use in the next step.

The company opens the Registration App, fills in their company details (name, legal entity, jurisdiction, contact email, agent type and purpose, KYA documents), and pastes their **public key**. The app stores the submission, assigns a UUID, and returns a **challenge nonce**. The company runs `iota-guardian-agent register` which signs the challenge with the agent's private key and submits the signature back — proving ownership of the private key without ever sharing it. The request status moves to `PENDING`.

The company can poll `GET /v1/registrations/{uuid}` at any time. The status transitions through `PENDING → UNDER_REVIEW → APPROVED | REJECTED`.

Once the Certifier has reviewed the KYA documents and approves the request, the Certifier Service backend builds the agent's DID Document from the submitted public key and publishes it to the IOTA Tangle. It then signs a Verifiable Credential using the Certifier's own private key. The resulting DID and VC are stored and returned to the Registration App. The company downloads the VC and stores it locally via the CLI. The agent's private key was generated at setup — it was never transmitted anywhere.

### 7.2 Certifier Onboarding

A Certifier Candidate opens the **Certifier Service** (`/register`), fills in their organisation details, and generates an Ed25519 keypair in-browser. They download the private key PEM as a backup. The browser submits the public key to `POST /v1/self-register`, which issues a challenge nonce. The browser auto-signs the nonce with the in-memory private key and submits the signature to `POST /v1/self-register/:id/challenge`. The backend verifies the Ed25519 signature (proof of key ownership) and sets status to `challenge_passed`. **The private key never leaves the browser.**

The Super Certifier reviews pending applications in the **Super Certifier Admin UI** (http://localhost:5173). Certifiers with status `challenge_passed` show **Approve** / **Reject** action buttons. On approval, the Super Certifier Admin backend builds the Certifier's DID Document from the registered public key, publishes it to the IOTA Tangle, and marks the certifier `active`. The Certifier's registration wizard detects the `active` status (via polling) and shows the assigned DID.

The Super Certifier operator then clicks **Publish DID Document** in the Admin UI. The backend re-signs and re-publishes the Super Certifier DID Document to the Tangle, embedding all `active` certifier DIDs in the `TrustedCertifiersRegistry` service entry. CVS instances pick up the change within their next refresh cycle (default 10 minutes).

### 7.3 AI Agent Content Access — Happy Path

The agent has completed onboarding (§7.1) and holds a valid `X-Guardian-Token` JWT. It sends a single header:
- `X-Guardian-Token`: the self-contained Guardian-issued JWT

The Guardian verifies the JWT signature locally using its own signing key and checks `exp`. The JWT is self-contained (embeds agent DID, public key, payment tx reference) — no CVS, x402, or Tangle call is needed. The request is forwarded to the origin immediately. This is the expected path for all repeat requests within the 5-minute token window.

### 7.4 AI Agent Content Access — First Request (Bootstrap Flow)

**Step 1 — No Guardian token → 401 with challenge nonce**

The agent has no `X-Guardian-Token`. JA3 fingerprint indicates a non-browser client. The Guardian generates a fresh challenge nonce C and returns:

```
HTTP/1.1 401 Unauthorized
Content-Type: application/json
X-Identify-At: https://registration.iotaguardian.io

{ "error": "identity_required", "challenge": "uuid:C", "identify_at": "..." }
```

**Step 2 — VP submitted → 402 payment challenge**

The agent constructs a VP JWT embedding nonce C, signs it (CLI `fetch` command), and retries with `X-Agent-VP: <vp-jwt>`. The Guardian calls CVS `POST /v1/verify` with the VP + expected nonce C. CVS performs both checks (VP signature → DID ownership; VC signature → certification). On success, the Guardian builds the x402 v2 `PAYMENT-REQUIRED` header (no x402 service call needed — the Guardian already knows payTo, amount, and nonce C) and returns:

```
HTTP/1.1 402 Payment Required
Content-Type: application/json
PAYMENT-REQUIRED: <base64(PaymentRequired JSON with scheme="exact", network="iota:rebased",
                          amount="1000", asset="iota", payTo="iota1qp...",
                          extra.nonce="uuid:C")>
```

Nonce C serves both VP challenge and payment binding — one nonce for both purposes.

**Step 3 — Payment → Guardian JWT issuance**

The agent sends an IOTA L1 transfer (nonce C in tx metadata), then retries with:
- `X-Agent-VP: <same vp-jwt>` (VP challenge C still matches)
- `PAYMENT-SIGNATURE: <base64(PaymentPayload with payload.tx_id + payload.nonce=C)>`

The Guardian calls x402 `POST /v1/verify` with `{ paymentPayload, paymentRequirements }`. x402 verifies the IOTA tx on the Tangle (amount, recipient, nonce C match) and returns a `SettlementResponse`. The Guardian **issues its own JWT** (signed with `guardian-jwt-key`, 5-minute TTL), forwards the request to the origin, and returns the response with `X-Guardian-Token` + `PAYMENT-RESPONSE` (base64 `SettlementResponse`) headers.

The agent caches the token. All subsequent requests within 5 minutes use only `X-Guardian-Token` (fast path, no service calls).

---

## 8. API Specifications

### 8.1 Guardian — Internal Config API

The Guardian exposes a management API (disabled in hardened mode):

```
GET  /admin/config          → current config (redacted secrets)
GET  /admin/stats           → request counts, block counts, revenue
POST /admin/reload          → hot-reload config without restart
GET  /admin/health          → liveness probe
```

### 8.2 Certificate Verifier Service

#### `POST /v1/verify`

Verifies a Verifiable Presentation (which wraps the agent's VC). Performs both VP signature verification (proof of DID ownership) and VC signature verification (proof of certification).

Request:
```json
{
  "verifiable_presentation": "<vp-jwt>",
  "challenge": "uuid:550e8400-e29b-41d4-a716-446655440000"
}
```

`challenge` must match the nonce the Guardian issued in its `401` response. The CVS verifies it is present and unaltered inside the VP JWT payload.

Response (200, valid):
```json
{
  "valid": true,
  "subject_did": "did:iota:testnet:0x...",
  "issuer_did": "did:iota:testnet:0xCERTIFIER...",
  "issuer_name": "Acme Compliance Ltd",
  "certification_level": "standard",
  "expires_at": "2025-01-01T00:00:00Z"
}
```

Response (200, invalid):
```json
{
  "valid": false,
  "reason": "vp_signature_invalid | vc_signature_invalid | challenge_mismatch | revoked | expired | untrusted_issuer | did_mismatch",
  "detail": "..."
}
```

### 8.3 Super Certifier Admin

#### `POST /v1/admin/certifiers`

Creates a new Certifier entry: builds the Certifier's DID Document from the submitted public key, publishes it to the IOTA Tangle, and sets status to `pending_challenge`. The Certifier is not yet trust-eligible — a challenge-response exchange must follow.

Request:
```json
{
  "public_key": "<hex-encoded Ed25519 public key>",
  "name": "Acme Compliance Ltd",
  "jurisdiction": "EU"
}
```

Response:
```json
{
  "did": "did:iota:testnet:0xCERTIFIER...",
  "status": "pending_challenge"
}
```

#### `GET /v1/admin/certifiers/{did}/challenge`

Issues a challenge nonce the Certifier Candidate must sign with their private key to prove ownership.

Response: `{ "nonce": "uuid:..." }`

#### `POST /v1/admin/certifiers/{did}/challenge`

Submits the Certifier Candidate's Ed25519 signature over the challenge nonce. On success, the Certifier status moves to `challenge_passed` and they become eligible for inclusion in the trusted list via `POST /v1/admin/publish`.

Request:
```json
{
  "nonce": "uuid:...",
  "signature": "<hex-encoded Ed25519 signature of the nonce bytes>"
}
```

Response: `{ "status": "challenge_passed" }`

#### `DELETE /v1/admin/certifiers/{did}`

Marks the Certifier as revoked in the pending DID Document update.

Response: `{ "status": "pending_publish" }`

#### `POST /v1/admin/publish`

Re-signs the Super Certifier DID Document with the current trusted certifiers list and publishes it to the IOTA Tangle.

Response:
```json
{
  "did": "did:iota:testnet:0xSUPER_CERTIFIER...",
  "published_at": "2024-01-01T00:00:00Z",
  "certifiers_count": 3
}
```

### 8.4 x402 Payment Facilitator

All requests and responses follow the [x402 v2 specification](https://github.com/coinbase/x402/blob/main/specs/x402-specification-v2.md). The facilitator implements the standard x402 Facilitator Interface with an IOTA-native `exact` scheme.

#### `POST /v1/verify`

Called by the Guardian to verify an IOTA L1 payment on the Tangle. No settlement is performed — the agent already executed the transaction.

Request (x402 v2 format):
```json
{
  "x402Version": 2,
  "paymentPayload": {
    "x402Version": 2,
    "resource": { "url": "https://example.com/content/article-123" },
    "accepted": {
      "scheme": "exact",
      "network": "iota:rebased",
      "amount": "1000",
      "asset": "iota",
      "payTo": "iota1qp...",
      "maxTimeoutSeconds": 60,
      "extra": { "nonce": "uuid:C", "decimals": 9 }
    },
    "payload": {
      "tx_id": "0xTANGLE_TX_HASH...",
      "nonce": "uuid:C"
    }
  },
  "paymentRequirements": {
    "scheme": "exact",
    "network": "iota:rebased",
    "amount": "1000",
    "asset": "iota",
    "payTo": "iota1qp...",
    "maxTimeoutSeconds": 60,
    "extra": { "nonce": "uuid:C", "decimals": 9 }
  }
}
```

Response (200, valid — x402 `SettlementResponse`):
```json
{
  "success": true,
  "transaction": "0xTANGLE_TX_HASH...",
  "network": "iota:rebased",
  "payer": "iota1q..."
}
```

Response (200, invalid — x402 `SettlementResponse`):
```json
{
  "success": false,
  "errorReason": "payment_not_found | insufficient_funds | invalid_nonce | payment_expired",
  "transaction": "",
  "network": "iota:rebased"
}
```

#### `GET /v1/supported`

Returns supported schemes and networks (x402 `SupportedResponse`):

```json
{
  "kinds": [
    { "x402Version": 2, "scheme": "exact", "network": "iota:rebased" }
  ],
  "extensions": []
}
```

### 8.5 Registration App API

#### `POST /v1/registrations`

Request:
```json
{
  "company_name": "Acme AI Corp",
  "legal_entity": "Acme AI Corp GmbH",
  "jurisdiction": "DE",
  "contact_email": "compliance@acmeai.com",
  "website": "https://acmeai.com",
  "agent_type": "web-scraper",
  "agent_description": "...",
  "certifier_did": "did:iota:testnet:0xCERTIFIER...",
  "agent_public_key": "<hex-encoded Ed25519 public key>"
}
```

Response:
```json
{
  "request_uuid": "uuid:...",
  "status": "PENDING",
  "challenge": "uuid:550e8400-e29b-41d4-a716-446655440000",
  "created_at": "2024-01-01T00:00:00Z"
}
```

The `challenge` nonce must be signed by the agent with its private key and submitted via `POST /v1/registrations/{uuid}/challenge` to prove key ownership. The `agent_did` is not assigned at this point — it is generated by the Certifier Service at approval time.

#### `GET /v1/registrations/{uuid}/challenge`

Issues a fresh challenge nonce the agent must sign to prove key ownership.

Response:
```json
{ "nonce": "uuid:550e8400-e29b-41d4-a716-446655440001" }
```

#### `POST /v1/registrations/{uuid}/challenge`

Submits the agent's Ed25519 signature over the nonce to prove key ownership.

Request:
```json
{
  "nonce": "uuid:550e8400-e29b-41d4-a716-446655440001",
  "signature": "<hex-encoded Ed25519 signature of the nonce bytes>"
}
```

Response:
```json
{ "status": "verified" }
```

#### `GET /v1/registrations/{uuid}`

Response:
```json
{
  "uuid": "uuid:...",
  "status": "APPROVED",
  "agent_did": "did:iota:testnet:0x...",
  "vc_download_url": "/v1/credentials/uuid:...",
  "certifier_did": "did:iota:testnet:0xCERTIFIER...",
  "updated_at": "2024-01-02T00:00:00Z"
}
```

---

## 9. Technology Stack

### Backend (Rust)

| Component | Crates |
|---|---|
| HTTP server | `axum`, `hyper`, `tower` |
| Async runtime | `tokio` |
| IOTA SDK | `iota-sdk` (IOTA Rebased) — CVS (read), x402 (read), Certifier Service (write), Super Certifier Admin (write), AI Agent CLI (write) |
| IOTA Identity | `identity_iota` 2.x — CVS, Certifier Service, Super Certifier Admin |
| Secret Storage | `secret-storage` (iotaledger) — `KeysStorage` trait; implemented by both backends inside `iota-guardian-secrets` |
| Secrets Manager crate | `iota-guardian-secrets` — `GuardianJwkStorage` + `GuardianKeyIdStorage` adapters; `FileSecretsBackend` + `AwsKmsSecretsBackend`; used by all services |
| CLI framework | `clap` — AI Agent CLI |
| JWT | `jsonwebtoken` |
| Serialization | `serde`, `serde_json` |
| Database | `sqlx` + PostgreSQL |
| Caching | in-process `HashMap`/`DashMap` for trusted certifier list (CVS only) |
| AWS KMS | `aws-sdk-kms` |
| Config | `config`, `toml` |
| Logging | `tracing`, `tracing-subscriber` |
| Error handling | `anyhow`, `thiserror` |
| Testing | `tokio-test`, `wiremock` |

### Frontend (React / Svelte)

| Concern | Library |
|---|---|
| Framework | SvelteKit (Svelte 5 runes) |
| Styling | Tailwind CSS |
| State | Zustand (React) / Svelte stores |
| HTTP client | `axios` or `fetch` |
| IOTA wallet | `@iota/sdk` JS bindings |
| Forms | React Hook Form / native Svelte |
| Notifications | `react-hot-toast` |

### Infrastructure

| Concern | Technology |
|---|---|
| Containerisation | Docker + Docker Compose |
| Orchestration | Kubernetes (optional, for cloud) |
| Database | PostgreSQL 15 |
| Secrets (prod) | AWS KMS |
| CI/CD | GitHub Actions |
| IOTA Node | IOTA Rebased public node or self-hosted |

---

## 10. Deployment Guide

### 10.1 Deployment Topologies

#### Local / Development

```
docker-compose up
```

Single `docker-compose.yml` starts:
- Guardian Proxy (port 8080)
- Certificate Verifier Service (port 8081)
- x402 Payment Facilitator (port 8082)
- Registration App (port 3000)
- PostgreSQL (port 5432)

All services use the **file-based Secrets Manager** pointing to `./dev-keys.json` (auto-generated on first run).

#### On-Premise / Self-Hosted

Each service is independently deployable as a Docker container. The website operator typically only deploys **Guardian Proxy** (the only mandatory component). CVS and x402 can be shared/hosted centrally.

Recommended on-premise topology:

```
[nginx/traefik]──►[guardian:8080]──►[origin server]
                        │
                        ├──► [cvs:8081]    (local or remote)
                        └──► [x402:8082]   (local or remote)
```

#### Cloud (AWS)

- Guardian: AWS ECS Fargate or EC2
- CVS: AWS Lambda (cold start acceptable for MVP) or ECS
- x402: AWS ECS
- Secrets: AWS KMS
- Database: AWS RDS PostgreSQL
- Registration App frontend: S3 + CloudFront
- Registration App backend: AWS ECS

### 10.2 Guardian Integration Steps (Website Operator)

1. Download and run the Guardian binary:
   ```bash
   curl -sSf https://install.iotaguardian.io | sh
   ```

2. Create `guardian.toml`:
   ```toml
   [guardian]
   listen = "0.0.0.0:8080"
   origin = "http://localhost:3000"

   [guardian.protected_paths]
   patterns = ["/api/**", "/content/**"]

   [guardian.pricing]
   per_request_nano_iota = 1000
   recipient_address = "iota1qp..."

   [guardian.services]
   certificate_verifier_url = "https://cvs.iotaguardian.io"
   payment_facilitator_url  = "https://x402.iotaguardian.io"
   ```

3. Run Guardian:
   ```bash
   iota-guardian --config guardian.toml
   ```

4. Update DNS/load balancer to point to Guardian instead of origin.

Total integration time target: **under 15 minutes**.

---

## 11. Security Considerations

### 11.1 Credential Replay Prevention

`X-Guardian-Token` JWTs are issued by the Guardian itself and include:
- `sub` — agent's DID (scoped to a specific agent)
- `tx` — IOTA payment tx reference (audit trail)
- `path` — path glob covered by this token
- `exp` — 5-minute TTL

The Guardian verifies the JWT signature using its own signing key (no external call). Replay within the TTL is accepted by design (the agent paid for it). Replay across different path scopes is blocked by the `path` claim. After TTL expiry the agent must re-verify identity and re-pay for a new token.

### 11.2 DID Spoofing Prevention

The DID claim inside `X-Agent-VP` is untrusted unless the VP JWT signature is valid. The CVS performs two binding checks:

1. **VP signature** — the VP JWT must be signed with the private key corresponding to the public key in the DID Document on the Tangle. An attacker who claims a DID they don't own cannot produce a valid VP signature. This is the proof of DID ownership.
2. **VC binding** — the VC's `credentialSubject.id` (or `sub` claim) must match the DID from the VP. The VC signature must be valid and the Certifier must be in the trusted registry. This proves the DID was certified by a real-world KYC/KYB auditor.

An attacker cannot spoof a DID without controlling the private key, and cannot fabricate a VC without compromising a trusted Certifier.

### 11.3 Payment Proof Forgery Prevention

Session tokens (`X-Guardian-Token`) are Ed25519-signed by the Guardian itself using its own private key (`guardian-jwt-key`). Verification is a local signature check against the Guardian's own public key — no external service call needed. The token includes the `tx_id` of the on-chain payment as an audit trail. An attacker cannot forge a token without the Guardian's private signing key.

### 11.4 Traffic Classification

The Guardian uses a header-driven, opt-in model — no User-Agent whitelist is maintained:

- **Compliant AI agents** include `X-Agent-VP`. They go through the full identity + payment pipeline.
- **Regular traffic** (human browsers) is identified by a browser-like JA3 TLS fingerprint and no `X-Agent-VP`. The Guardian passes this traffic through transparently on protected paths.
- **Rogue scraping** (bots that ignore the protocol) receive no special treatment in MVP — they access content freely, but without identity or accountability. Rate-limiting and TLS fingerprint-based blocking (JA3) are planned for v0.2 to address non-compliant bots.

### 11.5 Key Security

- Production deployments MUST use the AWS KMS backend.
- File-based backend keys MUST be stored on encrypted volumes.
- Certifier private keys MUST reside in an HSM — never on disk in production.
- All inter-service communication MUST use TLS in production.

### 11.6 DID Document Integrity

DID Documents anchored on the IOTA Tangle are immutable once published (controlled by the DID controller key). Rotation is supported via IOTA Identity key agreement methods. Revocation of entire agent DID is possible via the IOTA Identity controller.

---

## 12. Multi-Agent Support

### 12.1 Overview

Multiple independent AI agents can run from the same machine using **named profiles**. Each profile is a self-contained agent identity — its own Ed25519 keypair, DID, and Verifiable Credential — stored in a dedicated directory.

The Guardian, CVS, and x402 are stateless and concurrent-safe — no changes needed on those services.

### 12.2 Profile Directory Layout

```
~/.iota-guardian-agent/
  profiles/
    default/            ← same data as the old flat layout
      config.json
      keys.json
      credential.vc.jwt
    acme-bot/           ← second agent
      config.json
      keys.json
      credential.vc.jwt
    beta-corp/          ← third agent
      config.json
      keys.json
      credential.vc.jwt
```

**Isolation**: Each profile's token cache, keypair, and VC are fully independent. Parallel `fetch` invocations with different profiles share no in-process state.

**Migration**: On first run, if `~/.iota-guardian-agent/config.json` exists and `profiles/default/` does not, the CLI auto-migrates the flat files to `profiles/default/`.

### 12.3 `--profile` Global Flag

```
iota-guardian-agent [--profile <name>] <command>
```

| Flag | Default | Scope |
|---|---|---|
| `--profile` / `-p` | `"default"` | global (applies to all subcommands) |

### 12.4 Demo Scenario

```bash
# Agent 1
iota-guardian-agent --profile acme-bot setup
iota-guardian-agent --profile acme-bot register --ra-url http://localhost:8085
# (approve in certifier dashboard)

# Agent 2
iota-guardian-agent --profile beta-corp setup
iota-guardian-agent --profile beta-corp register --ra-url http://localhost:8085
# (approve in certifier dashboard)

# Parallel fetch — both agents hit the same WordPress site
iota-guardian-agent --profile acme-bot  fetch http://localhost:8090/the-future-of-ai-content-licensing/ &
iota-guardian-agent --profile beta-corp fetch http://localhost:8090/getting-started-with-iota-identity/ &
wait
# Both return 200; each uses its own credentials and payment
```

---

## 13. MVP Scope & Roadmap

### MVP (v0.1) — Hackathon Deliverables

| Component | Status | Notes |
|---|---|---|
| Guardian (WordPress Plugin + Rust Proxy) | JWT fast path; 401 identity gate; 402 payment gate | WP plugin (primary); standalone proxy (secondary) |
| Certificate Verifier Service (Rust) | VP + VC signature verification, DID resolution | REST API |
| x402 Payment Facilitator (Rust) | 402 challenge + IOTA L1 tx verification | REST API |
| Secrets Manager (Rust lib) | file backend only | KMS in v0.2 |
| AI Agent Registration App | registration + KYB + status polling + VC download | SvelteKit + Rust API |
| Certifier Service | request review + agent DID generation + VC issuance | SvelteKit + Rust API |
| Super Certifier Admin | certifier onboarding + DID Document publish | SvelteKit + Rust API |
| AI Agent CLI (Rust) | setup + register + fetch + multi-profile | `identity_iota` + `iota-sdk` |
| WordPress Admin Dashboard | real-time agent activity, Chart.js graphs, transaction history | PHP plugin |
| Docker Compose | all services + WordPress bundled | one-command startup |

### v0.2 — Post-Hackathon

- Agent SDK libraries: TypeScript (`@iota-guardian/js-sdk`) and Python (`pip install iota-guardian`)
- KMS/Vault backend for secrets (replace dev key files)
- TLS, rate limiting per agent DID, persistent nonce store (Redis)
- Multi-certifier governance via DID controller multi-sig
- VC expiration policies and renewal flow
- Platform plugins: Nginx, Caddy, Drupal
- WP plugin: per-page pricing, agent blocklist, revenue reports, webhooks

### v1.0 — Production

- SOC2/GDPR compliance, SLA 99.9%, horizontal scaling, multi-region (EU/US/APAC)
- Guardian as WASM for edge runtimes (Cloudflare Workers, Fastly, Deno Deploy)
- VC revocation, selective disclosure, cross-chain DID resolution
- KYB integration, jurisdiction-aware compliance (EU AI Act, US state laws)
- Content licensing marketplace, transparent pricing discovery