> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vana.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Full specification

> The complete normative Data Portability Protocol specification. Version 1.0.0.

This is the complete normative specification for the Vana Data Portability Protocol. It is the authoritative reference for implementers. For a guided introduction, see [Personal servers](/protocol-reference/personal-servers) and the other Protocol Reference pages. For focused topics, see the other [Protocol Reference](/protocol-reference/personal-servers) pages.

***

## 1. Introduction

The Data Portability Protocol enables users to:

* Collect personal data from various platforms
* Store data under their control
* Grant third-party applications access to specific data scopes
* Revoke access at any time
* Maintain auditable records of all data access

### Design principles

| Principle                 | Description                                                                 |
| ------------------------- | --------------------------------------------------------------------------- |
| **User sovereignty**      | The user controls their data and who can access it                          |
| **Local-first**           | Data is stored on the user's device by default                              |
| **Protocol-native**       | Grants and data registry entries live onchain for verifiability             |
| **Encryption by default** | Data is encrypted before upload; TLS in transit                             |
| **Extensibility**         | New data sources and storage backends can be added without protocol changes |

***

## 2. Terminology

### Protocol entities

| Term                        | Definition                                                                                                                    |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **User**                    | A human who owns data and controls access. Identified by wallet address.                                                      |
| **Personal Server**         | Protocol-recognized environment that stores user data and responds to access requests. Registered onchain.                    |
| **Builder**                 | Third-party application requesting data access. Registered onchain with public key and app URL.                               |
| **DP RPC**                  | Service providing fast API access with eventual chain consistency. Single-sequencer, bounded facilitator (not decentralised). |
| **Data Portability Client** | Software enabling protocol interaction (e.g. Data Connect desktop app). NOT a protocol participant.                           |
| **Passport**                | Client-chosen UX layer for wallet authentication. Non-protocol component.                                                     |
| **Storage Backend**         | Service storing encrypted data blobs (Vana Storage, Google Drive, Dropbox, IPFS).                                             |

### Protocol objects

| Term                         | Definition                                                                      |
| ---------------------------- | ------------------------------------------------------------------------------- |
| **Data File**                | Blob containing user data for a specific scope. Immutable after write.          |
| **DataRegistry File Record** | Onchain registry entry: fileId, URL, schemaId, permissions.                     |
| **Grant**                    | Signed permission allowing a builder to access specific data scopes.            |
| **Scope**                    | Hierarchical identifier for a data type (e.g. `instagram.profile`).             |
| **Data Connector**           | Module that extracts data from a specific platform. Not part of the protocol.   |
| **Schema Registry**          | Onchain registry (`DataRefinerRegistry`) mapping schemaId to schema definition. |

### Cryptographic primitives

| Primitive                 | Specification                                                                       |
| ------------------------- | ----------------------------------------------------------------------------------- |
| **Grant signature**       | EIP-712 typed data signature proving user consent                                   |
| **Master key material**   | Raw signature bytes from EIP-191 `personal_sign` over `"vana-master-key-v1"`        |
| **Scope key**             | `HKDF-SHA256(master_key_material, "vana", "scope:{scope}")` — 32 bytes              |
| **Data encryption**       | OpenPGP password-based encryption; password = `hex(scope_key)` (64-char hex string) |
| **Request authorization** | EIP-191 signature over canonicalized JSON payload (`Web3Signed` scheme)             |

***

## 3. Protocol model

### Architecture layers

```
┌─────────────────────────────────────────────────────────┐
│  Apps Layer                                             │
│  Builder apps (e.g. Flipboard), Vana Trace              │
└────────────────────────┬────────────────────────────────┘
                         │ grants / signed requests
┌────────────────────────▼────────────────────────────────┐
│  Protocol Layer                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐ │
│  │Personal Server│  │Storage Backend│  │    DP RPC     │ │
│  └──────────────┘  └──────────────┘  └───────────────┘ │
│                                       ┌───────────────┐ │
│                                       │   Vana L1     │ │
│                                       └───────────────┘ │
└────────────────────────┬────────────────────────────────┘
                         │ data collection
┌────────────────────────▼────────────────────────────────┐
│  Client Layer                                           │
│  Desktop App (Passport, Bundled Server, Connectors)     │
└─────────────────────────────────────────────────────────┘
```

### Key distinction

* **Desktop App**: NOT a protocol participant. Does NOT register onchain. Controls the Personal Server.
* **Personal Server**: IS a protocol participant. MUST register onchain. Can act unattended.

Multiple Desktop Apps can control the same Personal Server.

***

## 4. Protocol components

### 4.1 Personal server

**Purpose:** Stores user data in plaintext, responds to authorized data requests, maintains access logs, operates unattended.

**Registration:** MUST be registered onchain via `DataPortabilityServers`. Uses EIP-712 signature-based operations.

#### Hosting options

| Option          | URL format                                      | Operator      | Data visibility       |
| --------------- | ----------------------------------------------- | ------------- | --------------------- |
| ODL Cloud       | `https://server.vana.com/u/{userId}`            | Vana          | Encrypted at rest     |
| Self-hosted     | `https://server.alice.com`                      | User          | User's choice         |
| Desktop-bundled | Local (tunneled via `{userId}.server.vana.org`) | User's device | May store unencrypted |

#### Implementation targets

| Target          | Runtime                           | Activation                  | Availability               |
| --------------- | --------------------------------- | --------------------------- | -------------------------- |
| Desktop-bundled | Embedded in Tauri app             | User opens app              | While app running          |
| ODL Cloud       | Firecracker MicroVM (Sprites.dev) | HTTP request auto-activates | Always (\~1-2s cold start) |
| Self-hosted     | Docker container                  | Always running              | User manages               |

The Personal Server does NOT require the user's wallet private key — only the master-key signature for key derivation.

#### API

**Data endpoints:**

| Method   | Path                        | Description                                                          |
| -------- | --------------------------- | -------------------------------------------------------------------- |
| `POST`   | `/v1/data/{scope}`          | Create data document                                                 |
| `GET`    | `/v1/data`                  | List scopes and metadata. Query: `?scopePrefix`, `?limit`, `?offset` |
| `GET`    | `/v1/data/{scope}`          | Read data. Query: `?fileId`, `?at` (ISO 8601)                        |
| `GET`    | `/v1/data/{scope}/versions` | List versions (metadata only)                                        |
| `DELETE` | `/v1/data/{scope}`          | Delete data (user-only)                                              |

**Grant endpoints:**

| Method | Path                | Description                                                          |
| ------ | ------------------- | -------------------------------------------------------------------- |
| `GET`  | `/v1/grants`        | List grants                                                          |
| `POST` | `/v1/grants`        | Create grant. Body: `{ granteeAddress, scopes, expiresAt?, nonce? }` |
| `POST` | `/v1/grants/verify` | Verify grant signature                                               |

**Other endpoints:**

| Method | Path              | Description        |
| ------ | ----------------- | ------------------ |
| `GET`  | `/v1/access-logs` | Access log history |
| `GET`  | `/health`         | Health check       |

**Sync endpoints (internal):**

| Method | Path                     | Description        |
| ------ | ------------------------ | ------------------ |
| `POST` | `/v1/sync/trigger`       | Force sync         |
| `GET`  | `/v1/sync/status`        | Sync status        |
| `POST` | `/v1/sync/file/{fileId}` | Sync specific file |

#### Write flow (`POST /v1/data/{scope}`)

1. Look up `schemaId` for scope via DP RPC
2. Validate request body against schema
3. Generate `collectedAt` timestamp (UTC)
4. Construct data file envelope
5. Store locally: `~/.vana/data/{scope}/{collectedAt}.json`
6. Return `201 Created`
7. Async: encrypt → upload to storage backend → register in DataRegistry

#### Authentication (builder requests)

All builder-initiated requests MUST include:

```
Authorization: Web3Signed <base64url(json)>.<signature>
```

**Payload fields (keys sorted alphabetically):**

| Field      | Type   | Required       | Description                          |
| ---------- | ------ | -------------- | ------------------------------------ |
| `aud`      | string | Yes            | Personal Server origin               |
| `bodyHash` | string | Yes            | Hash of request body (empty for GET) |
| `exp`      | number | Yes            | Expiration (Unix seconds)            |
| `grantId`  | string | For data reads | Onchain `permissionId`               |
| `iat`      | number | Yes            | Issued-at (Unix seconds)             |
| `method`   | string | Yes            | HTTP method                          |
| `uri`      | string | Yes            | Request path and query               |

**Signing:** JSON canonicalized (keys sorted) → UTF-8 → base64url (no padding). Signature is EIP-191 over ASCII bytes of base64url string.

**Verification:** Personal Server recovers signer, verifies registered builder, validates `aud`/`method`/`uri`/timestamps, checks grant validity for data reads.

#### Access control

| Endpoint                                                                | Access                     |
| ----------------------------------------------------------------------- | -------------------------- |
| `POST /v1/data/{scope}`                                                 | User only                  |
| `GET /v1/data`, `GET /v1/data/{scope}`, `GET /v1/data/{scope}/versions` | User or builder with grant |
| `DELETE /v1/data/{scope}`                                               | User only                  |
| `POST /v1/grants`, `GET /v1/grants`                                     | User only                  |
| `GET /v1/access-logs`                                                   | User only                  |

#### MCP server

Personal Server includes an MCP server for AI assistant integration.

**Resources:** `vana://files`, `vana://file/{scope}`, `vana://file/{scope}/metadata`, `vana://grants`, `vana://schemas`, `vana://schema/{schemaId}`

**Tools:** `list_files`, `get_file`, `search_files` — all require EIP-191 signature verification.

#### Tunneling

Desktop-bundled servers use Vana-managed FRP (Fast Reverse Proxy) for internet accessibility:

* FRP server: `proxy.server.vana.org`
* User URL: `https://{userId}.server.vana.org`
* Wildcard DNS with Let's Encrypt TLS
* Tunnel starts on app open, terminates on close

#### Local data hierarchy

```
~/.vana/
├── data/{scope}/{YYYY-MM-DDTHH-mm-ssZ}.json    # Decrypted user data
├── logs/access-{YYYY-MM-DD}.log                  # Access logs (JSON lines)
├── index.db                                       # Local registry index (SQLite)
└── server.json                                    # Server configuration
```

***

### 4.2 DP RPC

**Endpoint:** `https://dp-rpc.vana.org`

**Purpose:** Provides fast API access to protocol operations with eventual chain consistency. Operated as a single sequencer / bounded facilitator (not decentralised); trust is bounded by the user signatures it carries and by onchain verifiability.

**Trust model:**

| Mode                            | Latency        | Trust             |
| ------------------------------- | -------------- | ----------------- |
| DP RPC only                     | \~50ms         | Trust Vana        |
| DP RPC + signature verification | \~50ms         | Trust user signed |
| DP RPC + spot-check chain       | \~50ms + async | Trust but verify  |
| Chain only                      | \~5-10s        | Trustless         |

#### DP RPC API

**Server operations:**

| Method | Path                           | Description                     |
| ------ | ------------------------------ | ------------------------------- |
| `POST` | `/v1/servers`                  | Register/update Personal Server |
| `GET`  | `/v1/servers/{address}`        | Get server info                 |
| `GET`  | `/v1/servers/{address}/status` | Confirmation status             |

**Grant operations:**

| Method   | Path                           | Description         |
| -------- | ------------------------------ | ------------------- |
| `POST`   | `/v1/grants`                   | Create grant        |
| `DELETE` | `/v1/grants/{grantId}`         | Revoke grant        |
| `GET`    | `/v1/grants/{grantId}`         | Get grant details   |
| `GET`    | `/v1/grants?user={address}`    | List user grants    |
| `GET`    | `/v1/grants?builder={address}` | List builder grants |
| `GET`    | `/v1/grants/{grantId}/status`  | Confirmation status |

**File operations:**

| Method | Path                                       | Description                                |
| ------ | ------------------------------------------ | ------------------------------------------ |
| `POST` | `/v1/files`                                | Register file record (`schemaId` required) |
| `GET`  | `/v1/files/{fileId}`                       | Get file record                            |
| `GET`  | `/v1/files?user={address}`                 | List user files                            |
| `GET`  | `/v1/files?user={address}&since={ISO8601}` | Files since timestamp                      |
| `GET`  | `/v1/files/{fileId}/status`                | Confirmation status                        |

**Schema operations:**

| Method | Path                        | Description               |
| ------ | --------------------------- | ------------------------- |
| `GET`  | `/v1/schemas/{schemaId}`    | Get schema metadata       |
| `GET`  | `/v1/schemas?scope={scope}` | Look up schemaId by scope |

**Builder operations:**

| Method | Path                            | Description         |
| ------ | ------------------------------- | ------------------- |
| `POST` | `/v1/builders`                  | Register builder    |
| `GET`  | `/v1/builders/{address}`        | Get builder info    |
| `GET`  | `/v1/builders/{address}/status` | Confirmation status |

**Utility:**

| Method | Path                                       | Description            |
| ------ | ------------------------------------------ | ---------------------- |
| `GET`  | `/v1/sync/status`                          | Chain sync status      |
| `GET`  | `/v1/nonces?user={address}&operation={op}` | Current and next nonce |

#### Request format

```
POST /v1/files
Authorization: Signature 0xabc123...def
Content-Type: application/json

{
  "url": "https://storage.vana.com/alice/encrypted/instagram-profile.enc",
  "schemaId": 7,
  "nonce": 42
}
```

#### Response format

```json theme={null}
{
  "data": {
    "fileId": "0x...",
    "url": "https://storage.vana.com/...",
    "schemaId": 7
  },
  "proof": {
    "userSignature": "0x...",
    "gatewaySignature": "0x...",
    "timestamp": 1737500000,
    "status": "pending",
    "estimatedConfirmation": "30s",
    "chainBlockHeight": null
  }
}
```

#### ID computation

The DP RPC assigns deterministically computed `bytes32` IDs:

```
fileId    = keccak256(abi.encode(domainSeparator, ownerAddress, url, schemaId))
serverId  = keccak256(abi.encode(domainSeparator, serverAddress, publicKey, serverUrl))
builderId = keccak256(abi.encode(domainSeparator, owner, granteeAddress, publicKey))
grantId   = keccak256(abi.encode(domainSeparator, granteeId, scopes, expiresAt, nonce))
```

***

### 4.3 Vana L1 (onchain contracts)

#### DataPortabilityServers

**Address:** `0x1483B1F634DBA75AeaE60da7f01A679aabd5ee2c` (Moksha Testnet)

Manages Personal Server registration and trust relationships.

```solidity theme={null}
struct ServerInfo {
    uint256 id;
    address owner;
    address serverAddress;
    string publicKey;
    string url;
}

struct TrustedServerInfo {
    uint256 id;
    address owner;
    address serverAddress;
    string publicKey;
    string url;
    uint256 startBlock;
    uint256 endBlock;
}

function addServerWithSignature(AddServerWithSignatureInput input, bytes signature) external;
function addAndTrustServerWithSignature(AddServerWithSignatureInput input, bytes signature) external;
function addAndTrustServerByManager(address ownerAddress, AddServerInput input) external;
function updateServer(uint256 serverId, string memory url) external;
function trustServer(uint256 serverId) external;
function trustServerWithSignature(TrustServerInput input, bytes signature) external;
function trustServerByManager(address userAddress, uint256 serverId) external;
function untrustServer(uint256 serverId) external;
function untrustServerWithSignature(UntrustServerInput input, bytes signature) external;
function servers(uint256 serverId) external view returns (ServerInfo memory);
function serverByAddress(address serverAddress) external view returns (ServerInfo memory);
function userServerValues(address userAddress) external view returns (TrustedServerInfo[] memory);
function userServers(address userAddress, uint256 serverId) external view returns (TrustedServerInfo memory);
function userNonce(address user) external view returns (uint256);
```

#### DataPortabilityGrantees

**Address:** `0x8325C0A0948483EdA023A1A2Fd895e62C5131234` (Moksha Testnet)

Manages builder registration.

```solidity theme={null}
struct GranteeInfo {
    address owner;
    address granteeAddress;
    string publicKey;
    string appUrl;
    uint256[] permissionIds;
}

function registerGrantee(address owner, address granteeAddress, string memory publicKey, string memory appUrl) external returns (uint256);
function grantees(uint256 granteeId) external view returns (GranteeInfo memory);
function granteeByAddress(address granteeAddress) external view returns (GranteeInfo memory);
function granteeAddressToId(address granteeAddress) external view returns (uint256);
```

#### DataPortabilityPermissions

**Address:** `0xD54523048AdD05b4d734aFaE7C68324Ebb7373eF` (Moksha Testnet)

Manages grant creation and revocation. Grants are **scope-native (V2)**: a grant carries a list of scopes and an expiry, not an enumeration of file IDs.

```solidity theme={null}
event PermissionSet(
    bytes32 id,
    address grantorAddress,
    bytes32 granteeId,
    string[] scopes,
    uint256 grantVersion,
    uint256 expiresAt
);

struct PermissionInfo {
    bytes32 id;
    address grantor;
    uint256 nonce;
    bytes32 granteeId;
    string[] scopes;
    uint256 grantVersion;
    uint256 expiresAt;
}

function addPermission(PermissionInput calldata permission, bytes calldata signature) external returns (bytes32);
function revokePermission(bytes32 permissionId) external;
function revokePermissionWithSignature(RevokePermissionInput calldata input, bytes calldata signature) external;
function permissions(bytes32 permissionId) external view returns (PermissionInfo memory);
```

The previous file-based permission contract (enumerating `fileIds`, with a `permissionFileIds` accessor) remains deployed on mainnet during migration, but is legacy — new grants MUST use the scope-native model above.

#### DataRegistry

**Address:** `0x8C8788f98385F6ba1adD4234e551ABba0f82Cb7C` (Moksha Testnet)

Stores immutable file records.

```solidity theme={null}
struct FileResponse {
    uint256 id;
    address ownerAddress;
    string url;
    uint256 schemaId;
    uint256 addedAtBlock;
}

function addFileWithSchema(string memory url, uint256 schemaId) external returns (uint256);
function addFileWithPermissionsAndSchema(string memory url, address ownerAddress, Permission[] memory permissions, uint256 schemaId) external returns (uint256);
function files(uint256 index) external view returns (FileResponse memory);
function fileIdByUrl(string memory url) external view returns (uint256);
```

Clients MUST register files with `schemaId`. Omitting schemaId is invalid for protocol compliance.

***

### 4.4 Builder access (Direct flow)

Builders integrate via the **Direct flow**: a browser-based approval plus backend polling. There is no builder-facing deep link or session-relay hop.

#### Steps

1. **Create request.** The builder backend calls `createAccessRequest({ scopes, ... })` and receives `{ requestId, approvalUrl, appAddress }`.
2. **User approves.** The user opens `approvalUrl` in a browser tab, reviews the requested scopes, and signs the EIP-712 grant. The signed grant is submitted onchain via DP RPC.
3. **Poll status.** The builder backend polls `getAccessRequestStatus(requestId)` until `status` is `approved` (or `denied`/`expired`). On approval the response is `{ status, personalServerUrl, grantId, scope }`.
4. **Read data.** The builder reads the approved data from the user's Personal Server at `personalServerUrl`, paying the protocol fee from escrow (see §4.5).

See [Build a Vana app](/build-a-vana-app) for the full integration walkthrough.

#### Status values

```
pending → approved
        → denied
        → expired
```

<Note>
  Desktop-bundled Personal Servers are reached over a Vana-managed relay/tunnel for internet reachability (FRP, `https://{userId}.server.vana.org`). This is infrastructure for Personal Server reachability only — it is distinct from, and not a substitute for, the deprecated builder session-relay flow.
</Note>

***

### 4.5 Fees & escrow

Protocol fees are collected through a per-account **escrow** contract. The Personal Server never holds funds — it only gates a read on the fee showing paid.

#### Escrow

Each account maintains a deposited balance that the protocol settles against on fee-bearing operations.

```solidity theme={null}
event Deposited(address from, address account, address asset, uint256 amount);
event Settled(address from, address to, address asset, uint256 amount, bytes32 ref);
event Withdrawn(address account, address asset, uint256 amount, bytes32 ref);
```

* **Deposit** — an account funds its escrow balance.
* **Settle** — the protocol moves funds from a payer to a payee against a referenced operation (`ref`).
* **Withdraw** — an account withdraws its unspent balance.

The fee **asset** is environment-dependent and MUST be read from the contract configuration — do not assume a fixed asset.

#### FeeRegistry

Fee amounts are held onchain in a **FeeRegistry**. Implementers MUST read the current amount onchain rather than hard-coding it. Two fee types exist:

| Fee type             | When charged        |
| -------------------- | ------------------- |
| `grant_registration` | One-time, per grant |
| `data_access`        | Per read            |

#### Paid read (`X-PAYMENT` challenge/retry)

A fee-bearing read uses a challenge/retry handshake:

1. Builder issues the read; if the fee is unpaid, the Personal Server responds with an `X-PAYMENT` challenge.
2. The SDK settles the fee from escrow and retries the request with an `X-PAYMENT` header carrying the payment receipt.
3. The Personal Server verifies the fee is settled, then returns the data. The read is gated on the fee showing paid.

The payment receipt shape is:

```json theme={null}
{
  "opType": "data_access",
  "opId": "0x...",
  "amount": "...",
  "asset": "0x...",
  "paidAt": 1737500000
}
```

***

## 5. Data formats

### 5.1 Scope taxonomy

```
{source}.{category}[.{subcategory}]
```

Source is the first segment. Schema definitions MUST encode the canonical scope.

### 5.2 Data file format

JSON envelope (v1):

```json theme={null}
{
  "$schema": "https://ipfs.io/<cid>",
  "version": "1.0",
  "scope": "instagram.profile",
  "collectedAt": "2026-01-21T10:00:00Z",
  "data": { ... }
}
```

The entire plaintext JSON is encrypted as a single OpenPGP blob before storage. No plaintext metadata alongside ciphertext.

### 5.3 Grant format (EIP-712)

```typescript theme={null}
{
  domain: {
    name: "Vana Data Portability",
    version: "2",
    chainId: 14800,
    verifyingContract: "0x...", // DataPortabilityPermissions (V2)
  },
  types: {
    Grant: [
      { name: "grantor", type: "address" },
      { name: "granteeId", type: "bytes32" },
      { name: "scopes", type: "string[]" },
      { name: "expiresAt", type: "uint256" },
      { name: "nonce", type: "uint256" },
    ],
  },
  primaryType: "Grant",
  message: {
    grantor: "0x...",   // User's wallet address
    granteeId: "0x...", // Builder's grantee id
    scopes: ["instagram.profile"],
    expiresAt: 0,
    nonce: 1,
  },
}
```

### 5.4 Access log format

```json theme={null}
{
  "logId": "uuid",
  "grantId": "0x...",
  "builder": "0x...",
  "action": "read",
  "scope": "instagram.profile",
  "timestamp": "2026-01-21T10:00:00Z",
  "ipAddress": "1.2.3.4",
  "userAgent": "BuilderSDK/1.0"
}
```

### 5.5 App manifest

W3C Web App Manifest with custom `vana` block. Discoverable via `<link rel="manifest">` at the builder's `appUrl`.

**Required `vana` fields:** `appUrl`, `privacyPolicyUrl`, `termsUrl`, `supportUrl`, `webhookUrl`, `signature`.

**Signature:** EIP-191 by builder address over canonical JSON of `vana` block (keys sorted, `signature` excluded).

**Verification:** Desktop App MUST verify manifest origin, `appUrl` match, and signature recovery before rendering consent.

### 5.6 Data connector metadata

```json theme={null}
{
  "connectorId": "instagram",
  "displayName": "Instagram",
  "scopes": [
    { "scope": "instagram.profile", "label": "Your Instagram profile", "description": "..." }
  ],
  "version": "1.0"
}
```

Data Connectors are NOT part of the protocol — they are implementation details of specific clients.

***

## 6. Protocol operations

### 6.1 User registration

1. User opens Desktop App
2. Redirect to Passport (identity provider)
3. Authenticate (social/email)
4. Wallet created or retrieved
5. Setup complete

### 6.2 Data collection

1. User clicks "Connect {platform}" in Desktop App
2. Embedded browser opens; user logs in
3. Data Connector scrapes data (user's IP, user's browser session)
4. Raw data sent to Personal Server via `POST /v1/data/{scope}`
5. Personal Server stores locally, then async: encrypt → upload → register

If no storage backend is selected, only local storage occurs.

### 6.3 Connect data flow (Direct flow)

1. User clicks "Connect data" on builder's web app
2. Builder backend calls `createAccessRequest({ scopes, ... })` → `{ requestId, approvalUrl, appAddress }`
3. User opens `approvalUrl` in a browser tab, reviews scopes, verifies the app manifest
4. User approves; the EIP-712 grant is signed and submitted onchain via DP RPC
5. Builder backend polls `getAccessRequestStatus(requestId)` until `approved` → `{ status, personalServerUrl, grantId, scope }`
6. Builder reads approved data from the Personal Server at `personalServerUrl`, paying the `data_access` fee from escrow (see §4.5)

### 6.4 Grant revocation

1. User clicks "Revoke" in Desktop App
2. Signs revocation
3. Submit `DELETE /v1/grants/{grantId}` to DP RPC
4. DP RPC marks revoked immediately; async chain sync
5. Personal Server blocks future requests
6. Builder receives `410 Grant revoked` on next request

### 6.5 Data deletion

DataRegistry entries are immutable. Deletion is implemented as:

1. User requests deletion in Desktop App
2. Personal Server deletes encrypted blob from storage backend
3. Local decrypted copy removed
4. Tombstone written via DP RPC
5. Other Personal Servers treat tombstoned records as non-existent (`410` or `404`)

***

## 7. Security

### 7.1 Encryption

* All user data MUST be encrypted with OpenPGP password-based encryption before writing to storage backends
* Password is `hex(scope_key)` where scope key is derived per section 2
* Personal Servers serve decrypted data to authorized builders over TLS
* Vana MUST NOT have access to plaintext data

### 7.2 Authentication

* Onchain operations MUST be signed using EIP-712 typed data
* Builder requests to Personal Servers MUST include `Authorization: Web3Signed` (EIP-191)
* Nonces MUST prevent replay attacks for onchain operations

### 7.3 Authorization

Personal Server MUST verify before serving data:

* Signature valid
* Grant not revoked
* Grant not expired
* Requested scope within granted scopes
* Authorization signer matches onchain grantee

### 7.4 Transport

* All HTTP endpoints MUST use TLS 1.3
* Personal Servers SHOULD implement certificate pinning
* DP RPC SHOULD implement rate limiting

### 7.5 Threat model

| Threat                        | Mitigation                                        |
| ----------------------------- | ------------------------------------------------- |
| Vana sees user data           | Data encrypted before upload; Vana has no key     |
| Builder exceeds granted scope | Personal Server validates scope on each request   |
| Grant replay                  | Nonces and timestamps in grant signature          |
| Malicious builder             | User must explicitly approve; can revoke anytime  |
| DP RPC lies about grants      | Grants include user signature; verifiable onchain |
| Personal Server compromised   | Storage backend data remains encrypted            |

***

## 8. Error handling

### Error code structure

Following SMTP convention, the protocol uses a 3-digit error code system:

| First digit | Meaning                               |
| ----------- | ------------------------------------- |
| `2xx`       | Success                               |
| `3xx`       | Intermediate (more input needed)      |
| `4xx`       | Temporary failure (retry may succeed) |
| `5xx`       | Permanent failure (do not retry)      |

| Second digit | Category                     |
| ------------ | ---------------------------- |
| `x0x`        | Syntax/format                |
| `x1x`        | Authentication/authorization |
| `x2x`        | Data/storage                 |
| `x3x`        | Grant/permission             |
| `x4x`        | Protocol/network             |
| `x5x`        | Rate limiting                |

### Error codes

| Code  | Description                              |
| ----- | ---------------------------------------- |
| `200` | Success                                  |
| `201` | Created                                  |
| `301` | Redirect to Personal Server              |
| `400` | Bad request (syntax error)               |
| `401` | Unauthorized (invalid signature)         |
| `403` | Forbidden (valid auth but not permitted) |
| `404` | Not found                                |
| `410` | Grant revoked                            |
| `411` | Grant expired                            |
| `412` | Scope not granted                        |
| `420` | Data not found at registry entry         |
| `421` | Storage backend unavailable              |
| `429` | Rate limited                             |
| `440` | Chain sync pending                       |
| `500` | Internal server error                    |
| `503` | Service unavailable                      |

### Error response format

```json theme={null}
{
  "error": {
    "code": 412,
    "message": "Scope not granted",
    "details": {
      "requestedScope": "instagram.messages",
      "grantedScopes": ["instagram.profile", "instagram.likes"]
    }
  }
}
```

***

## 9. Extensibility

### Adding data sources

1. Define scope taxonomy (`{source}.{category}`)
2. Create JSON Schema for data structure
3. Register schema in `DataRefinerRegistry`
4. Build a Data Connector (optional — not protocol)

### Adding storage backends

Implement the `StorageBackend` interface:

```typescript theme={null}
interface StorageBackend {
  upload(key: string, data: Uint8Array): Promise<string>;
  download(url: string): Promise<Uint8Array>;
  delete(url: string): Promise<boolean>;
  exists(url: string): Promise<boolean>;
}
```

### Adding capabilities

Personal Servers declare capabilities in registration:

```
capabilities: ["storage", "compute", "mcp"]
```

New capabilities can be defined without protocol changes.

***

## 10. Builder SDK

Published as `@opendatalabs/vana-sdk` on NPM. Builders use the **Direct flow** (§4.4): a server-side controller creates and polls access requests, and a React hook drives the browser approval.

### Server-side usage

```typescript theme={null}
import { createDirectDataController } from "@opendatalabs/vana-sdk/server";
import { createEscrowGatewayClient } from "@opendatalabs/vana-sdk/node";

const controller = createDirectDataController({
  privateKey: process.env.VANA_APP_PRIVATE_KEY,
  dpRpcUrl: "https://dp-rpc.vana.org",
  escrow: createEscrowGatewayClient({ /* fee payment from escrow */ }),
});

// 1. Create an access request
const { requestId, approvalUrl, appAddress } = await controller.createAccessRequest({
  scopes: ["instagram.profile"],
});

// 2. Poll until the user approves in the browser
const status = await controller.getAccessRequestStatus(requestId);
// -> { status: "approved", personalServerUrl, grantId, scope }

// 3. Read approved data (fee paid from escrow via X-PAYMENT retry)
const profile = await controller.readApprovedData({
  requestId,
  scope: "instagram.profile",
});
```

### React client usage

```typescript theme={null}
import { useDirectVanaConnect } from "@opendatalabs/vana-sdk/react";

// Hook wired to your backend routes (createRequest / getStatus / readResult)
const { connect, status, result, reset } = useDirectVanaConnect({
  createRequest: "/api/vana/create-request",
  getStatus: "/api/vana/get-status",
  readResult: "/api/vana/read-result",
});
```

See [Build a Vana app](/build-a-vana-app) for the full example.

***

## Appendix A: SMTP analogy

| SMTP concept              | Data Portability equivalent          |
| ------------------------- | ------------------------------------ |
| RFC 5321                  | This specification                   |
| Mail User Agent (MUA)     | Data Portability Client              |
| Mail Transfer Agent (MTA) | Personal Server                      |
| Mail Delivery Agent (MDA) | Storage Backend                      |
| SMTP Server               | DP RPC                               |
| Email address             | Wallet address + Personal Server URL |
| Email message             | Data File                            |
| Mailbox                   | Scope                                |
| SMTP EHLO                 | Server registration                  |
| SMTP MAIL FROM            | Grant creation                       |
| SMTP RCPT TO              | Builder address                      |
| SMTP DATA                 | Data file upload                     |
| SMTP QUIT                 | Grant revocation                     |
| Spam filter               | Grant approval                       |
| Bounce message            | Error response                       |

## Appendix B: ODL Cloud reference architecture

### Sprites.dev integration

ODL Cloud uses Sprites.dev for per-user stateful MicroVMs (Firecracker):

* Hardware-level isolation per user
* Stateful storage persists between activations
* HTTP auto-activation wakes sleeping VMs
* Pay-per-use billing; scales to zero when idle
* Up to 8 CPU, 16 GB RAM per Sprite

### Cold start flow

1. Builder calls `GET https://user-abc.server.vana.com/data`
2. Sprites edge detects inactive Sprite → assigns compute (\~300ms)
3. Sprite boots with persisted filesystem
4. Personal Server starts, decrypts data using delegated signature
5. Request proxied to port 8080, response returned
6. After idle timeout → Sprite sleeps (billing stops, data persists)

**Total cold start latency:** \~1-2 seconds.

### Delegated signature lifecycle

| Event                   | Action                                                                                |
| ----------------------- | ------------------------------------------------------------------------------------- |
| User enables ODL Cloud  | Sprite provisioned; user signs `"vana-master-key-v1"`, signature encrypted and stored |
| Sprite activates        | Signature decrypted in-memory; master key derived; data decrypted                     |
| User disables ODL Cloud | Sprite deleted; data remains in storage backend                                       |

The delegated signature never expires. Revocation = disabling ODL Cloud.

### Cost estimates

| Usage pattern                 | Monthly cost per user |
| ----------------------------- | --------------------- |
| Light (few requests/month)    | \~\$0.10-0.50         |
| Medium (daily builder access) | \~\$0.50-2.00         |
| Heavy (continuous access)     | \~\$2.00-10.00        |
