How Developer-Controlled Wallets Work

Developer-controlled wallets give you complete programmatic control while keeping private keys secure through asymmetric encryption. This page explains the architecture, key management, and security model.

How does the architecture work?

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  Your Backend   │     │   UTXOS API     │     │  UTXOS Storage  │
│                 │     │                 │     │                 │
│  Entity Secret  │────▶│  Create Wallet  │────▶│ Encrypted Keys  │
│  (Private Key)  │     │                 │     │                 │
│                 │◀────│  Return Wallet  │◀────│                 │
│  Decrypt & Sign │     │                 │     │                 │
└─────────────────┘     └─────────────────┘     └─────────────────┘

What is the Entity Secret?

The Entity Secret is a public-private key pair that protects all your developer-controlled wallets.

Setup

  1. Navigate to Dashboard > Project Settings > Security
  2. Click Generate Key Pair
  3. Public key: Stored by UTXOS to encrypt wallet private keys
  4. Private key: Download and store securely; required to decrypt and sign
🚫

UTXOS does not store your Entity Secret private key. If you lose it, you permanently lose access to all associated wallets.

How Encryption Works

When you create a wallet:

const walletInfo = await sdk.wallet.createWallet();

The following happens:

  1. UTXOS generates a new wallet private key
  2. The private key is immediately encrypted with your Entity Secret public key
  3. Only the encrypted private key is stored
  4. Wallet metadata (ID, addresses) is returned to you

When you retrieve a wallet:

const { cardanoWallet } = await sdk.wallet.getWallet(walletId, "cardano");

The following happens:

  1. UTXOS returns the encrypted private key
  2. Your SDK decrypts it using your Entity Secret private key (in your infrastructure)
  3. The decrypted wallet is available for signing

How do you create wallets?

Basic Creation

import { Web3Sdk } from "@utxos/sdk";
 
const sdk = new Web3Sdk({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  apiKey: process.env.UTXOS_API_KEY,
  privateKey: process.env.UTXOS_PRIVATE_KEY,
  network: "testnet",
});
 
const walletInfo = await sdk.wallet.createWallet();
console.log(`Wallet ID: ${walletInfo.id}`);
console.log(`Address: ${walletInfo.address}`);

Creation with Tags

Use tags to organize wallets by purpose:

const paymentWallet = await sdk.wallet.createWallet({
  tags: ["payments", "production"]
});
 
const airdropWallet = await sdk.wallet.createWallet({
  tags: ["airdrops", "campaign-2024"]
});

Wallet Info Response

{
  id: "abc123...",           // Unique wallet identifier
  address: "addr_test1...",  // Cardano address
  publicKey: "ed25519...",   // Public key
  createdAt: "2024-01-15...",
  tags: ["payments"]
}

Store the wallet ID in your database. You need it to retrieve the wallet later.

How do you retrieve wallets?

Single Wallet

Retrieve a wallet for a specific chain:

// Cardano wallet
const { info, cardanoWallet } = await sdk.wallet.getWallet(walletId, "cardano");
 
// Spark wallet
const { info, sparkIssuerWallet } = await sdk.wallet.getWallet(walletId, "spark");
 
// Initialize both chains at once
const { info, cardanoWallet, sparkWallet } = await sdk.wallet.initWallet(walletId);

All Project Wallets

List all wallets in your project:

// Paginated
const { data: wallets, pagination } = await sdk.wallet.getProjectWallets();
 
// All wallets (fetches all pages)
const allWallets = await sdk.wallet.getAllProjectWallets();

Filter by Tags

const paymentWallets = await sdk.wallet.getProjectWallets({
  tags: ["payments"]
});

How do you use wallets?

Once retrieved, developer-controlled wallets provide a CIP-30 compatible interface:

Get Address

const address = await cardanoWallet.getChangeAddress();

Get UTXOs

const utxos = await cardanoWallet.getUtxos();

Sign Transaction

const signedTx = await cardanoWallet.signTx(unsignedTx);
// Optional: partial signing
const signedTx = await cardanoWallet.signTx(unsignedTx, true);

Sign Data

const signature = await cardanoWallet.signData(payload);

Submit Transaction

const txHash = await cardanoWallet.submitTx(signedTx);

Complete Example

Build, sign, and submit a transaction:

import { Web3Sdk } from "@utxos/sdk";
import { BlockfrostProvider, MeshTxBuilder } from "@meshsdk/core";
 
// Initialize provider
const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY);
 
// Initialize SDK
const sdk = new Web3Sdk({
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
  apiKey: process.env.UTXOS_API_KEY,
  privateKey: process.env.UTXOS_PRIVATE_KEY,
  network: "testnet",
  fetcher: provider,
  submitter: provider,
});
 
async function sendPayment(walletId: string, recipient: string, amount: string) {
  // Retrieve the wallet
  const { cardanoWallet } = await sdk.wallet.getWallet(walletId, "cardano");
 
  // Build the transaction
  const tx = new MeshTxBuilder({ fetcher: provider });
 
  tx.txOut(recipient, [{ unit: "lovelace", quantity: amount }])
    .changeAddress(await cardanoWallet.getChangeAddress())
    .selectUtxosFrom(await cardanoWallet.getUtxos());
 
  const unsignedTx = await tx.complete();
 
  // Sign and submit
  const signedTx = await cardanoWallet.signTx(unsignedTx);
  const txHash = await cardanoWallet.submitTx(signedTx);
 
  return txHash;
}
 
// Usage
const txHash = await sendPayment(
  "your-wallet-id",
  "addr_test1qz...",
  "5000000" // 5 ADA in lovelace
);
console.log(`Transaction submitted: ${txHash}`);

Security Best Practices

⚠️

Follow these practices to protect your wallets and funds.

Entity Secret Management

DoDo Not
Store in secure secrets manager (AWS Secrets Manager, HashiCorp Vault)Commit to version control
Use environment variables on serversExpose in client-side code
Create backups in separate secure locationsShare with unauthorized personnel
Rotate if you suspect compromiseStore in plaintext files

API Key Security

DoDo Not
Use separate keys for development/productionUse production keys in development
Restrict API key permissions where possibleShare keys between applications
Rotate keys periodicallyLog API keys

Infrastructure

DoDo Not
Run wallet operations on secure serversRun on client-side applications
Use HTTPS for all API callsUse HTTP in production
Implement rate limitingAllow unlimited transaction requests
Log transactions for auditingStore private keys in logs

Error Handling

try {
  const { cardanoWallet } = await sdk.wallet.getWallet(walletId, "cardano");
  const signedTx = await cardanoWallet.signTx(unsignedTx);
} catch (error) {
  if (error.code === "WALLET_NOT_FOUND") {
    // Wallet ID does not exist
  } else if (error.code === "DECRYPTION_FAILED") {
    // Entity Secret mismatch
  } else if (error.code === "INSUFFICIENT_FUNDS") {
    // Wallet balance too low
  }
}

Next Steps