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
- Application Configuration
- Prividium™ SDK Initialization
- Authentication and OAuth Scopes
- Session Management
- RPC URL Architecture
- Browser Extension Wallets
- Reading from Contracts
- Sending Transactions
- Error Handling
- Summary
- 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
- Access Admin Panel: Open the Prividium™ Admin Panel.
- Create Application: Register a new client. Save the OAuth Client Id displayed after creation as your
CLIENT_ID. - Configure CORS: Add your application's origin URL to the allowed CORS origins.
- 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():
// 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
- User clicks authenticate button
- SDK opens popup with Prividium™ User Panel
- User authenticates. The requested scopes are validated by the User Panel before auth completes
- Popup redirects to
/callbackpage - SDK handles callback and stores JWT token
- Popup closes automatically
- Main window receives authentication success
Request scopes explicitly when you start the popup flow:
await prividium.authorize({
scopes: ['wallet:required', 'network:required']
});<!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}inlocalStorage - OAuth state:
prividium_auth_state_${chainId}inlocalStorage
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:
| Field | Type | Description |
|---|---|---|
expiresAt | Date | Current session expiry deadline |
renewableUntil | Date | Hard cap — extend() cannot push the deadline past this |
secondsRemaining | number | Approximate seconds until the session expires |
canExtend | boolean | true 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 beforeexpiresAtto 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.gAuthorization: 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
- User authenticates with wallet scopes - This triggers the generation of a wallet token via the Prividium™ API
- Obtain the RPC URL - Use
prividium.getWalletRpcUrl()to retrieve the user's custom RPC endpoint (/rpc/wallet/{token}) - Configure wallet network - Use
prividium.addNetworkToWallet()or manually configure the wallet with this RPC URL - 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
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:
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:
- Pre-fetch nonce, gas estimate, and gas price using the authenticated RPC client (
prividium.transport) - Authorize transaction via
prividium.authorizeTransaction() - 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
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.
// 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)
});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:
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:
- Create an authenticated RPC client with
createPrividiumClient({ chain, transport, account }) - Fetch
nonce,gas, andgasPricethrough that authenticated client - Call
prividium.authorizeTransaction()with the sametoAddress,nonce,calldata, andvalue - Pass those exact values to
walletClient.sendTransaction()
// ✅ 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.
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 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:
- Authentication: Use SDK's popup-based OAuth flow with appropriate scopes
- Reads: Always use
prividium.transport(authenticated/rpcendpoint) - Writes: Pre-fetch nonce, gas, gas price using authenticated client, authorize wallet transaction, then send via browser extension wallet
- Network: Configure browser extension wallet with
/wallet/{token}RPC URL viaaddNetworkToWallet() - Environment: Use base Prividium™ api URL in your RPC URL environment variable, SDK constructs wallet URLs automatically
- Errors: Handle 401 errors by ensuring authenticated clients and transaction authorizations
- Session management: Configure
onSessionExpiringto warn users before session expiry; callinfo.extend()whencanExtendistrueto keep idle sessions alive
These patterns ensure secure, reliable interactions with Prividium™ chains.