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

You can use any frontend framework to develop a frontend application for Prividium. In this example, we will use Vue.js
with `vite`, `viem`, and Typescript.

This guide assumes you have already deployed an ERC20 token contract to your Prividium network as shown in the
[Deploying Contracts](/developers/quickstart/deploying-contracts) quickstart guide.

## Project setup

Create a new Vue app with TypeScript and `vite` and move into the project folder:

:::code-group
```bash [npm]
npm create vite@7 prividium-quickstart-app -- --template vue-ts
```

```bash [pnpm]
pnpm create vite@7 prividium-quickstart-app -- --template vue-ts
```

```bash [yarn]
yarn create vite@7 prividium-quickstart-app -- --template vue-ts
```

```bash [bun]
bun create vite@7 prividium-quickstart-app -- --template vue-ts
```
:::

```bash
cd prividium-quickstart-app
```

Then install the `prividium` and `viem` npm packages:

:::code-group
```bash [npm]
npm install prividium viem
```

```bash [pnpm]
pnpm add prividium viem
```

```bash [yarn]
yarn add prividium viem
```

```bash [bun]
bun add prividium viem
```
:::

## Prividium client

Create a new file in the `src` folder called `prividium.ts`.

```bash
touch src/prividium.ts
```

This file will be used to:

1. Define the chain information.
2. Instantiate the Prividium chain object.

Copy and paste the code below into `prividium.ts`.

**prividium.ts**

```typescript
import { createPrividiumChain } from 'prividium';
import { defineChain } from 'viem';

export const prividiumChain = defineChain({
    id: Number.parseInt(import.meta.env.VITE_CHAIN_ID, 10),
    name: import.meta.env.VITE_CHAIN_NAME,
    nativeCurrency: {
        name: import.meta.env.VITE_NATIVE_CURRENCY_NAME || import.meta.env.VITE_NATIVE_CURRENCY_SYMBOL,
        symbol: import.meta.env.VITE_NATIVE_CURRENCY_SYMBOL,
        decimals: 18
    },
    rpcUrls: {
        default: {
            http: [import.meta.env.VITE_PRIVIDIUM_RPC_URL]
        },
        public: {
            http: [import.meta.env.VITE_PRIVIDIUM_RPC_URL]
        }
    },
    blockExplorers: {
        default: {
            name: 'Prividium Explorer',
            url: import.meta.env.VITE_PRIVIDIUM_BLOCK_EXPLORER_URL
        }
    },
    testnet: true
});

export const prividium = createPrividiumChain({
    clientId: import.meta.env.VITE_CLIENT_ID,
    chain: prividiumChain,
    authBaseUrl: import.meta.env.VITE_AUTH_BASE_URL,
    redirectUrl: `${window.location.origin}/auth-callback.html`,
    prividiumApiBaseUrl: import.meta.env.VITE_PRIVIDIUM_API_URL
});
```

### Callback URL

Create a file in the project root folder called `auth-callback.html`

```bash
touch auth-callback.html
```

The Prividium SDK requires a callback page to complete the authentication flow securely. Later on, this callback page
will be added to the "User apps" tab in the admin panel to authorize the app to perform authentication for the network.

Copy and paste the code below into `auth-callback.html`.

**auth-callback.html**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Authentication Callback</title>
  </head>
  <body>
    <div>
      <h2>Completing sign in</h2>
      <p>You can close this window if it does not close automatically.</p>
      <div id="error-wrap"><span id="error-message"></span></div>
    </div>
    <script type="module" src="/src/auth-callback.ts"></script>
  </body>
</html>
```

Next, create a file in the `src` folder called `auth-callback.ts`.

```bash
touch src/auth-callback.ts
```

Copy and paste the code below into `auth-callback.ts`.

**auth-callback.ts**

```typescript
import { handleAuthCallback } from 'prividium';

handleAuthCallback((error) => {
    const errorEl = document.getElementById('error-message');
    const errorWrap = document.getElementById('error-wrap');

    if (errorEl && errorWrap && error) {
        errorEl.textContent = error;
        errorWrap.style.display = 'block';
    }
});
```

Finally, edit the `vite.config.ts` file to support the callback url:

**vite.config.ts**

```typescript
import { resolve } from 'node:path';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
    plugins: [vue()],
    build: {
        rollupOptions: {
            input: {
                main: resolve(__dirname, 'index.html'),
                'auth-callback': resolve(__dirname, 'auth-callback.html')
            }
        }
    }
});
```

## User authentication

To enable users to log in to your Prividium network, you can use the `prividium.authorize` method. You can pass optional
requirements into this method as well. For this example, we are going to require that the user has a wallet associated
with their account, and the wallet has the correct chain configuration.

To check if the user is currently logged in, you can use the `prividium.isAuthorized` method.

Once logged in, the app can fetch some data about the user's profile using `prividium.fetchUser`.

Edit `App.vue` to use the code below:

**App.vue**

```vue
<script setup lang="ts">
import { type Address, isAddress } from 'viem';
import { computed, onMounted, ref } from 'vue';
import { useTokenContract } from './composables/useTokenContract';
import { prividium } from './prividium';

type UserProfile = Awaited<ReturnType<typeof prividium.fetchUser>>;

const isAuthenticated = ref(prividium.isAuthorized());
const isAuthenticating = ref(false);
const authError = ref('');
const userProfile = ref<UserProfile | null>(null);

const walletAddress = ref<Address | ''>('');
const walletError = ref('');

const recipient = ref('');
const amount = ref('');

const {
    tokenAddress,
    tokenSymbol,
    formattedBalance,
    isBalanceLoading,
    txStatus,
    isTxPending,
    readBalance,
    sendTokens
} = useTokenContract(walletAddress);

const shortWallet = computed(() => {
    if (!walletAddress.value) return 'Not connected';
    return `${walletAddress.value.slice(0, 6)}...${walletAddress.value.slice(-4)}`;
});

async function loadProfile() {
    if (!isAuthenticated.value) {
        userProfile.value = null;
        return;
    }

    try {
        userProfile.value = await prividium.fetchUser();
    } catch (error) {
        authError.value = error instanceof Error ? error.message : 'Failed to load profile';
    }
}

async function signIn() {
    isAuthenticating.value = true;
    authError.value = '';

    try {
        await prividium.authorize({ scopes: ['wallet:required', 'network:required'] });
        isAuthenticated.value = true;
        await loadProfile();
    } catch (error) {
        isAuthenticated.value = false;
        authError.value = error instanceof Error ? error.message : 'Authentication failed';
    } finally {
        isAuthenticating.value = false;
    }
}

function signOut() {
    prividium.unauthorize();
    isAuthenticated.value = false;
    userProfile.value = null;
    walletAddress.value = '';
}

async function connectWallet() {
    walletError.value = '';

    try {
        if (!window.ethereum) {
            throw new Error('No injected wallet found. Install MetaMask or another EVM wallet.');
        }

        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        const firstAccount = Array.isArray(accounts) ? accounts[0] : undefined;
        if (typeof firstAccount !== 'string' || !isAddress(firstAccount)) {
            throw new Error('No valid wallet address was returned.');
        }

        walletAddress.value = firstAccount;
        await switchOrAddNetwork();
        await readBalance();
    } catch (error) {
        walletError.value = error instanceof Error ? error.message : 'Failed to connect wallet';
    }
}

async function switchOrAddNetwork() {
    if (!window.ethereum) return;

    const chainIdHex = `0x${prividium.chain.id.toString(16)}`;
    try {
        await window.ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [{ chainId: chainIdHex }]
        });
    } catch (error: unknown) {
        const walletErrorCode =
            typeof error === 'object' && error !== null && 'code' in error
                ? (error as { code?: number }).code
                : undefined;

        if (walletErrorCode === 4902) {
            await prividium.addNetworkToWallet();
            return;
        }

        throw error;
    }
}

async function submitTransfer() {
    await sendTokens(recipient.value, amount.value);
    if (!isTxPending.value && txStatus.value.startsWith('Transaction submitted:')) {
        amount.value = '';
    }
}

onMounted(async () => {
    if (isAuthenticated.value) {
        await loadProfile();
    }
});
</script>

<template>
  <main class="app-shell">
    <h1>Prividium Quickstart Token App</h1>

    <section class="card">
      <h2>Authentication</h2>
      <p v-if="isAuthenticated">Signed in</p>
      <p v-else>Not signed in</p>
      <div class="row">
        <button v-if="!isAuthenticated" :disabled="isAuthenticating" @click="signIn">
          {{ isAuthenticating ? 'Signing in...' : 'Sign in with Prividium' }}
        </button>
        <button v-else class="secondary" @click="signOut">Sign out</button>
      </div>
      <p v-if="authError" class="error">{{ authError }}</p>
    </section>

    <section v-if="isAuthenticated" class="card">
      <h2>User Profile</h2>
      <p><strong>Name:</strong> {{ userProfile?.displayName || '-' }}</p>
      <p><strong>User ID:</strong> {{ userProfile?.id || '-' }}</p>
      <p><strong>Wallet Count:</strong> {{ userProfile?.wallets?.length ?? 0 }}</p>
    </section>

    <section v-if="isAuthenticated" class="card">
      <h2>Wallet</h2>
      <p><strong>Connected Wallet:</strong> {{ shortWallet }}</p>
      <div class="row">
        <button @click="connectWallet">Connect wallet</button>
        <button class="secondary" :disabled="!walletAddress || isBalanceLoading" @click="readBalance">
          {{ isBalanceLoading ? 'Refreshing...' : 'Refresh balance' }}
        </button>
      </div>
      <p v-if="walletError" class="error">{{ walletError }}</p>
    </section>

    <section v-if="isAuthenticated" class="card">
      <h2>QuickstartToken</h2>
      <p><strong>Contract:</strong> {{ tokenAddress }}</p>
      <p><strong>Balance:</strong> <span id="balance-value">{{ formattedBalance }} {{ tokenSymbol }}</span></p>

      <div class="form-row">
        <label for="recipient">Recipient</label>
        <input id="recipient" v-model="recipient" placeholder="0x..." />
      </div>

      <div class="form-row">
        <label for="amount">Amount</label>
        <input id="amount" v-model="amount" placeholder="1.0" />
      </div>

      <button :disabled="isTxPending" @click="submitTransfer">
        {{ isTxPending ? 'Authorizing and sending...' : 'Send tokens' }}
      </button>
      <p v-if="txStatus" class="status">{{ txStatus }}</p>
    </section>
  </main>
</template>
```

## Using contracts

Create a new composable file called `useTokenContract.ts`:

```bash
mkdir src/composables
touch src/composables/useTokenContract.ts
```

Copy and paste the code below:

**useTokenContract.ts**

```typescript
import { createPrividiumClient } from 'prividium';
import {
    type Address,
    createWalletClient,
    custom,
    encodeFunctionData,
    erc20Abi,
    formatUnits,
    isAddress,
    parseUnits,
    zeroAddress
} from 'viem';
import { computed, type Ref, ref } from 'vue';
import { prividium, prividiumChain } from '../prividium';

export function useTokenContract(walletAddress: Ref<Address | ''>) {
    const tokenAddress = import.meta.env.VITE_TOKEN_ADDRESS as Address;

    const tokenSymbol = ref('TOKEN');
    const tokenDecimals = ref(18);
    const tokenBalance = ref<bigint | null>(null);
    const isBalanceLoading = ref(false);
    const txStatus = ref('');
    const isTxPending = ref(false);

    const formattedBalance = computed(() => {
        if (tokenBalance.value === null) return '-';
        return formatUnits(tokenBalance.value, tokenDecimals.value);
    });

    function getRpcClient() {
        if (!window.ethereum || !walletAddress.value) {
            console.log('Wallet is not connected.');
            return;
        }
        return createPrividiumClient({
            chain: prividiumChain,
            transport: prividium.transport,
            account: walletAddress.value
        });
    }

    function getWalletClient() {
        if (!window.ethereum || !walletAddress.value) {
            console.log('Wallet is not connected.');
            return;
        }
        return createWalletClient({
            chain: prividium.chain,
            transport: custom(window.ethereum)
        });
    }

    async function readBalance() {
        if (!isAddress(tokenAddress)) {
            throw new Error('VITE_TOKEN_ADDRESS is not a valid token address.');
        }

        isBalanceLoading.value = true;
        try {
            const rpc = getRpcClient();
            if (!rpc) {
                console.log('MISSING RPC CLIENT');
                return;
            }
            const [symbol, decimals, balance] = await Promise.all([
                rpc.readContract({ address: tokenAddress, abi: erc20Abi, functionName: 'symbol' }),
                rpc.readContract({ address: tokenAddress, abi: erc20Abi, functionName: 'decimals' }),
                rpc.readContract({
                    address: tokenAddress,
                    abi: erc20Abi,
                    functionName: 'balanceOf',
                    args: [walletAddress.value || zeroAddress]
                })
            ]);

            tokenSymbol.value = symbol;
            tokenDecimals.value = Number(decimals);
            tokenBalance.value = balance;
        } finally {
            isBalanceLoading.value = false;
        }
    }

    async function sendTokens(recipient: string, amount: string) {
        txStatus.value = '';

        if (!isAddress(tokenAddress)) {
            txStatus.value = 'Set VITE_TOKEN_ADDRESS to your deployed QuickstartToken contract address.';
            return;
        }

        if (!walletAddress.value) {
            txStatus.value = 'Connect your wallet first.';
            return;
        }

        if (!isAddress(recipient)) {
            txStatus.value = 'Recipient must be a valid address.';
            return;
        }

        let parsedAmount: bigint;
        try {
            parsedAmount = parseUnits(amount, tokenDecimals.value);
        } catch {
            txStatus.value = 'Amount is invalid.';
            return;
        }

        if (parsedAmount <= 0n) {
            txStatus.value = 'Amount must be greater than zero.';
            return;
        }

        isTxPending.value = true;
        try {
            const rpc = getRpcClient();
            if (!rpc) {
                console.log('MISSING RPC CLIENT');
                return;
            }

            const calldata = encodeFunctionData({
                abi: erc20Abi,
                functionName: 'transfer',
                args: [recipient as Address, parsedAmount]
            });

            const nonce = await rpc.getTransactionCount({ address: walletAddress.value });

            await prividium.authorizeTransaction({
                walletAddress: walletAddress.value,
                toAddress: tokenAddress,
                nonce,
                calldata
            });

            const gas = await rpc.estimateGas({
                account: walletAddress.value,
                to: tokenAddress,
                data: calldata
            });
            const gasPrice = await rpc.getGasPrice();
            const walletClient = getWalletClient();
            if (!walletClient) {
                console.log('MISSING WALLET CLIENT');
                return;
            }
            const hash = await walletClient.sendTransaction({
                account: walletAddress.value,
                to: tokenAddress,
                data: calldata,
                nonce,
                gas,
                gasPrice,
                chain: prividiumChain
            });

            txStatus.value = `Transaction submitted: ${hash}`;
            await readBalance();
        } catch (error) {
            txStatus.value = error instanceof Error ? error.message : 'Failed to send token transaction';
        } finally {
            isTxPending.value = false;
        }
    }

    return {
        tokenAddress,
        tokenSymbol,
        tokenDecimals,
        tokenBalance,
        formattedBalance,
        isBalanceLoading,
        txStatus,
        isTxPending,
        readBalance,
        sendTokens
    };
}
```

### Reading contracts

In standard web3 applications, it's possible to create a public client capable of reading the chain without the user
signing in with their wallet. However with Prividium, it's required that a user must be authenticated and their wallet
address known before reading the chain. This is because even read operations for contracts can be configured to allow
some users but not others. Additionally, the request must go through the user's special RPC url, which contains their
Per-User RPC token.

Because of this, instead of using `viem`'s default `createPublicClient` method for read operations, we use the
`createPrividiumClient` method from the `prividium` SDK. This prividium client can then be used to read the state of the
chain or contracts as long as the contract permissions are configured to allow this.

### Sending transactions

All write transactions (transactions that change the state of a contract or the balance of a wallet) require
authorization first before sending. These transactions are authorized via the `authorizeTransaction` method available on
the Prividium instance created with `createPrividiumChain`.

This can be seen in the `sendTokens` function.

:::note[Why wasn't authorizeTransaction used in the previous section?]
In the previous scripting section, this wasn't required because we were using the prividium proxy, which handles the
authentication and authorization steps for us.
:::

Permissions need to be defined for a contract to call the `authorizeTransaction` method, even if the user has
`full_sequencer_rpc_access`.

## Environment variables

Create a `.env` file in the project root folder, and copy and paste the example configuration below.

**.env.example**

```bash
VITE_CLIENT_ID=<your-client-id>
VITE_TOKEN_ADDRESS=0x...

# Keep these values for a local prividium instance
VITE_CHAIN_ID=506
VITE_CHAIN_NAME=Prividium Local
VITE_NATIVE_CURRENCY_NAME=Ether
VITE_NATIVE_CURRENCY_SYMBOL=ETH
VITE_AUTH_BASE_URL=http://localhost:3001
VITE_PRIVIDIUM_API_URL=http://localhost:8000
VITE_PRIVIDIUM_RPC_URL=http://localhost:8000/rpc
VITE_PRIVIDIUM_BLOCK_EXPLORER_URL=http://localhost:3010
```

For the `VITE_TOKEN_ADDRESS`, use the ERC20 token contract address you already have deployed.

For a local instance of Prividium, you can keep all of the default network values. For a live testnet or mainnet, make
sure to update all the values to match your network.

### Setting up the client ID

To get the value for the `VITE_CLIENT_ID`, your application must be registered in the admin panel.

If you don't have admin access in the Prividium instance you are using, you'll need to ask an admin to set this up for
you. Otherwise, you can follow the steps below.

In the "User Apps" tab of the admin panel, click on the "New application" button to create a new user application.

Set the "Whitelisted Origin" as `http://localhost:5173`.

Under `Redirect URIs` add `http://localhost:5173/auth-callback.html`. This should match the path for the callback URL
set up previously.

Give the app a name, and then click "Save".

Once created, copy the OAuth Client ID value and use it for the `VITE_CLIENT_ID` in your `.env` file. This is used in
the Prividium client in the `prividium.ts` file you created earlier.

## (Optional) Add styling

To add some better styling for the app, replace the `src/style.css` file with the one below.

**style.css**

```css
:root {
    font-family:
        ui-sans-serif,
        system-ui,
        -apple-system,
        Segoe UI,
        Roboto,
        Helvetica,
        Arial,
        sans-serif;
    color: #1a1f25;
    background: #f2f5f7;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
}

#app {
    min-height: 100vh;
}

.app-shell {
    width: min(760px, 94vw);
    margin: 28px auto;
    display: grid;
    gap: 14px;
}

h1,
h2 {
    margin: 0 0 10px;
}

.card {
    background: #fff;
    border: 1px solid #d6dde5;
    border-radius: 12px;
    padding: 16px;
}

.row {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

button,
input {
    border-radius: 8px;
    border: 1px solid #c5ced8;
    font: inherit;
}

button {
    cursor: pointer;
    padding: 8px 12px;
    background: #0f6dff;
    color: #fff;
    border-color: #0f6dff;
}

button.secondary {
    background: #fff;
    color: #1a1f25;
    border-color: #c5ced8;
}

button:disabled {
    opacity: 0.6;
    cursor: default;
}

.form-row {
    display: grid;
    gap: 6px;
    margin-bottom: 10px;
}

input {
    padding: 8px;
}

.error {
    color: #a22222;
}

.status {
    margin-top: 10px;
    word-break: break-word;
}
```

## Testing the application

Use Chrome browser for testing this application.

### Setting up your wallet

Log in with a Prividium account in the user panel and make sure the account has a wallet associated with the account. If
using `local-prividium`, you can log in with any of the default wallets listed in the `README.md` file. Add one to your
Metamask using the private key.

Once you've added the wallet, go to the "Wallets" tab of the user panel and click on the "Add Network to Wallet" button
to add the network with your Per-User RPC token.

:::note
**Adding the network again**

If you have already completed this step previously with a different account or with a local instance of Prividium™ that
you restarted, or you have regenerated your Per-User RPC token, delete the old Prividium™ network and add it again.
:::

### Running the application

Run the command in the project folder to start the local development server:

:::code-group
```bash [npm]
npm run dev
```

```bash [pnpm]
pnpm dev
```

```bash [yarn]
yarn dev
```

```bash [bun]
bun dev
```
:::

The app will be running at [`http://localhost:5173/`](http://localhost:5173/).

You should now be able to:

* Log in with Prividium
* See your user profile information
* Connect your wallet
* See your wallet's token balance
* Transfer tokens to another user
