Skip to content
Prividium

Building Web Applications

Build Web3 applications on Prividium™ chains using the prividium SDK.

Examples are shown for both viem and ethers.js v6. Use the tabs to switch between libraries. The Prividium™ SDK API (createPrividiumChain, prividium.authorize, prividium.authorizeTransaction, etc.) is identical in both — only the web3 library calls differ.

Table of Contents

  1. Application Configuration
  2. Prividium™ SDK Initialization
  3. Authentication and OAuth Scopes
  4. Session Management
  5. RPC URL Architecture
  6. Browser Extension Wallets
  7. Reading from Contracts
  8. Sending Transactions
  9. Error Handling
  10. Summary
  11. Next Steps

Application Configuration

Register a client in the Prividium™ Admin Panel before building your web application. This client authenticates users and authorizes access to your application.

1. Register OAuth Client

  1. Access Admin Panel: Open the Prividium™ Admin Panel.
  2. Create Application: Register a new client. Save the OAuth Client Id displayed after creation as your CLIENT_ID.
  3. Configure CORS: Add your application's origin URL to the allowed CORS origins.
  4. Set Redirect URL: Set the callback URL (e.g., https://your-app.com/callback) for post-auth redirection.

2. Configure Application URLs

Configure these URLs in your application:

  • RPC URL: The base proxy RPC endpoint (e.g., https://proxy.prividium.dev/rpc)
  • Prividium™ API URL: The Prividium™ API base URL (e.g., https://api.prividium.dev)
  • Auth Base URL: The authentication service URL (e.g., https://user-panel.prividium.dev)
  • Callback URL: Your application's callback route (matches the redirect URL in the OAuth client)

Prividium™ SDK Initialization

Use the prividium SDK to initialize and connect to your chain:

Requirements:

  • Chain ID: The chain ID of your Prividium™ network
  • Chain Metadata: Display name, native currency details, and block explorer URL (if applicable)

Initialize the Prividium™ SDK using createPrividiumChain():

prividium.ts
// src/lib/prividium.js
import { createPrividiumChain } from 'prividium';
import { createPublicClient, defineChain } from 'viem';

const PRIVIDIUM_API_BASE_URL = process.env.VITE_PRIVIDIUM_API_BASE_URL ?? 'http://localhost:8000';
const PRIVIDIUM_CLIENT_ID = process.env.VITE_PRIVIDIUM_CLIENT_ID;
const AUTH_BASE_URL = process.env.VITE_AUTH_BASE_URL ?? 'http://localhost:3001';
const AUTH_CALLBACK_URL = process.env.VITE_AUTH_CALLBACK_URL ?? `${window.location.origin}/callback`;
const CHAIN_ID = Number(process.env.VITE_CHAIN_ID ?? '6565');
const CHAIN_NAME = process.env.VITE_CHAIN_NAME ?? 'Prividium Local';
const NATIVE_CURRENCY_NAME = process.env.VITE_NATIVE_CURRENCY_NAME ?? 'Ether';
const NATIVE_CURRENCY_SYMBOL = process.env.VITE_NATIVE_CURRENCY_SYMBOL ?? 'ETH';
const NATIVE_CURRENCY_DECIMALS = Number(process.env.VITE_NATIVE_CURRENCY_DECIMALS ?? '18');
const BLOCK_EXPLORER_URL = process.env.VITE_BLOCK_EXPLORER_URL ?? 'http://localhost:3010';

if (!PRIVIDIUM_CLIENT_ID) {
    throw new Error('Set VITE_PRIVIDIUM_CLIENT_ID to the client ID of your Prividium app');
}

export const prividiumChain = defineChain({
    id: CHAIN_ID,
    name: CHAIN_NAME,
    nativeCurrency: {
        name: NATIVE_CURRENCY_NAME,
        symbol: NATIVE_CURRENCY_SYMBOL,
        decimals: NATIVE_CURRENCY_DECIMALS
    },
    rpcUrls: {
        default: {
            http: [new URL('/rpc', PRIVIDIUM_API_BASE_URL).toString()]
        }
    },
    blockExplorers: BLOCK_EXPLORER_URL
        ? {
              default: {
                  name: 'Explorer',
                  url: BLOCK_EXPLORER_URL
              }
          }
        : undefined
});

// Initialize Prividium SDK
export const prividium = createPrividiumChain({
    clientId: PRIVIDIUM_CLIENT_ID,
    chain: prividiumChain,
    authBaseUrl: AUTH_BASE_URL,
    prividiumApiBaseUrl: PRIVIDIUM_API_BASE_URL,
    redirectUrl: AUTH_CALLBACK_URL,
    onAuthExpiry: () => {
        console.log('Authentication expired - please reconnect');
    }
});

export async function authorizeWalletNetworkScopes() {
    if (!prividium.isAuthorized()) {
        await prividium.authorize({
            scopes: ['wallet:required', 'network:required']
        });
    }
}

export const publicClient = createPublicClient({
    chain: prividium.chain,
    transport: prividium.transport
});

Authentication and OAuth Scopes

Overview

Prividium™ uses OAuth 2.0 with popup-based authentication. The authentication flow determines your application's permitted operations.

Authentication assumes the user already exists in Prividium™ and has the required wallet association and roles. See User Onboarding (Backend-Assisted) before implementing browser wallet sign-in for new users.

OAuth Scopes

Prividium™ supports two OAuth scopes that control application capabilities:

  • wallet:required - Ensures the user has at least one wallet address.
  • network:required - Generates a custom RPC endpoint via the user panel. Ensures correct wallet chain configuration.

Authentication Flow

  1. User clicks authenticate button
  2. SDK opens popup with Prividium™ User Panel
  3. User authenticates. The requested scopes are validated by the User Panel before auth completes
  4. Popup redirects to /callback page
  5. SDK handles callback and stores JWT token
  6. Popup closes automatically
  7. Main window receives authentication success

Request scopes explicitly when you start the popup flow:

prividium.ts
        await prividium.authorize({
            scopes: ['wallet:required', 'network:required']
        });
authorize-callback.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Prividium Auth Callback</title>
    <script type="module">
      import { handleAuthCallback } from 'prividium';

      handleAuthCallback((errorMessage) => {
        const status = document.getElementById('callback-status');
        if (!status) return;

        if (errorMessage) {
          status.textContent = `Authentication error: ${errorMessage}`;
          status.setAttribute('data-state', 'error');
          return;
        }

        status.textContent = 'Authentication complete. You can return to the app tab.';
        status.setAttribute('data-state', 'success');
      });
    </script>
  </head>
  <body>
    <p id="callback-status" data-state="pending">Completing authentication...</p>
  </body>
</html>

Token Storage

The Prividium™ SDK automatically manages token storage:

  • Storage key: prividium_token_${chainId} in localStorage
  • OAuth state: prividium_auth_state_${chainId} in localStorage

Session Management

When the Prividium™ API has USER_IDLE_TIMEOUT_SECONDS configured, sessions use a sliding idle deadline bounded by a hard cap. The SDK extends sessions automatically on authenticated activity. Configure onSessionExpiring in createPrividiumChain to warn users before their session ends:

const prividium = createPrividiumChain({
  // ... other config
  onSessionExpiring: (info) => {
    if (info.canExtend) {
      // Session is in idle-timeout mode and can still be extended
      showStaySignedInPrompt({
        secondsRemaining: info.secondsRemaining,
        onConfirm: () => info.extend()
      });
    } else {
      // Session has reached the absolute cap — prompt the user to re-login
      showReLoginPrompt();
    }
  }
});

The SessionExpiringInfo object passed to the callback contains:

FieldTypeDescription
expiresAtDateCurrent session expiry deadline
renewableUntilDateHard cap — extend() cannot push the deadline past this
secondsRemainingnumberApproximate seconds until the session expires
canExtendbooleantrue when calling extend() will push the deadline forward
extend() => Promise<void>Extends the session by one idle-timeout interval

Two optional timing parameters tune the scheduler:

  • warningLeadTime — milliseconds before expiresAt to fire the callback (default: 60000)
  • idleCheckInterval — how often the scheduler checks for inactivity and extends (default: 300000)

RPC URL Architecture

Prividium™ provides separate RPC endpoints which handle authentication differently:

  • Base URL: https://YOUR-PRIVIDIUM-ENDPOINT/rpc: This is the base URL for all RPC operations and requires authentication headers (e.g Authorization: Bearer <token>). Commonly used in scripts and server-side code.
  • Per-User URL: https://YOUR-PRIVIDIUM-ENDPOINT/rpc/wallet/{token}: This is a user-specific URL that embeds a persistent authentication token in the path. It requires per-transaction authorization for write operations. Commonly used in browser-based wallets (e.g., MetaMask) which cannot attach custom headers.

Browser Extension Wallets

Browser extension wallets (like Metamask) require a custom RPC endpoint configuration to work with Prividium.

Why Custom RPC Endpoint is Required

Browser extension wallets do not support authentication headers. They cannot use the default /rpc endpoint directly. Instead, use the per-user /rpc/wallet/{token} RPC endpoint, which embeds the authentication token in the URL path.

Authentication RPC Endpoint

When users authenticate with wallet scopes (wallet:required and network:required), Prividium™ generates a user-specific wallet token and provides a custom RPC endpoint (/rpc/wallet/{token}). This endpoint:

  • Embeds authentication in the URL - No auth headers needed
  • Is user-specific - The token remains constant for the user until rotated
  • Enforces transaction authorizations - Each write operation requires an active authorization provided via the API

Configuration Flow

  1. User authenticates with wallet scopes - This triggers the generation of a wallet token via the Prividium™ API
  2. Obtain the RPC URL - Use prividium.getWalletRpcUrl() to retrieve the user's custom RPC endpoint (/rpc/wallet/{token})
  3. Configure wallet network - Use prividium.addNetworkToWallet() or manually configure the wallet with this RPC URL
  4. User connects wallet - The user must still explicitly connect their browser wallet to your application (authentication alone does not automatically connect the wallet)

Example Integration

configure-prividium-wallet-network.ts
import { authorizeWalletNetworkScopes, prividium } from './lib/prividium';

export async function configurePrividiumWalletNetwork() {
    await authorizeWalletNetworkScopes();

    const walletRpcUrl = await prividium.getWalletRpcUrl();
    await prividium.addNetworkToWallet();

    return walletRpcUrl;
}

Reading from Contracts

The examples below use a minimal Greeter contract. See the Sample Contract section for the Solidity source and compilation steps.

Use prividium.transport to create an authenticated RPC client for read operations:

read-greeting.ts
import { createPrividiumClient } from 'prividium';
import { type Address, createWalletClient, custom, getContract } from 'viem';
import { prividium } from './lib/prividium';

export const GREETER_ADDRESS = process.env.VITE_GREETER_ADDRESS as Address;

export const greeterAbi = [
    {
        type: 'function',
        name: 'getGreeting',
        inputs: [],
        outputs: [{ name: '', type: 'string' }],
        stateMutability: 'view'
    }
];

export async function readGreeting() {
    if (!window.ethereum) {
        throw new Error('Wallet extension not detected');
    }

    const walletClient = createWalletClient({
        chain: prividium.chain,
        transport: custom(window.ethereum)
    });

    const [address] = await walletClient.getAddresses();
    if (!address) {
        throw new Error('Connect a wallet before reading contract data');
    }

    const rpcClient = createPrividiumClient({
        chain: prividium.chain,
        transport: prividium.transport,
        account: address
    });

    const contract = getContract({
        address: GREETER_ADDRESS,
        abi: greeterAbi,
        client: rpcClient
    });

    return await contract.read.getGreeting();
}

Sending Transactions

⚠️ Critical Requirements

When sending transactions, you MUST:

  1. Pre-fetch nonce, gas estimate, and gas price using the authenticated RPC client (prividium.transport)
  2. Authorize transaction via prividium.authorizeTransaction()
  3. Pass pre-fetched values to walletClient.sendTransaction()

Why pre-fetching is required: Wallets lack authentication headers. Pre-fetch transaction parameters using the authenticated client so the wallet only signs and broadcasts.

Complete Transaction Pattern

send-greeting.ts
import { createPrividiumClient } from 'prividium';
import { type Address, createWalletClient, custom, encodeFunctionData } from 'viem';
import { prividium } from './lib/prividium';

export const GREETER_ADDRESS = process.env.VITE_GREETER_ADDRESS as Address;

export const greeterAbi = [
    {
        type: 'function',
        name: 'updateGreeting',
        inputs: [{ name: 'greeting', type: 'string' }],
        outputs: [],
        stateMutability: 'nonpayable'
    }
];

export async function sendGreeting(nextGreeting: string) {
    if (!window.ethereum) {
        throw new Error('Wallet extension not detected');
    }

    const walletClient = createWalletClient({
        chain: prividium.chain,
        transport: custom(window.ethereum)
    });

    const [address] = await walletClient.getAddresses();
    if (!address) {
        throw new Error('Connect a wallet before sending a transaction');
    }

    const rpcClient = createPrividiumClient({
        chain: prividium.chain,
        transport: prividium.transport,
        account: address
    });

    const data = encodeFunctionData({
        abi: greeterAbi,
        functionName: 'updateGreeting',
        args: [nextGreeting]
    });

    // Pre-fetch transaction parameters using authenticated client
    const nonce = await rpcClient.getTransactionCount({ address });
    const gas = await rpcClient.estimateGas({
        account: address,
        to: GREETER_ADDRESS,
        data,
        value: 0n
    });
    const gasPrice = await rpcClient.getGasPrice();

    // Authorize transaction (REQUIRED before each transaction)
    await prividium.authorizeTransaction({
        walletAddress: address,
        toAddress: GREETER_ADDRESS,
        nonce, // MUST match the authorized nonce
        calldata: data,
        value: 0n
    });

    // Send transaction with pre-fetched values
    const hash = await walletClient.sendTransaction({
        account: address,
        to: GREETER_ADDRESS,
        data,
        nonce,
        gas,
        gasPrice,
        value: 0n,
        chain: prividium.chain
    });

    return hash;
}

Transaction Authorization

For detailed information about transaction authorizations, their lifecycle, and how they work with the Per-User RPC endpoint, see Per-User RPC Documentation.

The sendGreeting() example above shows the required pattern: compute the calldata, fetch the nonce, authorize the exact transaction, then send it with the same parameters.

send-greeting.ts
    // Authorize transaction (REQUIRED before each transaction)
    await prividium.authorizeTransaction({
        walletAddress: address,
        toAddress: GREETER_ADDRESS,
        nonce, // MUST match the authorized nonce
        calldata: data,
        value: 0n
    });

Error Handling

Common Errors and Solutions

401 Unauthorized on Read Operations

Cause: Using an unauthenticated provider or transport for RPC calls.

import { custom } from 'viem';
import { prividium } from './lib/prividium';
 
// ❌ Wrong - uses MetaMask RPC (no auth headers)
const wrongClient = createPublicClient({
  chain: prividium.chain,
  transport: custom(window.ethereum)
});
401.ts
import { createPublicClient, getContract } from 'viem';
import { prividium } from './lib/prividium';
import { GREETER_ADDRESS, greeterAbi } from './read-greeting';

// ✅ Correct - uses authenticated transport
const correctClient = createPublicClient({
    chain: prividium.chain,
    transport: prividium.transport // This handles auth headers
});

Error "RPC method eth_call requires an account to be provided for the client"

Cause: Missing account in RPC client for read operations

Solution:

eth-call.ts
import { createPrividiumClient } from 'prividium';
import { walletClient } from './account';
import { prividium } from './lib/prividium';

export async function badCall() {
    // ❌ Wrong - omits account, so eth_call has no `from` and permission checks fail
    const clientWithoutAccount = createPrividiumClient({
        chain: prividium.chain,
        transport: prividium.transport,
        account: undefined
    });

    const response = await clientWithoutAccount.call({
        to: '0x000000000000000000000000000000000000dEaD',
        data: '0x'
    }); // Throws before the request is sent

    return response;
}

export async function goodCall() {
    const [address] = await walletClient.getAddresses();
    if (!address) {
        throw new Error('Connect a wallet before sending a transaction');
    }

    // ✅ Always provide account so eth_call includes `from` for permission checks
    const clientWithAccount = createPrividiumClient({
        chain: prividium.chain,
        transport: prividium.transport,
        account: address
    });

    const response = await clientWithAccount.call({
        to: '0x000000000000000000000000000000000000dEaD',
        data: '0x'
    });
    return response;
}

401 Unauthorized on Write Operations

Cause: Missing transaction authorization or not pre-fetching parameters

Solution:

Use the sendGreeting() flow above. Write requests fail when any of these steps are missing:

  1. Create an authenticated RPC client with createPrividiumClient({ chain, transport, account })
  2. Fetch nonce, gas, and gasPrice through that authenticated client
  3. Call prividium.authorizeTransaction() with the same toAddress, nonce, calldata, and value
  4. Pass those exact values to walletClient.sendTransaction()
txn-auth.ts
    // ✅ Always pre-fetch using authenticated client
    const nonce = await rpcClient.getTransactionCount({ address });
    const gasEstimate = await rpcClient.estimateGas({
        account: address,
        to: toAddress,
        data,
        value: 0n
    });
    const gasPrice = await rpcClient.getGasPrice();

    // ✅ Always enable wallet token before sending
    await prividium.authorizeTransaction({
        walletAddress: address,
        toAddress,
        nonce,
        calldata: data,
        value: 0n
    });

"Wallet is on wrong network"

Cause: Browser wallet not configured with the /wallet/{token} RPC URL.

ensure-prividium-network.ts
import { createWalletClient, custom } from 'viem';
import { authorizeWalletNetworkScopes, prividium } from './lib/prividium';

export async function ensurePrividiumNetwork() {
    if (!window.ethereum) {
        throw new Error('Wallet extension not detected');
    }

    await authorizeWalletNetworkScopes();

    await prividium.addNetworkToWallet();

    const walletClient = createWalletClient({
        chain: prividium.chain,
        transport: custom(window.ethereum)
    });

    const chainId = await walletClient.getChainId();
    if (chainId !== prividium.chain.id) {
        throw new Error(`Wallet is connected to ${chainId}, expected ${prividium.chain.id}`);
    }
}

For a full list of common issues and best practices, see Developer troubleshooting.

Minimizing ABI Exposure

When building dApps, contract ABIs are typically bundled into JavaScript. Since static assets are served without authentication, anyone can extract ABIs and discover contract interfaces.

Prividium provides prividium.fetchContractAbi() to fetch ABIs filtered to only functions the user can access:

fetch-abi.ts
    // Fetch ABI at runtime instead of bundling
    const { abi, functions } = await prividium.fetchContractAbi(CONTRACT_ADDRESS);

    const contract = getContract({
        address: CONTRACT_ADDRESS,
        abi,
        client: publicClient
    });

For detailed patterns and best practices, see Minimizing Contract ABI Exposure.

Summary

Key takeaways for building on Prividium:

  1. Authentication: Use SDK's popup-based OAuth flow with appropriate scopes
  2. Reads: Always use prividium.transport (authenticated /rpc endpoint)
  3. Writes: Pre-fetch nonce, gas, gas price using authenticated client, authorize wallet transaction, then send via browser extension wallet
  4. Network: Configure browser extension wallet with /wallet/{token} RPC URL via addNetworkToWallet()
  5. Environment: Use base Prividium™ api URL in your RPC URL environment variable, SDK constructs wallet URLs automatically
  6. Errors: Handle 401 errors by ensuring authenticated clients and transaction authorizations
  7. Session management: Configure onSessionExpiring to warn users before session expiry; call info.extend() when canExtend is true to keep idle sessions alive

These patterns ensure secure, reliable interactions with Prividium™ chains.

Next Steps