Server Wallets
Create and manage MPC wallets from your backend using
ServerWalletManager.
Server wallets let you create, sign, and transact with smart accounts entirely from your backend — no browser, no user interaction required. They use the same DKLS23 Threshold Signature Scheme as client-side wallets, but authentication is via API keys instead of user sessions.
Use Cases
- Custodial wallets — Manage wallets on behalf of users
- Treasury management — Automated fund transfers and multi-sig operations
- Game economies — In-game currency and NFT management
- Service accounts — Bots, automated trading, payment processing
Installation
npm install @embarkai/coreThe dkls23-wasm module is loaded automatically when needed for DKG and signing operations.
Quick Start
import {
createServerWalletManager,
MemoryKeyshareStorage,
} from '@embarkai/core'
const manager = createServerWalletManager({
apiKey: process.env.EMBARK_API_KEY!,
chainId: 994873017, // Lumia Mainnet
storage: new MemoryKeyshareStorage(), // Dev only! Use secure storage in production
})
// Create a wallet (runs DKG protocol internally)
const wallet = await manager.createWallet('treasury_main')
console.log('Owner (EOA):', wallet.ownerAddress)
console.log('Smart Account:', wallet.smartAccountAddress)
// Send a transaction
const userOpHash = await wallet.sendUserOperation(
'0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
'1000000000000000000', // 1 LUMIA in wei
)
// Wait for confirmation
const receipt = await wallet.waitForUserOperationReceipt(userOpHash)
console.log('TX Hash:', receipt.transactionHash)
console.log('Success:', receipt.success)Configuration
interface ServerWalletManagerConfig {
/** Project API key from EmbarkAI Dashboard (required) */
apiKey: string
/** Keyshare storage implementation (required) */
storage: KeyshareStorage
/** Target chain ID (required) */
chainId: number
/** Override TSS API URL */
tssUrl?: string
/** Override bundler RPC URL (resolved from chainId if omitted) */
bundlerUrl?: string
/** Override blockchain RPC URL (resolved from chainId if omitted) */
rpcUrl?: string
/** Override AA factory address (resolved from chainId if omitted) */
factoryAddress?: `0x${string}`
/** Override EntryPoint v0.7 address (resolved from chainId if omitted) */
entryPointAddress?: `0x${string}`
/** Override paymaster address for sponsored transactions */
paymasterAddress?: `0x${string}`
/** Max call gas limit for UserOperations (default: 5,000,000) */
maxCallGasLimit?: bigint
/** Max verification gas limit (default: 5,000,000) */
maxVerificationGasLimit?: bigint
/** Enable debug logging (default: false) */
debug?: boolean
}Most fields are resolved automatically from built-in chain configurations when you provide a supported chainId. You only need apiKey, storage, and chainId to get started.
API Key Setup
- Go to the EmbarkAI Dashboard .
- Select your project.
- Navigate to API Keys.
- Click Generate Server API Key.
- Copy the key (shown only once) and store it in your environment.
# .env
EMBARK_API_KEY=lp_00ab351d95c0eaf3ee499006a6a32402c31177bef4c7c1f26fed8cb947e15966Keyshare Storage
When a wallet is created via DKG, the manager stores the client-side keyshare using your provided KeyshareStorage implementation. This keyshare is required for all future signing operations.
Storage Interface
interface KeyshareStorage {
set(userId: string, keyshare: string): Promise<void> | void
get(userId: string): Promise<string | null> | string | null
has(userId: string): Promise<boolean> | boolean
list(): Promise<string[]> | string[]
}Built-in: MemoryKeyshareStorage
For development and testing only. Keyshares are lost when the process exits.
import { MemoryKeyshareStorage } from '@embarkai/core'
const storage = new MemoryKeyshareStorage()Custom: Secure Storage (Production)
In production, implement KeyshareStorage backed by a secure system such as AWS KMS, HashiCorp Vault, Azure Key Vault, or an encrypted database.
import type { KeyshareStorage } from '@embarkai/core'
class VaultKeyshareStorage implements KeyshareStorage {
async set(userId: string, keyshare: string): Promise<void> {
// Encrypt and store in your vault/KMS
await vault.write(`secret/keyshares/${userId}`, { data: keyshare })
}
async get(userId: string): Promise<string | null> {
try {
const result = await vault.read(`secret/keyshares/${userId}`)
return result.data.data
} catch {
return null
}
}
async has(userId: string): Promise<boolean> {
return (await this.get(userId)) !== null
}
async list(): Promise<string[]> {
const keys = await vault.list('secret/keyshares/')
return keys.data.keys
}
}Warning: Never delete keyshares. If a keyshare is lost, the wallet becomes permanently inaccessible and all funds are unrecoverable. Always use redundant, encrypted storage with backups.
Wallet Operations
Create a Wallet
const wallet = await manager.createWallet('unique-wallet-id')Internally this runs the full DKLS23 DKG protocol (3 rounds) with the TSS service, stores the resulting keyshare, and returns a ServerWallet instance.
Get an Existing Wallet
const wallet = await manager.getWallet('treasury_main')
console.log(wallet.ownerAddress) // EOA derived from TSS public key
console.log(wallet.smartAccountAddress) // Predicted AA addressList All Wallets
const wallets = await manager.listWallets()
for (const w of wallets) {
console.log(w.userId, w.smartAccountAddress)
}Check Wallet Existence
const exists = await manager.walletExists('treasury_main')
const hasLocal = await manager.hasKeyshare('treasury_main')Sending Transactions
Single Transaction
const userOpHash = await wallet.sendUserOperation(
'0xRecipientAddress', // to
'1000000000000000000', // value in wei
'0x', // calldata (optional)
'standard', // fee type: 'economy' | 'standard' | 'fast'
)Batch Transaction
Execute multiple operations atomically in a single UserOperation:
const userOpHash = await wallet.sendBatchUserOperation([
{ to: '0xAddr1', value: '1000000000000000000', data: '0x' },
{ to: '0xAddr2', value: '0', data: '0xabcdef...' },
], 'standard')Parallel Operations
Use nonceKey to send multiple UserOperations concurrently without nonce conflicts:
const [hash1, hash2] = await Promise.all([
wallet.sendUserOperation(addr1, value1, '0x', 'standard', 0n),
wallet.sendUserOperation(addr2, value2, '0x', 'standard', 1n),
])Transaction Receipts
// Non-blocking check
const receipt = await wallet.getUserOperationReceipt(userOpHash)
if (receipt?.success) {
console.log('TX:', receipt.transactionHash)
}
// Blocking wait with polling
const receipt = await wallet.waitForUserOperationReceipt(userOpHash, {
pollIntervalMs: 2000, // Check every 2 seconds
maxPollTimeMs: 120000, // Timeout after 2 minutes
})Receipt fields:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the operation succeeded |
transactionHash | string | On-chain transaction hash |
blockNumber | string | Block number |
blockHash | string | Block hash |
actualGasUsed | string | Gas consumed |
actualGasCost | string | Total gas cost in wei |
logs | any[] | Transaction event logs |
Signing
Sign arbitrary 32-byte digests using the TSS protocol:
const signature = await wallet.signDigest(
'0xabcdef1234567890...', // 32-byte hex hash
"m/44'/60'/0'/0/0", // BIP-44 path (optional)
)
console.log(signature.r) // hex string
console.log(signature.s) // hex string
console.log(signature.v) // 27 or 28Vault Backup
Back up keyshares to EmbarkAI ShareVault with password encryption:
// Backup
await manager.backupToVault('treasury_main', 'secure-password')
// Check backup exists
const { hasBackup } = await manager.checkBackupExists('treasury_main')
// Restore to local storage
await manager.restoreFromVault('treasury_main', 'secure-password')Multi-Chain
Create separate manager instances for each chain:
import { createServerWalletManager, MemoryKeyshareStorage } from '@embarkai/core'
const storage = new MemoryKeyshareStorage()
const lumiaManager = createServerWalletManager({
apiKey: process.env.EMBARK_API_KEY!,
chainId: 994873017, // Lumia Mainnet
storage,
})
const sepoliaManager = createServerWalletManager({
apiKey: process.env.EMBARK_API_KEY!,
chainId: 11155111, // Sepolia
storage,
})The same wallet (identified by userId) can be used across chains — the TSS key pair is chain-agnostic. Only the smart account address differs per chain.
Security Best Practices
- Never commit API keys to version control. Use environment variables or a secrets manager.
- Use secure keyshare storage in production (AWS KMS, HashiCorp Vault, etc.).
- Never delete keyshares — loss is permanent and unrecoverable.
- Use random wallet IDs instead of predictable patterns like
user_1. - Implement rate limiting on wallet creation endpoints.
- Enable audit logging for all wallet operations.
- Rotate API keys periodically via the Dashboard.