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 and the other Protocol Reference pages. For focused topics, see the other Protocol Reference 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})
- Look up
schemaId for scope via DP RPC
- Validate request body against schema
- Generate
collectedAt timestamp (UTC)
- Construct data file envelope
- Store locally:
~/.vana/data/{scope}/{collectedAt}.json
- Return
201 Created
- 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 |
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
}
{
"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.
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.
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.
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.
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
- Create request. The builder backend calls
createAccessRequest({ scopes, ... }) and receives { requestId, approvalUrl, appAddress }.
- 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.
- Poll status. The builder backend polls
getAccessRequestStatus(requestId) until status is approved (or denied/expired). On approval the response is { status, personalServerUrl, grantId, scope }.
- 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 for the full integration walkthrough.
Status values
pending → approved
→ denied
→ expired
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.
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.
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:
- Builder issues the read; if the fee is unpaid, the Personal Server responds with an
X-PAYMENT challenge.
- The SDK settles the fee from escrow and retries the request with an
X-PAYMENT header carrying the payment receipt.
- 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:
{
"opType": "data_access",
"opId": "0x...",
"amount": "...",
"asset": "0x...",
"paidAt": 1737500000
}
5.1 Scope taxonomy
{source}.{category}[.{subcategory}]
Source is the first segment. Schema definitions MUST encode the canonical scope.
JSON envelope (v1):
{
"$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.
{
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,
},
}
{
"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.
{
"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
- User opens Desktop App
- Redirect to Passport (identity provider)
- Authenticate (social/email)
- Wallet created or retrieved
- Setup complete
6.2 Data collection
- User clicks “Connect ” in Desktop App
- Embedded browser opens; user logs in
- Data Connector scrapes data (user’s IP, user’s browser session)
- Raw data sent to Personal Server via
POST /v1/data/{scope}
- 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)
- User clicks “Connect data” on builder’s web app
- Builder backend calls
createAccessRequest({ scopes, ... }) → { requestId, approvalUrl, appAddress }
- User opens
approvalUrl in a browser tab, reviews scopes, verifies the app manifest
- User approves; the EIP-712 grant is signed and submitted onchain via DP RPC
- Builder backend polls
getAccessRequestStatus(requestId) until approved → { status, personalServerUrl, grantId, scope }
- Builder reads approved data from the Personal Server at
personalServerUrl, paying the data_access fee from escrow (see §4.5)
6.4 Grant revocation
- User clicks “Revoke” in Desktop App
- Signs revocation
- Submit
DELETE /v1/grants/{grantId} to DP RPC
- DP RPC marks revoked immediately; async chain sync
- Personal Server blocks future requests
- Builder receives
410 Grant revoked on next request
6.5 Data deletion
DataRegistry entries are immutable. Deletion is implemented as:
- User requests deletion in Desktop App
- Personal Server deletes encrypted blob from storage backend
- Local decrypted copy removed
- Tombstone written via DP RPC
- 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": {
"code": 412,
"message": "Scope not granted",
"details": {
"requestedScope": "instagram.messages",
"grantedScopes": ["instagram.profile", "instagram.likes"]
}
}
}
9. Extensibility
Adding data sources
- Define scope taxonomy (
{source}.{category})
- Create JSON Schema for data structure
- Register schema in
DataRefinerRegistry
- Build a Data Connector (optional — not protocol)
Adding storage backends
Implement the StorageBackend interface:
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
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
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 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
- Builder calls
GET https://user-abc.server.vana.com/data
- Sprites edge detects inactive Sprite → assigns compute (~300ms)
- Sprite boots with persisted filesystem
- Personal Server starts, decrypts data using delegated signature
- Request proxied to port 8080, response returned
- 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 |