Skip to content
guideOctober 21, 2026 · 9 min read

How to build a crypto wallet app in 2026: custody model, recovery, and getting through the stores

What we learned shipping a custodial wallet on Android, iOS, macOS and Windows: the custody decision, recovery without a seed phrase, send safety, and store review.

Kostiantyn Boiko, Blockchain Engineer

We build the wallet that runs as EX5 Wallet on Android, iOS, macOS and Windows. In October 2026 we took it through Google Play, submitted it to Apple, signed and notarized the Mac app and ran a full audit of the mobile code. This is what we would tell a team starting the same project, in the order the decisions have to be made.

Decide who holds the keys first

Every later decision depends on this one: your recovery flow, your licence questions, your store declarations, even which bugs can lose money.

  • Non-custodial: the user holds the keys, usually as a seed phrase. You never touch funds, and if the user loses the phrase, nobody can help them.
  • Custodial: the operator holds the keys, and the app is a client of a backend that signs. Users sign up with an email and a password and recover their account like any other app.

We build custodial, because our customers are exchanges, brokers and payment companies whose users expect a “forgot password” link and a support desk. That has consequences. Google Play’s crypto policy applies to custodial wallets and explicitly leaves non-custodial ones out of scope. Apple expects an organization account. In most regulated markets a custodial wallet is a service that needs a licence, which is why the country list below matters.

The upside is architectural. The app contains no keys and no chain code. Everything that touches funds (address generation, signing, limits, the ledger) lives on the server, so a compromised phone cannot sign a withdrawal on its own and a client bug cannot create money.

One codebase per platform family, one engine underneath

What we ship:

  • iOS and Android from one React Native codebase on Expo SDK 54 (React Native 0.81, New Architecture on), targeting Android API 36.
  • macOS and Windows as a desktop app in a Rust shell (Tauri 2) with a React UI. It is not Electron: it uses the system WebView, and the session cookie, CSRF token and device token never reach the WebView: device secrets live in the macOS Keychain or the Windows Credential Manager.
  • A shared TypeScript wallet engine for API calls, formatting and validation. In our case the desktop copies it from the mobile repo through a sync manifest. That works, but the copy drifts: on one day in October the mobile repo was 19 commits ahead. If you start from scratch, make it a real shared package.

Two rules that have saved us since:

  • Money is never a float. Our audit found a balance view that added 0.70 and 0.10 and printed 0.79. Every amount is now a decimal string handled with integer arithmetic.
  • Release numbers live in one place. We had to bump the version in six places, including the runtime version that over-the-air updates use to decide which build an update fits. Missing one ships an update to the wrong binary.

Recovery without a seed phrase

A custodial wallet’s recovery is a password reset, and it is the most attacked flow you will build. Ours:

  1. The request endpoint always answers the same way, whether or not the account exists, so it cannot be used to find users.
  2. The edge rate-limits requests per network (per /64 on IPv6), three a minute.
  3. The email link is valid for an hour, works once, and only the newest link works.
  4. Confirming a new password signs out every session and every PIN device, sends a “password changed” email, and holds withdrawals for 24 hours.

The 24-hour hold is the part people push back on, and it is the part that matters. If an attacker gets into someone’s email, the reset gives them an account they cannot empty before the owner notices the email. We apply the same hold when two-factor authentication is switched on while the account holds funds. Receiving is never blocked.

Make every send hard to get wrong

Most money lost in wallets is lost by the user, to a wrong address or a wrong network. What we check before a send can be confirmed:

  • Format per network. A TRON address in an Ethereum send gets “Change network”, not an error after the fact.
  • Lookalike addresses. Address poisoning works by sending you dust from an address that shares its first and last characters with one you use. We compare the destination against saved, recent and own addresses: same length, same first four and last four characters, different middle. The user then chooses between the known address and continuing anyway.
  • Payment links. This one we learned the hard way. An EIP-681 token payment link names both the token contract and the recipient, and an early version of our parser resolved such links to the token contract, so the transfer would have gone to the contract instead of the person. We fixed it in October: the parser never returns a contract address, and it refuses unknown tokens, other chain IDs, ENS names and request types it does not understand.
  • Second factor on every send. We started with Face ID or PIN on the send screen and moved to a time-based one-time code on every send. If 2FA is off, the first send asks the user to turn it on.

Sends within a daily limit go out automatically; larger ones wait for a person to review them.

Google Play: what it actually asked for

The Android app went live on 20 October 2026, under a day after we submitted the production release and about two days after the first production build.

  • Use an organization developer account. Play requires one for crypto wallets, and the 12-testers-for-14-days closed-test rule for new personal accounts does not apply to it.
  • Declare only what the binary does. Our financial features declaration is “Cryptocurrency wallet”. Convert and Earn exist in the code but are compiled out of store builds, and the build fails closed if the switch is missing, so the binary always matches the declaration.
  • Pick countries by licence, not by market size. We published in 135 countries and left out the markets where a custodial wallet needs a licence: the EU and EEA, the UK, the US, Canada, Japan, South Korea and others, plus sanctioned countries.
  • Give the reviewer an account they can use. Our first plan was a demo account with 2FA on and the code key in the review notes. Google rejected it as a “multi-factor authentication wall”. The fix is a separate review account with 2FA off, kept that way.
  • Check the permissions you did not ask for. Expo’s prebuild added media read permissions because a media library package was installed, and audio recording came in from the camera package’s manifest. We removed the packages and blocked the permissions explicitly.
  • Add the Play signing key to assetlinks.json. Play re-signs your app, so the fingerprint of your upload key is not the one users’ phones see. Without Play’s fingerprint in the file, app links and password sharing break for everyone who installed from the store.
  • The first upload is manual. Automated submission was rejected until a build had been uploaded by hand once. After that, releases go through CI.

One wording choice that matters for review and for honesty: the app says “sending fee”, not “network fee”, because a flat fee per coin is not the network cost. On most networks it is well above it, and on TRON it can be below it (we wrote up the TRON numbers separately).

Apple: what we prepared, and where it stands

At the time of writing Apple has not ruled on the wallet. The first build waited about four days without a decision; we replaced it with an updated build on 20 October, and it is in review. We will update this section with the outcome.

What we prepared, because Guideline 3.1.5 is where crypto apps fail:

  • An organization account, which 3.1.5(i) requires for wallets.
  • Review notes that say plainly what the app is: custodial, the countries it is closed in and why, and that Convert and Earn are compiled out.
  • Two review accounts. One has 2FA on and a small balance, so the reviewer can try a send. 2FA has to be switched on at least 24 hours before submission because of the withdrawal hold, and we refused to switch the hold off on the server for everyone just to suit review. The other account has no balance, so the reviewer can delete it end to end, which Apple requires.
  • A narrower country list than on Android: 46 exclusions, about 130 countries open.

An exchange app we also ship got a harder set of questions under 3.1.5(iii) that same week: licence or registration evidence for every country the app was available in, who executes the trades, and the removal of EEA storefronts. If your app trades, have those answers ready before you submit.

Small things that cost us hours in App Store Connect: the app name was already taken in two languages, so those listings use a suffix; the App Review contact had to be created through the API because the web form silently dropped it; and uploads stopped with a 403 until an updated Program License Agreement was accepted in the account.

Desktop: signing is a distribution problem

  • macOS is the easy one once it works. The DMG is a universal binary, signed with a Developer ID, notarized and stapled. The first notarization took about 48 minutes. Our CI runs on a headless Mac where Tauri’s own keychain signing failed, so we sign with rcodesign and notarize with notarytool.
  • Chrome’s “download failed” on Mac was not a signing problem. It was Safe Browsing’s warning for uncommon downloads, which fades as a file builds reputation.
  • Windows is the hard one. An unsigned installer shows “unknown publisher” in SmartScreen. Since 2024 an EV certificate no longer buys instant reputation either; every certificate type builds it over successive signed releases. We chose Azure Artifact Signing (keys in Microsoft’s HSM, signing from CI with no stored secret) and are waiting for Azure’s identity validation. Until it completes, the Windows build is unsigned, and we say so.

A CI lesson worth stating plainly: never run a signing build in verbose mode. One verbose build printed a temporary certificate password into the log. We deleted the log, rotated the password, and banned the flag.

Test the state, not just the screens

Our audit before the second store release went through the mobile app in four parallel review passes and checked each finding against the source. The bugs it found were not the kind unit tests catch; they were about state and navigation:

  • Sign-out has to clear everything. Data from the previous user can survive in memory, and a PIN device registered to one account can stay attached after another account signs in.
  • Losing the session has to reset the whole navigation stack. Replacing the top screen is not enough: a swipe back shows the signed-out wallet. A deep link opened from cold can also land a signed-out user on a money screen if nothing guards it.
  • Route guards must wait for persisted state. Ours ran before the stored session was restored, sent signed-in users to the login screen, and left a half-finished verification step as a dead end.
  • A late 401 must not kill a newer session. We tag each session with an epoch and ignore failures from older ones.

The navigation bugs were reproduced with the real router before they were fixed. Real-device testing is still a separate step; a passing test suite did not replace it for us and will not for you.

What to build first

  1. The backend: accounts, 2FA, devices, limits, custody and the ledger. The apps are clients of it.
  2. Recovery and send safety, before any screen that looks good in a demo.
  3. Android, because Play’s review is faster and its rules are explicit.
  4. iOS, with review notes and accounts prepared as above.
  5. Desktop, once you have a signing plan for Windows.

The wallet described here is our Wallet product, with the server side in Custody. It ships under your brand and your developer accounts, with the store package (review notes, country lists, test-account setup) included. Contact sales if you want to see it running.

Book a 30-minute scoping call

Crypto exchange, payments, wallet, custody and token launch software, already running in production. Run it on your own servers, license the source code, or let us host it under your brand.