> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.prividium.com/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

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

Examples are shown for both [viem](https://viem.sh) and [ethers.js v6](https://docs.ethers.org/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.

:::note
**Architecture Overview**

For a high-level understanding of Prividium's architecture and components, see
[Prividium™ Architecture](/getting-started/introduction/prividium-architecture).
:::

## Table of Contents

1. [Application Configuration](#application-configuration)
2. [Prividium™ SDK Initialization](#prividium-sdk-initialization)
3. [Authentication and OAuth Scopes](#authentication-and-oauth-scopes)
4. [Session Management](#session-management)
5. [RPC URL Architecture](#rpc-url-architecture)
6. [Browser Extension Wallets](#browser-extension-wallets)
7. [Reading from Contracts](#reading-from-contracts)
8. [Sending Transactions](#sending-transactions)
9. [Error Handling](#error-handling)
10. [Summary](#summary)
11. [Next Steps](#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**

```typescript
// 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 ?? '506');
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
});
```

The `prividium` SDK accepts a plain chain object. There is no need to import `defineChain` from viem. Replace
`createPublicClient` with a `createAuthenticatedProvider()` helper that builds an ethers `JsonRpcProvider` carrying the
current session token:

**prividium.ts**

```typescript
// src/lib/prividium.ts
import { ethers } from 'ethers';
import { createPrividiumChain } from 'prividium';


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 ?? '506');
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 = {
    id: CHAIN_ID,
    name: CHAIN_NAME,
    nativeCurrency: {
        name: NATIVE_CURRENCY_NAME,
        symbol: NATIVE_CURRENCY_SYMBOL,
        decimals: NATIVE_CURRENCY_DECIMALS
    },
    ...(BLOCK_EXPLORER_URL && {
        blockExplorers: {
            default: { name: 'Explorer', url: BLOCK_EXPLORER_URL }
        }
    })
};

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']
        });
    }
}

// Creates an ethers JsonRpcProvider authenticated with the current session token.
// A new provider is created per call so the token is always fresh.
export function createAuthenticatedProvider() {
    const headers = prividium.getAuthHeaders();
    if (!headers) {
        throw new Error('Not authenticated. Please authorize first.');
    }
    const rpcUrl = new URL('/rpc', PRIVIDIUM_API_BASE_URL).toString();
    const fetchRequest = new ethers.FetchRequest(rpcUrl);
    for (const [key, value] of Object.entries(headers)) {
        fetchRequest.setHeader(key, value as string);
    }
    const network = new ethers.Network(CHAIN_NAME, CHAIN_ID);
    return new ethers.JsonRpcProvider(fetchRequest, network, { staticNetwork: network, batchMaxCount: 20 });
}
```

:::note[NOTE]
`createAuthenticatedProvider()` replaces both `createPublicClient` and `createPrividiumClient` from the viem tab. Call
it once per operation so each request carries the latest token.
:::

:::note[NOTE]
Adjust environment variable access for your framework (e.g., `import.meta.env.*` for Vite, `process.env.*` for
Node.js).
:::

## 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)](/developers/applications/user-onboarding-login) 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. The User Panel issues a session bound to your application's OAuth client id
5. Popup redirects to `/callback` page
6. SDK handles callback and stores the token
7. Popup closes automatically
8. Main window receives authentication success

### What the Token Can Access

The token your application receives is not the user's account session. The Permissions API accepts it on the application
surface only:

* `POST /rpc` — JSON-RPC with the user's permissions
* `GET /api/profiles/me` — the user's profile, roles and wallets
* `GET /api/user-wallets` — the user's wallet addresses
* `GET /api/contracts/{address}/abi` — the ABI filtered to what the user may call
* `GET /api/wallet/personal-rpc-token`, `POST /api/wallet/invalidate`, `POST /api/wallet/transaction-authorization` —
  the per-user wallet RPC endpoints the SDK uses
* `GET /api/auth/current-session`, `POST /api/auth/extend-session`, `POST /api/auth/logout` — the session lifecycle

Every other endpoint, including the admin API, returns `401` for an application token even when the signed-in user is an
administrator. Use a [SIWE-authenticated backend](/developers/applications/user-onboarding-login) for administrative
calls. The token still carries the user's full RPC authority; see
[Security Limitations](/developers/applications/security-limitations#6-application-tokens-carry-the-users-rpc-authority).

Request scopes explicitly when you start the popup flow:

**prividium.ts**

```typescript
        await prividium.authorize({
            scopes: ['wallet:required', 'network:required']
        });
```

:::info[IMPORTANT]
Your application's callback page must call `handleAuthCallback()`
:::

**authorize-callback.html**

```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`

:::note[NOTE]
Use the SDK's `isAuthorized()` and `getAuthHeaders()` methods. Never manage tokens manually.
:::

## 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:

```typescript
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              |

:::note[NOTE]
When `USER_IDLE_TIMEOUT_SECONDS` is not set on the server, sessions use a fixed expiry and `canExtend` is always
`false`. Configuring `onSessionExpiring` is still recommended — it handles both modes with the same callback.
:::

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.

:::note[NOTE]
Per-User RPC Details: For comprehensive details on the Per-User RPC URL architecture, transaction authorizations, and
wallet RPC behavior, see [Per-User RPC Documentation](/rpc/per-user-rpc).
:::

## 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.

:::note[Per-User RPC Details]
For comprehensive details on the Per-User RPC URL architecture, token lifecycle, transaction authorizations, and
wallet RPC behavior, see [Per-User RPC Documentation](/rpc/per-user-rpc).
:::

### 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**

```typescript
import { authorizeWalletNetworkScopes, prividium } from './lib/prividium';

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

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

    return walletRpcUrl;
}
```

**configure-prividium-wallet-network.ts**

```typescript
import { authorizeWalletNetworkScopes, prividium } from './lib/prividium';

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

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

    return walletRpcUrl;
}
```

:::info[IMPORTANT]
Authentication provides the configuration, but users must still connect their wallet. Authentication and wallet
connection are separate steps.
:::

### Reading from Contracts

The examples below use a minimal Greeter contract. See the
[Sample Contract](/developers/scripting/contract-deployment-scripting#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**

```typescript
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();
}
```

Use `createAuthenticatedProvider()` for the read call. ethers.js accepts a human-readable ABI string, so no JSON ABI
object is needed:

**read-greeting.ts**

```typescript
import { ethers } from 'ethers';
import { createAuthenticatedProvider } from './lib/prividium';

export const GREETER_ADDRESS = process.env.VITE_GREETER_ADDRESS as `0x${string}`;

export const greeterAbi = ['function getGreeting() view returns (string)'];

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

    const browserProvider = new ethers.BrowserProvider(window.ethereum);
    const accounts = await browserProvider.listAccounts();
    if (accounts.length === 0) {
        throw new Error('Connect a wallet before reading contract data');
    }

    const provider = createAuthenticatedProvider();
    const contract = new ethers.Contract(GREETER_ADDRESS, greeterAbi, provider);

    return await contract.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**

```typescript
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;
}
```

The pre-fetch + authorize pattern is identical to the viem version. The ethers equivalents are:
`Interface.encodeFunctionData`, `provider.getTransactionCount`, `provider.estimateGas`, `provider.getFeeData`, and
`signer.sendTransaction`:

**send-greeting.ts**

```typescript
import { ethers } from 'ethers';
import { createAuthenticatedProvider, prividium } from './lib/prividium';

const GREETER_ADDRESS_ENV = process.env.VITE_GREETER_ADDRESS;
if (!GREETER_ADDRESS_ENV) {
    throw new Error('Set VITE_GREETER_ADDRESS to the deployed Greeter contract address');
}

// ethers types addresses and calldata as plain strings; the Prividium SDK wants `0x`-prefixed hex.
export const GREETER_ADDRESS = GREETER_ADDRESS_ENV as `0x${string}`;

export const greeterAbi = ['function updateGreeting(string greeting)'];

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

    const browserProvider = new ethers.BrowserProvider(window.ethereum);
    const accounts = await browserProvider.listAccounts();
    if (accounts.length === 0) {
        throw new Error('Connect a wallet before sending a transaction');
    }

    const address = accounts[0].address as `0x${string}`;
    const signer = await browserProvider.getSigner();

    const iface = new ethers.Interface(greeterAbi);
    const data = iface.encodeFunctionData('updateGreeting', [nextGreeting]) as `0x${string}`;

    // Pre-fetch transaction parameters using authenticated provider
    const authenticatedProvider = createAuthenticatedProvider();
    const nonce = await authenticatedProvider.getTransactionCount(address);
    const gasEstimate = await authenticatedProvider.estimateGas({
        from: address,
        to: GREETER_ADDRESS,
        data,
        value: 0n
    });
    const gasPrice = (await authenticatedProvider.getFeeData()).gasPrice;

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

    // Send transaction via browser wallet with pre-fetched values
    const tx = await signer.sendTransaction({
        to: GREETER_ADDRESS,
        data,
        nonce,
        gasLimit: gasEstimate,
        gasPrice,
        value: 0n
    });

    return tx.hash;
}
```

### Transaction Authorization

:::info[IMPORTANT]
Call `prividium.authorizeTransaction()` before **every transaction**. Permission is transaction-specific and expires
after 1 hour.
:::

For detailed information about transaction authorizations, their lifecycle, and how they work with the Per-User RPC
endpoint, see [Per-User RPC Documentation](/rpc/per-user-rpc#transaction-authorizations).

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**

```typescript
    // 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.

```javascript
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**

```typescript
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
});
```

```javascript
import { ethers } from 'ethers';

// ❌ Wrong — plain JsonRpcProvider has no auth headers
const wrongProvider = new ethers.JsonRpcProvider('https://proxy.prividium.dev/rpc');
```

**401.ts**

```typescript
import { createAuthenticatedProvider } from './lib/prividium';
import { GREETER_ADDRESS, greeterAbi } from './read-greeting';

export async function createProvider() {
    // ✅ Correct — injects the current session token on every request
    const correctProvider = createAuthenticatedProvider();
    return correctProvider;
}
```

#### 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**

```typescript
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;
}
```

:::note[NOTE]
This error is specific to `createPrividiumClient` (viem). ethers.js does not have this constraint.
:::

#### 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**

```typescript
    // ✅ 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
    });

```

**txn-auth.ts**

```typescript
    // ✅ Always pre-fetch using the authenticated provider
    const authenticatedProvider = createAuthenticatedProvider();
    const nonce = await authenticatedProvider.getTransactionCount(address);
    const gasEstimate = await authenticatedProvider.estimateGas({ from: address, to: toAddress, data, value: 0n });
    const gasPrice = (await authenticatedProvider.getFeeData()).gasPrice;

    // ✅ Always call authorizeTransaction 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**

```typescript
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}`);
    }
}
```

**ensure-prividium-network.ts**

```typescript
import { ethers } from 'ethers';
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 provider = new ethers.BrowserProvider(window.ethereum);
    const network = await provider.getNetwork();
    // chainId is a bigint in ethers v6
    const chainId = Number(network.chainId);
    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](/developers/applications/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**

```typescript
    // 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](/developers/applications/minimizing-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

* [Contract Deployment and Scripting](/developers/scripting/contract-deployment-scripting)
