Self-custodial means the server never sees a key. The user’s recovery phrase stays on
the device, the private key is re-derived only when a transaction has to be signed, and
the node accepts nothing but a properly signed transaction. Any flow that asks a user to
POST a private key is a simulator flow, not a wallet flow — see What never goes to a
server.
Network facts
1. Key derivation
YouthChain uses the standard Ethereum derivation path, so a phrase created in your app can be restored in MetaMask (and vice versa) with no translation layer.src/lib/ycWallet.ts — deriveAccount, newMnemonic,
isValidMnemonic, toChecksumAddress.
2. Key storage
Three rules, in order of importance:- The phrase goes in the platform secure store.
react-native-encrypted-storagemaps to the iOS Keychain and Android EncryptedSharedPreferences. NeverAsyncStorage, never plain files, never your backend. - The private key is never persisted. Store the phrase; re-derive the key each time you need to sign, and drop it. Callers must not hold a reference.
- Cache the address, not the key. The address is public and needed on nearly every render; reading it should not touch key material.
userId matters: one device can host more than one account, and signing out must
not hand the next user the previous user’s wallet.
Reference implementation: src/lib/ycWallet.ts — saveWallet, getAddress,
getMnemonic, getAccount, forgetWallet.
Backup and recovery
The recovery phrase is the only way back into the account. Your app must ship, before it holds any real value:- a backup screen that reveals the phrase behind a device-auth prompt (biometric or PIN);
- a restore flow that accepts a phrase, validates it with BIP-39 checksum
(
validateMnemonic), and derives the same address; - a clear statement that losing the phrase loses the funds — there is no server-side reset.
3. Signing transactions
The node accepts legacy EIP-155 transactions (decode_legacy_tx, crates/yc-node/src/rpc.rs).
No EIP-1559 typed envelope is required on the signing path.
Signing payload — RLP of nine items, the last three being chainId, 0, 0:
v = recovery + chainId * 2 + 35 (so v is 17469 or 17470 on chain 7717):
eth_sendRawTransaction.
Reference implementation: src/lib/ycTx.ts — rlpEncode, toMinimalBytes,
signTransaction. Verified against Hardhat and official EIP-155 vectors (12 unit tests).
Gas parameters that work today
Always read the nonce with
eth_getTransactionCount(address, "pending") immediately before
signing; two signed transactions sharing a nonce means the second is dropped silently.
4. Reading balances
Pending YCR is not an ERC-20 balance — it is accrued inside
YCRRewards and only
becomes a token balance after a claim. Show the two separately, or users will report the
“balance” as wrong.
Reference implementation: src/lib/ycChain.ts — getNativeBalance, getTokenBalance,
getRewards.
5. Which paths are gasless, and which are not
This is the design decision most integrations get wrong, so it is stated plainly:Swap, end to end
eth_sendRawTransaction;
poll eth_getTransactionReceipt for each before advancing the UI.
Reference implementation: src/lib/ycChain.ts — swapYcrForYce, quoteYcrForYce,
getAllowance; selectors in src/lib/ycTx.ts.
6. What never goes to a server
For a real user, the only things that leave the device are:- the public address;
- signed transaction bytes;
- non-sensitive device metadata (OS, model, app version, battery) for the fleet view.
7. Integration checklist
- Phrase generated with a real CSPRNG (
react-native-get-random-valuesimported first) - Derivation on
m/44'/60'/0'/0/0, address verified against MetaMask for the same phrase - Phrase in Keychain / EncryptedSharedPreferences, keyed per user
- Private key never written to disk, never logged, never sent over the network
- Backup screen behind device auth + restore flow validating the BIP-39 checksum
- Sign-out clears the wallet for that user only
- Nonce read as
"pending"immediately before each signature - Signed transactions verified against a known EIP-155 vector in CI
- Pending YCR and ERC-20 YCR shown as distinct numbers
- RPC/API host read from config, not hardcoded in the binary
Reference implementation
The YouthApp React Native client implements all of the above:
Two invariants that code holds and yours should too: an engagement call is never awaited
by a user action, and no chain error ever propagates into a social interaction. A user
with no wallet simply earns nothing — the like still works.