Back to blog
Dec 15, 2024
6 min read
Muhammad Waqar Ilyas

Scaling to 500K Users: Lessons from Building Cryptokara Wallet

Real-world insights and architectural decisions from scaling a decentralized cryptocurrency wallet to half a million active users, including performance optimization, security measures, and infrastructure challenges.

When I joined KryptoMind LLC to lead the development of Cryptokara, a decentralized cryptocurrency wallet, I didn’t anticipate the scale we’d eventually reach. Today, with over 500,000 active users managing millions in crypto transactions daily, I want to share the architectural decisions, challenges, and lessons learned that made this scale possible.

The Challenge: Building Trust at Scale

Building a cryptocurrency wallet isn’t just about moving numbers between addresses. You’re asking users to trust you with their life savings. One bug, one security flaw, one moment of downtime could mean financial ruin for thousands. This reality shaped every decision we made.

Architecture: Mobile-First with React Native

We chose React Native for several strategic reasons:

  1. Single Codebase, Dual Platform: With limited resources, maintaining separate iOS and Android apps wasn’t feasible
  2. Performance: For a wallet app, 60 FPS scrolling through transaction history isn’t negotiable
  3. Native Modules: Direct access to secure storage and biometric authentication

Key Technical Decisions

Offline-First Architecture

// We implemented a robust offline queue system
class TransactionQueue {
  private queue: Transaction[] = [];

  async queueTransaction(tx: Transaction) {
    await AsyncStorage.setItem(`pending_tx_${tx.id}`, JSON.stringify(tx));
    this.queue.push(tx);
  }

  async processQueue() {
    // Only process when online and synced
    if (!this.isOnline() || !this.isSynced()) return;

    for (const tx of this.queue) {
      try {
        await this.broadcastTransaction(tx);
        await this.removeFromQueue(tx.id);
      } catch (error) {
        // Retry logic with exponential backoff
        await this.scheduleRetry(tx);
      }
    }
  }
}

This approach meant users could create transactions during network issues, and we’d broadcast them when connectivity returned.

Security: Non-Negotiable from Day One

Private Key Management

We never stored private keys on our servers. Period. Instead:

// Keys encrypted with user's PIN using AES-256
const encryptPrivateKey = async (privateKey: string, pin: string) => {
  const salt = await generateSalt();
  const key = await deriveKey(pin, salt);

  return {
    encrypted: await encrypt(privateKey, key),
    salt: salt.toString("hex"),
    algorithm: "aes-256-gcm",
  };
};

Biometric Authentication

We implemented multiple layers:

  • Face ID / Fingerprint for app access
  • Additional PIN for transactions over $1,000
  • Hardware security module integration for high-value accounts

Scaling Challenges We Faced

1. Real-Time Price Updates

With 500K users checking prices simultaneously, polling APIs wasn’t sustainable.

Solution: WebSocket connections with intelligent batching

class PriceUpdateManager {
  private connections = new Map<string, WebSocket[]>();

  broadcastPriceUpdate(currency: string, price: number) {
    const sockets = this.connections.get(currency) || [];

    // Batch updates every 2 seconds
    const batch = {
      currency,
      price,
      timestamp: Date.now(),
      // Include multiple currencies in one message
      batch: this.getPendingUpdates(),
    };

    sockets.forEach((socket) => {
      if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify(batch));
      }
    });
  }
}

2. Transaction History Pagination

Loading 10,000+ transactions crashed the app. We implemented:

  • Virtual scrolling with react-native-virtualized-list
  • Progressive loading (50 transactions at a time)
  • Indexed database queries in MongoDB
// Optimized MongoDB query with proper indexing
db.transactions.createIndex({
  userId: 1,
  timestamp: -1,
});

// Query with cursor-based pagination
const getTransactions = async (userId, cursor, limit = 50) => {
  const query = { userId };
  if (cursor) {
    query.timestamp = { $lt: cursor };
  }

  return db.transactions
    .find(query)
    .sort({ timestamp: -1 })
    .limit(limit)
    .toArray();
};

3. Multi-Chain Support

Supporting Bitcoin, Ethereum, BSC, and TRON meant different:

  • Transaction formats
  • Gas fee calculations
  • Block confirmation times
  • Wallet derivation paths

We abstracted this with a chain adapter pattern:

interface ChainAdapter {
  createWallet(mnemonic: string): Promise<Wallet>;
  getBalance(address: string): Promise<string>;
  estimateFee(transaction: Transaction): Promise<string>;
  broadcastTransaction(signed: SignedTransaction): Promise<string>;
}

class EthereumAdapter implements ChainAdapter {
  async estimateFee(transaction: Transaction): Promise<string> {
    const gasPrice = await this.provider.getGasPrice();
    const gasLimit = await this.estimateGas(transaction);
    return gasPrice.mul(gasLimit).toString();
  }
}

Performance Optimizations That Mattered

1. Lazy Loading & Code Splitting

// Don't load staking module until user opens staking
const StakingScreen = lazy(() => import("./screens/Staking"));

// Preload high-priority screens during splash
const preloadScreens = async () => {
  await Promise.all([
    import("./screens/Wallet"),
    import("./screens/Send"),
    import("./screens/Receive"),
  ]);
};

2. Image Caching for Token Icons

// Custom cache with expiry
const iconCache = new Map<string, CachedImage>();

const getCachedIcon = async (tokenId: string) => {
  const cached = iconCache.get(tokenId);

  if (cached && Date.now() - cached.timestamp < 86400000) {
    return cached.data;
  }

  const image = await fetchTokenIcon(tokenId);
  iconCache.set(tokenId, {
    data: image,
    timestamp: Date.now(),
  });

  return image;
};

3. Background Sync with Expo Task Manager

TaskManager.defineTask(BACKGROUND_SYNC, async () => {
  try {
    // Sync pending transactions
    await transactionQueue.processQueue();

    // Update balances
    await balanceService.syncBalances();

    // Check for incoming transactions
    await notificationService.checkIncoming();

    return BackgroundFetch.Result.NewData;
  } catch (error) {
    return BackgroundFetch.Result.Failed;
  }
});

Database Design for Scale

MongoDB was perfect for our use case:

// Efficient schema design
{
  _id: ObjectId,
  userId: String, // indexed
  wallets: [{
    address: String,
    chain: String,
    balance: Decimal128, // Precise decimal storage
    lastSynced: Date
  }],
  transactions: { // Separate collection
    // Reference by userId
  },
  preferences: {
    currency: String,
    notifications: Boolean
  }
}

Sharding Strategy

// Shard by userId for even distribution
sh.shardCollection("cryptokara.users", { userId: 1 });
sh.shardCollection("cryptokara.transactions", { userId: 1, timestamp: -1 });

Monitoring & Observability

We used:

  • Sentry for crash reporting (99.9% crash-free rate)
  • Custom metrics for transaction success rates
  • MongoDB Atlas monitoring for database performance
// Track critical metrics
const recordTransaction = async (tx: Transaction) => {
  const startTime = Date.now();

  try {
    await broadcastTransaction(tx);

    metrics.increment("transactions.success", {
      chain: tx.chain,
      amount_range: getAmountRange(tx.amount),
    });
  } catch (error) {
    metrics.increment("transactions.failed", {
      chain: tx.chain,
      error: error.code,
    });
    throw error;
  } finally {
    metrics.timing("transactions.duration", Date.now() - startTime);
  }
};

Key Takeaways

  1. Security First: Never compromise on security for features
  2. Offline-First: Mobile apps must work without internet
  3. Performance: 60 FPS isn’t optional for financial apps
  4. Monitoring: You can’t fix what you can’t measure
  5. Gradual Rollout: Use feature flags for risky changes
  6. User Trust: One bad transaction experience can lose a user forever

The Results

  • 500K+ active users
  • 99.9% uptime
  • 4.5+ app store rating
  • <100ms average transaction creation time
  • Zero security breaches

Building Cryptokara taught me that scaling isn’t just about infrastructure—it’s about building trust, one secure transaction at a time.


Have questions about building scalable mobile applications? Feel free to reach out. I’m always happy to share more detailed insights.