Documentation Index
Fetch the complete documentation index at: https://developers.particle.network/llms.txt
Use this file to discover all available pages before exploring further.
Quickstart: Integrating Universal Accounts into a Web App
Explore how Universal Accounts are integrated into a working dApp and see the SDK in action.
This guide walks you through the process of integrating Universal Account features into a working demo app built with Particle Auth and the Universal Accounts SDK.
We’ll explain how user logins, account initialization, and balance display are handled in the code. You’ll also learn how to:
- Initialize a Universal Account after a user logs in.
- Fetch and display the user's Universal Account addresses.
- Fetch and display the user's unified balance across chains.
You can use this as a reference or base for your own app.
This example showcases a Next.js app using [Particle Auth](/social-logins/auth/introduction) as its login method.
However, the same logic can be applied to any other EOA-based provider or signer.
Getting Started
To start integrating Universal Accounts:
This repository includes the full working demo app.
```bash Terminal theme={null}
git clone https://github.com/particle-network/universal-accounts-quickstart.git
```
First, create a project in the [Particle Dashboard](https://dashboard.particle.network/) to get the required credentials.
<Note> The same project keys are used for both Particle Auth and Universal Accounts.</Note>
In this example, we use **Particle Auth** for user authentication. However, you can use any EOA-compatible provider or signer.
Regardless of your choice, you’ll still need to create a project in the **Particle Dashboard** and initialize **Universal Accounts** using the project credentials.
Create a `.env` file in the root of the `ua-quickstart` directory with the following variables:
```env .env theme={null}
NEXT_PUBLIC_PROJECT_ID=""
NEXT_PUBLIC_CLIENT_KEY=""
NEXT_PUBLIC_APP_ID=""
```
Install the project dependencies using your preferred package manager:
<CodeGroup>
```bash yarn theme={null}
yarn install
```
```bash npm theme={null}
npm install
```
</CodeGroup>
Start the development server:
<CodeGroup>
```bash yarn theme={null}
yarn dev
```
```bash npm theme={null}
npm run dev
```
</CodeGroup>
Once the app is running, log in with any supported method via Particle Auth. After logging in, the app will display your Universal Account addresses and unified balance.
Universal Accounts are standalone smart accounts. As such, to fund yours, you will need to transfer assets into it—unless you're logging into a pre-existing Universal Account, created through [UniversalX](https://universalx.app) or any other Universal Accounts-enabled app.
Features Walkthrough
1. Universal Account Initialization
After the user logs in, the app creates a new Universal Account instance inside a useEffect.
The constructor requires:
- The connected user’s address.
- Your Particle project credentials.
- Optional config settings.
Within the demo app, this looks as follows:
import { useEthereum } from "@particle-network/authkit";
import { UniversalAccount } from "@particle-network/universal-account-sdk";
// In the app
const { address } = useEthereum();
const ua = new UniversalAccount({
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
projectClientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!,
projectAppUuid: process.env.NEXT_PUBLIC_APP_ID!,
ownerAddress: address,
// If not set it will use auto-slippage
tradeConfig: {
slippageBps: 100, // 1% slippage tolerance
universalGas: true, // Prioritize PARTI token to pay for gas
//usePrimaryTokens: [SUPPORTED_TOKEN_TYPE.SOL], // Specify token to use as source (only for swaps)
},
});
In this case, the user's address is retrieved directly from Particle Auth after they log in.
**Code location:** `page.tsx` → `useEffect` watching `connected && address`
2. Fetching a Universal Account's Addresses
The app fetches a Universal Account's EVM and Solana Universal Account addresses using getSmartAccountOptions().
This returns a UA's:
- EOA/Owner Account (from Particle Auth).
- EVM Universal Account address.
- Solana Universal Account address.
const smartAccountOptions = await universalAccount.getSmartAccountOptions();
The addresses are stored in state and rendered in the UI.
**Code location:** `page.tsx` → `useEffect` watching `universalAccount && address`
3. Fetching A Universal Account's Unified Balance
To show the user's total balance of Primary Assets across chains, the app uses getPrimaryAssets() from the Universal Accounts SDK.
This returns:
totalAmountInUSD
- Detailed breakdown per token + chain (if needed).
Learn how to retrieve a full Primary Assets breakdown on our SDK reference.
The following code snippet shows how to retrieve the user's Primary Assets balance:
const primaryAssets = await universalAccount.getPrimaryAssets();
The UI then displays the account's Primary Assets balance in USD:
<p className="text-2xl font-bold text-green-400">
${primaryAssets?.totalAmountInUSD || "0.00"}
</p>
[Primary Assets](/universal-accounts/cha/chains#primary-assets) are key tokens for which Universal Accounts have deep liquidity. As such, the SDK uses them as the base assets for any cross-chain operation—including gas, swaps, and liquidity routing.
**Code location:** `page.tsx` → `useEffect` watching `universalAccount && address`
4. Sending Transactions
Within the demo app, you can find examples for two types of transactions, both using a Universal Account:
- A custom contract call with its destination on the Base Mainnet.
- A USDT swap on Arbitrum.
Both transactions use the user's Universal Account to handle asset conversion and automate liquidity routing.
Let's take a closer look into both:
Contract Call Example
To interact with a smart contract—such as calling a function like checkIn()—you can use createUniversalTransaction() with the transactions and expectTokens fields.
This method lets you define one or more contract calls while specifying which tokens the contract expects to receive or require:
import { useEthereum } from "@particle-network/authkit";
const { provider } = useEthereum();
import { CHAIN_ID, SUPPORTED_TOKEN_TYPE } from "@particle-network/universal-account-sdk";
// In the app
const transaction = await universalAccount.createUniversalTransaction({
chainId: CHAIN_ID.BASE_MAINNET,
// This example expects 0.0000001 ETH on base mainnet
// If your assets(USDC, USDT, SOL, etc.) are on other chains, the SDK will convert them to ETH on base mainnet
expectTokens: [
{
type: SUPPORTED_TOKEN_TYPE.ETH,
amount: "0.0000001",
},
],
transactions: [
{
to: contractAddress,
data: encodedFunctionData,
value: toBeHex(parseEther("0.0000001")),
},
],
});
const signature = await provider.signMessage(transaction.rootHash);
await universalAccount.sendTransaction(transaction, signature);
In this case, the provider instance of Particle Auth is used to sign the transaction directly.
In this example, the transaction expects 0.0000001 ETH on Base Mainnet. Even if the user doesn't have ETH on Base, Universal Accounts will convert assets from other chains to meet this requirement.
**Code location:** `page.tsx` → `handleTransaction()`
Token Swap Example
To swap tokens (e.g., 1 USDT on Arbitrum in this example), the demo app uses createBuyTransaction().
It does so in the following way:
const transaction = await universalAccount.createBuyTransaction({
token: {
chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT on Arbitrum
},
amountInUSD: "1",
});
const signature = await provider.signMessage(transaction.rootHash);
await universalAccount.sendTransaction(transaction, signature);
The user can use any token on any supported chain to initiate the transfer—Universal Accounts handle conversion to the destination asset and routing automatically.
**Code location:** `page.tsx` → `handleTransferTransaction()`
Full Code Reference
Check out the complete page.tsx file below to see how everything fits together.
You can also copy/paste the below file into your project or use it as a base for your own integration:
"use client";
import { useConnect, useEthereum } from "@particle-network/authkit";
import {
CHAIN_ID,
SUPPORTED_TOKEN_TYPE,
type IAssetsResponse,
UniversalAccount,
} from "@particle-network/universal-account-sdk";
import { Interface, parseEther, toBeHex } from "ethers";
import { useEffect, useState } from "react";
export default function Home() {
// Particle Auth hooks
const { connect, disconnect, connected } = useConnect();
const { address, provider } = useEthereum();
// Transaction state - stores the URL of the latest transaction
const [transactionUrl, setTransactionUrl] = useState("");
// Universal Account instance states
const [universalAccount, setUniversalAccount] =
useState<UniversalAccount | null>(null);
// Smart account addresses for different chains
const [accountInfo, setAccountInfo] = useState({
ownerAddress: "",
evmSmartAccount: "", // EVM-based chains (Ethereum, Base, etc)
solanaSmartAccount: "", // Solana chain
});
// Aggregated balance across all chains
const [primaryAssets, setPrimaryAssets] = useState<IAssetsResponse | null>(
null
);
// === Authentication Handlers ===
const handleLogin = () => {
if (!connected) connect({});
};
const handleDisconnect = () => {
if (connected) disconnect();
};
// === Initialize UniversalAccount ===
useEffect(() => {
if (connected && address) {
// Create new UA instance when user connects
const ua = new UniversalAccount({
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
projectClientKey: process.env.NEXT_PUBLIC_CLIENT_KEY!,
projectAppUuid: process.env.NEXT_PUBLIC_APP_ID!,
ownerAddress: address,
// If not set it will use auto-slippage
tradeConfig: {
slippageBps: 100, // 1% slippage tolerance
universalGas: true, // Enable gas abstraction
//usePrimaryTokens: [SUPPORTED_TOKEN_TYPE.SOL], // Specify token to use as source (only for swaps)
},
});
console.log("UniversalAccount initialized:", ua);
setUniversalAccount(ua);
} else {
// Reset UA when user disconnects
setUniversalAccount(null);
}
}, [connected, address]);
// === Fetch Smart Account Addresses ===
useEffect(() => {
if (!universalAccount || !address) return;
const fetchSmartAccountAddresses = async () => {
// Get smart account addresses for both EVM and Solana
const options = await universalAccount.getSmartAccountOptions();
setAccountInfo({
ownerAddress: address, // EOA address
evmSmartAccount: options.smartAccountAddress || "", // EVM smart account
solanaSmartAccount: options.solanaSmartAccountAddress || "", // Solana smart account
});
console.log("Smart Account Options:", options);
};
fetchSmartAccountAddresses();
}, [universalAccount, address]);
// === Fetch Primary Assets ===
useEffect(() => {
if (!universalAccount || !address) return;
const fetchPrimaryAssets = async () => {
// Get aggregated balance across all chains
// This includes ETH, USDC, USDT, etc. on various chains
const assets = await universalAccount.getPrimaryAssets();
setPrimaryAssets(assets);
};
fetchPrimaryAssets();
}, [universalAccount, address]);
// === Send Cross-chain Transaction ===
const handleTransaction = async () => {
// Safety check - all these are required for transactions
if (!universalAccount || !connected || !provider) {
console.error("Transaction prerequisites not met");
return;
}
const contractAddress = "0x14dcD77D7C9DA51b83c9F0383a995c40432a4578";
const interf = new Interface(["function checkIn() public payable"]);
const transaction = await universalAccount.createUniversalTransaction({
chainId: CHAIN_ID.BASE_MAINNET,
// expect you need 0.0000001 ETH on base mainnet
// if your money(USDC, USDT, SOL, etc.) is on other chain, will convert to ETH on base mainnet
expectTokens: [
{
type: SUPPORTED_TOKEN_TYPE.ETH,
amount: "0.0000001",
},
],
transactions: [
{
to: contractAddress,
data: interf.encodeFunctionData("checkIn"),
value: toBeHex(parseEther("0.0000001")),
},
],
});
const signature = await provider.signMessage(transaction.rootHash);
const sendResult = await universalAccount.sendTransaction(
transaction,
signature
);
setTransactionUrl(
`https://universalx.app/activity/details?id=${sendResult.transactionId}`
);
};
// === Send USDT Transfer Transaction ===
const handleTransferTransaction = async () => {
// Safety check - ensure wallet is connected and UA is initialized
if (!universalAccount || !connected || !provider) {
console.error("Transaction prerequisites not met");
return;
}
const transaction = await universalAccount.createBuyTransaction({
token: {
chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
}, // USDT on Arbitrum
amountInUSD: "1",
});
const signature = await provider.signMessage(transaction.rootHash);
const sendResult = await universalAccount.sendTransaction(
transaction,
signature
);
console.log("Transaction sent:", sendResult);
setTransactionUrl(
`https://universalx.app/activity/details?id=${sendResult.transactionId}`
);
};
return (
<main className="min-h-screen flex items-center justify-center bg-gray-200 text-gray-900 p-4">
<div className="w-full max-w-4xl space-y-8">
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-4xl font-bold text-purple-700">
Universal Accounts Quickstart
</h1>
<p className="text-lg text-gray-600">
Particle Auth + Universal Accounts
</p>
</div>
{!connected ? (
<div className="w-full max-w-md mx-auto bg-gray-50 border border-gray-200 rounded-lg shadow-md p-6">
<div className="text-center mb-6">
<h2 className="text-2xl font-semibold text-gray-900">
Get Started
</h2>
<p className="text-gray-600 mt-2">
Login to get started with Universal Accounts
</p>
</div>
<div className="flex justify-center">
<button
onClick={handleLogin}
className="w-full max-w-xs bg-purple-600 hover:bg-purple-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
>
Login
</button>
</div>
</div>
) : (
<>
{/* Connection Status */}
<div className="w-full bg-gray-50 border border-gray-200 rounded-lg shadow-md p-6 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="space-y-1">
<h2 className="text-lg font-semibold text-gray-700">
Owner Address (EOA)
</h2>
<p className="font-mono text-sm text-purple-700 break-all">
{address}
</p>
</div>
<button
onClick={handleDisconnect}
className="shrink-0 bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200"
>
Disconnect
</button>
</div>
{/* Account Summary */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Smart Accounts */}
<div className="bg-gray-50 border border-gray-200 rounded-lg shadow-md p-6">
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-700">
Universal Account Addresses
</h2>
</div>
<div className="space-y-3">
<div>
<p className="text-sm text-gray-600">EVM</p>
<p className="font-mono text-sm text-purple-700 break-all">
{accountInfo.evmSmartAccount}
</p>
</div>
<div>
<p className="text-sm text-gray-600">Solana</p>
<p className="font-mono text-sm text-purple-700 break-all">
{accountInfo.solanaSmartAccount}
</p>
</div>
</div>
</div>
{/* Balance */}
<div className="bg-gray-50 border border-gray-200 rounded-lg shadow-md p-6 flex flex-col justify-between">
<div className="mb-4">
<h2 className="text-lg font-semibold text-gray-700">
Universal Balance
</h2>
<p className="text-gray-600 text-sm mt-1">
Aggregated{" "}
<a
className="text-purple-700 hover:underline"
href="https://developers.particle.network/universal-accounts/cha/chains#primary-assets"
>
primary assets
</a>{" "}
from every chain
</p>
</div>
<div>
Quickstart: Integrating Universal Accounts into a Web App
This guide walks you through the process of integrating Universal Account features into a working demo app built with Particle Auth and the Universal Accounts SDK.
We’ll explain how user logins, account initialization, and balance display are handled in the code. You’ll also learn how to:
You can use this as a reference or base for your own app.
This example showcases a Next.js app using [Particle Auth](/social-logins/auth/introduction) as its login method.However, the same logic can be applied to any other EOA-based provider or signer.
Getting Started
To start integrating Universal Accounts:
This repository includes the full working demo app.Features Walkthrough
1. Universal Account Initialization
After the user logs in, the app creates a new Universal Account instance inside a
useEffect.The constructor requires:
Within the demo app, this looks as follows:
2. Fetching a Universal Account's Addresses
The app fetches a Universal Account's EVM and Solana Universal Account addresses using
getSmartAccountOptions().This returns a UA's:
The addresses are stored in state and rendered in the UI.
**Code location:** `page.tsx` → `useEffect` watching `universalAccount && address`3. Fetching A Universal Account's Unified Balance
To show the user's total balance of Primary Assets across chains, the app uses
getPrimaryAssets()from the Universal Accounts SDK.This returns:
- Detailed breakdown per token + chain (if needed).
Learn how to retrieve a full Primary Assets breakdown on our SDK reference.totalAmountInUSDThe following code snippet shows how to retrieve the user's Primary Assets balance:
The UI then displays the account's Primary Assets balance in USD:
4. Sending Transactions
Within the demo app, you can find examples for two types of transactions, both using a Universal Account:
Both transactions use the user's Universal Account to handle asset conversion and automate liquidity routing.
Let's take a closer look into both:
Contract Call Example
To interact with a smart contract—such as calling a function like
checkIn()—you can usecreateUniversalTransaction()with thetransactionsandexpectTokensfields.This method lets you define one or more contract calls while specifying which tokens the contract expects to receive or require:
In this case, the
In this example, the transaction expects 0.0000001 ETH on Base Mainnet. Even if the user doesn't have ETH on Base, Universal Accounts will convert assets from other chains to meet this requirement. **Code location:** `page.tsx` → `handleTransaction()`providerinstance of Particle Auth is used to sign the transaction directly.Token Swap Example
To swap tokens (e.g., 1 USDT on Arbitrum in this example), the demo app uses
createBuyTransaction().It does so in the following way:
Full Code Reference
Check out the complete
page.tsxfile below to see how everything fits together.You can also copy/paste the below file into your project or use it as a base for your own integration: