Wallet As A Service Usage Guide

UTXOS Wallet as a Service provides a hosted wallet solution that eliminates the need for users to install browser extensions or manage private keys. It’s perfect for onboarding new users to your Cardano dApp with minimal friction.

Integration Options

There are three main ways to integrate UTXOS wallet as a service:

  1. @meshsdk/web3-sdk - For any TypeScript/JavaScript applications
  2. @meshsdk/react - For React/Next.js applications with pre-built UI components

Prerequisites

Before you begin, you need to set up a project:

1. Create A Free Account

  1. Create an account by logging in with one of the supported providers:
Login illustration

2. Create A Project

From the dashboard:

  1. Click “Create New Project”
  2. Enter your project name and http:localhost:3000/ for now
Login illustration

3. Get Your UTXOS Credentials

  1. Copy your Project ID from project settings - Used to identify your project
Login illustration
  1. Save this value into your codebase .env file
# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id # safe to expose on client

4. Blockchain Data Provider

This is boilerplate to work with Cardano and not specific to UTXOS usage

  1. Get a free API key from Blockfrost or any another supported provider
  2. Edit your .env file to expose new BLOCKFROST_API_KEY to the server
  3. Find out how to setup blockfrost

Method 1: Using @meshsdk/web3-sdk

This method is ideal for both client and server-side applications, or any JavaScript/TypeScript environment where you need programmatic wallet access.

TLDR: Minimal required code to integrate wallet as a service:

import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
 
// Configure UTXOS wallet options
const options: EnableWeb3WalletOptions = {
  networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: preprod, 1: mainnet
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID, // https://utxos.dev/dashboard
};
 
// Enable the wallet
const wallet = await Web3Wallet.enable(options);

1. Install Dependencies

Install the required packages:

npm install @meshsdk/web3-sdk @meshsdk/provider

2. Set Up Environment Variables

Edit your .env file with your project id, blockchain provider api key, and the network:

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id # https://utxos.dev/dashboard
BLOCKFROST_API_KEY_PREPROD=your_blockfrost_api_key # https://blockfrost.io/dashboard
NEXT_PUBLIC_NETWORK_ID=0  # 0 for preprod, 1 for mainnet

3. Initialize The Wallet

Create a wallet instance with proper error handling:

import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
import { BlockfrostProvider } from "@meshsdk/provider";
 
async function initializeWallet() {
  try {
    // Initialize the blockchain data provider with secure api endpoint
    // const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY_PREPROD); // quick start method (insecure)
    const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
 
    // Configure UTXOS wallet options
    const options: EnableWeb3WalletOptions = {
      networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: preprod, 1: mainnet
      fetcher: provider,
      submitter: provider,
      projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID, // https://utxos.dev/dashboard
    };
 
    // Enable the wallet
    const wallet = await Web3Wallet.enable(options);
 
    console.log("Wallet initialized successfully");
    return wallet;
  } catch (error) {
    console.error("Failed to initialize wallet:", error);
    throw error;
  }
}
 
// Usage
const wallet = await initializeWallet();

4. Verify Wallet Connection

Test that your wallet is working correctly, for example by fetching the wallet address:

const address = await wallet.getChangeAddress();
 
// Chain-specific operations must use the appropriate chain wallet
// For Cardano
if (wallet.cardano) {
  const utxos = await wallet.cardano.getUtxos();
  console.log("Cardano UTXOs:", utxos);
}
 
// For Bitcoin
if (wallet.bitcoin) {
  const utxos = await wallet.bitcoin.getUtxos();
  console.log("Bitcoin UTXOs:", utxos);
}

Method 2: Using @meshsdk/react

This method provides pre-built React components and hooks, making it perfect for React/Next.js applications with minimal setup.

TLDR: Minimal required code to integrate wallet as a service:

import { CardanoWallet, useWallet } from "@meshsdk/react";
import { BlockfrostProvider } from "@meshsdk/core";
 
const provider = new BlockfrostProvider(`/api/blockfrost/preprod/` || `YOUR_API_KEY`);
 
<CardanoWallet
  web3Services={{
    networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0,
    fetcher: provider,
    submitter: provider,
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  }}
/>

1. Install Dependencies

npm install @meshsdk/core @meshsdk/react

2. Set Up The Wallet Provider

Wrap your app with the MeshProvider to make wallet context available throughout your application:

// app/layout.js or pages/_app.js (Next.js) or App.jsx (React)
import "@meshsdk/react/styles.css";
import { MeshProvider } from "@meshsdk/react";
 
export default function App({ Component, pageProps }) {
  return (
    <MeshProvider>
      <Component {...pageProps} />
    </MeshProvider>
  );
}

3. Create A Wallet Connection Component

Build a component that handles wallet connection with user feedback:

import { CardanoWallet, useWallet } from "@meshsdk/react";
import { BlockfrostProvider } from "@meshsdk/core";
import { useState, useEffect } from "react";
 
// const provider = new BlockfrostProvider(process.env.NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD); // Quick start method (insecure)
const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
 
export default function WalletConnection() {
  const { wallet, connected, connecting } = useWallet();
  const [balance, setBalance] = useState(null);
  const [error, setError] = useState(null);
 
  // Fetch wallet balance when connected
  useEffect(() => {
    async function fetchBalance() {
      if (connected && wallet) {
        try {
          const utxos = await wallet.getUtxos();
          const totalLovelace = utxos.reduce((sum, utxo) => {
            const lovelace = utxo.output.amount.find(
              (asset) => asset.unit === "lovelace",
            );
            return sum + parseInt(lovelace?.quantity || "0");
          }, 0);
          setBalance(totalLovelace / 1_000_000);
        } catch (err) {
          setError("Failed to fetch balance");
          console.error(err);
        }
      }
    }
 
    fetchBalance();
  }, [connected, wallet]);
 
  return (
    <div className="rounded-lg bg-white p-6 shadow-md">
      <h2 className="mb-4 text-xl font-bold">Wallet Connection</h2>
 
      {!connected ? (
        <div>
          <p className="mb-4 text-gray-600">
            Connect your wallet to start using the application
          </p>
          <CardanoWallet
            web3Services={{
              networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0,
              fetcher: provider,
              submitter: provider,
              projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
            }}
          />
          {connecting && (
            <p className="mt-2 text-blue-600">Connecting wallet...</p>
          )}
        </div>
      ) : (
        <div>
          <p className="font-medium text-green-600">✅ Wallet Connected</p>
          {balance !== null && (
            <p className="mt-2">Balance: {balance.toFixed(2)} ADA</p>
          )}
          {error && <p className="mt-2 text-red-600">{error}</p>}
        </div>
      )}
    </div>
  );
}

4. Use Wallet In Components

Access wallet functionality throughout your app:

import { useWallet } from "@meshsdk/react";
 
export default function TransactionComponent() {
  const { wallet, connected } = useWallet();
 
  const handleTransaction = async () => {
    if (!connected || !wallet) {
      alert("Please connect your wallet first");
      return;
    }
 
    try {
      const address = await wallet.getChangeAddress();
      console.log("Sending from:", address);
 
      // Your transaction logic here
      const tx = '';
 
      const signedTx = await wallet.signTx(tx);
    } catch (error) {
      console.error("Transaction failed:", error);
    }
  };
 
  return (
    <button
      onClick={handleTransaction}
      disabled={!connected}
      className="rounded bg-blue-500 px-4 py-2 text-white disabled:opacity-50"
    >
      Test Transaction
    </button>
  );
}

Wallet API Reference

The wallet provides a comprehensive API for blockchain interactions. See all available methods in the SDK documentation.

Here are some common operations you can perform with the wallet:

Address Management

// Get the primary wallet address (works for any chain)
const changeAddress = await wallet.getChangeAddress();
 
// Chain-specific address operations
// Cardano operations
if (wallet.cardano) {
  // Get all wallet addresses (if multiple)
  const addresses = await wallet.cardano.getUsedAddresses();
 
  // Get unused addresses for receiving funds
  const unusedAddresses = await wallet.cardano.getUnusedAddresses();
}
 
// Bitcoin operations
if (wallet.bitcoin) {
  // Get Bitcoin address
  const bitcoinAddress = wallet.bitcoin.getAddress();
}

UTXO And Assets

// Chain-specific UTXO operations
// Cardano operations
if (wallet.cardano) {
  // Get all UTXOs for the wallet
  const utxos = await wallet.cardano.getUtxos();
 
  // Get collateral UTXOs (for smart contract interactions)
  const collateral = await wallet.cardano.getCollateral();
}
 
// Bitcoin operations
if (wallet.bitcoin) {
  // Bitcoin UTXOs follow a different structure
  const utxos = await wallet.bitcoin.getUtxos();
}

Sign Operations

// Sign a transaction (available in main wallet for both chains)
const signedTx = await wallet.signTx(unsignedTx, (partialSign = false));
 
// Chain-specific transaction operations
// Cardano operations
if (wallet.cardano) {
  // Submit a signed transaction to the Cardano network
  const txHash = await wallet.cardano.submitTx(signedTx);
}
 
// Sign arbitrary data (for authentication/verification)
// Available in main wallet for both chains
const signature = await wallet.signData(address, message);

Asset Information

// Chain-specific asset operations
// Cardano operations
if (wallet.cardano) {
  // Get wallet balance (including native assets)
  const balance = await wallet.cardano.getBalance();
 
  // Get specific asset balance
  const assetBalance = await wallet.cardano.getAssets();
 
  // Get policy IDs of assets in wallet
  const policyIds = await wallet.cardano.getPolicyIds();
 
  // Get assets from a specific policy ID
  const assets = await wallet.cardano.getPolicyIdAssets("asset_policy_id");
}

Wallet Export

Allow user to view their private keys for backup or migration purposes:

// Export wallet data
await wallet.exportWallet();

This will open a secure Iframe for the user to export their private keys

Complete Examples

Example 1: Simple Transaction

Here’s a complete example of sending ADA from the wallet:

import { Web3Wallet, EnableWeb3WalletOptions } from "@meshsdk/web3-sdk";
import { BlockfrostProvider, MeshTxBuilder } from "@meshsdk/core";
 
async function sendADA(recipientAddress, amountADA) {
  try {
    // Initialize provider and wallet
    const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
 
    const options: EnableWeb3WalletOptions = {
      networkId: 0, // preprod
      fetcher: provider,
      submitter: provider,
      projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    };
 
    const wallet = await Web3Wallet.enable(options);
 
    // Build transaction
    const tx = new MeshTxBuilder({
      fetcher: provider,
    });
 
    const amountLovelace = (amountADA * 1_000_000).toString();
    
    // Build the asset unit (policy ID + asset name)
    const assetUnit = policyId + assetName;
 
    tx.txOut(recipientAddress, [
      { unit: "lovelace", quantity: amountLovelace }
    ])
    tx.txOut(recipientAddress, [
      { unit: "lovelace", quantity: "1500000" },
      { unit: assetUnit, quantity: "1" }, // NFT
    ])
    .changeAddress(await wallet.getChangeAddress())
    .selectUtxosFrom(await wallet.cardano.getUtxos());
 
    // Complete, sign, and submit
    const unsignedTx = await tx.complete();
    console.log("Transaction built successfully");
 
    const signedTx = await wallet.signTx(unsignedTx);
    console.log("Transaction signed");
 
    const txHash = await wallet.cardano.submitTx(signedTx);
    console.log("Transaction submitted:", txHash);
 
    return txHash;
  } catch (error) {
    console.error("Transaction failed:", error);
    throw error;
  }
}
 
// Usage
await sendADA("addr_test1...", 5); // Send 5 ADA

Example 2: Smart Contract Interaction

Interact with a smart contract using the wallet:

import { PlutusScript, resolvePaymentKeyHash } from "@meshsdk/core";
 
async function interactWithContract(contractAddress, datum, redeemer) {
  try {
    const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
    const wallet = await Web3Wallet.enable({
      networkId: 0,
      fetcher: provider,
      submitter: provider,
      projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    });
 
    // Get script UTXOs at the contract address
    const scriptUtxos = await provider.fetchUTXOs(contractAddress);
 
    if (scriptUtxos.length === 0) {
      throw new Error("No UTXOs found at contract address");
    }
 
    const tx = new MeshTxBuilder({ fetcher: provider });
 
    // Consume from script
    tx
      .txIn(
        scriptUtxos[0].input.txHash,
        scriptUtxos[0].input.outputIndex,
        scriptUtxos[0].output.amount,
        contractAddress,
      )
      .spendingPlutusScript(PlutusScript.fromCbor("your_script_cbor"))
      .txInInlineDatumPresent()
      .txInRedeemerValue(redeemer)
 
      // Add collateral for script execution
      .txInCollateral(
        (await wallet.cardano.getCollateral())[0].input.txHash,
        (await wallet.cardano.getCollateral())[0].input.outputIndex,
        (await wallet.cardano.getCollateral())[0].output.amount,
        (await wallet.cardano.getCollateral())[0].output.address,
      )
 
      // Output back to wallet
      .txOut(await wallet.getChangeAddress(), [
        { unit: "lovelace", quantity: "2000000" },
      ])
      .changeAddress(await wallet.getChangeAddress())
      .selectUtxosFrom(await wallet.cardano.getUtxos())
      .requiredSignerHash(
        resolvePaymentKeyHash(await wallet.getChangeAddress()),
      );
 
    const unsignedTx = await tx.complete();
    const signedTx = await wallet.signTx(unsignedTx);
    const txHash = await wallet.submitTx(signedTx);
 
    console.log("Contract interaction successful:", txHash);
    return txHash;
  } catch (error) {
    console.error("Contract interaction failed:", error);
    throw error;
  }
}

Security Considerations

Never expose your API keys in client-side code

⚠️
  • Never expose your API keys in client-side code - Always validate addresses before sending transactions - Implement proper error handling for production applications - Use environment variables for sensitive configuration - Test thoroughly on testnet before mainnet deployment - Verify the correct chain is used when working with multi-chain applications

Set Up Blockchain Data Provider

This is boilerplate to work with Cardano and not specific to UTXOS usage

Edit your .env file to expose new BLOCKFROST_API_KEY to the server

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
BLOCKFROST_API_KEY_PREPROD=your_blockfrost_api_key # never expose on client
  1. We need a safe way to consume BLOCKFROST_API_KEY without exposing it’s value. The solution varies slightly for your environment. In Next.js (App directory) we create a new file called app/api/blockfrost/[...slug]/route.ts and paste in this function.
async function handleBlockfrostRequest(
  request,
  context,
) {
  try {
    const { params } = context;
    const slug = params.slug || [];
    const network = slug[0];
 
    // Network configuration
    const networkConfig = getNetworkConfig(network);
    if (!networkConfig.key) {
      return {
        status: 500,
        headers: { "Content-Type": "application/json" },
        body: { error: `Missing Blockfrost API key for network: ${network}` },
      };
    }
 
    // Construct endpoint
    const endpointPath = slug.slice(1).join("/") || "";
    const queryString = getQueryString(request.url);
    const endpoint = endpointPath + queryString;
 
    // Set headers
    const headers = {
      project_id: networkConfig.key,
    };
 
    if (endpointPath === "tx/submit" || endpointPath === "utils/txs/evaluate") {
      headers["Content-Type"] = "application/cbor";
    } else {
      headers["Content-Type"] = "application/json";
    }
 
    // Forward request to Blockfrost
    const url = `${networkConfig.baseUrl}/${endpoint}`;
    const blockfrostResponse = await fetch(url, {
      method: request.method,
      headers,
      body: request.method !== "GET" ? request.body : undefined,
    });
 
    // Handle 404 for UTXOs as empty wallet
    if (blockfrostResponse.status === 404 && endpointPath.includes("/utxos")) {
      return {
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: [],
      };
    }
 
    // Handle errors
    if (!blockfrostResponse.ok) {
      const errorBody = await blockfrostResponse.text();
      return {
        status: blockfrostResponse.status,
        headers: { "Content-Type": "application/json" },
        body: {
          error: `Blockfrost API error: ${blockfrostResponse.status} ${blockfrostResponse.statusText}`,
          details: errorBody,
        },
      };
    }
 
    // Handle CBOR endpoints
    if (endpointPath === "utils/txs/evaluate" || endpointPath === "tx/submit") {
      const responseData = await blockfrostResponse.text();
      return {
        status: blockfrostResponse.status,
        headers: { "Content-Type": "application/json" },
        body: responseData,
      };
    }
 
    // Handle JSON responses
    const responseData = await blockfrostResponse.json();
    return {
      status: 200,
      headers: { "Content-Type": "application/json" },
      body: responseData,
    };
  } catch (error: unknown) {
    console.error("Blockfrost API route error:", error);
    const errorMessage = error instanceof Error ? error.message : String(error);
 
    return {
      status: 500,
      headers: { "Content-Type": "application/json" },
      body: { error: errorMessage },
    };
  }
}
 
// Helper functions
function getQueryString(url) {
  const qIndex = url.indexOf("?");
  return qIndex !== -1 ? url.substring(qIndex) : "";
}
 
function getNetworkConfig(network) {
  switch (network) {
    case "mainnet":
      return {
        key: process.env.BLOCKFROST_API_KEY_MAINNET,
        baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
      };
    // add different networks
    default: // preprod
      return {
        key: process.env.BLOCKFROST_API_KEY_PREPROD,
        baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
      };
  }
}
 
// Next.js App router specific exports
export async function GET(
  request,
  { params },
) {
  return createAppRouterHandler(request, params);
}
 
export async function POST(
  request,
  { params },
) {
  return createAppRouterHandler(request, params);
}

Now we can initialize BlockfrostProvider with our hosted api route and keep our credentials secure!