Inside a super-app: MoMo's architecture, read from the binary

Tags
Computer Science
Software Development
App Dev
AI summary
Static analysis teardown of MoMo 5.16.0: a 14-module federated mini-app platform, a four-runtime request path, native whitebox signing, and a 234-route product map recovered from the shipped binary.
Published
October 12, 2026
Author
Peter of H
I spent some time reverse engineering MoMo 5.16.0, Vietnam's dominant e-wallet app, using only static analysis. No running the app, no rooted device, just the shipped artifact. This post is what the architecture looks like once you can actually see it.
Everything below is verified from decompiled code and bundled assets, not guessed from screenshots.
notion image

The app is not one app

MoMo ships a thin native host plus 14 independently deployable React Native mini-apps. The host is Kotlin Multiplatform with Compose. The mini-apps are Hermes bytecode bundles (version 96) inside ZIP files in the APK assets, wired together with Webpack Module Federation through Callstack Repack 5.1.2 on React Native 0.80.1.
The module list reads like a product org chart: bank, transfer, billpay, investment, paylaterverse, ekyc, chat, transactionhistory, mobilecenter, promotionhub, heodat, finance, cornerstone (the shell host), and appx. Each feature module carries per-screen chunks with a manifest of checksums, so the host can verify and hot-swap individual screens.
The consequence that matters: features ship on a 12-hour over-the-air cycle, not an app-store cycle. The app registers a periodic WorkManager job (miniapp_background_fetch, unmetered network only), plus FCM push types like miniapp_version_check that trigger silent update workers. Bundle delivery comes from a dedicated CDN pipeline: rigver-appversion for manifests, rigver-rollout for staged percentage rollouts, rigver-dynacfg-app for dynamic config, rigver-miniapp-load for the zips themselves. Every endpoint has a failover host, and the zips mirror to a second CDN (VCDN) with a ?backup=true flag on the primary.
notion image

The request path is deliberately deep

A mini-app API call crosses four runtimes before it hits the wire:
  1. JS: callPlatformApiSync/Async in shared library chunks serializes the request object and calls nitroApiShared().
  1. Nitro/JSI: libmomoapiproxy.so, a Margelo Nitro hybrid object, bridges JS to Java without the RN bridge queue.
  1. Java/KMP: IOCPlatformApi resolves to PlatformApiManager, which dispatches to feature clients over a Ktor client.
  1. Security SDK: before leaving, MMSecuritySDKMain.signRequestData injects whitebox headers (WBSIGN, WBSIGN-CODE, WBCODE, VSIGN, VTS, MMK, META, wbmky, wbmtd, wbti) computed in native code, gated by an endpoint whitelist trie and remote config flags.
TLS pinning lives in libmmguard.so (15 MB, embeds OpenSSL 3.6.0), separate from the signing library. The signing path fails open: an exception in the whitebox bridge returns an empty map, meaning the request goes out unsigned rather than not at all. That is a product decision, availability over strictness, and worth noting as a pattern.
notion image

The security layer is a product in itself

com.momosecurity initializes via androidx.startup before Application.onCreate finishes: MMSInitializer to initWbSign to MomoPro.initInstance(context, imei) and into libmmwbapi.so. The SDK defines 14 threat categories: root, emulator, app-lib tamper, runtime tamper, malware, RAT, overlay, screenshare, virtual tap, sideloaded accessibility, virtual space, bootloader, plus sensor-based emulator detection through an ISensorDetector interface. Root checking is native (NativeHelper.checkRootNative), and the app self-attests by hashing its own signing certificates (MomoPro.getAppSignerHash).
initJni in the signing library uses blinded function pointers with large constant masks, so RegisterNatives resolves real JNI targets only at runtime. Static analysis hits a designed wall there.
notion image

The API surface is a microservice map

Mining the decompiled JS bundles produced 234 internal routes across roughly 50 named services. Reading the service names is the fastest way to understand the product portfolio:
  • Money movement: P2P_FUND_TRANSFER_INIT/CONFIRM, P2P_M2MU_*, transhis for history, AIOQR for unified QR
  • Credit: credit_paylater_platform alone has 20 routes covering cash-out, installment conversion, activation, and lock/unlock
  • Savings gamification: pigwalk-* is eight separate services (pigfeed, gear, leaderboard, milestone, diary, inventory, achievement, missions) for a Tamagotchi-style savings game
  • Engagement engine: vouchergate-* (6 services), kindle-*, gnosis missions, expowa coin economy, athena coupons, aidhub campaigns, cosmos membership
  • Social: helios/chat-api for P2P chat, coreroom for rooms, helios-oa-manager for Official Accounts (business channels)
  • Identity: ekyc/v3 with tiered flows, sub_wallet_ekyc for light KYC via face or NFC chip-reading the national CCCD card, agent_relatives for agent-assisted KYC
  • Platform: gateway/mini/v1/iam and oauth/login make MoMo an identity provider for its mini-app ecosystem
  • Unexpected: telco/v2/products/iot sells IoT SIMs (paired with a bundled ESP32 provisioning library), plus travel SIM SKUs
notion image

Environment topology

The decompiled EnvironmentEndPoints enum defines DEV, UAT, STAGING, PROD with full host lists. Interesting choices: STAGING runs on production hosts with staging- path infixes and shares gRPC and Firebase infrastructure with PROD. DEV and UAT have identical mini-app endpoint lists. The binary selects at runtime through a singleton, and the shipped build answers on production hosts. Every critical endpoint is a two-element failover list, primary momocdn.net, failover momoapp.vn.
notion image

Architectural takeaways

  1. Federation beats monolith at this scale. Per-screen OTA chunks with checksum manifests let a 14-team org ship independently. The cost is real: 312 Hermes bundles, duplicated shared libraries, and a build system that embeds a whole registry.
  1. Move trust decisions to native code. Signing, pinning, root detection, and device attestation all happen below the language layer an attacker can most easily patch. The whitebox headers are opaque to anyone reading JavaScript.
  1. Fail open where money flow matters. Unsigned requests still attempt. Availability is the product.
  1. The engagement engine is larger than the payment engine. Counting routes, the gamification stack outweighs the transfer stack roughly 2:1. In a super-app, retention machinery is the real core.
  1. Leaked build metadata is an architectural smell. The cornerstone bundle embeds its package.json, including internal registry URLs and dependency paths. Anything your build system knows, your shipped artifact can tell.
The full evidence trail, decompiled flows, endpoint matrices, and 234-route inventory live in my audit workspace. If there is interest, a follow-up post can cover the security findings specifically: the zero-IV AES-CBC PIN signature scheme, and a production telemetry webhook credential shipped inside a bundled library.