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
- Navigate to Dashboard > Project Settings > Security
- Click Generate Key Pair
- Public key: Stored by UTXOS to encrypt wallet private keys
- 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:
- UTXOS generates a new wallet private key
- The private key is immediately encrypted with your Entity Secret public key
- Only the encrypted private key is stored
- Wallet metadata (ID, addresses) is returned to you
When you retrieve a wallet:
const { cardanoWallet } = await sdk.wallet.getWallet(walletId, "cardano");The following happens:
- UTXOS returns the encrypted private key
- Your SDK decrypts it using your Entity Secret private key (in your infrastructure)
- 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
| Do | Do Not |
|---|---|
| Store in secure secrets manager (AWS Secrets Manager, HashiCorp Vault) | Commit to version control |
| Use environment variables on servers | Expose in client-side code |
| Create backups in separate secure locations | Share with unauthorized personnel |
| Rotate if you suspect compromise | Store in plaintext files |
API Key Security
| Do | Do Not |
|---|---|
| Use separate keys for development/production | Use production keys in development |
| Restrict API key permissions where possible | Share keys between applications |
| Rotate keys periodically | Log API keys |
Infrastructure
| Do | Do Not |
|---|---|
| Run wallet operations on secure servers | Run on client-side applications |
| Use HTTPS for all API calls | Use HTTP in production |
| Implement rate limiting | Allow unlimited transaction requests |
| Log transactions for auditing | Store 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
- Cardano Integration - Complete setup guide
- Entity Secret - Security configuration
- Transaction Sponsorship - Pay fees for your users