Insights

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.

Measured 2026-09-06Volta Wallet 5.0.5.prod (build 377), Google PlayRead from the code; one live run, in which the app never reached XRPayNet's server

In one screen

16
chains, per the store listing, behind one phrase
10K+
Google Play downloads
1
the phone's only input: the word count
4
requests that carry the phrase
0
key or signing routines on the phone
226
XRPL accounts activated by XRPayNet's app wallet
  • The phrase is made on the server. The app sends a word count (12, 15 or 24) to /wallets/mnemonic/get and 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.
Words used below
Recovery phrase. The 12, 15 or 24 words (the BIP-39 standard) from which every private key of a wallet is derived. Whoever has the words has the funds.
Non-custodial. The keys exist only on the user's own device, so the operator cannot move the funds. Custodial: the operator can.
Signing. Approving a transaction with the private key. Where signing happens is where the key is.
Multi-signature. A transaction approved by several keys at once. On the XRP Ledger the signers are listed inside the transaction.
Hash. A fixed-length fingerprint of some data. The app names its wallet to the server by the hash of the phrase; the hash cannot be turned back into the words.
Checksum. A few bits folded into the last word of a phrase that reveal a mistyped word. Checking it needs no secret.
Trust line. On the XRP Ledger, a holder's opt-in to receive a token. It is a transaction, so the app has the server sign it too.

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.

Where the phrase and the keys live
A non-custodial walletkeys never leave the phonePhone makesthe phrasePhone turns itinto keysPhone signsthe transactionBlockchainreceives itVolta Wallet 5.0.5keys never reach the phoneServer makesthe phrasePhone shows it,posts it backServer holds itand signsBlockchainreceives itFilled: the step that holds the phrase or the keys
The top row is what the store listing describes. The bottom row is what the decompiled build does, step by step, in the two sections that follow.

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.

Where the phrase is made, and where it goes
Your phoneShows the phrase; checks a typed oneKeeps a copy in the phone's encrypted storeNo key derivation, no signingLearns its addresses from the serverBackups off, Google Play requiredXRPayNet serverHosted on Google's cloud, behind a loginand Google's check that the app is genuineMakes the phrase; files the wallet by its hashDerives the addressesSigns every transaction16 blockchainsBitcoin, XRP, Solana, Binance Smart Chain,Dogecoin, Litecoin and ten moreReceive what the server signed1. Word count12, 15 or 24: the phone's only input2. The recovery phrasemade on the server, shown to you3. The phrase, backplain text, nothing encrypted, on create and restore4. Every transactionthe phrase travels with the confirmation5. Signed transactionthe phone never touches a key
Every arrow is a request in the decompiled app. None could be watched on the wire: the build quits on any phone that did not install it from Google Play.
As decompiled from the Dart code of build 5.0.5 (377)
// 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()
What it amounts to
// 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 });
The reply to /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.

1. Prepare
/wallets/wallet/chain/transaction/create
chainIdwhich chain
amounthow much
torecipient
tokenIdwhich token
tagdestination tag
partialpartial-payment flag
signaturenone
Intent only. The server builds the transaction and returns it for the user to confirm.
2. Confirm
/wallets/wallet/chain/transaction/confirm
chainIdwhich chain
mnemonicthe recovery phrase
transactionwhat the server built
transactionTypepayment or trust line
tokenIdwhich token
The phrase goes with the confirmation. Signing happens on the other side.
Also carrying the phrase
/chain/createchainIds, mnemonic
/bridge/sendchainId, mnemonic, approve…, transfer…
/mnemonic/createname, image, mnemonic
/mnemonic/deletehash, hashes
Adding a chain, bridging, and creating or restoring a wallet all send it. Deleting sends only the hash.
Field names are the request bodies as written in the decompiled app, in the order the app writes them. Highlighted: the phrase, or its absence.

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 needsIts jobIn Volta 5.0.5
BIP-39 word list and checksumcheck that a phrase is well-formedpresent, checking only
BIP-39 seed (PBKDF2, HMAC-SHA512)turn the words into the root secretabsent
BIP-32 / BIP-44 derivationone root secret into one key per chainabsent
secp256k1keys and signatures for Bitcoin, Ethereum, Binance Smart Chain, Dogecoin, Litecoin, XRP Ledgerabsent
ed25519keys and signatures for Solanaabsent
base58, bech32, keccak, ripemdturn a key into an addressabsent
SHA-256the hash by which the app names its wallet to the serverpresent
Searched by name across all 1,779 decompiled files. The BIP-39 library survives only as “read a sentence” and “check its checksum”; the hashing libraries only as SHA-256.

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.

226
accounts, all still funded, none deleted
226
first payments with two signatures
219
with destination tag 123456
220
with the same 48-drop fee (0.000048 XRP)
32,651 XRP
in those first payments
67,520 XRP
held by the accounts today

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.

XRPL accounts whose first XRP came from XRPayNet's app wallet
2022, from 12 July29 (12.8%)
202372 (31.9%)
2024114 (50.4%)
2025, to 11 August11 (4.9%)
Each account's first transaction is a payment from r3P1bYdy…LQaw. 226 accounts, 2022-07-12 to 2025-08-11.

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

Also found
In the code
A chat-room webhook, token included
A complete Discord webhook address is a plain string in the app. Crashes are posted to it as 'Exception', 'Stack Trace', 'User ID' and 'User'. Anyone who reads the string can post to the same room, and crash text from a wallet can carry fragments of requests.
In the shipped files
A secret in the shipped .env
assets/flutter_assets/.env ships WIREDASH_PROJECT_ID and WIREDASH_SECRET for the in-app feedback service. A value inside an app is public.
In the manifest
Contacts, overlay and advertising IDs
READ_CONTACTS and WRITE_CONTACTS, SYSTEM_ALERT_WINDOW, the advertising-ID permissions and Firebase Analytics, in a finance app. Write access to the address book is hard to explain for a wallet.
In the manifest
App Check debug provider shipped
The debug attestation provider is registered in the release build beside Play Integrity. It does nothing unless a debug token is enabled, so this is a footnote, not a hole.
Done right
In the code
The local copy is encrypted
The phone's copy of the phrase sits in the phone's encrypted store (Android Keystore) and android:allowBackup is false, so it cannot be copied out through a device backup. Good hygiene for a copy that, given everything above, is not the one that matters.
Seen on a phone
Google Play is enforced
On a phone that did not install the app from Google Play it shows 'Something went wrong' and quits: a licence check runs before the first screen and Firebase App Check is enforced. A sideloaded copy run with every connection logged opened 497 connections in two minutes, 454 of them to Google's licence-check service and the rest to connectivity checks, and none to XRPayNet. Real anti-tamper, and the reason no request body here comes from the wire.
Store listing
The build is Google Play's own
The package carries Google Play's source stamp and the app's signing certificate; both hashes are in the sources below, so the same file can be pulled and decompiled again.

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?

Do we recommend Volta Wallet?
Not while the phrase is made and used on the server.
Why not
  • 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
What would fix it
  • 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
If you use it today
  • 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
The middle column is what XRPayNet would have to ship; the right-hand one is what to do meanwhile.

Sources

  • Volta Wallet 5.0.5.prod (build 377), com.xrpaynet.app, arm64, the Google Play build: source-stamp certificate SHA-256 3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d, signing certificate 47fa031f292a7de6d752e1c8ec962d030b4cec4a4072e3420ab5084ffa87c315, base.apk e61f18d14cc9f3951b6d35a23c2b6fc48678d9fb07e9a371271b795a25da8f35, split bundle 04c7fc21f2b7a6df55c6b51c76d51cb7fd098b7c5746e650460e0990f1e38d38. 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.