Skip to main content
The Universal Accounts SDK provides chain abstraction for your app by integrating Universal Accounts. These, in turn, offer your users a single account, balance, and interaction point across EVM chains and Solana. Our SDK seamlessly integrates with your existing connection flow, with minimal setup.

Learn More About Universal Accounts

What are Universal Accounts, how do they work, and what problems do they solve?
To integrate Universal Accounts:
The following examples use a React-based app alongside Particle Auth for authentication and wallet connection.However, the Universal Accounts SDK is provider-agnostic—alongside it, you can also use Particle Connect, Web3Modal, RainbowKit, or any signer.You can also use the SDK to construct and sign transactions programmatically on the server side.
1

Connect a user's account

A user logs into their account by connecting a wallet or via a social login.
2

Initialize Universal Accounts

Once connected, pass the user’s EOA address to the SDK and configure your project’s details.
3

Use the UA instance

Use the returned Universal Account instance to fetch data and send transactions across chains.When sending a transaction, the SDK will create a UserOperation and return a rootHash. This hash must be signed by the connected EOA, then passed back into sendTransaction() to broadcast.
Under the hood, all routing, bridging, and gas abstraction will be handled by Particle Network’s infrastructure.

Getting Started

Installation

Once your app is set up, install the Universal Accounts SDK:
The SDK depends on ethers.js internally, but you’re not required to use it in your application.You can use any provider or signer logic that fits your setup.
yarn add @particle-network/universal-account-sdk ethers

Import and Configure

You can access and import the UniversalAccount class in your app from @particle-network/universal-account-sdk:
import { UniversalAccount } from "@particle-network/universal-account-sdk";
Then, initialize the UA instance once a user has connected:
The Universal Account SDK requires Particle project credentials from the Particle Dashboard.
To retrieve those values for configuration within your application, follow these steps:
Sign up or Log in into the Particle dashboard
Login into Particle.
Create Particle project.
Create web app.
Find app's credentials.
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
    //usePrimaryTokens: [SUPPORTED_TOKEN_TYPE.SOL], // Specify token to use as source (only for swaps)
  },
});
You can now use the ua instance to fetch data (a Universal Account’s balance or addresses) and send transactions.

Check Out UA Initialization in this Sample Repository

Repository of sample Next.js app with social logins via Particle Auth + UA.

Control which tokens are used for swaps

When initializing a Universal Account, you can control which tokens are eligible to be used as the source for swap operations. To do this, set the usePrimaryTokens field inside the tradeConfig object. This lets you restrict swap logic to specific tokens (e.g. only allow SOL to be spent, not USDT or ETH). Example:
tradeConfig: {
  usePrimaryTokens: [SUPPORTED_TOKEN_TYPE.SOL],
}
This is useful if you want to:
  • Ensure predictable token usage during swaps
  • Prevent certain tokens from being auto-selected as swap input
  • Customize the user experience around token prioritization

Get a Universal Account’s Addresses

A Universal Account is composed of multiple addresses, each relevant to a specific interaction layer:
  1. Owner Address: The EOA that owns the Universal Account and signs transactions (e.g., from MetaMask or via a social login).
  2. EVM Universal Address: The UA address used on EVM-compatible chains.
  3. Solana Universal Address: The UA address used on Solana.
The EVM and Solana Universal Addresses are distinct due to the way deposits work on each network.You can deposit any EVM token to the EVM Universal Address, and any Solana token to the Solana Universal Address. EVM and Solana assets will be accessible through the same UA instance, and balance lookups and transactions will remain unified at the SDK level.
You can retrieve all relevant addresses from an initialized Universal Account instance as follows:
const smartAccountOptions = await ua.getSmartAccountOptions();

const accountInfo = {
  ownerAddress: smartAccountOptions.ownerAddress, // EOA that owns the Universal Account
  evmUaAddress: smartAccountOptions.smartAccountAddress!, // EVM UA
  solanaUaAddress: smartAccountOptions.solanaSmartAccountAddress!, // SOL UA
};

console.log("Smart Account info:", accountInfo);

UA Info Management

This repository includes a sample Next.js app with social logins via Particle Auth alongside Universal Accounts.

Primary Assets & Unified Balance

Universal Accounts can hold many assets across supported chains.
Among these, Primary Assets are special: they have the deepest liquidity and can be used for cross-chain swaps, liquidity routing, and gas payments.
Why this matters: You can give users a single, unified balance of their spendable assets—regardless of which chain those assets are actually on.
The full list of supported Primary Assets is available on the Supported chains and Primary Assets page.

Fetch Unified Balance (the quick way)

The easiest way to display a user’s total value is by fetching their Unified Balance—the sum of all Primary Assets across chains:
const primaryAssets = await ua.getPrimaryAssets();
console.log("Unified Balance:", primaryAssets.totalAmountInUSD);
This gives you a single number (totalAmountInUSD) that represents the cross-chain total balance of Primary Assets in USD. Perfect for showing a dashboard balance or portfolio total in one call.

Inspect Primary Assets in Detail

If you need more than just the total, getPrimaryAssets() also returns a detailed list of assets:
const primaryAssets = await ua.getPrimaryAssets();
console.log("Primary Assets:", JSON.stringify(primaryAssets, null, 2));
The following is the structure of the response:
{
  assets: AssetInfo[],       // assets breakdown (per-token and chain)
  totalAmountInUSD: number   // unified total
}
Each AssetInfo entry aggregates a token across chains, including per-chain breakdowns.
FieldDescription
tokenTypeToken identifier (e.g., “eth”, “usdt”)
priceCurrent USD price
amountTotal amount across chains (human-readable)
amountInUSDTotal USD value
chainAggregationPer-chain balance breakdowns

chainAggregation format

Each chainAggregation entry details the balance and metadata of the token on a specific chain:
FieldDescription
token.chainIdChain ID
token.addressToken contract address
amountToken amount (human-readable float)
amountInUSDUSD value
rawAmountToken amount in raw units (integer, stringified)
token.decimalsERC-20 decimals
token.realDecimalsAdjusted decimals for display
token.isMultiChainPart of multi-chain registry
token.isMultiChainDefaultDefault canonical version across chains
For native assets like ETH, the token.address field will be 0x0000000000000000000000000000000000000000.

Sending a Transfer Transaction

The Universal Accounts SDK lets you send tokens to any address across supported chains using the createTransferTransaction() method. Like other transactions, transfers don’t require the user to hold assets or gas tokens on the destination chain—liquidity and gas are abstracted behind the scenes. Once you construct the transfer, the SDK returns a rootHash to sign. You sign it with the connected EOA (e.g., from Particle Auth), then call sendTransaction() to broadcast:
import { CHAIN_ID, UniversalAccount } from "@particle-network/universal-account-sdk";
import { useEthereum } from "@particle-network/authkit";

const { provider } = useEthereum();

const transaction = await ua.createTransferTransaction({
  token: {
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
    address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT on Arbitrum
  },
  amount: "0.1", // Amount to send (human-readable string)
  receiver: receiverAddress, // Target address
});

const signature = await provider.signMessage(transaction.rootHash);
const result = await ua.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
For native assets like ETH, the token address will be 0x0000000000000000000000000000000000000000.
The returned TransactionResult will include the transaction ID, as well as metadata like token movements and fee breakdowns. You can find more details about this in the TransactionResult section below.

View a Sample Transfer Transaction

See how to send cross-chain transfers in a demo Next.js app leveraging Universal Accounts and Particle Auth.

Sending a Custom Payable Transaction

The Universal Accounts SDK supports sending contract interactions, including payable transactions, through the createUniversalTransaction() method. In this example, we interact with a smart contract on the Base Mainnet that requires exactly 0.0000001 ETH to execute a checkIn() function. By specifying an expectTokens array, the SDK ensures the account has the necessary ETH on Base—even if the user’s assets are on other chains or in different tokens (e.g., USDC, USDT). The SDK will handle all additional required cross-chain routing and token conversion under the hood. Once the transaction is created, it will return a rootHash value representing the payload to be signed. You can then use a signer (e.g., Particle Auth) to sign this hash and broadcast it using sendTransaction(). The following code snippet shows how to use the Universal Accounts SDK to send a payable transaction:
import { CHAIN_ID, SUPPORTED_TOKEN_TYPE } from "@particle-network/universal-account-sdk";
import { Interface, parseEther, toBeHex } from "ethers";
import { useEthereum } from "@particle-network/authkit";

// Extract the provider from Particle Auth
const { provider } = useEthereum();

const contractAddress = "0x14dcD77D7C9DA51b83c9F0383a995c40432a4578";
const interf = new Interface(["function checkIn() public payable"]);

const transaction = await ua.createUniversalTransaction({
  chainId: CHAIN_ID.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 result = await ua.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
The returned TransactionResult will include the transaction’s ID and metadata like token movements and fee breakdowns.

Sending a Buy Transaction

The Universal Accounts SDK supports initiating buy/swap transactions directly through the createBuyTransaction() method. This allows you to programmatically route an amount in USD into a target token (e.g., USDT on Arbitrum), without requiring the user to hold funds on the destination chain. Once the transaction is created, it returns a rootHash value representing the payload to be signed. You then use your signer (in this case, Particle Auth) to sign the message, and pass the result into sendTransaction() to broadcast it:
You can specify the specific tokens you want to use as source for the swap by setting the usePrimaryTokens property in the tradeConfig object when initializing the Universal Account.
tradeConfig: {
    usePrimaryTokens: [SUPPORTED_TOKEN_TYPE.SOL],
  },
import { CHAIN_ID, UniversalAccount } from "@particle-network/universal-account-sdk";
import { useEthereum } from "@particle-network/authkit";

// extract the provider from Particle Auth
const { provider } = useEthereum();

// In your app
const transaction = await ua.createBuyTransaction({
  token: {
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
    address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT on Arbitrum
  },
  amountInUSD: "10", // Target amount in USD sourced from primary assets held
});

const signature = await provider.signMessage(transaction.rootHash);
const result = await ua.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
The sendTransaction method will then return a TransactionResult object, which includes the transaction ID and other metadata.

Sample Swap Transaction

See how to initiate a swap transaction in a demo Next.js app using both Particle Auth and Universal Accounts.

Sending a Sell Transaction

The Universal Accounts SDK supports initiating sell/swap transactions through the createSellTransaction() method. This allows you to programmatically sell a token (e.g., ARB on Arbitrum) into a target primary asset, with the transaction routed through Particle’s Chain Abstraction layer. Once the transaction is created, it returns a rootHash payload that needs to be signed. You then use your signer (e.g., Particle Auth) to sign and pass it into sendTransaction() to broadcast.
The amount you specify is the raw token amount to be sold (no decimals adjustment needed). Ensure the Universal Account has enough balance of the token before calling createSellTransaction().
import { CHAIN_ID, UniversalAccount } from "@particle-network/universal-account-sdk";
import { useEthereum } from "@particle-network/authkit";

// extract the provider from Particle Auth
const { provider } = useEthereum();

// In your app
const transaction = await ua.createSellTransaction({
  token: {
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
    address: "0x912CE59144191C1204E64559FE8253a0e49E6548", // ARB on Arbitrum
  },
  amount: "0.1", // Amount of token to sell
});

const signature = await provider.signMessage(transaction.rootHash);
const result = await ua.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
The sendTransaction() method returns a TransactionResult object with the transaction ID and other metadata.

Sending a Conversion Transaction

You can convert between Primary Assets with the createConvertTransaction method. The example below demonstrates how to convert any primary asset into another—USDC on Arbitrum, in this case:
import { CHAIN_ID, SUPPORTED_TOKEN_TYPE, UniversalAccount } from "@particle-network/universal-account-sdk";
import { useEthereum } from "@particle-network/authkit";

// extract the provider from Particle Auth
const { provider } = useEthereum();

// In your app
const transaction = await ua.createConvertTransaction({
    expectToken: { type: SUPPORTED_TOKEN_TYPE.USDC, amount: '1' },
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
});

const signature = await provider.signMessage(transaction.rootHash);
const result = await ua.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
This method is useful to convert assets directly to the target chain.

Solana Transactions

Universal Accounts support Solana as well. You can swap to and from SOL using the createTransferTransaction() method, even if you hold no assets on Solana. Here is an example:
import { CHAIN_ID } from "@particle-network/universal-account-sdk";
import { useEthereum } from "@particle-network/authkit";

// extract the provider from Particle Auth
const { provider } = useEthereum();

// In your app
const transaction = await ua.createBuyTransaction({
    // Buy sol
    // token: { chainId: CHAIN_ID.SOLANA_MAINNET, address: "0x0000000000000000000000000000000000000000" },
    
    // Buy a token
    token: { chainId: CHAIN_ID.SOLANA_MAINNET, address: "6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN" },
    // buy $0.001 of trump
    amountInUSD: "0.001",
});

const signature = await provider.signMessage(transaction.rootHash);
const result = await universalAccount.sendTransaction(transaction, signature);

console.log("Explorer URL:", `https://universalx.app/activity/details?id=${result.transactionId}`);
Even if your Universal Account doesn’t hold SOL for gas, you can still purchase SOL or any other Solana token. Universal Accounts will automatically handle routing and liquidity across chains to cover gas fees.
Solana transactions can be signed with any provider compatible with the Universal Accounts SDK. In this case, Particle Auth is used to sign the rootHash. You can combine this swap transaction with a SOL transfer to automatically convert EVM-based assets to SOL and send them to another account in a single flow.

Transaction Preview

The transaction object returned by methods like createTransferTransaction() provides a full preview of the transaction before it’s executed. This includes key details such as estimated fees, token transfers, and other relevant metadata—allowing you to display clear, actionable information to users before confirmation. For example:
page.tsx
const transaction = await universalAccount.createBuyTransaction({
  token: {
    chainId: CHAIN_ID.BSC_MAINNET,
    address: "0x59264f02D301281f3393e1385c0aEFd446Eb0F00", // PARTI token on BNB Chain
  },
  amountInUSD: "1",
});
The returned transaction object includes metadata such as sender and recipient addresses, tokens used, and estimated fees. Here’s how to extract and display the estimated fees:
page.tsx
import { formatUnits } from "ethers";

const feeQuote = transaction.feeQuotes[0];
const fee = feeQuote.fees.totals;

console.log("Total fee (USD):", `$${formatUnits(fee.feeTokenAmountInUSD, 18)}`);
console.log("Gas fee (USD):", `$${formatUnits(fee.gasFeeTokenAmountInUSD, 18)}`);
console.log("Service fee (USD):", `$${formatUnits(fee.transactionServiceFeeTokenAmountInUSD, 18)}`);
console.log("LP fee (USD):", `$${formatUnits(fee.transactionLPFeeTokenAmountInUSD, 18)}`);
For a full breakdown of the preview structure and practical usage examples, see the Preview Transaction Details with Universal Accounts guide.

sendTransaction() Response Structure

After broadcasting a transaction with sendTransaction(), the SDK will return a detailed object containing its execution status, fee breakdowns, token flows, and analytics. Below is a breakdown of the key fields within this object:
FieldDescription
transactionIdUnique ID of the transaction (used to query status or activity details)
modeNetwork mode, typically "mainnet" or "testnet"
sender / receiverAddress that initiated and received the transaction (usually same)
typeTransaction type (e.g. "universal")
statusExecution status code (internal enum)
tagTransaction tag (e.g., "buy" or "swap")
created_at / updated_atISO timestamps for lifecycle tracking
FieldDescription
totals.feeTokenAmountInUSDTotal fee in USD
feeTokens[]List of tokens used to pay fees, with symbols and USD values
freeGasFee / freeServiceFeeWhether any component was waived
Tokens deducted from the user’s account to fund the transaction. Each entry includes:
  • token.symbol
  • token.chainId
  • amount and amountInUSD
  • Full metadata (decimals, icon, etc.)
Provides the most useful high-level insight into what changed:
FieldDescription
decr[]Tokens deducted from the user (chain, token, amount)
incr[]Tokens received by the user
swaps[]Swap routes (e.g. from USDC to USDT via 1inch)
tokenBalances[]Final post-transaction token balances
FieldDescription
slippageSlippage used for the route (in basis points)
totalFeeInUSDFinal USD fee value
totalDecrAmountInUSDTotal USD value deducted
totalIncrAmountInUSDTotal USD value received
priceImpactEstimated price impact (0 if none)
minReceiveAmountInUSDMinimum expected amount (to be received, post-slippage) in USD
minReceiveTokenToken targeted by the buy/swap action

Using Particle Connect with Universal Accounts

The following example uses Particle Connect instead of Particle Auth. It shows how to sign and send a Universal Account transaction using a connected wallet.
When using Particle Connect, you can access the connected wallet through the useWallets() hook. The primaryWallet object exposes a walletClient, which acts as the signer. This lets you sign the Universal Account transaction payload (rootHash) using any wallet connected via Particle Connect. The following code snippet shows how to use the Universal Accounts SDK to sign a transaction with Particle Connect:
import { useWallets, useAccount } from "@particle-network/connectkit";
import { CHAIN_ID } from "@particle-network/universal-account-sdk";

// Get wallet from Particle Connect
const [primaryWallet] = useWallets();
const walletClient = primaryWallet?.getWalletClient();
const { address } = useAccount();

// Create a cross-chain transfer transaction via Universal Accounts
const transaction = await ua.createTransferTransaction({
  token: {
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
    address: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // USDT on Arbitrum
  },
  amount: "0.1", // Amount to send (human-readable string)
  receiver: receiverAddress, // Target address
});

// Sign the transaction's root hash using connected wallet
const signature = await walletClient?.signMessage({
  account: address as `0x${string}`,
  message: { raw: transaction.rootHash },
});

// Send the signed transaction via Universal Account SDK
const sendResult = await universalAccount.sendTransaction(
  transaction,
  signature
);

// Log UniversalX explorer link
console.log(
  "Explorer URL:",
  `https://universalx.app/activity/details?id=${sendResult.transactionId}`
);

Using Universal Accounts in the backend

The Universal Accounts SDK can also be used in backend environments to construct and sign transactions programmatically. The example below demonstrates usage with ethers.js and a private key in Node.js:
import { UniversalAccount, CHAIN_ID } from "@particle-network/universal-account-sdk";
import { Wallet, getBytes } from "ethers";

// Initialize wallet
const wallet = new Wallet("PRIVATE_KEY_OR_MNEMONIC");

// Create a Universal Account instance
const ua = new UniversalAccount({
  projectId: "UA_PROJECT_ID",
  ownerAddress: wallet.address,
  tradeConfig: {
    slippageBps: 100,      // Set slippage to 1% (100 basis points)
    universalGas: true     // Use PARTI tokens to cover gas fees
  }
});

// Create a transaction to buy $0.1 worth of ARB on Arbitrum
const tx = await ua.createBuyTransaction({
  token: {
    chainId: CHAIN_ID.ARBITRUM_MAINNET_ONE,
    address: "0x912CE59144191C1204E64559FE8253a0e49E6548" // ARB token contract
  },
  amountInUSD: "0.1"
});

// Sign and send the transaction
const result = await ua.sendTransaction(tx, wallet.signMessageSync(getBytes(tx.rootHash)));

// Log the transaction result and link to explorer
console.log("Transaction ID:", result.transactionId);
console.log("View on Explorer:", `https://universalx.app/activity/details?id=${result.transactionId}`);

Registering a UniversalX Account

The Universal Accounts SDK also allows you to register an account on UniversalX, a chain-abstracted trading platform built upon Universal Accounts. This can automatically onboard your users into UniversalX when they create or initialize a Universal Account. UniversalX registration is optional and only needs to be done once per UA.
import { UniversalAccount, createUnsignedMessage } from "@particle-network/universal-account-sdk";
import { randomUUID } from "crypto";

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: 'USER_ADDRESS',
});

// Fetch smart account info to get the UA address
const smartAccountOptions = await ua.getSmartAccountOptions();

// Optional invite code
const inviteCode = "000000";

// Prepare registration payload
const deviceId = randomUUID();
const timestamp = Date.now();

const unsignedMessage = createUnsignedMessage(
  smartAccountOptions.smartAccountAddress!,
  deviceId,
  timestamp
);

// Sign message using EOA (can be a backend wallet)
const signature = provider.signMessage(unsignedMessage);

// Register the UniversalX account
const result = await ua.register(inviteCode, deviceId, timestamp, signature);

if (!!result.token) {
  console.log("Registration successful.");
} else {
  console.error("Registration failed:", result);
}
You can also pass an invite code ("000000" by default) if you have one.
I