Volta Wallet's Recovery Phrase Is Made on Its Server
Volta Wallet, by XRPayNet, is listed on Google Play and the App Store as a fully non-custodial wallet: “your private keys stay with you”. The Android build in the store does something else. It asks XRPayNet's server for a recovery phrase, shows that phrase to the user, posts it back, and then sends it again with every transaction the user confirms. The phone holds no code that could derive a key or sign anything; even its own addresses come from the server.
In one screen
- The phrase is made on the server. The app sends a word count (12, 15 or 24) to
/wallets/mnemonic/getand shows whatever comes back. It contains no code that could make a phrase itself. - It goes back in plain JSON (readable text; nothing is encrypted before sending) when a wallet is created or restored, then again inside every transaction confirmation, every added chain and every bridge transfer.
- The phone cannot sign. No key-derivation or signing library exists in the app; the only hashing left is SHA-256 and the BIP-39 checksum. Addresses are fetched from the server by the phrase's hash.
- Whoever controls that server, an employee, an intruder or a court order, controls every user's funds on every chain. The store listing says the opposite.
What the stores and the website promise
“As a fully non-custodial wallet, your private keys stay with you – no third-party custody means true ownership and enhanced privacy.”Updated 16 December 2025 · 10K+ downloads · screenshots: "The Non Custodial Wallet That Does It All", "You own your secret keys"
“the ultimate non-custodial crypto app that puts you in full control of your digital assets”Version 5.0.5, released 17 December 2025 · seller XRPAYNET GLOBAL LIMITED
“Non-Custodial Wallets. Some available chains include: Bitcoin, XRP, Solana, Binance Smart Chain, Dogecoin, Litecoin & many more.”Same page: "safeguarded through advanced security measures, including a 24-word mnemonic"
The app, on the screen that shows a new phrase“We will never ask you for your recovery phrase.”create_nc_account_screen.dart:886. True in the narrowest sense: it does not need to ask.
“Non-custodial” describes where the keys live. In a wallet that is non-custodial the phone makes the phrase, turns it into keys and signs every transaction itself; the operator never sees any of it. Volta's build moves each of those steps to the server.
How a wallet is born
Creating a wallet takes four requests. The phone sends a word count and receives the phrase. The user writes the words down and taps them back in order. The phone then posts the phrase to the server, receives a hash, and asks for its wallet by that hash: the chains and addresses come back in the reply. At no point does the phone turn the words into a key.
// WalletsCubit.getMnemonic (wallets_cubit.dart 27697-27808) // 0x8dacec: r16 = "wordCount" // the only thing the phone sends // 0x8dad3c: r0 = Map._fromLiteral() // 0x8dad44: r2 = "/wallets/mnemonic/get" // 0x8dad5c: r0 = post() // FirebaseHelper::post // 0x8dad68: r0 = Await() // 0x8dae04: r0 = copyWith() // copyWith(creationMnemonic: <the reply>) // create_nc_account_screen.dart 382, 886 // 0x8d9288: r0 = "Write down your recovery phrase" // 0x8d979c: r0 = "We will never ask you for your recovery phrase." // WalletsCubit.createWallet (wallets_cubit.dart 28478-28552) // 0x8dc410: r16 = "name" // 0x8dc454: r16 = "image" // 0x8dc470: r16 = "mnemonic" // the phrase, verbatim // 0x8dc488: r0 = Map._fromLiteral() // 0x8dc490: r2 = "/wallets/mnemonic/create" // 0x8dc4a8: r0 = post() // 0x8dc4c4: r16 = "hash" // the server's reply // FirebaseHelper.post (firebase_helper.dart 234-380) // 0x7bfd04: r16 = "Content-Type" // 0x7bfd10: r16 = "application/json" // 0x7bfd1c: r16 = "X-Firebase-AppCheck" // 0x7bfdd0: r16 = "Authorization" // 0x7bfda8: r16 = "Bearer " // 0x7bfe8c: r0 = jsonEncode() // plain JSON, nothing encrypted // 0x7bfea0: r0 = post()
// create a wallet
const phrase = await post('/wallets/mnemonic/get', { wordCount }); // 12, 15 or 24
showAndVerify(phrase); // "Tap the words in the correct order"
const { hash } = await post('/wallets/mnemonic/create', { name, image, mnemonic: phrase });
const wallets = await post('/wallets/get', { hashes: [hash] }); // the addresses come back
// send anything
const tx = await post('/wallets/wallet/chain/transaction/create', { chainId, amount, to, tokenId, tag, partial });
await post('/wallets/wallet/chain/transaction/confirm', { chainId, mnemonic: phrase, transaction: tx, transactionType, tokenId });/wallets/mnemonic/get becomes creationMnemonic, the phrase the next screen tells the user to write down. Line numbers refer to the decompiled output of the build whose hashes are in the sources.How a payment is sent
Sending is two requests. The first carries the intent: chain, amount, recipient, token. The server builds the transaction and returns it for the user to confirm. The second carries the confirmation, and the phrase goes with it. The same is true of a trust line, an added chain and a bridge transfer.
What the phone cannot do
A wallet that signs on the phone has to carry the code for it: turning the phrase into keys, the curves the chains use for signatures, and the address formats. In this build every one of those pieces is missing.
| What signing on the phone needs | Its job | In Volta 5.0.5 |
|---|---|---|
| BIP-39 word list and checksum | check that a phrase is well-formed | present, checking only |
| BIP-39 seed (PBKDF2, HMAC-SHA512) | turn the words into the root secret | absent |
| BIP-32 / BIP-44 derivation | one root secret into one key per chain | absent |
| secp256k1 | keys and signatures for Bitcoin, Ethereum, Binance Smart Chain, Dogecoin, Litecoin, XRP Ledger | absent |
| ed25519 | keys and signatures for Solana | absent |
| base58, bech32, keccak, ripemd | turn a key into an address | absent |
| SHA-256 | the hash by which the app names its wallet to the server | present |
So the phone cannot make a key, cannot make an address and cannot sign. It does not know its own addresses until /wallets/get sends them. A PIN, a fingerprint and the encrypted local copy guard a copy of the phrase that the server does not need.
Why it matters
Non-custodial has one meaning: the keys exist only where the user is, so nobody else can move the funds. Volta inverts it. The server sees the phrase before the user does, files the wallet under the phrase's hash, and receives the phrase again with every transaction. Anyone with access to that server, or to any log, proxy or crash report on the way to it, has every phrase; and one phrase opens Bitcoin, XRP, Solana, Binance Smart Chain, Dogecoin, Litecoin and the rest at once.
The XRPH Wallet hack is what a server-side seed store looks like when it fails: 4,011 wallets emptied in three hours. There the phone did sign its own payments, and the seed left the device through one feature, staking. Volta's phrase never has a moment on the phone alone.
The two companies are not strangers. On 21 December 2022 XRPayNet announced“our official new partnership with @XRPHealthcare that will deliver staking rewards as soon as their Private Offering has completed on January 15, 2023. Stake XRPH & Earn XRPH!” Staking is the feature through which the XRPH app later sent seeds to its server. Whether XRPayNet had any hand in that feature is not public, and nothing in either build says so.
Nor is this a new design. The build still carries what its own code calls the old custodial wallet, run through a third-party wallet service (Tatum), and a migration path out of it (/wallets/migrate, sending oldWallets). The version that replaced it kept the keys on a server, now XRPayNet's own.
On the ledger
XRPayNet publishes a domain file for the XRP Ledger that lists r3P1bYdym4buYg1BugJBw7n4aCrXiZLQaw with the description “Wallet used by the Volta Wallet app”. An XRP Ledger account only comes into existence when someone sends it its first XRP, and that first payment is written on the ledger for good. 226 accounts got theirs from this wallet.
Two signatures on every payment, the same destination tag on 219 of them (a number a sender attaches to a payment, normally so an exchange can tell its customers apart) and the same fee on 220: the fingerprint of one automated system, not a person pressing send.
First: 627509A939… (10 XRP, 12 July 2022) · largest: 72EE0122E7… (5,000 XRP, 16 November 2023) · last: 9F83C2DC41… (15 XRP, 11 August 2025). It is a floor for the app's XRP Ledger users: whoever funded their address from an exchange never touched this wallet, and the other fifteen chains do not show here at all.
Everything else in the build
What could not be checked
No request body in this article was watched on the wire. Before its first screen the build asks Google whether it was installed from the Play Store onto an unmodified phone (Google's licence check and Firebase App Check), and quits with “Something went wrong” if not; a rooted phone or an emulator, the tools that decrypt an app's traffic, never gets past that dialog (497 connections logged in a two-minute run, none to XRPayNet). The request bodies above are read from the code, not captured. What the server does with the phrase after it arrives, how long it keeps it, who can read it, whether it is encrypted where it rests, is not visible from the app. Only the Android build was examined; the iOS app was not.
One name misleads: /wallets/mnemonic/get sounds like a lookup, but in this build its only input is the word count and its reply becomes the new wallet's phrase. Whether the same route can return an existing phrase is a server-side question the app cannot answer.
Do we recommend it?
- The phrase is generated by XRPayNet, which files the wallet under its hash
- It is sent again with every transaction
- The phone cannot sign, so nothing can be fixed from the user's side
- The store listing says the opposite
- Make the phrase on the phone from the operating system's random source
- Derive keys and sign on the phone; send only signed transactions
- Never transmit the phrase. A backup, if offered, encrypted on the phone with a secret the server never sees, and opt-in
- Call the product custodial until then
- Treat the balance as held by XRPayNet, like an exchange account
- Move what you cannot afford to lose to a wallet that signs on the device
- Do not reuse a Volta phrase anywhere else: it passed through a server the day it was made and on every transaction since
- Do not restore a phrase from another wallet into Volta: the restore screen sends it to the server the same way
Sources
- Volta Wallet 5.0.5.prod (build 377),
com.xrpaynet.app, arm64, the Google Play build: source-stamp certificate SHA-2563257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d, signing certificate47fa031f292a7de6d752e1c8ec962d030b4cec4a4072e3420ab5084ffa87c315, base.apke61f18d14cc9f3951b6d35a23c2b6fc48678d9fb07e9a371271b795a25da8f35, split bundle04c7fc21f2b7a6df55c6b51c76d51cb7fd098b7c5746e650460e0990f1e38d38. Decompiled with Blutter (Dart 3.10.3) into 1,779 files; line numbers above refer to that output. - Store and website texts: Google Play, App Store, xrpaynet.com/volta-app, read 2026-09-06.
- Network run: the same build sideloaded on an emulator behind a logging proxy, 2026-09-06, two minutes from launch to the quit dialog; every connection counted by host.
- Ledger: XRPayNet's xrp-ledger.toml; the 226 activations and all 226 funding payments from a full-history XRP Ledger node and our index, 2026-09-06; every hash links to the explorer.
- The XRPH Wallet hack: 4,011 wallets emptied in three hours, for the comparison; XRPayNet's partnership post of 21 December 2022, quoted above.