Skip to main content
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

  1. A builder requests access to specific scopes (e.g. instagram.profile, youtube.watch_history)
  2. The user sees the request and approves or denies it in the Desktop App
  3. On approval, the Personal Server signs an EIP-712 typed data structure and submits it to the Gateway
  4. The Gateway returns a grantId (the onchain permissionId) and syncs it to the chain asynchronously
  5. The builder uses the grantId to make signed requests to the user’s Personal Server

Grant format (EIP-712)

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
  },
};
FieldDescription
userThe user granting access (wallet address)
builderThe builder receiving access (registered address)
scopesArray of scope identifiers the grant covers
expiresAtUnix timestamp for expiration; 0 means the grant never expires
nonceMonotonically 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:
  1. Calls POST /v1/grants on itself with { granteeAddress, scopes, expiresAt?, nonce? }
  2. Signs the EIP-712 typed data with the user’s wallet
  3. Submits the signed grant to the Gateway (POST /v1/grants)
  4. The Gateway validates the signature, stores the grant, and returns a grantId
  5. 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:
  1. The user signs a revocation
  2. The Desktop App submits DELETE /v1/grants/{grantId} to the Gateway
  3. The Gateway marks the grant as revoked immediately
  4. The revocation is synced to the chain asynchronously
  5. 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:
  1. Signature valid — The grant signature matches the claimed user address
  2. Not revoked — The grant has not been revoked (checked via Gateway or chain)
  3. Not expiredexpiresAt is 0 or in the future
  4. Scope match — The requested scope is within the granted scopes
  5. 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:
CodeMeaning
401Invalid signature or unauthorized
403Valid auth but not permitted
410Grant revoked
411Grant expired
412Scope 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

MethodPathDescription
POST/v1/session/initCreate a session (builder-signed)
GET/v1/session/{sessionId}/pollPoll for session completion
POST/v1/session/claimClaim a session (Desktop App)
POST/v1/session/{sessionId}/approveApprove with grant details
POST/v1/session/{sessionId}/denyDeny the request
The POST /v1/session/init request must include an Authorization: Web3Signed header signed by the builder. 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.
MethodPathDescription
POST/v1/grantsCreate 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}/statusGet 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,
});