Custody, Defined
A wallet is custodial if someone else can move the funds. That is the whole test. Not "does it have a nice recovery flow" but: can any party other than you sign a transfer?
An app that stores your private key on its server is custodial even if it never touches the key. An app whose keys live on your device is non-custodial even if it is ugly. The distinction is where the key sleeps.
Four Checks
1. Contract code is verified
For on-chain wallets (multisigs, vaults, smart wallets), the contract bytecode must match public source code. Compare it directly:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://rpc.ankr.com/eth");
const onChain = await provider.getCode(walletContractAddress);
const verified = await fetch(`https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${walletContractAddress}&apikey=${API_KEY}`)
.then((r) => r.json());
if (onChain !== ethers.zeroPadValue("0x" + verified.result[0].bytecode.replace(/^0x/, ""), 32)) {
console.log("bytecode does not match published source");
}
If the bytecode differs from the verified source, the deployed contract is not what you read. Stop there.
2. Keys never leave the device
Non-custodial wallets sign locally. You can test this: put the phone in airplane mode and sign a message. If signing works offline, the key is local. If it fails or requires a server, the key is elsewhere.
3. Sign and verify
Sign a message, then recover the signer. The recovered address must be the address you control. See the full walkthrough in my post on keys, seeds, and derivation paths.
4. Withdraw to a fresh address
Send a small amount to a wallet you created from a phrase you wrote down yourself. Then send it out. If the app can do this without your signature, custody has left the building.
What the Redact Build Did, and Did Not, Get Right
Redact, the privacy app I built for a Monad hackathon, applied these checks to itself. Keys are generated and stored client-side, the contract is verified, and signing happens in the browser. Duress mode lives in the client too, which means a forced phone unlock shows a decoy balance rather than a real one.
The deposit flow did not complete. A submission-blocking SDK version mismatch between the relay package and the deployed contract ABI surfaced on the last day, and fixing it meant re-deploying the contract with a changed interface, which we could not do in the remaining hours. The submission documents this rather than claiming a clean end-to-end run.
That failure is normal engineering. The checks above are not about which projects shipped cleanly. They are about whether the software you are about to trust can prove where it keeps your keys.
The Rule
A wallet you cannot verify is a wallet you do not control. Run the four checks on any new app before moving meaningful amounts. They take an afternoon and they are the difference between using a product and being used by one.