This is the canonical reference for putting a self-custodial wallet inside a mobile or web app on YouthChain. Everything here is implemented and shipped in the YouthApp React Native client — the file references at the end of each section point at that working code.
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

These hosts are DNS names under chain.youthapp.io, so a server move only changes DNS and the URLs stay the same. Avoid the older *.98-87-222-81.sslip.io URLs: they encode the server IP and break when the server moves, which already broke every published URL once.

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.
On React Native, import react-native-get-random-values before any crypto library, or generateMnemonic() throws for lack of a CSPRNG.
Display: addresses go on screen in EIP-55 mixed-case checksum form so a user can spot a mistyped address. Lowercase is equally valid on the wire — checksum is a display concern only. Reference implementation: src/lib/ycWallet.ts — deriveAccount, newMnemonic, isValidMnemonic, toChecksumAddress.

2. Key storage

Three rules, in order of importance:
  1. The phrase goes in the platform secure store. react-native-encrypted-storage maps to the iOS Keychain and Android EncryptedSharedPreferences. Never AsyncStorage, never plain files, never your backend.
  2. 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.
  3. Cache the address, not the key. The address is public and needed on nearly every render; reading it should not touch key material.
Keying by 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.
A wallet screen that shows a phrase the app then discards is worse than no wallet: the user believes they hold a key that opens nothing. If your app previously displayed a phrase without deriving from it, treat every one of those accounts as custodial and migrate them explicitly — do not silently re-point the balance.

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:
Broadcast payload — RLP of nine items with the signature, where v = recovery + chainId * 2 + 35 (so v is 17469 or 17470 on chain 7717):
Integers are RLP-encoded big-endian with no leading zeroes, and zero is the empty string — getting this wrong produces a signature that recovers to the wrong sender and the node rejects the transaction with no useful error. Broadcast the resulting hex with eth_sendRawTransaction.
Calldata whose first byte is 0x01–0x07 is read as an engagement action. The node mirrors each block’s engagement into the on-chain oracle; its decoder first checks for the recordAction selector 0x8ab2a273, and failing that treats data[0] as an action byte — 0x01 post, 0x02 like, 0x03 share, 0x04 comment, 0x05 watch, 0x06 nft_create, 0x07 tip (crates/yc-types/src/engagement.rs:120,170).A contract call whose function selector happens to start in that range (e.g. 0x06fdde03) is therefore also recorded as engagement. Keep this in mind when you send arbitrary calldata, and do not rely on it as an API — a magic prefix or a dedicated to address is the planned tightening.
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:
The gasless claim (POST /api/mobile/claim) is a testnet-only path. The whitepaper-compliant path is YCRRewards.claim(uint8) signed by the user’s own wallet. Mainnet will require the wallet-signed path — build the signed version now and keep the relay behind a flag, or you will be rewriting the claim screen under deadline.

Swap, end to end

Both transactions are signed on the device and broadcast with 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

No endpoint on this network accepts a user’s private key. Older app builds sent a privateKey field to POST /api/mobile/engagement; the indexer ignores it and relays through its own oracle wallet. If you find that field in an old client, delete it — a wallet that transmits its key is not a wallet.
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-values imported 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.