The XRPH Wallet Hack: 4,011 Wallets Emptied, and Every Key Was Guessable
On the evening of 3 September 2026 an address that did not exist an hour earlier received the entire balance of 4,011 wallets. Three hours later the money was on a bridge to Ethereum. We read the whole thing off the ledger, then pulled the wallet app from Google Play and took it apart.
In one screen
- All 4,011 wallets belong to XRPH Wallet users. We checked every one on the ledger: 98.6% carry the app's own fingerprint, 95.5% were created in it, and none ever used other wallet software.
- The sweep started at 22:07 UTC and ran biggest wallet first. By 22:30 the collector held 242,536 XRP.
- 311,613 XRP went into NEAR Intents, came out on Ethereum as 178.46 ETH within a minute of each deposit, and became 445,198 DAI at one address that has not moved since.
- XRP Healthcare confirmed the count and the DAI figure. We followed every step to that address ourselves.
- We took the app apart. Payments are signed on the phone; the seed is stored unencrypted on the phone; and the staking feature sends the seed to XRP Healthcare's server. That staking code still ships.
- The reason every wallet could be emptied is in how the app makes keys. It feeds the key generator a line of text instead of raw randomness, and draws the digits from Math.random(). Fourteen digits end up deciding a wallet, about 2^46 possibilities, small enough to rebuild every key offline with no server and no phone. XRP Healthcare's developers published the same finding on 7 September.
- 1,225 wallets staked, nearly all between December 2023 and July 2024. 1,198 of them were drained, and so was the company's own staking wallet, whose key only the server holds. Yet seven in ten victims never staked: the exposure is the app's whole user base, not one feature.
- Asked on X, XRP Healthcare first said the wallet "is designed as a non-custodial wallet", then that it "was not aware that any part of the wallet's staking functionality was less than fully non-custodial" and is putting the question to the developers who wrote it.
- Our verdict: no. The keys of every wallet it made are already weak and rebuildable, so existing wallets cannot be made safe; they must be remade with a corrected app. What would change that is listed near the end.
Minute by minute
Nothing here required an exploit of the XRP Ledger. Each wallet signed an ordinary payment of its own balance. The ledger shows what the keys did, not how the attacker got them, and XRP Healthcare says that part is still under investigation. What the app itself does with those keys is further down.
Biggest wallets first
10,281 payments from 4,011 wallets: nearly all inside three hours, the last stragglers 27 hours later, 4,256 of them token transfers. Forty-five of the emptied accounts were then deleted, which sends the reserve to the collector as well.
Who was hit
The dump
The attacker did not wait to cash out. The first XRPH swap came 31 minutes after the first sweep, while wallets were still being emptied. XRPHAI went the same way: 2.43 million taken from 199 wallets and sold with five offers for 1,272 XRP in total. The market for it was that thin.
Where the money went
The collector kept 4.5 XRP. The wallet that created it, rhAtfg…VM4Y, dates from 2024, was funded by a withdrawal from the exchange VALR, and created nothing else. The two addresses the XRP passed through on its way out were not the attacker's: NEAR Intents issues a fresh XRPL address to each depositor, credits the deposit the moment it lands, and sweeps the address into its own wallet with a key of its own. That is why each was created by the attacker's payment and emptied nine seconds later.
Inside the app
The build in both stores on 3 September was 8.0.15: on Google Play since 28 July, on the App Store since 30 July, 50,000+ installs on Android, listed as a non-custodial wallet. We installed it from Google Play, decompiled its 1.3 million lines of JavaScript, and ran it on a rooted emulator with every encrypted connection opened, using a throwaway wallet funded with real XRP and XRPH.
{ amount, address, secret: seed, stakeType } to XRP Healthcare's server, and unstaking sends the seed again. 1,225 wallets used it, nearly all between December 2023 and July 2024. The screen now says: "Due to the scarcity of XRPH tokens, staking is no longer available. Existing stakers may continue until their current period ends." We had nothing staked, so this path never fired in our session. The code ships in the current build, and unstaking still uses it.Math.random(), joins them into a line of text, and passes that to Wallet.fromEntropy(), which expects raw bytes; the text is read as single-digit bytes and all but the first sixteen characters are dropped. Fourteen digits are left, so every wallet the app ever made is one of about 2^46 (72,899,838,000,090) and can be rebuilt offline. This is the defect that empties the wallets, set out in "Two paths to a weak key".Where does the seed actually leave the phone for the operator? In the staking feature. Staking, unstaking and claiming rewards each read the seed straight off the stored account and post it as secret to XRP Healthcare's worker. The panel shows the stake and unstake code as it comes out of the decompiler; the table lists the staking calls and one more.
// Staking screen (bundle_decompiled.js 836356-836377, then 836426)
r7['amount'] = r8;
r9 = r10.classicAddress; r7['address'] = r9;
r9 = r10.seed; r7['secret'] = r9; // the wallet seed
r9 = _closure2_slot3; r7['stakeType'] = r9;
r8 = r7.bind(r8)(r6); // useStake().mutateAsync(body)
// useStake (660203-660210)
r3 = r1.client;
r2['method'] = 'POST';
r2['data'] = a0; // the body above, seed included
r1 = 'wallet/stake';
r1 = r3.bind(r4)(r1, r2); // client('wallet/stake', ...)
// Unstake sheet (837292-837305) and useWithdrawStake (660304-660310)
r9 = r10.seed; r7['secret'] = r9; r4['secret'] = r7; r4['id'] = r8.unStakeId;
r1 = 'wallet/withdrawStake/' + id; r1['method'] = 'POST'; r1['data'] = r6.secret;
// API client base (472706-472708)
r1 = 'https://xrphealthcare.sptdmw.workers.dev';// stake
const body = { amount, address: account.classicAddress, secret: account.seed, stakeType };
client('wallet/stake', { method: 'POST', data: body });
// unstake or claim
client('wallet/withdrawStake/' + unStakeId, { method: 'POST', data: { secret: account.seed } });secret to the worker; claiming rewards uses the same withdrawStake call. The ordinary send does the opposite: it passes the seed to a local signing routine (Wallet.fromSeed, then submit to a node) and only the signed transaction leaves the phone, which is what both our live sends showed. Line numbers refer to our decompiled bundle; the APK hash in the sources lets anyone reproduce them.| Handler | Seed in the body as | Bundle line | Status |
|---|---|---|---|
| Stake | secret: seed | 836356 | Used |
| Unstake / withdraw | secret: { secret: seed } | 837278 | Used |
| Claim rewards | secret: { secret: seed } | 859505 | In the code |
| Swap approval (socket) | walletKey: seed | 1020472 | In the code |
// staking sends the seed to the worker as "secret"
stake client('wallet/stake', { amount, address, secret: seed, stakeType })
unstake client('wallet/withdrawStake/' + id, { secret: { secret: seed }, id })
claim client('wallet/withdrawStake/' + id, { secret: { secret: seed }, id })
// worker base: https://xrphealthcare.sptdmw.workers.dev
// the swap-approval screen emits the seed as walletKey over a socket (swap is disabled)
swap approval socket.emit('swap-response', { walletKey: seed, ... })
// the ordinary send does the OPPOSITE — it signs on the device, the seed never leaves:
send Wallet.fromSeed(seed).sign(tx); submitAndWait(signed)secret to the worker (stake, and unstake and claim via withdrawStake); the swap-approval screen emits it as walletKey over a socket, though the in-app swap is disabled. The ordinary send is not here: it signs on the device with Wallet.fromSeed and submits only the signed transaction, so the seed stays put, which is what our live sends showed. Staking is confirmed used from the ledger; the swap path sits in the code. Line numbers refer to our decompiled bundle.The one other place the seed is put on the wire is the swap-approval screen, which emits it as walletKey over a socket; the in-app swap still reads "coming soon", so that path sits in the code unused. The ordinary send does the opposite: both the plain send and the password-gated send hand the seed to a local signing routine (Wallet.fromSeed, then submit to a node), and only the signed transaction leaves, which is why our live sends showed no seed on the wire; the payment-review step only writes the details into the app's own state. So the seed reaches the operator through the staking feature, and would through the dormant swap screen, not through sending. This is separate from the key defect below, which is what actually emptied the wallets, but every wallet that staked also handed the operator its seed.
Two paths to a weak key
This is the part that empties a wallet without touching it. When the app makes a wallet it never uses the phone's secure random source. It builds the "padlock" the user is shown, eight blocks of six digits, and turns that into the wallet's secret key. Two separate mistakes in how it does so shrink the key from a number no one could ever guess to one a normal computer can list in full.
// The digit source — generateRandomNumber (bundle_decompiled.js 364276)
r0 = Math.random(); // <-- ordinary RNG, not the phone's secure one
r1 = r0 * 899999;
r0 = r1 + 100000; // one 6-digit block: 100000..999998
return r0;
// createNewPadlock (364394): eight blocks -> one space-joined string
padlock = { A, B, C, D, E, F, G, H }; // each = 6 digits from the line above
r6 = A.join('') + ' ' + B.join('') + ' ' + C.join('') + ' ' + D.join('')
+ ' ' + E.join('') + ' ' + F.join('') + ' ' + G.join('') + ' ' + H.join('');
r5 = Wallet.fromEntropy;
r3 = r5.bind(Wallet)(r6); // a 55-char STRING is passed; fromEntropy wants 16 bytes
// generateNewWallet (654121, recovery path) — the same call
r2 = Wallet.fromEntropy;
r5 = r2.bind(Wallet)(a1);// what xrpl + ripple-keypairs then do with that string
entropy = Uint8Array.from("528875 921568 528239 ...") // '5'->5 '2'->2 ' '->0 (bytes 0-9)
entropy = entropy.slice(0, 16) // keep 16 chars, drop the other 39
// 14 digits are left to decide the wallet:
// block A (6) + space(0) + block B (6) + space(0) + first 2 of block C
// blocks D..H — 30 of the 48 random digits — never touch the key
//
// 899,999 x 899,999 x 90 = 72,899,838,000,090 (about 2^46)Math.random(), not a secure generator, and they are handed to Wallet.fromEntropy() as text, so the library reads them as single-digit bytes and keeps only the first sixteen characters. Line numbers refer to our decompiled bundle; the APK hash in the sources lets anyone reproduce them.Path one, the wrong type. The eight blocks are joined into a single line of text and handed to Wallet.fromEntropy(), a function that expects sixteen raw bytes, not text. The line is read one character at a time, so every byte ends up holding a value of only 0 to 9 instead of the full 0 to 255 a real key byte carries, and only the first sixteen characters are kept. Thirty of the forty-eight random digits never reach the key at all, and two of the sixteen survivors are spaces, which count as zero. Fourteen digits decide the entire wallet.
Path two, the wrong source. Even those fourteen digits come from Math.random(), the everyday generator a program uses to shuffle a list or drive an animation, not the operating system's secure one. It was never built to make a key, and on its own it can only make the real number of reachable wallets smaller, never larger.
Put together, the number of possible wallets falls from 2^128, a figure with thirty-nine digits that no one could search in many lifetimes, to about 2^46: 72,899,838,000,090, roughly seventy-three trillion. That is small enough to list every possible key on an ordinary computer and check each one against funded accounts on the ledger. Rebuilding a key needs nothing from the owner and nothing from any server. It fits every part of what the ledger showed on 3 September: the whole user base rather than one feature, each withdrawal signed with the account's own key, and one automated sweep.
XRP Healthcare's own developers reached the same finding and published it on 7 September, tracing the fault to a change made on 13 June 2023 and reproducing it against their code: three padlocks that agree only in their first fourteen digits produce one identical wallet address. Their root-cause report is the company's first explanation of why the wallets could be emptied. The same code sits in the build on Google Play today.
Staking was live for eight months
The "staking is no longer available" message is not a leftover. From December 2023 to July 2024 XRP Healthcare ran staking through the app, at 25% a year for a twelve-month lock and 20% for six, and because staking moves tokens, the ledger holds every stake. They all went to one wallet, r9ejnH…vK5b, created by the company's treasury the week before launch.
New stakes stopped in July 2024. The seed did not stop leaving: unstaking and rollovers kept posting it until at least 30 November 2025, and the code is in the build in the stores today. The stake request names no token, only an amount, an address, the seed and a lock type, so a new staking round, for XRPH or for XRPHAI, the second token the company launched in January 2025 and promotes inside the app for "reward boosts", would send every new staker's seed to the server again without a line changing.
Every hacked wallet, checked
Of the 4,011 wallets that paid into the collector, one was the attacker's own, funding it with 15.4 XRP; the other 4,010 are the victims, and together they lost 267,664 XRP. We read the full history of each from a full-history node, accepted it only once its first transaction sat within a day of the day the wallet was created, and looked for the marks the XRPH Wallet leaves on the ledger.
1,199 wallets (29.9%) · 148,397 XRP (55.4%)
2,755 wallets (68.7%) · 119,256 XRP (44.6%)
56 wallets (1.4%) · 12 XRP (0.0%)
| wallet | created | the app's trustline | drained, 3 Sep 2026 |
|---|---|---|---|
| rehrNN…fyJL | 22 Apr 2025 | A11E26AE74… | 5,745.5 XRP |
| rNLyFo…3TXG | 13 Dec 2023 | 98E68C769D… | 5,070.1 XRP |
| rJRTkj…z4sq | 22 Mar 2026 | 80DE99CE20… | 3,371.2 XRP |
Download all 4,010 addresses (CSV) — each with the XRP swept from it, the number of token transfers, and whether it staked or was created in the app. Sorted by amount; the columns are the checks above.
- The staking wallet went the same way at 23:45 that night, by the same script: a check cancelled to free its reserve, then its 0.78 XRPH and 1.79 XRP into the collector. No phone ever held that key. The server that pays stakers does, and the attacker used it.
- Stakers are three in ten victims and held more than half the XRP. The other seven in ten are the same kind of wallet in every other respect: born in the app, used nowhere else, and 1,204 of them never sent a payment in their life. Nothing in build 8.0.15 sends their seed anywhere.
- "Used nowhere else" is partly the format. The app never shows a seed or a recovery phrase; its only backup is a "padlock combination", eight six-digit secret numbers, which Xaman can import but most people never carry across. Either way, every one of these wallets was signed for by the XRPH Wallet alone.
What the app explains, and what it does not
- The seed is put on the wire to the operator by the staking feature: stake, unstake and claim all post it to the worker as the field "secret". The dormant swap-approval screen would send it too, as "walletKey" over a socket. The ordinary send does not: it signs on the device and keeps the seed. Staking is confirmed used from the ledger, so every one of the 1,225 stakers handed the operator their seed. This is a separate failure from the key defect that actually emptied the wallets.
- The reason it is the whole user base and not just stakers is the key defect, not the seed-to-server paths. Weak keys reach every wallet the app ever made, staker or not; the seed-to-server paths only reach wallets that used those features. That is why 98.6% of the drained wallets carry the app's fingerprint, none ever used another wallet, and seven in ten never staked, yet all were emptied in one sweep.
- The alternatives for that remainder are weak: theft from phones does not scale to 4,011 wallets in three hours, the app has no over-the-air update channel, and no new build reached either store between late July and the hack.
Asked on X on 5 September whether seed phrases are ever transmitted to or stored on its servers, XRP Healthcare answered: "XRPH is designed as a non-custodial wallet, with users retaining control of their wallet credentials and funds." The staking code sends the seed to their server. Asked a second time, more specifically, they replied later that day: "XRP Healthcare was not aware that any part of the wallet's staking functionality was less than fully non-custodial, and we would never knowingly market a product as non-custodial if that weren't the case. This is now being put directly to the developers responsible for the wallet's code." They add that they have filed a report with law enforcement and asked every exchange and swap service in the funds' path to flag and freeze. It is the first acknowledgement that the staking feature was not non-custodial.
Their root-cause report names the key defect but says nothing about the seed being sent to their server. These remain open:
- Which earlier builds sent the seed to the server for sending, creating, importing or syncing a wallet, and until when?
- Where were the seeds received for staking kept, and was the staking wallet's own key kept with them?
- Was that store breached from outside, or used by someone who had access?
Do we recommend it?
- Signs payments on the phone, and we watched it do so.
- Sends no wallet data to crash reporting or analytics.
- Every wallet it ever made can be rebuilt from public information: it passed the key generator a line of text and drew the digits from Math.random(), shrinking the space of possible keys from 2^128 to about 2^46. This is what emptied 4,011 wallets in one night.
- Its staking feature posted users’ seeds to the company’s server, and the server-held staking wallet was drained the same night.
- The seed sits unencrypted on the phone.
- An account system with a shared API key and a session token that never expires, behind a wallet sold as non-custodial.
- No certificate pinning.
- The staking code still ships and unstaking still uses it; a new staking round, for XRPH or XRPHAI, would send every staker’s seed to the server again unless the code changes.
- A corrected key generator, and every existing wallet migrated to a new key: the old keys are already weak and cannot be patched.
- No feature, in any build, that sends the seed off the phone.
- The seed in the phone’s secure key store, and sessions that expire.
- An independent audit of the rebuilt app, published.
Until then, anyone holding XRP or XRPH should use a wallet whose keys never leave the device, created with a new seed. That is the same advice for everyone, not only for people who were hit.
Why no alarm went off
A wallet emptying itself into a brand-new address is the most common thing on the ledger: it is what a user moving to a new wallet does. Every drain detector, ours included, therefore ignores a single sweep unless the destination is already known to be a drainer.
What the ledger showed on 3 September was different in one respect only: hundreds of unrelated wallets did it to the same address inside minutes. That shape has no innocent explanation.
The rule now runs live in our ledger feed. It needs 25 distinct wallets, funded from at least five different sources and created over a span of at least 90 days, emptying into one address under a week old within six hours. Exchanges consolidate into old, named wallets; a person moves one balance; a deposit address carries a tag. None of them trip it.
If you used XRPH Wallet
- Treat every key that was ever in the app as compromised. Move anything left to a wallet created with a new seed in different software.
- Do not import the old seed into a new app. The seed is the problem, not the app it lives in.
- If you ever staked in the app, your seed was posted to XRP Healthcare's server as part of that feature. One more reason the old seed cannot be kept.
- Ignore anyone offering recovery for a fee or a signature. The funds are on Ethereum; nobody can pull them back by having you sign something.
- Check any address before you pay it: our drain tracker and the scam list behind it are updated live.
Sources
- Collector rGGXaBdSRUfdarKDkt2csxL67F8MEGxHVB, hops raDNxjwbjGCUAix8rKCzrHYY2r9AsCjs1P and rEwVZRuLvAAo5PkkU7ZTRPJ7km4hX7o6J7, and every transaction linked above, read from a full-history XRPL node on 5 September 2026.
- Victim profile from our ledger index: the wallet that funded each of the 4,011 senders, its creation date and its trustlines. The downloadable list of all 4,010 victims carries the XRP swept from each, from the on-chain drain records.
- XRPH price from our 5-minute candles of the XRPH/XRP market.
- Wallet count and dollar figure: XRP Healthcare's statements on X, 4 September 2026. The Ethereum side we read ourselves, below; their DAI figure matches it.
- The app:
com.xrphwallet8.0.15 (build 815) installed from Google Play on 5 September 2026, base APK SHA-256 98ef9778d75a5a630816798c1458f25cb3715e266d5e91ef8c7fe5d6925e07ce, Hermes bytecode decompiled with hermes-dec. Run on a rooted Android 13 emulator with a mitmproxy certificate installed system-wide; every HTTP request and WebSocket frame logged in full and searched for the test wallet's seed, private key, entropy in hex and base64, secret-number groups and public key. - Test wallet rEtETFXqPDKzgkF7pVrbBN2vnM4fRw8JkS, funded from our own wallet and emptied by us afterwards: DEX buy AC28524C93…, payments 03F8C1E70B… (0.3 XRP) and 50B4D9EB8C… (100 XRPH), both captured leaving the phone already signed.
- Store listings read on 5 September 2026: Google Play (updated 28 July 2026, 50K+ downloads) and the App Store lookup service (8.0.15, released 30 July 2026, seller XRP Healthcare Limited).
- Staking: the full history of r9ejnHYRqpUcdcVpP9jrubpgMGBvV2vK5b (7,574 transactions) and of its treasury rQD582WZt9xx4SVKfBcpwRBYJhSpretDo7, read from a full-history node on 5 September 2026. A stake is an XRPH payment into that wallet; the treasury's own top-ups and two exchange wallets are excluded from the user counts.
- The wallet check: the full history of each of the 4,010 victims (the 4,011th payment into the collector is the attacker's own funding), read from a full-history node on 5 September 2026 and accepted only when its first transaction falls within a day of the creation date in our index, which 3,962 did at once; the rest were re-read until they did, and the 48 with no recorded creation date were read from two nodes with the longer answer kept. A partial answer from a load-balanced node looks complete and carries no error, which is why the check exists. Marks as defined in the anatomy figure.
- XRP Healthcare's posts on X, dated from their status ids: 13 Nov 2023, 22 Nov 2023, 30 Nov 2023 (with the APR replies), 3 Mar 2024, 2 Apr 2024, 13 Apr 2024 and 23 Jul 2024, which links the press release "XRP Healthcare Halts New Staking of XRPH Token to Preserve Finite Supply".
- Past the bridge: NEAR Intents' own transactions, read from an archival NEAR node and NearBlocks (the credits F4HeE2d6qt… and H1fcCZoizC…, each naming the XRPL payment it came from; the swaps and withdrawals An8z3wD7eb… and 5nBcH9S8Ri…, each naming the Ethereum address); Ethereum from Blockscout and a public node, balances read 5 September 2026 at 15:00 UTC: 0x3DC7BFf29Fc5a051aAB250516BaBD74ae7068930 holds 445,197.999216 DAI and 0.4527 ETH.
- Promotion by other accounts, for the record: 21 Dec 2022, 18 Jan 2023, 24 Jan 2024, 14 Apr 2024, 14 Jan 2025 and 30 Aug 2026.