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.
A grant is a signed permission that allows a builder to access specific scopes of a user’s data. Grants are the access-control primitive of the Data Portability Protocol — no builder can read data without one, and every grant can be revoked by the user at any time.
How grants work
- A builder requests access to specific scopes (e.g.
instagram.profile, youtube.watch_history)
- The user sees the request and approves or denies it in the Desktop App
- On approval, the Personal Server signs an EIP-712 typed data structure and submits it to the Gateway
- The Gateway returns a
grantId (the onchain permissionId) and syncs it to the chain asynchronously
- The builder uses the
grantId to make signed requests to the user’s Personal Server
Grants use EIP-712 typed data signatures for cryptographic verifiability.
const grantTypedData = {
domain: {
name: "Vana Data Portability",
version: "1",
chainId: 14800,
verifyingContract: "0x...", // DataPortabilityPermissions address
},
types: {
Grant: [
{ name: "user", type: "address" },
{ name: "builder", type: "address" },
{ name: "scopes", type: "string[]" },
{ name: "expiresAt", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
},
primaryType: "Grant",
message: {
user: "0x...", // User's wallet address
builder: "0x...", // Builder's registered address
scopes: ["instagram.profile", "instagram.likes"],
expiresAt: 0, // 0 = no expiration
nonce: 1, // Replay protection
},
};
| Field | Description |
|---|
user | The user granting access (wallet address) |
builder | The builder receiving access (registered address) |
scopes | Array of scope identifiers the grant covers |
expiresAt | Unix timestamp for expiration; 0 means the grant never expires |
nonce | Monotonically increasing value per user, prevents replay |
The grantId returned by the Gateway corresponds to the onchain permissionId in the DataPortabilityPermissions contract.
Grant lifecycle
create → active → revoked
→ expired
Create
The user approves a grant request in the Desktop App. The Personal Server:
- Calls
POST /v1/grants on itself with { granteeAddress, scopes, expiresAt?, nonce? }
- Signs the EIP-712 typed data with the user’s wallet
- Submits the signed grant to the Gateway (
POST /v1/grants)
- The Gateway validates the signature, stores the grant, and returns a
grantId
- The grant is synced to the
DataPortabilityPermissions contract asynchronously
Active
While active, the builder can read data for the granted scopes by including the grantId in Web3Signed requests to the Personal Server.
Revoked
The user can revoke a grant at any time from the Desktop App. Revocation:
- The user signs a revocation
- The Desktop App submits
DELETE /v1/grants/{grantId} to the Gateway
- The Gateway marks the grant as revoked immediately
- The revocation is synced to the chain asynchronously
- The Personal Server blocks all future requests using this
grantId
Revocations take effect immediately at the Gateway level, even before onchain confirmation.
Expired
If expiresAt is set and the current time exceeds it, the grant is expired. The Personal Server rejects requests with expired grants.
Verification
When a builder makes a data request, the Personal Server verifies the grant before serving data:
- Signature valid — The grant signature matches the claimed user address
- Not revoked — The grant has not been revoked (checked via Gateway or chain)
- Not expired —
expiresAt is 0 or in the future
- Scope match — The requested scope is within the granted scopes
- Signer match — The request’s
Authorization header recovers to the builder address that matches the grant’s grantee
If any check fails, the Personal Server returns an error:
| Code | Meaning |
|---|
401 | Invalid signature or unauthorized |
403 | Valid auth but not permitted |
410 | Grant revoked |
411 | Grant expired |
412 | Scope not granted |
Connect data flow
The “Connect Data” flow is how users grant a builder access through a web popup and deep link to the Desktop App. It uses the Session Relay service to coordinate between the builder’s web app and the user’s Desktop App.
Flow overview
Builder web app Session Relay Desktop App
│ │ │
│ 1. POST /v1/session/init │ │
│ ─────────────────────────────────>│ │
│ { granteeAddress, scopes } │ │
│ │ │
│ 2. sessionId + deepLinkUrl │ │
│ <─────────────────────────────────│ │
│ │ │
│ 3. Display deep link to user │ │
│ ·································│·······················>│
│ │ │
│ │ 4. POST /claim │
│ │ <─────────────────────│
│ │ { sessionId, secret }│
│ │ │
│ │ 5. Session details │
│ │ ─────────────────────>│
│ │ │
│ │ │ 6. User approves
│ │ │
│ │ 7. POST /approve │
│ │ <─────────────────────│
│ │ { grantId, scopes } │
│ │ │
│ 8. Poll returns grant │ │
│ <─────────────────────────────────│ │
│ { grantId, userAddress } │ │
Session states
pending → claimed → approved
→ expired (15 minutes)
→ denied
Session relay API
| Method | Path | Description |
|---|
POST | /v1/session/init | Create a session (builder-signed) |
GET | /v1/session/{sessionId}/poll | Poll for session completion |
POST | /v1/session/claim | Claim a session (Desktop App) |
POST | /v1/session/{sessionId}/approve | Approve with grant details |
POST | /v1/session/{sessionId}/deny | Deny the request |
The POST /v1/session/init request must include an Authorization: Web3Signed header signed by the builder.
Deep links
The Desktop App receives session details via a deep link:
vana://connect?sessionId={sessionId}&secret={secret}
Grant payload
When the builder polls and the session is approved, it receives:
{
"grantId": "0x...",
"userAddress": "0x...",
"builderAddress": "0x...",
"scopes": ["instagram.profile", "instagram.likes"],
"expiresAt": 0,
"app_user_id": "optional"
}
The builder uses grantId to make data requests and userAddress to resolve the Personal Server URL via the Gateway.
Onchain contract
Grants are stored onchain in the DataPortabilityPermissions contract.
Address: 0xD54523048AdD05b4d734aFaE7C68324Ebb7373eF (Moksha Testnet)
Data structures
struct PermissionInfo {
uint256 id;
address grantor;
uint256 nonce;
uint256 granteeId;
string grant; // Serialized grant data
uint256 startBlock;
uint256 endBlock; // 0 = active, >0 = revoked at block
uint256[] fileIds;
}
Contract functions
// Create a grant (called by Gateway with user signature)
function addPermission(
PermissionInput calldata permission,
bytes calldata signature
) external returns (uint256);
// Revoke a grant
function revokePermission(uint256 permissionId) external;
// Revoke with signature (via Gateway)
function revokePermissionWithSignature(
RevokePermissionInput calldata input,
bytes calldata signature
) external;
// Read grant details
function permissions(uint256 permissionId)
external view returns (PermissionInfo memory);
// Get file IDs associated with a grant
function permissionFileIds(uint256 permissionId)
external view returns (uint256[] memory);
Gateway grant API
The Data Portability RPC (Gateway) provides fast API access to grant operations with eventual chain consistency.
| Method | Path | Description |
|---|
POST | /v1/grants | Create a grant |
DELETE | /v1/grants/{grantId} | Revoke a grant |
GET | /v1/grants/{grantId} | Get grant details |
GET | /v1/grants?user={address} | List grants for a user |
GET | /v1/grants?builder={address} | List grants for a builder |
GET | /v1/grants/{grantId}/status | Get onchain confirmation status |
Nonces
Nonces prevent replay attacks. Before creating a grant, query the current nonce:
GET /v1/nonces?user={address}&operation=grant
Builder SDK
The @opendatalabs/connect SDK provides helpers for the Connect Data flow and data access. See Integrate Vana for the full integration guide.
import { createSessionRelay, createDataClient } from "@opendatalabs/connect/server";
// Create a session for the Connect Data flow
const relay = createSessionRelay({
privateKey: process.env.VANA_APP_PRIVATE_KEY,
granteeAddress: "0x...",
sessionRelayUrl: "https://session-relay.vana.org",
});
const session = await relay.initSession({
scopes: ["instagram.profile"],
});
// After user approves, read their data
const result = await relay.pollUntilComplete(session.sessionId);
const data = createDataClient({
privateKey: process.env.VANA_APP_PRIVATE_KEY,
gatewayUrl: "https://gateway.vana.org",
});
const serverUrl = await data.resolveServerUrl(result.grant.userAddress);
const profile = await data.fetchData({
serverUrl,
scope: "instagram.profile",
grantId: result.grant.grantId,
});